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

Side by Side Diff: watcher/lib/src/directory_watcher/mac_os.dart

Issue 1400473008: Roll Observatory packages and add a roll script (Closed) Base URL: git@github.com:dart-lang/observatory_pub_packages.git@master
Patch Set: Created 5 years, 2 months 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
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.mac_os;
6
7 import 'dart:async';
8 import 'dart:io';
9
10 import '../directory_watcher.dart';
11 import '../constructable_file_system_event.dart';
12 import '../path_set.dart';
13 import '../resubscribable.dart';
14 import '../utils.dart';
15 import '../watch_event.dart';
16
17 /// Uses the FSEvents subsystem to watch for filesystem events.
18 ///
19 /// FSEvents has two main idiosyncrasies that this class works around. First, it
20 /// will occasionally report events that occurred before the filesystem watch
21 /// was initiated. Second, if multiple events happen to the same file in close
22 /// succession, it won't report them in the order they occurred. See issue
23 /// 14373.
24 ///
25 /// This also works around issues 16003 and 14849 in the implementation of
26 /// [Directory.watch].
27 class MacOSDirectoryWatcher extends ResubscribableWatcher
28 implements DirectoryWatcher {
29 String get directory => path;
30
31 MacOSDirectoryWatcher(String directory)
32 : super(directory, () => new _MacOSDirectoryWatcher(directory));
33 }
34
35 class _MacOSDirectoryWatcher
36 implements DirectoryWatcher, ManuallyClosedWatcher {
37 String get directory => path;
38 final String path;
39
40 Stream<WatchEvent> get events => _eventsController.stream;
41 final _eventsController = new StreamController<WatchEvent>.broadcast();
42
43 bool get isReady => _readyCompleter.isCompleted;
44
45 Future get ready => _readyCompleter.future;
46 final _readyCompleter = new Completer();
47
48 /// The set of files that are known to exist recursively within the watched
49 /// directory.
50 ///
51 /// The state of files on the filesystem is compared against this to determine
52 /// the real change that occurred when working around issue 14373. This is
53 /// also used to emit REMOVE events when subdirectories are moved out of the
54 /// watched directory.
55 final PathSet _files;
56
57 /// The subscription to the stream returned by [Directory.watch].
58 ///
59 /// This is separate from [_subscriptions] because this stream occasionally
60 /// needs to be resubscribed in order to work around issue 14849.
61 StreamSubscription<FileSystemEvent> _watchSubscription;
62
63 /// The subscription to the [Directory.list] call for the initial listing of
64 /// the directory to determine its initial state.
65 StreamSubscription<FileSystemEntity> _initialListSubscription;
66
67 /// The subscriptions to [Directory.list] calls for listing the contents of a
68 /// subdirectory that was moved into the watched directory.
69 final _listSubscriptions = new Set<StreamSubscription<FileSystemEntity>>();
70
71 /// The timer for tracking how long we wait for an initial batch of bogus
72 /// events (see issue 14373).
73 Timer _bogusEventTimer;
74
75 _MacOSDirectoryWatcher(String path)
76 : path = path,
77 _files = new PathSet(path) {
78 _startWatch();
79
80 // Before we're ready to emit events, wait for [_listDir] to complete and
81 // for enough time to elapse that if bogus events (issue 14373) would be
82 // emitted, they will be.
83 //
84 // If we do receive a batch of events, [_onBatch] will ensure that these
85 // futures don't fire and that the directory is re-listed.
86 Future.wait([
87 _listDir(),
88 _waitForBogusEvents()
89 ]).then((_) => _readyCompleter.complete());
90 }
91
92 void close() {
93 if (_watchSubscription != null) _watchSubscription.cancel();
94 if (_initialListSubscription != null) _initialListSubscription.cancel();
95 _watchSubscription = null;
96 _initialListSubscription = null;
97
98 for (var subscription in _listSubscriptions) {
99 subscription.cancel();
100 }
101 _listSubscriptions.clear();
102
103 _eventsController.close();
104 }
105
106 /// The callback that's run when [Directory.watch] emits a batch of events.
107 void _onBatch(List<FileSystemEvent> batch) {
108 // If we get a batch of events before we're ready to begin emitting events,
109 // it's probable that it's a batch of pre-watcher events (see issue 14373).
110 // Ignore those events and re-list the directory.
111 if (!isReady) {
112 // Cancel the timer because bogus events only occur in the first batch, so
113 // we can fire [ready] as soon as we're done listing the directory.
114 _bogusEventTimer.cancel();
115 _listDir().then((_) => _readyCompleter.complete());
116 return;
117 }
118
119 _sortEvents(batch).forEach((path, events) {
120 var canonicalEvent = _canonicalEvent(events);
121 events = canonicalEvent == null ?
122 _eventsBasedOnFileSystem(path) : [canonicalEvent];
123
124 for (var event in events) {
125 if (event is FileSystemCreateEvent) {
126 if (!event.isDirectory) {
127 // If we already know about the file, treat it like a modification.
128 // This can happen if a file is copied on top of an existing one.
129 // We'll see an ADD event for the latter file when from the user's
130 // perspective, the file's contents just changed.
131 var type = _files.contains(path)
132 ? ChangeType.MODIFY
133 : ChangeType.ADD;
134
135 _emitEvent(type, path);
136 _files.add(path);
137 continue;
138 }
139
140 if (_files.containsDir(path)) continue;
141
142 var subscription;
143 subscription = new Directory(path).list(recursive: true)
144 .listen((entity) {
145 if (entity is Directory) return;
146 if (_files.contains(path)) return;
147
148 _emitEvent(ChangeType.ADD, entity.path);
149 _files.add(entity.path);
150 }, onError: (e, stackTrace) {
151 _emitError(e, stackTrace);
152 }, onDone: () {
153 _listSubscriptions.remove(subscription);
154 }, cancelOnError: true);
155 _listSubscriptions.add(subscription);
156 } else if (event is FileSystemModifyEvent) {
157 assert(!event.isDirectory);
158 _emitEvent(ChangeType.MODIFY, path);
159 } else {
160 assert(event is FileSystemDeleteEvent);
161 for (var removedPath in _files.remove(path)) {
162 _emitEvent(ChangeType.REMOVE, removedPath);
163 }
164 }
165 }
166 });
167 }
168
169 /// Sort all the events in a batch into sets based on their path.
170 ///
171 /// A single input event may result in multiple events in the returned map;
172 /// for example, a MOVE event becomes a DELETE event for the source and a
173 /// CREATE event for the destination.
174 ///
175 /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
176 /// contain any events relating to [path].
177 Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
178 var eventsForPaths = {};
179
180 // FSEvents can report past events, including events on the root directory
181 // such as it being created. We want to ignore these. If the directory is
182 // really deleted, that's handled by [_onDone].
183 batch = batch.where((event) => event.path != path).toList();
184
185 // Events within directories that already have events are superfluous; the
186 // directory's full contents will be examined anyway, so we ignore such
187 // events. Emitting them could cause useless or out-of-order events.
188 var directories = unionAll(batch.map((event) {
189 if (!event.isDirectory) return new Set();
190 if (event is! FileSystemMoveEvent) return new Set.from([event.path]);
191 return new Set.from([event.path, event.destination]);
192 }));
193
194 isInModifiedDirectory(path) =>
195 directories.any((dir) => path != dir && path.startsWith(dir));
196
197 addEvent(path, event) {
198 if (isInModifiedDirectory(path)) return;
199 var set = eventsForPaths.putIfAbsent(path, () => new Set());
200 set.add(event);
201 }
202
203 for (var event in batch) {
204 // The Mac OS watcher doesn't emit move events. See issue 14806.
205 assert(event is! FileSystemMoveEvent);
206 addEvent(event.path, event);
207 }
208
209 return eventsForPaths;
210 }
211
212 /// Returns the canonical event from a batch of events on the same path, if
213 /// one exists.
214 ///
215 /// If [batch] doesn't contain any contradictory events (e.g. DELETE and
216 /// CREATE, or events with different values for [isDirectory]), this returns a
217 /// single event that describes what happened to the path in question.
218 ///
219 /// If [batch] does contain contradictory events, this returns `null` to
220 /// indicate that the state of the path on the filesystem should be checked to
221 /// determine what occurred.
222 FileSystemEvent _canonicalEvent(Set<FileSystemEvent> batch) {
223 // An empty batch indicates that we've learned earlier that the batch is
224 // contradictory (e.g. because of a move).
225 if (batch.isEmpty) return null;
226
227 var type = batch.first.type;
228 var isDir = batch.first.isDirectory;
229 var hadModifyEvent = false;
230
231 for (var event in batch.skip(1)) {
232 // If one event reports that the file is a directory and another event
233 // doesn't, that's a contradiction.
234 if (isDir != event.isDirectory) return null;
235
236 // Modify events don't contradict either CREATE or REMOVE events. We can
237 // safely assume the file was modified after a CREATE or before the
238 // REMOVE; otherwise there will also be a REMOVE or CREATE event
239 // (respectively) that will be contradictory.
240 if (event is FileSystemModifyEvent) {
241 hadModifyEvent = true;
242 continue;
243 }
244 assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent);
245
246 // If we previously thought this was a MODIFY, we now consider it to be a
247 // CREATE or REMOVE event. This is safe for the same reason as above.
248 if (type == FileSystemEvent.MODIFY) {
249 type = event.type;
250 continue;
251 }
252
253 // A CREATE event contradicts a REMOVE event and vice versa.
254 assert(type == FileSystemEvent.CREATE || type == FileSystemEvent.DELETE);
255 if (type != event.type) return null;
256 }
257
258 // If we got a CREATE event for a file we already knew about, that comes
259 // from FSEvents reporting an add that happened prior to the watch
260 // beginning. If we also received a MODIFY event, we want to report that,
261 // but not the CREATE.
262 if (type == FileSystemEvent.CREATE && hadModifyEvent &&
263 _files.contains(batch.first.path)) {
264 type = FileSystemEvent.MODIFY;
265 }
266
267 switch (type) {
268 case FileSystemEvent.CREATE:
269 // Issue 16003 means that a CREATE event for a directory can indicate
270 // that the directory was moved and then re-created.
271 // [_eventsBasedOnFileSystem] will handle this correctly by producing a
272 // DELETE event followed by a CREATE event if the directory exists.
273 if (isDir) return null;
274 return new ConstructableFileSystemCreateEvent(batch.first.path, false);
275 case FileSystemEvent.DELETE:
276 return new ConstructableFileSystemDeleteEvent(batch.first.path, isDir);
277 case FileSystemEvent.MODIFY:
278 return new ConstructableFileSystemModifyEvent(
279 batch.first.path, isDir, false);
280 default: throw 'unreachable';
281 }
282 }
283
284 /// Returns one or more events that describe the change between the last known
285 /// state of [path] and its current state on the filesystem.
286 ///
287 /// This returns a list whose order should be reflected in the events emitted
288 /// to the user, unlike the batched events from [Directory.watch]. The
289 /// returned list may be empty, indicating that no changes occurred to [path]
290 /// (probably indicating that it was created and then immediately deleted).
291 List<FileSystemEvent> _eventsBasedOnFileSystem(String path) {
292 var fileExisted = _files.contains(path);
293 var dirExisted = _files.containsDir(path);
294 var fileExists = new File(path).existsSync();
295 var dirExists = new Directory(path).existsSync();
296
297 var events = [];
298 if (fileExisted) {
299 if (fileExists) {
300 events.add(new ConstructableFileSystemModifyEvent(path, false, false));
301 } else {
302 events.add(new ConstructableFileSystemDeleteEvent(path, false));
303 }
304 } else if (dirExisted) {
305 if (dirExists) {
306 // If we got contradictory events for a directory that used to exist and
307 // still exists, we need to rescan the whole thing in case it was
308 // replaced with a different directory.
309 events.add(new ConstructableFileSystemDeleteEvent(path, true));
310 events.add(new ConstructableFileSystemCreateEvent(path, true));
311 } else {
312 events.add(new ConstructableFileSystemDeleteEvent(path, true));
313 }
314 }
315
316 if (!fileExisted && fileExists) {
317 events.add(new ConstructableFileSystemCreateEvent(path, false));
318 } else if (!dirExisted && dirExists) {
319 events.add(new ConstructableFileSystemCreateEvent(path, true));
320 }
321
322 return events;
323 }
324
325 /// The callback that's run when the [Directory.watch] stream is closed.
326 void _onDone() {
327 _watchSubscription = null;
328
329 // If the directory still exists and we're still expecting bogus events,
330 // this is probably issue 14849 rather than a real close event. We should
331 // just restart the watcher.
332 if (!isReady && new Directory(path).existsSync()) {
333 _startWatch();
334 return;
335 }
336
337 // FSEvents can fail to report the contents of the directory being removed
338 // when the directory itself is removed, so we need to manually mark the
339 // files as removed.
340 for (var file in _files.toSet()) {
341 _emitEvent(ChangeType.REMOVE, file);
342 }
343 _files.clear();
344 close();
345 }
346
347 /// Start or restart the underlying [Directory.watch] stream.
348 void _startWatch() {
349 // Batch the FSEvent changes together so that we can dedup events.
350 var innerStream = new Directory(path).watch(recursive: true)
351 .transform(new BatchedStreamTransformer<FileSystemEvent>());
352 _watchSubscription = innerStream.listen(_onBatch,
353 onError: _eventsController.addError,
354 onDone: _onDone);
355 }
356
357 /// Starts or restarts listing the watched directory to get an initial picture
358 /// of its state.
359 Future _listDir() {
360 assert(!isReady);
361 if (_initialListSubscription != null) _initialListSubscription.cancel();
362
363 _files.clear();
364 var completer = new Completer();
365 var stream = new Directory(path).list(recursive: true);
366 _initialListSubscription = stream.listen((entity) {
367 if (entity is! Directory) _files.add(entity.path);
368 },
369 onError: _emitError,
370 onDone: completer.complete,
371 cancelOnError: true);
372 return completer.future;
373 }
374
375 /// Wait 200ms for a batch of bogus events (issue 14373) to come in.
376 ///
377 /// 200ms is short in terms of human interaction, but longer than any Mac OS
378 /// watcher tests take on the bots, so it should be safe to assume that any
379 /// bogus events will be signaled in that time frame.
380 Future _waitForBogusEvents() {
381 var completer = new Completer();
382 _bogusEventTimer = new Timer(
383 new Duration(milliseconds: 200),
384 completer.complete);
385 return completer.future;
386 }
387
388 /// Emit an event with the given [type] and [path].
389 void _emitEvent(ChangeType type, String path) {
390 if (!isReady) return;
391 _eventsController.add(new WatchEvent(type, path));
392 }
393
394 /// Emit an error, then close the watcher.
395 void _emitError(error, StackTrace stackTrace) {
396 _eventsController.addError(error, stackTrace);
397 close();
398 }
399 }
OLDNEW
« no previous file with comments | « watcher/lib/src/directory_watcher/linux.dart ('k') | watcher/lib/src/directory_watcher/polling.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698