Harden the file watcher and remove the reopen-by-id fast path - #57
Conversation
… path, and close the mutation gaps Split out of PR #56, which had grown to 248 files across nine crates and was churning under review. This crate's work is a separate deliverable that happened to travel in the same branch, and it comes out cleanly: `windows-file-watcher`'s Cargo.toml is not in that diff at all -- its dependencies never changed -- and it depends on none of `windows-topology-sys`, `windows-waitable-queues` or `windows-placement-probe`, which is where most of that PR's churn lives. **Squashed deliberately.** The 22 file-watcher-only commits are not reproduced here, because 8 further commits mixed this crate with others and untangling them would produce history that never existed. The original commits remain on `mikegrier/deferred-namespace-ops` for archaeology. BREAKING CHANGE: the reopen-by-id fast path is removed. It was root-caused as impossible rather than merely unused -- a handle reopened by file id rejects the watcher's own read -- so the path could not have worked and its removal takes away nothing a caller could have relied on. What this contains, by theme: - **A use-after-free in the cancellation path.** The watcher freed a cancelled read's buffer without waiting for the kernel to finish with it. It now waits. - **`StandingHold::drop`'s dead release path** replaced with a tripwire, after a mutation run showed nothing could reach it. - **Bounded waits throughout the queue tests**, so a broken wake fails the suite instead of hanging it -- with `NOTIFY_TIMEOUT` measured rather than guessed. - **D-85: paths are the caller's, verbatim.** Recorded as a decision, with the write-only `canonical_path` dropped and the query kept, and a test guarding the pass-through against a helpfully-added prefix. - **Mutation-gap coverage** across `queue.rs`, `directory.rs`, `monitor.rs` and the notification categories, including which survivors are uncatchable and why. Verified on this branch, against main's dependencies rather than the other branch's: 392 tests pass, and `cargo fmt --check`, `cargo clippy --all-targets --all-features -D warnings` and rustdoc under the CI deny flags are all clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
crates/windows-file-watcher/src/directory/tests.rs constructs “UTF-16 unit”-sized path fixtures using .len() on formatted strings (UTF-8 bytes), which can make the new boundary tests flaky or incorrect on non-ASCII paths.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR hardens windows-file-watcher by removing the (now proven impossible) reopen-by-file-id fast path, tightening correctness around overlapped I/O cancellation/arming, and strengthening the test suite to fail fast (bounded waits + mutation-gap coverage) rather than hanging.
Changes:
- Removed reopen-by-id (
OpenFileById) re-establishment and updated design/docs + tests to assert the OS limitation directly. - Extracted and tested the
ReadDirectoryChangesWsubmission classification seam to prevent buffer lifetime misreads (use-after-free class). - Expanded test coverage and reliability: bounded waits, regression guards for multi-watch ordering, and focused unit tests for mutation survivors (queue/monitor/directory helpers + path pass-through decision D-85).
File summaries
| File | Description |
|---|---|
| crates/windows-file-watcher/tests/watched_paths.rs | Lowers NOTIFY_TIMEOUT and points to measured justification. |
| crates/windows-file-watcher/tests/reopen_by_id_cannot_be_watched.rs | New integration test asserting ReadDirectoryChangesW rejects by-id directory handles. |
| crates/windows-file-watcher/tests/fault_detail.rs | Aligns NOTIFY_TIMEOUT with measured 5s budget. |
| crates/windows-file-watcher/tests/consumer_test_surface.rs | Bounds a blocking drain-loop assertion via thread + deadline. |
| crates/windows-file-watcher/src/watcher/tests.rs | Lowers NOTIFY_TIMEOUT to 5s; adds mutation-gap tests and bounded drain-loop assertions. |
| crates/windows-file-watcher/src/watcher.rs | Removes reopen-by-id logic; factors classify_submission; improves volume-change warning context; adds test-only seams. |
| crates/windows-file-watcher/src/watch/tests.rs | Introduces Drain helper to avoid discarding notifications across subscriptions; adds regression test. |
| crates/windows-file-watcher/src/session/tests.rs | Bounds blocking recv() disconnect behavior with thread + deadline. |
| crates/windows-file-watcher/src/session.rs | Documents D-85 “paths are caller’s verbatim” semantics and long-path implications. |
| crates/windows-file-watcher/src/queue/tests.rs | Reworks tests to avoid suite hangs (bounded waits + explicit end-of-stream assertions); adds tripwire test. |
| crates/windows-file-watcher/src/queue.rs | Replaces StandingHold::drop release logic with tripwire; updates reservation-transfer docs and invariants. |
| crates/windows-file-watcher/src/monitor/tests.rs | Adds focused tests for monitor predicates/state projection, cleanup invariants, and mutation survivors. |
| crates/windows-file-watcher/src/monitor.rs | Factors duplicated boolean rules into predicates used by both production and tests. |
| crates/windows-file-watcher/src/directory/tests.rs | Replaces reopen-by-id characterization tests with canonical-path + D-85 pass-through + helper/mutation-gap coverage. |
| crates/windows-file-watcher/src/directory.rs | Removes OpenFileById reopen plumbing; documents verbatim path encoding (wide_path) and canonical-path semantics. |
| crates/windows-file-watcher/PLANS.md | Updates “no active plan” text for milestones through M16. |
| crates/windows-file-watcher/DESIGN-NOTES.md | Updates D-80 and records D-85; revises reopen-by-id rationale with executable evidence. |
| crates/windows-file-watcher/COMPLETED-PLANS.md | Archives M16 monitor mutation closure. |
| crates/windows-file-watcher/CHECKLIST.md | Updates milestone status, records M15 items, and closes the horizon item for reopen-by-id. |
Review details
Suppressed comments (2)
crates/windows-file-watcher/src/directory/tests.rs:714
deep_dir_of_prefixed_lenclaims to size a path to an exact target in UTF-16 units, but it usesformat!("\\\\?\\{}", path.display()).len()which counts UTF-8 bytes of a display string. On non-ASCII paths this mis-sizes the fixture and can break the boundary walk tests unexpectedly.
loop {
let current = format!(r"\\?\{}", path.display()).len();
assert!(
current + 2 <= target,
"the base path is already too long to hit {target}"
crates/windows-file-watcher/src/directory/tests.rs:739
- In
canonical_path_is_exact_on_both_sides_of_its_first_buffer,assert_eq!(prefixed.len(), target, ...)compares UTF-8 byte length, not the advertised UTF-16 unit length. This assertion can fail (or pass without meaning what it says) depending on non-ASCII characters in the base path; useencode_wide().count()for the check.
let prefixed = format!(r"\\?\{}", deep.display());
assert_eq!(prefixed.len(), target, "the fixture must be exactly sized");
std::fs::create_dir_all(&prefixed).expect("create the sized directory");
- Files reviewed: 20/20 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| let dir = TempDir::new("canonical-long"); | ||
| let mut deep = dir.path().to_path_buf(); | ||
| while format!(r"\\?\{}", deep.display()).len() < 560 { | ||
| deep.push("segment-0123456789abcdef"); | ||
| } |
There was a problem hiding this comment.
Fixed in bf51474, and the finding was right on both counts.
There were three sites, not two. The suppressed comments named deep_dir_of_prefixed_len and the exactness assertion; the third is the loop that grows a path until it passes 560 units, which was also counting bytes and so could stop short of the overflow it exists to cause. All three now go through one verbatim helper.
That helper also closes a second defect none of the comments named. Every site built its prefixed path with format!("{}", p.display()), and Path::display is documented to replace invalid sequences with U+FFFD -- so the string handed to create_dir_all could differ from the path that was measured. Composing OsStrings keeps the caller's bytes intact, which is also what this crate promises callers under D-85.
Measured with wtf_string::Wtf16String rather than encode_wide().count(): it is already a dependency of this crate for the lossless OsStr interop D-8 requires, and it holds the string in the encoding Windows uses, so its len is the number under test rather than a conversion of one.
Guarded by a test, because nothing else here could catch a regression. Every fixture is built under a temp directory, and on an ASCII temp path both counts agree exactly -- so reverting the helper would leave the whole suite green on this machine and on CI, and mis-size the boundary fixtures only for someone whose %TEMP% has a non-ASCII character. verbatim_counts_utf16_units_rather_than_bytes asserts the property directly, and is sabotage-verified: restoring str::len fails it with café must expose the divergence: 17 units vs 17 bytes.
393 tests pass; fmt, clippy -D warnings and rustdoc under the CI deny flags are clean.
…bytes Raised by Copilot on PR #57. `deep_dir_of_prefixed_len` documents itself as building a path "exactly `target` UTF-16 units" and measured it with `str::len`, which counts UTF-8 bytes. The two agree for ASCII and diverge for anything else, so on a host whose `%TEMP%` contains a non-ASCII character the fixture is mis-sized -- and these are precisely the tests that pin the 512-unit buffer boundary, so a mis-sized fixture stops testing the boundary while still passing. The review named two sites; there were three. The third grows a path until it passes 560 units and was also counting bytes, so it could stop short of the overflow it exists to cause. All three now go through one `verbatim` helper. That helper also closes a second defect the review did not name. Every site built its prefixed path with `format!("{}", p.display())`, and `Path::display` is documented to replace invalid sequences with U+FFFD -- so the string handed to `create_dir_all` could differ from the path that was measured. Composing `OsString`s keeps the caller's bytes intact, which is also what this crate promises callers under D-85. Measured with `wtf_string::Wtf16String`, already a dependency of this crate for the lossless `OsStr` interop D-8 requires. It holds the string in the encoding Windows uses, so its `len` is the number under test rather than a conversion of one. Guarded by a test, because nothing else here could catch a regression: every fixture is built under a temp directory, and on an ASCII temp path both counts agree, so reverting the helper would leave the suite green on this machine and on CI. Sabotage-verified -- restoring `str::len` fails the new test with "café must expose the divergence: 17 units vs 17 bytes". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
A few newly-added/modified tests have verified correctness issues that can weaken assertions or risk hangs, and they should be fixed before merge.
Review details
Suppressed comments (3)
Previously missed (2) — in code that hasn't changed since the last review.
crates/windows-file-watcher/src/queue/tests.rs:216
- This drain loop uses
recv_timeout, which returnsNonefor timeout as well as for teardown. That means the loop can terminate after 5s even if the receiver was never woken on the final sender drop, weakening the assertion and potentially masking a broken wake. Since this test knows exactly how many notifications should arrive, drain by count withnext(..)and then assert the stream ended withassert_stream_ended(..).
This issue also appears on line 249 of the same file.
crates/windows-file-watcher/tests/reopen_by_id_cannot_be_watched.rs:171
CancelIo(handle)'s return value is ignored. If cancellation fails (e.g. the IRP already completed, or another unexpected error), the subsequentGetOverlappedResult(..., bWait=TRUE)can block indefinitely and hang this test. Capture the return value and handleERROR_NOT_FOUND(already complete) explicitly; treat any other failure as a hard test failure.
crates/windows-file-watcher/src/queue/tests.rs:252
- Same issue as above:
recv_timeoutreturnsNoneon timeout as well as disconnect, so this loop can end after 5s without proving the stream actually ended (and without proving the last-sender wake happened). Because the test knows it should receive exactly2 * EACHnotifications, drain by count usingnext(..)and then callassert_stream_ended(..)to make the end-of-stream condition explicit.
let mut seen_a = Vec::new();
let mut seen_b = Vec::new();
while let Some(item) = receiver.recv_timeout(Duration::from_secs(5)) {
let name = names(&item).remove(0);
- Files reviewed: 20/20 changed files
- Comments generated: 0 new
- Review effort level: Lite
Two findings from the PR #57 review, both in tests this PR added. Two drain loops used `while let Some(..) = recv_timeout(..)`, which reads as "drain until the stream ends" and is not: `recv_timeout` answers `None` both for "the stream ended" and for "nothing arrived in time". `assert_stream_ended` exists precisely to separate those and says so in its own documentation -- so the rule was stated correctly in one place while two sites did not follow it. Both now drain exactly the number of notifications they know to expect with `next`, then call `assert_stream_ended`. That is more precise as well as more honest: a lost notification now fails at the index it went missing rather than after a 5s timeout with a count mismatch, and an extra one fails explicitly instead of being consumed silently. **Measured rather than claimed:** breaking the last-sender wake (`let last = false` in `Drop for Sender`) does *not* fail these two even after the rewrite, because by that point every item has been consumed and `senders` is already zero, so `recv_timeout` reports the end without ever blocking. Two dedicated tests -- `a_blocked_receiver_is_woken_by_the_last_sender_dropping` and `disconnection_signals_the_doorbell` -- do fail, which is where that property is actually covered. The rewrite is verified by a different sabotage: leaving one notification unconsumed now fails `assert_stream_ended`. `CancelIo`'s return value was ignored, so a failed cancellation could leave the read outstanding and the following `GetOverlappedResult(.., bWait=TRUE)` waiting on an operation nothing will complete. The return is now captured and asserted -- but *after* the wait, not instead of it, which is where this departs from the review's suggested remedy. Returning or panicking at the cancel site would unwind the frame while an IRP may still be outstanding against `overlapped` and `buffer`, reintroducing exactly the use-after-free the function exists to prevent. Waiting on an uncancelled read hangs; freeing a buffer the kernel still owns corrupts memory, and this crate has already seen that as a `STATUS_STACK_BUFFER_OVERRUN` with no useful diagnosis. The safe failure is chosen over the fast one, and the reasoning is recorded at the site so it is not "simplified" later. `ERROR_NOT_FOUND` is accepted: it means the IRP had already completed, a race this helper is allowed to lose, and the wait then returns immediately. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review
|
There was a problem hiding this comment.
🔵 Needs a closer look
A few newly added/edited test fixtures and diagnostics still use lossy Path::display()/incorrect \\?\\ spelling in D-85 guard areas, and the “tripwire” in StandingHold::drop is currently compiled out in release builds.
Review details
Suppressed comments (4)
Previously missed (2) — in code that hasn't changed since the last review.
crates/windows-file-watcher/src/directory/tests.rs:524
case_sensitivity_is_read_from_the_directory_rather_than_assumedpasses the directory path tofsutil.exeviapath.display().to_string(), which round-trips through UTF-8 (and can be lossy for non-Unicode paths). Since this file already calls outPath::displaylossiness as part of the D-85 guards, it’s safer and more consistent to pass the path as anOsStr/Pathdirectly toCommand.
This issue also appears in the following locations of the same file:
- line 638
- line 679
crates/windows-file-watcher/src/queue.rs:847
StandingHold::dropis described as a “tripwire”, but it currently usesdebug_assert!, which is compiled out in release builds. If this invariant is ever violated outside an unwind (e.g., a future discard path forgets to settle the reservation), release builds would silently proceed and could leave capacity accounting corrupted with no diagnostic. Usingassert!keeps the “no extra panic during an unwind” property (because the predicate isstd::thread::panicking()), while still failing fast in release when the invariant is violated.
crates/windows-file-watcher/src/directory/tests.rs:640
a_caller_supplied_verbatim_prefix_is_forwarded_and_honouredbuilds its\\?\path viaformat!(.., dir.path().display()), which can be lossy and undermines the test’s own D-85 guarantee that caller-supplied bytes are forwarded verbatim. Also, the expect message currently spells the prefix as\\?\\instead of\\?\.
let prefixed = format!(r"\\?\{}", dir.path().display());
let handle = DirectoryHandle::open(Path::new(&prefixed))
.expect("a caller's own `\\?\\` path must be forwarded unchanged and open");
crates/windows-file-watcher/src/directory/tests.rs:681
- The expect string in
canonical_path_grows_its_buffer_when_the_path_does_not_fitspells the verbatim prefix as\\?\\; the actual prefix is\\?\(as used elsewhere in this file and in the test setup). Keeping the spelling consistent matters because this message is part of diagnosing prefix/parsing-mode failures.
let handle = DirectoryHandle::open(Path::new(&prefixed))
.expect("a caller's own `\\?\\` path opens past MAX_PATH (D-85)");
let reported = handle.canonical_path().expect("canonical path");
- Files reviewed: 20/20 changed files
- Comments generated: 0 new
- Review effort level: Lite
…g paths through display() Four findings from the PR #57 review. `StandingHold::drop` called itself a tripwire and used `debug_assert!`, so it did not exist in release builds -- exactly where a future discard path forgetting to settle its reservation would corrupt the capacity accounting silently. It is now `assert!`. The "no second panic during an unwind" property is preserved by the predicate rather than by the assertion's flavour: when the thread is already panicking the assertion passes and the original panic, the real diagnostic, propagates untouched. Its test loses `#[cfg(debug_assertions)]` with it. That gate was correct for a `debug_assert!` -- a test for an assertion the release build does not contain would fail there -- and inverted once the assertion became unconditional, leaving the release configuration as the only one nothing checked. Verified in both directions: the test passes under `--release` now, and reverting to `debug_assert!` fails it there with "test did not panic as expected". Three test sites routed paths through `Path::display()`, which is documented to replace invalid sequences with U+FFFD. The review named the `fsutil` invocation; the same defect was also in `mklink`'s arguments in monitor/tests.rs and in the trailing-separator fixture. All now pass `OsStr`s, which matters most for the external commands: a formatted path could point `fsutil` or `mklink` at a different directory than the one the test then watches, so the test would quietly be measuring something else. This is the same class the `verbatim` helper closed in bf51474, in the sites that helper did not cover. Two `expect` messages spelled the verbatim prefix `\\?\\`, which renders as `\?\`. They are raw strings now and render `\\?\`. The spelling matters because these messages are the diagnosis when a prefix or parsing-mode assumption fails. 337 tests pass in both debug and release; fmt, clippy `-D warnings` and rustdoc under the CI deny flags are clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review
|
There was a problem hiding this comment.
🔵 Needs a closer look
Two test-harness changes should be adjusted for correctness/consistency (watch test timeout alignment and assert_stream_ended documentation implying wake guarantees it does not enforce).
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
crates/windows-file-watcher/src/watch/tests.rs:24
src/watch/tests.rsstill uses a 30sNOTIFY_TIMEOUT, but the newDrain::wait_forhelper will now spend up to that bound per missing notification. That undermines the PR’s stated goal of keeping mutation-broken delivery runs inside cargo-mutants’ kill deadline (the same motivation used to lower the otherNOTIFY_TIMEOUTcopies to 5s). Consider aligning this timeout to 5s (and documenting that it matches the measured bound insrc/watcher/tests.rs) so failures go red quickly instead of burning 30s per wait.
crates/windows-file-watcher/src/queue/tests.rs:642- The
assert_stream_endeddoc comment says pairingrecv_timeout(..).is_none()withis_disconnected()makes a broken wake satisfy neither. That is not necessarily true:is_disconnected()is derived from the sender count under the mutex, while a missing wake/notify can still leaverecv_timeoutreturningNoneonly after its timeout; this helper doesn’t prove that a blockedrecv()was woken promptly (it just distinguishes “stream still live” from “senders==0”). Please adjust the comment to match what the function actually asserts, so future changes don’t rely on an incorrect guarantee.
- Files reviewed: 20/20 changed files
- Comments generated: 0 new
- Review effort level: Lite
…n the last timeout Two findings from the PR #57 review, both about tests describing themselves inaccurately rather than behaving wrongly. `assert_stream_ended`'s documentation claimed that pairing `recv_timeout` with `is_disconnected` means "a broken wake satisfies neither". That is false, and I had already measured it false in the previous round -- breaking the last-sender wake leaves every caller of this helper passing -- then reported it in a PR comment and left the claim standing in the code. The reviewer found the same thing by reading. A conclusion recorded only in a review thread is not recorded. The comment now says what the helper actually asserts: nothing more arrived, and the queue agrees there is nobody left to send. It names the two tests that do cover the wake, and says plainly not to reach for this helper to cover one -- that is how a gap gets filed as closed. Re-verified against the current code: `let last = false` in `Drop for Sender` fails `a_blocked_receiver_is_woken_by_the_last_sender_dropping` and `disconnection_signals_the_doorbell`, and nothing else. The same false diagnosis was in both assertion messages, which blamed a missing wake for conditions neither can distinguish. They now describe what was actually observed. `watch/tests.rs` kept a 30s `NOTIFY_TIMEOUT` when the other three copies were lowered to 5s, which put that module back in the case the measurement exists to avoid: 93.6s at 30s, killed by cargo-mutants' deadline and filed as `timeout` rather than `caught`, against 31.8s at 5s. It matters more here than the count of waits suggests, because `Drain::wait_for` spends the budget per notification it is still looking for. Now 5s, with the evidence and the shared residual risk recorded beside it. 337 tests pass; fmt, clippy `-D warnings` and rustdoc under the CI deny flags are clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review
|
There was a problem hiding this comment.
🔵 Needs a closer look
A design note paragraph claims the StandingHold::drop tripwire is a debug_assert!, but the implementation is an assert!, so the documentation should be corrected to match shipped behavior.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
crates/windows-file-watcher/DESIGN-NOTES.md:864
- This paragraph says the tripwire is a
debug_assert!(std::thread::panicking(), ...), but the implementation insrc/queue.rsuses a non-debugassert!(and even documents "assert!, not debug_assert!"). The design note should match the shipped behavior.
- Files reviewed: 20/20 changed files
- Comments generated: 0 new
- Review effort level: Lite
Raised by the PR #57 review. `DESIGN-NOTES.md` still described the tripwire as `debug_assert!(std::thread::panicking(), ...)` after `deab234` made it an unconditional `assert!` -- so the design note, which is the authority for current behaviour, contradicted the code it documents. This is the fourth time in this PR's review that a fact was corrected in one place and left standing in another, and the repository's own rule prescribes the remedy: sweep the distinguishing term before committing a contract correction. Doing that here found a **second** stale site the review did not name, in COMPLETED-CHECKLIST.md. The two are treated differently on purpose. The design note is rewritten, because it states what is true now, and it gains the reasoning that was only at the call site: nothing is traded away by making the assertion unconditional, since the "do not raise a second panic during an unwind" property comes from the predicate being `std::thread::panicking()` rather than from the assertion's flavour. The archive is annotated rather than rewritten. It records what was *done*, and a `debug_assert!` is what that item landed; changing the sentence would falsify the history it exists to hold. A dated note above it points at the current form instead, so the claim can no longer be read as present tense while the record stays accurate. Swept `debug_assert` across the crate's markdown, `src/` and `tests/`: 8 mentions, 2 corrected, 6 are the new explanations of the change. 337 tests pass; fmt is clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review
|
There was a problem hiding this comment.
🔵 Needs a closer look
deep_dir_of_prefixed_len strips the \\?\ prefix via to_str(), which can double-prefix/mis-size fixtures on non-UTF-8 paths and should be made UTF-16/OsStr-based.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
crates/windows-file-watcher/src/directory/tests.rs:797
deep_dir_of_prefixed_lenstrips the\\?\prefix by converting the canonical path withto_str(). That makes the behavior depend on UTF-8 validity: on paths containing non-UTF-8 sequences (which is exactly why the surrounding code avoidsdisplay()), the prefix won’t be stripped andverbatim(&path)will double-prefix, mis-sizing the fixture and potentially making the boundary assertions flaky.
- Files reviewed: 20/20 changed files
- Comments generated: 0 new
- Review effort level: Lite
Raised by the PR #57 review, in the helper introduced two commits earlier to fix this exact class. `deep_dir_of_prefixed_len` stripped the `\\?\` that `canonicalize` returns using `to_str().and_then(strip_prefix)`, which makes the answer depend on UTF-8 validity. A path holding an unpaired surrogate -- legal on Windows, and the case `Wtf16String` exists for -- gives `None` from `to_str`, so the prefix survived and `verbatim` then added a second one. The fixture would be built at `\\?\\\?\C:\...`, four units longer than asked for, and the boundary assertions it feeds would fail for a reason none of them names. Stripping now compares encoded UTF-16 units, which answers the same question without ever asking whether the path is valid UTF-8. `unprefixed` is the inverse of `verbatim`, and the two are named and documented as a pair. Guarded by a round-trip test on a path containing `0xD800`, because nothing else could catch this: every fixture in the file is built under a temp directory whose path is ASCII, so both implementations agree on every path the suite uses. Sabotage-verified -- restoring the `to_str` form fails it with the double prefix visible in the units, `[92, 92, 63, 92, 92, 92, 63, 92, ...]` against `[92, 92, 63, 92, ...]`. 339 tests pass; fmt, clippy `-D warnings` and rustdoc under the CI deny flags are clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixed in
|
There was a problem hiding this comment.
🔵 Needs a closer look
It combines a breaking behavioral removal with substantial OS-facing watcher/test refactoring, and should get a final human review pass despite the strong test/measurement support.
Review details
- Files reviewed: 20/20 changed files
- Comments generated: 0 new
- Review effort level: Lite
Split out of #56, which had grown to 248 files across nine crates and was churning under review. This crate's work is a separate deliverable that happened to travel in the same branch.
Why this separates cleanly
Three facts, each verified rather than assumed:
windows-file-watcher'sCargo.tomlis not in Add waitable queues and the placement probe, and reshape the topology model #56's diff at all -- its dependencies never changed.windows-topology-sys,windows-waitable-queuesorwindows-placement-probe, which is where most of that PR's churn lives. Its only path dependencies arewindows-overlapped-io-sys,windows-threadpool-sysandwtf-string.lib.rsparts are the same cross-cutting change (compiling READMEs as doctests) repeated in each.Squashed deliberately
The 22 file-watcher-only commits are not reproduced here. Eight further commits in #56 mixed this crate with others, so reconstructing per-commit history would fabricate something that never existed. The original commits remain on
mikegrier/deferred-namespace-opsfor archaeology.Breaking change
The reopen-by-id fast path is removed. It was root-caused as impossible rather than merely unused -- a handle reopened by file id rejects the watcher's own read -- so the path could not have worked, and removing it takes away nothing a caller could have relied on.
tests/reopen_by_id_cannot_be_watched.rsis the evidence, and it is why the removal is safe rather than merely convenient.What is in here
StandingHold::drop's dead release path replaced with a tripwire, after a mutation run showed nothing could reach it.NOTIFY_TIMEOUTmeasured rather than guessed.canonical_pathdropped and the query kept, plus a test guarding the pass-through against a helpfully-added prefix.queue.rs,directory.rs,monitor.rsand the notification categories -- including which survivors are uncatchable and why, rather than leaving them as unexplained gaps.Verification
Run on this branch, against
main's dependencies rather than #56's -- which is the point of the split, so it is what was tested:cargo fmt --check-- cleancargo clippy --all-targets --all-features -- -D warnings-- cleancargo doc --no-deps --all-featuresunder the CI deny flags (-D warnings -D rustdoc::broken_intra_doc_links) -- clean#[cfg(test)] mod tests { }blocks in the diffmain-- no dangling cross-referencesCI has not run yet:
ci.ymltriggers onpull_request, so this PR is what starts it.