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