Backport fixes from master to 0.9.7+x.
diff --git a/pkgs/watcher/benchmark/path_set.dart b/pkgs/watcher/benchmark/path_set.dart new file mode 100644 index 0000000..aba3ed7 --- /dev/null +++ b/pkgs/watcher/benchmark/path_set.dart
@@ -0,0 +1,147 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Benchmarks for the PathSet class. +library watcher.benchmark.path_set; + +import 'dart:io'; +import 'dart:math' as math; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:path/path.dart' as p; + +import 'package:watcher/src/path_set.dart'; + +final String root = Platform.isWindows ? r"C:\root" : "/root"; + +/// Base class for benchmarks on [PathSet]. +abstract class PathSetBenchmark extends BenchmarkBase { + PathSetBenchmark(String method) : super("PathSet.$method"); + + final PathSet pathSet = new PathSet(root); + + /// Use a fixed [Random] with a constant seed to ensure the tests are + /// deterministic. + final math.Random random = new math.Random(1234); + + /// Walks over a virtual directory [depth] levels deep invoking [callback] + /// for each "file". + /// + /// Each virtual directory contains ten entries: either subdirectories or + /// files. + void walkTree(int depth, callback(String path)) { + recurse(path, remainingDepth) { + for (var i = 0; i < 10; i++) { + var padded = i.toString().padLeft(2, '0'); + if (remainingDepth == 0) { + callback(p.join(path, "file_$padded.txt")); + } else { + var subdir = p.join(path, "subdirectory_$padded"); + recurse(subdir, remainingDepth - 1); + } + } + } + + recurse(root, depth); + } +} + +class AddBenchmark extends PathSetBenchmark { + AddBenchmark() : super("add()"); + + final List<String> paths = []; + + void setup() { + // Make a bunch of paths in about the same order we expect to get them from + // Directory.list(). + walkTree(3, paths.add); + } + + void run() { + for (var path in paths) pathSet.add(path); + } +} + +class ContainsBenchmark extends PathSetBenchmark { + ContainsBenchmark() : super("contains()"); + + final List<String> paths = []; + + void setup() { + // Add a bunch of paths to the set. + walkTree(3, (path) { + pathSet.add(path); + paths.add(path); + }); + + // Add some non-existent paths to test the false case. + for (var i = 0; i < 100; i++) { + paths.addAll([ + "/nope", + "/root/nope", + "/root/subdirectory_04/nope", + "/root/subdirectory_04/subdirectory_04/nope", + "/root/subdirectory_04/subdirectory_04/subdirectory_04/nope", + "/root/subdirectory_04/subdirectory_04/subdirectory_04/nope/file_04.txt", + ]); + } + } + + void run() { + var contained = 0; + for (var path in paths) { + if (pathSet.contains(path)) contained++; + } + + if (contained != 10000) throw "Wrong result: $contained"; + } +} + +class PathsBenchmark extends PathSetBenchmark { + PathsBenchmark() : super("toSet()"); + + void setup() { + walkTree(3, pathSet.add); + } + + void run() { + var count = 0; + for (var _ in pathSet.paths) { + count++; + } + + if (count != 10000) throw "Wrong result: $count"; + } +} + +class RemoveBenchmark extends PathSetBenchmark { + RemoveBenchmark() : super("remove()"); + + final List<String> paths = []; + + void setup() { + // Make a bunch of paths. Do this here so that we don't spend benchmarked + // time synthesizing paths. + walkTree(3, (path) { + pathSet.add(path); + paths.add(path); + }); + + // Shuffle the paths so that we delete them in a random order that + // hopefully mimics real-world file system usage. Do the shuffling here so + // that we don't spend benchmarked time shuffling. + paths.shuffle(random); + } + + void run() { + for (var path in paths) pathSet.remove(path); + } +} + +main() { + new AddBenchmark().report(); + new ContainsBenchmark().report(); + new PathsBenchmark().report(); + new RemoveBenchmark().report(); +}
diff --git a/pkgs/watcher/lib/src/async_queue.dart b/pkgs/watcher/lib/src/async_queue.dart index b83493d..adf6671 100644 --- a/pkgs/watcher/lib/src/async_queue.dart +++ b/pkgs/watcher/lib/src/async_queue.dart
@@ -2,8 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library watcher.async_queue; - import 'dart:async'; import 'dart:collection';
diff --git a/pkgs/watcher/lib/src/constructable_file_system_event.dart b/pkgs/watcher/lib/src/constructable_file_system_event.dart index d00a1dc..63b51c1 100644 --- a/pkgs/watcher/lib/src/constructable_file_system_event.dart +++ b/pkgs/watcher/lib/src/constructable_file_system_event.dart
@@ -2,8 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library watcher.constructable_file_system_event; - import 'dart:io'; abstract class _ConstructableFileSystemEvent implements FileSystemEvent {
diff --git a/pkgs/watcher/lib/src/directory_watcher.dart b/pkgs/watcher/lib/src/directory_watcher.dart index 8283785..6beebd0 100644 --- a/pkgs/watcher/lib/src/directory_watcher.dart +++ b/pkgs/watcher/lib/src/directory_watcher.dart
@@ -2,11 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library watcher.directory_watcher; - import 'dart:io'; -import 'watch_event.dart'; import '../watcher.dart'; import 'directory_watcher/linux.dart'; import 'directory_watcher/mac_os.dart';
diff --git a/pkgs/watcher/lib/src/directory_watcher/linux.dart b/pkgs/watcher/lib/src/directory_watcher/linux.dart index a747839..f327bb0 100644 --- a/pkgs/watcher/lib/src/directory_watcher/linux.dart +++ b/pkgs/watcher/lib/src/directory_watcher/linux.dart
@@ -2,12 +2,13 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library watcher.directory_watcher.linux; - import 'dart:async'; import 'dart:io'; +import 'package:async/async.dart'; + import '../directory_watcher.dart'; +import '../path_set.dart'; import '../resubscribable.dart'; import '../utils.dart'; import '../watch_event.dart'; @@ -32,8 +33,8 @@ class _LinuxDirectoryWatcher implements DirectoryWatcher, ManuallyClosedWatcher { - String get directory => path; - final String path; + String get directory => _files.root; + String get path => _files.root; Stream<WatchEvent> get events => _eventsController.stream; final _eventsController = new StreamController<WatchEvent>.broadcast(); @@ -43,15 +44,17 @@ Future get ready => _readyCompleter.future; final _readyCompleter = new Completer(); - /// The last known state for each entry in this directory. - /// - /// The keys in this map are the paths to the directory entries; the values - /// are [_EntryState]s indicating whether the entries are files or - /// directories. - final _entries = new Map<String, _EntryState>(); + /// A stream group for the [Directory.watch] events of [path] and all its + /// subdirectories. + var _nativeEvents = new StreamGroup<FileSystemEvent>(); - /// The watchers for subdirectories of [directory]. - final _subWatchers = new Map<String, _LinuxDirectoryWatcher>(); + /// All known files recursively within [path]. + final PathSet _files; + + /// [Directory.watch] streams for [path]'s subdirectories, indexed by name. + /// + /// A stream is in this map if and only if it's also in [_nativeEvents]. + final _subdirStreams = <String, Stream<FileSystemEvent>>{}; /// A set of all subscriptions that this watcher subscribes to. /// @@ -59,93 +62,51 @@ /// watcher is closed. final _subscriptions = new Set<StreamSubscription>(); - _LinuxDirectoryWatcher(this.path) { + _LinuxDirectoryWatcher(String path) + : _files = new PathSet(path) { + _nativeEvents.add(new Directory(path).watch().transform( + new StreamTransformer.fromHandlers(handleDone: (sink) { + // Once the root directory is deleted, no more new subdirectories will be + // watched. + _nativeEvents.close(); + sink.close(); + }))); + // Batch the inotify changes together so that we can dedup events. - var innerStream = new Directory(path).watch() + var innerStream = _nativeEvents.stream .transform(new BatchedStreamTransformer<FileSystemEvent>()); _listen(innerStream, _onBatch, onError: _eventsController.addError, onDone: _onDone); - _listen(new Directory(path).list(), (entity) { - _entries[entity.path] = new _EntryState(entity is Directory); - if (entity is! Directory) return; - _watchSubdir(entity.path); + _listen(new Directory(path).list(recursive: true), (entity) { + if (entity is Directory) { + _watchSubdir(entity.path); + } else { + _files.add(entity.path); + } }, onError: (error, stackTrace) { _eventsController.addError(error, stackTrace); close(); }, onDone: () { - _waitUntilReady().then((_) => _readyCompleter.complete()); + _readyCompleter.complete(); }, cancelOnError: true); } - /// Returns a [Future] that completes once all the subdirectory watchers are - /// fully initialized. - Future _waitUntilReady() { - return Future.wait(_subWatchers.values.map((watcher) => watcher.ready)) - .then((_) { - if (_subWatchers.values.every((watcher) => watcher.isReady)) return null; - return _waitUntilReady(); - }); - } - void close() { for (var subscription in _subscriptions) { subscription.cancel(); } - for (var watcher in _subWatchers.values) { - watcher.close(); - } - _subWatchers.clear(); _subscriptions.clear(); + _subdirStreams.clear(); + _files.clear(); + _nativeEvents.close(); _eventsController.close(); } - /// Returns all files (not directories) that this watcher knows of are - /// recursively in the watched directory. - Set<String> get _allFiles { - var files = new Set<String>(); - _getAllFiles(files); - return files; - } - - /// Helper function for [_allFiles]. - /// - /// Adds all files that this watcher knows of to [files]. - void _getAllFiles(Set<String> files) { - files.addAll(_entries.keys - .where((path) => _entries[path] == _EntryState.FILE).toSet()); - for (var watcher in _subWatchers.values) { - watcher._getAllFiles(files); - } - } - /// Watch a subdirectory of [directory] for changes. - /// - /// If the subdirectory was added after [this] began emitting events, its - /// contents will be emitted as ADD events. void _watchSubdir(String path) { - if (_subWatchers.containsKey(path)) return; - var watcher = new _LinuxDirectoryWatcher(path); - _subWatchers[path] = watcher; - - // TODO(nweiz): Catch any errors here that indicate that the directory in - // question doesn't exist and silently stop watching it instead of - // propagating the errors. - _listen(watcher.events, (event) { - if (isReady) _eventsController.add(event); - }, onError: (error, stackTrace) { - _eventsController.addError(error, stackTrace); - close(); - }, onDone: () { - if (_subWatchers[path] == watcher) _subWatchers.remove(path); - - // It's possible that a directory was removed and recreated very quickly. - // If so, make sure we're still watching it. - if (new Directory(path).existsSync()) _watchSubdir(path); - }); - // TODO(nweiz): Right now it's possible for the watcher to emit an event for // a file before the directory list is complete. This could lead to the user // seeing a MODIFY or REMOVE event for a file before they see an ADD event, @@ -157,96 +118,110 @@ // top-level clients such as barback as well, and could be implemented with // a wrapper similar to how listening/canceling works now. - // If a directory is added after we're finished with the initial scan, emit - // an event for each entry in it. This gives the user consistently gets an - // event for every new file. - watcher.ready.then((_) { - if (!isReady || _eventsController.isClosed) return; - _listen(new Directory(path).list(recursive: true), (entry) { - if (entry is Directory) return; - _eventsController.add(new WatchEvent(ChangeType.ADD, entry.path)); - }, onError: (error, stackTrace) { - // Ignore an exception caused by the dir not existing. It's fine if it - // was added and then quickly removed. - if (error is FileSystemException) return; - - _eventsController.addError(error, stackTrace); - close(); - }, cancelOnError: true); - }); + // TODO(nweiz): Catch any errors here that indicate that the directory in + // question doesn't exist and silently stop watching it instead of + // propagating the errors. + var stream = new Directory(path).watch(); + _subdirStreams[path] = stream; + _nativeEvents.add(stream); } /// The callback that's run when a batch of changes comes in. void _onBatch(List<FileSystemEvent> batch) { - var changedEntries = new Set<String>(); - var oldEntries = new Map.from(_entries); + var files = new Set(); + var dirs = new Set(); + var changed = new Set(); // inotify event batches are ordered by occurrence, so we treat them as a - // log of what happened to a file. + // log of what happened to a file. We only emit events based on the + // difference between the state before the batch and the state after it, not + // the intermediate state. for (var event in batch) { // If the watched directory is deleted or moved, we'll get a deletion // event for it. Ignore it; we handle closing [this] when the underlying // stream is closed. if (event.path == path) continue; - changedEntries.add(event.path); + changed.add(event.path); if (event is FileSystemMoveEvent) { - changedEntries.add(event.destination); - _changeEntryState(event.path, ChangeType.REMOVE, event.isDirectory); - _changeEntryState(event.destination, ChangeType.ADD, event.isDirectory); - } else { - _changeEntryState(event.path, _changeTypeFor(event), event.isDirectory); - } - } + files.remove(event.path); + dirs.remove(event.path); - for (var path in changedEntries) { - emitEvent(ChangeType type) { - if (isReady) _eventsController.add(new WatchEvent(type, path)); - } - - var oldState = oldEntries[path]; - var newState = _entries[path]; - - if (oldState != _EntryState.FILE && newState == _EntryState.FILE) { - emitEvent(ChangeType.ADD); - } else if (oldState == _EntryState.FILE && newState == _EntryState.FILE) { - emitEvent(ChangeType.MODIFY); - } else if (oldState == _EntryState.FILE && newState != _EntryState.FILE) { - emitEvent(ChangeType.REMOVE); - } - - if (oldState == _EntryState.DIRECTORY) { - var watcher = _subWatchers.remove(path); - if (watcher == null) continue; - for (var path in watcher._allFiles) { - _eventsController.add(new WatchEvent(ChangeType.REMOVE, path)); + changed.add(event.destination); + if (event.isDirectory) { + files.remove(event.destination); + dirs.add(event.destination); + } else { + files.add(event.destination); + dirs.remove(event.destination); } - watcher.close(); + } else if (event is FileSystemDeleteEvent) { + files.remove(event.path); + dirs.remove(event.path); + } else if (event.isDirectory) { + files.remove(event.path); + dirs.add(event.path); + } else { + files.add(event.path); + dirs.remove(event.path); } + } - if (newState == _EntryState.DIRECTORY) _watchSubdir(path); + _applyChanges(files, dirs, changed); + } + + /// Applies the net changes computed for a batch. + /// + /// The [files] and [dirs] sets contain the files and directories that now + /// exist, respectively. The [changed] set contains all files and directories + /// that have changed (including being removed), and so is a superset of + /// [files] and [dirs]. + void _applyChanges(Set<String> files, Set<String> dirs, Set<String> changed) { + for (var path in changed) { + var stream = _subdirStreams.remove(path); + if (stream != null) _nativeEvents.add(stream); + + // Unless [path] was a file and still is, emit REMOVE events for it or its + // contents, + if (files.contains(path) && _files.contains(path)) continue; + for (var file in _files.remove(path)) { + _emit(ChangeType.REMOVE, file); + } + } + + for (var file in files) { + if (_files.contains(file)) { + _emit(ChangeType.MODIFY, file); + } else { + _emit(ChangeType.ADD, file); + _files.add(file); + } + } + + for (var dir in dirs) { + _watchSubdir(dir); + _addSubdir(dir); } } - /// Changes the known state of the entry at [path] based on [change] and - /// [isDir]. - void _changeEntryState(String path, ChangeType change, bool isDir) { - if (change == ChangeType.ADD || change == ChangeType.MODIFY) { - _entries[path] = new _EntryState(isDir); - } else { - assert(change == ChangeType.REMOVE); - _entries.remove(path); - } - } + /// Emits [ChangeType.ADD] events for the recursive contents of [path]. + void _addSubdir(String path) { + _listen(new Directory(path).list(recursive: true), (entity) { + if (entity is Directory) { + _watchSubdir(entity.path); + } else { + _files.add(entity.path); + _emit(ChangeType.ADD, entity.path); + } + }, onError: (error, stackTrace) { + // Ignore an exception caused by the dir not existing. It's fine if it + // was added and then quickly removed. + if (error is FileSystemException) return; - /// Determines the [ChangeType] associated with [event]. - ChangeType _changeTypeFor(FileSystemEvent event) { - if (event is FileSystemDeleteEvent) return ChangeType.REMOVE; - if (event is FileSystemCreateEvent) return ChangeType.ADD; - - assert(event is FileSystemModifyEvent); - return ChangeType.MODIFY; + _eventsController.addError(error, stackTrace); + close(); + }, cancelOnError: true); } /// Handles the underlying event stream closing, indicating that the directory @@ -254,28 +229,23 @@ void _onDone() { // Most of the time when a directory is removed, its contents will get // individual REMOVE events before the watch stream is closed -- in that - // case, [_entries] will be empty here. However, if the directory's removal - // is caused by a MOVE, we need to manually emit events. + // case, [_files] will be empty here. However, if the directory's removal is + // caused by a MOVE, we need to manually emit events. if (isReady) { - _entries.forEach((path, state) { - if (state == _EntryState.DIRECTORY) return; - _eventsController.add(new WatchEvent(ChangeType.REMOVE, path)); - }); + for (var file in _files.paths) { + _emit(ChangeType.REMOVE, file); + } } - // The parent directory often gets a close event before the subdirectories - // are done emitting events. We wait for them to finish before we close - // [events] so that we can be sure to emit a remove event for every file - // that used to exist. - Future.wait(_subWatchers.values.map((watcher) { - try { - return watcher.events.toList(); - } on StateError catch (_) { - // It's possible that [watcher.events] is closed but the onDone event - // hasn't reached us yet. It's fine if so. - return new Future.value(); - } - })).then((_) => close()); + close(); + } + + /// Emits a [WatchEvent] with [type] and [path] if this watcher is in a state + /// to emit events. + void _emit(ChangeType type, String path) { + if (!isReady) return; + if (_eventsController.isClosed) return; + _eventsController.add(new WatchEvent(type, path)); } /// Like [Stream.listen], but automatically adds the subscription to @@ -290,22 +260,3 @@ _subscriptions.add(subscription); } } - -/// An enum for the possible states of entries in a watched directory. -class _EntryState { - final String _name; - - /// The entry is a file. - static const FILE = const _EntryState._("file"); - - /// The entry is a directory. - static const DIRECTORY = const _EntryState._("directory"); - - const _EntryState._(this._name); - - /// Returns [DIRECTORY] if [isDir] is true, and [FILE] otherwise. - factory _EntryState(bool isDir) => - isDir ? _EntryState.DIRECTORY : _EntryState.FILE; - - String toString() => _name; -}
diff --git a/pkgs/watcher/lib/src/directory_watcher/mac_os.dart b/pkgs/watcher/lib/src/directory_watcher/mac_os.dart index 487225e..8a17e2e 100644 --- a/pkgs/watcher/lib/src/directory_watcher/mac_os.dart +++ b/pkgs/watcher/lib/src/directory_watcher/mac_os.dart
@@ -2,8 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library watcher.directory_watcher.mac_os; - import 'dart:async'; import 'dart:io'; @@ -58,7 +56,7 @@ /// /// This is separate from [_subscriptions] because this stream occasionally /// needs to be resubscribed in order to work around issue 14849. - StreamSubscription<FileSystemEvent> _watchSubscription; + StreamSubscription<List<FileSystemEvent>> _watchSubscription; /// The subscription to the [Directory.list] call for the initial listing of /// the directory to determine its initial state. @@ -116,9 +114,9 @@ return; } - _sortEvents(batch).forEach((path, events) { - var canonicalEvent = _canonicalEvent(events); - events = canonicalEvent == null ? + _sortEvents(batch).forEach((path, eventSet) { + var canonicalEvent = _canonicalEvent(eventSet); + var events = canonicalEvent == null ? _eventsBasedOnFileSystem(path) : [canonicalEvent]; for (var event in events) { @@ -139,7 +137,7 @@ if (_files.containsDir(path)) continue; - var subscription; + StreamSubscription<FileSystemEntity> subscription; subscription = new Directory(path).list(recursive: true) .listen((entity) { if (entity is Directory) return; @@ -175,7 +173,7 @@ /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it /// contain any events relating to [path]. Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) { - var eventsForPaths = {}; + var eventsForPaths = <String, Set>{}; // FSEvents can report past events, including events on the root directory // such as it being created. We want to ignore these. If the directory is @@ -187,8 +185,10 @@ // events. Emitting them could cause useless or out-of-order events. var directories = unionAll(batch.map((event) { if (!event.isDirectory) return new Set(); - if (event is! FileSystemMoveEvent) return new Set.from([event.path]); - return new Set.from([event.path, event.destination]); + if (event is FileSystemMoveEvent) { + return new Set.from([event.path, event.destination]); + } + return new Set.from([event.path]); })); isInModifiedDirectory(path) => @@ -294,7 +294,7 @@ var fileExists = new File(path).existsSync(); var dirExists = new Directory(path).existsSync(); - var events = []; + var events = <FileSystemEvent>[]; if (fileExisted) { if (fileExists) { events.add(new ConstructableFileSystemModifyEvent(path, false, false)); @@ -337,7 +337,7 @@ // FSEvents can fail to report the contents of the directory being removed // when the directory itself is removed, so we need to manually mark the // files as removed. - for (var file in _files.toSet()) { + for (var file in _files.paths) { _emitEvent(ChangeType.REMOVE, file); } _files.clear();
diff --git a/pkgs/watcher/lib/src/directory_watcher/polling.dart b/pkgs/watcher/lib/src/directory_watcher/polling.dart index 7f417d6..ebc1709 100644 --- a/pkgs/watcher/lib/src/directory_watcher/polling.dart +++ b/pkgs/watcher/lib/src/directory_watcher/polling.dart
@@ -2,8 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library watcher.directory_watcher.polling; - import 'dart:async'; import 'dart:io';
diff --git a/pkgs/watcher/lib/src/directory_watcher/windows.dart b/pkgs/watcher/lib/src/directory_watcher/windows.dart index 0899519..67a2741 100644 --- a/pkgs/watcher/lib/src/directory_watcher/windows.dart +++ b/pkgs/watcher/lib/src/directory_watcher/windows.dart
@@ -3,8 +3,6 @@ // BSD-style license that can be found in the LICENSE file. // TODO(rnystrom): Merge with mac_os version. -library watcher.directory_watcher.windows; - import 'dart:async'; import 'dart:collection'; import 'dart:io'; @@ -136,7 +134,7 @@ event is FileSystemDeleteEvent || (FileSystemEntity.typeSync(path) == FileSystemEntityType.NOT_FOUND)) { - for (var path in _files.toSet()) { + for (var path in _files.paths) { _emitEvent(ChangeType.REMOVE, path); } _files.clear(); @@ -163,10 +161,10 @@ /// The callback that's run when [Directory.watch] emits a batch of events. void _onBatch(List<FileSystemEvent> batch) { - _sortEvents(batch).forEach((path, events) { + _sortEvents(batch).forEach((path, eventSet) { - var canonicalEvent = _canonicalEvent(events); - events = canonicalEvent == null ? + var canonicalEvent = _canonicalEvent(eventSet); + var events = canonicalEvent == null ? _eventsBasedOnFileSystem(path) : [canonicalEvent]; for (var event in events) { @@ -182,20 +180,20 @@ if (_files.containsDir(path)) continue; var stream = new Directory(path).list(recursive: true); - var sub; - sub = stream.listen((entity) { + StreamSubscription<FileSystemEntity> subscription; + subscription = stream.listen((entity) { if (entity is Directory) return; if (_files.contains(path)) return; _emitEvent(ChangeType.ADD, entity.path); _files.add(entity.path); }, onDone: () { - _listSubscriptions.remove(sub); + _listSubscriptions.remove(subscription); }, onError: (e, stackTrace) { - _listSubscriptions.remove(sub); + _listSubscriptions.remove(subscription); _emitError(e, stackTrace); }, cancelOnError: true); - _listSubscriptions.add(sub); + _listSubscriptions.add(subscription); } else if (event is FileSystemModifyEvent) { if (!event.isDirectory) { _emitEvent(ChangeType.MODIFY, path); @@ -219,15 +217,17 @@ /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it /// contain any events relating to [path]. Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) { - var eventsForPaths = {}; + var eventsForPaths = <String, Set>{}; // Events within directories that already have events are superfluous; the // directory's full contents will be examined anyway, so we ignore such // events. Emitting them could cause useless or out-of-order events. var directories = unionAll(batch.map((event) { if (!event.isDirectory) return new Set(); - if (event is! FileSystemMoveEvent) return new Set.from([event.path]); - return new Set.from([event.path, event.destination]); + if (event is FileSystemMoveEvent) { + return new Set.from([event.path, event.destination]); + } + return new Set.from([event.path]); })); isInModifiedDirectory(path) => @@ -322,7 +322,7 @@ var fileExists = new File(path).existsSync(); var dirExists = new Directory(path).existsSync(); - var events = []; + var events = <FileSystemEvent>[]; if (fileExisted) { if (fileExists) { events.add(new ConstructableFileSystemModifyEvent(path, false, false)); @@ -357,7 +357,7 @@ _watchSubscription = null; // Emit remove events for any remaining files. - for (var file in _files.toSet()) { + for (var file in _files.paths) { _emitEvent(ChangeType.REMOVE, file); } _files.clear();
diff --git a/pkgs/watcher/lib/src/file_watcher.dart b/pkgs/watcher/lib/src/file_watcher.dart index 9b31537..17c5f2e 100644 --- a/pkgs/watcher/lib/src/file_watcher.dart +++ b/pkgs/watcher/lib/src/file_watcher.dart
@@ -2,11 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library watcher.file_watcher; - import 'dart:io'; -import 'watch_event.dart'; import '../watcher.dart'; import 'file_watcher/native.dart'; import 'file_watcher/polling.dart';
diff --git a/pkgs/watcher/lib/src/file_watcher/native.dart b/pkgs/watcher/lib/src/file_watcher/native.dart index 1862e7b..f413a72 100644 --- a/pkgs/watcher/lib/src/file_watcher/native.dart +++ b/pkgs/watcher/lib/src/file_watcher/native.dart
@@ -2,8 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library watcher.file_watcher.native; - import 'dart:async'; import 'dart:io';
diff --git a/pkgs/watcher/lib/src/file_watcher/polling.dart b/pkgs/watcher/lib/src/file_watcher/polling.dart index 3480ae2..3f2e9f1 100644 --- a/pkgs/watcher/lib/src/file_watcher/polling.dart +++ b/pkgs/watcher/lib/src/file_watcher/polling.dart
@@ -2,8 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library watcher.file_watcher.polling; - import 'dart:async'; import 'dart:io';
diff --git a/pkgs/watcher/lib/src/path_set.dart b/pkgs/watcher/lib/src/path_set.dart index e9f7d32..3726e1f 100644 --- a/pkgs/watcher/lib/src/path_set.dart +++ b/pkgs/watcher/lib/src/path_set.dart
@@ -2,8 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library watcher.path_dart; - import 'dart:collection'; import 'package:path/path.dart' as p; @@ -21,32 +19,24 @@ /// The path set's directory hierarchy. /// - /// Each level of this hierarchy has the same structure: a map from strings to - /// other maps, which are further levels of the hierarchy. A map with no - /// elements indicates a path that was added to the set that has no paths - /// beneath it. Such a path should not be treated as a directory by - /// [containsDir]. - final _entries = new Map<String, Map>(); - - /// The set of paths that were explicitly added to this set. - /// - /// This is needed to disambiguate a directory that was explicitly added to - /// the set from a directory that was implicitly added by adding a path - /// beneath it. - final _paths = new Set<String>(); + /// Each entry represents a directory or file. It may be a file or directory + /// that was explicitly added, or a parent directory that was implicitly + /// added in order to add a child. + final _Entry _entries = new _Entry(); PathSet(this.root); /// Adds [path] to the set. void add(String path) { path = _normalize(path); - _paths.add(path); - var parts = _split(path); - var dir = _entries; + var parts = p.split(path); + var entry = _entries; for (var part in parts) { - dir = dir.putIfAbsent(part, () => {}); + entry = entry.contents.putIfAbsent(part, () => new _Entry()); } + + entry.isExplicit = true; } /// Removes [path] and any paths beneath it from the set and returns the @@ -59,110 +49,140 @@ /// empty set. Set<String> remove(String path) { path = _normalize(path); - var parts = new Queue.from(_split(path)); + var parts = new Queue.from(p.split(path)); // Remove the children of [dir], as well as [dir] itself if necessary. // // [partialPath] is the path to [dir], and a prefix of [path]; the remaining // components of [path] are in [parts]. - recurse(dir, partialPath) { + Set<String> recurse(dir, partialPath) { if (parts.length > 1) { // If there's more than one component left in [path], recurse down to // the next level. var part = parts.removeFirst(); - var entry = dir[part]; - if (entry == null || entry.isEmpty) return new Set(); + var entry = dir.contents[part]; + if (entry == null || entry.contents.isEmpty) return new Set(); partialPath = p.join(partialPath, part); var paths = recurse(entry, partialPath); // After removing this entry's children, if it has no more children and // it's not in the set in its own right, remove it as well. - if (entry.isEmpty && !_paths.contains(partialPath)) dir.remove(part); + if (entry.contents.isEmpty && !entry.isExplicit) { + dir.contents.remove(part); + } return paths; } // If there's only one component left in [path], we should remove it. - var entry = dir.remove(parts.first); + var entry = dir.contents.remove(parts.first); if (entry == null) return new Set(); - if (entry.isEmpty) { - _paths.remove(path); - return new Set.from([path]); + if (entry.contents.isEmpty) { + return new Set.from([p.join(root, path)]); } - var set = _removePathsIn(entry, path); - if (_paths.contains(path)) { - _paths.remove(path); - set.add(path); + var set = _explicitPathsWithin(entry, path); + if (entry.isExplicit) { + set.add(p.join(root, path)); } + return set; } return recurse(_entries, root); } - /// Recursively removes and returns all paths in [dir]. + /// Recursively lists all of the explicit paths within [dir]. /// - /// [root] should be the path to [dir]. - Set<String> _removePathsIn(Map dir, String root) { - var removedPaths = new Set(); + /// [dirPath] should be the path to [dir]. + Set<String> _explicitPathsWithin(_Entry dir, String dirPath) { + var paths = new Set<String>(); recurse(dir, path) { - dir.forEach((name, entry) { + dir.contents.forEach((name, entry) { var entryPath = p.join(path, name); - if (_paths.remove(entryPath)) removedPaths.add(entryPath); + if (entry.isExplicit) paths.add(p.join(root, entryPath)); + recurse(entry, entryPath); }); } - recurse(dir, root); - return removedPaths; + recurse(dir, dirPath); + return paths; } /// Returns whether [this] contains [path]. /// /// This only returns true for paths explicitly added to [this]. /// Implicitly-added directories can be inspected using [containsDir]. - bool contains(String path) => _paths.contains(_normalize(path)); + bool contains(String path) { + path = _normalize(path); + var entry = _entries; + + for (var part in p.split(path)) { + entry = entry.contents[part]; + if (entry == null) return false; + } + + return entry.isExplicit; + } /// Returns whether [this] contains paths beneath [path]. bool containsDir(String path) { path = _normalize(path); - var dir = _entries; + var entry = _entries; - for (var part in _split(path)) { - dir = dir[part]; - if (dir == null) return false; + for (var part in p.split(path)) { + entry = entry.contents[part]; + if (entry == null) return false; } - return !dir.isEmpty; + return !entry.contents.isEmpty; } - /// Returns a [Set] of all paths in [this]. - Set<String> toSet() => _paths.toSet(); + /// All of the paths explicitly added to this set. + List<String> get paths { + var result = <String>[]; + + recurse(dir, path) { + for (var name in dir.contents.keys) { + var entry = dir.contents[name]; + var entryPath = p.join(path, name); + if (entry.isExplicit) result.add(entryPath); + recurse(entry, entryPath); + } + } + + recurse(_entries, root); + return result; + } /// Removes all paths from [this]. void clear() { - _paths.clear(); - _entries.clear(); + _entries.contents.clear(); } - String toString() => _paths.toString(); - /// Returns a normalized version of [path]. /// /// This removes any extra ".." or "."s and ensure that the returned path /// begins with [root]. It's an error if [path] isn't within [root]. String _normalize(String path) { - var relative = p.relative(p.normalize(path), from: root); - var parts = p.split(relative); - // TODO(nweiz): replace this with [p.isWithin] when that exists (issue - // 14980). - if (!p.isRelative(relative) || parts.first == '..' || parts.first == '.') { - throw new ArgumentError('Path "$path" is not inside "$root".'); - } - return p.join(root, relative); - } + assert(p.isWithin(root, path)); - /// Returns the segments of [path] beneath [root]. - List<String> _split(String path) => p.split(p.relative(path, from: root)); + return p.relative(p.normalize(path), from: root); + } +} + +/// A virtual file system entity tracked by the [PathSet]. +/// +/// It may have child entries in [contents], which implies it's a directory. +class _Entry { + /// The child entries contained in this directory. + final Map<String, _Entry> contents = {}; + + /// If this entry was explicitly added as a leaf file system entity, this + /// will be true. + /// + /// Otherwise, it represents a parent directory that was implicitly added + /// when added some child of it. + bool isExplicit = false; }
diff --git a/pkgs/watcher/lib/src/resubscribable.dart b/pkgs/watcher/lib/src/resubscribable.dart index 2844c1e..aeefe93 100644 --- a/pkgs/watcher/lib/src/resubscribable.dart +++ b/pkgs/watcher/lib/src/resubscribable.dart
@@ -2,8 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library watcher.resubscribable; - import 'dart:async'; import '../watcher.dart';
diff --git a/pkgs/watcher/lib/src/stat.dart b/pkgs/watcher/lib/src/stat.dart index d36eff3..05ee9ba 100644 --- a/pkgs/watcher/lib/src/stat.dart +++ b/pkgs/watcher/lib/src/stat.dart
@@ -2,8 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library watcher.stat; - import 'dart:async'; import 'dart:io';
diff --git a/pkgs/watcher/lib/src/utils.dart b/pkgs/watcher/lib/src/utils.dart index 007c84c..d263f2f 100644 --- a/pkgs/watcher/lib/src/utils.dart +++ b/pkgs/watcher/lib/src/utils.dart
@@ -2,8 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library watcher.utils; - import 'dart:async'; import 'dart:io'; import 'dart:collection'; @@ -33,7 +31,7 @@ /// [broadcast] defaults to false. Stream futureStream(Future<Stream> future, {bool broadcast: false}) { var subscription; - var controller; + StreamController controller; future = future.catchError((e, stackTrace) { // Since [controller] is synchronous, it's likely that emitting an error @@ -94,7 +92,7 @@ /// microtasks. class BatchedStreamTransformer<T> implements StreamTransformer<T, List<T>> { Stream<List<T>> bind(Stream<T> input) { - var batch = new Queue(); + var batch = new Queue<T>(); return new StreamTransformer<T, List<T>>.fromHandlers( handleData: (event, sink) { batch.add(event);
diff --git a/pkgs/watcher/lib/src/watch_event.dart b/pkgs/watcher/lib/src/watch_event.dart index be6d70c..54093a5 100644 --- a/pkgs/watcher/lib/src/watch_event.dart +++ b/pkgs/watcher/lib/src/watch_event.dart
@@ -2,8 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library watcher.watch_event; - /// An event describing a single change to the file system. class WatchEvent { /// The manner in which the file at [path] has changed.
diff --git a/pkgs/watcher/lib/watcher.dart b/pkgs/watcher/lib/watcher.dart index 79dcc0d..b3cebe6 100644 --- a/pkgs/watcher/lib/watcher.dart +++ b/pkgs/watcher/lib/watcher.dart
@@ -2,8 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library watcher; - import 'dart:async'; import 'dart:io';
diff --git a/pkgs/watcher/pubspec.yaml b/pkgs/watcher/pubspec.yaml index 2e033f1..c0152e0 100644 --- a/pkgs/watcher/pubspec.yaml +++ b/pkgs/watcher/pubspec.yaml
@@ -1,14 +1,17 @@ name: watcher -version: 0.9.7 +version: 0.9.8-dev author: Dart Team <misc@dartlang.org> -homepage: http://github.com/dart-lang/watcher +homepage: https://github.com/dart-lang/watcher description: > A file system watcher. It monitors changes to contents of directories and sends notifications when files have been added, removed, or modified. environment: sdk: '>=1.9.0 <2.0.0' dependencies: + async: '^1.2.0' + collection: '^1.0.0' path: '>=0.9.0 <2.0.0' dev_dependencies: + benchmark_harness: '^1.0.4' scheduled_test: '^0.12.0' test: '^0.12.0'
diff --git a/pkgs/watcher/test/directory_watcher/shared.dart b/pkgs/watcher/test/directory_watcher/shared.dart index ff48cb2..c95ee39 100644 --- a/pkgs/watcher/test/directory_watcher/shared.dart +++ b/pkgs/watcher/test/directory_watcher/shared.dart
@@ -246,6 +246,37 @@ expectModifyEvent("new/file.txt"); }); + test('notifies when a file is replaced by a subdirectory', () { + writeFile("new"); + writeFile("old/file.txt"); + startWatcher(); + + deleteFile("new"); + renameDir("old", "new"); + inAnyOrder([ + isRemoveEvent("new"), + isRemoveEvent("old/file.txt"), + isAddEvent("new/file.txt") + ]); + }); + + test('notifies when a subdirectory is replaced by a file', () { + writeFile("old"); + writeFile("new/file.txt"); + startWatcher(); + + renameDir("new", "newer"); + renameFile("old", "new"); + inAnyOrder([ + isRemoveEvent("new/file.txt"), + isAddEvent("newer/file.txt"), + isRemoveEvent("old"), + isAddEvent("new") + ]); + }, onPlatform: { + "mac-os": new Skip("https://github.com/dart-lang/watcher/issues/21") + }); + test('emits events for many nested files added at once', () { withPermutations((i, j, k) => writeFile("sub/sub-$i/sub-$j/file-$k.txt"));
diff --git a/pkgs/watcher/test/file_watcher/native_test.dart b/pkgs/watcher/test/file_watcher/native_test.dart index 30d0eca..cbf11b6 100644 --- a/pkgs/watcher/test/file_watcher/native_test.dart +++ b/pkgs/watcher/test/file_watcher/native_test.dart
@@ -6,7 +6,6 @@ import 'package:scheduled_test/scheduled_test.dart'; import 'package:watcher/src/file_watcher/native.dart'; -import 'package:watcher/watcher.dart'; import 'shared.dart'; import '../utils.dart';
diff --git a/pkgs/watcher/test/no_subscription/mac_os_test.dart b/pkgs/watcher/test/no_subscription/mac_os_test.dart index d5b1c8e..499aff3 100644 --- a/pkgs/watcher/test/no_subscription/mac_os_test.dart +++ b/pkgs/watcher/test/no_subscription/mac_os_test.dart
@@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. @TestOn('mac-os') +@Skip("Flaky due to sdk#23877") import 'package:scheduled_test/scheduled_test.dart'; import 'package:watcher/src/directory_watcher/mac_os.dart';
diff --git a/pkgs/watcher/test/path_set_test.dart b/pkgs/watcher/test/path_set_test.dart index 13e797c..d3420d3 100644 --- a/pkgs/watcher/test/path_set_test.dart +++ b/pkgs/watcher/test/path_set_test.dart
@@ -6,8 +6,6 @@ import 'package:test/test.dart'; import 'package:watcher/src/path_set.dart'; -import 'utils.dart'; - Matcher containsPath(String path) => predicate((set) => set is PathSet && set.contains(path), 'set contains "$path"'); @@ -42,10 +40,6 @@ set.add(p.absolute("root/path/to/file")); expect(set, containsPath("root/path/to/file")); }); - - test("that's not beneath the root throws an error", () { - expect(() => set.add("path/to/file"), throwsArgumentError); - }); }); group("removing a path", () { @@ -78,7 +72,7 @@ expect(set, isNot(containsPath("root/path/to/two"))); expect(set, isNot(containsPath("root/path/to/sub/three"))); }); - + test("that's a directory in the set removes and returns it and all files " "beneath it", () { set.add("root/path"); @@ -110,10 +104,6 @@ expect(set.remove(p.absolute("root/path/to/file")), unorderedEquals([p.normalize("root/path/to/file")])); }); - - test("that's not beneath the root throws an error", () { - expect(() => set.remove("path/to/file"), throwsArgumentError); - }); }); group("containsPath()", () { @@ -143,10 +133,6 @@ set.add("root/path/to/file"); expect(set, containsPath(p.absolute("root/path/to/file"))); }); - - test("with a path that's not beneath the root throws an error", () { - expect(() => set.contains("path/to/file"), throwsArgumentError); - }); }); group("containsDir()", () { @@ -198,13 +184,13 @@ }); }); - group("toSet", () { + group("paths", () { test("returns paths added to the set", () { set.add("root/path"); set.add("root/path/to/one"); set.add("root/path/to/two"); - expect(set.toSet(), unorderedEquals([ + expect(set.paths, unorderedEquals([ "root/path", "root/path/to/one", "root/path/to/two", @@ -216,7 +202,7 @@ set.add("root/path/to/two"); set.remove("root/path/to/two"); - expect(set.toSet(), unorderedEquals([p.normalize("root/path/to/one")])); + expect(set.paths, unorderedEquals([p.normalize("root/path/to/one")])); }); }); @@ -227,7 +213,7 @@ set.add("root/path/to/two"); set.clear(); - expect(set.toSet(), isEmpty); + expect(set.paths, isEmpty); }); }); }
diff --git a/pkgs/watcher/test/utils.dart b/pkgs/watcher/test/utils.dart index 7dd8332..e3c2a1b 100644 --- a/pkgs/watcher/test/utils.dart +++ b/pkgs/watcher/test/utils.dart
@@ -2,8 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -library watcher.test.utils; - import 'dart:io'; import 'package:path/path.dart' as p; @@ -13,9 +11,6 @@ import 'package:watcher/src/stat.dart'; import 'package:watcher/src/utils.dart'; -// TODO(nweiz): remove this when issue 15042 is fixed. -import 'package:watcher/src/directory_watcher/mac_os.dart'; - /// The path to the temporary sandbox created for each test. All file /// operations are implicitly relative to this directory. String _sandboxDir; @@ -292,7 +287,7 @@ // Make sure we always use the same separator on Windows. path = p.normalize(path); - var milliseconds = _mockFileModificationTimes.putIfAbsent(path, () => 0); + _mockFileModificationTimes.putIfAbsent(path, () => 0); _mockFileModificationTimes[path]++; } }, "write file $path"); @@ -316,7 +311,7 @@ to = p.normalize(to); // Manually update the mock modification time for the file. - var milliseconds = _mockFileModificationTimes.putIfAbsent(to, () => 0); + _mockFileModificationTimes.putIfAbsent(to, () => 0); _mockFileModificationTimes[to]++; }, "rename file $from to $to"); } @@ -349,9 +344,10 @@ /// Returns a set of all values returns by [callback]. /// /// [limit] defaults to 3. -Set withPermutations(callback(int i, int j, int k), {int limit}) { +Set/*<S>*/ withPermutations/*<S>*/(/*=S*/ callback(int i, int j, int k), + {int limit}) { if (limit == null) limit = 3; - var results = new Set(); + var results = new Set/*<S>*/(); for (var i = 0; i < limit; i++) { for (var j = 0; j < limit; j++) { for (var k = 0; k < limit; k++) {