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