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 }