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