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 }