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 }