Skip to content

Conversation

@davidmorgan
Copy link
Contributor

@davidmorgan davidmorgan commented Dec 8, 2025

(I guess dart-ecosystem-team was added to the review due to the min SDK version bump and corresponding test workflow change, hi!)

The MacOS directory watcher is fundamentally the same as the Windows one: both watch the root recursively and need to poll the filesystem to learn more about events. It turns out that it can work for both :) with some Windows-specific fixes patched in.

Remove the Windows watcher code, use the Mac one, adapt it as needed.

I didn't rename the Mac watcher files as that would make the diffs hard to read, will do that in a follow-up.

  • Add support for logging to the Windows watcher when it runs in an isolate, it's hard to debug issues otherwise :)
  • Remove utils.dart, remove a few things to the one place they are used and event batching code to its own file.
  • Move event batching code into event_batching.dart and add a test. Add buffering by path version that's equivalent to what the Windows watcher was doing, except that it checks all buffered events via a single timer instead of maintaining+resetting one timer per path.
  • Make unix_paths support Windows path separators too, rename to paths. Add test coverage around relative paths / filesystem roots to guard against this breaking something Windows-specific.
  • Add test coverage around case-insensitive file systems. This shows up several issues on Windows+Mac, but they are existing issues not new to this PR, so leave them as-is for now.
  • Use the fake_async package to test event batching with buffering by path, this introduces a min SDK version of 3.4 so increase the min SDK version.
  • PathSet is no longer used, replaced by the nested DirectoryTree instances in the combined Mac+Windows watcher, remove it + test.
  • Remove Windows-specific tests that were targeted at the old Windows implementation. These robustness tests are anyway well covered by the newer e2e tests.

@github-actions
Copy link

github-actions bot commented Dec 8, 2025

Package publishing

Package Version Status Publish tag (post-merge)
package:bazel_worker 1.1.5 already published at pub.dev
package:benchmark_harness 2.4.0 already published at pub.dev
package:boolean_selector 2.1.2 already published at pub.dev
package:browser_launcher 1.1.3 already published at pub.dev
package:cli_config 0.2.1-wip WIP (no publish necessary)
package:cli_util 0.5.0-wip WIP (no publish necessary)
package:clock 1.1.3-wip WIP (no publish necessary)
package:code_builder 4.11.1-wip WIP (no publish necessary)
package:coverage 1.15.0 already published at pub.dev
package:csslib 1.0.2 already published at pub.dev
package:extension_discovery 2.1.0 already published at pub.dev
package:file 7.0.2-wip WIP (no publish necessary)
package:file_testing 3.1.0-wip WIP (no publish necessary)
package:glob 2.1.3 already published at pub.dev
package:graphs 2.3.3-wip WIP (no publish necessary)
package:html 0.15.7-wip WIP (no publish necessary)
package:io 1.1.0-wip WIP (no publish necessary)
package:json_rpc_2 4.1.0-wip WIP (no publish necessary)
package:markdown 7.3.1-wip WIP (no publish necessary)
package:mime 2.1.0-wip WIP (no publish necessary)
package:oauth2 2.0.5 already published at pub.dev
package:package_config 2.3.0-wip WIP (no publish necessary)
package:pool 1.5.2 already published at pub.dev
package:process 5.0.5 already published at pub.dev
package:pub_semver 2.2.0 already published at pub.dev
package:pubspec_parse 1.5.1-wip WIP (no publish necessary)
package:source_map_stack_trace 2.1.3-wip WIP (no publish necessary)
package:source_maps 0.10.14-wip WIP (no publish necessary)
package:source_span 1.10.1 already published at pub.dev
package:sse 4.1.8 already published at pub.dev
package:stack_trace 1.12.2-wip (error) pubspec version (1.12.2-wip) and changelog (1.12.2-dev) don't agree
package:stream_channel 2.1.4 already published at pub.dev
package:stream_transform 2.1.2-wip WIP (no publish necessary)
package:string_scanner 1.4.1 already published at pub.dev
package:term_glyph 1.2.3-wip WIP (no publish necessary)
package:test_reflective_loader 0.5.0 ready to publish test_reflective_loader-v0.5.0
package:timing 1.0.2 already published at pub.dev
package:unified_analytics 8.0.9 ready to publish unified_analytics-v8.0.9
package:watcher 1.1.5-wip WIP (no publish necessary)
package:yaml 3.1.3 already published at pub.dev
package:yaml_edit 2.2.3 already published at pub.dev

Documentation at https://github.com/dart-lang/ecosystem/wiki/Publishing-automation.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is an excellent pull request that unifies the directory watcher implementations for macOS and Windows. By removing the old Windows-specific code and adapting the macOS watcher to be cross-platform, you've significantly improved the maintainability of the codebase. The changes thoughtfully address Windows-specific filesystem quirks, and the refactoring of utility files into event_batching.dart, paths.dart, and testing.dart greatly improves code organization. The test suite has also been enhanced with better utilities and new tests for edge cases like relative paths and case-insensitive filesystems. I have a few minor suggestions to further polish this work, mainly around documentation and test cleanup. Overall, this is a high-quality and impactful change.

