Skip to main content

FsWatchPool

Struct FsWatchPool 

Source
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

Source

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.

Source

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.

Source

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).

Source

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).

Source

fn subscribe_target( self: &Arc<Self>, target: PathBuf, mode: RecursiveMode, ) -> Subscription

Source

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.

Source

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.

Source

pub fn health(&self) -> FsWatchHealth

Source

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.

Source

fn try_watch(&self, target: &Path, mode: RecursiveMode) -> Result<(), String>

Source

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.

Source

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:

  1. The handle leak this fn exists to fix — notify 7.0.0’s Windows backend (add_watch in the vendored windows.rs) opens a brand-new CreateFileW handle + CreateSemaphoreW on every watch() call, unconditionally, and silently drops the previous entry’s handles with no cleanup — see docs/status/STATUS_FS_WATCH_SWEEP_HANDLE_LEAK_2026_08_22.md.
  2. A real coverage gap — unwatch() then watch() 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, but config_watcher_fs, EditorFileWatcher, and MediaFileWatcher all 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.

Source

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§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<T> Downcast for T
where T: Any,

§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert 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>

Convert 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)

Convert &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)

Convert &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
where T: Any + Send + Sync,

§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync> ⓘ

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts 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
§

impl<T> PolicyExt for T
where T: ?Sized,

§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] only if self and other return Action::Follow. Read more
§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more