OLD | NEW |
| (Empty) |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library watcher.directory_watcher.linux; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:io'; | |
9 | |
10 import '../directory_watcher.dart'; | |
11 import '../resubscribable.dart'; | |
12 import '../utils.dart'; | |
13 import '../watch_event.dart'; | |
14 | |
15 /// Uses the inotify subsystem to watch for filesystem events. | |
16 /// | |
17 /// Inotify doesn't suport recursively watching subdirectories, nor does | |
18 /// [Directory.watch] polyfill that functionality. This class polyfills it | |
19 /// instead. | |
20 /// | |
21 /// This class also compensates for the non-inotify-specific issues of | |
22 /// [Directory.watch] producing multiple events for a single logical action | |
23 /// (issue 14372) and providing insufficient information about move events | |
24 /// (issue 14424). | |
25 class LinuxDirectoryWatcher extends ResubscribableWatcher | |
26 implements DirectoryWatcher { | |
27 String get directory => path; | |
28 | |
29 LinuxDirectoryWatcher(String directory) | |
30 : super(directory, () => new _LinuxDirectoryWatcher(directory)); | |
31 } | |
32 | |
33 class _LinuxDirectoryWatcher | |
34 implements DirectoryWatcher, ManuallyClosedWatcher { | |
35 String get directory => path; | |
36 final String path; | |
37 | |
38 Stream<WatchEvent> get events => _eventsController.stream; | |
39 final _eventsController = new StreamController<WatchEvent>.broadcast(); | |
40 | |
41 bool get isReady => _readyCompleter.isCompleted; | |
42 | |
43 Future get ready => _readyCompleter.future; | |
44 final _readyCompleter = new Completer(); | |
45 | |
46 /// The last known state for each entry in this directory. | |
47 /// | |
48 /// The keys in this map are the paths to the directory entries; the values | |
49 /// are [_EntryState]s indicating whether the entries are files or | |
50 /// directories. | |
51 final _entries = new Map<String, _EntryState>(); | |
52 | |
53 /// The watchers for subdirectories of [directory]. | |
54 final _subWatchers = new Map<String, _LinuxDirectoryWatcher>(); | |
55 | |
56 /// A set of all subscriptions that this watcher subscribes to. | |
57 /// | |
58 /// These are gathered together so that they may all be canceled when the | |
59 /// watcher is closed. | |
60 final _subscriptions = new Set<StreamSubscription>(); | |
61 | |
62 _LinuxDirectoryWatcher(this.path) { | |
63 // Batch the inotify changes together so that we can dedup events. | |
64 var innerStream = new Directory(path).watch() | |
65 .transform(new BatchedStreamTransformer<FileSystemEvent>()); | |
66 _listen(innerStream, _onBatch, | |
67 onError: _eventsController.addError, | |
68 onDone: _onDone); | |
69 | |
70 _listen(new Directory(path).list(), (entity) { | |
71 _entries[entity.path] = new _EntryState(entity is Directory); | |
72 if (entity is! Directory) return; | |
73 _watchSubdir(entity.path); | |
74 }, onError: (error, stackTrace) { | |
75 _eventsController.addError(error, stackTrace); | |
76 close(); | |
77 }, onDone: () { | |
78 _waitUntilReady().then((_) => _readyCompleter.complete()); | |
79 }, cancelOnError: true); | |
80 } | |
81 | |
82 /// Returns a [Future] that completes once all the subdirectory watchers are | |
83 /// fully initialized. | |
84 Future _waitUntilReady() { | |
85 return Future.wait(_subWatchers.values.map((watcher) => watcher.ready)) | |
86 .then((_) { | |
87 if (_subWatchers.values.every((watcher) => watcher.isReady)) return null; | |
88 return _waitUntilReady(); | |
89 }); | |
90 } | |
91 | |
92 void close() { | |
93 for (var subscription in _subscriptions) { | |
94 subscription.cancel(); | |
95 } | |
96 for (var watcher in _subWatchers.values) { | |
97 watcher.close(); | |
98 } | |
99 | |
100 _subWatchers.clear(); | |
101 _subscriptions.clear(); | |
102 _eventsController.close(); | |
103 } | |
104 | |
105 /// Returns all files (not directories) that this watcher knows of are | |
106 /// recursively in the watched directory. | |
107 Set<String> get _allFiles { | |
108 var files = new Set<String>(); | |
109 _getAllFiles(files); | |
110 return files; | |
111 } | |
112 | |
113 /// Helper function for [_allFiles]. | |
114 /// | |
115 /// Adds all files that this watcher knows of to [files]. | |
116 void _getAllFiles(Set<String> files) { | |
117 files.addAll(_entries.keys | |
118 .where((path) => _entries[path] == _EntryState.FILE).toSet()); | |
119 for (var watcher in _subWatchers.values) { | |
120 watcher._getAllFiles(files); | |
121 } | |
122 } | |
123 | |
124 /// Watch a subdirectory of [directory] for changes. | |
125 /// | |
126 /// If the subdirectory was added after [this] began emitting events, its | |
127 /// contents will be emitted as ADD events. | |
128 void _watchSubdir(String path) { | |
129 if (_subWatchers.containsKey(path)) return; | |
130 var watcher = new _LinuxDirectoryWatcher(path); | |
131 _subWatchers[path] = watcher; | |
132 | |
133 // TODO(nweiz): Catch any errors here that indicate that the directory in | |
134 // question doesn't exist and silently stop watching it instead of | |
135 // propagating the errors. | |
136 _listen(watcher.events, (event) { | |
137 if (isReady) _eventsController.add(event); | |
138 }, onError: (error, stackTrace) { | |
139 _eventsController.addError(error, stackTrace); | |
140 close(); | |
141 }, onDone: () { | |
142 if (_subWatchers[path] == watcher) _subWatchers.remove(path); | |
143 | |
144 // It's possible that a directory was removed and recreated very quickly. | |
145 // If so, make sure we're still watching it. | |
146 if (new Directory(path).existsSync()) _watchSubdir(path); | |
147 }); | |
148 | |
149 // TODO(nweiz): Right now it's possible for the watcher to emit an event for | |
150 // a file before the directory list is complete. This could lead to the user | |
151 // seeing a MODIFY or REMOVE event for a file before they see an ADD event, | |
152 // which is bad. We should handle that. | |
153 // | |
154 // One possibility is to provide a general means (e.g. | |
155 // `DirectoryWatcher.eventsAndExistingFiles`) to tell a watcher to emit | |
156 // events for all the files that already exist. This would be useful for | |
157 // top-level clients such as barback as well, and could be implemented with | |
158 // a wrapper similar to how listening/canceling works now. | |
159 | |
160 // If a directory is added after we're finished with the initial scan, emit | |
161 // an event for each entry in it. This gives the user consistently gets an | |
162 // event for every new file. | |
163 watcher.ready.then((_) { | |
164 if (!isReady || _eventsController.isClosed) return; | |
165 _listen(new Directory(path).list(recursive: true), (entry) { | |
166 if (entry is Directory) return; | |
167 _eventsController.add(new WatchEvent(ChangeType.ADD, entry.path)); | |
168 }, onError: (error, stackTrace) { | |
169 // Ignore an exception caused by the dir not existing. It's fine if it | |
170 // was added and then quickly removed. | |
171 if (error is FileSystemException) return; | |
172 | |
173 _eventsController.addError(error, stackTrace); | |
174 close(); | |
175 }, cancelOnError: true); | |
176 }); | |
177 } | |
178 | |
179 /// The callback that's run when a batch of changes comes in. | |
180 void _onBatch(List<FileSystemEvent> batch) { | |
181 var changedEntries = new Set<String>(); | |
182 var oldEntries = new Map.from(_entries); | |
183 | |
184 // inotify event batches are ordered by occurrence, so we treat them as a | |
185 // log of what happened to a file. | |
186 for (var event in batch) { | |
187 // If the watched directory is deleted or moved, we'll get a deletion | |
188 // event for it. Ignore it; we handle closing [this] when the underlying | |
189 // stream is closed. | |
190 if (event.path == path) continue; | |
191 | |
192 changedEntries.add(event.path); | |
193 | |
194 if (event is FileSystemMoveEvent) { | |
195 changedEntries.add(event.destination); | |
196 _changeEntryState(event.path, ChangeType.REMOVE, event.isDirectory); | |
197 _changeEntryState(event.destination, ChangeType.ADD, event.isDirectory); | |
198 } else { | |
199 _changeEntryState(event.path, _changeTypeFor(event), event.isDirectory); | |
200 } | |
201 } | |
202 | |
203 for (var path in changedEntries) { | |
204 emitEvent(ChangeType type) { | |
205 if (isReady) _eventsController.add(new WatchEvent(type, path)); | |
206 } | |
207 | |
208 var oldState = oldEntries[path]; | |
209 var newState = _entries[path]; | |
210 | |
211 if (oldState != _EntryState.FILE && newState == _EntryState.FILE) { | |
212 emitEvent(ChangeType.ADD); | |
213 } else if (oldState == _EntryState.FILE && newState == _EntryState.FILE) { | |
214 emitEvent(ChangeType.MODIFY); | |
215 } else if (oldState == _EntryState.FILE && newState != _EntryState.FILE) { | |
216 emitEvent(ChangeType.REMOVE); | |
217 } | |
218 | |
219 if (oldState == _EntryState.DIRECTORY) { | |
220 var watcher = _subWatchers.remove(path); | |
221 if (watcher == null) continue; | |
222 for (var path in watcher._allFiles) { | |
223 _eventsController.add(new WatchEvent(ChangeType.REMOVE, path)); | |
224 } | |
225 watcher.close(); | |
226 } | |
227 | |
228 if (newState == _EntryState.DIRECTORY) _watchSubdir(path); | |
229 } | |
230 } | |
231 | |
232 /// Changes the known state of the entry at [path] based on [change] and | |
233 /// [isDir]. | |
234 void _changeEntryState(String path, ChangeType change, bool isDir) { | |
235 if (change == ChangeType.ADD || change == ChangeType.MODIFY) { | |
236 _entries[path] = new _EntryState(isDir); | |
237 } else { | |
238 assert(change == ChangeType.REMOVE); | |
239 _entries.remove(path); | |
240 } | |
241 } | |
242 | |
243 /// Determines the [ChangeType] associated with [event]. | |
244 ChangeType _changeTypeFor(FileSystemEvent event) { | |
245 if (event is FileSystemDeleteEvent) return ChangeType.REMOVE; | |
246 if (event is FileSystemCreateEvent) return ChangeType.ADD; | |
247 | |
248 assert(event is FileSystemModifyEvent); | |
249 return ChangeType.MODIFY; | |
250 } | |
251 | |
252 /// Handles the underlying event stream closing, indicating that the directory | |
253 /// being watched was removed. | |
254 void _onDone() { | |
255 // Most of the time when a directory is removed, its contents will get | |
256 // individual REMOVE events before the watch stream is closed -- in that | |
257 // case, [_entries] will be empty here. However, if the directory's removal | |
258 // is caused by a MOVE, we need to manually emit events. | |
259 if (isReady) { | |
260 _entries.forEach((path, state) { | |
261 if (state == _EntryState.DIRECTORY) return; | |
262 _eventsController.add(new WatchEvent(ChangeType.REMOVE, path)); | |
263 }); | |
264 } | |
265 | |
266 // The parent directory often gets a close event before the subdirectories | |
267 // are done emitting events. We wait for them to finish before we close | |
268 // [events] so that we can be sure to emit a remove event for every file | |
269 // that used to exist. | |
270 Future.wait(_subWatchers.values.map((watcher) { | |
271 try { | |
272 return watcher.events.toList(); | |
273 } on StateError catch (_) { | |
274 // It's possible that [watcher.events] is closed but the onDone event | |
275 // hasn't reached us yet. It's fine if so. | |
276 return new Future.value(); | |
277 } | |
278 })).then((_) => close()); | |
279 } | |
280 | |
281 /// Like [Stream.listen], but automatically adds the subscription to | |
282 /// [_subscriptions] so that it can be canceled when [close] is called. | |
283 void _listen(Stream stream, void onData(event), {Function onError, | |
284 void onDone(), bool cancelOnError}) { | |
285 var subscription; | |
286 subscription = stream.listen(onData, onError: onError, onDone: () { | |
287 _subscriptions.remove(subscription); | |
288 if (onDone != null) onDone(); | |
289 }, cancelOnError: cancelOnError); | |
290 _subscriptions.add(subscription); | |
291 } | |
292 } | |
293 | |
294 /// An enum for the possible states of entries in a watched directory. | |
295 class _EntryState { | |
296 final String _name; | |
297 | |
298 /// The entry is a file. | |
299 static const FILE = const _EntryState._("file"); | |
300 | |
301 /// The entry is a directory. | |
302 static const DIRECTORY = const _EntryState._("directory"); | |
303 | |
304 const _EntryState._(this._name); | |
305 | |
306 /// Returns [DIRECTORY] if [isDir] is true, and [FILE] otherwise. | |
307 factory _EntryState(bool isDir) => | |
308 isDir ? _EntryState.DIRECTORY : _EntryState.FILE; | |
309 | |
310 String toString() => _name; | |
311 } | |
OLD | NEW |