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 }