Skip to content

Commit f5920a2

Browse files
authored
Fix PollingDirectoryWatcher for delete during poll. (#2212)
1 parent 289ba0d commit f5920a2

File tree

4 files changed

+58
-3
lines changed

4 files changed

+58
-3
lines changed

pkgs/watcher/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
moved onto `b`, it would be reported as three events: delete `a`, delete `b`,
1010
create `b`. Now it's reported as two events: delete `a`, modify `b`. This
1111
matches the behavior of the Linux and MacOS watchers.
12+
- Bug fix: with `PollingDirectoryWatcher`, fix spurious modify event emitted
13+
because of a file delete during polling.
1214

1315
## 1.1.4
1416

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,13 @@ class _PollingDirectoryWatcher
160160

161161
if (_events.isClosed) return;
162162

163-
_lastModifieds[file] = modified;
164163
_polledFiles.add(file);
164+
if (modified == null) {
165+
// The file was in the directory listing but has been removed since then.
166+
// Don't add to _lastModifieds, it will be reported as a REMOVE.
167+
return;
168+
}
169+
_lastModifieds[file] = modified;
165170

166171
// Only notify if we're ready to emit events.
167172
if (!isReady) return;

pkgs/watcher/test/directory_watcher/polling_test.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,39 @@ void main() {
3232
writeFile('b.txt', contents: 'after');
3333
await expectModifyEvent('b.txt');
3434
});
35+
36+
// A poll does an async directory list then checks mtime on each file. Check
37+
// handling of a file that is deleted between the two.
38+
test('deletes during poll', () async {
39+
await startWatcher();
40+
41+
for (var i = 0; i != 300; ++i) {
42+
writeFile('$i');
43+
}
44+
// A series of deletes with delays in between for 300ms, which will
45+
// intersect with the 100ms polling multiple times.
46+
for (var i = 0; i != 300; ++i) {
47+
deleteFile('$i');
48+
await Future<void>.delayed(const Duration(milliseconds: 1));
49+
}
50+
51+
final events =
52+
await takeEvents(duration: const Duration(milliseconds: 500));
53+
54+
// Events should be adds and removes that pair up, with no modify events.
55+
final adds = <String>{};
56+
final removes = <String>{};
57+
for (var event in events) {
58+
if (event.type == ChangeType.ADD) {
59+
adds.add(event.path);
60+
} else if (event.type == ChangeType.REMOVE) {
61+
removes.add(event.path);
62+
} else {
63+
fail('Unexpected event: $event');
64+
}
65+
}
66+
expect(adds, removes);
67+
});
3568
});
3669

3770
// Also test with delayed writes and real mtimes.

pkgs/watcher/test/utils.dart

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,11 +254,24 @@ Future<WatchEvent?> waitForEvent({
254254
return result;
255255
}
256256

257-
/// Expects that no events are omitted for [duration].
257+
/// Expects that no events are emitted for [duration].
258258
Future expectNoEvents({Duration duration = const Duration(seconds: 1)}) async {
259259
expect(await waitForEvent(duration: duration), isNull);
260260
}
261261

262+
/// Takes all events emitted for [duration].
263+
Future<List<WatchEvent>> takeEvents({required Duration duration}) async {
264+
final result = <WatchEvent>[];
265+
final stopwatch = Stopwatch()..start();
266+
while (stopwatch.elapsed < duration) {
267+
final event = await waitForEvent(duration: duration - stopwatch.elapsed);
268+
if (event != null) {
269+
result.add(event);
270+
}
271+
}
272+
return result;
273+
}
274+
262275
/// Expects that the next event emitted will be for an add event for [path].
263276
Future expectAddEvent(String path) =>
264277
_expectOrCollect(isWatchEvent(ChangeType.ADD, path));
@@ -445,7 +458,9 @@ void renameDir(String from, String to) {
445458
final knownFilePaths = mockFileModificationTimes.keys.toList();
446459
for (final filePath in knownFilePaths) {
447460
if (p.isWithin(from, filePath)) {
448-
mockFileModificationTimes[filePath.replaceAll(from, to)] =
461+
final movedPath =
462+
p.normalize(p.join(to, filePath.substring(from.length + 1)));
463+
mockFileModificationTimes[movedPath] =
449464
mockFileModificationTimes[filePath]!;
450465
mockFileModificationTimes.remove(filePath);
451466
}

0 commit comments

Comments
 (0)