Skip to content

Commit a0af8b5

Browse files
authored
Use Event extension type for MacOS DirectoryWatcher. (#2201)
1 parent 0f50f06 commit a0af8b5

File tree

5 files changed

+158
-140
lines changed

5 files changed

+158
-140
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class _LinuxDirectoryWatcher
8282
})));
8383

8484
// Batch the inotify changes together so that we can dedup events.
85-
var innerStream = _nativeEvents.stream.map(Event.new).batchEvents();
85+
var innerStream = _nativeEvents.stream.batchAndConvertEvents();
8686
_listen(innerStream, _onBatch,
8787
onError: (Object error, StackTrace stackTrace) {
8888
// Guarantee that ready always completes.

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

Lines changed: 94 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'dart:io';
88
import 'package:path/path.dart' as p;
99

1010
import '../directory_watcher.dart';
11+
import '../event.dart';
1112
import '../path_set.dart';
1213
import '../resubscribable.dart';
1314
import '../utils.dart';
@@ -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<Event>>? _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<Event> 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.
@@ -132,8 +133,8 @@ class _MacOSDirectoryWatcher
132133
: [canonicalEvent];
133134

134135
for (var event in events) {
135-
if (event is FileSystemCreateEvent) {
136-
if (!event.isDirectory) {
136+
switch (event.type) {
137+
case EventType.createFile:
137138
// If we already know about the file, treat it like a modification.
138139
// This can happen if a file is copied on top of an existing one.
139140
// We'll see an ADD event for the latter file when from the user's
@@ -143,157 +144,117 @@ class _MacOSDirectoryWatcher
143144

144145
_emitEvent(type, path);
145146
_files.add(path);
146-
continue;
147-
}
148-
149-
if (_files.containsDir(path)) continue;
150-
151-
var stream = Directory(path)
152-
.list(recursive: true)
153-
.ignoring<PathNotFoundException>();
154-
var subscription = stream.listen((entity) {
155-
if (entity is Directory) return;
156-
if (_files.contains(path)) return;
157-
158-
_emitEvent(ChangeType.ADD, entity.path);
159-
_files.add(entity.path);
160-
}, cancelOnError: true);
161-
subscription.onDone(() {
162-
_listSubscriptions.remove(subscription);
163-
});
164-
subscription.onError(_emitError);
165-
_listSubscriptions.add(subscription);
166-
} else if (event is FileSystemModifyEvent) {
167-
assert(!event.isDirectory);
168-
_emitEvent(ChangeType.MODIFY, path);
169-
} else {
170-
assert(event is FileSystemDeleteEvent);
171-
for (var removedPath in _files.remove(path)) {
172-
_emitEvent(ChangeType.REMOVE, removedPath);
173-
}
147+
148+
case EventType.createDirectory:
149+
if (_files.containsDir(path)) continue;
150+
151+
var stream = Directory(path)
152+
.list(recursive: true)
153+
.ignoring<PathNotFoundException>();
154+
var subscription = stream.listen((entity) {
155+
if (entity is Directory) return;
156+
if (_files.contains(path)) return;
157+
158+
_emitEvent(ChangeType.ADD, entity.path);
159+
_files.add(entity.path);
160+
}, cancelOnError: true);
161+
subscription.onDone(() {
162+
_listSubscriptions.remove(subscription);
163+
});
164+
subscription.onError(_emitError);
165+
_listSubscriptions.add(subscription);
166+
167+
case EventType.modifyFile:
168+
_emitEvent(ChangeType.MODIFY, path);
169+
170+
case EventType.delete:
171+
for (var removedPath in _files.remove(path)) {
172+
_emitEvent(ChangeType.REMOVE, removedPath);
173+
}
174+
175+
case EventType.moveFile:
176+
case EventType.moveDirectory:
177+
case EventType.modifyDirectory:
178+
assert(event.type.isNeverReceivedOnMacOS);
174179
}
175180
}
176181
});
177182
}
178183

