Skip to content

Commit

Permalink
Merge pull request dart-lang/watcher#91 from michalt/custom-watcher-f…
Browse files Browse the repository at this point in the history
…actory

Implement the ability to register custom watcher implementations
  • Loading branch information
davidmorgan authored Oct 8, 2020
2 parents 4ddf84d + 9f639cd commit ed5783b
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 3 deletions.
83 changes: 83 additions & 0 deletions pkgs/watcher/lib/src/custom_watcher_factory.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import '../watcher.dart';

/// Defines a way to create a custom watcher instead of the default ones.
///
/// This will be used when a [DirectoryWatcher] or [FileWatcher] would be
/// created and will take precedence over the default ones.
abstract class CustomWatcherFactory {
/// Uniquely identify this watcher.
String get id;

/// Tries to create a [DirectoryWatcher] for the provided path.
///
/// Returns `null` if the path is not supported by this factory.
DirectoryWatcher createDirectoryWatcher(String path, {Duration pollingDelay});

/// Tries to create a [FileWatcher] for the provided path.
///
/// Returns `null` if the path is not supported by this factory.
FileWatcher createFileWatcher(String path, {Duration pollingDelay});
}

/// Registers a custom watcher.
///
/// It's only allowed to register a watcher factory once per [id] and at most
/// one factory should apply to any given file (creating a [Watcher] will fail
/// otherwise).
void registerCustomWatcherFactory(CustomWatcherFactory customFactory) {
if (_customWatcherFactories.containsKey(customFactory.id)) {
throw ArgumentError('A custom watcher with id `${customFactory.id}` '
'has already been registered');
}
_customWatcherFactories[customFactory.id] = customFactory;
}

/// Tries to create a custom [DirectoryWatcher] and returns it.
///
/// Returns `null` if no custom watcher was applicable and throws a [StateError]
/// if more than one was.
DirectoryWatcher createCustomDirectoryWatcher(String path,
{Duration pollingDelay}) {
DirectoryWatcher customWatcher;
String customFactoryId;
for (var watcherFactory in customWatcherFactories) {
if (customWatcher != null) {
throw StateError('Two `CustomWatcherFactory`s applicable: '
'`$customFactoryId` and `${watcherFactory.id}` for `$path`');
}
customWatcher =
watcherFactory.createDirectoryWatcher(path, pollingDelay: pollingDelay);
customFactoryId = watcherFactory.id;
}
return customWatcher;
}

/// Tries to create a custom [FileWatcher] and returns it.
///
/// Returns `null` if no custom watcher was applicable and throws a [StateError]
/// if more than one was.
FileWatcher createCustomFileWatcher(String path, {Duration pollingDelay}) {
FileWatcher customWatcher;
String customFactoryId;
for (var watcherFactory in customWatcherFactories) {
if (customWatcher != null) {
throw StateError('Two `CustomWatcherFactory`s applicable: '
'`$customFactoryId` and `${watcherFactory.id}` for `$path`');
}
customWatcher =
watcherFactory.createFileWatcher(path, pollingDelay: pollingDelay);
customFactoryId = watcherFactory.id;
}
return customWatcher;
}

/// Unregisters a custom watcher and returns it.
///
/// Returns `null` if the id was never registered.
CustomWatcherFactory unregisterCustomWatcherFactory(String id) =>
_customWatcherFactories.remove(id);

Iterable<CustomWatcherFactory> get customWatcherFactories =>
_customWatcherFactories.values;

final _customWatcherFactories = <String, CustomWatcherFactory>{};
6 changes: 5 additions & 1 deletion pkgs/watcher/lib/src/directory_watcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
import 'dart:io';

import '../watcher.dart';
import 'custom_watcher_factory.dart';
import 'directory_watcher/linux.dart';
import 'directory_watcher/mac_os.dart';
import 'directory_watcher/windows.dart';
import 'directory_watcher/polling.dart';
import 'directory_watcher/windows.dart';

