Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/watcher.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
63 changes: 37 additions & 26 deletions pkgs/watcher/lib/src/directory_watcher/linux.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -81,7 +82,7 @@ class _LinuxDirectoryWatcher
})));

// Batch the inotify changes together so that we can dedup events.
var innerStream = _nativeEvents.stream.batchEvents();
var innerStream = _nativeEvents.stream.map(Event.new).batchEvents();
_listen(innerStream, _onBatch,
onError: (Object error, StackTrace stackTrace) {
// Guarantee that ready always completes.
Expand Down Expand Up @@ -145,7 +146,7 @@ class _LinuxDirectoryWatcher
}

/// The callback that's run when a batch of changes comes in.
void _onBatch(List<FileSystemEvent> batch) {
void _onBatch(List<Event> batch) {
var files = <String>{};
var dirs = <String>{};
var changed = <String>{};
Expand All @@ -162,30 +163,40 @@ 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;

changed.add(destination);
if (event.isDirectory) {
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);
switch (event.type) {
case EventType.moveFile:
files.remove(event.path);
dirs.remove(event.path);
var destination = event.destination;
if (destination != null) {
changed.add(destination);
files.add(destination);
dirs.remove(destination);
}

case EventType.moveDirectory:
files.remove(event.path);
dirs.remove(event.path);
var destination = event.destination;
if (destination != null) {
changed.add(destination);
files.remove(destination);
dirs.add(destination);
}

case EventType.delete:
files.remove(event.path);
dirs.remove(event.path);

case EventType.createDirectory:
case EventType.modifyDirectory:
files.remove(event.path);
dirs.add(event.path);

case EventType.createFile:
case EventType.modifyFile:
files.add(event.path);
dirs.remove(event.path);
}
}

Expand Down
59 changes: 59 additions & 0 deletions pkgs/watcher/lib/src/event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 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 replacing [FileSystemEvent] for `package:watcher` internal
/// use.
///
/// The [FileSystemDeleteEvent] subclass of [FileSystemEvent] does something
/// surprising for `isDirectory`: it always returns `false`. The constructor
/// accepts a boolean called `isDirectory` but discards it.
///
/// So, this extension type hides `isDirectory` and instead provides an
/// [EventType] enum with the seven types of event actually used.
extension type Event(FileSystemEvent event) {
/// See [FileSystemEvent.path].
String get path => event.path;

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 => event.type == FileSystemEvent.move
? (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;
}
11 changes: 6 additions & 5 deletions pkgs/watcher/lib/src/file_watcher/native.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:io';

import '../event.dart';
import '../file_watcher.dart';
import '../resubscribable.dart';
import '../utils.dart';
Expand Down Expand Up @@ -33,7 +34,7 @@ class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher {
Future<void> get ready => _readyCompleter.future;
final _readyCompleter = Completer<void>();

StreamSubscription<List<FileSystemEvent>>? _subscription;
StreamSubscription<List<Event>>? _subscription;

/// On MacOS only, whether the file existed on startup.
bool? _existedAtStartup;
Expand All @@ -50,7 +51,7 @@ class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher {
var file = File(path);

// Batch the events together so that we can dedupe them.
var stream = file.watch().batchEvents();
var stream = file.watch().map(Event.new).batchEvents();

if (Platform.isMacOS) {
var existedAtStartupFuture = file.exists();
Expand All @@ -65,8 +66,8 @@ class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher {
onError: _eventsController.addError, onDone: _onDone);
}

void _onBatch(List<FileSystemEvent> batch) {
if (batch.any((event) => event.type == FileSystemEvent.delete)) {
void _onBatch(List<Event> batch) {
if (batch.any((event) => event.type == EventType.delete)) {
// If the file is deleted, the underlying stream will close. We handle
// emitting our own REMOVE event in [_onDone].
return;
Expand All @@ -77,7 +78,7 @@ class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher {
// 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)) {
batch.every((event) => event.type == EventType.createFile)) {
return;
}
}
Expand Down
2 changes: 1 addition & 1 deletion pkgs/watcher/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading