diff --git a/lib/src/custom_watcher_factory.dart b/lib/src/custom_watcher_factory.dart new file mode 100644 index 0000000..b7f81fa --- /dev/null +++ b/lib/src/custom_watcher_factory.dart @@ -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 get customWatcherFactories => + _customWatcherFactories.values; + +final _customWatcherFactories = {}; diff --git a/lib/src/directory_watcher.dart b/lib/src/directory_watcher.dart index e0ef3fc..3fe5004 100644 --- a/lib/src/directory_watcher.dart +++ b/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,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); diff --git a/lib/src/file_watcher.dart b/lib/src/file_watcher.dart index c4abddd..c41e5e6 100644 --- a/lib/src/file_watcher.dart +++ b/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,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. diff --git a/lib/watcher.dart b/lib/watcher.dart index 107ac8f..f5c7d3e 100644 --- a/lib/watcher.dart +++ b/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/test/custom_watcher_factory_test.dart b/test/custom_watcher_factory_test.dart new file mode 100644 index 0000000..6c9ffd8 --- /dev/null +++ b/test/custom_watcher_factory_test.dart @@ -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(); + 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(); + 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())); + }); + + 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())); + expect(() => DirectoryWatcher('dir'), throwsA(isA())); + }); +} + +class _MemFs { + final _streams = >>{}; + + StreamController watchStream(String path) { + var controller = StreamController(); + _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 _controller; + + _MemFsWatcher(this._path, this._controller); + + @override + String get path => _path; + + @override + String get directory => throw UnsupportedError('directory is not supported'); + + @override + Stream get events => _controller.stream; + + @override + bool get isReady => true; + + @override + Future 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)); +}