pub struct FsWatchPool {
inner: Mutex<Inner>,
health: HealthState,
backend: WatchBackend,
tx: Sender<FsWatchEvent>,
}Fields§
§inner: Mutex<Inner>§health: HealthState§backend: WatchBackend§tx: Sender<FsWatchEvent>Implementations§
Source§impl FsWatchPool
impl FsWatchPool
Sourcepub fn new() -> Arc<Self> ⓘ
pub fn new() -> Arc<Self> ⓘ
Construct the pool and start its background event-bridging and
health-sweep tasks. Always succeeds — if even the PollWatcher
fallback fails to construct (practically never happens; it’s pure
userspace polling), the pool still comes up with watcher: None and
every subscribe call fails cleanly into degraded_paths rather than
panicking or requiring every caller to handle an Option<Arc<Self>>
at the top level. Live-update is additive everywhere it’s used.
Sourcepub fn subscribe_file(self: &Arc<Self>, path: &Path) -> Subscription
pub fn subscribe_file(self: &Arc<Self>, path: &Path) -> Subscription
Watch the file at path (specifically: its parent directory,
non-recursively — see module doc). Returns a fresh Subscription
even when the underlying OS watch is currently failing (tracked in
degraded_paths instead) — a degraded subscription still becomes
live automatically once retry/sweep succeeds, with no action needed
from the caller.
Sourcepub fn subscribe_dir(self: &Arc<Self>, path: &Path) -> Subscription
pub fn subscribe_dir(self: &Arc<Self>, path: &Path) -> Subscription
Watch the directory at path directly (non-recursive — matching
media_file_watcher.rs’s existing behavior; extension/content
filtering is the caller’s concern, not the pool’s).
Sourcepub fn subscribe_dir_recursive(self: &Arc<Self>, path: &Path) -> Subscription
pub fn subscribe_dir_recursive(self: &Arc<Self>, path: &Path) -> Subscription
Watch path and everything under it (widget packages: an edit deep in
a package folder reloads it).
fn subscribe_target( self: &Arc<Self>, target: PathBuf, mode: RecursiveMode, ) -> Subscription
Sourcepub fn unsubscribe(&self, sub: Subscription)
pub fn unsubscribe(&self, sub: Subscription)
Stop watching on behalf of sub. Idempotent-safe: unsubscribing the
same handle twice would underflow the refcount, so this takes self
by shared ref but sub by value specifically to discourage reuse —
callers that need multiple independent subscriptions to the same
path should call subscribe_file/subscribe_dir once per logical
subscriber, matching how editor_file_watcher.rs already tracks one
entry per block_id today.
Sourcepub fn events(&self) -> Receiver<FsWatchEvent>
pub fn events(&self) -> Receiver<FsWatchEvent>
Raw change events for every currently-watched target, pool-wide.
Each call returns an independent receiver — subscribe to this once
per domain module (not once per file), and filter down to the paths
that module actually cares about, same as today’s per-watcher
Inner.watched_paths filtering.
pub fn health(&self) -> FsWatchHealth
Sourcefn start_watch(self: &Arc<Self>, target: PathBuf, mode: RecursiveMode)
fn start_watch(self: &Arc<Self>, target: PathBuf, mode: RecursiveMode)
First attempt at a new target, plus a bounded retry-with-backoff if
it fails — see RETRY_BACKOFF’s doc comment. Runs the retries on a
spawned task so subscribe_file/subscribe_dir never blocks on it.
fn try_watch(&self, target: &Path, mode: RecursiveMode) -> Result<(), String>
Sourcefn handle_backend_error(&self, e: Error)
fn handle_backend_error(&self, e: Error)
Handle an async error surfaced by the notify backend’s own callback
(piped through raw_rx in Self::new’s bridge task). Extracted out
of that task so a test can drive a real notify::Error through it
directly (reagent P2 on #2722: the original three sweep regression
tests all simulated degradation via a direct health.mark_degraded
call, so none of them actually exercised this fn or proved that
notify::Error::paths lines up with the exact PathBuf key
inner.targets uses — the assumption sweep()’s is_degraded()
filter depends on to ever re-arm a target that failed this way).
notify::Error carries the affected path(s) when it can attribute
one; mark each degraded so the next sweep re-arms it. A path-less
error (rare — not attributable to any specific target) can only be
logged.
Sourcefn sweep(&self)
fn sweep(&self)
Re-arm every currently-degraded target — the backstop that lets a
path recover from a failed watch() attempt (at start_watch time
or a prior sweep) without a dedicated retry loop running forever.
Deliberately does NOT touch a target that already looks healthy
(codex P2 on #2722, revising this fn’s first version): the original
fix here unconditionally called unwatch() then watch() on every
subscribed target every tick, reasoning that this was needed to
detect a watch that died silently (no error ever surfaced). Two
problems with that, both real:
- The handle leak this fn exists to fix —
notify7.0.0’s Windows backend (add_watchin the vendoredwindows.rs) opens a brand-newCreateFileWhandle +CreateSemaphoreWon everywatch()call, unconditionally, and silently drops the previous entry’s handles with no cleanup — seedocs/status/STATUS_FS_WATCH_SWEEP_HANDLE_LEAK_2026_08_22.md. - A real coverage gap —
unwatch()thenwatch()leaves the target genuinely unwatched for the gap between the two calls.native_memory_drift’s slow-path reconciliation sweep backstops a missed event there, butconfig_watcher_fs,EditorFileWatcher, andMediaFileWatcherall rely solely on the broadcast stream — a create/modify landing in that gap would go unnoticed until some later, unrelated change on the same path happened to trigger a refresh.
Skipping healthy targets fixes both: nothing is ever double-watched
(no leak) and a healthy watch is never touched (no gap). The
honestly-bounded trade-off: a watch that fails silently — dies
without notify or the OS ever reporting an error on it — is no
longer self-healed by this sweep, only a watch that’s already known
degraded (an explicit prior failure). Accepted because a truly
silent death has no confirmed occurrence in this codebase’s history
(the scenarios HEALTH_SWEEP_INTERVAL’s own doc comment lists —
inotify instance-limit churn, flaky network mounts — are Linux-
flavored concerns on a Windows-primary codebase), against a
certain, systematic cost (leak or gap, every target, every tick) for
every other watch the alternative would have imposed.
Race with concurrent unsubscribe() — closed by re-checking under
one held lock (reagent P1, second review round on #2722): the
degraded-target list above is a snapshot; without re-verification, a
concurrent unsubscribe() dropping the last subscriber for one of
these targets between the snapshot and this loop’s watch() call
would remove it from targets and unwatch() it, while this sweep’s
in-flight watch() call still re-established a native watch for the
now-untracked path — orphaning exactly the handle pair this fn exists
to stop leaking, just via a race instead of per-tick re-watching.
Each target’s re-check + unwatch() + watch() now happens under a
single inner lock acquisition (not try_watch(), which takes its
own lock and would deadlock if called while this one is held) — a
concurrent unsubscribe() either finishes its own removal entirely
before this runs (this loop then sees the target is gone and skips
it) or entirely after (its own unwatch() then correctly tears down
the watch this loop just re-armed) — Mutex serializes the two, so
no interleaving in between is possible.
Sourcefn rearm_if_still_subscribed(&self, target: PathBuf)
fn rearm_if_still_subscribed(&self, target: PathBuf)
The per-target half of a sweep: re-verify target is still
subscribed, then unwatch() + watch() it, all under one held
inner lock (see sweep()’s doc comment for why the re-check and
the single lock both matter). Split out from sweep()’s loop body
so a test can call it directly with a target captured before a
real unsubscribe() ran — deterministically reproducing “sweep
already decided to process this target, then it was removed before
the action ran” without needing to win a real thread-scheduling
race.
Auto Trait Implementations§
impl !Freeze for FsWatchPool
impl !RefUnwindSafe for FsWatchPool
impl !UnwindSafe for FsWatchPool
impl Send for FsWatchPool
impl Sync for FsWatchPool
impl Unpin for FsWatchPool
impl UnsafeUnpin for FsWatchPool
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.§impl<T> DowncastSync for T
impl<T> DowncastSync for T
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more