#include "autoconfig.h" #ifdef RCL_MONITOR /* Copyright (C) 2006-2021 J.F.Dockes * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ /* The code for the Win32 version of the monitor was largely copied from efsw: * https://github.com/SpartanJ/efsw * LICENSE for the original WIN32 code: * Copyright (c) 2020 Martín Lucas Golini * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * This software is a fork of the "simplefilewatcher" by James Wynn (james@jameswynn.com) * http://code.google.com/p/simplefilewatcher/ also MIT licensed. */ #include "autoconfig.h" #include #include #include #include "safeunistd.h" #include "log.h" #include "rclmon.h" #include "rclinit.h" #include "fstreewalk.h" #include "pathut.h" /** * Recoll real time monitor event receiver. This file has code to interface * to FAM, inotify, etc. and place events on the event queue. */ /** Virtual interface for the actual filesystem monitoring module. */ class RclMonitor { public: RclMonitor() {} virtual ~RclMonitor() {} virtual bool addWatch(const string& path, bool isDir) = 0; virtual bool getEvent(RclMonEvent& ev, int msecs = -1) = 0; virtual bool ok() const = 0; // Does this monitor generate 'exist' events at startup? virtual bool generatesExist() const { return false; } virtual bool isRecursive() const { return false; } // Save significant errno after monitor calls int saved_errno{0}; }; // Monitor factory. We only have one compiled-in kind at a time, no // need for a 'kind' parameter static RclMonitor *makeMonitor(); /** * Create directory watches during the initial file system tree walk. * * This class is a callback for the file system tree walker * class. The callback method alternatively creates the directory * watches and flushes the event queue (to avoid a possible overflow * while we create the watches) */ class WalkCB : public FsTreeWalkerCB { public: WalkCB(RclConfig *conf, RclMonitor *mon, RclMonEventQueue *queue, FsTreeWalker& walker) : m_config(conf), m_mon(mon), m_queue(queue), m_walker(walker) {} virtual ~WalkCB() {} virtual FsTreeWalker::Status processone( const string &fn, const struct PathStat *st, FsTreeWalker::CbFlag flg) { MONDEB("walkerCB: processone " << fn << " m_mon " << m_mon << " m_mon->ok " << (m_mon ? m_mon->ok() : false) << "\n"); if (flg == FsTreeWalker::FtwDirEnter || flg == FsTreeWalker::FtwDirReturn) { m_config->setKeyDir(fn); // Set up skipped patterns for this subtree. m_walker.setSkippedNames(m_config->getSkippedNames()); } if (flg == FsTreeWalker::FtwDirEnter) { // Create watch when entering directory, but first empty // whatever events we may already have on queue while (m_queue->ok() && m_mon->ok()) { RclMonEvent ev; if (m_mon->getEvent(ev, 0)) { if (ev.m_etyp != RclMonEvent::RCLEVT_NONE) m_queue->pushEvent(ev); } else { MONDEB("walkerCB: no event pending\n"); break; } } if (!m_mon || !m_mon->ok()) return FsTreeWalker::FtwError; // We do nothing special if addWatch fails for a reasonable reason if (!m_mon->isRecursive() && !m_mon->addWatch(fn, true)) { if (m_mon->saved_errno != EACCES && m_mon->saved_errno != ENOENT) { LOGINF("walkerCB: addWatch failed\n"); return FsTreeWalker::FtwError; } } } else if (!m_mon->generatesExist() && flg == FsTreeWalker::FtwRegular) { // Have to synthetize events for regular files existence // at startup because the monitor does not do it // Note 2011-09-29: no sure this is actually needed. We just ran // an incremental indexing pass (before starting the // monitor). Why go over the files once more ? The only // reason I can see would be to catch modifications that // happen between the incremental and the start of // monitoring ? There should be another way: maybe start // monitoring without actually handling events (just // queue), then run incremental then start handling // events ? ** But we also have to do it on a directory // move! So keep it ** We could probably skip it on the initial run though. RclMonEvent ev; ev.m_path = fn; ev.m_etyp = RclMonEvent::RCLEVT_MODIFY; m_queue->pushEvent(ev); } return FsTreeWalker::FtwOk; } private: RclConfig *m_config; RclMonitor *m_mon; RclMonEventQueue *m_queue; FsTreeWalker& m_walker; }; static bool rclMonAddTopWatches( FsTreeWalker& walker, RclConfig& lconfig, RclMonitor *mon, RclMonEventQueue *queue) { // Get top directories from config. Special monitor sublist if // set, else full list. vector tdl = lconfig.getTopdirs(true); if (tdl.empty()) { LOGERR("rclMonRcvRun:: top directory list (topdirs param.) not found " "in configuration or topdirs list parse error"); queue->setTerminate(); return false; } // Walk the directory trees to add watches WalkCB walkcb(&lconfig, mon, queue, walker); for (const auto& dir : tdl) { lconfig.setKeyDir(dir); // Adjust the follow symlinks options bool follow; if (lconfig.getConfParam("followLinks", &follow) && follow) { walker.setOpts(FsTreeWalker::FtwFollow); } else { walker.setOpts(FsTreeWalker::FtwOptNone); } if (path_isdir(dir, follow)) { LOGDEB("rclMonRcvRun: walking " << dir << "\n"); // If the fs watcher is recursive, we add the watches for the topdirs here, and walk the // tree just for generating initial events. if (mon->isRecursive() && !mon->addWatch(dir, true)) { if (mon->saved_errno != EACCES && mon->saved_errno != ENOENT) { LOGERR("rclMonAddTopWatches: addWatch failed for [" << dir << "]\n"); return false; } } if (walker.walk(dir, walkcb) != FsTreeWalker::FtwOk) { LOGERR("rclMonRcvRun: tree walk failed\n"); return false; } if (walker.getErrCnt() > 0) { LOGINFO("rclMonRcvRun: fs walker errors: " << walker.getReason() << "\n"); } } else { // We have to special-case regular files which are part of the topdirs list because the // tree walker only adds watches for directories if (!mon->addWatch(dir, false)) { LOGSYSERR("rclMonRcvRun", "addWatch", dir); } } } bool doweb = false; lconfig.getConfParam("processwebqueue", &doweb); if (doweb) { string webqueuedir = lconfig.getWebQueueDir(); if (!mon->addWatch(webqueuedir, true)) { LOGERR("rclMonRcvRun: addwatch (webqueuedir) failed\n"); if (mon->saved_errno != EACCES && mon->saved_errno != ENOENT) return false; } } return true; } static bool rclMonAddSubWatches( const std::string& path, FsTreeWalker& walker, RclConfig& lconfig, RclMonitor *mon, RclMonEventQueue *queue) { WalkCB walkcb(&lconfig, mon, queue, walker); if (walker.walk(path, walkcb) != FsTreeWalker::FtwOk) { LOGERR("rclMonRcvRun: walking new dir " << path << " : " << walker.getReason() << "\n"); return false; } if (walker.getErrCnt() > 0) { LOGINFO("rclMonRcvRun: fs walker errors: " << walker.getReason() << "\n"); } return true; } // Don't push events for skipped files. This would get filtered on the processing side // anyway, but causes unnecessary wakeups and messages. Do not test skippedPaths here, // this would be incorrect (because a topdir can be under a skippedPath and this was // handled while adding the watches). Also we let the other side process onlyNames. static bool rclMonShouldSkip(const std::string& path, RclConfig& lconfig, FsTreeWalker& walker) { lconfig.setKeyDir(path_getfather(path)); walker.setSkippedNames(lconfig.getSkippedNames()); if (walker.inSkippedNames(path_getsimple(path))) return true; return false; } // Main thread routine: create watches, then forever wait for and queue events void *rclMonRcvRun(void *q) { RclMonEventQueue *queue = (RclMonEventQueue *)q; LOGDEB("rclMonRcvRun: running\n"); recoll_threadinit(); // Make a local copy of the configuration as it doesn't like // concurrent accesses. It's ok to copy it here as the other // thread will not work before we have sent events. RclConfig lconfig(*queue->getConfig()); // Create the fam/whatever interface object RclMonitor *mon; if ((mon = makeMonitor()) == 0) { LOGERR("rclMonRcvRun: makeMonitor failed\n"); queue->setTerminate(); return 0; } FsTreeWalker walker; walker.setSkippedPaths(lconfig.getDaemSkippedPaths()); if (!rclMonAddTopWatches(walker, lconfig, mon, queue)) { LOGERR("rclMonRcvRun: addtopwatches failed\n"); goto terminate; } // Forever wait for monitoring events and add them to queue: MONDEB("rclMonRcvRun: waiting for events. q->ok(): " << queue->ok() << "\n"); while (queue->ok() && mon->ok()) { RclMonEvent ev; // Note: I could find no way to get the select call to return when a signal is delivered to // the process (it goes to the main thread, from which I tried to close or write to the // select fd, with no effect). So set a timeout so that an intr will be detected if (mon->getEvent(ev, 2000)) { if (rclMonShouldSkip(ev.m_path, lconfig, walker)) continue; if (ev.m_etyp == RclMonEvent::RCLEVT_DIRCREATE) { // Recursive addwatch: there may already be stuff inside this directory. E.g.: files // were quickly created, or this is actually the target of a directory move. This is // necessary for inotify, but it seems that fam/gamin is doing the job for us so // that we are generating double events here (no big deal as prc will sort/merge). LOGDEB("rclMonRcvRun: walking new dir " << ev.m_path << "\n"); if (!rclMonAddSubWatches(ev.m_path, walker, lconfig, mon, queue)) { goto terminate; } } if (ev.m_etyp != RclMonEvent::RCLEVT_NONE) queue->pushEvent(ev); } } terminate: queue->setTerminate(); LOGINFO("rclMonRcvRun: monrcv thread routine returning\n"); return 0; } // Utility routine used by both the fam/gamin and inotify versions to get // rid of the id-path translation for a moved dir bool eraseWatchSubTree(map& idtopath, const string& top) { bool found = false; MONDEB("Clearing map for [" << top << "]\n"); map::iterator it = idtopath.begin(); while (it != idtopath.end()) { if (it->second.find(top) == 0) { found = true; it = idtopath.erase(it); } else { it++; } } return found; } // We dont compile both the inotify and the fam interface and inotify // has preference #ifndef RCL_USE_INOTIFY #ifdef RCL_USE_FAM ////////////////////////////////////////////////////////////////////////// /** Fam/gamin -based monitor class */ #include #include #include #include /** FAM based monitor class. We have to keep a record of FAM watch request numbers to directory names as the event only contain the request number and file name, not the full path */ class RclFAM : public RclMonitor { public: RclFAM(); virtual ~RclFAM(); virtual bool addWatch(const string& path, bool isdir); virtual bool getEvent(RclMonEvent& ev, int msecs = -1); bool ok() const {return m_ok;} virtual bool generatesExist() const {return true;} private: bool m_ok; FAMConnection m_conn; void close() { FAMClose(&m_conn); m_ok = false; } map m_idtopath; const char *event_name(int code); }; // Translate event code to string (debug) const char *RclFAM::event_name(int code) { static const char *famevent[] = { "", "FAMChanged", "FAMDeleted", "FAMStartExecuting", "FAMStopExecuting", "FAMCreated", "FAMMoved", "FAMAcknowledge", "FAMExists", "FAMEndExist" }; static char unknown_event[30]; if (code < FAMChanged || code > FAMEndExist) { sprintf(unknown_event, "unknown (%d)", code); return unknown_event; } return famevent[code]; } RclFAM::RclFAM() : m_ok(false) { if (FAMOpen2(&m_conn, "Recoll")) { LOGERR("RclFAM::RclFAM: FAMOpen2 failed, errno " << errno << "\n"); return; } m_ok = true; } RclFAM::~RclFAM() { if (ok()) FAMClose(&m_conn); } static jmp_buf jbuf; static void onalrm(int sig) { longjmp(jbuf, 1); } bool RclFAM::addWatch(const string& path, bool isdir) { if (!ok()) return false; bool ret = false; MONDEB("RclFAM::addWatch: adding " << path << "\n"); // It happens that the following call block forever. // We'd like to be able to at least terminate on a signal here, but // gamin forever retries its write call on EINTR, so it's not even useful // to unblock signals. SIGALRM is not used by the main thread, so at least // ensure that we exit after gamin gets stuck. if (setjmp(jbuf)) { LOGERR("RclFAM::addWatch: timeout talking to FAM\n"); return false; } signal(SIGALRM, onalrm); alarm(20); FAMRequest req; if (isdir) { if (FAMMonitorDirectory(&m_conn, path.c_str(), &req, 0) != 0) { LOGERR("RclFAM::addWatch: FAMMonitorDirectory failed\n"); goto out; } } else { if (FAMMonitorFile(&m_conn, path.c_str(), &req, 0) != 0) { LOGERR("RclFAM::addWatch: FAMMonitorFile failed\n"); goto out; } } m_idtopath[req.reqnum] = path; ret = true; out: alarm(0); return ret; } // Note: return false only for queue empty or error // Return EVT_NONE for bad event to keep queue processing going bool RclFAM::getEvent(RclMonEvent& ev, int msecs) { if (!ok()) return false; MONDEB("RclFAM::getEvent:\n"); fd_set readfds; int fam_fd = FAMCONNECTION_GETFD(&m_conn); FD_ZERO(&readfds); FD_SET(fam_fd, &readfds); MONDEB("RclFAM::getEvent: select. fam_fd is " << fam_fd << "\n"); // Fam / gamin is sometimes a bit slow to send events. Always add // a little timeout, because if we fail to retrieve enough events, // we risk deadlocking in addwatch() if (msecs == 0) msecs = 2; struct timeval timeout; if (msecs >= 0) { timeout.tv_sec = msecs / 1000; timeout.tv_usec = (msecs % 1000) * 1000; } int ret; if ((ret=select(fam_fd+1, &readfds, 0, 0, msecs >= 0 ? &timeout : 0)) < 0) { LOGERR("RclFAM::getEvent: select failed, errno " << errno << "\n"); close(); return false; } else if (ret == 0) { // timeout MONDEB("RclFAM::getEvent: select timeout\n"); return false; } MONDEB("RclFAM::getEvent: select returned " << ret << "\n"); if (!FD_ISSET(fam_fd, &readfds)) return false; // ?? 2011/03/15 gamin v0.1.10. There is initially a single null // byte on the connection so the first select always succeeds. If // we then call FAMNextEvent we stall. Using FAMPending works // around the issue, but we did not need this in the past and this // is most weird. if (FAMPending(&m_conn) <= 0) { MONDEB("RclFAM::getEvent: FAMPending says no events\n"); return false; } MONDEB("RclFAM::getEvent: call FAMNextEvent\n"); FAMEvent fe; if (FAMNextEvent(&m_conn, &fe) < 0) { LOGERR("RclFAM::getEvent: FAMNextEvent: errno " << errno << "\n"); close(); return false; } MONDEB("RclFAM::getEvent: FAMNextEvent returned\n"); map::const_iterator it; if ((!path_isabsolute(fe.filename)) && (it = m_idtopath.find(fe.fr.reqnum)) != m_idtopath.end()) { ev.m_path = path_cat(it->second, fe.filename); } else { ev.m_path = fe.filename; } MONDEB("RclFAM::getEvent: " << event_name(fe.code) < " " << ev.m_path << "\n"); switch (fe.code) { case FAMCreated: if (path_isdir(ev.m_path)) { ev.m_etyp = RclMonEvent::RCLEVT_DIRCREATE; break; } /* FALLTHROUGH */ case FAMChanged: case FAMExists: // Let the other side sort out the status of this file vs the db ev.m_etyp = RclMonEvent::RCLEVT_MODIFY; break; case FAMMoved: case FAMDeleted: ev.m_etyp = RclMonEvent::RCLEVT_DELETE; // We would like to signal a directory here to enable cleaning // the subtree (on a dir move), but can't test the actual file // which is gone, and fam doesn't tell us if it's a dir or reg. // Let's rely on the fact that a directory should be watched if (eraseWatchSubTree(m_idtopath, ev.m_path)) ev.m_etyp |= RclMonEvent::RCLEVT_ISDIR; break; case FAMStartExecuting: case FAMStopExecuting: case FAMAcknowledge: case FAMEndExist: default: // Have to return something, this is different from an empty queue, // esp if we are trying to empty it... if (fe.code != FAMEndExist) LOGDEB("RclFAM::getEvent: got other event " << fe.code << "!\n"); ev.m_etyp = RclMonEvent::RCLEVT_NONE; break; } return true; } #endif // RCL_USE_FAM #endif // ! INOTIFY #ifdef RCL_USE_INOTIFY ////////////////////////////////////////////////////////////////////////// /** Inotify-based monitor class */ #include #include class RclIntf : public RclMonitor { public: RclIntf() : m_ok(false), m_fd(-1), m_evp(0), m_ep(0) { if ((m_fd = inotify_init()) < 0) { LOGERR("RclIntf:: inotify_init failed, errno " << errno << "\n"); return; } m_ok = true; } virtual ~RclIntf() { close(); } virtual bool addWatch(const string& path, bool isdir); virtual bool getEvent(RclMonEvent& ev, int msecs = -1); bool ok() const {return m_ok;} private: bool m_ok; int m_fd; map m_idtopath; // Watch descriptor to name #define EVBUFSIZE (32*1024) char m_evbuf[EVBUFSIZE]; // Event buffer char *m_evp; // Pointer to next event or 0 char *m_ep; // Pointer to end of events const char *event_name(int code); void close() { if (m_fd >= 0) { ::close(m_fd); m_fd = -1; } m_ok = false; } }; const char *RclIntf::event_name(int code) { code &= ~(IN_ISDIR|IN_ONESHOT); switch (code) { case IN_ACCESS: return "IN_ACCESS"; case IN_MODIFY: return "IN_MODIFY"; case IN_ATTRIB: return "IN_ATTRIB"; case IN_CLOSE_WRITE: return "IN_CLOSE_WRITE"; case IN_CLOSE_NOWRITE: return "IN_CLOSE_NOWRITE"; case IN_CLOSE: return "IN_CLOSE"; case IN_OPEN: return "IN_OPEN"; case IN_MOVED_FROM: return "IN_MOVED_FROM"; case IN_MOVED_TO: return "IN_MOVED_TO"; case IN_MOVE: return "IN_MOVE"; case IN_CREATE: return "IN_CREATE"; case IN_DELETE: return "IN_DELETE"; case IN_DELETE_SELF: return "IN_DELETE_SELF"; case IN_MOVE_SELF: return "IN_MOVE_SELF"; case IN_UNMOUNT: return "IN_UNMOUNT"; case IN_Q_OVERFLOW: return "IN_Q_OVERFLOW"; case IN_IGNORED: return "IN_IGNORED"; default: { static char msg[50]; sprintf(msg, "Unknown event 0x%x", code); return msg; } }; } bool RclIntf::addWatch(const string& path, bool) { if (!ok()) return false; MONDEB("RclIntf::addWatch: adding " << path << "\n"); // CLOSE_WRITE is covered through MODIFY. CREATE is needed for mkdirs uint32_t mask = IN_MODIFY | IN_CREATE | IN_MOVED_FROM | IN_MOVED_TO | IN_DELETE // IN_ATTRIB used to be not needed to receive extattr // modification events, which was a bit weird because only ctime is // set, and now it is... | IN_ATTRIB #ifdef IN_DONT_FOLLOW | IN_DONT_FOLLOW #endif #ifdef IN_EXCL_UNLINK | IN_EXCL_UNLINK #endif ; int wd; if ((wd = inotify_add_watch(m_fd, path.c_str(), mask)) < 0) { saved_errno = errno; LOGERR("RclIntf::addWatch: inotify_add_watch failed. errno " << saved_errno << "\n"); if (errno == ENOSPC) { LOGERR("RclIntf::addWatch: ENOSPC error may mean that you should " "increase the inotify kernel constants. See inotify(7)\n"); } return false; } m_idtopath[wd] = path; return true; } // Note: return false only for queue empty or error // Return EVT_NONE for bad event to keep queue processing going bool RclIntf::getEvent(RclMonEvent& ev, int msecs) { if (!ok()) return false; ev.m_etyp = RclMonEvent::RCLEVT_NONE; MONDEB("RclIntf::getEvent:\n"); if (m_evp == 0) { fd_set readfds; FD_ZERO(&readfds); FD_SET(m_fd, &readfds); struct timeval timeout; if (msecs >= 0) { timeout.tv_sec = msecs / 1000; timeout.tv_usec = (msecs % 1000) * 1000; } int ret; MONDEB("RclIntf::getEvent: select\n"); if ((ret = select(m_fd + 1, &readfds, 0, 0, msecs >= 0 ? &timeout : 0)) < 0) { LOGSYSERR("RclIntf::getEvent", "select", ""); close(); return false; } else if (ret == 0) { MONDEB("RclIntf::getEvent: select timeout\n"); // timeout return false; } MONDEB("RclIntf::getEvent: select returned\n"); if (!FD_ISSET(m_fd, &readfds)) return false; int rret; if ((rret=read(m_fd, m_evbuf, sizeof(m_evbuf))) <= 0) { LOGSYSERR("RclIntf::getEvent", "read", sizeof(m_evbuf)); close(); return false; } m_evp = m_evbuf; m_ep = m_evbuf + rret; } struct inotify_event *evp = (struct inotify_event *)m_evp; m_evp += sizeof(struct inotify_event); if (evp->len > 0) m_evp += evp->len; if (m_evp >= m_ep) m_evp = m_ep = 0; map::const_iterator it; if ((it = m_idtopath.find(evp->wd)) == m_idtopath.end()) { LOGERR("RclIntf::getEvent: unknown wd " << evp->wd << "\n"); return true; } ev.m_path = it->second; if (evp->len > 0) { ev.m_path = path_cat(ev.m_path, evp->name); } MONDEB("RclIntf::getEvent: " << event_name(evp->mask) << " " << ev.m_path << "\n"); if ((evp->mask & IN_MOVED_FROM) && (evp->mask & IN_ISDIR)) { // We get this when a directory is renamed. Erase the subtree // entries in the map. The subsequent MOVED_TO will recreate // them. This is probably not needed because the watches // actually still exist in the kernel, so that the wds // returned by future addwatches will be the old ones, and the // map will be updated in place. But still, this feels safer eraseWatchSubTree(m_idtopath, ev.m_path); } // IN_ATTRIB used to be not needed, but now it is if (evp->mask & (IN_MODIFY|IN_ATTRIB)) { ev.m_etyp = RclMonEvent::RCLEVT_MODIFY; } else if (evp->mask & (IN_DELETE | IN_MOVED_FROM)) { ev.m_etyp = RclMonEvent::RCLEVT_DELETE; if (evp->mask & IN_ISDIR) ev.m_etyp |= RclMonEvent::RCLEVT_ISDIR; } else if (evp->mask & (IN_CREATE | IN_MOVED_TO)) { if (evp->mask & IN_ISDIR) { ev.m_etyp = RclMonEvent::RCLEVT_DIRCREATE; } else { // We used to return null event because we would get a // modify event later, but it seems not to be the case any // more (10-2011). So generate MODIFY event ev.m_etyp = RclMonEvent::RCLEVT_MODIFY; } } else if (evp->mask & (IN_IGNORED)) { if (!m_idtopath.erase(evp->wd)) { LOGDEB0("Got IGNORE event for unknown watch\n"); } else { eraseWatchSubTree(m_idtopath, ev.m_path); } } else { LOGDEB("RclIntf::getEvent: unhandled event " << event_name(evp->mask) << " " << evp->mask << " " << ev.m_path << "\n"); return true; } return true; } #endif // RCL_USE_INOTIFY #ifdef _WIN32 /* * WIN32 VERSION NOTES: * * - When using non-recursive watches (one per dir), it appeared that * watching a subdirectory of a given directory prevented renaming * the top directory, Windows says: can't rename because open or a * file in it is open. This is mostly why we use recursive watches * on the topdirs only. */ #include #include #include #include #include "safewindows.h" typedef long WatchID; class WatcherWin32; class RclFSWatchWin32; enum class Action {Add = 1, Delete = 2, Modify = 3, Move = 4}; // Virtual interface for the monitor callback. Note: this for compatibility with the efsw code, as // rclmon uses a pull, not push interface. The callback pushes the events to a local queue from // which they are then pulled by the upper level code. class FileWatchListener { public: virtual ~FileWatchListener() {} virtual void handleFileAction(WatchID watchid, const std::string& dir, const std::string& fn, Action action, bool isdir, std::string oldfn = "" ) = 0; }; // Internal watch data. This piggy-back our actual data pointer to the MS overlapped pointer. This // is a bit of a hack, and we could probably use event Ids instead. struct WatcherStructWin32 { OVERLAPPED Overlapped; WatcherWin32 *Watch; }; // Actual data structure for one directory watch class WatcherWin32 { public: WatchID ID; FileWatchListener *Listener{nullptr}; bool Recursive; std::string DirName; std::string OldFileName; HANDLE DirHandle{nullptr}; // do NOT make this bigger than 64K because it will fail if the folder being watched is on the // network! (see http://msdn.microsoft.com/en-us/library/windows/desktop/aa365465(v=vs.85).aspx) BYTE Buffer[8 * 1024]; DWORD NotifyFilter{0}; bool StopNow{false}; RclFSWatchWin32 *Watch{nullptr}; }; // The efsw top level file system watcher: manages all the directory watches. class RclFSWatchWin32 { public: RclFSWatchWin32(); virtual ~RclFSWatchWin32(); // Add a directory watch // On error returns -1 WatchID addWatch(const std::string& directory, FileWatchListener *watcher, bool recursive); // 2nd stage of action processing (after the static handler which just reads the data) void handleAction(WatcherWin32 *watch, const std::string& fn, unsigned long action); bool ok() const { return mInitOK; } // Fetch events, with msecs timeout if there are no more void run(DWORD msecs); private: HANDLE mIOCP; // Using a vector because we don't remove watches. Change to list if needed. std::vector mWatches; bool mInitOK{false}; WatchID mLastWatchID{0}; std::mutex mWatchesLock; bool pathInWatches(const std::string& path); /// Remove all directory watches. void removeAllWatches(); }; // Adapter for the rclmon interface class RclMonitorWin32 : public RclMonitor, public FileWatchListener { public: virtual ~RclMonitorWin32() {} virtual bool addWatch(const string& path, bool /*isDir*/) override { MONDEB("RclMonitorWin32::addWatch: " << path << "\n"); return m_fswatcher.addWatch(path, this, true) != -1; } virtual bool getEvent(RclMonEvent& ev, int msecs = -1) { PRETEND_USE(msecs); if (!m_events.empty()) { ev = m_events.front(); m_events.pop(); return true; } m_fswatcher.run(msecs); if (!m_events.empty()) { ev = m_events.front(); m_events.pop(); return true; } return false; } virtual bool ok() const override { return m_fswatcher.ok(); } // Does this monitor generate 'exist' events at startup? virtual bool generatesExist() const override { return false; } // Can the caller avoid setting watches on subdirs ? virtual bool isRecursive() const override { return true; } virtual void handleFileAction(WatchID watchid, const std::string& dir, const std::string& fn, Action action, bool isdir, std::string oldfn = "") { MONDEB("RclMonitorWin32::handleFileAction: dir [" << dir << "] fn [" << fn << "] act " << int(action) << " isdir " << isdir << " oldfn [" << oldfn << "]\n"); RclMonEvent event; switch (action) { case Action::Move: case Action::Add: event.m_etyp = isdir ? RclMonEvent::RCLEVT_DIRCREATE : RclMonEvent::RCLEVT_MODIFY; break; case Action::Delete: event.m_etyp = RclMonEvent::RCLEVT_DELETE; if (isdir) { event.m_etyp |= RclMonEvent::RCLEVT_ISDIR; } break; case Action::Modify: event.m_etyp = RclMonEvent::RCLEVT_MODIFY; break; } event.m_path = path_cat(dir, fn); m_events.push(event); } // Save significant errno after monitor calls int saved_errno{0}; private: std::queue m_events; RclFSWatchWin32 m_fswatcher; }; /// Stops monitoring a directory. void DestroyWatch(WatcherStructWin32 *pWatch) { if (pWatch) { WatcherWin32 *ww32 = pWatch->Watch; ww32->StopNow = true; CancelIoEx(ww32->DirHandle, &pWatch->Overlapped); CloseHandle(ww32->DirHandle); delete ww32; // Shouldn't we call heapfree on the parameter here ?? } } /// Refreshes the directory monitoring. bool RefreshWatch(WatcherStructWin32 *pWatch) { WatcherWin32 *ww32 = pWatch->Watch; return ReadDirectoryChangesW( ww32->DirHandle, ww32->Buffer, sizeof(ww32->Buffer), ww32->Recursive, ww32->NotifyFilter, NULL, &pWatch->Overlapped, NULL ) != 0; } /// Starts monitoring a directory. WatcherStructWin32 *CreateWatch(LPCWSTR szDirectory, bool recursive, DWORD NotifyFilter, HANDLE iocp) { WatcherStructWin32 *wsw32; size_t ptrsize = sizeof(*wsw32); wsw32 =static_cast(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, ptrsize)); WatcherWin32 *ww32 = new WatcherWin32(); wsw32->Watch = ww32; ww32->DirHandle = CreateFileW( szDirectory, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL ); if (ww32->DirHandle != INVALID_HANDLE_VALUE && CreateIoCompletionPort(ww32->DirHandle, iocp, 0, 1)) { ww32->NotifyFilter = NotifyFilter; ww32->Recursive = recursive; if (RefreshWatch(wsw32)) { return wsw32; } } CloseHandle(ww32->DirHandle); delete ww32; HeapFree(GetProcessHeap(), 0, wsw32); return NULL; } RclFSWatchWin32::RclFSWatchWin32() : mLastWatchID(0) { mIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 1); if (mIOCP && mIOCP != INVALID_HANDLE_VALUE) mInitOK = true; } RclFSWatchWin32::~RclFSWatchWin32() { mInitOK = false; if (mIOCP && mIOCP != INVALID_HANDLE_VALUE) { PostQueuedCompletionStatus(mIOCP, 0, reinterpret_cast(this), NULL); } removeAllWatches(); CloseHandle(mIOCP); } WatchID RclFSWatchWin32::addWatch(const std::string& _dir,FileWatchListener *watcher,bool recursive) { LOGDEB("RclFSWatchWin32::addWatch: " << _dir << " recursive " << recursive << "\n"); std::string dir(_dir); path_slashize(dir); if (!path_isdir(dir)) { LOGDEB("RclFSWatchWin32::addWatch: not a directory: " << dir << "\n"); return 0; } if (!path_readable(dir)) { LOGINF("RclFSWatchWin32::addWatch: not readable: " << dir << "\n"); return 0; } path_catslash(dir); auto wdir = utf8towchar(dir); std::unique_lock lock(mWatchesLock); if (pathInWatches(dir)) { MONDEB("RclFSWatchWin32::addWatch: already in watches: " << dir << "\n"); return 0; } WatchID watchid = ++mLastWatchID; WatcherStructWin32 *watch = CreateWatch( wdir.get(), recursive, FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_SIZE, mIOCP ); if (nullptr == watch) { LOGINF("RclFSWatchWin32::addWatch: CreateWatch failed\n"); return -1; } // Add the handle to the handles vector watch->Watch->ID = watchid; watch->Watch->Watch = this; watch->Watch->Listener = watcher; watch->Watch->DirName = dir; mWatches.push_back(watch); return watchid; } void RclFSWatchWin32::removeAllWatches() { std::unique_lock lock(mWatchesLock); for( auto& watchp : mWatches) { DestroyWatch(watchp); } mWatches.clear(); } // Unpacks events and passes them to the event processor void CALLBACK WatchCallback(DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped) { if (dwNumberOfBytesTransfered == 0 || NULL == lpOverlapped) { return; } WatcherStructWin32 *wsw32 = (WatcherStructWin32*)lpOverlapped; WatcherWin32 *ww32 = wsw32->Watch; PFILE_NOTIFY_INFORMATION pNotify; size_t offset = 0; do { pNotify = (PFILE_NOTIFY_INFORMATION) &ww32->Buffer[offset]; offset += pNotify->NextEntryOffset; std::string sfn; wchartoutf8(pNotify->FileName, sfn, pNotify->FileNameLength / sizeof(WCHAR)); ww32->Watch->handleAction(ww32, sfn, pNotify->Action); } while (pNotify->NextEntryOffset != 0); if (!ww32->StopNow) { RefreshWatch(wsw32); } } void RclFSWatchWin32::run(DWORD msecs) { if (!mWatches.empty()) { DWORD numOfBytes = 0; OVERLAPPED* ov = NULL; ULONG_PTR compKey = 0; BOOL res = FALSE; DWORD ms = msecs == -1 ? INFINITE : msecs; while ((res = GetQueuedCompletionStatus(mIOCP, &numOfBytes, &compKey, &ov, ms))) { if (compKey != 0 && compKey == reinterpret_cast(this)) { // Called from ~RclFSWatchWin32. Must exit. MONDEB("RclFSWatchWin32::run: queuedcompletion said need exit\n"); return; } else { std::unique_lock lock(mWatchesLock); WatchCallback(numOfBytes, ov); } } } else { // No watches yet. MONDEB("RclFSWatchWin32::run: no watches yet\n"); DWORD ms = msecs == -1 ? 1000 : msecs; std::this_thread::sleep_for(std::chrono::milliseconds(ms)); } } void RclFSWatchWin32::handleAction(WatcherWin32 *watch, const std::string& _fn, unsigned long action) { std::string fn(_fn); Action fwAction; path_slashize(fn); MONDEB("handleAction: fn [" << fn << "] action " << action << "\n"); // In case fn is not a simple name but a relative path (probably // possible/common if recursive is set ?), sort out the directory // path and simple file name. std::string newpath = path_cat(watch->DirName, fn); bool isdir = path_isdir(newpath); std::string simplefn = path_getsimple(newpath); std::string folderPath = path_getfather(newpath); switch (action) { case FILE_ACTION_RENAMED_OLD_NAME: watch->OldFileName = fn; /* FALLTHROUGH */ case FILE_ACTION_REMOVED: fwAction = Action::Delete; // The system does not tell us if this was a directory, but we // need the info. Check if it was in the watches. // TBD: for a delete, we should delete all watches on the subtree ! path_catslash(newpath); for (auto& watchp : mWatches) { if (watchp->Watch->DirName == newpath) { isdir = true; break; } } break; case FILE_ACTION_ADDED: fwAction = Action::Add; break; case FILE_ACTION_MODIFIED: fwAction = Action::Modify; break; case FILE_ACTION_RENAMED_NEW_NAME: { fwAction = Action::Move; // If this is a directory, possibly update the watches. TBD: this seems wrong because we // should process the whole subtree ? Also probably not needed at all because we are // recursive and only set watches on the top directories. if (isdir) { // Update the new directory path std::string oldpath = path_cat(watch->DirName, watch->OldFileName); path_catslash(oldpath); for (auto& watchp : mWatches) { if (watchp->Watch->DirName == oldpath) { watchp->Watch->DirName = newpath; break; } } } std::string oldFolderPath = watch->DirName + watch->OldFileName.substr(0, watch->OldFileName.find_last_of("/\\")); if (folderPath == oldFolderPath) { watch->Listener->handleFileAction(watch->ID, folderPath, simplefn, fwAction, isdir, path_getsimple(watch->OldFileName)); } else { // Calling the client with non-simple paths?? watch->Listener->handleFileAction(watch->ID, watch->DirName, fn, fwAction, isdir, watch->OldFileName); } return; } default: return; }; watch->Listener->handleFileAction(watch->ID, folderPath, simplefn, fwAction, isdir); } bool RclFSWatchWin32::pathInWatches(const std::string& path) { for (const auto& wsw32 : mWatches) { if (wsw32->Watch->DirName == path ) { return true; } } return false; } #endif // _WIN32 /////////////////////////////////////////////////////////////////////// // The monitor 'factory' static RclMonitor *makeMonitor() { #ifdef _WIN32 return new RclMonitorWin32; #else # ifdef RCL_USE_INOTIFY return new RclIntf; # elif defined(RCL_USE_FAM) return new RclFAM; # endif #endif LOGINFO("RclMonitor: neither Inotify nor Fam was compiled as file system " "change notification interface\n"); return 0; } #endif // RCL_MONITOR