mirror of
https://github.com/dart-lang/sdk
synced 2024-11-02 12:24:24 +00:00
Wrap Directory.watch on Mac OS for the watcher package.
R=rnystrom@google.com BUG=14428 Review URL: https://codereview.chromium.org//66163002 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@30168 260f80e4-7a28-3924-810f-c04153c831b5
This commit is contained in:
parent
a00c36e401
commit
03337e9e5f
13 changed files with 983 additions and 2 deletions
|
@ -330,5 +330,11 @@ unittest/test/mock_test: StaticWarning, OK # testing unimplemented members
|
|||
[ $runtime == vm && ($system == windows || $system == macos) ]
|
||||
watcher/test/*/linux_test: Skip
|
||||
|
||||
[ $runtime == vm && ($system == windows || $system == linux) ]
|
||||
watcher/test/*/mac_os_test: Skip
|
||||
|
||||
[ $runtime == vm && $system == linux ]
|
||||
watcher/test/*/linux_test: Pass, Slow # Issue 14606
|
||||
|
||||
[ $runtime == vm && $system == macos ]
|
||||
watcher/test/no_subscribers/mac_os_test: Fail # Issue 14793 (see test file for details)
|
||||
|
|
60
pkg/watcher/lib/src/constructable_file_system_event.dart
Normal file
60
pkg/watcher/lib/src/constructable_file_system_event.dart
Normal file
|
@ -0,0 +1,60 @@
|
|||
// Copyright (c) 2013, 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.
|
||||
|
||||
library watcher.constructable_file_system_event;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
abstract class _ConstructableFileSystemEvent implements FileSystemEvent {
|
||||
final bool isDirectory;
|
||||
final String path;
|
||||
final int type;
|
||||
|
||||
_ConstructableFileSystemEvent(this.path, this.isDirectory);
|
||||
}
|
||||
|
||||
class ConstructableFileSystemCreateEvent extends _ConstructableFileSystemEvent
|
||||
implements FileSystemCreateEvent {
|
||||
final type = FileSystemEvent.CREATE;
|
||||
|
||||
ConstructableFileSystemCreateEvent(String path, bool isDirectory)
|
||||
: super(path, isDirectory);
|
||||
|
||||
String toString() => "FileSystemCreateEvent('$path')";
|
||||
}
|
||||
|
||||
class ConstructableFileSystemDeleteEvent extends _ConstructableFileSystemEvent
|
||||
implements FileSystemDeleteEvent {
|
||||
final type = FileSystemEvent.DELETE;
|
||||
|
||||
ConstructableFileSystemDeleteEvent(String path, bool isDirectory)
|
||||
: super(path, isDirectory);
|
||||
|
||||
String toString() => "FileSystemDeleteEvent('$path')";
|
||||
}
|
||||
|
||||
class ConstructableFileSystemModifyEvent extends _ConstructableFileSystemEvent
|
||||
implements FileSystemModifyEvent {
|
||||
final bool contentChanged;
|
||||
final type = FileSystemEvent.MODIFY;
|
||||
|
||||
ConstructableFileSystemModifyEvent(String path, bool isDirectory,
|
||||
this.contentChanged)
|
||||
: super(path, isDirectory);
|
||||
|
||||
String toString() =>
|
||||
"FileSystemModifyEvent('$path', contentChanged=$contentChanged)";
|
||||
}
|
||||
|
||||
class ConstructableFileSystemMoveEvent extends _ConstructableFileSystemEvent
|
||||
implements FileSystemMoveEvent {
|
||||
final String destination;
|
||||
final type = FileSystemEvent.MOVE;
|
||||
|
||||
ConstructableFileSystemMoveEvent(String path, bool isDirectory,
|
||||
this.destination)
|
||||
: super(path, isDirectory);
|
||||
|
||||
String toString() => "FileSystemMoveEvent('$path', '$destination')";
|
||||
}
|
|
@ -9,6 +9,7 @@ import 'dart:io';
|
|||
|
||||
import 'watch_event.dart';
|
||||
import 'directory_watcher/linux.dart';
|
||||
import 'directory_watcher/mac_os.dart';
|
||||
import 'directory_watcher/polling.dart';
|
||||
|
||||
/// Watches the contents of a directory and emits [WatchEvent]s when something
|
||||
|
@ -54,6 +55,7 @@ abstract class DirectoryWatcher {
|
|||
/// watchers.
|
||||
factory DirectoryWatcher(String directory, {Duration pollingDelay}) {
|
||||
if (Platform.isLinux) return new LinuxDirectoryWatcher(directory);
|
||||
if (Platform.isMacOS) return new MacOSDirectoryWatcher(directory);
|
||||
return new PollingDirectoryWatcher(directory, pollingDelay: pollingDelay);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,8 +12,6 @@ import '../utils.dart';
|
|||
import '../watch_event.dart';
|
||||
import 'resubscribable.dart';
|
||||
|
||||
import 'package:stack_trace/stack_trace.dart';
|
||||
|
||||
/// Uses the inotify subsystem to watch for filesystem events.
|
||||
///
|
||||
/// Inotify doesn't suport recursively watching subdirectories, nor does
|
||||
|
|
355
pkg/watcher/lib/src/directory_watcher/mac_os.dart
Normal file
355
pkg/watcher/lib/src/directory_watcher/mac_os.dart
Normal file
|
@ -0,0 +1,355 @@
|
|||
// Copyright (c) 2013, 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.
|
||||
|
||||
library watcher.directory_watcher.mac_os;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import '../constructable_file_system_event.dart';
|
||||
import '../path_set.dart';
|
||||
import '../utils.dart';
|
||||
import '../watch_event.dart';
|
||||
import 'resubscribable.dart';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// Uses the FSEvents subsystem to watch for filesystem events.
|
||||
///
|
||||
/// FSEvents has two main idiosyncrasies that this class works around. First, it
|
||||
/// will occasionally report events that occurred before the filesystem watch
|
||||
/// was initiated. Second, if multiple events happen to the same file in close
|
||||
/// succession, it won't report them in the order they occurred. See issue
|
||||
/// 14373.
|
||||
///
|
||||
/// This also works around issues 14793, 14806, and 14849 in the implementation
|
||||
/// of [Directory.watch].
|
||||
class MacOSDirectoryWatcher extends ResubscribableDirectoryWatcher {
|
||||
MacOSDirectoryWatcher(String directory)
|
||||
: super(directory, () => new _MacOSDirectoryWatcher(directory));
|
||||
}
|
||||
|
||||
class _MacOSDirectoryWatcher implements ManuallyClosedDirectoryWatcher {
|
||||
final String directory;
|
||||
|
||||
Stream<WatchEvent> get events => _eventsController.stream;
|
||||
final _eventsController = new StreamController<WatchEvent>.broadcast();
|
||||
|
||||
bool get isReady => _readyCompleter.isCompleted;
|
||||
|
||||
Future get ready => _readyCompleter.future;
|
||||
final _readyCompleter = new Completer();
|
||||
|
||||
/// The number of event batches that have been received from
|
||||
/// [Directory.watch].
|
||||
///
|
||||
/// This is used to determine if the [Directory.watch] stream was falsely
|
||||
/// closed due to issue 14849. A close caused by events in the past will only
|
||||
/// happen before or immediately after the first batch of events.
|
||||
int batches = 0;
|
||||
|
||||
/// The set of files that are known to exist recursively within the watched
|
||||
/// directory.
|
||||
///
|
||||
/// The state of files on the filesystem is compared against this to determine
|
||||
/// the real change that occurred when working around issue 14373. This is
|
||||
/// also used to emit REMOVE events when subdirectories are moved out of the
|
||||
/// watched directory.
|
||||
final PathSet _files;
|
||||
|
||||
/// The subscription to the stream returned by [Directory.watch].
|
||||
///
|
||||
/// This is separate from [_subscriptions] because this stream occasionally
|
||||
/// needs to be resubscribed in order to work around issue 14849.
|
||||
StreamSubscription<FileSystemEvent> _watchSubscription;
|
||||
|
||||
/// A set of subscriptions that this watcher subscribes to.
|
||||
///
|
||||
/// These are gathered together so that they may all be canceled when the
|
||||
/// watcher is closed. This does not include [_watchSubscription].
|
||||
final _subscriptions = new Set<StreamSubscription>();
|
||||
|
||||
_MacOSDirectoryWatcher(String directory)
|
||||
: directory = directory,
|
||||
_files = new PathSet(directory) {
|
||||
_startWatch();
|
||||
|
||||
_listen(new Directory(directory).list(recursive: true),
|
||||
(entity) {
|
||||
if (entity is! Directory) _files.add(entity.path);
|
||||
},
|
||||
onError: _emitError,
|
||||
onDone: _readyCompleter.complete,
|
||||
cancelOnError: true);
|
||||
}
|
||||
|
||||
void close() {
|
||||
for (var subscription in _subscriptions) {
|
||||
subscription.cancel();
|
||||
}
|
||||
_subscriptions.clear();
|
||||
if (_watchSubscription != null) _watchSubscription.cancel();
|
||||
_watchSubscription = null;
|
||||
_eventsController.close();
|
||||
}
|
||||
|
||||
/// The callback that's run when [Directory.watch] emits a batch of events.
|
||||
void _onBatch(List<FileSystemEvent> batch) {
|
||||
batches++;
|
||||
|
||||
_sortEvents(batch).forEach((path, events) {
|
||||
var canonicalEvent = _canonicalEvent(events);
|
||||
events = canonicalEvent == null ?
|
||||
_eventsBasedOnFileSystem(path) : [canonicalEvent];
|
||||
|
||||
for (var event in events) {
|
||||
if (event is FileSystemCreateEvent) {
|
||||
if (!event.isDirectory) {
|
||||
_emitEvent(ChangeType.ADD, path);
|
||||
_files.add(path);
|
||||
continue;
|
||||
}
|
||||
|
||||
_listen(new Directory(path).list(recursive: true), (entity) {
|
||||
if (entity is Directory) return;
|
||||
_emitEvent(ChangeType.ADD, entity.path);
|
||||
_files.add(entity.path);
|
||||
}, onError: _emitError, cancelOnError: true);
|
||||
} else if (event is FileSystemModifyEvent) {
|
||||
assert(!event.isDirectory);
|
||||
_emitEvent(ChangeType.MODIFY, path);
|
||||
} else {
|
||||
assert(event is FileSystemDeleteEvent);
|
||||
for (var removedPath in _files.remove(path)) {
|
||||
_emitEvent(ChangeType.REMOVE, removedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Sort all the events in a batch into sets based on their path.
|
||||
///
|
||||
/// A single input event may result in multiple events in the returned map;
|
||||
/// for example, a MOVE event becomes a DELETE event for the source and a
|
||||
/// CREATE event for the destination.
|
||||
///
|
||||
/// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
|
||||
/// contain any events relating to [directory].
|
||||
Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
|
||||
var eventsForPaths = {};
|
||||
|
||||
// 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
|
||||
// really deleted, that's handled by [_onDone].
|
||||
batch = batch.where((event) => event.path != directory).toList();
|
||||
|
||||
// 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]);
|
||||
}));
|
||||
|
||||
isInModifiedDirectory(path) =>
|
||||
directories.any((dir) => path != dir && path.startsWith(dir));
|
||||
|
||||
addEvent(path, event) {
|
||||
if (isInModifiedDirectory(path)) return;
|
||||
var set = eventsForPaths.putIfAbsent(path, () => new Set());
|
||||
set.add(event);
|
||||
}
|
||||
|
||||
for (var event in batch.where((event) => event is! FileSystemMoveEvent)) {
|
||||
addEvent(event.path, event);
|
||||
}
|
||||
|
||||
// Issue 14806 means that move events can be misleading if they're in the
|
||||
// same batch as another modification of a related file. If they are, we
|
||||
// make the event set empty to ensure we check the state of the filesystem.
|
||||
// Otherwise, treat them as a DELETE followed by an ADD.
|
||||
for (var event in batch.where((event) => event is FileSystemMoveEvent)) {
|
||||
if (eventsForPaths.containsKey(event.path) ||
|
||||
eventsForPaths.containsKey(event.destination)) {
|
||||
|
||||
if (!isInModifiedDirectory(event.path)) {
|
||||
eventsForPaths[event.path] = new Set();
|
||||
}
|
||||
if (!isInModifiedDirectory(event.destination)) {
|
||||
eventsForPaths[event.destination] = new Set();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
addEvent(event.path, new ConstructableFileSystemDeleteEvent(
|
||||
event.path, event.isDirectory));
|
||||
addEvent(event.destination, new ConstructableFileSystemCreateEvent(
|
||||
event.path, event.isDirectory));
|
||||
}
|
||||
|
||||
return eventsForPaths;
|
||||
}
|
||||
|
||||
/// Returns the canonical event from a batch of events on the same path, if
|
||||
/// one exists.
|
||||
///
|
||||
/// If [batch] doesn't contain any contradictory events (e.g. DELETE and
|
||||
/// CREATE, or events with different values for [isDirectory]), this returns a
|
||||
/// single event that describes what happened to the path in question.
|
||||
///
|
||||
/// If [batch] does contain contradictory events, this returns `null` to
|
||||
/// indicate that the state of the path on the filesystem should be checked to
|
||||
/// determine what occurred.
|
||||
FileSystemEvent _canonicalEvent(Set<FileSystemEvent> batch) {
|
||||
// An empty batch indicates that we've learned earlier that the batch is
|
||||
// contradictory (e.g. because of a move).
|
||||
if (batch.isEmpty) return null;
|
||||
|
||||
var type = batch.first.type;
|
||||
var isDir = batch.first.isDirectory;
|
||||
|
||||
for (var event in batch.skip(1)) {
|
||||
// If one event reports that the file is a directory and another event
|
||||
// doesn't, that's a contradiction.
|
||||
if (isDir != event.isDirectory) return null;
|
||||
|
||||
// Modify events don't contradict either CREATE or REMOVE events. We can
|
||||
// safely assume the file was modified after a CREATE or before the
|
||||
// REMOVE; otherwise there will also be a REMOVE or CREATE event
|
||||
// (respectively) that will be contradictory.
|
||||
if (event is FileSystemModifyEvent) continue;
|
||||
assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent);
|
||||
|
||||
// If we previously thought this was a MODIFY, we now consider it to be a
|
||||
// CREATE or REMOVE event. This is safe for the same reason as above.
|
||||
if (type == FileSystemEvent.MODIFY) {
|
||||
type = event.type;
|
||||
continue;
|
||||
}
|
||||
|
||||
// A CREATE event contradicts a REMOVE event and vice versa.
|
||||
assert(type == FileSystemEvent.CREATE || type == FileSystemEvent.DELETE);
|
||||
if (type != event.type) return null;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case FileSystemEvent.CREATE:
|
||||
// Issue 14793 means that CREATE events can actually mean DELETE, so we
|
||||
// should always check the filesystem for them.
|
||||
return null;
|
||||
case FileSystemEvent.DELETE:
|
||||
return new ConstructableFileSystemDeleteEvent(batch.first.path, isDir);
|
||||
case FileSystemEvent.MODIFY:
|
||||
return new ConstructableFileSystemModifyEvent(
|
||||
batch.first.path, isDir, false);
|
||||
default: assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns one or more events that describe the change between the last known
|
||||
/// state of [path] and its current state on the filesystem.
|
||||
///
|
||||
/// This returns a list whose order should be reflected in the events emitted
|
||||
/// to the user, unlike the batched events from [Directory.watch]. The
|
||||
/// returned list may be empty, indicating that no changes occurred to [path]
|
||||
/// (probably indicating that it was created and then immediately deleted).
|
||||
List<FileSystemEvent> _eventsBasedOnFileSystem(String path) {
|
||||
var fileExisted = _files.contains(path);
|
||||
var dirExisted = _files.containsDir(path);
|
||||
var fileExists = new File(path).existsSync();
|
||||
var dirExists = new Directory(path).existsSync();
|
||||
|
||||
var events = [];
|
||||
if (fileExisted) {
|
||||
if (fileExists) {
|
||||
events.add(new ConstructableFileSystemModifyEvent(path, false, false));
|
||||
} else {
|
||||
events.add(new ConstructableFileSystemDeleteEvent(path, false));
|
||||
}
|
||||
} else if (dirExisted) {
|
||||
if (dirExists) {
|
||||
// If we got contradictory events for a directory that used to exist and
|
||||
// still exists, we need to rescan the whole thing in case it was
|
||||
// replaced with a different directory.
|
||||
events.add(new ConstructableFileSystemDeleteEvent(path, true));
|
||||
events.add(new ConstructableFileSystemCreateEvent(path, true));
|
||||
} else {
|
||||
events.add(new ConstructableFileSystemDeleteEvent(path, true));
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileExisted && fileExists) {
|
||||
events.add(new ConstructableFileSystemCreateEvent(path, false));
|
||||
} else if (!dirExisted && dirExists) {
|
||||
events.add(new ConstructableFileSystemCreateEvent(path, true));
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
/// The callback that's run when the [Directory.watch] stream is closed.
|
||||
void _onDone() {
|
||||
_watchSubscription = null;
|
||||
|
||||
// If the directory still exists and we haven't seen more than one batch,
|
||||
// this is probably issue 14849 rather than a real close event. We should
|
||||
// just restart the watcher.
|
||||
if (batches < 2 && new Directory(directory).existsSync()) {
|
||||
_startWatch();
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 as
|
||||
// removed.
|
||||
for (var file in _files.toSet()) {
|
||||
_emitEvent(ChangeType.REMOVE, file);
|
||||
}
|
||||
_files.clear();
|
||||
close();
|
||||
}
|
||||
|
||||
/// Start or restart the underlying [Directory.watch] stream.
|
||||
void _startWatch() {
|
||||
// Batch the FSEvent changes together so that we can dedup events.
|
||||
var innerStream = new Directory(directory).watch(recursive: true).transform(
|
||||
new BatchedStreamTransformer<FileSystemEvent>());
|
||||
_watchSubscription = innerStream.listen(_onBatch,
|
||||
onError: _eventsController.addError,
|
||||
onDone: _onDone);
|
||||
}
|
||||
|
||||
/// Emit an event with the given [type] and [path].
|
||||
void _emitEvent(ChangeType type, String path) {
|
||||
if (!isReady) return;
|
||||
|
||||
// Don't emit ADD events for files that we already know about. Such an event
|
||||
// probably comes from FSEvents reporting an add that happened prior to the
|
||||
// watch beginning.
|
||||
if (type == ChangeType.ADD && _files.contains(path)) return;
|
||||
|
||||
_eventsController.add(new WatchEvent(type, path));
|
||||
}
|
||||
|
||||
/// Emit an error, then close the watcher.
|
||||
void _emitError(error, StackTrace stackTrace) {
|
||||
_eventsController.add(error, stackTrace);
|
||||
close();
|
||||
}
|
||||
|
||||
/// Like [Stream.listen], but automatically adds the subscription to
|
||||
/// [_subscriptions] so that it can be canceled when [close] is called.
|
||||
void _listen(Stream stream, void onData(event), {Function onError,
|
||||
void onDone(), bool cancelOnError}) {
|
||||
var subscription;
|
||||
subscription = stream.listen(onData, onError: onError, onDone: () {
|
||||
_subscriptions.remove(subscription);
|
||||
if (onDone != null) onDone();
|
||||
}, cancelOnError: cancelOnError);
|
||||
_subscriptions.add(subscription);
|
||||
}
|
||||
}
|
168
pkg/watcher/lib/src/path_set.dart
Normal file
168
pkg/watcher/lib/src/path_set.dart
Normal file
|
@ -0,0 +1,168 @@
|
|||
// Copyright (c) 2013, 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.
|
||||
|
||||
library watcher.path_dart;
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// A set of paths, organized into a directory hierarchy.
|
||||
///
|
||||
/// When a path is [add]ed, it creates an implicit directory structure above
|
||||
/// that path. Directories can be inspected using [containsDir] and removed
|
||||
/// using [remove]. If they're removed, their contents are removed as well.
|
||||
///
|
||||
/// The paths in the set are normalized so that they all begin with [root].
|
||||
class PathSet {
|
||||
/// The root path, which all paths in the set must be under.
|
||||
final String root;
|
||||
|
||||
/// 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>();
|
||||
|
||||
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;
|
||||
for (var part in parts) {
|
||||
dir = dir.putIfAbsent(part, () => {});
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes [path] and any paths beneath it from the set and returns the
|
||||
/// removed paths.
|
||||
///
|
||||
/// Even if [path] itself isn't in the set, if it's a directory containing
|
||||
/// paths that are in the set those paths will be removed and returned.
|
||||
///
|
||||
/// If neither [path] nor any paths beneath it are in the set, returns an
|
||||
/// empty set.
|
||||
Set<String> remove(String path) {
|
||||
path = _normalize(path);
|
||||
var parts = new Queue.from(_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) {
|
||||
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.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);
|
||||
return paths;
|
||||
}
|
||||
|
||||
// If there's only one component left in [path], we should remove it.
|
||||
var entry = dir.remove(parts.first);
|
||||
if (entry == null) return new Set();
|
||||
|
||||
if (entry.isEmpty) {
|
||||
_paths.remove(path);
|
||||
return new Set.from([path]);
|
||||
}
|
||||
|
||||
var set = _removePathsIn(entry, path);
|
||||
if (_paths.contains(path)) {
|
||||
_paths.remove(path);
|
||||
set.add(path);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
return recurse(_entries, root);
|
||||
}
|
||||
|
||||
/// Recursively removes and returns all paths in [dir].
|
||||
///
|
||||
/// [root] should be the path to [dir].
|
||||
Set<String> _removePathsIn(Map dir, String root) {
|
||||
var removedPaths = new Set();
|
||||
recurse(dir, path) {
|
||||
dir.forEach((name, entry) {
|
||||
var entryPath = p.join(path, name);
|
||||
if (_paths.remove(entryPath)) removedPaths.add(entryPath);
|
||||
recurse(entry, entryPath);
|
||||
});
|
||||
}
|
||||
|
||||
recurse(dir, root);
|
||||
return removedPaths;
|
||||
}
|
||||
|
||||
/// 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));
|
||||
|
||||
/// Returns whether [this] contains paths beneath [path].
|
||||
bool containsDir(String path) {
|
||||
path = _normalize(path);
|
||||
var dir = _entries;
|
||||
|
||||
for (var part in _split(path)) {
|
||||
dir = dir[part];
|
||||
if (dir == null) return false;
|
||||
}
|
||||
|
||||
return !dir.isEmpty;
|
||||
}
|
||||
|
||||
/// Returns a [Set] of all paths in [this].
|
||||
Set<String> toSet() => _paths.toSet();
|
||||
|
||||
/// Removes all paths from [this].
|
||||
void clear() {
|
||||
_paths.clear();
|
||||
_entries.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);
|
||||
}
|
||||
|
||||
/// Returns the segments of [path] beneath [root].
|
||||
List<String> _split(String path) => p.split(p.relative(path, from: root));
|
||||
}
|
|
@ -18,6 +18,10 @@ bool isDirectoryNotFoundException(error) {
|
|||
return error.osError.errorCode == notFoundCode;
|
||||
}
|
||||
|
||||
/// Returns the union of all elements in each set in [sets].
|
||||
Set unionAll(Iterable<Set> sets) =>
|
||||
sets.fold(new Set(), (union, set) => union.union(set));
|
||||
|
||||
/// Returns a buffered stream that will emit the same values as the stream
|
||||
/// returned by [future] once [future] completes.
|
||||
///
|
||||
|
|
67
pkg/watcher/test/directory_watcher/mac_os_test.dart
Normal file
67
pkg/watcher/test/directory_watcher/mac_os_test.dart
Normal file
|
@ -0,0 +1,67 @@
|
|||
// Copyright (c) 2013, 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.
|
||||
|
||||
import 'package:scheduled_test/scheduled_test.dart';
|
||||
import 'package:watcher/src/directory_watcher/mac_os.dart';
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
import 'shared.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
main() {
|
||||
initConfig();
|
||||
|
||||
watcherFactory = (dir) => new MacOSDirectoryWatcher(dir);
|
||||
|
||||
setUp(createSandbox);
|
||||
|
||||
sharedTests();
|
||||
|
||||
test('DirectoryWatcher creates a MacOSDirectoryWatcher on Mac OS', () {
|
||||
expect(new DirectoryWatcher('.'),
|
||||
new isInstanceOf<MacOSDirectoryWatcher>());
|
||||
});
|
||||
|
||||
test('does not notify about the watched directory being deleted and '
|
||||
'recreated immediately before watching', () {
|
||||
createDir("dir");
|
||||
writeFile("dir/old.txt");
|
||||
deleteDir("dir");
|
||||
createDir("dir");
|
||||
|
||||
startWatcher(dir: "dir");
|
||||
writeFile("dir/newer.txt");
|
||||
expectAddEvent("dir/newer.txt");
|
||||
});
|
||||
|
||||
test('notifies even if the file contents are unchanged', () {
|
||||
writeFile("a.txt", contents: "same");
|
||||
writeFile("b.txt", contents: "before");
|
||||
startWatcher();
|
||||
writeFile("a.txt", contents: "same");
|
||||
writeFile("b.txt", contents: "after");
|
||||
expectModifyEvent("a.txt");
|
||||
expectModifyEvent("b.txt");
|
||||
});
|
||||
|
||||
test('emits events for many nested files moved out then immediately back in',
|
||||
() {
|
||||
withPermutations((i, j, k) =>
|
||||
writeFile("dir/sub/sub-$i/sub-$j/file-$k.txt"));
|
||||
startWatcher(dir: "dir");
|
||||
|
||||
renameDir("dir/sub", "sub");
|
||||
renameDir("sub", "dir/sub");
|
||||
|
||||
inAnyOrder(() {
|
||||
withPermutations((i, j, k) =>
|
||||
expectRemoveEvent("dir/sub/sub-$i/sub-$j/file-$k.txt"));
|
||||
});
|
||||
|
||||
inAnyOrder(() {
|
||||
withPermutations((i, j, k) =>
|
||||
expectAddEvent("dir/sub/sub-$i/sub-$j/file-$k.txt"));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -6,6 +6,8 @@ import 'package:scheduled_test/scheduled_test.dart';
|
|||
|
||||
import '../utils.dart';
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
sharedTests() {
|
||||
test('does not notify for files that already exist when started', () {
|
||||
// Make some pre-existing files.
|
||||
|
@ -218,5 +220,21 @@ sharedTests() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("emits events for many files added at once in a subdirectory with the "
|
||||
"same name as a removed file", () {
|
||||
writeFile("dir/sub");
|
||||
withPermutations((i, j, k) =>
|
||||
writeFile("old/sub-$i/sub-$j/file-$k.txt"));
|
||||
startWatcher(dir: "dir");
|
||||
|
||||
deleteFile("dir/sub");
|
||||
renameDir("old", "dir/sub");
|
||||
inAnyOrder(() {
|
||||
expectRemoveEvent("dir/sub");
|
||||
withPermutations((i, j, k) =>
|
||||
expectAddEvent("dir/sub/sub-$i/sub-$j/file-$k.txt"));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
46
pkg/watcher/test/no_subscription/mac_os_test.dart
Normal file
46
pkg/watcher/test/no_subscription/mac_os_test.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) 2013, 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.
|
||||
|
||||
import 'package:scheduled_test/scheduled_test.dart';
|
||||
import 'package:watcher/src/directory_watcher/mac_os.dart';
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
import 'shared.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
// This is currently failing due to issue 14793. The reason is fairly complex:
|
||||
//
|
||||
// 1. As part of the test, an "unwatched.txt" file is created while there are no
|
||||
// active watchers on the containing directory.
|
||||
//
|
||||
// 2. A watcher is then added.
|
||||
//
|
||||
// 3. The watcher lists the contents of the directory and notices that
|
||||
// "unwatched.txt" already exists.
|
||||
//
|
||||
// 4. Since FSEvents reports past changes (issue 14373), the IO event stream
|
||||
// emits a CREATED event for "unwatched.txt".
|
||||
//
|
||||
// 5. Due to issue 14793, the watcher cannot trust that this is really a CREATED
|
||||
// event and checks the status of "unwatched.txt" on the filesystem against
|
||||
// its internal state.
|
||||
//
|
||||
// 6. "unwatched.txt" exists on the filesystem and the watcher knows about it
|
||||
// internally as well. It assumes this means that the file was modified.
|
||||
//
|
||||
// 7. The watcher emits an unexpected MODIFIED event for "unwatched.txt",
|
||||
// causing the test to fail.
|
||||
//
|
||||
// Once issue 14793 is fixed, this will no longer be the case and the test will
|
||||
// work again.
|
||||
|
||||
main() {
|
||||
initConfig();
|
||||
|
||||
watcherFactory = (dir) => new MacOSDirectoryWatcher(dir);
|
||||
|
||||
setUp(createSandbox);
|
||||
|
||||
sharedTests();
|
||||
}
|
235
pkg/watcher/test/path_set_test.dart
Normal file
235
pkg/watcher/test/path_set_test.dart
Normal file
|
@ -0,0 +1,235 @@
|
|||
// Copyright (c) 2013, 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.
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:unittest/unittest.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"');
|
||||
|
||||
Matcher containsDir(String path) => predicate((set) =>
|
||||
set is PathSet && set.containsDir(path),
|
||||
'set contains directory "$path"');
|
||||
|
||||
void main() {
|
||||
initConfig();
|
||||
|
||||
var set;
|
||||
setUp(() => set = new PathSet("root"));
|
||||
|
||||
group("adding a path", () {
|
||||
test("stores the path in the set", () {
|
||||
set.add("root/path/to/file");
|
||||
expect(set, containsPath("root/path/to/file"));
|
||||
});
|
||||
|
||||
test("that's a subdir of another path keeps both in the set", () {
|
||||
set.add("root/path");
|
||||
set.add("root/path/to/file");
|
||||
expect(set, containsPath("root/path"));
|
||||
expect(set, containsPath("root/path/to/file"));
|
||||
});
|
||||
|
||||
test("that's not normalized normalizes the path before storing it", () {
|
||||
set.add("root/../root/path/to/../to/././file");
|
||||
expect(set, containsPath("root/path/to/file"));
|
||||
});
|
||||
|
||||
test("that's absolute normalizes the path before storing it", () {
|
||||
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", () {
|
||||
test("that's in the set removes and returns that path", () {
|
||||
set.add("root/path/to/file");
|
||||
expect(set.remove("root/path/to/file"),
|
||||
unorderedEquals(["root/path/to/file"]));
|
||||
expect(set, isNot(containsPath("root/path/to/file")));
|
||||
});
|
||||
|
||||
test("that's not in the set returns an empty set", () {
|
||||
set.add("root/path/to/file");
|
||||
expect(set.remove("root/path/to/nothing"), isEmpty);
|
||||
});
|
||||
|
||||
test("that's a directory removes and returns all files beneath it", () {
|
||||
set.add("root/outside");
|
||||
set.add("root/path/to/one");
|
||||
set.add("root/path/to/two");
|
||||
set.add("root/path/to/sub/three");
|
||||
|
||||
expect(set.remove("root/path"), unorderedEquals([
|
||||
"root/path/to/one",
|
||||
"root/path/to/two",
|
||||
"root/path/to/sub/three"
|
||||
]));
|
||||
|
||||
expect(set, containsPath("root/outside"));
|
||||
expect(set, isNot(containsPath("root/path/to/one")));
|
||||
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");
|
||||
set.add("root/path/to/one");
|
||||
set.add("root/path/to/two");
|
||||
set.add("root/path/to/sub/three");
|
||||
|
||||
expect(set.remove("root/path"), unorderedEquals([
|
||||
"root/path",
|
||||
"root/path/to/one",
|
||||
"root/path/to/two",
|
||||
"root/path/to/sub/three"
|
||||
]));
|
||||
|
||||
expect(set, isNot(containsPath("root/path")));
|
||||
expect(set, isNot(containsPath("root/path/to/one")));
|
||||
expect(set, isNot(containsPath("root/path/to/two")));
|
||||
expect(set, isNot(containsPath("root/path/to/sub/three")));
|
||||
});
|
||||
|
||||
test("that's not normalized removes and returns the normalized path", () {
|
||||
set.add("root/path/to/file");
|
||||
expect(set.remove("root/../root/path/to/../to/./file"),
|
||||
unorderedEquals(["root/path/to/file"]));
|
||||
});
|
||||
|
||||
test("that's absolute removes and returns the normalized path", () {
|
||||
set.add("root/path/to/file");
|
||||
expect(set.remove(p.absolute("root/path/to/file")),
|
||||
unorderedEquals(["root/path/to/file"]));
|
||||
});
|
||||
|
||||
test("that's not beneath the root throws an error", () {
|
||||
expect(() => set.remove("path/to/file"), throwsArgumentError);
|
||||
});
|
||||
});
|
||||
|
||||
group("containsPath()", () {
|
||||
test("returns false for a non-existent path", () {
|
||||
set.add("root/path/to/file");
|
||||
expect(set, isNot(containsPath("root/path/to/nothing")));
|
||||
});
|
||||
|
||||
test("returns false for a directory that wasn't added explicitly", () {
|
||||
set.add("root/path/to/file");
|
||||
expect(set, isNot(containsPath("root/path")));
|
||||
});
|
||||
|
||||
test("returns true for a directory that was added explicitly", () {
|
||||
set.add("root/path");
|
||||
set.add("root/path/to/file");
|
||||
expect(set, containsPath("root/path"));
|
||||
});
|
||||
|
||||
test("with a non-normalized path normalizes the path before looking it up",
|
||||
() {
|
||||
set.add("root/path/to/file");
|
||||
expect(set, containsPath("root/../root/path/to/../to/././file"));
|
||||
});
|
||||
|
||||
test("with an absolute path normalizes the path before looking it up", () {
|
||||
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()", () {
|
||||
test("returns true for a directory that was added implicitly", () {
|
||||
set.add("root/path/to/file");
|
||||
expect(set, containsDir("root/path"));
|
||||
expect(set, containsDir("root/path/to"));
|
||||
});
|
||||
|
||||
test("returns true for a directory that was added explicitly", () {
|
||||
set.add("root/path");
|
||||
set.add("root/path/to/file");
|
||||
expect(set, containsDir("root/path"));
|
||||
});
|
||||
|
||||
test("returns false for a directory that wasn't added", () {
|
||||
expect(set, isNot(containsDir("root/nothing")));
|
||||
});
|
||||
|
||||
test("returns false for a non-directory path that was added", () {
|
||||
set.add("root/path/to/file");
|
||||
expect(set, isNot(containsDir("root/path/to/file")));
|
||||
});
|
||||
|
||||
test("returns false for a directory that was added implicitly and then "
|
||||
"removed implicitly", () {
|
||||
set.add("root/path/to/file");
|
||||
set.remove("root/path/to/file");
|
||||
expect(set, isNot(containsDir("root/path")));
|
||||
});
|
||||
|
||||
test("returns false for a directory that was added explicitly whose "
|
||||
"children were then removed", () {
|
||||
set.add("root/path");
|
||||
set.add("root/path/to/file");
|
||||
set.remove("root/path/to/file");
|
||||
expect(set, isNot(containsDir("root/path")));
|
||||
});
|
||||
|
||||
test("with a non-normalized path normalizes the path before looking it up",
|
||||
() {
|
||||
set.add("root/path/to/file");
|
||||
expect(set, containsDir("root/../root/path/to/../to/."));
|
||||
});
|
||||
|
||||
test("with an absolute path normalizes the path before looking it up", () {
|
||||
set.add("root/path/to/file");
|
||||
expect(set, containsDir(p.absolute("root/path")));
|
||||
});
|
||||
});
|
||||
|
||||
group("toSet", () {
|
||||
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([
|
||||
"root/path",
|
||||
"root/path/to/one",
|
||||
"root/path/to/two",
|
||||
]));
|
||||
});
|
||||
|
||||
test("doesn't return paths removed from the set", () {
|
||||
set.add("root/path/to/one");
|
||||
set.add("root/path/to/two");
|
||||
set.remove("root/path/to/two");
|
||||
|
||||
expect(set.toSet(), unorderedEquals(["root/path/to/one"]));
|
||||
});
|
||||
});
|
||||
|
||||
group("clear", () {
|
||||
test("removes all paths from the set", () {
|
||||
set.add("root/path");
|
||||
set.add("root/path/to/one");
|
||||
set.add("root/path/to/two");
|
||||
|
||||
set.clear();
|
||||
expect(set.toSet(), isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
20
pkg/watcher/test/ready/mac_os_test.dart
Normal file
20
pkg/watcher/test/ready/mac_os_test.dart
Normal file
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) 2013, 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.
|
||||
|
||||
import 'package:scheduled_test/scheduled_test.dart';
|
||||
import 'package:watcher/src/directory_watcher/mac_os.dart';
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
import 'shared.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
main() {
|
||||
initConfig();
|
||||
|
||||
watcherFactory = (dir) => new MacOSDirectoryWatcher(dir);
|
||||
|
||||
setUp(createSandbox);
|
||||
|
||||
sharedTests();
|
||||
}
|
|
@ -137,6 +137,8 @@ void startWatcher({String dir}) {
|
|||
// people think it might be the root cause.
|
||||
if (currentSchedule.errors.isEmpty) {
|
||||
expect(allEvents, hasLength(numEvents));
|
||||
} else {
|
||||
currentSchedule.addDebugInfo("Events fired:\n${allEvents.join('\n')}");
|
||||
}
|
||||
}, "reset watcher");
|
||||
|
||||
|
|
Loading…
Reference in a new issue