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

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

Issue 312743002: Use 'Directory.watch' on Windows in pkg/watcher, instead of pooling. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years, 6 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 | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 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. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 library watcher.directory_watcher.mac_os; 5 library watcher.directory_watcher.windows;
6 6
7 import 'dart:async'; 7 import 'dart:async';
8 import 'dart:io'; 8 import 'dart:io';
9 9
10 import 'package:path/path.dart' as p; 10 import 'package:path/path.dart' as p;
11 import 'package:stack_trace/stack_trace.dart'; 11 import 'package:stack_trace/stack_trace.dart';
12 12
13 import '../constructable_file_system_event.dart'; 13 import '../constructable_file_system_event.dart';
14 import '../path_set.dart'; 14 import '../path_set.dart';
15 import '../utils.dart'; 15 import '../utils.dart';
16 import '../watch_event.dart'; 16 import '../watch_event.dart';
17 import 'resubscribable.dart'; 17 import 'resubscribable.dart';
18 18
19 /// Uses the FSEvents subsystem to watch for filesystem events. 19 class WindowsDirectoryWatcher extends ResubscribableDirectoryWatcher {
20 /// 20 WindowsDirectoryWatcher(String directory)
21 /// FSEvents has two main idiosyncrasies that this class works around. First, it 21 : super(directory, () => new _WindowsDirectoryWatcher(directory));
22 /// will occasionally report events that occurred before the filesystem watch
23 /// was initiated. Second, if multiple events happen to the same file in close
24 /// succession, it won't report them in the order they occurred. See issue
25 /// 14373.
26 ///
27 /// This also works around issues 16003 and 14849 in the implementation of
28 /// [Directory.watch].
29 class MacOSDirectoryWatcher extends ResubscribableDirectoryWatcher {
30 // TODO(nweiz): remove these when issue 15042 is fixed.
31 static var logDebugInfo = false;
32 static var _count = 0;
33
34 MacOSDirectoryWatcher(String directory)
35 : super(directory, () => new _MacOSDirectoryWatcher(directory, _count++));
36 } 22 }
37 23
38 class _MacOSDirectoryWatcher implements ManuallyClosedDirectoryWatcher { 24 class _WindowsDirectoryWatcher implements ManuallyClosedDirectoryWatcher {
39 // TODO(nweiz): remove these when issue 15042 is fixed.
40 static var _count = 0;
41 final String _id;
42
43 final String directory; 25 final String directory;
44 26
45 Stream<WatchEvent> get events => _eventsController.stream; 27 Stream<WatchEvent> get events => _eventsController.stream;
46 final _eventsController = new StreamController<WatchEvent>.broadcast(); 28 final _eventsController = new StreamController<WatchEvent>.broadcast();
47 29
48 bool get isReady => _readyCompleter.isCompleted; 30 bool get isReady => _readyCompleter.isCompleted;
49 31
50 Future get ready => _readyCompleter.future; 32 Future get ready => _readyCompleter.future;
51 final _readyCompleter = new Completer(); 33 final _readyCompleter = new Completer();
52 34
53 /// The set of files that are known to exist recursively within the watched 35 /// The set of files that are known to exist recursively within the watched
54 /// directory. 36 /// directory.
55 /// 37 ///
56 /// The state of files on the filesystem is compared against this to determine 38 /// The state of files on the filesystem is compared against this to determine
57 /// the real change that occurred when working around issue 14373. This is 39 /// the real change that occurred. This is also used to emit REMOVE events
58 /// also used to emit REMOVE events when subdirectories are moved out of the 40 /// when subdirectories are moved out of the watched directory.
59 /// watched directory.
60 final PathSet _files; 41 final PathSet _files;
61 42
62 /// The subscription to the stream returned by [Directory.watch]. 43 /// The subscription to the stream returned by [Directory.watch].
63 ///
64 /// This is separate from [_subscriptions] because this stream occasionally
65 /// needs to be resubscribed in order to work around issue 14849.
66 StreamSubscription<FileSystemEvent> _watchSubscription; 44 StreamSubscription<FileSystemEvent> _watchSubscription;
67 45
46 /// The subscription to the stream returned by [Directory.watch] of the
47 /// parent directory of [directory]. This is needed to detect removal of
48 /// [directory], as Windows will lock the directory when watching.
49 StreamSubscription<FileSystemEvent> _parentWatchSubscription;
50
68 /// The subscription to the [Directory.list] call for the initial listing of 51 /// The subscription to the [Directory.list] call for the initial listing of
69 /// the directory to determine its initial state. 52 /// the directory to determine its initial state.
70 StreamSubscription<FileSystemEntity> _initialListSubscription; 53 StreamSubscription<FileSystemEntity> _initialListSubscription;
71 54
72 /// The subscription to the [Directory.list] call for listing the contents of 55 /// The subscription to the [Directory.list] call for listing the contents of
73 /// a subdirectory that was moved into the watched directory. 56 /// a subdirectory that was moved into the watched directory.
74 StreamSubscription<FileSystemEntity> _listSubscription; 57 StreamSubscription<FileSystemEntity> _listSubscription;
75 58
76 /// The timer for tracking how long we wait for an initial batch of bogus 59 _WindowsDirectoryWatcher(String directory)
77 /// events (see issue 14373). 60 : directory = directory, _files = new PathSet(directory) {
78 Timer _bogusEventTimer;
79
80 _MacOSDirectoryWatcher(String directory, int parentId)
81 : directory = directory,
82 _files = new PathSet(directory),
83 _id = "$parentId/${_count++}" {
84 _startWatch(); 61 _startWatch();
85 62 _startParentWatcher();
86 // Before we're ready to emit events, wait for [_listDir] to complete and 63
87 // for enough time to elapse that if bogus events (issue 14373) would be 64 // Before we're ready to emit events, wait for [_listDir] to complete.
88 // emitted, they will be. 65 _listDir().then(_readyCompleter.complete);
89 //
90 // If we do receive a batch of events, [_onBatch] will ensure that these
91 // futures don't fire and that the directory is re-listed.
92 Future.wait([
93 _listDir().then((_) {
94 if (MacOSDirectoryWatcher.logDebugInfo) {
95 print("[$_id] finished initial directory list");
96 }
97 }),
98 _waitForBogusEvents()
99 ]).then((_) {
100 if (MacOSDirectoryWatcher.logDebugInfo) {
101 print("[$_id] watcher is ready, known files:");
102 for (var file in _files.toSet()) {
103 print("[$_id] ${p.relative(file, from: directory)}");
104 }
105 }
106 _readyCompleter.complete();
107 });
108 } 66 }
109 67
110 void close() { 68 void close() {
111 if (MacOSDirectoryWatcher.logDebugInfo) {
112 print("[$_id] watcher is closed\n${new Chain.current().terse}");
113 }
114 if (_watchSubscription != null) _watchSubscription.cancel(); 69 if (_watchSubscription != null) _watchSubscription.cancel();
70 if (_parentWatchSubscription != null) _parentWatchSubscription.cancel();
115 if (_initialListSubscription != null) _initialListSubscription.cancel(); 71 if (_initialListSubscription != null) _initialListSubscription.cancel();
116 if (_listSubscription != null) _listSubscription.cancel(); 72 if (_listSubscription != null) _listSubscription.cancel();
117 _watchSubscription = null; 73 _watchSubscription = null;
74 _parentWatchSubscription = null;
118 _initialListSubscription = null; 75 _initialListSubscription = null;
119 _listSubscription = null; 76 _listSubscription = null;
120 _eventsController.close(); 77 _eventsController.close();
121 } 78 }
122 79
80 void _startParentWatcher() {
81 var absoluteDir = p.absolute(directory);
82 var parent = p.dirname(absoluteDir);
83 if (FileSystemEntity.identicalSync(parent, directory)) return;
Søren Gjesse 2014/06/03 14:14:53 This line deserved a comment.
Anders Johnsen 2014/06/04 08:46:46 Done.
84 _parentWatchSubscription =
85 Chain.track(new Directory(parent).watch(recursive: false))
kasperl 2014/06/03 11:28:09 I'd put the result of calling Chain.track into a s
Anders Johnsen 2014/06/03 11:44:14 Done.
86 .listen((event) {
87 if (!event.path.startsWith(absoluteDir)) {
88 // Only look at events for 'directory'.
89 return;
90 }
91 // Test if the directory is removed.
Søren Gjesse 2014/06/03 14:14:53 Please extend the comment with the information on
Anders Johnsen 2014/06/04 08:46:46 Done.
92 if (FileSystemEntity.typeSync(directory) ==
93 FileSystemEntityType.NOT_FOUND) {
94 for (var path in _files.toSet()) {
95 _emitEvent(ChangeType.REMOVE, path);
96 }
97 _files.clear();
98 close();
99 }
100 }, onError: (error) {
101 // Ignore errors, simply close the stream.
102 _parentWatchSubscription.cancel();
103 _parentWatchSubscription = null;
104 });
105 }
106
123 /// The callback that's run when [Directory.watch] emits a batch of events. 107 /// The callback that's run when [Directory.watch] emits a batch of events.
124 void _onBatch(List<FileSystemEvent> batch) { 108 void _onBatch(List<FileSystemEvent> batch) {
125 if (MacOSDirectoryWatcher.logDebugInfo) { 109 // If we get a batch of events before we're ready to begin emitting events.
126 print("[$_id] ======== batch:");
127 for (var event in batch) {
128 print("[$_id] ${_formatEvent(event)}");
129 }
130
131 print("[$_id] known files:");
132 for (var file in _files.toSet()) {
133 print("[$_id] ${p.relative(file, from: directory)}");
134 }
135 }
136
137 // If we get a batch of events before we're ready to begin emitting events,
138 // it's probable that it's a batch of pre-watcher events (see issue 14373).
139 // Ignore those events and re-list the directory. 110 // Ignore those events and re-list the directory.
Søren Gjesse 2014/06/03 14:14:53 I don't understand the 're-list' part of this comm
Anders Johnsen 2014/06/04 08:46:46 Done.
140 if (!isReady) { 111 if (!isReady) {
141 if (MacOSDirectoryWatcher.logDebugInfo) {
142 print("[$_id] not ready to emit events, re-listing directory");
143 }
144
145 // Cancel the timer because bogus events only occur in the first batch, so
146 // we can fire [ready] as soon as we're done listing the directory.
147 _bogusEventTimer.cancel();
148 _listDir().then((_) {
149 if (MacOSDirectoryWatcher.logDebugInfo) {
150 print("[$_id] watcher is ready, known files:");
151 for (var file in _files.toSet()) {
152 print("[$_id] ${p.relative(file, from: directory)}");
153 }
154 }
155 _readyCompleter.complete();
156 });
157 return; 112 return;
158 } 113 }
159 114
160 _sortEvents(batch).forEach((path, events) { 115 _sortEvents(batch).forEach((path, events) {
161 var relativePath = p.relative(path, from: directory); 116 var relativePath = p.relative(path, from: directory);
162 if (MacOSDirectoryWatcher.logDebugInfo) {
163 print("[$_id] events for $relativePath:");
164 for (var event in events) {
165 print("[$_id] ${_formatEvent(event)}");
166 }
167 }
168 117
169 var canonicalEvent = _canonicalEvent(events); 118 var canonicalEvent = _canonicalEvent(events);
170 events = canonicalEvent == null ? 119 events = canonicalEvent == null ?
171 _eventsBasedOnFileSystem(path) : [canonicalEvent]; 120 _eventsBasedOnFileSystem(path) : [canonicalEvent];
172 if (MacOSDirectoryWatcher.logDebugInfo) {
173 print("[$_id] canonical event for $relativePath: "
174 "${_formatEvent(canonicalEvent)}");
175 print("[$_id] actionable events for $relativePath: "
176 "${events.map(_formatEvent)}");
177 }
178 121
179 for (var event in events) { 122 for (var event in events) {
180 if (event is FileSystemCreateEvent) { 123 if (event is FileSystemCreateEvent) {
181 if (!event.isDirectory) { 124 if (!event.isDirectory) {
182 // Don't emit ADD events for files or directories that we already
183 // know about. Such an event comes from FSEvents reporting an add
184 // that happened prior to the watch beginning.
185 if (_files.contains(path)) continue;
186
187 _emitEvent(ChangeType.ADD, path); 125 _emitEvent(ChangeType.ADD, path);
188 _files.add(path); 126 _files.add(path);
189 continue; 127 continue;
190 } 128 }
191 129
192 if (_files.containsDir(path)) continue; 130 if (_files.containsDir(path)) continue;
Søren Gjesse 2014/06/03 14:14:53 There is a contains check for a created directory,
Anders Johnsen 2014/06/04 08:46:46 Done.
193 131
194 var stream = Chain.track(new Directory(path).list(recursive: true)); 132 var stream = Chain.track(new Directory(path).list(recursive: true));
195 _listSubscription = stream.listen((entity) { 133 _listSubscription = stream.listen((entity) {
196 if (entity is Directory) return; 134 if (entity is Directory) return;
197 if (_files.contains(path)) return; 135 if (_files.contains(path)) return;
198 136
199 _emitEvent(ChangeType.ADD, entity.path); 137 _emitEvent(ChangeType.ADD, entity.path);
200 _files.add(entity.path); 138 _files.add(entity.path);
201 }, onError: (e, stackTrace) { 139 }, onError: (e, stackTrace) {
202 if (MacOSDirectoryWatcher.logDebugInfo) {
203 print("[$_id] got error listing $relativePath: $e");
204 }
205 _emitError(e, stackTrace); 140 _emitError(e, stackTrace);
206 }, cancelOnError: true); 141 }, cancelOnError: true);
207 } else if (event is FileSystemModifyEvent) { 142 } else if (event is FileSystemModifyEvent) {
208 assert(!event.isDirectory); 143 assert(!event.isDirectory);
209 _emitEvent(ChangeType.MODIFY, path); 144 _emitEvent(ChangeType.MODIFY, path);
210 } else { 145 } else {
211 assert(event is FileSystemDeleteEvent); 146 assert(event is FileSystemDeleteEvent);
212 for (var removedPath in _files.remove(path)) { 147 for (var removedPath in _files.remove(path)) {
213 _emitEvent(ChangeType.REMOVE, removedPath); 148 _emitEvent(ChangeType.REMOVE, removedPath);
214 } 149 }
215 } 150 }
216 } 151 }
217 }); 152 });
218
219 if (MacOSDirectoryWatcher.logDebugInfo) {
220 print("[$_id] ======== batch complete");
221 }
222 } 153 }
223 154
224 /// Sort all the events in a batch into sets based on their path. 155 /// Sort all the events in a batch into sets based on their path.
225 /// 156 ///
226 /// A single input event may result in multiple events in the returned map; 157 /// A single input event may result in multiple events in the returned map;
227 /// for example, a MOVE event becomes a DELETE event for the source and a 158 /// for example, a MOVE event becomes a DELETE event for the source and a
228 /// CREATE event for the destination. 159 /// CREATE event for the destination.
229 /// 160 ///
230 /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it 161 /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
231 /// contain any events relating to [directory]. 162 /// contain any events relating to [directory].
232 Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) { 163 Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
233 var eventsForPaths = {}; 164 var eventsForPaths = {};
234 165
235 // FSEvents can report past events, including events on the root directory 166 // FSEvents can report past events, including events on the root directory
Søren Gjesse 2014/06/03 14:14:53 There is some Mac OS related stuff in the comment,
Anders Johnsen 2014/06/04 08:46:46 Yesh, didn't search for FSEvent, only macos.
236 // such as it being created. We want to ignore these. If the directory is 167 // such as it being created. We want to ignore these. If the directory is
237 // really deleted, that's handled by [_onDone]. 168 // really deleted, that's handled by [_onDone].
238 batch = batch.where((event) => event.path != directory).toList(); 169 batch = batch.where((event) => event.path != directory).toList();
239 170
240 // Events within directories that already have events are superfluous; the 171 // Events within directories that already have events are superfluous; the
241 // directory's full contents will be examined anyway, so we ignore such 172 // directory's full contents will be examined anyway, so we ignore such
242 // events. Emitting them could cause useless or out-of-order events. 173 // events. Emitting them could cause useless or out-of-order events.
243 var directories = unionAll(batch.map((event) { 174 var directories = unionAll(batch.map((event) {
244 if (!event.isDirectory) return new Set(); 175 if (!event.isDirectory) return new Set();
245 if (event is! FileSystemMoveEvent) return new Set.from([event.path]); 176 if (event is! FileSystemMoveEvent) return new Set.from([event.path]);
246 return new Set.from([event.path, event.destination]); 177 return new Set.from([event.path, event.destination]);
247 })); 178 }));
248 179
249 isInModifiedDirectory(path) => 180 isInModifiedDirectory(path) =>
250 directories.any((dir) => path != dir && path.startsWith(dir)); 181 directories.any((dir) => path != dir && path.startsWith(dir));
251 182
252 addEvent(path, event) { 183 addEvent(path, event) {
253 if (isInModifiedDirectory(path)) return; 184 if (isInModifiedDirectory(path)) return;
254 var set = eventsForPaths.putIfAbsent(path, () => new Set()); 185 var set = eventsForPaths.putIfAbsent(path, () => new Set());
255 set.add(event); 186 set.add(event);
256 } 187 }
257 188
258 for (var event in batch) { 189 for (var event in batch) {
259 // The Mac OS watcher doesn't emit move events. See issue 14806. 190 if (event is FileSystemMoveEvent) {
260 assert(event is! FileSystemMoveEvent); 191 addEvent((event as FileSystemMoveEvent).destination, event);
kasperl 2014/06/03 11:28:09 Use an extra local rather than a cast.
Anders Johnsen 2014/06/03 11:44:14 Done.
192 }
261 addEvent(event.path, event); 193 addEvent(event.path, event);
262 } 194 }
263 195
264 return eventsForPaths; 196 return eventsForPaths;
265 } 197 }
266 198
267 /// Returns the canonical event from a batch of events on the same path, if 199 /// Returns the canonical event from a batch of events on the same path, if
268 /// one exists. 200 /// one exists.
269 /// 201 ///
270 /// If [batch] doesn't contain any contradictory events (e.g. DELETE and 202 /// If [batch] doesn't contain any contradictory events (e.g. DELETE and
(...skipping 18 matching lines...) Expand all
289 if (isDir != event.isDirectory) return null; 221 if (isDir != event.isDirectory) return null;
290 222
291 // Modify events don't contradict either CREATE or REMOVE events. We can 223 // Modify events don't contradict either CREATE or REMOVE events. We can
292 // safely assume the file was modified after a CREATE or before the 224 // safely assume the file was modified after a CREATE or before the
293 // REMOVE; otherwise there will also be a REMOVE or CREATE event 225 // REMOVE; otherwise there will also be a REMOVE or CREATE event
294 // (respectively) that will be contradictory. 226 // (respectively) that will be contradictory.
295 if (event is FileSystemModifyEvent) { 227 if (event is FileSystemModifyEvent) {
296 hadModifyEvent = true; 228 hadModifyEvent = true;
297 continue; 229 continue;
298 } 230 }
299 assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent); 231 assert(event is FileSystemCreateEvent ||
232 event is FileSystemDeleteEvent ||
233 event is FileSystemMoveEvent);
300 234
301 // If we previously thought this was a MODIFY, we now consider it to be a 235 // If we previously thought this was a MODIFY, we now consider it to be a
302 // CREATE or REMOVE event. This is safe for the same reason as above. 236 // CREATE or REMOVE event. This is safe for the same reason as above.
303 if (type == FileSystemEvent.MODIFY) { 237 if (type == FileSystemEvent.MODIFY) {
304 type = event.type; 238 type = event.type;
305 continue; 239 continue;
306 } 240 }
307 241
308 // A CREATE event contradicts a REMOVE event and vice versa. 242 // A CREATE event contradicts a REMOVE event and vice versa.
309 assert(type == FileSystemEvent.CREATE || type == FileSystemEvent.DELETE); 243 assert(type == FileSystemEvent.CREATE ||
244 type == FileSystemEvent.DELETE ||
245 type == FileSystemEvent.MOVE);
310 if (type != event.type) return null; 246 if (type != event.type) return null;
311 } 247 }
312 248
313 // If we got a CREATE event for a file we already knew about, that comes 249 // If we got a CREATE event for a file we already knew about, that comes
314 // from FSEvents reporting an add that happened prior to the watch 250 // from FSEvents reporting an add that happened prior to the watch
315 // beginning. If we also received a MODIFY event, we want to report that, 251 // beginning. If we also received a MODIFY event, we want to report that,
316 // but not the CREATE. 252 // but not the CREATE.
317 if (type == FileSystemEvent.CREATE && hadModifyEvent && 253 if (type == FileSystemEvent.CREATE && hadModifyEvent &&
318 _files.contains(batch.first.path)) { 254 _files.contains(batch.first.path)) {
319 type = FileSystemEvent.MODIFY; 255 type = FileSystemEvent.MODIFY;
320 } 256 }
321 257
322 switch (type) { 258 switch (type) {
323 case FileSystemEvent.CREATE: 259 case FileSystemEvent.CREATE:
324 // Issue 16003 means that a CREATE event for a directory can indicate 260 return new ConstructableFileSystemCreateEvent(batch.first.path, isDir);
325 // that the directory was moved and then re-created.
326 // [_eventsBasedOnFileSystem] will handle this correctly by producing a
327 // DELETE event followed by a CREATE event if the directory exists.
328 if (isDir) return null;
329 return new ConstructableFileSystemCreateEvent(batch.first.path, false);
330 case FileSystemEvent.DELETE: 261 case FileSystemEvent.DELETE:
331 return new ConstructableFileSystemDeleteEvent(batch.first.path, isDir); 262 return new ConstructableFileSystemDeleteEvent(batch.first.path, isDir);
332 case FileSystemEvent.MODIFY: 263 case FileSystemEvent.MODIFY:
333 return new ConstructableFileSystemModifyEvent( 264 return new ConstructableFileSystemModifyEvent(
334 batch.first.path, isDir, false); 265 batch.first.path, isDir, false);
266 case FileSystemEvent.MOVE:
267 return null;
335 default: assert(false); 268 default: assert(false);
336 } 269 }
337 } 270 }
338 271
339 /// Returns one or more events that describe the change between the last known 272 /// Returns one or more events that describe the change between the last known
340 /// state of [path] and its current state on the filesystem. 273 /// state of [path] and its current state on the filesystem.
341 /// 274 ///
342 /// This returns a list whose order should be reflected in the events emitted 275 /// This returns a list whose order should be reflected in the events emitted
343 /// to the user, unlike the batched events from [Directory.watch]. The 276 /// to the user, unlike the batched events from [Directory.watch]. The
344 /// returned list may be empty, indicating that no changes occurred to [path] 277 /// returned list may be empty, indicating that no changes occurred to [path]
345 /// (probably indicating that it was created and then immediately deleted). 278 /// (probably indicating that it was created and then immediately deleted).
346 List<FileSystemEvent> _eventsBasedOnFileSystem(String path) { 279 List<FileSystemEvent> _eventsBasedOnFileSystem(String path) {
347 var fileExisted = _files.contains(path); 280 var fileExisted = _files.contains(path);
348 var dirExisted = _files.containsDir(path); 281 var dirExisted = _files.containsDir(path);
349 var fileExists = new File(path).existsSync(); 282 var fileExists = new File(path).existsSync();
350 var dirExists = new Directory(path).existsSync(); 283 var dirExists = new Directory(path).existsSync();
351 284
352 if (MacOSDirectoryWatcher.logDebugInfo) {
353 print("[$_id] checking file system for "
354 "${p.relative(path, from: directory)}");
355 print("[$_id] file existed: $fileExisted");
356 print("[$_id] dir existed: $dirExisted");
357 print("[$_id] file exists: $fileExists");
358 print("[$_id] dir exists: $dirExists");
359 }
360
361 var events = []; 285 var events = [];
362 if (fileExisted) { 286 if (fileExisted) {
363 if (fileExists) { 287 if (fileExists) {
364 events.add(new ConstructableFileSystemModifyEvent(path, false, false)); 288 events.add(new ConstructableFileSystemModifyEvent(path, false, false));
365 } else { 289 } else {
366 events.add(new ConstructableFileSystemDeleteEvent(path, false)); 290 events.add(new ConstructableFileSystemDeleteEvent(path, false));
367 } 291 }
368 } else if (dirExisted) { 292 } else if (dirExisted) {
369 if (dirExists) { 293 if (dirExists) {
370 // If we got contradictory events for a directory that used to exist and 294 // If we got contradictory events for a directory that used to exist and
371 // still exists, we need to rescan the whole thing in case it was 295 // still exists, we need to rescan the whole thing in case it was
372 // replaced with a different directory. 296 // replaced with a different directory.
373 events.add(new ConstructableFileSystemDeleteEvent(path, true)); 297 events.add(new ConstructableFileSystemDeleteEvent(path, true));
374 events.add(new ConstructableFileSystemCreateEvent(path, true)); 298 events.add(new ConstructableFileSystemCreateEvent(path, true));
375 } else { 299 } else {
376 events.add(new ConstructableFileSystemDeleteEvent(path, true)); 300 events.add(new ConstructableFileSystemDeleteEvent(path, true));
377 } 301 }
378 } 302 }
379 303
380 if (!fileExisted && fileExists) { 304 if (!fileExisted && fileExists) {
381 events.add(new ConstructableFileSystemCreateEvent(path, false)); 305 events.add(new ConstructableFileSystemCreateEvent(path, false));
382 } else if (!dirExisted && dirExists) { 306 } else if (!dirExisted && dirExists) {
383 events.add(new ConstructableFileSystemCreateEvent(path, true)); 307 events.add(new ConstructableFileSystemCreateEvent(path, true));
384 } 308 }
385 309
386 return events; 310 return events;
387 } 311 }
388 312
389 /// The callback that's run when the [Directory.watch] stream is closed. 313 /// The callback that's run when the [Directory.watch] stream is closed.
314 /// Note that this is unlikely to happen on Windows, unless the system itself
315 /// closes the handle.
390 void _onDone() { 316 void _onDone() {
391 if (MacOSDirectoryWatcher.logDebugInfo) print("[$_id] stream closed");
392
393 _watchSubscription = null; 317 _watchSubscription = null;
394 318
395 // If the directory still exists and we're still expecting bogus events, 319 // Emit remove-events for any remaining files.
396 // this is probably issue 14849 rather than a real close event. We should
397 // just restart the watcher.
398 if (!isReady && new Directory(directory).existsSync()) {
399 if (MacOSDirectoryWatcher.logDebugInfo) {
400 print("[$_id] fake closure (issue 14849), re-opening stream");
401 }
402 _startWatch();
403 return;
404 }
405
406 // FSEvents can fail to report the contents of the directory being removed
407 // when the directory itself is removed, so we need to manually mark the
408 // files as removed.
409 for (var file in _files.toSet()) { 320 for (var file in _files.toSet()) {
410 _emitEvent(ChangeType.REMOVE, file); 321 _emitEvent(ChangeType.REMOVE, file);
411 } 322 }
412 _files.clear(); 323 _files.clear();
413 close(); 324 close();
414 } 325 }
415 326
416 /// Start or restart the underlying [Directory.watch] stream. 327 /// Start or restart the underlying [Directory.watch] stream.
417 void _startWatch() { 328 void _startWatch() {
418 // Batch the FSEvent changes together so that we can dedup events. 329 // Batch the FSEvent changes together so that we can dedup events.
419 var innerStream = 330 var innerStream =
420 Chain.track(new Directory(directory).watch(recursive: true)) 331 Chain.track(new Directory(directory).watch(recursive: true))
421 .transform(new BatchedStreamTransformer<FileSystemEvent>()); 332 .transform(new BatchedStreamTransformer<FileSystemEvent>());
422 _watchSubscription = innerStream.listen(_onBatch, 333 _watchSubscription = innerStream.listen(_onBatch,
423 onError: _eventsController.addError, 334 onError: _eventsController.addError,
424 onDone: _onDone); 335 onDone: _onDone);
425 } 336 }
426 337
427 /// Starts or restarts listing the watched directory to get an initial picture 338 /// Starts or restarts listing the watched directory to get an initial picture
428 /// of its state. 339 /// of its state.
429 Future _listDir() { 340 Future _listDir() {
430 assert(!isReady); 341 assert(!isReady);
431 if (_initialListSubscription != null) _initialListSubscription.cancel(); 342 if (_initialListSubscription != null) _initialListSubscription.cancel();
432 343
433 _files.clear(); 344 _files.clear();
434 var completer = new Completer(); 345 var completer = new Completer();
435 var stream = Chain.track(new Directory(directory).list(recursive: true)); 346 var stream = Chain.track(new Directory(directory).list(recursive: true));
436 _initialListSubscription = stream.listen((entity) { 347 _initialListSubscription = stream.listen((entity) {
437 if (entity is! Directory) _files.add(entity.path); 348 if (entity is! Directory) _files.add(entity.path);
kasperl 2014/06/03 11:28:09 Weird indentation.
Anders Johnsen 2014/06/03 11:44:14 Done.
438 }, 349 },
439 onError: _emitError, 350 onError: _emitError,
440 onDone: completer.complete, 351 onDone: completer.complete,
441 cancelOnError: true); 352 cancelOnError: true);
442 return completer.future; 353 return completer.future;
443 } 354 }
444 355
445 /// Wait 200ms for a batch of bogus events (issue 14373) to come in.
446 ///
447 /// 200ms is short in terms of human interaction, but longer than any Mac OS
448 /// watcher tests take on the bots, so it should be safe to assume that any
449 /// bogus events will be signaled in that time frame.
450 Future _waitForBogusEvents() {
451 var completer = new Completer();
452 _bogusEventTimer = new Timer(
453 new Duration(milliseconds: 200),
454 completer.complete);
455 return completer.future;
456 }
457
458 /// Emit an event with the given [type] and [path]. 356 /// Emit an event with the given [type] and [path].
459 void _emitEvent(ChangeType type, String path) { 357 void _emitEvent(ChangeType type, String path) {
460 if (!isReady) return; 358 if (!isReady) return;
461 359
462 if (MacOSDirectoryWatcher.logDebugInfo) {
463 print("[$_id] emitting $type ${p.relative(path, from: directory)}");
464 }
465
466 _eventsController.add(new WatchEvent(type, path)); 360 _eventsController.add(new WatchEvent(type, path));
467 } 361 }
468 362
469 /// Emit an error, then close the watcher. 363 /// Emit an error, then close the watcher.
470 void _emitError(error, StackTrace stackTrace) { 364 void _emitError(error, StackTrace stackTrace) {
471 if (MacOSDirectoryWatcher.logDebugInfo) {
472 print("[$_id] emitting error: $error\n" +
473 "${new Chain.forTrace(stackTrace).terse}");
474 }
475 _eventsController.addError(error, stackTrace); 365 _eventsController.addError(error, stackTrace);
476 close(); 366 close();
477 } 367 }
478
479 // TODO(nweiz): remove this when issue 15042 is fixed.
480 /// Return a human-friendly string representation of [event].
481 String _formatEvent(FileSystemEvent event) {
482 if (event == null) return 'null';
483
484 var path = p.relative(event.path, from: directory);
485 var type = event.isDirectory ? 'directory' : 'file';
486 if (event is FileSystemCreateEvent) {
487 return "create $type $path";
488 } else if (event is FileSystemDeleteEvent) {
489 return "delete $type $path";
490 } else if (event is FileSystemModifyEvent) {
491 return "modify $type $path";
492 } else if (event is FileSystemMoveEvent) {
493 return "move $type $path to "
494 "${p.relative(event.destination, from: directory)}";
495 }
496 }
497 } 368 }
369
kasperl 2014/06/03 11:28:09 Not needed.
Anders Johnsen 2014/06/03 11:44:14 Done.
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698