1 /**
2  * Copyright: Copyright Jason White, 2016
3  * License:   MIT
4  * Authors:   Jason White
5  */
6 module button.watcher.inotify;
7 
8 version (linux):
9 
10 import button.state;
11 import button.resource;
12 
13 import core.sys.posix.unistd;
14 import core.sys.posix.poll;
15 import core.sys.linux.sys.inotify;
16 
17 extern (C) {
18     private size_t strnlen(const(char)* s, size_t maxlen);
19 }
20 
21 /**
22  * Wrapper for an inotify_event.
23  */
24 private struct Event
25 {
26     // Maximum size that an inotify_event can b. This is used to determine a
27     // good buffer size.
28     static immutable max = inotify_event.sizeof * 256;
29 
30     int wd;
31     uint mask;
32     uint cookie;
33     const(char)[] name;
34 
35     this(inotify_event* e)
36     {
37         wd     = e.wd;
38         mask   = e.mask;
39         cookie = e.cookie;
40         name   = e.name.ptr[0 .. strnlen(e.name.ptr, e.len)];
41     }
42 }
43 
44 /**
45  * An infinite input range of chunks of changes. Each item in the range is an
46  * array of changed resources. That is, for each item in the range, a new build
47  * should be started. If many files are changed over a short period of time
48  * (depending on the delay), they will be included in one chunk.
49  */
50 struct ChangeChunks
51 {
52     private
53     {
54         import std.array : Appender;
55 
56         // inotify file descriptor
57         int fd = -1;
58 
59         enum maxEvents = 32;
60 
61         BuildState state;
62 
63         Appender!(Index!Resource[]) current;
64 
65         // Mapping of watches to directories. This is needed to find the path to
66         // the directory that is being watched.
67         string[int] watches;
68 
69         // Number of milliseconds to wait. Wait indefinitely by default.
70         int delay = -1;
71     }
72 
73     // This is an infinite range.
74     enum empty = false;
75 
76     this(BuildState state, string watchDir, size_t delay)
77     {
78         import std.path : filenameCmp, dirName, buildNormalizedPath;
79         import std.container.rbtree;
80         import std.file : exists;
81         import core.sys.linux.sys.inotify;
82         import io.file.stream : sysEnforce;
83         import std.conv : to;
84 
85         this.state = state;
86 
87         if (delay == 0)
88             this.delay = -1;
89         else
90             this.delay = delay.to!int;
91 
92         fd = inotify_init1(IN_NONBLOCK);
93         sysEnforce(fd != -1, "Failed to initialize inotify");
94 
95         alias less = (a,b) => filenameCmp(a, b) < 0;
96 
97         auto rbt = redBlackTree!(less, string)();
98 
99         // Find all directories.
100         foreach (key; state.enumerate!ResourceKey)
101             rbt.insert(dirName(key.path));
102 
103         // Watch each (unique) directory. Note that we only watch directories
104         // instead of individual files so that we are less likely to run out of
105         // file descriptors. Later, we filter out events for files we are not
106         // interested in.
107         foreach (dir; rbt[])
108         {
109             auto realDir = buildNormalizedPath(watchDir, dir);
110 
111             if (exists(realDir))
112             {
113                 auto watch = addWatch(realDir,
114                         IN_CREATE | IN_DELETE | IN_CLOSE_WRITE);
115                 watches[watch] = dir;
116             }
117         }
118 
119         popFront();
120     }
121 
122     ~this()
123     {
124         if (fd != -1)
125             close(fd);
126     }
127 
128     /**
129      * Adds a path to be watched by inotify.
130      */
131     private int addWatch(const(char)[] path, uint mask = IN_ALL_EVENTS)
132     {
133         import std.internal.cstring : tempCString;
134         import io.file.stream : sysEnforce;
135         import std.format : format;
136 
137         immutable wd = inotify_add_watch(fd, path.tempCString(), mask);
138 
139         sysEnforce(wd != -1, "Failed to watch path '%s'".format(path));
140 
141         return wd;
142     }
143 
144     /**
145      * Removes a watch from inotify.
146      */
147     private void removeWatch(int wd)
148     {
149         inotify_rm_watch(fd, wd);
150     }
151 
152     /**
153      * Returns an array of resource indices that have been (potentially)
154      * modified. They still need to be checked to determine if their contents
155      * changed.
156      */
157     const(Index!Resource)[] front()
158     {
159         return current.data;
160     }
161 
162     /**
163      * Called when events are ready to be read.
164      */
165     private void handleEvents(ubyte[] buf)
166     {
167         import std.path : buildNormalizedPath;
168         import io.file.stream : SysException;
169         import core.stdc.errno : errno, EAGAIN;
170 
171         // Window into the valid region of the buffer.
172         ubyte[] window;
173 
174         while (true)
175         {
176             immutable len = read(fd, buf.ptr, buf.length);
177             if (len == -1)
178             {
179                 // Nothing more to read, break out of the loop.
180                 if (errno == EAGAIN)
181                     break;
182 
183                 throw new SysException("Failed to read inotify events");
184             }
185 
186             window = buf[0 .. len];
187 
188             // Loop over the events
189             while (window.length)
190             {
191                 auto e = cast(inotify_event*)window.ptr;
192                 auto event = Event(e);
193 
194                 auto path = buildNormalizedPath(watches[event.wd], event.name);
195 
196                 // Since we monitor directories and not specific files, we must
197                 // check if we received a change that we are actually interested in.
198                 auto id = state.find(path);
199                 if (id != Index!Resource.Invalid)
200                     current.put(id);
201 
202                 window = window[inotify_event.sizeof + e.len .. $];
203             }
204         }
205     }
206 
207     /**
208      * Accumulates changes.
209      */
210     void popFront()
211     {
212         import io.file.stream : SysException;
213         import core.stdc.errno : errno, EINTR;
214 
215         pollfd[1] pollFds = [pollfd(fd, POLLIN)];
216 
217         // Buffer to hold the events. Multiple events can be read at a time.
218         ubyte[maxEvents * Event.max] buf;
219 
220         current.clear();
221 
222         while (true)
223         {
224             // Wait for more events. If we haven't received any yet, wait
225             // indefinitely. Otherwise, give up after a certain delay and return
226             // what we've received.
227             immutable n = poll(pollFds.ptr, pollFds.length,
228                     current.data.length ? delay : -1);
229             if (n == -1)
230             {
231                 if (errno == EINTR)
232                     continue;
233 
234                 throw new SysException("Failed to poll for inotify events");
235             }
236             else if (n == 0)
237             {
238                 // Poll timed out and we've got events, so lets use them.
239                 if (current.data.length > 0)
240                     break;
241             }
242             else if (n > 0)
243             {
244                 if (pollFds[0].revents & POLLIN)
245                     handleEvents(buf);
246 
247                 // Can't ever time out. Yield any events we have.
248                 if (delay == -1 && current.data.length > 0)
249                     break;
250             }
251         }
252     }
253 }