1 /**
2     A tiny library to work with Linux's kernel inotify subsystem.
3 
4 */
5 module dinotify;
6 
7 import core.time;
8 import core.sys.posix.unistd;
9 import core.sys.posix.poll;
10 import core.sys.linux.sys.inotify;
11 import std.exception;
12 
13 private:
14 
15 ///
16 unittest
17 {
18     import std.process, std.stdio : writeln, writefln;
19 
20     auto monitor = iNotify();
21     executeShell("rm -rf tmp");
22     executeShell("mkdir tmp");
23     // literals are zero-terminated
24     monitor.add("tmp".ptr, IN_CREATE | IN_DELETE);
25     ubyte[] data = [1, 2, 3, 4];
26     executeShell("touch tmp/killme");
27     auto events = monitor.read();
28     assert(events[0].mask == IN_CREATE);
29     assert(events[0].name == "killme");
30 
31     executeShell("rm -rf tmp/killme");
32     events = monitor.read();
33     assert(events[0].mask == IN_DELETE);
34 
35     // Note: watched directory doesn't track events in sub-directories
36     executeShell("mkdir tmp/some-dir");
37     executeShell("touch tmp/some-dir/victim");
38     events = monitor.read();
39     assert(events.length == 1);
40     assert(events[0].mask == (IN_ISDIR | IN_CREATE));
41     assert(events[0].name == "some-dir");
42 }
43 
44 // core.sys.linux is lacking, so just list proper prototypes on our own
45 extern (C)
46 {
47     size_t strnlen(const(char)* s, size_t maxlen);
48     enum NAME_MAX = 255;
49 }
50     
51 auto size(ref inotify_event e)
52 {
53     return e.sizeof + e.len;
54 }
55 
56 // Get name out of event structure
57 const(char)[] name(ref inotify_event e)
58 {
59     auto ptr = cast(const(char)*)(&e+1); 
60     auto len = strnlen(ptr, e.len);
61     return ptr[0..len];
62 }
63 
64 auto maxEvent()
65 {
66     return inotify_event.sizeof + NAME_MAX + 1;
67 }
68 
69 /// Type-safe watch descriptor to help discern it from normal file descriptors
70 public struct Watch
71 {
72     private int wd;   
73 }
74 
75 /// D-ified inotify event, holds slice to temporary buffer with z-string.
76 public struct Event
77 {
78     Watch watch;
79     uint mask, cookie;
80     const(char)[] name;
81 }
82 
83 public struct INotify
84 {
85     private int fd = -1; // inotify fd
86     private ubyte[] buffer;
87     private Event[] events;
88     
89     private this(int fd)
90     {
91         enforce(fd >= 0, "failed to init inotify");
92         this.fd = fd;
93         buffer = new ubyte[20*maxEvent];
94     }
95 
96     @disable this(this);
97 
98     public @property int descriptor(){ return fd; }
99 
100     /// Add path to watch set of this INotify instance
101     Watch add(const(char)* path, uint mask)
102     {
103         auto w = Watch(inotify_add_watch(fd, path, mask));
104         enforce(w.wd >= 0, "failed to add inotify watch");
105         return w;
106     }
107 
108     /// ditto
109     Watch add(const(char)[] path, uint mask)
110     {
111         auto zpath = path ~ '\0';
112         return add(zpath.ptr, mask);
113     }
114 
115     /// Remove watch descriptor from this this INotify instance
116     void remove(Watch w)
117     {
118         enforce(inotify_rm_watch(fd, w.wd) == 0, "failed to remove inotify watch");
119     }
120 
121     /**
122         Issue a blocking read to get a bunch of events,
123         there is at least one event in the returned slice.
124         
125         If no event occurs within specified timeout returns empty array.
126         Occuracy of timeout is in miliseconds.
127 
128         Note that returned slice is mutable.
129         This indicates that it is invalidated on 
130         the next call to read, just like byLine in std.stdio.
131     */
132     Event[] read(Duration timeout)
133     {
134         return readImpl(cast(int)timeout.total!"msecs");
135     }
136 
137     Event[] read()
138     {
139         return readImpl(-1);
140     }
141 
142     private Event[] readImpl(int timeout)
143     {
144         pollfd pfd;
145         pfd.fd = fd;
146         pfd.events = POLLIN;
147         if (poll(&pfd, 1, timeout) <= 0) return null;
148         long len = .read(fd, buffer.ptr, buffer.length);
149         enforce(len > 0, "failed to read inotify event");
150         ubyte* head = buffer.ptr;
151         events.length = 0;
152         events.assumeSafeAppend();
153         while (len > 0)
154         {
155             auto eptr = cast(inotify_event*)head;
156             auto sz = size(*eptr);
157             head += sz;
158             len -= sz;
159             events ~= Event(Watch(eptr.wd), eptr.mask, eptr.cookie, name(*eptr));
160         }
161         return events;
162     }
163     
164     ~this()
165     {
166         if(fd >= 0)
167             close(fd);
168     }
169 }
170 
171 /// Create new INotify struct
172 public auto iNotify()
173 {
174     return INotify(inotify_init1(IN_NONBLOCK));
175 }
176 
177 /++
178     Event as returned by INotifyTree.
179     In constrast to Event, it has full path and no watch descriptor.
180 +/
181 public struct TreeEvent
182 {
183     uint mask;
184     string path;
185 }
186 
187 /++
188     Track events in the whole directory tree, automatically adding watches to
189     any new sub-directories and stopping watches in the deleted ones.
190 +/
191 public struct INotifyTree
192 {
193     private INotify inotify;
194     private uint mask;
195     private Watch[string] watches;
196     private string[Watch] paths;
197     TreeEvent[] events;
198 
199     private void addWatch(string dirPath)
200     {
201         auto wd = watches[dirPath] = inotify.add(dirPath, mask | IN_CREATE | IN_DELETE_SELF);
202         paths[wd] = dirPath;
203     }
204 
205     private void rmWatch(Watch w)
206     {
207         auto p = paths[w];
208         paths.remove(w);
209         watches.remove(p);
210     }
211 
212     private this(string path, uint mask)
213     {
214         import std.file;
215 
216         inotify = iNotify();
217         this.mask = mask;
218         addWatch(path); //root
219         foreach (d; dirEntries(path, SpanMode.breadth))
220         {
221             if (d.isDir)
222                 addWatch(d.name);
223         }
224     }
225 
226     public @property int descriptor(){ return inotify.descriptor; }
227 
228     private TreeEvent[] readImpl(int timeout)
229     {
230         events.length = 0;
231         events.assumeSafeAppend();
232         // filter events for IN_DELETE_SELF to remove watches
233         // and monitor IN_CREATE with IN_ISDIR to create new watches
234         do
235         {
236             auto evs = inotify.readImpl(timeout);
237             if (evs.length == 0) return null;
238             foreach (e; evs)
239             {
240                 assert(e.watch in paths); //invariant
241                 string path = paths[e.watch];
242                 path ~= "/" ~ e.name; //FIXME: always allocates
243                 if (e.mask & IN_ISDIR)
244                 {
245                     if (e.mask & IN_CREATE)
246                         addWatch(path);
247                     else if (e.mask & IN_DELETE_SELF)
248                     {
249                         rmWatch(e.watch);
250                     }
251                 }
252                 // user may not be interested in IN_CREATE or IN_DELETE_SELF
253                 // but we have to track them
254                 if (mask & e.mask)
255                     events ~= TreeEvent(e.mask, path);
256             }
257         }
258         while (events.length == 0); // some events get filtered... may be even all of them
259         return events;
260     }
261 
262     ///
263     TreeEvent[] read(Duration timeout)
264     {
265         return readImpl(cast(int)timeout.total!"msecs");
266     }   
267 
268     ///
269     TreeEvent[] read()
270     {
271         return readImpl(-1);
272     }   
273 }
274 
275 ///
276 public auto iNotifyTree(string path, uint mask)
277 {
278     return INotifyTree(path, mask);
279 }
280 
281 ///
282 unittest
283 {
284     import std.process;
285     import core.thread;
286 
287     executeShell("rm -rf tmp");
288     executeShell("mkdir -p tmp/dir1/dir11");
289     executeShell("mkdir -p tmp/dir1/dir12");
290     auto ntree = iNotifyTree("tmp/dir1", IN_CREATE | IN_DELETE);
291     executeShell("touch tmp/dir1/dir11/a.tmp");
292     executeShell("touch tmp/dir1/dir12/b.tmp");
293     executeShell("rm -rf tmp/dir1/dir12");
294     auto evs = ntree.read();
295     assert(evs.length == 4);
296     // a & b files created
297     assert(evs[0].mask == IN_CREATE && evs[0].path == "tmp/dir1/dir11/a.tmp");
298     assert(evs[1].mask == IN_CREATE && evs[1].path == "tmp/dir1/dir12/b.tmp");
299     // b deleted as part of sub-tree
300     assert(evs[2].mask == IN_DELETE && evs[2].path == "tmp/dir1/dir12/b.tmp");
301     assert(evs[3].mask == (IN_DELETE | IN_ISDIR) && evs[3].path == "tmp/dir1/dir12");
302     evs = ntree.read(10.msecs);
303     assert(evs.length == 0);
304     auto t = new Thread((){
305         Thread.sleep(1000.msecs);
306         executeShell("touch tmp/dir1/dir11/c.tmp");
307     }).start();
308     evs = ntree.read(10.msecs);
309     t.join();
310     assert(evs.length == 0);
311     evs = ntree.read(10.msecs);
312     assert(evs.length == 1);
313 }
314