KeiSeiKit-1.0/_primitives/_rust/kei-watch/src/watcher.rs
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

114 lines
3.6 KiB
Rust

//! Public [`Watcher`] type — owns the notify backend and the pump thread.
//!
//! Layout invariants:
//! - exactly one pump thread per watcher
//! - pump thread exits when `notify::Watcher` is dropped (closes
//! notify's sender, which closes the pump's `recv`)
//! - `Drop` explicitly drops the notify watcher first, then joins the
//! pump — preventing zombie threads
use crate::error::{from_notify, WatchError};
use crate::event::Event;
use crate::pump;
use notify::{RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher};
use std::path::Path;
use std::sync::mpsc::{self, Receiver};
use std::thread::JoinHandle;
use std::time::Duration;
/// Filesystem watcher. Synchronous API only; see crate docs.
pub struct Watcher {
inner: Option<RecommendedWatcher>,
out_rx: Receiver<Event>,
pump_handle: Option<JoinHandle<()>>,
}
impl Watcher {
/// Construct a new watcher. Spawns one background thread for the
/// event pump and initialises the notify backend with its own
/// internal thread(s).
pub fn new() -> Result<Self, WatchError> {
let (n_tx, n_rx) = mpsc::channel::<notify::Result<notify::Event>>();
let (o_tx, o_rx) = mpsc::channel::<Event>();
let inner = notify::recommended_watcher(n_tx).map_err(from_notify)?;
let pump_handle = pump::spawn(n_rx, o_tx);
Ok(Self {
inner: Some(inner),
out_rx: o_rx,
pump_handle: Some(pump_handle),
})
}
/// Begin watching `path`. When `recursive=true`, all descendants
/// are watched too; otherwise only the path itself (and its
/// immediate children if a directory).
pub fn watch(&mut self, path: &Path, recursive: bool) -> Result<(), WatchError> {
let mode = if recursive {
RecursiveMode::Recursive
} else {
RecursiveMode::NonRecursive
};
let inner = self.inner.as_mut().expect("watcher initialised");
inner.watch(path, mode).map_err(from_notify)
}
/// Stop watching `path`.
pub fn unwatch(&mut self, path: &Path) -> Result<(), WatchError> {
let inner = self.inner.as_mut().expect("watcher initialised");
inner.unwatch(path).map_err(from_notify)
}
/// Block until either an event arrives or `timeout` elapses.
/// Returns `None` on timeout or channel closure.
pub fn next_event(&self, timeout: Duration) -> Option<Event> {
self.out_rx.recv_timeout(timeout).ok()
}
/// Non-blocking: drain all currently queued events.
pub fn drain(&self) -> Vec<Event> {
let mut out = Vec::new();
while let Ok(ev) = self.out_rx.try_recv() {
out.push(ev);
}
out
}
}
impl Drop for Watcher {
fn drop(&mut self) {
// Dropping the notify watcher closes the pump's input channel;
// the pump loop exits, and we can join its thread cleanly.
drop(self.inner.take());
if let Some(h) = self.pump_handle.take() {
let _ = h.join();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn new_and_drop_is_clean() {
let w = Watcher::new().unwrap();
drop(w);
}
#[test]
fn watch_missing_path_is_error() {
let mut w = Watcher::new().unwrap();
let r = w.watch(Path::new("/definitely/does/not/exist/kei-watch"), false);
assert!(r.is_err());
}
#[test]
fn drain_on_idle_is_empty() {
let d = tempdir().unwrap();
let mut w = Watcher::new().unwrap();
w.watch(d.path(), false).unwrap();
// No activity → drain returns empty.
assert!(w.drain().is_empty());
}
}