Skip to content

Commit

Permalink
Implement the ability to register custom watcher implementations
Browse files Browse the repository at this point in the history
  • Loading branch information
michalt committed Sep 23, 2020
1 parent aa284bd commit b1c8092
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 3 deletions.
46 changes: 46 additions & 0 deletions lib/src/custom_watcher_factory.dart
Original file line number Diff line number Diff line change
@@ -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>{};
11 changes: 10 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,14 @@ abstract class DirectoryWatcher implements Watcher {
/// 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);
Expand Down
8 changes: 8 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,13 @@ abstract class FileWatcher implements Watcher {
/// 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.
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
139 changes: 139 additions & 0 deletions test/custom_watcher_factory_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
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 {
await memFs.clear();
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));
}
}
}

void clear() async {
for (var controllers in _streams.values) {
for (var controller in controllers) {
await controller.close();
}
}
}
}

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

0 comments on commit b1c8092

Please sign in to comment.