/// Watches the contents of a directory and emits [WatchEvent]s when something
/// in the directory has changed.
Expand All @@ -29,6 +30,9 @@ abstract class DirectoryWatcher implements Watcher {
/// watchers.
factory DirectoryWatcher(String directory, {Duration pollingDelay}) {
if (FileSystemEntity.isWatchSupported) {
var customWatcher =
createCustomDirectoryWatcher(directory, pollingDelay: pollingDelay);
if (customWatcher != null) return customWatcher;
if (Platform.isLinux) return LinuxDirectoryWatcher(directory);
if (Platform.isMacOS) return MacOSDirectoryWatcher(directory);
if (Platform.isWindows) return WindowsDirectoryWatcher(directory);
Expand Down
5 changes: 5 additions & 0 deletions pkgs/watcher/lib/src/file_watcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'dart:io';

import '../watcher.dart';
import 'custom_watcher_factory.dart';
import 'file_watcher/native.dart';
import 'file_watcher/polling.dart';

Expand All @@ -29,6 +30,10 @@ abstract class FileWatcher implements Watcher {
/// and higher CPU usage. Defaults to one second. Ignored for non-polling
/// watchers.
factory FileWatcher(String file, {Duration pollingDelay}) {
var customWatcher =
createCustomFileWatcher(file, pollingDelay: pollingDelay);
if (customWatcher != null) return customWatcher;

// [File.watch] doesn't work on Windows, but
// [FileSystemEntity.isWatchSupported] is still true because directory
// watching does work.
Expand Down
9 changes: 7 additions & 2 deletions pkgs/watcher/lib/watcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@
import 'dart:async';
import 'dart:io';

import 'src/watch_event.dart';
import 'src/directory_watcher.dart';
import 'src/file_watcher.dart';
import 'src/watch_event.dart';

export 'src/watch_event.dart';
export 'src/custom_watcher_factory.dart'
show
CustomWatcherFactory,
registerCustomWatcherFactory,
unregisterCustomWatcherFactory;
export 'src/directory_watcher.dart';
export 'src/directory_watcher/polling.dart';
export 'src/file_watcher.dart';
export 'src/file_watcher/polling.dart';
export 'src/watch_event.dart';

abstract class Watcher {
/// The path to the file or directory whose contents are being monitored.
Expand Down
146 changes: 146 additions & 0 deletions pkgs/watcher/test/custom_watcher_factory_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import 'dart:async';

import 'package:test/test.dart';
import 'package:watcher/watcher.dart';

import 'utils.dart';

void main() {
_MemFs memFs;
final defaultFactoryId = 'MemFs';

setUp(() {
memFs = _MemFs();
registerCustomWatcherFactory(_MemFsWatcherFactory(defaultFactoryId, memFs));
});

tearDown(() async {
unregisterCustomWatcherFactory(defaultFactoryId);
});

test('notifes for files', () async {
var watcher = FileWatcher('file.txt');

var completer = Completer<WatchEvent>();
watcher.events.listen((event) => completer.complete(event));
await watcher.ready;
memFs.add('file.txt');
var event = await completer.future;

expect(event.type, ChangeType.ADD);
expect(event.path, 'file.txt');
});

test('notifes for directories', () async {
var watcher = DirectoryWatcher('dir');

var completer = Completer<WatchEvent>();
watcher.events.listen((event) => completer.complete(event));
await watcher.ready;
memFs.add('dir');
var event = await completer.future;

expect(event.type, ChangeType.ADD);
expect(event.path, 'dir');
});

test('unregister works', () async {
unregisterCustomWatcherFactory(defaultFactoryId);

watcherFactory = (path) => FileWatcher(path);
try {
// This uses standard files, so it wouldn't trigger an event in
// _MemFsWatcher.
writeFile('file.txt');
await startWatcher(path: 'file.txt');
deleteFile('file.txt');
} finally {
watcherFactory = null;
}

await expectRemoveEvent('file.txt');
});

test('registering twice throws', () async {
expect(
() => registerCustomWatcherFactory(
_MemFsWatcherFactory(defaultFactoryId, memFs)),
throwsA(isA<ArgumentError>()));
});

test('finding two applicable factories throws', () async {
// Note that _MemFsWatcherFactory always returns a watcher, so having two
// will always produce a conflict.
registerCustomWatcherFactory(_MemFsWatcherFactory('Different id', memFs));
expect(() => FileWatcher('file.txt'), throwsA(isA<StateError>()));
expect(() => DirectoryWatcher('dir'), throwsA(isA<StateError>()));
});
}

class _MemFs {
final _streams = <String, Set<StreamController<WatchEvent>>>{};

StreamController<WatchEvent> watchStream(String path) {
var controller = StreamController<WatchEvent>();
_streams
.putIfAbsent(path, () => <StreamController<WatchEvent>>{})
.add(controller);
return controller;
}

void add(String path) {
var controllers = _streams[path];
if (controllers != null) {
for (var controller in controllers) {
controller.add(WatchEvent(ChangeType.ADD, path));
}
}
}

void remove(String path) {
var controllers = _streams[path];
if (controllers != null) {
for (var controller in controllers) {
controller.add(WatchEvent(ChangeType.REMOVE, path));
}
}
}
}

class _MemFsWatcher implements FileWatcher, DirectoryWatcher, Watcher {
final String _path;
final StreamController<WatchEvent> _controller;

_MemFsWatcher(this._path, this._controller);

@override
String get path => _path;

@override
String get directory => throw UnsupportedError('directory is not supported');

@override
Stream<WatchEvent> get events => _controller.stream;

@override
bool get isReady => true;

@override
Future<void> get ready async {}
}

class _MemFsWatcherFactory implements CustomWatcherFactory {
@override
final String id;
final _MemFs _memFs;
_MemFsWatcherFactory(this.id, this._memFs);

@override
DirectoryWatcher createDirectoryWatcher(String path,
{Duration pollingDelay}) =>
_MemFsWatcher(path, _memFs.watchStream(path));

@override
FileWatcher createFileWatcher(String path, {Duration pollingDelay}) =>
_MemFsWatcher(path, _memFs.watchStream(path));
}

0 comments on commit ed5783b

Please sign in to comment.