1 /** 2 * ae.sys.inotify 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.sys.inotify; 15 16 version(linux): 17 18 import core.sys.posix.unistd; 19 import core.sys.linux.sys.inotify; 20 21 import std.exception; 22 import std.stdio; 23 import std.string; 24 25 import ae.net.asockets; 26 import ae.utils.meta : singleton; 27 28 /// An inotify connection. 29 struct INotify 30 { 31 /// http://man7.org/linux/man-pages/man7/inotify.7.html 32 enum Mask : uint32_t 33 { 34 access = IN_ACCESS , /// 35 modify = IN_MODIFY , /// 36 attrib = IN_ATTRIB , /// 37 closeWrite = IN_CLOSE_WRITE , /// 38 closeNoWrite = IN_CLOSE_NOWRITE, /// 39 open = IN_OPEN , /// 40 movedFrom = IN_MOVED_FROM , /// 41 movedTo = IN_MOVED_TO , /// 42 create = IN_CREATE , /// 43 remove = IN_DELETE , /// 44 removeSelf = IN_DELETE_SELF , /// 45 moveSelf = IN_MOVE_SELF , /// 46 47 unmount = IN_UMOUNT , /// 48 qOverflow = IN_Q_OVERFLOW , /// 49 ignored = IN_IGNORED , /// 50 close = IN_CLOSE , /// 51 move = IN_MOVE , /// 52 onlyDir = IN_ONLYDIR , /// 53 dontFollow = IN_DONT_FOLLOW , /// 54 exclUnlink = IN_EXCL_UNLINK , /// 55 maskAdd = IN_MASK_ADD , /// 56 isDir = IN_ISDIR , /// 57 oneShot = IN_ONESHOT , /// 58 allEvents = IN_ALL_EVENTS , /// 59 } 60 61 /// Identifies an inotify watch. 62 static struct WatchDescriptor { private int wd; } 63 64 /// Callback type. 65 alias INotifyHandler = void delegate(in char[] name, Mask mask, uint cookie); 66 67 /// Add an inotify watch. Returns the inotify watch descriptor. 68 WatchDescriptor add(string path, Mask mask, INotifyHandler handler) 69 { 70 assert(handler); 71 if (fd < 0) 72 start(); 73 auto wd = inotify_add_watch(fd, path.toStringz(), mask); 74 errnoEnforce(wd >= 0, "inotify_add_watch"); 75 handlers[wd] = handler; 76 activeHandlers++; 77 return WatchDescriptor(wd); 78 } 79 80 /// Remove an inotify watch using its descriptor. 81 void remove(WatchDescriptor wd) 82 { 83 assert(handlers.get(wd.wd, null) != null, "No such descriptor registered"); 84 auto result = inotify_rm_watch(fd, wd.wd); 85 errnoEnforce(result >= 0, "inotify_rm_watch"); 86 handlers[wd.wd] = null; // Keep tombstone to avoid race conditions 87 activeHandlers--; 88 if (!activeHandlers) 89 stop(); 90 } 91 92 private: 93 int fd = -1; 94 FileConnection conn; 95 96 INotifyHandler[int] handlers; 97 size_t activeHandlers; 98 99 void start() 100 { 101 assert(fd < 0, "Already started"); 102 fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); 103 errnoEnforce(fd >= 0, "inotify_init1"); 104 105 conn = new FileConnection(fd); 106 //conn.daemon = true; 107 conn.handleReadData = &onReadData; 108 } 109 110 void stop() 111 { 112 assert(fd >= 0, "Not started"); 113 conn.disconnect(); 114 fd = -1; 115 handlers = null; 116 } 117 118 void onReadData(Data data) 119 { 120 while (data.length) 121 data.enter((scope contents) { 122 enforce(data.length >= inotify_event.sizeof, "Insufficient bytes for inotify_event"); 123 auto pheader = cast(inotify_event*)contents.ptr; // inotify_event is non-copyable 124 auto end = inotify_event.sizeof + pheader.len; 125 enforce(data.length >= end, "Insufficient bytes for inotify name"); 126 auto name = cast(char[])contents[inotify_event.sizeof .. end]; 127 128 auto p = name.indexOf('\0'); 129 if (p >= 0) 130 name = name[0..p]; 131 132 if (pheader.wd == -1) 133 { 134 // Overflow - notify all watch descriptors 135 foreach (wd, handler; handlers) 136 if (handler) 137 handler(name, cast(Mask)pheader.mask, pheader.cookie); 138 } 139 else 140 { 141 auto phandler = pheader.wd in handlers; 142 enforce(phandler, "Unregistered inotify watch descriptor"); 143 if (*phandler) 144 (*phandler)(name, cast(Mask)pheader.mask, pheader.cookie); 145 else 146 debug (ae_inotify) stderr.writeln("Dropping inotify event for removed handler"); 147 } 148 data = data[end..$]; 149 }); 150 } 151 } 152 153 /// The global inotify connection. 154 INotify iNotify; 155 156 /// 157 unittest 158 { 159 import std.file, ae.sys.file; 160 161 if ("tmp".exists) "tmp".removeRecurse(); 162 mkdir("tmp"); 163 scope(exit) "tmp".removeRecurse(); 164 165 INotify.Mask[] events; 166 INotify.WatchDescriptor wd; 167 wd = iNotify.add("tmp", INotify.Mask.create | INotify.Mask.remove, 168 (in char[] name, INotify.Mask mask, uint cookie) 169 { 170 assert(name == "killme"); 171 events ~= mask; 172 if (events.length == 2) 173 iNotify.remove(wd); 174 } 175 ); 176 touch("tmp/killme"); 177 remove("tmp/killme"); 178 socketManager.loop(); 179 180 assert(events == [INotify.Mask.create, INotify.Mask.remove]); 181 }