diff --git a/README.md b/README.md index 0fe3934..5401b92 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Events | yes | yes | no | no Signals | yes² | yes² | no | no Files | yes | yes | no | no UI Integration | no | no | no | no -File watcher | no | no | no | no +File watcher | yes² | yes² | no | no ² Currently only supported on Linux diff --git a/source/eventcore/driver.d b/source/eventcore/driver.d index d4afb02..c8dd1f3 100644 --- a/source/eventcore/driver.d +++ b/source/eventcore/driver.d @@ -182,7 +182,7 @@ interface EventDriverTimers { bool isPending(TimerID timer); bool isPeriodic(TimerID timer); void wait(TimerID timer, TimerCallback callback); - void cancelWait(TimerID timer, TimerCallback callback); + void cancelWait(TimerID timer); /** Increments the reference count of the given resource. */ @@ -198,9 +198,7 @@ interface EventDriverTimers { interface EventDriverWatchers { @safe: /*@nogc:*/ nothrow: - WatcherID watchDirectory(string path, bool recursive); - void wait(WatcherID watcher, FileChangesCallback callback); - void cancelWait(WatcherID watcher); + WatcherID watchDirectory(string path, bool recursive, FileChangesCallback callback); /** Increments the reference count of the given resource. */ @@ -211,7 +209,7 @@ interface EventDriverWatchers { Once the reference count reaches zero, all associated resources will be freed and the resource descriptor gets invalidated. */ - void releaseRef(WatcherID descriptor); + bool releaseRef(WatcherID descriptor); } @@ -224,7 +222,7 @@ alias FileIOCallback = void delegate(FileFD, IOStatus, size_t); alias EventCallback = void delegate(EventID); alias SignalCallback = void delegate(SignalListenID, SignalStatus, int); alias TimerCallback = void delegate(TimerID); -alias FileChangesCallback = void delegate(WatcherID, in FileChange[] changes); +alias FileChangesCallback = void delegate(WatcherID, in ref FileChange change); @system alias DataInitializer = void function(void*); enum ExitReason { @@ -304,10 +302,16 @@ enum SignalStatus { */ struct FileChange { /// The type of change - FileChangeKind type; + FileChangeKind kind; - /// Path of the file/directory that was changed - string path; + /// Directory containing the changed file + string directory; + + /// Determines if the changed entity is a file or a directory. + bool isDirectory; + + /// Name of the changed file + const(char)[] name; } struct Handle(string NAME, T, T invalid_value = T.init) { diff --git a/source/eventcore/drivers/posix.d b/source/eventcore/drivers/posix.d index 54bfd97..07cf509 100644 --- a/source/eventcore/drivers/posix.d +++ b/source/eventcore/drivers/posix.d @@ -52,7 +52,8 @@ final class PosixEventDriver(Loop : PosixEventLoop) : EventDriver { version (linux) alias DNSDriver = EventDriverDNS_GAIA!(EventsDriver, SignalsDriver); else alias DNSDriver = EventDriverDNS_GAI!(EventsDriver, SignalsDriver); alias FileDriver = ThreadedFileEventDriver!EventsDriver; - alias WatcherDriver = PosixEventDriverWatchers!Loop; + version (linux) alias WatcherDriver = InotifyEventDriverWatchers!Loop; + else alias WatcherDriver = PosixEventDriverWatchers!Loop; Loop m_loop; CoreDriver m_core; @@ -1149,6 +1150,122 @@ final class PosixEventDriverSignals(Loop : PosixEventLoop) : EventDriverSignals } } +final class InotifyEventDriverWatchers(Loop : PosixEventLoop) : EventDriverWatchers +{ + import core.sys.posix.fcntl, core.sys.posix.unistd, core.sys.linux.sys.inotify; + import std.file; + + private { + Loop m_loop; + string[int][int] m_watches; // TODO: use a @nogc (allocator based) map + } + + this(Loop loop) { m_loop = loop; } + + final override WatcherID watchDirectory(string path, bool recursive, FileChangesCallback callback) + { + enum IN_NONBLOCK = 0x800; // value in core.sys.linux.sys.inotify is incorrect + auto handle = () @trusted { return inotify_init1(IN_NONBLOCK); } (); + if (handle == -1) return WatcherID.invalid; + + addWatch(handle, path); + if (recursive) { + try { + if (path.isDir) () @trusted { + foreach (de; path.dirEntries(SpanMode.shallow)) + if (de.isDir) addWatch(handle, de.name); + } (); + } catch (Exception e) { + // TODO: decide if this should be ignored or if the error should be forwarded + } + } + + m_loop.initFD(FD(handle)); + m_loop.registerFD(FD(handle), EventMask.read); + m_loop.setNotifyCallback!(EventType.read)(FD(handle), &onChanges); + m_loop.m_fds[handle].readCallback = () @trusted { return cast(IOCallback)callback; } (); + + processEvents(WatcherID(handle)); + + return WatcherID(handle); + } + + final override void addRef(WatcherID descriptor) + { + assert(m_loop.m_fds[descriptor].refCount > 0, "Adding reference to unreferenced event FD."); + m_loop.m_fds[descriptor].refCount++; + } + + final override bool releaseRef(WatcherID descriptor) + { + FD fd = cast(FD)descriptor; + assert(m_loop.m_fds[fd].refCount > 0, "Releasing reference to unreferenced event FD."); + if (--m_loop.m_fds[fd].refCount == 0) { + m_loop.unregisterFD(fd); + m_loop.clearFD(fd); + m_watches.remove(fd); + /*errnoEnforce(*/close(fd)/* == 0)*/; + return false; + } + + return true; + } + + private void onChanges(FD fd) + { + processEvents(cast(WatcherID)fd); + } + + private void processEvents(WatcherID id) + { + import core.stdc.stdio : FILENAME_MAX; + import core.stdc.string : strlen; + + ubyte[inotify_event.sizeof + FILENAME_MAX + 1] buf = void; + while (true) { + auto ret = () @trusted { return read(id, &buf[0], buf.length); } (); + + if (ret == -1 && errno == EAGAIN) + break; + assert(ret <= buf.length); + + auto rem = buf[0 .. ret]; + while (rem.length > 0) { + auto ev = () @trusted { return cast(inotify_event*)rem.ptr; } (); + FileChange ch; + if (ev.mask & (IN_CREATE|IN_MOVED_TO)) + ch.kind = FileChangeKind.added; + else if (ev.mask & (IN_DELETE|IN_DELETE_SELF|IN_MOVE_SELF|IN_MOVED_FROM)) + ch.kind = FileChangeKind.removed; + else if (ev.mask & IN_MODIFY) + ch.kind = FileChangeKind.modified; + + auto name = () @trusted { return ev.name.ptr[0 .. strlen(ev.name.ptr)]; } (); + ch.directory = m_watches[id][ev.wd]; + ch.isDirectory = (ev.mask & IN_ISDIR) != 0; + ch.name = name; + addRef(id); + auto cb = () @trusted { return cast(FileChangesCallback)m_loop.m_fds[id].readCallback; } (); + cb(id, ch); + if (!releaseRef(id)) break; + + rem = rem[inotify_event.sizeof + ev.len .. $]; + } + } + } + + private bool addWatch(int handle, string path) + { + import std.string : toStringz; + enum EVENTS = IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | + IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO; + immutable wd = () @trusted { return inotify_add_watch(handle, path.toStringz, EVENTS); } (); + if (wd == -1) return false; + m_watches[cast(int)handle][wd] = path; + return true; + } +} + final class PosixEventDriverWatchers(Loop : PosixEventLoop) : EventDriverWatchers { @safe: /*@nogc:*/ nothrow: private Loop m_loop; diff --git a/tests/0-dirwatcher.d b/tests/0-dirwatcher.d new file mode 100644 index 0000000..482a9d4 --- /dev/null +++ b/tests/0-dirwatcher.d @@ -0,0 +1,66 @@ +/++ dub.sdl: + name "test" + dependency "eventcore" path=".." ++/ +module test; + +import eventcore.core; +import std.stdio : File, writefln; +import std.file : exists, remove; +import core.time : msecs; + +bool s_done; +int s_cnt = 0; + +enum testFilename = "test.dat"; + +void main() +{ + if (exists(testFilename)) + remove(testFilename); + + auto id = eventDriver.watchers.watchDirectory(".", false, (id, ref change) { + switch (s_cnt++) { + default: assert(false); + case 0: + assert(change.kind == FileChangeKind.added); + assert(change.directory == "."); + assert(change.name == testFilename); + break; + case 1: + assert(change.kind == FileChangeKind.modified); + assert(change.directory == "."); + assert(change.name == testFilename); + break; + case 2: + assert(change.kind == FileChangeKind.removed); + assert(change.directory == "."); + assert(change.name == testFilename); + s_done = true; + eventDriver.core.exit(); + break; + } + }); + + auto fil = File(testFilename, "wt"); + + auto tm = eventDriver.timers.create(); + eventDriver.timers.set(tm, 100.msecs); + eventDriver.timers.wait(tm, (tm) { + scope (failure) assert(false); + fil.write("test"); + fil.close(); + eventDriver.timers.set(tm, 100.msecs); + eventDriver.timers.wait(tm, (tm) { + scope (failure) assert(false); + remove(testFilename); + }); + }); + + ExitReason er; + do er = eventDriver.core.processEvents(); + while (er == ExitReason.idle); + //assert(er == ExitReason.outOfWaiters); // FIXME: see above + assert(s_done); + s_done = false; +}