Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the ability to register custom watcher implementations #91

Merged
merged 5 commits into from
Oct 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions 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 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 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 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 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));
}