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 }