179184
/// Sort all the events in a batch into sets based on their path.
180185
///
181-
/// A single input event may result in multiple events in the returned map;
182-
/// for example, a MOVE event becomes a DELETE event for the source and a
183-
/// CREATE event for the destination.
186+
/// Events for `path` are discarded.
184187
///
185-
/// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
186-
/// contain any events relating to [path].
187-
Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
188-
var eventsForPaths = <String, Set<FileSystemEvent>>{};
188+
/// Events under directories that are created are discarded.
189+
Map<String, Set<Event>> _sortEvents(List<Event> batch) {
190+
var eventsForPaths = <String, Set<Event>>{};
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
192194
// really deleted, that's handled by [_onDone].
193195
batch = batch.where((event) => event.path != path).toList();
194196

195-
// Events within directories that already have events are superfluous; the
196-
// directory's full contents will be examined anyway, so we ignore such
197-
// events. Emitting them could cause useless or out-of-order events.
198-
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};
197+
// Events within directories that already have create events are not needed
198+
// as the directory's full content will be listed.
199+
var createdDirectories = unionAll(batch.map((event) {
200+
return event.type == EventType.createDirectory
201+
? {event.path}
202+
: const <String>{};
207203
}));
208204

209-
bool isInModifiedDirectory(String path) =>
210-
directories.any((dir) => path != dir && p.isWithin(dir, path));
205+
bool isInCreatedDirectory(String path) =>
206+
createdDirectories.any((dir) => path != dir && p.isWithin(dir, path));
211207

212-
void addEvent(String path, FileSystemEvent event) {
213-
if (isInModifiedDirectory(path)) return;
214-
eventsForPaths.putIfAbsent(path, () => <FileSystemEvent>{}).add(event);
208+
void addEvent(String path, Event event) {
209+
if (isInCreatedDirectory(path)) return;
210+
eventsForPaths.putIfAbsent(path, () => <Event>{}).add(event);
215211
}
216212

217213
for (var event in batch) {
218-
// The Mac OS watcher doesn't emit move events. See issue 14806.
219-
assert(event is! FileSystemMoveEvent);
220214
addEvent(event.path, event);
221215
}
222216

223217
return eventsForPaths;
224218
}
225219

