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