Implement inotify based directory watcher.

This commit is contained in:
Sönke Ludwig 2016-10-17 21:42:09 +02:00
parent 8ecc583e4d
commit e4e78a860a
4 changed files with 198 additions and 11 deletions

View file

@ -33,7 +33,7 @@ Events | yes | yes | no | no
Signals | yes² | yes² | no | no Signals | yes² | yes² | no | no
Files | yes | yes | no | no Files | yes | yes | no | no
UI Integration | no | no | 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 ² Currently only supported on Linux

View file

@ -182,7 +182,7 @@ interface EventDriverTimers {
bool isPending(TimerID timer); bool isPending(TimerID timer);
bool isPeriodic(TimerID timer); bool isPeriodic(TimerID timer);
void wait(TimerID timer, TimerCallback callback); void wait(TimerID timer, TimerCallback callback);
void cancelWait(TimerID timer, TimerCallback callback); void cancelWait(TimerID timer);
/** Increments the reference count of the given resource. /** Increments the reference count of the given resource.
*/ */
@ -198,9 +198,7 @@ interface EventDriverTimers {
interface EventDriverWatchers { interface EventDriverWatchers {
@safe: /*@nogc:*/ nothrow: @safe: /*@nogc:*/ nothrow:
WatcherID watchDirectory(string path, bool recursive); WatcherID watchDirectory(string path, bool recursive, FileChangesCallback callback);
void wait(WatcherID watcher, FileChangesCallback callback);
void cancelWait(WatcherID watcher);
/** Increments the reference count of the given resource. /** 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 Once the reference count reaches zero, all associated resources will be
freed and the resource descriptor gets invalidated. 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 EventCallback = void delegate(EventID);
alias SignalCallback = void delegate(SignalListenID, SignalStatus, int); alias SignalCallback = void delegate(SignalListenID, SignalStatus, int);
alias TimerCallback = void delegate(TimerID); 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*); @system alias DataInitializer = void function(void*);
enum ExitReason { enum ExitReason {
@ -304,10 +302,16 @@ enum SignalStatus {
*/ */
struct FileChange { struct FileChange {
/// The type of change /// The type of change
FileChangeKind type; FileChangeKind kind;
/// Path of the file/directory that was changed /// Directory containing the changed file
string path; 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) { struct Handle(string NAME, T, T invalid_value = T.init) {

View file

@ -52,7 +52,8 @@ final class PosixEventDriver(Loop : PosixEventLoop) : EventDriver {
version (linux) alias DNSDriver = EventDriverDNS_GAIA!(EventsDriver, SignalsDriver); version (linux) alias DNSDriver = EventDriverDNS_GAIA!(EventsDriver, SignalsDriver);
else alias DNSDriver = EventDriverDNS_GAI!(EventsDriver, SignalsDriver); else alias DNSDriver = EventDriverDNS_GAI!(EventsDriver, SignalsDriver);
alias FileDriver = ThreadedFileEventDriver!EventsDriver; alias FileDriver = ThreadedFileEventDriver!EventsDriver;
alias WatcherDriver = PosixEventDriverWatchers!Loop; version (linux) alias WatcherDriver = InotifyEventDriverWatchers!Loop;
else alias WatcherDriver = PosixEventDriverWatchers!Loop;
Loop m_loop; Loop m_loop;
CoreDriver m_core; 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 { final class PosixEventDriverWatchers(Loop : PosixEventLoop) : EventDriverWatchers {
@safe: /*@nogc:*/ nothrow: @safe: /*@nogc:*/ nothrow:
private Loop m_loop; private Loop m_loop;

66
tests/0-dirwatcher.d Normal file
View file

@ -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;
}