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 <vladimir@thecybershadow.net>
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 struct INotify
29 {
30 	/// http://man7.org/linux/man-pages/man7/inotify.7.html
31 	enum Mask : uint32_t
32 	{
33 		access        = IN_ACCESS       ,
34 		modify        = IN_MODIFY       ,
35 		attrib        = IN_ATTRIB       ,
36 		closeWrite    = IN_CLOSE_WRITE  ,
37 		closeNoWrite  = IN_CLOSE_NOWRITE,
38 		open          = IN_OPEN         ,
39 		movedFrom     = IN_MOVED_FROM   ,
40 		movedTo       = IN_MOVED_TO     ,
41 		create        = IN_CREATE       ,
42 		remove        = IN_DELETE       ,
43 		removeSelf    = IN_DELETE_SELF  ,
44 		moveSelf      = IN_MOVE_SELF    ,
45 
46 		unmount       = IN_UMOUNT       ,
47 		qOverflow     = IN_Q_OVERFLOW   ,
48 		ignored       = IN_IGNORED      ,
49 		close         = IN_CLOSE        ,
50 		move          = IN_MOVE         ,
51 		onlyDir       = IN_ONLYDIR      ,
52 		dontFollow    = IN_DONT_FOLLOW  ,
53 		exclUnlink    = IN_EXCL_UNLINK  ,
54 		maskAdd       = IN_MASK_ADD     ,
55 		isDir         = IN_ISDIR        ,
56 		oneShot       = IN_ONESHOT      ,
57 		allEvents     = IN_ALL_EVENTS   ,
58 	}
59 
60 	static struct WatchDescriptor { int wd; }
61 
62 	alias INotifyHandler = void delegate(in char[] name, Mask mask, uint cookie);
63 
64 	WatchDescriptor add(string path, Mask mask, INotifyHandler handler)
65 	{
66 		if (fd < 0)
67 			start();
68 		auto wd = inotify_add_watch(fd, path.toStringz(), mask);
69 		errnoEnforce(wd >= 0, "inotify_add_watch");
70 		handlers[wd] = handler;
71 		return WatchDescriptor(wd);
72 	}
73 
74 	void remove(WatchDescriptor wd)
75 	{
76 		auto result = inotify_rm_watch(fd, wd.wd);
77 		errnoEnforce(result >= 0, "inotify_rm_watch");
78 		handlers.remove(wd.wd);
79 		if (!handlers.length)
80 			stop();
81 	}
82 
83 private:
84 	int fd = -1;
85 	FileConnection conn;
86 
87 	INotifyHandler[int] handlers;
88 
89 	void start()
90 	{
91 		assert(fd < 0, "Already started");
92 		fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
93 		errnoEnforce(fd >= 0, "inotify_init1");
94 
95 		conn = new FileConnection(fd);
96 		//conn.daemon = true;
97 		conn.handleReadData = &onReadData;
98 	}
99 
100 	void stop()
101 	{
102 		assert(fd >= 0, "Not started");
103 		conn.disconnect();
104 		fd = -1;
105 	}
106 
107 	void onReadData(Data data)
108 	{
109 		while (data.length)
110 		{
111 			enforce(data.length >= inotify_event.sizeof, "Insufficient bytes for inotify_event");
112 			auto pheader = cast(inotify_event*)data.contents.ptr;
113 			auto end = inotify_event.sizeof + pheader.len;
114 			enforce(data.length >= end, "Insufficient bytes for inotify name");
115 			auto name = cast(char[])data.contents[inotify_event.sizeof .. end];
116 
117 			auto p = name.indexOf('\0');
118 			if (p >= 0)
119 				name = name[0..p];
120 
121 			if (pheader.wd == -1)
122 			{
123 				// Overflow - notify all watch descriptors
124 				foreach (wd, handler; handlers)
125 					handler(name, cast(Mask)pheader.mask, pheader.cookie);
126 			}
127 			else
128 			{
129 				auto phandler = pheader.wd in handlers;
130 				enforce(phandler, "Unregistered inotify watch descriptor");
131 				(*phandler)(name, cast(Mask)pheader.mask, pheader.cookie);
132 			}
133 			data = data[end..$];
134 		}
135 	}
136 }
137 
138 INotify iNotify;
139 
140 unittest
141 {
142 	import std.file, ae.sys.file;
143 
144 	if ("tmp".exists) "tmp".removeRecurse();
145 	mkdir("tmp");
146 	scope(exit) "tmp".removeRecurse();
147 
148 	INotify.Mask[] events;
149 	INotify.WatchDescriptor wd;
150 	wd = iNotify.add("tmp", INotify.Mask.create | INotify.Mask.remove,
151 		(in char[] name, INotify.Mask mask, uint cookie)
152 		{
153 			assert(name == "killme");
154 			events ~= mask;
155 			if (events.length == 2)
156 				iNotify.remove(wd);
157 		}
158 	);
159     touch("tmp/killme");
160     remove("tmp/killme");
161     socketManager.loop();
162 
163     assert(events == [INotify.Mask.create, INotify.Mask.remove]);
164 }