Skip to content

Commit 50b4514

Browse files
authored
Use Event extension type for Windows DirectoryWatcher. (#2203)
1 parent a0af8b5 commit 50b4514

File tree

3 files changed

+127
-145
lines changed

3 files changed

+127
-145
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ class _MacOSDirectoryWatcher
172172
_emitEvent(ChangeType.REMOVE, removedPath);
173173
}
174174

175+
// Dropped by [Event.checkAndConvert].
175176
case EventType.moveFile:
176177
case EventType.moveDirectory:
177178
case EventType.modifyDirectory:
@@ -183,7 +184,7 @@ class _MacOSDirectoryWatcher
183184

184185
/// Sort all the events in a batch into sets based on their path.
185186
///
186-
/// Events for `path` are discarded.
187+
/// Events for [path] are discarded.
187188
///
188189
/// Events under directories that are created are discarded.
189190
Map<String, Set<Event>> _sortEvents(List<Event> batch) {

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

Lines changed: 102 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'dart:io';
1010
import 'package:path/path.dart' as p;
1111

1212
import '../directory_watcher.dart';
13+
import '../event.dart';
1314
import '../path_set.dart';
1415
import '../resubscribable.dart';
1516
import '../utils.dart';
@@ -26,11 +27,13 @@ class WindowsDirectoryWatcher extends ResubscribableWatcher
2627

2728
class _EventBatcher {
2829
static const Duration _batchDelay = Duration(milliseconds: 100);
29-
final List<FileSystemEvent> events = [];
30+
final List<Event> events = [];
3031
Timer? timer;
3132

3233
void addEvent(FileSystemEvent event, void Function() callback) {
33-
events.add(event);
34+
final convertedEvent = Event.checkAndConvert(event);
35+
if (convertedEvent == null) return;
36+
events.add(convertedEvent);
3437
timer?.cancel();
3538
timer = Timer(_batchDelay, callback);
3639
}
@@ -173,172 +176,136 @@ class _WindowsDirectoryWatcher
173176
}
174177

175178
/// The callback that's run when [Directory.watch] emits a batch of events.
176-
void _onBatch(List<FileSystemEvent> batch) {
179+
void _onBatch(List<Event> batch) {
177180
_sortEvents(batch).forEach((path, eventSet) {
178181
var canonicalEvent = _canonicalEvent(eventSet);
179182
var events = canonicalEvent == null
180183
? _eventsBasedOnFileSystem(path)
181184
: [canonicalEvent];
182185

183186
for (var event in events) {
184-
if (event is FileSystemCreateEvent) {
185-
if (!event.isDirectory) {
187+
switch (event.type) {
188+
case EventType.createFile:
186189
if (_files.contains(path)) continue;
187-
188190
_emitEvent(ChangeType.ADD, path);
189191
_files.add(path);
190-
continue;
191-
}
192192

193-
if (_files.containsDir(path)) continue;
194-
195-
// "Path not found" can be caused by creating then quickly removing
196-
// a directory: continue without reporting an error. Nested files
197-
// that get removed during the `list` are already ignored by `list`
198-
// itself, so there are no other types of "path not found" that
199-
// might need different handling here.
200-
var stream = Directory(path)
201-
.list(recursive: true)
202-
.ignoring<PathNotFoundException>();
203-
var subscription = stream.listen((entity) {
204-
if (entity is Directory) return;
205-
if (_files.contains(entity.path)) return;
206-
207-
_emitEvent(ChangeType.ADD, entity.path);
208-
_files.add(entity.path);
209-
}, cancelOnError: true);
210-
subscription.onDone(() {
211-
_listSubscriptions.remove(subscription);
212-
});
213-
subscription.onError((Object e, StackTrace stackTrace) {
214-
_listSubscriptions.remove(subscription);
215-
_emitError(e, stackTrace);
216-
});
217-
_listSubscriptions.add(subscription);
218-
} else if (event is FileSystemModifyEvent) {
219-
if (!event.isDirectory) {
193+
case EventType.createDirectory:
194+
if (_files.containsDir(path)) continue;
195+
196+
// "Path not found" can be caused by creating then quickly removing
197+
// a directory: continue without reporting an error. Nested files
198+
// that get removed during the `list` are already ignored by `list`
199+
// itself, so there are no other types of "path not found" that
200+
// might need different handling here.
201+
var stream = Directory(path)
202+
.list(recursive: true)
203+
.ignoring<PathNotFoundException>();
204+
var subscription = stream.listen((entity) {
205+
if (entity is Directory) return;
206+
if (_files.contains(entity.path)) return;
207+
208+
_emitEvent(ChangeType.ADD, entity.path);
209+
_files.add(entity.path);
210+
}, cancelOnError: true);
211+
subscription.onDone(() {
212+
_listSubscriptions.remove(subscription);
213+
});
214+
subscription.onError((Object e, StackTrace stackTrace) {
215+
_listSubscriptions.remove(subscription);
216+
_emitError(e, stackTrace);
217+
});
218+
_listSubscriptions.add(subscription);
219+
220+
case EventType.modifyFile:
220221
_emitEvent(ChangeType.MODIFY, path);
221-
}
222-
} else {
223-
assert(event is FileSystemDeleteEvent);
224-
for (var removedPath in _files.remove(path)) {
225-
_emitEvent(ChangeType.REMOVE, removedPath);
226-
}
222+
223+
case EventType.delete:
224+
for (var removedPath in _files.remove(path)) {
225+
_emitEvent(ChangeType.REMOVE, removedPath);
226+
}
227+
228+
// Move events are removed by `_canonicalEvent` and never returned by
229+
// `_eventsBasedOnFileSystem`.
230+
case EventType.moveFile:
231+
case EventType.moveDirectory:
232+
throw StateError(event.type.name);
233+
234+
// Dropped by [Event.checkAndConvert].
235+
case EventType.modifyDirectory:
236+
assert(event.type.isIgnoredOnWindows);
227237
}
228238
}
229239
});
230240
}
231241

232242
/// Sort all the events in a batch into sets based on their path.
233243
///
234-
/// A single input event may result in multiple events in the returned map;
235-
/// for example, a MOVE event becomes a DELETE event for the source and a
236-
/// CREATE event for the destination.
237-
///
238-
/// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
239-
/// contain any events relating to [path].
240-
Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
241-
var eventsForPaths = <String, Set<FileSystemEvent>>{};
242-
243-
// Events within directories that already have events are superfluous; the
244-
// directory's full contents will be examined anyway, so we ignore such
245-
// events. Emitting them could cause useless or out-of-order events.
244+
/// Events for [path] are discarded.
245+
Map<String, Set<Event>> _sortEvents(List<Event> batch) {
246+
var eventsForPaths = <String, Set<Event>>{};
247+
248+
// Events within created or moved directories are not needed as the
249+
// directory's full contents will be listed.
246250
var directories = unionAll(
247251
batch.map((event) {
248-
if (!event.isDirectory) return <String>{};
249-
if (event is FileSystemMoveEvent) {
250-
var destination = event.destination;
251-
if (destination != null) {
252-
return {event.path, destination};
253-
}
252+
if (event.type == EventType.createDirectory ||
253+
event.type == EventType.moveDirectory) {
254+
final destination = event.destination;
255+
return {event.path, if (destination != null) destination};
254256
}
255-
return {event.path};
257+
return const <String>{};
256258
}),
257259
);
258260

259261
bool isInModifiedDirectory(String path) =>
260262
directories.any((dir) => path != dir && p.isWithin(dir, path));
261263

262-
void addEvent(String path, FileSystemEvent event) {
264+
void addEvent(String path, Event event) {
263265
if (isInModifiedDirectory(path)) return;
264-
eventsForPaths.putIfAbsent(path, () => <FileSystemEvent>{}).add(event);
266+
eventsForPaths.putIfAbsent(path, () => <Event>{}).add(event);
265267
}
266268

267269
for (var event in batch) {
268-
if (event is FileSystemMoveEvent) {
269-
var destination = event.destination;
270-
if (destination != null) {
271-
addEvent(destination, event);
272-
}
273-
}
274270
addEvent(event.path, event);
271+
final destination = event.destination;
272+
if (destination != null) {
273+
addEvent(destination, event);
274+
}
275275
}
276276

277277
return eventsForPaths;
278278
}
279279

280-
/// Returns the canonical event from a batch of events on the same path, if
281-
/// one exists.
282-
///
283-
/// If [batch] doesn't contain any contradictory events (e.g. DELETE and
284-
/// CREATE, or events with different values for `isDirectory`), this returns a
285-
/// single event that describes what happened to the path in question.
286-
///
287-
/// If [batch] does contain contradictory events, this returns `null` to
288-
/// indicate that the state of the path on the filesystem should be checked to
289-
/// determine what occurred.
290-
FileSystemEvent? _canonicalEvent(Set<FileSystemEvent> batch) {
291-
// An empty batch indicates that we've learned earlier that the batch is
292-
// contradictory (e.g. because of a move).
280+
/// Returns the canonical event from a batch of events on the same path, or
281+
/// `null` to indicate that the filesystem should be checked.
282+
Event? _canonicalEvent(Set<Event> batch) {
283+
// If the batch is empty, return `null`.
293284
if (batch.isEmpty) return null;
294285

295-
var type = batch.first.type;
296-
var isDir = batch.first.isDirectory;
297-
298-
for (var event in batch.skip(1)) {
299-
// If one event reports that the file is a directory and another event
300-
// doesn't, that's a contradiction.
301-
if (isDir != event.isDirectory) return null;
302-
303-
// Modify events don't contradict either CREATE or REMOVE events. We can
304-
// safely assume the file was modified after a CREATE or before the
305-
// REMOVE; otherwise there will also be a REMOVE or CREATE event
306-
// (respectively) that will be contradictory.
307-
if (event is FileSystemModifyEvent) continue;
308-
assert(
309-
event is FileSystemCreateEvent ||
310-
event is FileSystemDeleteEvent ||
311-
event is FileSystemMoveEvent,
312-
);
313-
314-
// If we previously thought this was a MODIFY, we now consider it to be a
315-
// CREATE or REMOVE event. This is safe for the same reason as above.
316-
if (type == FileSystemEvent.modify) {
317-
type = event.type;
318-
continue;
319-
}
320-
321-
// A CREATE event contradicts a REMOVE event and vice versa.
322-
assert(
323-
type == FileSystemEvent.create ||
324-
type == FileSystemEvent.delete ||
325-
type == FileSystemEvent.move,
326-
);
327-
if (type != event.type) return null;
286+
// Resolve the event type for the batch.
287+
var types = batch.map((e) => e.type).toSet();
288+
EventType type;
289+
if (types.length == 1) {
290+
// There's only one event.
291+
type = types.single;
292+
} else if (types.length == 2 &&
293+
types.contains(EventType.modifyFile) &&
294+
types.contains(EventType.createFile)) {
295+
// Combine events of type [EventType.modifyFile] and
296+
// [EventType.createFile] to one event.
297+
type = EventType.createFile;
298+
} else {
299+
// There are incompatible event types, check the filesystem.
300+
return null;
328301
}
329302

330-
switch (type) {
331-
case FileSystemEvent.create:
332-
return FileSystemCreateEvent(batch.first.path, isDir);
333-
case FileSystemEvent.delete:
334-
return FileSystemDeleteEvent(batch.first.path, isDir);
335-
case FileSystemEvent.modify:
336-
return FileSystemModifyEvent(batch.first.path, isDir, false);
337-
case FileSystemEvent.move:
338-
return null;
339-
default:
340-
throw StateError('unreachable');
303+
// Move events are always resolved by checking the filesystem.
304+
if (type == EventType.moveFile || type == EventType.moveDirectory) {
305+
return null;
341306
}
307+
308+
return batch.firstWhere((e) => e.type == type);
342309
}
343310

344311
/// Returns zero or more events that describe the change between the last
@@ -348,7 +315,7 @@ class _WindowsDirectoryWatcher
348315
/// to the user, unlike the batched events from [Directory.watch]. The
349316
/// returned list may be empty, indicating that no changes occurred to [path]
350317
/// (probably indicating that it was created and then immediately deleted).
351-
List<FileSystemEvent> _eventsBasedOnFileSystem(String path) {
318+
List<Event> _eventsBasedOnFileSystem(String path) {
352319
var fileExisted = _files.contains(path);
353320
var dirExisted = _files.containsDir(path);
354321

@@ -358,32 +325,32 @@ class _WindowsDirectoryWatcher
358325
fileExists = File(path).existsSync();
359326
dirExists = Directory(path).existsSync();
360327
} on FileSystemException {
361-
return const <FileSystemEvent>[];
328+
return const <Event>[];
362329
}
363330

364-
var events = <FileSystemEvent>[];
331+
var events = <Event>[];
365332
if (fileExisted) {
366333
if (fileExists) {
367-
events.add(FileSystemModifyEvent(path, false, false));
334+
events.add(Event.modifyFile(path));
368335
} else {
369-
events.add(FileSystemDeleteEvent(path, false));
336+
events.add(Event.delete(path));
370337
}
371338
} else if (dirExisted) {
372339
if (dirExists) {
373340
// If we got contradictory events for a directory that used to exist and
374341
// still exists, we need to rescan the whole thing in case it was
375342
// replaced with a different directory.
376-
events.add(FileSystemDeleteEvent(path, true));
377-
events.add(FileSystemCreateEvent(path, true));
343+
events.add(Event.delete(path));
344+
events.add(Event.createDirectory(path));
378345
} else {
379-
events.add(FileSystemDeleteEvent(path, true));
346+
events.add(Event.delete(path));
380347
}
381348
}
382349

383350
if (!fileExisted && fileExists) {
384-
events.add(FileSystemCreateEvent(path, false));
351+
events.add(Event.createFile(path));
385352
} else if (!dirExisted && dirExists) {
386-
events.add(FileSystemCreateEvent(path, true));
353+
events.add(Event.createDirectory(path));
387354
}
388355

389356
return events;

0 commit comments

Comments
 (0)