@github-actions
Copy link

github-actions bot commented Dec 8, 2025

PR Health

License Headers ✔️
// 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.
Files
no missing headers

All source files should start with a license header.

Unrelated files missing license headers
Files
pkgs/bazel_worker/benchmark/benchmark.dart
pkgs/benchmark_harness/integration_test/perf_benchmark_test.dart
pkgs/boolean_selector/example/example.dart
pkgs/clock/lib/clock.dart
pkgs/clock/lib/src/clock.dart
pkgs/clock/lib/src/default.dart
pkgs/clock/lib/src/stopwatch.dart
pkgs/clock/lib/src/utils.dart
pkgs/clock/test/clock_test.dart
pkgs/clock/test/default_test.dart
pkgs/clock/test/stopwatch_test.dart
pkgs/clock/test/utils.dart
pkgs/coverage/lib/src/coverage_options.dart
pkgs/html/example/main.dart
pkgs/html/lib/dom.dart
pkgs/html/lib/dom_parsing.dart
pkgs/html/lib/html_escape.dart
pkgs/html/lib/parser.dart
pkgs/html/lib/src/constants.dart
pkgs/html/lib/src/encoding_parser.dart
pkgs/html/lib/src/html_input_stream.dart
pkgs/html/lib/src/list_proxy.dart
pkgs/html/lib/src/query_selector.dart
pkgs/html/lib/src/token.dart
pkgs/html/lib/src/tokenizer.dart
pkgs/html/lib/src/treebuilder.dart
pkgs/html/lib/src/utils.dart
pkgs/html/test/dom_test.dart
pkgs/html/test/parser_feature_test.dart
pkgs/html/test/parser_test.dart
pkgs/html/test/query_selector_test.dart
pkgs/html/test/selectors/level1_baseline_test.dart
pkgs/html/test/selectors/level1_lib.dart
pkgs/html/test/selectors/selectors.dart
pkgs/html/test/support.dart
pkgs/html/test/tokenizer_test.dart
pkgs/html/test/trie_test.dart
pkgs/html/tool/generate_trie.dart
pkgs/pubspec_parse/test/git_uri_test.dart
pkgs/stack_trace/example/example.dart
pkgs/watcher/test/custom_watcher_factory_test.dart
pkgs/yaml_edit/example/example.dart

This check can be disabled by tagging the PR with skip-license-check.

API leaks ✔️

The following packages contain symbols visible in the public API, but not exported by the library. Export these symbols or remove them from your publicly visible API.

Package Leaked API symbol Leaking sources

This check can be disabled by tagging the PR with skip-leaking-check.

Breaking changes ⚠️
Package Change Current Version New Version Needed Version Looking good?
watcher Non-Breaking 1.1.4 1.1.5-wip 1.2.0
Got "1.1.5-wip" expected >= "1.2.0" (non-breaking changes)
⚠️

This check can be disabled by tagging the PR with skip-breaking-check.

Coverage ⚠️
File Coverage
pkgs/watcher/lib/src/directory_watcher.dart 💚 50 %
pkgs/watcher/lib/src/directory_watcher/directory_list.dart 💚 68 % ⬆️ 1 %
pkgs/watcher/lib/src/directory_watcher/event_tree.dart 💚 69 %
pkgs/watcher/lib/src/directory_watcher/linux/native_watch.dart 💚 100 %
pkgs/watcher/lib/src/directory_watcher/linux/watch_tree.dart 💚 96 %
pkgs/watcher/lib/src/directory_watcher/linux/watch_tree_root.dart 💚 94 %
pkgs/watcher/lib/src/directory_watcher/macos/directory_tree.dart 💔 0 % ⬇️ NaN %
pkgs/watcher/lib/src/directory_watcher/macos/native_watch.dart 💔 0 % ⬇️ NaN %
pkgs/watcher/lib/src/directory_watcher/macos/watched_directory_tree.dart 💔 0 % ⬇️ NaN %
pkgs/watcher/lib/src/directory_watcher/polling.dart 💔 78 % ⬇️ 6 %
pkgs/watcher/lib/src/directory_watcher/windows.dart 💔 0 % ⬇️ NaN %
pkgs/watcher/lib/src/directory_watcher/windows_isolate_directory_watcher.dart 💔 0 % ⬇️ NaN %
pkgs/watcher/lib/src/event.dart 💚 73 % ⬆️ 8 %
pkgs/watcher/lib/src/event_batching.dart 💚 100 %
pkgs/watcher/lib/src/file_watcher/native.dart 💚 81 %
pkgs/watcher/lib/src/paths.dart 💚 71 %
pkgs/watcher/lib/src/testing.dart 💚 25 %

