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

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

Issue 66163002: Wrap Directory.watch on Mac OS for the watcher package. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: code review 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
« no previous file with comments | « pkg/watcher/lib/src/directory_watcher/linux.dart ('k') | pkg/watcher/lib/src/path_set.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 '../constructable_file_system_event.dart';
11 import '../path_set.dart';
12 import '../utils.dart';
13 import '../watch_event.dart';
14 import 'resubscribable.dart';
15
16 import 'package:path/path.dart' as p;
17
18 /// Uses the FSEvents subsystem to watch for filesystem events.
19 ///
20 /// FSEvents has two main idiosyncrasies that this class works around. First, it
21 /// will occasionally report events that occurred before the filesystem watch
22 /// was initiated. Second, if multiple events happen to the same file in close
23 /// succession, it won't report them in the order they occurred. See issue
24 /// 14373.
25 ///
26 /// This also works around issues 14793, 14806, and 14849 in the implementation
27 /// of [Directory.watch].
28 class MacOSDirectoryWatcher extends ResubscribableDirectoryWatcher {
29 MacOSDirectoryWatcher(String directory)
30 : super(directory, () => new _MacOSDirectoryWatcher(directory));
31 }
32
33 class _MacOSDirectoryWatcher implements ManuallyClosedDirectoryWatcher {
34 final String directory;
35
36 Stream<WatchEvent> get events => _eventsController.stream;
37 final _eventsController = new StreamController<WatchEvent>.broadcast();
38
39 bool get isReady => _readyCompleter.isCompleted;
40
41 Future get ready => _readyCompleter.future;
42 final _readyCompleter = new Completer();
43
44 /// The number of event batches that have been received from
45 /// [Directory.watch].
46 ///
47 /// This is used to determine if the [Directory.watch] stream was falsely
48 /// closed due to issue 14849. A close caused by events in the past will only
49 /// happen before or immediately after the first batch of events.
50 int batches = 0;
51
52 /// The set of files that are known to exist recursively within the watched
53 /// directory.
54 ///
55 /// The state of files on the filesystem is compared against this to determine
56 /// the real change that occurred when working around issue 14373. This is
57 /// also used to emit REMOVE events when subdirectories are moved out of the
58 /// watched directory.
59 final PathSet _files;
60
61 /// The subscription to the stream returned by [Directory.watch].
62 ///
63 /// This is separate from [_subscriptions] because this stream occasionally
64 /// needs to be resubscribed in order to work around issue 14849.
65 StreamSubscription<FileSystemEvent> _watchSubscription;
66
67 /// A set of subscriptions that this watcher subscribes to.
68 ///
69 /// These are gathered together so that they may all be canceled when the
70 /// watcher is closed. This does not include [_watchSubscription].
71 final _subscriptions = new Set<StreamSubscription>();
72
73 _MacOSDirectoryWatcher(String directory)
74 : directory = directory,
75 _files = new PathSet(directory) {
76 _startWatch();
77
78 _listen(new Directory(directory).list(recursive: true),
79 (entity) {
80 if (entity is! Directory) _files.add(entity.path);
81 },
82 onError: _emitError,
83 onDone: _readyCompleter.complete,
84 cancelOnError: true);
85 }
86
87 void close() {
88 for (var subscription in _subscriptions) {
89 subscription.cancel();
90 }
91 _subscriptions.clear();
92 if (_watchSubscription != null) _watchSubscription.cancel();
93 _watchSubscription = null;
94 _eventsController.close();
95 }
96
97 /// The callback that's run when [Directory.watch] emits a batch of events.
98 void _onBatch(List<FileSystemEvent> batch) {
99 batches++;
100
101 _sortEvents(batch).forEach((path, events) {
102 var canonicalEvent = _canonicalEvent(events);
103 events = canonicalEvent == null ?
104 _eventsBasedOnFileSystem(path) : [canonicalEvent];
105
106 for (var event in events) {
107 if (event is FileSystemCreateEvent) {
108 if (!event.isDirectory) {
109 _emitEvent(ChangeType.ADD, path);
110 _files.add(path);
111 continue;
112 }
113
114 _listen(new Directory(path).list(recursive: true), (entity) {
115 if (entity is Directory) return;
116 _emitEvent(ChangeType.ADD, entity.path);
117 _files.add(entity.path);
118 }, onError: _emitError, cancelOnError: true);
119 } else if (event is FileSystemModifyEvent) {
120 assert(!event.isDirectory);
121 _emitEvent(ChangeType.MODIFY, path);
122 } else {
123 assert(event is FileSystemDeleteEvent);
124 for (var removedPath in _files.remove(path)) {
125 _emitEvent(ChangeType.REMOVE, removedPath);
126 }
127 }
128 }
129 });
130 }
131
132 /// Sort all the events in a batch into sets based on their path.
133 ///
134 /// A single input event may result in multiple events in the returned map;
135 /// for example, a MOVE event becomes a DELETE event for the source and a
136 /// CREATE event for the destination.
137 ///
138 /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
139 /// contain any events relating to [directory].
140 Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
141 var eventsForPaths = {};
142
143 // FSEvents can report past events, including events on the root directory
144 // such as it being created. We want to ignore these. If the directory is
145 // really deleted, that's handled by [_onDone].
146 batch = batch.where((event) => event.path != directory).toList();
147
148 // Events within directories that already have events are superfluous; the
149 // directory's full contents will be examined anyway, so we ignore such
150 // events. Emitting them could cause useless or out-of-order events.
151 var directories = unionAll(batch.map((event) {
152 if (!event.isDirectory) return new Set();
153 if (event is! FileSystemMoveEvent) return new Set.from([event.path]);
154 return new Set.from([event.path, event.destination]);
155 }));
156
157 isInModifiedDirectory(path) =>
158 directories.any((dir) => path != dir && path.startsWith(dir));
159
160 addEvent(path, event) {
161 if (isInModifiedDirectory(path)) return;
162 var set = eventsForPaths.putIfAbsent(path, () => new Set());
163 set.add(event);
164 }
165
166 for (var event in batch.where((event) => event is! FileSystemMoveEvent)) {
167 addEvent(event.path, event);
168 }
169
170 // Issue 14806 means that move events can be misleading if they're in the
171 // same batch as another modification of a related file. If they are, we
172 // make the event set empty to ensure we check the state of the filesystem.
173 // Otherwise, treat them as a DELETE followed by an ADD.
174 for (var event in batch.where((event) => event is FileSystemMoveEvent)) {
175 if (eventsForPaths.containsKey(event.path) ||
176 eventsForPaths.containsKey(event.destination)) {
177
178 if (!isInModifiedDirectory(event.path)) {
179 eventsForPaths[event.path] = new Set();
180 }
181 if (!isInModifiedDirectory(event.destination)) {
182 eventsForPaths[event.destination] = new Set();
183 }
184
185 continue;
186 }
187
188 addEvent(event.path, new ConstructableFileSystemDeleteEvent(
189 event.path, event.isDirectory));
190 addEvent(event.destination, new ConstructableFileSystemCreateEvent(
191 event.path, event.isDirectory));
192 }
193
194 return eventsForPaths;
195 }
196
197 /// Returns the canonical event from a batch of events on the same path, if
198 /// one exists.
199 ///
200 /// If [batch] doesn't contain any contradictory events (e.g. DELETE and
201 /// CREATE, or events with different values for [isDirectory]), this returns a
202 /// single event that describes what happened to the path in question.
203 ///
204 /// If [batch] does contain contradictory events, this returns `null` to
205 /// indicate that the state of the path on the filesystem should be checked to
206 /// determine what occurred.
207 FileSystemEvent _canonicalEvent(Set<FileSystemEvent> batch) {
208 // An empty batch indicates that we've learned earlier that the batch is
209 // contradictory (e.g. because of a move).
210 if (batch.isEmpty) return null;
211
212 var type = batch.first.type;
213 var isDir = batch.first.isDirectory;
214
215 for (var event in batch.skip(1)) {
216 // If one event reports that the file is a directory and another event
217 // doesn't, that's a contradiction.
218 if (isDir != event.isDirectory) return null;
219
220 // Modify events don't contradict either CREATE or REMOVE events. We can
221 // safely assume the file was modified after a CREATE or before the
222 // REMOVE; otherwise there will also be a REMOVE or CREATE event
223 // (respectively) that will be contradictory.
224 if (event is FileSystemModifyEvent) continue;
225 assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent);
226
227 // If we previously thought this was a MODIFY, we now consider it to be a
228 // CREATE or REMOVE event. This is safe for the same reason as above.
229 if (type == FileSystemEvent.MODIFY) {
230 type = event.type;
231 continue;
232 }
233
234 // A CREATE event contradicts a REMOVE event and vice versa.
235 assert(type == FileSystemEvent.CREATE || type == FileSystemEvent.DELETE);
236 if (type != event.type) return null;
237 }
238
239 switch (type) {
240 case FileSystemEvent.CREATE:
241 // Issue 14793 means that CREATE events can actually mean DELETE, so we
242 // should always check the filesystem for them.
243 return null;
244 case FileSystemEvent.DELETE:
245 return new ConstructableFileSystemDeleteEvent(batch.first.path, isDir);
246 case FileSystemEvent.MODIFY:
247 return new ConstructableFileSystemModifyEvent(
248 batch.first.path, isDir, false);
249 default: assert(false);
250 }
251 }
252
253 /// Returns one or more events that describe the change between the last known
254 /// state of [path] and its current state on the filesystem.
255 ///
256 /// This returns a list whose order should be reflected in the events emitted
257 /// to the user, unlike the batched events from [Directory.watch]. The
258 /// returned list may be empty, indicating that no changes occurred to [path]
259 /// (probably indicating that it was created and then immediately deleted).
260 List<FileSystemEvent> _eventsBasedOnFileSystem(String path) {
261 var fileExisted = _files.contains(path);
262 var dirExisted = _files.containsDir(path);
263 var fileExists = new File(path).existsSync();
264 var dirExists = new Directory(path).existsSync();
265
266 var events = [];
267 if (fileExisted) {
268 if (fileExists) {
269 events.add(new ConstructableFileSystemModifyEvent(path, false, false));
270 } else {
271 events.add(new ConstructableFileSystemDeleteEvent(path, false));
272 }
273 } else if (dirExisted) {
274 if (dirExists) {
275 // If we got contradictory events for a directory that used to exist and
276 // still exists, we need to rescan the whole thing in case it was
277 // replaced with a different directory.
278 events.add(new ConstructableFileSystemDeleteEvent(path, true));
279 events.add(new ConstructableFileSystemCreateEvent(path, true));
280 } else {
281 events.add(new ConstructableFileSystemDeleteEvent(path, true));
282 }
283 }
284
285 if (!fileExisted && fileExists) {
286 events.add(new ConstructableFileSystemCreateEvent(path, false));
287 } else if (!dirExisted && dirExists) {
288 events.add(new ConstructableFileSystemCreateEvent(path, true));
289 }
290
291 return events;
292 }
293
294 /// The callback that's run when the [Directory.watch] stream is closed.
295 void _onDone() {
296 _watchSubscription = null;
297
298 // If the directory still exists and we haven't seen more than one batch,
299 // this is probably issue 14849 rather than a real close event. We should
300 // just restart the watcher.
301 if (batches < 2 && new Directory(directory).existsSync()) {
302 _startWatch();
303 return;
304 }
305
306 // FSEvents can fail to report the contents of the directory being removed
307 // when the directory itself is removed, so we need to manually mark the as
308 // removed.
309 for (var file in _files.toSet()) {
310 _emitEvent(ChangeType.REMOVE, file);
311 }
312 _files.clear();
313 close();
314 }
315
316 /// Start or restart the underlying [Directory.watch] stream.
317 void _startWatch() {
318 // Batch the FSEvent changes together so that we can dedup events.
319 var innerStream = new Directory(directory).watch(recursive: true).transform(
320 new BatchedStreamTransformer<FileSystemEvent>());
321 _watchSubscription = innerStream.listen(_onBatch,
322 onError: _eventsController.addError,
323 onDone: _onDone);
324 }
325
326 /// Emit an event with the given [type] and [path].
327 void _emitEvent(ChangeType type, String path) {
328 if (!isReady) return;
329
330 // Don't emit ADD events for files that we already know about. Such an event
331 // probably comes from FSEvents reporting an add that happened prior to the
332 // watch beginning.
333 if (type == ChangeType.ADD && _files.contains(path)) return;
334
335 _eventsController.add(new WatchEvent(type, path));
336 }
337
338 /// Emit an error, then close the watcher.
339 void _emitError(error, StackTrace stackTrace) {
340 _eventsController.add(error, stackTrace);
341 close();
342 }
343
344 /// Like [Stream.listen], but automatically adds the subscription to
345 /// [_subscriptions] so that it can be canceled when [close] is called.
346 void _listen(Stream stream, void onData(event), {Function onError,
347 void onDone(), bool cancelOnError}) {
348 var subscription;
349 subscription = stream.listen(onData, onError: onError, onDone: () {
350 _subscriptions.remove(subscription);
351 if (onDone != null) onDone();
352 }, cancelOnError: cancelOnError);
353 _subscriptions.add(subscription);
354 }
355 }
OLDNEW
« no previous file with comments | « pkg/watcher/lib/src/directory_watcher/linux.dart ('k') | pkg/watcher/lib/src/path_set.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698