1 ///
2 module async.watcher;
3 
4 import async.types;
5 
6 import async.events;
7 import async.internals.path;
8 import std.container : Array;
9 import std.file;
10 
11 /// Watches one or more directories in the local filesystem for the specified events
12 /// by calling a custom event handler asynchroneously when they occur.
13 ///
14 /// Usage: run() the object, start watching directories, receive an event in your handler,
15 /// read the changes by draining the buffer.
16 final nothrow class AsyncDirectoryWatcher
17 {
18 nothrow:
19 private:
20 	EventLoop m_evLoop;
21 	Array!WatchInfo m_directories;
22 	fd_t m_fd;
23 	debug bool m_dataRemaining;
24 public:
25 	///
26 	this(EventLoop evl)
27 	in { assert(evl !is null); }
28 	body { m_evLoop = evl; }
29 
30 	mixin DefStatus;
31 
32 	/// Fills the buffer with file/folder events and returns the number
33 	/// of events consumed. Returns 0 when the buffer is drained.
34 	uint readChanges(ref DWChangeInfo[] dst) {
35 		uint cnt = m_evLoop.readChanges(m_fd, dst);
36 
37 		debug {
38 			if (cnt < dst.length)
39 				m_dataRemaining = false;
40 		}
41 		return cnt;
42 	}
43 
44 	/// Registers the object in the underlying event loop and sends notifications
45 	/// related to buffer activity by calling the specified handler.
46 	bool run(void delegate() del) {
47 		DWHandler handler;
48 		handler.del = del;
49 		handler.ctxt = this;
50 
51 		m_fd = m_evLoop.run(this, handler);
52 		// import std.stdio;
53 		// try writeln("Running with FD: ", m_fd); catch {}
54 
55 		if (m_fd == fd_t.init)
56 			return false;
57 		return true;
58 	}
59 
60 	/// Starts watching for file events in the specified directory,
61 	/// recursing into subdirectories will add those and its files
62 	/// to the watch list as well.
63 	bool watchDir(string path, DWFileEvent ev = DWFileEvent.ALL, bool recursive = false) {
64 
65 		try
66 		{
67 			path = Path(path).toNativeString();
68 			//import std.stdio;
69 			//writeln("Watching ", path);
70 			bool addWatch() {
71 				WatchInfo info;
72 				info.events = ev;
73 				try info.path = Path(path); catch (Exception) {}
74 				info.recursive = recursive;
75 
76 				//writeln("Watch: ", info.path.toNativeString());
77 				uint wd = m_evLoop.watch(m_fd, info);
78 				//writeln("Watching WD: ", wd);
79 				if (wd == 0 && m_evLoop.status.code != Status.OK)
80 					return false;
81 				info.wd = wd;
82 				try m_directories.insert(info); catch (Exception) {}
83 				return true;
84 			}
85 
86 			if (!addWatch())
87 				return false;
88 
89 		}
90 		catch (Exception e) {
91 			static if (DEBUG) {
92 				import std.stdio;
93 				try writeln("Could not add directory: " ~ e.toString()); catch {}
94 			}
95 			return false;
96 		}
97 
98 		return true;
99 	}
100 
101 	/// Removes the directory and its files from the event watch list.
102 	/// Recursive will remove all subdirectories in the watch list.
103 	bool unwatchDir(string path, bool recursive) {
104 		import std.algorithm : countUntil;
105 
106 		try {
107 			path = Path(path).toString();
108 
109 			bool removeWatch(string path) {
110 				auto idx = m_directories[].countUntil!((a,b) => a.path == b)(Path(path));
111 				if (idx < 0)
112 					return true;
113 
114 				if (!m_evLoop.unwatch(m_fd, m_directories[idx].wd))
115 					return false;
116 
117 				m_directories.linearRemove(m_directories[idx .. idx+1]);
118 				return true;
119 			}
120 			removeWatch(path);
121 			if (recursive && path.isDir) {
122 				foreach (de; path.dirEntries(SpanMode.shallow)) {
123 					if (de.isDir){
124 						if (!removeWatch(path))
125 							return false;
126 					}
127 				}
128 			}
129 		} catch (Exception) {}
130 		return true;
131 	}
132 
133 	/// Cleans up underlying resources.
134 	bool kill()
135 	in { assert(m_fd != fd_t.init); }
136 	body {
137 		return m_evLoop.kill(this);
138 	}
139 
140 	///
141 	@property fd_t fd() const {
142 		return m_fd;
143 	}
144 
145 package:
146 	version(Posix) mixin EvInfoMixins;
147 
148 	@property void fd(fd_t val) {
149 		m_fd = val;
150 	}
151 
152 }
153 
154 /// Represents one event on one file in a watched directory.
155 struct DWChangeInfo {
156 	/// The event triggered by the file/folder
157 	DWFileEvent event;
158 	/// The os-independent address of the file/folder
159 	private Path m_path;
160 
161 	///
162 	@property string path() {
163 		return m_path.toNativeString();
164 	}
165 
166 	///
167 	@property void path(Path p) {
168 		m_path = p;
169 	}
170 
171 }
172 
173 /// List of events that can be watched for. They must be 'Or'ed together
174 /// to combined them when calling watch(). OS-triggerd events are exclusive.
175 enum DWFileEvent : uint {
176 	ERROR = 0, ///
177 	MODIFIED = 0x00000002, ///
178 	MOVED_FROM = 0x00000040, ///
179 	MOVED_TO = 0x00000080, ///
180 	CREATED = 0x00000100, ///
181 	DELETED = 0x00000200, ///
182 	ALL = MODIFIED | MOVED_FROM | MOVED_TO | CREATED | DELETED ///
183 }
184 
185 
186 package struct DWHandler {
187 	AsyncDirectoryWatcher ctxt;
188 	void delegate() del;
189 	void opCall(){
190 		assert(ctxt !is null);
191 		debug ctxt.m_dataRemaining = true;
192 		del();
193 		debug {
194 			assert(!ctxt.m_dataRemaining, "You must read all changes when you receive a notification for directory changes");
195 		}
196 		assert(ctxt !is null);
197 		return;
198 	}
199 }
200 
201 package struct WatchInfo {
202 	DWFileEvent events;
203 	Path path;
204 	bool recursive;
205 	uint wd; // watch descriptor
206 }