|
| 1 | + |
| 2 | +#include "InotifyFileWatcher.hpp" |
| 3 | + |
| 4 | +#include <errno.h> |
| 5 | +#include <limits.h> |
| 6 | +#include <poll.h> |
| 7 | +#include <string.h> |
| 8 | +#include <sys/inotify.h> |
| 9 | +#include <unistd.h> |
| 10 | + |
| 11 | +#ifdef DEBUG |
| 12 | +#include <iostream> |
| 13 | +#endif |
| 14 | + |
| 15 | +namespace { |
| 16 | + |
| 17 | +// IN_CREATE/IN_DELETE/IN_MOVED_*: children appearing, disappearing, or being |
| 18 | +// renamed within the watched directory. |
| 19 | +// IN_MODIFY/IN_CLOSE_WRITE: content changes to children. |
| 20 | +// IN_DELETE_SELF/IN_MOVE_SELF: the watched directory itself goes away. |
| 21 | +constexpr uint32_t kWatchMask = IN_CREATE | IN_DELETE | IN_DELETE_SELF | |
| 22 | + IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO | |
| 23 | + IN_MODIFY | IN_CLOSE_WRITE; |
| 24 | + |
| 25 | +constexpr size_t kEventBufLen = |
| 26 | + 64 * (sizeof(struct inotify_event) + NAME_MAX + 1); |
| 27 | + |
| 28 | +} // namespace |
| 29 | + |
| 30 | +InotifyFileWatcher::InotifyFileWatcher() { |
| 31 | + inotifyFd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK); |
| 32 | + if (inotifyFd == -1) { |
| 33 | + isValid = false; |
| 34 | + return; |
| 35 | + } |
| 36 | + |
| 37 | + // A pipe lets the destructor unblock poll() cleanly without signals. |
| 38 | + if (pipe(wakeupPipe) == -1) { |
| 39 | + close(inotifyFd); |
| 40 | + inotifyFd = -1; |
| 41 | + isValid = false; |
| 42 | + return; |
| 43 | + } |
| 44 | + |
| 45 | + eventThread = std::thread(&InotifyFileWatcher::eventLoop, this); |
| 46 | +} |
| 47 | + |
| 48 | +InotifyFileWatcher::~InotifyFileWatcher() { |
| 49 | + isValid = false; |
| 50 | + stopping = true; |
| 51 | + |
| 52 | + // Unblock the event loop thread. |
| 53 | + char byte = 0; |
| 54 | + write(wakeupPipe[1], &byte, 1); |
| 55 | + |
| 56 | + if (eventThread.joinable()) |
| 57 | + eventThread.join(); |
| 58 | + |
| 59 | + close(wakeupPipe[0]); |
| 60 | + close(wakeupPipe[1]); |
| 61 | + // Closing the inotify fd automatically removes all of its watches. |
| 62 | + if (inotifyFd >= 0) |
| 63 | + close(inotifyFd); |
| 64 | +} |
| 65 | + |
| 66 | +efsw::WatchID InotifyFileWatcher::addWatch(const std::string &path, |
| 67 | + efsw::FileWatchListener *listener, |
| 68 | + bool /* _useRecursion */) { |
| 69 | +#ifdef DEBUG |
| 70 | + std::cout << "InotifyFileWatcher::addWatch: " << path << std::endl; |
| 71 | +#endif |
| 72 | + if (!isValid) |
| 73 | + return efsw::Errors::WatcherFailed; |
| 74 | + |
| 75 | + std::string dir = path; |
| 76 | + if (dir.empty() || dir.back() != '/') |
| 77 | + dir += '/'; |
| 78 | + |
| 79 | + int wd = inotify_add_watch(inotifyFd, dir.c_str(), kWatchMask); |
| 80 | + if (wd < 0) { |
| 81 | + switch (errno) { |
| 82 | + case ENOENT: |
| 83 | + return efsw::Errors::FileNotFound; |
| 84 | + case EACCES: |
| 85 | + return efsw::Errors::FileNotReadable; |
| 86 | + default: |
| 87 | + return efsw::Errors::WatcherFailed; |
| 88 | + } |
| 89 | + } |
| 90 | + |
| 91 | + std::lock_guard<std::mutex> lock(mapMutex); |
| 92 | + efsw::WatchID handle = nextHandleID++; |
| 93 | + handlesToWatches[handle] = {dir, listener, wd}; |
| 94 | + wdToHandles.insert({wd, handle}); |
| 95 | + return handle; |
| 96 | +} |
| 97 | + |
| 98 | +void InotifyFileWatcher::removeWatch(efsw::WatchID handle) { |
| 99 | + int wd = -1; |
| 100 | + bool wasLastHandleForWd = false; |
| 101 | + { |
| 102 | + std::lock_guard<std::mutex> lock(mapMutex); |
| 103 | + auto it = handlesToWatches.find(handle); |
| 104 | + if (it == handlesToWatches.end()) |
| 105 | + return; |
| 106 | + wd = it->second.wd; |
| 107 | + handlesToWatches.erase(it); |
| 108 | + |
| 109 | + auto range = wdToHandles.equal_range(wd); |
| 110 | + for (auto wit = range.first; wit != range.second; ++wit) { |
| 111 | + if (wit->second == handle) { |
| 112 | + wdToHandles.erase(wit); |
| 113 | + break; |
| 114 | + } |
| 115 | + } |
| 116 | + wasLastHandleForWd = (wdToHandles.find(wd) == wdToHandles.end()); |
| 117 | + } |
| 118 | + |
| 119 | + if (wasLastHandleForWd) { |
| 120 | + // EINVAL here just means the kernel already removed this watch (e.g. the |
| 121 | + // directory was deleted); safe to ignore. |
| 122 | + inotify_rm_watch(inotifyFd, wd); |
| 123 | + } |
| 124 | +} |
| 125 | + |
| 126 | +void InotifyFileWatcher::forgetWatch(int wd) { |
| 127 | + std::lock_guard<std::mutex> lock(mapMutex); |
| 128 | + auto range = wdToHandles.equal_range(wd); |
| 129 | + for (auto it = range.first; it != range.second;) { |
| 130 | + handlesToWatches.erase(it->second); |
| 131 | + it = wdToHandles.erase(it); |
| 132 | + } |
| 133 | +} |
| 134 | + |
| 135 | +void InotifyFileWatcher::sendFileAction(int wd, const std::string &filename, |
| 136 | + efsw::Action action, |
| 137 | + const std::string &oldFilename) { |
| 138 | + // Copy out (handle, dir, listener) under the lock, then call into listener |
| 139 | + // code without holding it. |
| 140 | + std::vector<std::tuple<efsw::WatchID, std::string, efsw::FileWatchListener *>> |
| 141 | + targets; |
| 142 | + { |
| 143 | + std::lock_guard<std::mutex> lock(mapMutex); |
| 144 | + auto range = wdToHandles.equal_range(wd); |
| 145 | + for (auto it = range.first; it != range.second; ++it) { |
| 146 | + auto hit = handlesToWatches.find(it->second); |
| 147 | + if (hit != handlesToWatches.end()) |
| 148 | + targets.emplace_back(it->second, hit->second.dir, hit->second.listener); |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + for (auto &[handle, dir, listener] : targets) |
| 153 | + listener->handleFileAction(handle, dir, filename, action, oldFilename); |
| 154 | +} |
| 155 | + |
| 156 | +void InotifyFileWatcher::eventLoop() { |
| 157 | + char buf[kEventBufLen]; |
| 158 | + |
| 159 | + // State for pairing IN_MOVED_FROM with a following IN_MOVED_TO that shares |
| 160 | + // its cookie and watch — i.e. a rename within the same directory. |
| 161 | + bool hasPendingMove = false; |
| 162 | + int pendingWd = -1; |
| 163 | + uint32_t pendingCookie = 0; |
| 164 | + std::string pendingFilename; |
| 165 | + |
| 166 | + auto flushPendingMove = [&]() { |
| 167 | + if (!hasPendingMove) |
| 168 | + return; |
| 169 | + // No matching IN_MOVED_TO arrived: the file was moved somewhere we're not |
| 170 | + // watching (or out of the filesystem entirely). |
| 171 | + sendFileAction(pendingWd, pendingFilename, efsw::Actions::Delete); |
| 172 | + hasPendingMove = false; |
| 173 | + }; |
| 174 | + |
| 175 | + while (!stopping) { |
| 176 | + struct pollfd fds[2]; |
| 177 | + fds[0] = {inotifyFd, POLLIN, 0}; |
| 178 | + fds[1] = {wakeupPipe[0], POLLIN, 0}; |
| 179 | + |
| 180 | + // If we're sitting on an unpaired IN_MOVED_FROM, don't block forever — |
| 181 | + // give its IN_MOVED_TO a brief window to show up, then resolve it. |
| 182 | + int timeoutMs = hasPendingMove ? 10 : -1; |
| 183 | + |
| 184 | + int r = poll(fds, 2, timeoutMs); |
| 185 | + if (r < 0) { |
| 186 | + if (errno == EINTR) |
| 187 | + continue; |
| 188 | + break; |
| 189 | + } |
| 190 | + |
| 191 | + if (stopping || (fds[1].revents & POLLIN)) |
| 192 | + break; |
| 193 | + |
| 194 | + if (r == 0) { |
| 195 | + flushPendingMove(); |
| 196 | + continue; |
| 197 | + } |
| 198 | + |
| 199 | + if (!(fds[0].revents & POLLIN)) |
| 200 | + continue; |
| 201 | + |
| 202 | + ssize_t len = read(inotifyFd, buf, sizeof(buf)); |
| 203 | + if (len <= 0) |
| 204 | + continue; |
| 205 | + |
| 206 | + ssize_t i = 0; |
| 207 | + while (i < len) { |
| 208 | + auto *event = reinterpret_cast<struct inotify_event *>(&buf[i]); |
| 209 | + i += sizeof(struct inotify_event) + event->len; |
| 210 | + |
| 211 | + if (event->mask & IN_Q_OVERFLOW) |
| 212 | + continue; |
| 213 | + |
| 214 | + std::string filename = event->len > 0 ? std::string(event->name) : ""; |
| 215 | + |
| 216 | + if (hasPendingMove) { |
| 217 | + if ((event->mask & IN_MOVED_TO) && event->wd == pendingWd && |
| 218 | + event->cookie == pendingCookie) { |
| 219 | + // Same-directory rename — almost always an atomic save. |
| 220 | + sendFileAction(event->wd, filename, efsw::Actions::Moved, |
| 221 | + pendingFilename); |
| 222 | + hasPendingMove = false; |
| 223 | + continue; |
| 224 | + } |
| 225 | + flushPendingMove(); |
| 226 | + // fall through and process this event normally |
| 227 | + } |
| 228 | + |
| 229 | + if (event->mask & IN_IGNORED) { |
| 230 | + // The kernel already removed this watch (explicit removeWatch, |
| 231 | + // directory deleted, or filesystem unmounted). |
| 232 | + forgetWatch(event->wd); |
| 233 | + continue; |
| 234 | + } |
| 235 | + |
| 236 | + if (event->mask & IN_MOVED_FROM) { |
| 237 | + hasPendingMove = true; |
| 238 | + pendingWd = event->wd; |
| 239 | + pendingCookie = event->cookie; |
| 240 | + pendingFilename = filename; |
| 241 | + continue; |
| 242 | + } |
| 243 | + |
| 244 | + if (event->mask & (IN_DELETE_SELF | IN_MOVE_SELF)) { |
| 245 | + // The watched directory itself is gone or has moved. Report it as a |
| 246 | + // deletion of the directory; the caller can re-addWatch if it cares |
| 247 | + // to keep following it (we have no way to learn its new path). |
| 248 | + sendFileAction(event->wd, "", efsw::Actions::Delete); |
| 249 | + continue; |
| 250 | + } |
| 251 | + |
| 252 | + if (event->mask & IN_CREATE) { |
| 253 | + sendFileAction(event->wd, filename, efsw::Actions::Add); |
| 254 | + } else if (event->mask & IN_DELETE) { |
| 255 | + sendFileAction(event->wd, filename, efsw::Actions::Delete); |
| 256 | + } else if (event->mask & IN_MOVED_TO) { |
| 257 | + // Arrived from outside this directory (or its IN_MOVED_FROM partner |
| 258 | + // already timed out): treat it as a brand-new file. |
| 259 | + sendFileAction(event->wd, filename, efsw::Actions::Add); |
| 260 | + sendFileAction(event->wd, filename, efsw::Actions::Modified); |
| 261 | + } else if (event->mask & (IN_MODIFY | IN_CLOSE_WRITE)) { |
| 262 | + sendFileAction(event->wd, filename, efsw::Actions::Modified); |
| 263 | + } |
| 264 | + } |
| 265 | + } |
| 266 | + |
| 267 | + flushPendingMove(); |
| 268 | +} |
0 commit comments