// 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. // VMOptions=--enable-isolate-groups --experimental-enable-isolate-groups-jit // VMOptions=--no-enable-isolate-groups 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')); final destination = event.destination; if (destination != null) { Expect.isTrue(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 exiting = Completer(); late Directory dir; Completer modificationEventReceived = Completer(); late StreamSubscription receiverSubscription; late 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 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 exiting = Completer(); 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(); testWatchMoveFile(); testWatchTruncateFile(); testWatchDeleteFile(); testWatchDeleteDir(); testWatchOnlyModifyFile(); testMultipleEvents(); testWatchNonRecursive(); testWatchNonExisting(); testWatchMoveSelf(); testWatchConsistentModifiedFile(); if (Platform.isWindows) testWatchOverflow(); }