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