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)); +}