1 /// Platform independent file watcher
2 /**
3  * Interface is expected to change in order to make it less platform specific.
4  */
5 module mecca.reactor.io.fs_watcher;
6 
7 // Licensed under the Boost license. Full copyright information in the AUTHORS file
8 
9 import mecca.log;
10 import mecca.reactor;
11 
12 version(linux):
13 
14 import mecca.reactor.io.inotify;
15 public import mecca.reactor.io.inotify : WatchDescriptor;
16 public import core.sys.linux.sys.inotify;
17 
18 
19 /// file watcher struct
20 struct FSWatcher {
21     /// Signature for the watch event callback delegate
22     alias void delegate(ref const(Inotifier.Event) wd) nothrow WatchEventCallback;
23 private:
24     FiberHandle watcherFiberHandle;
25     WatchEventCallback[WatchDescriptor] registeredWatchers; // XXX TODO: switch to non-GC hashing
26     Inotifier notifier;
27 
28 public:
29     /**
30      * Add a watch point.
31      *
32      * Params:
33      * path = The path to watch
34      * mask = The events we should watch for. See details in `Inotifier`.
35      * callback = The delegate to be called when the events happen. Will be passed a `Inotifier.Event` struct. The code
36      *  will run under a critical section, so it must not yield.
37      */
38     @notrace WatchDescriptor addWatch(const(char)[] path, uint mask, WatchEventCallback callback) @safe {
39         if( registeredWatchers.length==0 )
40             open();
41 
42         auto wd = notifier.watch(path, mask);
43 
44         registeredWatchers[wd] = callback;
45 
46         return wd;
47     }
48 
49     /// Remove a watch point
50     @notrace void removeWatch(WatchDescriptor wd) @safe @nogc {
51         notifier.removeWatch(wd);
52         registeredWatchers.remove(wd);
53     }
54 
55 private:
56     @notrace void open() @safe @nogc {
57         notifier.open();
58         watcherFiberHandle = theReactor.spawnFiber( &watcherFiber );
59     }
60 
61     @notrace void closing() nothrow @safe @nogc {
62         notifier.close();
63         watcherFiberHandle.reset();
64     }
65 
66     void watcherFiber() {
67         scope(exit) {
68             closing();
69         }
70 
71         while(true) {
72             auto event = notifier.getEvent();
73             WatchEventCallback* cb = event.wd in registeredWatchers;
74             if( cb is null ) {
75                 WARN!"Received event %s on %s with mask %x but no handler"(event.wd, event.name, event.mask);
76                 continue;
77             }
78 
79             with(theReactor.criticalSection) {
80                 (*cb)(*event);
81             }
82         }
83     }
84 }
85 
86 __gshared FSWatcher fsWatcher;
87 
88 unittest {
89     import mecca.lib.exception;
90 
91     WatchDescriptor wd;
92     uint numEvents;
93 
94     void eventCB(ref const(Inotifier.Event) event) nothrow {
95         INFO!"handle %s mask %x cookie %s len %s name %s"( event.wd, event.mask, event.cookie, event.len, event.name );
96         numEvents++;
97     }
98 
99     void testBody() {
100         import core.sys.posix.stdlib;
101         import std..string;
102         import std.file;
103         import std.process;
104         import std.exception;
105 
106         import mecca.lib.time;
107 
108         string iPath = format("%s/meccaUT-XXXXXX\0", tempDir());
109         char[] path;
110         path.length = iPath.length;
111         path[] = iPath[];
112         errnoEnforce( mkdtemp(path.ptr) !is null );
113         scope(exit) execute( ["rm", "-rf", path] );
114 
115         iPath.length = 0;
116         iPath ~= path[0..$-1];
117         DEBUG!"Using directory %s for inotify tests"( iPath );
118 
119         wd = fsWatcher.addWatch(
120                 path,
121                 Inotifier.IN_MODIFY|Inotifier.IN_ATTRIB|Inotifier.IN_CREATE|Inotifier.IN_DELETE|Inotifier.IN_MOVED_FROM|Inotifier.IN_MOVED_TO|Inotifier.IN_ONLYDIR,
122                 &eventCB);
123         DEBUG!"watch registration returned %s"(wd);
124 
125         uint historicalNumEvents;
126         assertEQ(numEvents, historicalNumEvents);
127         theReactor.yield();
128         assertEQ(numEvents, historicalNumEvents);
129         theReactor.yield();
130         assertEQ(numEvents, historicalNumEvents);
131 
132         execute( ["touch", iPath ~ "/file1"] );
133         theReactor.sleep(1.msecs);
134         assertGT(numEvents, historicalNumEvents);
135         historicalNumEvents = numEvents;
136         theReactor.yield();
137         assertEQ(numEvents, historicalNumEvents);
138         theReactor.yield();
139         assertEQ(numEvents, historicalNumEvents);
140         rename( iPath ~ "/file1", iPath ~ "/file2" );
141         theReactor.sleep(1.msecs);
142         assertGT(numEvents, historicalNumEvents);
143         historicalNumEvents = numEvents;
144         theReactor.yield();
145         assertEQ(numEvents, historicalNumEvents);
146         theReactor.yield();
147         assertEQ(numEvents, historicalNumEvents);
148     }
149 
150     testWithReactor(&testBody);
151 }