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

Side by Side Diff: observatory_pub_packages/watcher/src/directory_watcher/linux.dart

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

Powered by Google App Engine
This is Rietveld 408576698