Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(177)

Side by Side Diff: pkg/watcher/lib/src/directory_watcher/linux.dart

Issue 46843003: Wrap Directory.watch on linux for the watcher package. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698