Skip to content

Commit 696a962

Browse files
committed
Add WatcherFilesystemEvent.
1 parent 6866f9b commit 696a962

File tree

7 files changed

+82
-41
lines changed

7 files changed

+82
-41
lines changed

pkgs/watcher/lib/src/directory_watcher/linux.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import '../path_set.dart';
1212
import '../resubscribable.dart';
1313
import '../utils.dart';
1414
import '../watch_event.dart';
15+
import '../watcher_filesystem_event.dart';
1516

1617
/// Uses the inotify subsystem to watch for filesystem events.
1718
///
@@ -145,7 +146,7 @@ class _LinuxDirectoryWatcher
145146
}
146147

147148
/// The callback that's run when a batch of changes comes in.
148-
void _onBatch(List<FileSystemEvent> batch) {
149+
void _onBatch(List<WatcherFilesystemEvent> batch) {
149150
var files = <String>{};
150151
var dirs = <String>{};
151152
var changed = <String>{};
@@ -162,15 +163,15 @@ class _LinuxDirectoryWatcher
162163

163164
changed.add(event.path);
164165

165-
if (event is FileSystemMoveEvent) {
166+
if (event.isMove) {
166167
files.remove(event.path);
167168
dirs.remove(event.path);
168169

169170
var destination = event.destination;
170171
if (destination == null) continue;
171172

172173
changed.add(destination);
173-
if (event.isDirectory) {
174+
if (event.isDirectory!) {
174175
files.remove(destination);
175176
dirs.add(destination);
176177
} else {
@@ -180,7 +181,7 @@ class _LinuxDirectoryWatcher
180181
} else if (event is FileSystemDeleteEvent) {
181182
files.remove(event.path);
182183
dirs.remove(event.path);
183-
} else if (event.isDirectory) {
184+
} else if (event.isDirectory!) {
184185
files.remove(event.path);
185186
dirs.add(event.path);
186187
} else {

pkgs/watcher/lib/src/directory_watcher/mac_os.dart

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import '../path_set.dart';
1212
import '../resubscribable.dart';
1313
import '../utils.dart';
1414
import '../watch_event.dart';
15+
import '../watcher_filesystem_event.dart';
1516

1617
/// Uses the FSEvents subsystem to watch for filesystem events.
1718
///
@@ -63,7 +64,7 @@ class _MacOSDirectoryWatcher
6364
///
6465
/// This is separate from [_listSubscriptions] because this stream
6566
/// occasionally needs to be resubscribed in order to work around issue 14849.
66-
StreamSubscription<List<FileSystemEvent>>? _watchSubscription;
67+
StreamSubscription<List<WatcherFilesystemEvent>>? _watchSubscription;
6768

6869
/// The subscription to the [Directory.list] call for the initial listing of
6970
/// the directory to determine its initial state.
@@ -109,7 +110,7 @@ class _MacOSDirectoryWatcher
109110
}
110111

111112
/// The callback that's run when [Directory.watch] emits a batch of events.
112-
void _onBatch(List<FileSystemEvent> batch) {
113+
void _onBatch(List<WatcherFilesystemEvent> batch) {
113114
// If we get a batch of events before we're ready to begin emitting events,
114115
// it's probable that it's a batch of pre-watcher events (see issue 14373).
115116
// Ignore those events and re-list the directory.
@@ -184,8 +185,9 @@ class _MacOSDirectoryWatcher
184185
///
185186
/// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
186187
/// contain any events relating to [path].
187-
Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
188-
var eventsForPaths = <String, Set<FileSystemEvent>>{};
188+
Map<String, Set<WatcherFilesystemEvent>> _sortEvents(
189+
List<WatcherFilesystemEvent> batch) {
190+
var eventsForPaths = <String, Set<WatcherFilesystemEvent>>{};
189191

190192
// FSEvents can report past events, including events on the root directory
191193
// such as it being created. We want to ignore these. If the directory is
@@ -196,22 +198,18 @@ class _MacOSDirectoryWatcher
196198
// directory's full contents will be examined anyway, so we ignore such
197199
// events. Emitting them could cause useless or out-of-order events.
198200
var directories = unionAll(batch.map((event) {
199-
if (!event.isDirectory) return <String>{};
200-
if (event is FileSystemMoveEvent) {
201-
var destination = event.destination;
202-
if (destination != null) {
203-
return {event.path, destination};
204-
}
205-
}
206-
return {event.path};
201+
if (event.isDirectory == false) return <String>{};
202+
return event.paths;
207203
}));
208204

209205
bool isInModifiedDirectory(String path) =>
210206
directories.any((dir) => path != dir && p.isWithin(dir, path));
211207

212-
void addEvent(String path, FileSystemEvent event) {
208+
void addEvent(String path, WatcherFilesystemEvent event) {
213209
if (isInModifiedDirectory(path)) return;
214-
eventsForPaths.putIfAbsent(path, () => <FileSystemEvent>{}).add(event);
210+
eventsForPaths
211+
.putIfAbsent(path, () => <WatcherFilesystemEvent>{})
212+
.add(event);
215213
}
216214

217215
for (var event in batch) {
@@ -233,29 +231,29 @@ class _MacOSDirectoryWatcher
233231
/// If [batch] does contain contradictory events, this returns `null` to
234232
/// indicate that the state of the path on the filesystem should be checked to
235233
/// determine what occurred.
236-
FileSystemEvent? _canonicalEvent(Set<FileSystemEvent> batch) {
234+
WatcherFilesystemEvent? _canonicalEvent(Set<WatcherFilesystemEvent> batch) {
237235
// An empty batch indicates that we've learned earlier that the batch is
238236
// contradictory (e.g. because of a move).
239237
if (batch.isEmpty) return null;
240238

241239
var type = batch.first.type;
242-
var isDir = batch.first.isDirectory;
240+
var isDirectory = batch.first.isDirectory;
243241
var hadModifyEvent = false;
244242

245243
for (var event in batch.skip(1)) {
246244
// If one event reports that the file is a directory and another event
247245
// doesn't, that's a contradiction.
248-
if (isDir != event.isDirectory) return null;
246+
if (isDirectory != event.isDirectory) return null;
249247

250248
// Modify events don't contradict either CREATE or REMOVE events. We can
251249
// safely assume the file was modified after a CREATE or before the
252250
// REMOVE; otherwise there will also be a REMOVE or CREATE event
253251
// (respectively) that will be contradictory.
254-
if (event is FileSystemModifyEvent) {
252+
if (event.isModify) {
255253
hadModifyEvent = true;
256254
continue;
257255
}
258-
assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent);
256+
assert(event.isCreate || event.isDelete);
259257

260258
// If we previously thought this was a MODIFY, we now consider it to be a
261259
// CREATE or REMOVE event. This is safe for the same reason as above.
@@ -285,12 +283,14 @@ class _MacOSDirectoryWatcher
285283
// that the directory was moved and then re-created.
286284
// [_eventsBasedOnFileSystem] will handle this correctly by producing a
287285
// DELETE event followed by a CREATE event if the directory exists.
288-
if (isDir) return null;
289-
return FileSystemCreateEvent(batch.first.path, false);
286+
if (isDirectory!) return null;
287+
return WatcherFilesystemEvent.create(batch.first.path,
288+
isDirectory: false);
290289
case FileSystemEvent.delete:
291-
return FileSystemDeleteEvent(batch.first.path, isDir);
290+
return WatcherFilesystemEvent.delete(batch.first.path);
292291
case FileSystemEvent.modify:
293-
return FileSystemModifyEvent(batch.first.path, isDir, false);
292+
return WatcherFilesystemEvent.modify(batch.first.path,
293+
isDirectory: isDirectory!);
294294
default:
295295
throw StateError('unreachable');
296296
}

pkgs/watcher/lib/src/file_watcher/native.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import '../file_watcher.dart';
99
import '../resubscribable.dart';
1010
import '../utils.dart';
1111
import '../watch_event.dart';
12+
import '../watcher_filesystem_event.dart';
1213

1314
/// Uses the native file system notifications to watch for filesystem events.
1415
///
@@ -33,7 +34,7 @@ class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher {
3334
Future<void> get ready => _readyCompleter.future;
3435
final _readyCompleter = Completer<void>();
3536

36-
StreamSubscription<List<FileSystemEvent>>? _subscription;
37+
StreamSubscription<List<WatcherFilesystemEvent>>? _subscription;
3738

3839
/// On MacOS only, whether the file existed on startup.
3940
bool? _existedAtStartup;
@@ -65,8 +66,8 @@ class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher {
6566
onError: _eventsController.addError, onDone: _onDone);
6667
}
6768

68-
void _onBatch(List<FileSystemEvent> batch) {
69-
if (batch.any((event) => event.type == FileSystemEvent.delete)) {
69+
void _onBatch(List<WatcherFilesystemEvent> batch) {
70+
if (batch.any((event) => event.isDelete)) {
7071
// If the file is deleted, the underlying stream will close. We handle
7172
// emitting our own REMOVE event in [_onDone].
7273
return;
@@ -76,8 +77,7 @@ class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher {
7677
// On MacOS, a spurious `create` event can be received for a file that is
7778
// created just before the `watch`. If the file existed at startup then it
7879
// should be ignored.
79-
if (_existedAtStartup! &&
80-
batch.every((event) => event.type == FileSystemEvent.create)) {
80+
if (_existedAtStartup! && batch.every((event) => event.isCreate)) {
8181
return;
8282
}
8383
}

pkgs/watcher/lib/src/utils.dart

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import 'dart:async';
66
import 'dart:collection';
77
import 'dart:io';
88

9+
import 'watcher_filesystem_event.dart';
10+
911
/// Returns `true` if [error] is a [FileSystemException] for a missing
1012
/// directory.
1113
bool isDirectoryNotFoundException(Object error) {
@@ -20,19 +22,19 @@ bool isDirectoryNotFoundException(Object error) {
2022
Set<T> unionAll<T>(Iterable<Set<T>> sets) =>
2123
sets.fold(<T>{}, (union, set) => union.union(set));
2224

23-
extension BatchEvents<T> on Stream<T> {
25+
extension BatchEvents on Stream<FileSystemEvent> {
2426
/// Batches all events that are sent at the same time.
2527
///
2628
/// When multiple events are synchronously added to a stream controller, the
2729
/// [StreamController] implementation uses [scheduleMicrotask] to schedule the
2830
/// asynchronous firing of each event. In order to recreate the synchronous
2931
/// batches, this collates all the events that are received in "nearby"
3032
/// microtasks.
31-
Stream<List<T>> batchEvents() {
32-
var batch = Queue<T>();
33-
return StreamTransformer<T, List<T>>.fromHandlers(
34-
handleData: (event, sink) {
35-
batch.add(event);
33+
Stream<List<WatcherFilesystemEvent>> batchEvents() {
34+
var batch = Queue<WatcherFilesystemEvent>();
35+
return StreamTransformer<FileSystemEvent,
36+
List<WatcherFilesystemEvent>>.fromHandlers(handleData: (event, sink) {
37+
batch.add(WatcherFilesystemEvent(event));
3638

3739
// [Timer.run] schedules an event that runs after any microtasks that have
3840
// been scheduled.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:io';
6+
7+
extension type WatcherFilesystemEvent(FileSystemEvent event) {
8+
bool get isDelete => type == FileSystemEvent.delete;
9+
bool get isCreate => type == FileSystemEvent.create;
10+
bool get isModify => type == FileSystemEvent.modify;
11+
bool get isMove => type == FileSystemEvent.move;
12+
13+
String get path => event.path;
14+
String? get destination =>
15+
isMove ? (event as FileSystemMoveEvent).destination : null;
16+
bool? get isDirectory => isDelete ? null : event.isDirectory;
17+
int get type => event.type;
18+
Set<String> get paths => {event.path, if (destination != null) destination!};
19+
20+
static WatcherFilesystemEvent create(String path,
21+
{required bool isDirectory}) =>
22+
WatcherFilesystemEvent(FileSystemCreateEvent(path, isDirectory));
23+
24+
static WatcherFilesystemEvent delete(String path) =>
25+
WatcherFilesystemEvent(FileSystemDeleteEvent(
26+
path,
27+
// `FileSystemDeleteEvent` just discards `isDirectory`, nothing to pass
28+
// here.
29+
false));
30+
31+
static WatcherFilesystemEvent modify(String path,
32+
{required bool isDirectory}) =>
33+
WatcherFilesystemEvent(FileSystemModifyEvent(
34+
path,
35+
isDirectory,
36+
// `contentChanged` is not used by `package:watcher`, don't set it.
37+
false));
38+
}

pkgs/watcher/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ repository: https://github.com/dart-lang/tools/tree/main/pkgs/watcher
77
issue_tracker: https://github.com/dart-lang/tools/labels/package%3Awatcher
88

99
environment:
10-
sdk: ^3.1.0
10+
sdk: ^3.3.0
1111

1212
dependencies:
1313
async: ^2.5.0

pkgs/watcher/test/directory_watcher/shared.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ void sharedTests() {
138138
renameFile('from.txt', 'to.txt');
139139
await inAnyOrder([isRemoveEvent('from.txt'), isModifyEvent('to.txt')]);
140140
}, onPlatform: {
141-
'windows': const Skip('https://github.com/dart-lang/watcher/issues/125')
141+
//'windows': const Skip('https://github.com/dart-lang/watcher/issues/125')
142142
});
143143
});
144144

@@ -281,7 +281,7 @@ void sharedTests() {
281281
isAddEvent('new')
282282
]);
283283
}, onPlatform: {
284-
'windows': const Skip('https://github.com/dart-lang/watcher/issues/21')
284+
// 'windows': const Skip('https://github.com/dart-lang/watcher/issues/21')
285285
});
286286

287287
test('emits events for many nested files added at once', () async {

0 commit comments

Comments
 (0)