1 module beangle.fs.watch; 2 3 import core.time; 4 import core.sys.posix.unistd; 5 import core.sys.posix.poll; 6 import core.sys.linux.sys.inotify; 7 import std.algorithm; 8 import std.exception; 9 import std.file; 10 import std.stdio; 11 import std..string; 12 import beangle.fs.inotify; 13 14 public struct Watch { 15 Event[] read(Duration timeout){ 16 return readImpl( cast(int)timeout.total!"msecs"); 17 } 18 19 Event[] read(){ 20 return readImpl( -1); 21 } 22 23 public @property int descriptor(){ 24 return queuefd; 25 } 26 private void addDir(string root){ 27 enforce( exists( root)); 28 add( root,this.mask | IN_CREATE | IN_DELETE_SELF); 29 foreach (d; dirEntries( root, SpanMode.breadth)) { 30 if (d.isDir && !d.isSymlink) add( d.name,this.mask| IN_CREATE | IN_DELETE_SELF); 31 } 32 } 33 34 private int add(string path,uint mask){ 35 import std.conv; 36 auto zpath = toStringz( path); 37 auto wd = inotify_add_watch( this.queuefd,zpath, mask); 38 if (wd>0){ 39 paths[wd] = path; 40 } 41 return wd; 42 } 43 44 private void remove(int wd){ 45 paths.remove( wd); 46 enforce( inotify_rm_watch( this.queuefd, wd) == 0, "failed to remove inotify watch"); 47 } 48 49 private const (char)[] name(ref inotify_event e) { 50 auto ptr = cast(const(char)*)(&e.name); 51 return fromStringz( ptr); 52 } 53 54 private Event[] readImpl(int timeout) { 55 pollfd pfd; 56 pfd.fd = queuefd; 57 pfd.events = POLLIN; 58 59 if (poll( &pfd, 1, timeout) <= 0) return null; 60 long len = .read( queuefd, buffer.ptr, buffer.length);// why . 61 enforce( len > 0, "failed to read inotify event"); // test why len >0 when no event happen. 62 ubyte* head = buffer.ptr; 63 events.length = 0; 64 events.assumeSafeAppend(); 65 while (len > 0) { 66 auto eptr = cast(inotify_event*)head; 67 auto size = (*eptr).sizeof + eptr.len; 68 head += size; 69 len -= size; 70 string path = paths[eptr.wd]; 71 path ~= "/" ~ name( *eptr); 72 auto e = Event( eptr.wd, eptr.mask, eptr.cookie,path ); 73 if (e.mask & IN_ISDIR) { 74 if (e.mask & IN_CREATE) { 75 add( path,this.mask | IN_CREATE | IN_DELETE_SELF); 76 } else if (e.mask & IN_DELETE_SELF) { 77 remove( e.wd); 78 } 79 } 80 if (mask & e.mask) { 81 events ~= e; 82 } 83 } 84 return events; 85 } 86 87 private int queuefd = -1; // inotify event queue file discriptor 88 private string[] roots; 89 private int mask; 90 private string[uint] paths; 91 private ubyte[] buffer; 92 private Event[] events; 93 94 this(int queuefd,string[] roots,int mask) { 95 enforce( queuefd >= 0, "failed to init inotify"); 96 this.queuefd = queuefd; 97 this.mask=mask; 98 //see http://man7.org/linux/man-pages/man7/inotify.7.html 99 buffer = new ubyte[1024*(inotify_event.sizeof + 256)]; 100 this.roots=roots; 101 foreach (string root;roots){ 102 this.addDir( root); 103 } 104 } 105 ~this(){ 106 stop(); 107 } 108 void stop(){ 109 if (queuefd >= 0) { 110 close( queuefd); 111 queuefd = -1; 112 } 113 } 114 } 115 116 public auto watch(string base,int mask) { 117 return Watch( inotify_init1( IN_NONBLOCK),[ base],mask); 118 } 119 120 unittest { 121 import std.process; 122 executeShell( "rm -rf temp"); 123 executeShell( "mkdir temp"); 124 auto monitor = watch( "temp",IN_CREATE | IN_DELETE); 125 executeShell( "touch temp/killme"); 126 auto events = monitor.read(); 127 assert(events[0].mask == IN_CREATE); 128 assert(events[0].path == "temp/killme"); 129 130 executeShell( "rm -rf temp/killme"); 131 events = monitor.read(); 132 assert(events[0].mask == IN_DELETE); 133 134 // watched directory and new sub-directory is not watched. 135 executeShell( "mkdir temp/dir"); 136 executeShell( "touch temp/dir/victim"); 137 events = monitor.read(); 138 assert(events.length == 1); 139 assert(events[0].mask == (IN_ISDIR | IN_CREATE)); 140 assert(events[0].path == "temp/dir"); 141 142 //monitor tree 143 executeShell( "rm -rf temp"); 144 executeShell( "mkdir -p temp/dir1"); 145 executeShell( "mkdir -p temp/dir2"); 146 monitor = watch( "temp", IN_CREATE | IN_DELETE); 147 executeShell( "touch temp/dir1/a.temp"); 148 executeShell( "touch temp/dir2/b.temp"); 149 executeShell( "rm -rf temp/dir2"); 150 auto evs = monitor.read(); 151 assert(evs.length == 4); 152 // a & b files created 153 assert(evs[0].mask == IN_CREATE && evs[0].path == "temp/dir1/a.temp"); 154 assert(evs[1].mask == IN_CREATE && evs[1].path == "temp/dir2/b.temp"); 155 // b deleted as part of sub-tree 156 assert(evs[2].mask == IN_DELETE && evs[2].path == "temp/dir2/b.temp"); 157 assert(evs[3].mask == (IN_DELETE | IN_ISDIR) && evs[3].path == "temp/dir2"); 158 evs = monitor.read( 10.msecs); 159 assert(evs.length == 0); 160 161 import core.thread; 162 auto t = new Thread( (){ 163 Thread.sleep( 1000.msecs); 164 executeShell( "touch temp/dir1/c.temp"); 165 }).start(); 166 evs = monitor.read( 10.msecs); 167 t.join(); 168 assert(evs.length == 0); 169 evs = monitor.read( 10.msecs); 170 assert(evs.length == 1); 171 172 executeShell( "rm -rf temp"); 173 }