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 			import std.stdio;
90 			try writeln("Could not add directory: " ~ e.toString()); catch {}
91 			return false;
92 		}
93 
94 		return true;
95 	}
96 
97 	/// Removes the directory and its files from the event watch list.
98 	/// Recursive will remove all subdirectories in the watch list.
99 	bool unwatchDir(string path, bool recursive) {
100 		import std.algorithm : countUntil;
101 
102 		try {
103 			path = Path(path).toString();
104 
105 			bool removeWatch(string path) {
106 				auto idx = m_directories[].countUntil!((a,b) => a.path == b)(Path(path));
107 				if (idx < 0)
108 					return true;
109 
110 				if (!m_evLoop.unwatch(m_fd, m_directories[idx].wd))
111 					return false;
112 
113 				m_directories.linearRemove(m_directories[idx .. idx+1]);
114 				return true;
115 			}
116 			removeWatch(path);
117 			if (recursive && path.isDir) {
118 				foreach (de; path.dirEntries(SpanMode.shallow)) {
119 					if (de.isDir){
120 						if (!removeWatch(path))
121 							return false;
122 					}
123 				}
124 			}
125 		} catch {}
126 		return true;
127 	}
128 
129 	/// Cleans up underlying resources.
130 	bool kill()
131 	in { assert(m_fd != fd_t.init); }
132 	body {
133 		return m_evLoop.kill(this);
134 	}
135 	
136 package:
137 	version(Posix) mixin EvInfoMixins;
138 	
139 	@property fd_t fd() const {
140 		return m_fd;
141 	}
142 	
143 	@property void fd(fd_t val) {
144 		m_fd = val;
145 	}
146 	
147 }
148 
149 /// Represents one event on one file in a watched directory.
150 struct DWChangeInfo {
151 	/// The event triggered by the file/folder
152 	DWFileEvent event;
153 	/// The os-independent address of the file/folder
154 	private Path m_path;
155 
156 	@property string path() {
157 		return m_path.toNativeString();
158 	}
159 
160 	@property void path(Path p) {
161 		m_path = p;
162 	}
163 
164 }
165 
166 /// List of events that can be watched for. They must be 'Or'ed together
167 /// to combined them when calling watch(). OS-triggerd events are exclusive.
168 enum DWFileEvent : uint {
169 	ERROR = 0,
170 	MODIFIED = 0x00000002,
171 	MOVED_FROM = 0x00000040,
172 	MOVED_TO = 0x00000080,
173 	CREATED = 0x00000100, 
174 	DELETED = 0x00000200,
175 	ALL = MODIFIED | MOVED_FROM | MOVED_TO | CREATED | DELETED
176 }
177 
178 
179 package struct DWHandler {
180 	AsyncDirectoryWatcher ctxt;
181 	void delegate() del;
182 	void opCall(){
183 		assert(ctxt !is null);
184 		debug ctxt.m_dataRemaining = true;
185 		del();
186 		debug {
187 			assert(!ctxt.m_dataRemaining, "You must read all changes when you receive a notification for directory changes");
188 		}
189 		assert(ctxt !is null);
190 		return;
191 	}
192 }
193 
194 package struct WatchInfo {
195 	DWFileEvent events;
196 	Path path;
197 	bool recursive;
198 	uint wd; // watch descriptor
199 }