diff --git a/.github/workflows/watcher.yaml b/.github/workflows/watcher.yaml index 6a3b9ba70..0afb7079a 100644 --- a/.github/workflows/watcher.yaml +++ b/.github/workflows/watcher.yaml @@ -54,7 +54,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - sdk: [3.1, dev] + sdk: [3.3, dev] steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c diff --git a/pkgs/watcher/lib/src/directory_watcher/linux.dart b/pkgs/watcher/lib/src/directory_watcher/linux.dart index 99e2cf50e..80497e0cb 100644 --- a/pkgs/watcher/lib/src/directory_watcher/linux.dart +++ b/pkgs/watcher/lib/src/directory_watcher/linux.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'package:async/async.dart'; import '../directory_watcher.dart'; +import '../event.dart'; import '../path_set.dart'; import '../resubscribable.dart'; import '../utils.dart'; @@ -145,7 +146,7 @@ class _LinuxDirectoryWatcher } /// The callback that's run when a batch of changes comes in. - void _onBatch(List batch) { + void _onBatch(List batch) { var files = {}; var dirs = {}; var changed = {}; @@ -162,30 +163,35 @@ class _LinuxDirectoryWatcher changed.add(event.path); - if (event is FileSystemMoveEvent) { - files.remove(event.path); - dirs.remove(event.path); - - var destination = event.destination; - if (destination == null) continue; + switch (event.type) { + case EventType.moveFile: + files.remove(event.path); + var destination = event.destination; + if (destination == null) continue; + changed.add(destination); + files.add(destination); + dirs.remove(destination); - changed.add(destination); - if (event.isDirectory) { + case EventType.moveDirectory: + dirs.remove(event.path); + var destination = event.destination; + if (destination == null) continue; files.remove(destination); dirs.add(destination); - } else { - files.add(destination); - dirs.remove(destination); - } - } else if (event is FileSystemDeleteEvent) { - files.remove(event.path); - dirs.remove(event.path); - } else if (event.isDirectory) { - files.remove(event.path); - dirs.add(event.path); - } else { - files.add(event.path); - dirs.remove(event.path); + + case EventType.delete: + files.remove(event.path); + dirs.remove(event.path); + + case EventType.createFile: + case EventType.modifyFile: + files.add(event.path); + dirs.remove(event.path); + + case EventType.createDirectory: + case EventType.modifyDirectory: + files.remove(event.path); + dirs.add(event.path); } } diff --git a/pkgs/watcher/lib/src/directory_watcher/mac_os.dart b/pkgs/watcher/lib/src/directory_watcher/mac_os.dart index 509cf6fe6..3efde9b4a 100644 --- a/pkgs/watcher/lib/src/directory_watcher/mac_os.dart +++ b/pkgs/watcher/lib/src/directory_watcher/mac_os.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'package:path/path.dart' as p; import '../directory_watcher.dart'; +import '../event.dart'; import '../path_set.dart'; import '../resubscribable.dart'; import '../utils.dart'; @@ -63,7 +64,7 @@ class _MacOSDirectoryWatcher /// /// This is separate from [_listSubscriptions] because this stream /// occasionally needs to be resubscribed in order to work around issue 14849. - StreamSubscription>? _watchSubscription; + StreamSubscription>? _watchSubscription; /// The subscription to the [Directory.list] call for the initial listing of /// the directory to determine its initial state. @@ -109,7 +110,7 @@ class _MacOSDirectoryWatcher } /// The callback that's run when [Directory.watch] emits a batch of events. - void _onBatch(List batch) { + void _onBatch(List batch) { // If we get a batch of events before we're ready to begin emitting events, // it's probable that it's a batch of pre-watcher events (see issue 14373). // Ignore those events and re-list the directory. @@ -132,8 +133,8 @@ class _MacOSDirectoryWatcher : [canonicalEvent]; for (var event in events) { - if (event is FileSystemCreateEvent) { - if (!event.isDirectory) { + switch (event.type) { + case EventType.createFile: // If we already know about the file, treat it like a modification. // This can happen if a file is copied on top of an existing one. // We'll see an ADD event for the latter file when from the user's @@ -143,34 +144,39 @@ class _MacOSDirectoryWatcher _emitEvent(type, path); _files.add(path); - continue; - } - - if (_files.containsDir(path)) continue; - - var stream = Directory(path) - .list(recursive: true) - .ignoring(); - var subscription = stream.listen((entity) { - if (entity is Directory) return; - if (_files.contains(path)) return; - - _emitEvent(ChangeType.ADD, entity.path); - _files.add(entity.path); - }, cancelOnError: true); - subscription.onDone(() { - _listSubscriptions.remove(subscription); - }); - subscription.onError(_emitError); - _listSubscriptions.add(subscription); - } else if (event is FileSystemModifyEvent) { - assert(!event.isDirectory); - _emitEvent(ChangeType.MODIFY, path); - } else { - assert(event is FileSystemDeleteEvent); - for (var removedPath in _files.remove(path)) { - _emitEvent(ChangeType.REMOVE, removedPath); - } + + case EventType.createDirectory: + if (_files.containsDir(path)) continue; + + var stream = Directory(path) + .list(recursive: true) + .ignoring(); + var subscription = stream.listen((entity) { + if (entity is Directory) return; + if (_files.contains(path)) return; + + _emitEvent(ChangeType.ADD, entity.path); + _files.add(entity.path); + }, cancelOnError: true); + subscription.onDone(() { + _listSubscriptions.remove(subscription); + }); + subscription.onError(_emitError); + _listSubscriptions.add(subscription); + + case EventType.modifyFile: + _emitEvent(ChangeType.MODIFY, path); + + case EventType.delete: + for (var removedPath in _files.remove(path)) { + _emitEvent(ChangeType.REMOVE, removedPath); + } + + // Guaranteed not present by `_sortEvents`. + case EventType.moveFile: + case EventType.moveDirectory: + case EventType.modifyDirectory: + throw StateError(event.type.name); } } }); @@ -178,45 +184,46 @@ class _MacOSDirectoryWatcher /// Sort all the events in a batch into sets based on their path. /// - /// A single input event may result in multiple events in the returned map; - /// for example, a MOVE event becomes a DELETE event for the source and a - /// CREATE event for the destination. + /// Events for `path` are discarded. + /// + /// Events under directories that are created or modified are discarded. /// - /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it - /// contain any events relating to [path]. - Map> _sortEvents(List batch) { - var eventsForPaths = >{}; + /// Three event types are not expected on MacOS, if encountered they will be + /// dropped with an assert fail to signal in tests. The types are: + /// [EventType.moveFile], [EventType.moveDirectory] and + /// [EventType.modifyDirectory]. See + /// https://github.com/dart-lang/sdk/issues/14806. + Map> _sortEvents(List batch) { + var eventsForPaths = >{}; // FSEvents can report past events, including events on the root directory // such as it being created. We want to ignore these. If the directory is // really deleted, that's handled by [_onDone]. batch = batch.where((event) => event.path != path).toList(); - // Events within directories that already have events are superfluous; the - // directory's full contents will be examined anyway, so we ignore such - // events. Emitting them could cause useless or out-of-order events. - var directories = unionAll(batch.map((event) { - if (!event.isDirectory) return {}; - if (event is FileSystemMoveEvent) { - var destination = event.destination; - if (destination != null) { - return {event.path, destination}; - } - } - return {event.path}; + // Events within directories that already have create events are not needed + // as the directory's full content will be listed. + var createdDirectories = unionAll(batch.map((event) { + return event.type == EventType.createDirectory + ? {event.path} + : const {}; })); - bool isInModifiedDirectory(String path) => - directories.any((dir) => path != dir && p.isWithin(dir, path)); + bool isInCreatedDirectory(String path) => + createdDirectories.any((dir) => path != dir && p.isWithin(dir, path)); - void addEvent(String path, FileSystemEvent event) { - if (isInModifiedDirectory(path)) return; - eventsForPaths.putIfAbsent(path, () => {}).add(event); + void addEvent(String path, Event event) { + if (isInCreatedDirectory(path)) return; + eventsForPaths.putIfAbsent(path, () => {}).add(event); } for (var event in batch) { - // The Mac OS watcher doesn't emit move events. See issue 14806. - assert(event is! FileSystemMoveEvent); + if (event.type == EventType.moveFile || + event.type == EventType.moveDirectory || + event.type == EventType.modifyDirectory) { + assert(false); + continue; + } addEvent(event.path, event); } @@ -233,67 +240,36 @@ class _MacOSDirectoryWatcher /// If [batch] does contain contradictory events, this returns `null` to /// indicate that the state of the path on the filesystem should be checked to /// determine what occurred. - FileSystemEvent? _canonicalEvent(Set batch) { - // An empty batch indicates that we've learned earlier that the batch is - // contradictory (e.g. because of a move). + Event? _canonicalEvent(Set batch) { if (batch.isEmpty) return null; - var type = batch.first.type; - var isDir = batch.first.isDirectory; - var hadModifyEvent = false; - - for (var event in batch.skip(1)) { - // If one event reports that the file is a directory and another event - // doesn't, that's a contradiction. - if (isDir != event.isDirectory) return null; - - // Modify events don't contradict either CREATE or REMOVE events. We can - // safely assume the file was modified after a CREATE or before the - // REMOVE; otherwise there will also be a REMOVE or CREATE event - // (respectively) that will be contradictory. - if (event is FileSystemModifyEvent) { - hadModifyEvent = true; - continue; - } - assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent); + var types = batch.map((e) => e.type).toSet(); - // If we previously thought this was a MODIFY, we now consider it to be a - // CREATE or REMOVE event. This is safe for the same reason as above. - if (type == FileSystemEvent.modify) { - type = event.type; - continue; + if (types.length == 2 && + types.contains(EventType.modifyFile) && + types.contains(EventType.createFile)) { + if (_files.contains(path)) { + types.remove(EventType.createFile); + } else { + types.remove(EventType.modifyFile); } - - // A CREATE event contradicts a REMOVE event and vice versa. - assert(type == FileSystemEvent.create || type == FileSystemEvent.delete); - if (type != event.type) return null; } - // If we got a CREATE event for a file we already knew about, that comes - // from FSEvents reporting an add that happened prior to the watch - // beginning. If we also received a MODIFY event, we want to report that, - // but not the CREATE. - if (type == FileSystemEvent.create && - hadModifyEvent && - _files.contains(batch.first.path)) { - type = FileSystemEvent.modify; + if (types.length != 1) { + return null; } - switch (type) { - case FileSystemEvent.create: - // Issue 16003 means that a CREATE event for a directory can indicate - // that the directory was moved and then re-created. - // [_eventsBasedOnFileSystem] will handle this correctly by producing a - // DELETE event followed by a CREATE event if the directory exists. - if (isDir) return null; - return FileSystemCreateEvent(batch.first.path, false); - case FileSystemEvent.delete: - return FileSystemDeleteEvent(batch.first.path, isDir); - case FileSystemEvent.modify: - return FileSystemModifyEvent(batch.first.path, isDir, false); - default: - throw StateError('unreachable'); + final type = types.first; + + if (type == EventType.createDirectory) { + // Issue 16003 means that a CREATE event for a directory can indicate + // that the directory was moved and then re-created. + // [_eventsBasedOnFileSystem] will handle this correctly by producing a + // DELETE event followed by a CREATE event if the directory exists. + return null; } + + return batch.firstWhere((e) => e.type == type); } /// Returns one or more events that describe the change between the last known @@ -303,35 +279,35 @@ class _MacOSDirectoryWatcher /// to the user, unlike the batched events from [Directory.watch]. The /// returned list may be empty, indicating that no changes occurred to [path] /// (probably indicating that it was created and then immediately deleted). - List _eventsBasedOnFileSystem(String path) { + List _eventsBasedOnFileSystem(String path) { var fileExisted = _files.contains(path); var dirExisted = _files.containsDir(path); var fileExists = File(path).existsSync(); var dirExists = Directory(path).existsSync(); - var events = []; + var events = []; if (fileExisted) { if (fileExists) { - events.add(FileSystemModifyEvent(path, false, false)); + events.add(Event.modifyFile(path)); } else { - events.add(FileSystemDeleteEvent(path, false)); + events.add(Event.delete(path)); } } else if (dirExisted) { if (dirExists) { // If we got contradictory events for a directory that used to exist and // still exists, we need to rescan the whole thing in case it was // replaced with a different directory. - events.add(FileSystemDeleteEvent(path, true)); - events.add(FileSystemCreateEvent(path, true)); + events.add(Event.delete(path)); + events.add(Event.createDirectory(path)); } else { - events.add(FileSystemDeleteEvent(path, true)); + events.add(Event.delete(path)); } } if (!fileExisted && fileExists) { - events.add(FileSystemCreateEvent(path, false)); + events.add(Event.createFile(path)); } else if (!dirExisted && dirExists) { - events.add(FileSystemCreateEvent(path, true)); + events.add(Event.createDirectory(path)); } return events; diff --git a/pkgs/watcher/lib/src/directory_watcher/windows.dart b/pkgs/watcher/lib/src/directory_watcher/windows.dart index 87eca0f2f..402babebd 100644 --- a/pkgs/watcher/lib/src/directory_watcher/windows.dart +++ b/pkgs/watcher/lib/src/directory_watcher/windows.dart @@ -10,6 +10,7 @@ import 'dart:io'; import 'package:path/path.dart' as p; import '../directory_watcher.dart'; +import '../event.dart'; import '../path_set.dart'; import '../resubscribable.dart'; import '../utils.dart'; @@ -26,11 +27,11 @@ class WindowsDirectoryWatcher extends ResubscribableWatcher class _EventBatcher { static const Duration _batchDelay = Duration(milliseconds: 100); - final List events = []; + final List events = []; Timer? timer; void addEvent(FileSystemEvent event, void Function() callback) { - events.add(event); + events.add(Event(event)); timer?.cancel(); timer = Timer(_batchDelay, callback); } @@ -173,7 +174,7 @@ class _WindowsDirectoryWatcher } /// The callback that's run when [Directory.watch] emits a batch of events. - void _onBatch(List batch) { + void _onBatch(List batch) { _sortEvents(batch).forEach((path, eventSet) { var canonicalEvent = _canonicalEvent(eventSet); var events = canonicalEvent == null @@ -181,49 +182,56 @@ class _WindowsDirectoryWatcher : [canonicalEvent]; for (var event in events) { - if (event is FileSystemCreateEvent) { - if (!event.isDirectory) { + switch (event.type) { + case EventType.createFile: if (_files.contains(path)) continue; - _emitEvent(ChangeType.ADD, path); _files.add(path); - continue; - } - if (_files.containsDir(path)) continue; - - // "Path not found" can be caused by creating then quickly removing - // a directory: continue without reporting an error. Nested files - // that get removed during the `list` are already ignored by `list` - // itself, so there are no other types of "path not found" that - // might need different handling here. - var stream = Directory(path) - .list(recursive: true) - .ignoring(); - var subscription = stream.listen((entity) { - if (entity is Directory) return; - if (_files.contains(entity.path)) return; - - _emitEvent(ChangeType.ADD, entity.path); - _files.add(entity.path); - }, cancelOnError: true); - subscription.onDone(() { - _listSubscriptions.remove(subscription); - }); - subscription.onError((Object e, StackTrace stackTrace) { - _listSubscriptions.remove(subscription); - _emitError(e, stackTrace); - }); - _listSubscriptions.add(subscription); - } else if (event is FileSystemModifyEvent) { - if (!event.isDirectory) { + case EventType.createDirectory: + if (_files.containsDir(path)) continue; + + // "Path not found" can be caused by creating then quickly removing + // a directory: continue without reporting an error. Nested files + // that get removed during the `list` are already ignored by `list` + // itself, so there are no other types of "path not found" that + // might need different handling here. + var stream = Directory(path) + .list(recursive: true) + .ignoring(); + var subscription = stream.listen((entity) { + if (entity is Directory) return; + if (_files.contains(entity.path)) return; + + _emitEvent(ChangeType.ADD, entity.path); + _files.add(entity.path); + }, cancelOnError: true); + subscription.onDone(() { + _listSubscriptions.remove(subscription); + }); + subscription.onError((Object e, StackTrace stackTrace) { + _listSubscriptions.remove(subscription); + _emitError(e, stackTrace); + }); + _listSubscriptions.add(subscription); + + case EventType.modifyFile: _emitEvent(ChangeType.MODIFY, path); - } - } else { - assert(event is FileSystemDeleteEvent); - for (var removedPath in _files.remove(path)) { - _emitEvent(ChangeType.REMOVE, removedPath); - } + + case EventType.delete: + for (var removedPath in _files.remove(path)) { + _emitEvent(ChangeType.REMOVE, removedPath); + } + + // Move events are removed by `_canonicalEvent` and never returned by + // `_eventsBasedOnFileSystem`. + // + // [EventType.modifyDirectory] is guaranteed not present by + // `_sortEvents`. + case EventType.moveFile: + case EventType.moveDirectory: + case EventType.modifyDirectory: + throw StateError(event.type.name); } } }); @@ -235,43 +243,42 @@ class _WindowsDirectoryWatcher /// for example, a MOVE event becomes a DELETE event for the source and a /// CREATE event for the destination. /// - /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it - /// contain any events relating to [path]. - Map> _sortEvents(List batch) { - var eventsForPaths = >{}; - - // Events within directories that already have events are superfluous; the - // directory's full contents will be examined anyway, so we ignore such - // events. Emitting them could cause useless or out-of-order events. + /// Events of type [EventType.modifyDirectory] are ignored and dropped because + /// they are always accompanied by either an [EventType.createDirectory] or an + /// [EventType.delete]. + Map> _sortEvents(List batch) { + var eventsForPaths = >{}; + + // Events within created or moved directories are not needed as the + // directory's full contents will be listed. var directories = unionAll( batch.map((event) { - if (!event.isDirectory) return {}; - if (event is FileSystemMoveEvent) { - var destination = event.destination; - if (destination != null) { - return {event.path, destination}; - } + if (event.type == EventType.createDirectory || + event.type == EventType.moveDirectory) { + final destination = event.destination; + return {event.path, if (destination != null) destination}; } - return {event.path}; + return const {}; }), ); bool isInModifiedDirectory(String path) => directories.any((dir) => path != dir && p.isWithin(dir, path)); - void addEvent(String path, FileSystemEvent event) { + void addEvent(String path, Event event) { if (isInModifiedDirectory(path)) return; - eventsForPaths.putIfAbsent(path, () => {}).add(event); + eventsForPaths.putIfAbsent(path, () => {}).add(event); } for (var event in batch) { - if (event is FileSystemMoveEvent) { - var destination = event.destination; - if (destination != null) { - addEvent(destination, event); - } + if (event.type == EventType.modifyDirectory) { + continue; + } + addEvent(path, event); + final destination = event.destination; + if (destination != null) { + addEvent(destination, event); } - addEvent(event.path, event); } return eventsForPaths; @@ -287,58 +294,28 @@ class _WindowsDirectoryWatcher /// If [batch] does contain contradictory events, this returns `null` to /// indicate that the state of the path on the filesystem should be checked to /// determine what occurred. - FileSystemEvent? _canonicalEvent(Set batch) { - // An empty batch indicates that we've learned earlier that the batch is - // contradictory (e.g. because of a move). + Event? _canonicalEvent(Set batch) { if (batch.isEmpty) return null; + var types = batch.map((e) => e.type).toSet(); - var type = batch.first.type; - var isDir = batch.first.isDirectory; - - for (var event in batch.skip(1)) { - // If one event reports that the file is a directory and another event - // doesn't, that's a contradiction. - if (isDir != event.isDirectory) return null; - - // Modify events don't contradict either CREATE or REMOVE events. We can - // safely assume the file was modified after a CREATE or before the - // REMOVE; otherwise there will also be a REMOVE or CREATE event - // (respectively) that will be contradictory. - if (event is FileSystemModifyEvent) continue; - assert( - event is FileSystemCreateEvent || - event is FileSystemDeleteEvent || - event is FileSystemMoveEvent, - ); - - // If we previously thought this was a MODIFY, we now consider it to be a - // CREATE or REMOVE event. This is safe for the same reason as above. - if (type == FileSystemEvent.modify) { - type = event.type; - continue; - } + if (types.length == 2 && + types.contains(EventType.modifyFile) && + types.contains(EventType.createFile)) { + types.remove(EventType.modifyFile); + } - // A CREATE event contradicts a REMOVE event and vice versa. - assert( - type == FileSystemEvent.create || - type == FileSystemEvent.delete || - type == FileSystemEvent.move, - ); - if (type != event.type) return null; + if (types.length != 1) { + return null; } - switch (type) { - case FileSystemEvent.create: - return FileSystemCreateEvent(batch.first.path, isDir); - case FileSystemEvent.delete: - return FileSystemDeleteEvent(batch.first.path, isDir); - case FileSystemEvent.modify: - return FileSystemModifyEvent(batch.first.path, isDir, false); - case FileSystemEvent.move: - return null; - default: - throw StateError('unreachable'); + final type = types.first; + + // Move events are always resolved by checking the filesystem state. + if (type == EventType.moveDirectory || type == EventType.moveFile) { + return null; } + + return batch.firstWhere((e) => e.type == type); } /// Returns zero or more events that describe the change between the last @@ -348,7 +325,7 @@ class _WindowsDirectoryWatcher /// to the user, unlike the batched events from [Directory.watch]. The /// returned list may be empty, indicating that no changes occurred to [path] /// (probably indicating that it was created and then immediately deleted). - List _eventsBasedOnFileSystem(String path) { + List _eventsBasedOnFileSystem(String path) { var fileExisted = _files.contains(path); var dirExisted = _files.containsDir(path); @@ -358,32 +335,32 @@ class _WindowsDirectoryWatcher fileExists = File(path).existsSync(); dirExists = Directory(path).existsSync(); } on FileSystemException { - return const []; + return const []; } - var events = []; + var events = []; if (fileExisted) { if (fileExists) { - events.add(FileSystemModifyEvent(path, false, false)); + events.add(Event.modifyFile(path)); } else { - events.add(FileSystemDeleteEvent(path, false)); + events.add(Event.delete(path)); } } else if (dirExisted) { if (dirExists) { // If we got contradictory events for a directory that used to exist and // still exists, we need to rescan the whole thing in case it was // replaced with a different directory. - events.add(FileSystemDeleteEvent(path, true)); - events.add(FileSystemCreateEvent(path, true)); + events.add(Event.delete(path)); + events.add(Event.createDirectory(path)); } else { - events.add(FileSystemDeleteEvent(path, true)); + events.add(Event.delete(path)); } } if (!fileExisted && fileExists) { - events.add(FileSystemCreateEvent(path, false)); + events.add(Event.createFile(path)); } else if (!dirExisted && dirExists) { - events.add(FileSystemCreateEvent(path, true)); + events.add(Event.createDirectory(path)); } return events; diff --git a/pkgs/watcher/lib/src/event.dart b/pkgs/watcher/lib/src/event.dart new file mode 100644 index 000000000..23f97ce79 --- /dev/null +++ b/pkgs/watcher/lib/src/event.dart @@ -0,0 +1,93 @@ +// Copyright (c) 2025, 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. + +import 'dart:io'; + +/// Extension type on [FileSystemEvent]. +/// +/// The [FileSystemDeleteEvent] subclass of [FileSystemEvent] does something +/// surprising for `isDirectory`: it always returns `false`. The constructor +/// accepts a boolean called `isDirectory` but discards it. +/// +/// This extension type provides an `isDelete` that returns `null` for delete +/// events, so it's clear that it's unspecified; static creation methods that +/// only take the values that are actually used. +extension type Event(FileSystemEvent event) { + /// A create event for a file at [path]. + static Event createFile(String path) => + Event(FileSystemCreateEvent(path, false)); + + /// A create event for a directory at [path]. + static Event createDirectory(String path) => + Event(FileSystemCreateEvent(path, true)); + + /// A delete event for [path]. + /// + /// Delete events do not specify whether they are for files or directories. + static Event delete(String path) => Event(FileSystemDeleteEvent( + path, + // `FileSystemDeleteEvent` just discards `isDirectory`. + false /* isDirectory */)); + + /// A modify event for the file at [path]. + static Event modifyFile(String path) => Event(FileSystemModifyEvent( + path, + false /* isDirectory */, + // Don't set it, even pass through from the OS, as it's never used. + false /* contentChanged */)); + + /// A modify event for the directory at [path]. + static Event modifyDirectory(String path) => Event(FileSystemModifyEvent( + path, + true /* isDirectory */, + // `contentChanged` is not used by `package:watcher`, don't set it. + false)); + + /// See [FileSystemEvent.path]. + String get path => event.path; + + bool get isDelete => event.type == FileSystemEvent.delete; + bool get isCreate => event.type == FileSystemEvent.create; + bool get isModify => event.type == FileSystemEvent.modify; + bool get isMove => event.type == FileSystemEvent.move; + + EventType get type { + switch (event.type) { + case FileSystemEvent.create: + return event.isDirectory + ? EventType.createDirectory + : EventType.createFile; + case FileSystemEvent.delete: + return EventType.delete; + case FileSystemEvent.modify: + return event.isDirectory + ? EventType.modifyDirectory + : EventType.modifyFile; + case FileSystemEvent.move: + return event.isDirectory ? EventType.moveDirectory : EventType.moveFile; + default: + throw StateError('Invalid event type ${event.type}.'); + } + } + + /// See [FileSystemMoveEvent.destination]. + /// + /// For other types of event, always `null`. + String? get destination => + isMove ? (event as FileSystemMoveEvent).destination : null; +} + +/// See [FileSystemEvent.type]. +/// +/// This additionally encodes [FileSystemEvent.isDirectory], which is specified +/// for all event types except deletes. +enum EventType { + delete, + createFile, + createDirectory, + modifyFile, + modifyDirectory, + moveFile, + moveDirectory; +} diff --git a/pkgs/watcher/lib/src/file_watcher/native.dart b/pkgs/watcher/lib/src/file_watcher/native.dart index 28cf8a180..79a6f0587 100644 --- a/pkgs/watcher/lib/src/file_watcher/native.dart +++ b/pkgs/watcher/lib/src/file_watcher/native.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:io'; +import '../event.dart'; import '../file_watcher.dart'; import '../resubscribable.dart'; import '../utils.dart'; @@ -33,7 +34,7 @@ class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher { Future get ready => _readyCompleter.future; final _readyCompleter = Completer(); - StreamSubscription>? _subscription; + StreamSubscription>? _subscription; /// On MacOS only, whether the file existed on startup. bool? _existedAtStartup; @@ -65,8 +66,8 @@ class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher { onError: _eventsController.addError, onDone: _onDone); } - void _onBatch(List batch) { - if (batch.any((event) => event.type == FileSystemEvent.delete)) { + void _onBatch(List batch) { + if (batch.any((event) => event.isDelete)) { // If the file is deleted, the underlying stream will close. We handle // emitting our own REMOVE event in [_onDone]. return; @@ -76,8 +77,7 @@ class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher { // On MacOS, a spurious `create` event can be received for a file that is // created just before the `watch`. If the file existed at startup then it // should be ignored. - if (_existedAtStartup! && - batch.every((event) => event.type == FileSystemEvent.create)) { + if (_existedAtStartup! && batch.every((event) => event.isCreate)) { return; } } diff --git a/pkgs/watcher/lib/src/utils.dart b/pkgs/watcher/lib/src/utils.dart index e5ef54c66..c4d608033 100644 --- a/pkgs/watcher/lib/src/utils.dart +++ b/pkgs/watcher/lib/src/utils.dart @@ -6,6 +6,8 @@ import 'dart:async'; import 'dart:collection'; import 'dart:io'; +import 'event.dart'; + /// Returns `true` if [error] is a [FileSystemException] for a missing /// directory. bool isDirectoryNotFoundException(Object error) { @@ -20,7 +22,7 @@ bool isDirectoryNotFoundException(Object error) { Set unionAll(Iterable> sets) => sets.fold({}, (union, set) => union.union(set)); -extension BatchEvents on Stream { +extension BatchEvents on Stream { /// Batches all events that are sent at the same time. /// /// When multiple events are synchronously added to a stream controller, the @@ -28,11 +30,11 @@ extension BatchEvents on Stream { /// asynchronous firing of each event. In order to recreate the synchronous /// batches, this collates all the events that are received in "nearby" /// microtasks. - Stream> batchEvents() { - var batch = Queue(); - return StreamTransformer>.fromHandlers( + Stream> batchEvents() { + var batch = Queue(); + return StreamTransformer>.fromHandlers( handleData: (event, sink) { - batch.add(event); + batch.add(Event(event)); // [Timer.run] schedules an event that runs after any microtasks that have // been scheduled. diff --git a/pkgs/watcher/pubspec.yaml b/pkgs/watcher/pubspec.yaml index c7119052d..fde8fe3dd 100644 --- a/pkgs/watcher/pubspec.yaml +++ b/pkgs/watcher/pubspec.yaml @@ -7,7 +7,7 @@ repository: https://github.com/dart-lang/tools/tree/main/pkgs/watcher issue_tracker: https://github.com/dart-lang/tools/labels/package%3Awatcher environment: - sdk: ^3.1.0 + sdk: ^3.3.0 dependencies: async: ^2.5.0