Implement the ability to register custom watcher implementations
This allows registering a special-purpose factory that returns its own
`Watcher` implementation that will take precedence over the default
ones. The main motivation for this is handling of file systems that need
custom code to watch for changes.
diff --git a/pkgs/watcher/lib/src/custom_watcher_factory.dart b/pkgs/watcher/lib/src/custom_watcher_factory.dart
new file mode 100644
index 0000000..ffac06b
--- /dev/null
+++ b/pkgs/watcher/lib/src/custom_watcher_factory.dart
@@ -0,0 +1,46 @@
+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.
+ ///
+ /// Should return `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.
+ ///
+ /// Should return `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 once per [id]. The [supportsPath]
+/// will be called to determine if the [createWatcher] should be used instead of
+/// the built-in watchers.
+///
+/// Note that we will try [CustomWatcherFactory] one by one in the order they
+/// were registered.
+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;
+}
+
+/// Unregisters a custom watcher and returns it (returns `null` if it was never
+/// registered).
+CustomWatcherFactory unregisterCustomWatcherFactory(String id) =>
+ _customWatcherFactories.remove(id);
+
+Iterable<CustomWatcherFactory> get customWatcherFactories =>
+ _customWatcherFactories.values;
+
+final _customWatcherFactories = <String, CustomWatcherFactory>{};
diff --git a/pkgs/watcher/lib/src/directory_watcher.dart b/pkgs/watcher/lib/src/directory_watcher.dart
index e0ef3fc..858d020 100644
--- a/pkgs/watcher/lib/src/directory_watcher.dart
+++ b/pkgs/watcher/lib/src/directory_watcher.dart
@@ -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.
@@ -29,6 +30,14 @@
/// watchers.
factory DirectoryWatcher(String directory, {Duration pollingDelay}) {
if (FileSystemEntity.isWatchSupported) {
+ for (var custom in customWatcherFactories) {
+ var watcher = custom.createDirectoryWatcher(directory,
+ pollingDelay: pollingDelay);
+ if (watcher != null) {
+ return watcher;
+ }
+ }
+
if (Platform.isLinux) return LinuxDirectoryWatcher(directory);
if (Platform.isMacOS) return MacOSDirectoryWatcher(directory);
if (Platform.isWindows) return WindowsDirectoryWatcher(directory);
diff --git a/pkgs/watcher/lib/src/file_watcher.dart b/pkgs/watcher/lib/src/file_watcher.dart
index c4abddd..0b7afc7 100644
--- a/pkgs/watcher/lib/src/file_watcher.dart
+++ b/pkgs/watcher/lib/src/file_watcher.dart
@@ -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';
@@ -29,6 +30,13 @@
/// and higher CPU usage. Defaults to one second. Ignored for non-polling
/// watchers.
factory FileWatcher(String file, {Duration pollingDelay}) {
+ for (var custom in customWatcherFactories) {
+ var watcher = custom.createFileWatcher(file, pollingDelay: pollingDelay);
+ if (watcher != null) {
+ return watcher;
+ }
+ }
+
// [File.watch] doesn't work on Windows, but
// [FileSystemEntity.isWatchSupported] is still true because directory
// watching does work.
diff --git a/pkgs/watcher/lib/watcher.dart b/pkgs/watcher/lib/watcher.dart
index 107ac8f..f5c7d3e 100644
--- a/pkgs/watcher/lib/watcher.dart
+++ b/pkgs/watcher/lib/watcher.dart
@@ -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.
diff --git a/pkgs/watcher/test/custom_watcher_factory_test.dart b/pkgs/watcher/test/custom_watcher_factory_test.dart
new file mode 100644
index 0000000..488b607
--- /dev/null
+++ b/pkgs/watcher/test/custom_watcher_factory_test.dart
@@ -0,0 +1,130 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:test/test.dart';
+import 'package:watcher/watcher.dart';
+
+void main() {
+ _MemFs memFs;
+
+ setUp(() {
+ memFs = _MemFs();
+ registerCustomWatcherFactory(_MemFsWatcherFactory(memFs));
+ });
+
+ tearDown(() async {
+ unregisterCustomWatcherFactory('MemFs');
+ });
+
+ 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 {
+ var memFactory = _MemFsWatcherFactory(memFs);
+ unregisterCustomWatcherFactory(memFactory.id);
+
+ var completer = Completer<dynamic>();
+ var watcher = FileWatcher('file.txt');
+ watcher.events.listen((e) {}, onError: (e) => completer.complete(e));
+ await watcher.ready;
+ memFs.add('file.txt');
+ var result = await completer.future;
+
+ expect(result, isA<FileSystemException>());
+ });
+
+ test('registering twice throws', () async {
+ expect(() => registerCustomWatcherFactory(_MemFsWatcherFactory(memFs)),
+ throwsA(isA<ArgumentError>()));
+ });
+}
+
+class _MemFs {
+ final _streams = <String, Set<StreamController<WatchEvent>>>{};
+
+ StreamController<WatchEvent> watchStream(String path) {
+ var controller = StreamController<WatchEvent>();
+ _streams.putIfAbsent(path, () => {}).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 {
+ final _MemFs _memFs;
+ _MemFsWatcherFactory(this._memFs);
+
+ @override
+ String get id => '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));
+}