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 if (fd < 0) 71 start(); 72 auto wd = inotify_add_watch(fd, path.toStringz(), mask); 73 errnoEnforce(wd >= 0, "inotify_add_watch"); 74 handlers[wd] = handler; 75 return WatchDescriptor(wd); 76 } 77 78 /// Remove an inotify watch using its descriptor. 79 void remove(WatchDescriptor wd) 80 { 81 auto result = inotify_rm_watch(fd, wd.wd); 82 errnoEnforce(result >= 0, "inotify_rm_watch"); 83 handlers.remove(wd.wd); 84 if (!handlers.length) 85 stop(); 86 } 87 88 private: 89 int fd = -1; 90 FileConnection conn; 91 92 INotifyHandler[int] handlers; 93 94 void start() 95 { 96 assert(fd < 0, "Already started"); 97 fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); 98 errnoEnforce(fd >= 0, "inotify_init1"); 99 100 conn = new FileConnection(fd); 101 //conn.daemon = true; 102 conn.handleReadData = &onReadData; 103 } 104 105 void stop() 106 { 107 assert(fd >= 0, "Not started"); 108 conn.disconnect(); 109 fd = -1; 110 } 111 112 void onReadData(Data data) 113 { 114 while (data.length) 115 { 116 enforce(data.length >= inotify_event.sizeof, "Insufficient bytes for inotify_event"); 117 auto pheader = cast(inotify_event*)data.contents.ptr; 118 auto end = inotify_event.sizeof + pheader.len; 119 enforce(data.length >= end, "Insufficient bytes for inotify name"); 120 auto name = cast(char[])data.contents[inotify_event.sizeof .. end]; 121 122 auto p = name.indexOf('\0'); 123 if (p >= 0) 124 name = name[0..p]; 125 126 if (pheader.wd == -1) 127 { 128 // Overflow - notify all watch descriptors 129 foreach (wd, handler; handlers) 130 handler(name, cast(Mask)pheader.mask, pheader.cookie); 131 } 132 else 133 { 134 auto phandler = pheader.wd in handlers; 135 enforce(phandler, "Unregistered inotify watch descriptor"); 136 (*phandler)(name, cast(Mask)pheader.mask, pheader.cookie); 137 } 138 data = data[end..$]; 139 } 140 } 141 } 142 143 /// The global inotify connection. 144 INotify iNotify; 145 146 /// 147 unittest 148 { 149 import std.file, ae.sys.file; 150 151 if ("tmp".exists) "tmp".removeRecurse(); 152 mkdir("tmp"); 153 scope(exit) "tmp".removeRecurse(); 154 155 INotify.Mask[] events; 156 INotify.WatchDescriptor wd; 157 wd = iNotify.add("tmp", INotify.Mask.create | INotify.Mask.remove, 158 (in char[] name, INotify.Mask mask, uint cookie) 159 { 160 assert(name == "killme"); 161 events ~= mask; 162 if (events.length == 2) 163 iNotify.remove(wd); 164 } 165 ); 166 touch("tmp/killme"); 167 remove("tmp/killme"); 168 socketManager.loop(); 169 170 assert(events == [INotify.Mask.create, INotify.Mask.remove]); 171 }