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