From 2cdb416e8d91ae17ab49c00eb1ce671b559cce52 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 29 Jul 2026 10:22:07 +0800 Subject: [PATCH] fix(host): make unsubscribing a watch take effect at the drop, not after it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's Windows job failed `watch_drop_unsubscribes`: an event for a file created *after* the subscription was dropped still reached a consumer holding a clone of the receiver. Tearing the watcher down is not instantaneous. The OS backend runs its own thread, and on Windows a `ReadDirectoryChangesW` completion can fire during teardown, reach the event closure while `raw_tx` is still alive, and be forwarded by a coalescer that has not yet noticed the disconnect. So "dropped" meant "stops delivering shortly", which is not what the subscription promises — and for a remote host it is the difference between releasing a server-side watch and leaking one. The handle now closes the delivery channel in its own `Drop`, before any of that unwinds. Batches already queued stay readable — `close` stops sends, not receives — which is the one thing a consumer racing its own drop may legitimately still see, and exactly what the conformance test allows for. Not this branch's bug: the change here is to git reads, not watches. But main is flaky in the same family — it failed the sibling `watch_coalesces_within_window` eleven hours ago and was hardened for that one — so this fixes the cause rather than loosening the test. Refs #239. --- crates/tty7-core/src/host/local.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/tty7-core/src/host/local.rs b/crates/tty7-core/src/host/local.rs index e49f3cc0..12a7c805 100644 --- a/crates/tty7-core/src/host/local.rs +++ b/crates/tty7-core/src/host/local.rs @@ -398,6 +398,27 @@ impl WatchedDirs { /// A live local watch: the notify watcher plus the set it is following. struct LocalWatch { inner: Mutex, + /// The delivery end, kept solely so dropping this handle can close it. + /// + /// Tearing the watcher down is not instantaneous — the OS backend has its + /// own thread, and on Windows a `ReadDirectoryChangesW` completion can fire + /// *during* teardown, reach the event closure while `raw_tx` is still + /// alive, and be forwarded by a coalescer that has not noticed the + /// disconnect yet. A consumer holding a clone of the receiver would then + /// see an event for a change made after it unsubscribed. + /// + /// Closing the channel here makes "dropped" mean "no further batches" at + /// the instant of the drop, whatever the backend does afterwards. Batches + /// already queued stay readable — `close` stops sends, not receives — which + /// is the one thing a consumer racing its own drop may legitimately still + /// see. + batch_tx: smol::channel::Sender>, +} + +impl Drop for LocalWatch { + fn drop(&mut self) { + self.batch_tx.close(); + } } struct LocalWatchInner { @@ -463,6 +484,7 @@ fn local_watch(dirs: &[PathBuf], gitignore: Arc>) -> io::R watcher, dirs: Arc::clone(&watched), }), + batch_tx: batch_tx.clone(), }; handle.set_dirs(dirs)?;