226-
/// Returns the canonical event from a batch of events on the same path, if
227-
/// one exists.
228-
///
229-
/// If [batch] doesn't contain any contradictory events (e.g. DELETE and
230-
/// CREATE, or events with different values for `isDirectory`), this returns a
231-
/// single event that describes what happened to the path in question.
232-
///
233-
/// If [batch] does contain contradictory events, this returns `null` to
234-
/// indicate that the state of the path on the filesystem should be checked to
235-
/// determine what occurred.
236-
FileSystemEvent? _canonicalEvent(Set<FileSystemEvent> batch) {
237-
// An empty batch indicates that we've learned earlier that the batch is
238-
// contradictory (e.g. because of a move).
220+
/// Returns the canonical event from a batch of events on the same path, or
221+
/// `null` to indicate that the filesystem should be checked.
222+
Event? _canonicalEvent(Set<Event> batch) {
223+
// If the batch is empty, return `null`.
239224
if (batch.isEmpty) return null;
240225

241-
var type = batch.first.type;
242-
var isDir = batch.first.isDirectory;
243-
var hadModifyEvent = false;
244-
245-
for (var event in batch.skip(1)) {
246-
// If one event reports that the file is a directory and another event
247-
// doesn't, that's a contradiction.
248-
if (isDir != event.isDirectory) return null;
249-
250-
// Modify events don't contradict either CREATE or REMOVE events. We can
251-
// safely assume the file was modified after a CREATE or before the
252-
// REMOVE; otherwise there will also be a REMOVE or CREATE event
253-
// (respectively) that will be contradictory.
254-
if (event is FileSystemModifyEvent) {
255-
hadModifyEvent = true;
256-
continue;
257-
}
258-
assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent);
259-
260-
// If we previously thought this was a MODIFY, we now consider it to be a
261-
// CREATE or REMOVE event. This is safe for the same reason as above.
262-
if (type == FileSystemEvent.modify) {
263-
type = event.type;
264-
continue;
226+
// Resolve the event type for the batch.
227+
var types = batch.map((e) => e.type).toSet();
228+
EventType type;
229+
if (types.length == 1) {
230+
// There's only one event.
231+
type = types.single;
232+
} else if (types.length == 2 &&
233+
types.contains(EventType.modifyFile) &&
234+
types.contains(EventType.createFile)) {
235+
// Combine events of type [EventType.modifyFile] and
236+
// [EventType.createFile] to one event.
237+
if (_files.contains(batch.first.path)) {
238+
// The file already existed: this can happen due to a create from
239+
// before the watcher started being reported.
240+
type = EventType.modifyFile;
241+
} else {
242+
type = EventType.createFile;
265243
}
266-
267-
// A CREATE event contradicts a REMOVE event and vice versa.
268-
assert(type == FileSystemEvent.create || type == FileSystemEvent.delete);
269-
if (type != event.type) return null;
244+
} else {
245+
// There are incompatible event types, check the filesystem.
246+
return null;
270247
}
271248

272-
// If we got a CREATE event for a file we already knew about, that comes
273-
// from FSEvents reporting an add that happened prior to the watch
274-
// beginning. If we also received a MODIFY event, we want to report that,
275-
// but not the CREATE.
276-
if (type == FileSystemEvent.create &&
277-
hadModifyEvent &&
278-
_files.contains(batch.first.path)) {
279-
type = FileSystemEvent.modify;
249+
// Issue 16003 means that a CREATE event for a directory can indicate
250+
// that the directory was moved and then re-created.
251+
// [_eventsBasedOnFileSystem] will handle this correctly by producing a
252+
// DELETE event followed by a CREATE event if the directory exists.
253+
if (type == EventType.createDirectory) {
254+
return null;
280255
}
281256

282-
switch (type) {
283-
case FileSystemEvent.create:
284-
// Issue 16003 means that a CREATE event for a directory can indicate
285-
// that the directory was moved and then re-created.
286-
// [_eventsBasedOnFileSystem] will handle this correctly by producing a
287-
// DELETE event followed by a CREATE event if the directory exists.
288-
if (isDir) return null;
289-
return FileSystemCreateEvent(batch.first.path, false);
290-
case FileSystemEvent.delete:
291-
return FileSystemDeleteEvent(batch.first.path, isDir);
292-
case FileSystemEvent.modify:
293-
return FileSystemModifyEvent(batch.first.path, isDir, false);
294-
default:
295-
throw StateError('unreachable');
296-
}
257+
return batch.firstWhere((e) => e.type == type);
297258
}
298259

299260
/// Returns one or more events that describe the change between the last known
@@ -303,35 +264,35 @@ class _MacOSDirectoryWatcher
303264
/// to the user, unlike the batched events from [Directory.watch]. The
304265
/// returned list may be empty, indicating that no changes occurred to [path]
305266
/// (probably indicating that it was created and then immediately deleted).
306-
List<FileSystemEvent> _eventsBasedOnFileSystem(String path) {
267+
List<Event> _eventsBasedOnFileSystem(String path) {
307268
var fileExisted = _files.contains(path);
308269
var dirExisted = _files.containsDir(path);
309270
var fileExists = File(path).existsSync();
310271
var dirExists = Directory(path).existsSync();
311272

312-
var events = <FileSystemEvent>[];
273+
var events = <Event>[];
313274
if (fileExisted) {
314275
if (fileExists) {
315-
events.add(FileSystemModifyEvent(path, false, false));
276+
events.add(Event.modifyFile(path));
316277
} else {
317-
events.add(FileSystemDeleteEvent(path, false));
278+
events.add(Event.delete(path));
318279
}
319280
} else if (dirExisted) {
320281
if (dirExists) {
321282
// If we got contradictory events for a directory that used to exist and
322283
// still exists, we need to rescan the whole thing in case it was
323284
// replaced with a different directory.
324-
events.add(FileSystemDeleteEvent(path, true));
325-
events.add(FileSystemCreateEvent(path, true));
285+
events.add(Event.delete(path));
286+
events.add(Event.createDirectory(path));
326287
} else {
327-
events.add(FileSystemDeleteEvent(path, true));
288+
events.add(Event.delete(path));
328289
}
329290
}
330291

331292
if (!fileExisted && fileExists) {
332-
events.add(FileSystemCreateEvent(path, false));
293+
events.add(Event.createFile(path));
333294
} else if (!dirExisted && dirExists) {
334-
events.add(FileSystemCreateEvent(path, true));
295+
events.add(Event.createDirectory(path));
335296
}
336297

337298
return events;
@@ -362,7 +323,8 @@ class _MacOSDirectoryWatcher
362323
/// Start or restart the underlying [Directory.watch] stream.
363324
void _startWatch() {
364325
// Batch the FSEvent changes together so that we can dedup events.
365-
var innerStream = Directory(path).watch(recursive: true).batchEvents();
326+
var innerStream =
327+
Directory(path).watch(recursive: true).batchAndConvertEvents();
366328
_watchSubscription = innerStream.listen(_onBatch,
367329
onError: _eventsController.addError, onDone: _onDone);
368330
}

pkgs/watcher/lib/src/event.dart

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,47 @@ import 'dart:io';
1313
///
1414
/// So, this extension type hides `isDirectory` and instead provides an
1515
/// [EventType] enum with the seven types of event actually used.
16-
extension type Event(FileSystemEvent event) {
16+
extension type Event._(FileSystemEvent event) {
17+
/// Converts [event] to an [Event].
18+
///
19+
/// Returns `null` and asserts `false` if [event] is unexpected on this
20+
/// platform. So, it will cause tests to fail but real code can continue
21+
/// ignoring the event.
22+
static Event? checkAndConvert(FileSystemEvent event) {
23+
var result = Event._(event);
24+
if (Platform.isMacOS) {
25+
if (result.type.isNeverReceivedOnMacOS) {
26+
assert(false);
27+
return null;
28+
}
29+
}
30+
return result;
31+
}
32+
33+
/// A create event for a file at [path].
34+
static Event createFile(String path) =>
35+
Event._(FileSystemCreateEvent(path, false));
36+
37+
/// A create event for a directory at [path].
38+
static Event createDirectory(String path) =>
39+
Event._(FileSystemCreateEvent(path, true));
40+
41+
/// A delete event for [path].
42+
///
43+
/// Delete events do not specify whether they are for files or directories.
44+
static Event delete(String path) => Event._(FileSystemDeleteEvent(
45+
path,
46+
// `FileSystemDeleteEvent` just discards `isDirectory`.
47+
false /* isDirectory */));
48+
49+
/// A modify event for the file at [path].
50+
static Event modifyFile(String path) => Event._(FileSystemModifyEvent(
51+
path,
52+
false /* isDirectory */,
53+
// Don't set `contentChanged`, even pass through from the OS, as
54+
// `package:watcher` never reads it.
55+
false /* contentChanged */));
56+
1757
/// See [FileSystemEvent.path].
1858
String get path => event.path;
1959

@@ -56,4 +96,13 @@ enum EventType {
5696
modifyDirectory,
5797
moveFile,
5898
moveDirectory;
99+
100+
bool get isNeverReceivedOnMacOS {
101+
// See https://github.com/dart-lang/sdk/issues/14806.
102+
if (this == moveFile || this == moveDirectory) {
103+
return true;
104+
}
105+
if (this == modifyDirectory) return true;
106+
return false;
107+
}
59108
}

0 commit comments

Comments
 (0)