| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| // @dart = 2.9 |
| |
| import "dart:async"; |
| import "dart:io"; |
| import "dart:isolate"; |
| |
| import "package:async_helper/async_helper.dart"; |
| import "package:expect/expect.dart"; |
| import "package:path/path.dart"; |
| |
| void testWatchCreateFile() { |
| var dir = Directory.systemTemp.createTempSync('dart_file_system_watcher'); |
| var file = new File(join(dir.path, 'file')); |
| |
| var watcher = dir.watch(); |
| |
| asyncStart(); |
| var sub; |
| sub = watcher.listen((event) { |
| if (event is FileSystemCreateEvent && event.path.endsWith('file')) { |
| Expect.isFalse(event.isDirectory); |
| asyncEnd(); |
| sub.cancel(); |
| dir.deleteSync(recursive: true); |
| } |
| }, onError: (e) { |
| dir.deleteSync(recursive: true); |
| throw e; |
| }); |
| |
| file.createSync(); |
| } |
| |
| void testWatchCreateDir() { |
| var dir = Directory.systemTemp.createTempSync('dart_file_system_watcher'); |
| var subdir = new Directory(join(dir.path, 'dir')); |
| |
| var watcher = dir.watch(); |
| |
| asyncStart(); |
| var sub; |
| sub = watcher.listen((event) { |
| if (event is FileSystemCreateEvent && event.path.endsWith('dir')) { |
| Expect.isTrue(event.isDirectory); |
| asyncEnd(); |
| sub.cancel(); |
| dir.deleteSync(recursive: true); |
| } |
| }, onError: (e) { |
| dir.deleteSync(recursive: true); |
| throw e; |
| }); |
| |
| subdir.createSync(); |
| } |
| |
| void testWatchModifyFile() { |
| var dir = Directory.systemTemp.createTempSync('dart_file_system_watcher'); |
| var file = new File(join(dir.path, 'file')); |
| file.createSync(); |
| |
| var watcher = dir.watch(); |
| |
| asyncStart(); |
| var sub; |
| sub = watcher.listen((event) { |
| if (event is FileSystemModifyEvent) { |
| Expect.isTrue(event.path.endsWith('file')); |
| sub.cancel(); |
| asyncEnd(); |
| dir.deleteSync(recursive: true); |
| } |
| }, onError: (e) { |
| dir.deleteSync(recursive: true); |
| throw e; |
| }); |
| |
| file.writeAsStringSync('a'); |
| } |
| |
| void testWatchTruncateFile() { |
| var dir = Directory.systemTemp.createTempSync('dart_file_system_watcher'); |
| var file = new File(join(dir.path, 'file')); |
| file.writeAsStringSync('ab'); |
| var fileHandle = file.openSync(mode: FileMode.append); |
| |
| var watcher = dir.watch(); |
| |
| asyncStart(); |
| var sub; |
| sub = watcher.listen((event) { |
| if (event is FileSystemModifyEvent) { |
| Expect.isTrue(event.path.endsWith('file')); |
| Expect.isTrue(event.contentChanged); |
| sub.cancel(); |
| asyncEnd(); |
| fileHandle.closeSync(); |
| dir.deleteSync(recursive: true); |
| } |
| }, onError: (e) { |
| dir.deleteSync(recursive: true); |
| throw e; |
| }); |
| |
| fileHandle.truncateSync(1); |
| } |
| |
| void testWatchMoveFile() { |
| // Mac OS doesn't report move events. |
| if (Platform.isMacOS) return; |
| var dir = Directory.systemTemp.createTempSync('dart_file_system_watcher'); |
| var file = new File(join(dir.path, 'file')); |
| file.createSync(); |
| |
| var watcher = dir.watch(); |
| |
| asyncStart(); |
| var sub; |
| sub = watcher.listen((event) { |
| if (event is FileSystemMoveEvent) { |
| Expect.isTrue(event.path.endsWith('file')); |
| if (event.destination != null) { |
| Expect.isTrue(event.destination.endsWith('file2')); |
| } |
| sub.cancel(); |
| asyncEnd(); |
| dir.deleteSync(recursive: true); |
| } |
| }, onError: (e) { |
| dir.deleteSync(recursive: true); |
| throw e; |
| }); |
| |
| file.renameSync(join(dir.path, 'file2')); |
| } |
| |
| void testWatchDeleteFile() { |
| var dir = Directory.systemTemp.createTempSync('dart_file_system_watcher'); |
| var file = new File(join(dir.path, 'file')); |
| file.createSync(); |
| |
| var watcher = dir.watch(); |
| |
| asyncStart(); |
| var sub; |
| sub = watcher.listen((event) { |
| if (event is FileSystemDeleteEvent) { |
| Expect.isTrue(event.path.endsWith('file')); |
| sub.cancel(); |
| asyncEnd(); |
| dir.deleteSync(recursive: true); |
| } |
| }, onError: (e) { |
| dir.deleteSync(recursive: true); |
| throw e; |
| }); |
| |
| file.deleteSync(); |
| } |
| |
| void testWatchDeleteDir() { |
| // Windows keeps the directory handle open, even though it's deleted. It'll |
| // be flushed completely, once the watcher is closed as well. |
| if (Platform.isWindows) return; |
| var dir = Directory.systemTemp.createTempSync('dart_file_system_watcher'); |
| var watcher = dir.watch(events: 0); |
| |
| asyncStart(); |
| watcher.listen((event) { |
| if (event is FileSystemDeleteEvent) { |
| Expect.isTrue(event.path == dir.path); |
| } |
| }, onDone: () { |
| asyncEnd(); |
| }); |
| |
| dir.deleteSync(); |
| } |
| |
| void testWatchOnlyModifyFile() { |
| var dir = Directory.systemTemp.createTempSync('dart_file_system_watcher'); |
| var file = new File(join(dir.path, 'file')); |
| |
| var watcher = dir.watch(events: FileSystemEvent.modify); |
| |
| asyncStart(); |
| var sub; |
| sub = watcher.listen((event) { |
| Expect.isTrue(event is FileSystemModifyEvent); |
| Expect.isTrue(event.path.endsWith('file')); |
| sub.cancel(); |
| asyncEnd(); |
| dir.deleteSync(recursive: true); |
| }, onError: (e) { |
| dir.deleteSync(recursive: true); |
| throw e; |
| }); |
| |
| file.createSync(); |
| file.writeAsStringSync('a'); |
| } |
| |
| void testMultipleEvents() { |
| var dir = Directory.systemTemp.createTempSync('dart_file_system_watcher'); |
| var file = new File(join(dir.path, 'file')); |
| var file2 = new File(join(dir.path, 'file2')); |
| |
| var watcher = dir.watch(); |
| |
| asyncStart(); |
| int state = 0; |
| var sub; |
| sub = watcher.listen((event) { |
| int newState = 0; |
| switch (event.type) { |
| case FileSystemEvent.create: |
| newState = 1; |
| break; |
| |
| case FileSystemEvent.modify: |
| newState = 2; |
| break; |
| |
| case FileSystemEvent.move: |
| newState = 3; |
| break; |
| |
| case FileSystemEvent.delete: |
| newState = 4; |
| sub.cancel(); |
| asyncEnd(); |
| dir.deleteSync(); |
| break; |
| } |
| if (!Platform.isMacOS) { |
| if (newState < state) throw "Bad state"; |
| } |
| state = newState; |
| }); |
| |
| file.createSync(); |
| file.writeAsStringSync('a'); |
| file.renameSync(file2.path); |
| file2.deleteSync(); |
| } |
| |
| void testWatchRecursive() { |
| var dir = Directory.systemTemp.createTempSync('dart_file_system_watcher'); |
| if (Platform.isLinux) { |
| Expect.throws(() => dir.watch(recursive: true)); |
| return; |
| } |
| var dir2 = new Directory(join(dir.path, 'dir')); |
| dir2.createSync(); |
| var file = new File(join(dir.path, 'dir/file')); |
| |
| var watcher = dir.watch(recursive: true); |
| |
| asyncStart(); |
| var sub; |
| sub = watcher.listen((event) { |
| if (event.path.endsWith('file')) { |
| sub.cancel(); |
| asyncEnd(); |
| dir.deleteSync(recursive: true); |
| } |
| }, onError: (e) { |
| dir.deleteSync(recursive: true); |
| throw e; |
| }); |
| |
| file.createSync(); |
| } |
| |
| void testWatchNonRecursive() { |
| var dir = Directory.systemTemp.createTempSync('dart_file_system_watcher'); |
| var dir2 = new Directory(join(dir.path, 'dir')); |
| dir2.createSync(); |
| var file = new File(join(dir.path, 'dir/file')); |
| |
| var watcher = dir.watch(recursive: false); |
| |
| asyncStart(); |
| var sub; |
| sub = watcher.listen((event) { |
| if (event.path.endsWith('file')) { |
| throw "File change event not expected"; |
| } |
| }, onError: (e) { |
| dir.deleteSync(recursive: true); |
| throw e; |
| }); |
| |
| file.createSync(); |
| |
| new Timer(const Duration(milliseconds: 300), () { |
| sub.cancel(); |
| asyncEnd(); |
| dir.deleteSync(recursive: true); |
| }); |
| } |
| |
| void testWatchNonExisting() { |
| // MacOS allows listening on non-existing paths. |
| if (Platform.isMacOS) return; |
| asyncStart(); |
| new Directory('__some_none_existing_dir__').watch().listen((_) { |
| Expect.fail('unexpected error'); |
| }, onError: (e) { |
| asyncEnd(); |
| Expect.isTrue(e is FileSystemException); |
| }); |
| } |
| |
| void testWatchMoveSelf() { |
| // Windows keeps the directory handle open, even though it's deleted. It'll |
| // be flushed completely, once the watcher is closed as well. |
| if (Platform.isWindows) return; |
| var dir = Directory.systemTemp.createTempSync('dart_file_system_watcher'); |
| var dir2 = new Directory(join(dir.path, 'dir'))..createSync(); |
| |
| var watcher = dir2.watch(); |
| |
| asyncStart(); |
| bool gotDelete = false; |
| watcher.listen((event) { |
| if (event is FileSystemDeleteEvent) { |
| Expect.isTrue(event.path.endsWith('dir')); |
| gotDelete = true; |
| } |
| }, onDone: () { |
| Expect.isTrue(gotDelete); |
| dir.deleteSync(recursive: true); |
| asyncEnd(); |
| }); |
| |
| dir2.renameSync(join(dir.path, 'new_dir')); |
| } |
| |
| testWatchConsistentModifiedFile() async { |
| // When file modification starts before the watcher listen() is called and the first event |
| // happens in a very short period of time the modifying event will be missed before the |
| // stream listen has been set up and the watcher will hang forever. |
| // Bug: https://github.com/dart-lang/sdk/issues/37233 |
| // Bug: https://github.com/dart-lang/sdk/issues/37909 |
| asyncStart(); |
| ReceivePort receivePort = ReceivePort(); |
| Completer<bool> exiting = Completer<bool>(); |
| |
| Directory dir; |
| Completer<bool> modificationEventReceived = Completer<bool>(); |
| |
| StreamSubscription receiverSubscription; |
| SendPort workerSendPort; |
| receiverSubscription = receivePort.listen((object) async { |
| if (object == 'modification_started') { |
| var watcher = dir.watch(); |
| var subscription; |
| // Wait for event and check the type |
| subscription = watcher.listen((data) async { |
| if (data is FileSystemModifyEvent) { |
| Expect.isTrue(data.path.endsWith('file')); |
| await subscription.cancel(); |
| modificationEventReceived.complete(true); |
| } |
| }); |
| return; |
| } |
| if (object == 'end') { |
| await receiverSubscription.cancel(); |
| exiting.complete(true); |
| return; |
| } |
| // init event |
| workerSendPort = object[0]; |
| dir = new Directory(object[1]); |
| }); |
| |
| Completer<bool> workerExitedCompleter = Completer(); |
| RawReceivePort exitReceivePort = RawReceivePort((object) { |
| workerExitedCompleter.complete(true); |
| }); |
| RawReceivePort errorReceivePort = RawReceivePort((object) { |
| print('worker errored: $object'); |
| }); |
| Isolate isolate = await Isolate.spawn(modifyFiles, receivePort.sendPort, |
| onExit: exitReceivePort.sendPort, onError: errorReceivePort.sendPort); |
| |
| await modificationEventReceived.future; |
| workerSendPort.send('end'); |
| |
| await exiting.future; |
| await workerExitedCompleter.future; |
| exitReceivePort.close(); |
| errorReceivePort.close(); |
| // Stop modifier isolate |
| isolate.kill(); |
| asyncEnd(); |
| } |
| |
| void modifyFiles(SendPort sendPort) async { |
| // Send sendPort back to listen for modification signal. |
| ReceivePort receivePort = ReceivePort(); |
| var dir = Directory.systemTemp.createTempSync('dart_file_system_watcher'); |
| |
| // Create file within the directory and keep modifying. |
| var file = new File(join(dir.path, 'file')); |
| file.createSync(); |
| bool done = false; |
| var subscription; |
| subscription = receivePort.listen((object) async { |
| if (object == 'end') { |
| await subscription.cancel(); |
| done = true; |
| } |
| }); |
| sendPort.send([receivePort.sendPort, dir.path]); |
| bool notificationSent = false; |
| while (!done) { |
| // Start modifying the file continuously before watcher start watching. |
| for (int i = 0; i < 100; i++) { |
| file.writeAsStringSync('a'); |
| } |
| if (!notificationSent) { |
| sendPort.send('modification_started'); |
| notificationSent = true; |
| } |
| await Future.delayed(Duration()); |
| } |
| // Clean up the directory and files |
| dir.deleteSync(recursive: true); |
| sendPort.send('end'); |
| } |
| |
| testWatchOverflow() async { |
| // When underlying buffer for ReadDirectoryChangesW overflows(on Windows), |
| // it will send an exception to Stream which has been listened. |
| // Bug: https://github.com/dart-lang/sdk/issues/37233 |
| asyncStart(); |
| ReceivePort receivePort = ReceivePort(); |
| Completer<bool> exiting = Completer<bool>(); |
| |
| Directory dir = |
| Directory.systemTemp.createTempSync('dart_file_system_watcher'); |
| var file = new File(join(dir.path, 'file')); |
| file.createSync(); |
| |
| Isolate isolate = |
| await Isolate.spawn(watcher, receivePort.sendPort, paused: true); |
| |
| var subscription; |
| subscription = receivePort.listen((object) async { |
| if (object == 'end') { |
| exiting.complete(true); |
| subscription.cancel(); |
| // Clean up the directory and files |
| dir.deleteSync(recursive: true); |
| asyncEnd(); |
| } else if (object == 'start') { |
| isolate.pause(isolate.pauseCapability); |
| // Populate the buffer to overflows and check for exception |
| for (int i = 0; i < 2000; i++) { |
| file.writeAsStringSync('a'); |
| } |
| isolate.resume(isolate.pauseCapability); |
| } |
| }); |
| // Resume paused isolate to create watcher |
| isolate.resume(isolate.pauseCapability); |
| |
| await exiting.future; |
| isolate.kill(); |
| } |
| |
| void watcher(SendPort sendPort) async { |
| runZonedGuarded(() { |
| var watcher = Directory.systemTemp.watch(recursive: true); |
| watcher.listen((data) async {}); |
| sendPort.send('start'); |
| }, (error, stack) { |
| print(error); |
| sendPort.send('end'); |
| }); |
| } |
| |
| void main() { |
| if (!FileSystemEntity.isWatchSupported) return; |
| testWatchCreateFile(); |
| testWatchCreateDir(); |
| testWatchModifyFile(); |
| testWatchTruncateFile(); |
| testWatchMoveFile(); |
| testWatchDeleteFile(); |
| testWatchDeleteDir(); |
| testWatchOnlyModifyFile(); |
| testMultipleEvents(); |
| testWatchNonRecursive(); |
| testWatchNonExisting(); |
| testWatchMoveSelf(); |
| testWatchConsistentModifiedFile(); |
| if (Platform.isWindows) testWatchOverflow(); |
| } |