This check for test coverage is informational (issues shown here will not fail the PR).

This check can be disabled by tagging the PR with skip-coverage-check.

Changelog Entry ✔️
Package Changed Files

Changes to files need to be accounted for in their respective changelogs.

This check can be disabled by tagging the PR with skip-changelog-check.

@github-actions github-actions bot added the type-infra A repository infrastructure change or enhancement label Dec 8, 2025
@davidmorgan davidmorgan force-pushed the push-zmmxmtltpyqo branch 4 times, most recently from df16638 to 6d951da Compare December 8, 2025 13:24
@davidmorgan davidmorgan marked this pull request as ready for review December 8, 2025 14:29
@davidmorgan davidmorgan requested review from a team as code owners December 8, 2025 14:29
(event) {
// Only look at events for [watchedDirectory].
final eventPath = AbsolutePath(event.path);
if (AbsolutePath(event.path).basename != watchedDirectory.basename) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (AbsolutePath(event.path).basename != watchedDirectory.basename) {
if (eventPath.basename != watchedDirectory.basename) {

Comment on lines +173 to +177
// Wait to work around https://github.com/dart-lang/sdk/issues/61378.
// Give the VM time to reset state after the error. See the issue for
// more discussion of the workaround.
await _subscription?.cancel();
await Future<void>.delayed(const Duration(milliseconds: 1));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have an issue or a TODO filed to remove this? Once we are OK bumping the min SDK in this package can we remove this because the linked issue is resolved?

_ready();
nativeWatch.close();
_eventsController.close();
if (!_eventsController.isClosed) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this something that could have been fixed before this change, or is it specifically happening because of the two code paths getting merged?

}

/// As [checkAndConvert] but also splits up move events.
static List<Event> checkAndConvertAndSplitMoves(FileSystemEvent event) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] IMO this name is less readable than if the operations were split up. Consider using an extension method if you prefer it to read as checkAndConvert(event).splitMoves().

Comment on lines +40 to +44
final result = tryRelativeTo(root);
if (result == null) {
throw ArgumentError('$this relativeTo $root');
}
if (_string == root._string) return RelativePath('');
return RelativePath(_string.substring(root._string.length + 1));
return result;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[optional]

Suggested change
final result = tryRelativeTo(root);
if (result == null) {
throw ArgumentError('$this relativeTo $root');
}
if (_string == root._string) return RelativePath('');
return RelativePath(_string.substring(root._string.length + 1));
return result;
return tryRelativeTo(root) ??
(throw ArgumentError('$this relativeTo $root'));

Comment on lines +126 to +138
final List<Event> _events = [];
DateTime _lastUpdated;

_BufferedEvents() : _lastUpdated = overridableDateTimeNow();

void add(Event event) {
_events.add(event);
_lastUpdated = overridableDateTimeNow();
}

DateTime get lastUpdated => _lastUpdated;

List<Event> get events => _events;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit]

Suggested change
final List<Event> _events = [];
DateTime _lastUpdated;
_BufferedEvents() : _lastUpdated = overridableDateTimeNow();
void add(Event event) {
_events.add(event);
_lastUpdated = overridableDateTimeNow();
}
DateTime get lastUpdated => _lastUpdated;
List<Event> get events => _events;
final List<Event> events = [];
DateTime _lastUpdated;
_BufferedEvents() : _lastUpdated = overridableDateTimeNow();
void add(Event event) {
_events.add(event);
_lastUpdated = overridableDateTimeNow();
}
DateTime get lastUpdated => _lastUpdated;

// Use the current directory name as a check to ensure both don't run at
// the same time. All other tests must use absolute paths.
final testDirectory = Directory(p.join(d.sandbox, 'for_relative_test'));
testDirectory.createSync();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Prefer to use the test_descriptor APIs to make changes under the sandbox directory.

// Testing with relative paths is hard! There's only one current directory
// across the whole VM, tests can run in different isolates, and this test
// runs twice: once with the native watcher and once with the polling one.
// Use the current directory name as a check to ensure both don't run at
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still has a race condition - I imagine that's at least part of the reason for the long 1 second delay before retries?

Should we instead be running with -j 1? Or should we use a file lock to remove the race condition?

});

group('on case-insensitive filesystem', () {
/// Whether the test filesystem is case sensitive.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Don't use 3 slashes for local method definitions since they don't get read by dart doc.

Suggested change
/// Whether the test filesystem is case sensitive.
// Whether the test filesystem is case sensitive.

Or better yet remove - I think it's redundant with the name.

}

test('events with case-only changes', () async {
if (filesystemIsCaseSensitive()) return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about checking the behavior of the file system before tests start, and use skip arguments?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package:watcher type-infra A repository infrastructure change or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants