use keyhog_sources::guard::{EventBuffer, GuardEvent, GuardReconciliationConfig};
use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use parking_lot::Mutex;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::mpsc;
use std::sync::Arc;
struct WatchedRoot {
buffer: Arc<Mutex<EventBuffer>>,
}
pub struct GuardWatcher {
watcher: Option<RecommendedWatcher>,
rx: mpsc::Receiver<notify::Result<notify::Event>>,
roots: HashMap<PathBuf, WatchedRoot>,
config: GuardReconciliationConfig,
}
impl GuardWatcher {
pub fn new(config: GuardReconciliationConfig) -> Result<Self, String> {
let (tx, rx) = mpsc::channel::<notify::Result<notify::Event>>();
let watcher = notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
let _ = tx.send(res);
})
.map_err(|e| format!("failed to create filesystem watcher: {}", e))?;
Ok(Self {
watcher: Some(watcher),
rx,
roots: HashMap::new(),
config,
})
}
pub fn new_disabled() -> Self {
let (_tx, rx) = mpsc::channel::<notify::Result<notify::Event>>();
Self {
watcher: None,
rx,
roots: HashMap::new(),
config: GuardReconciliationConfig::default(),
}
}
pub fn coalesce_window_ms(&self) -> u64 {
self.config.coalesce_window_ms
}
pub fn add_root(&mut self, path: PathBuf) -> Result<(), String> {
if self.roots.contains_key(&path) {
return Err(format!("root already watched: {}", path.display()));
}
if let Some(ref mut watcher) = self.watcher {
watcher
.watch(&path, RecursiveMode::Recursive)
.map_err(|e| {
format!(
"failed to watch {}: {}; on Linux raise fs.inotify.max_user_watches",
path.display(),
e
)
})?;
}
let buffer = Arc::new(Mutex::new(EventBuffer::new(
self.config.max_pending_events_per_root,
)));
self.roots.insert(path, WatchedRoot { buffer });
Ok(())
}
pub fn remove_root(&mut self, path: &std::path::Path) {
if self.roots.remove(path).is_some() {
if let Some(ref mut watcher) = self.watcher {
let _ = watcher.unwatch(path);
}
}
}
pub fn poll_events(&self) -> Vec<(PathBuf, Vec<GuardEvent>)> {
let mut results: HashMap<PathBuf, Vec<GuardEvent>> = HashMap::new();
loop {
match self.rx.try_recv() {
Ok(Ok(event)) => {
if let Some(path) = event.paths.first() {
if let Some(root) = self.find_root_for_path(path) {
let guard_events = normalize_notify_event(&event);
if let Some(buffer) = self.roots.get(&root) {
let mut buf = buffer.buffer.lock();
for ge in &guard_events {
buf.push(ge.clone());
}
}
}
}
}
Ok(Err(_)) => {
for root in self.roots.keys() {
results
.entry(root.clone())
.or_default()
.push(GuardEvent::ReconcileSubtree(root.clone()));
}
}
Err(mpsc::TryRecvError::Empty) => break,
Err(mpsc::TryRecvError::Disconnected) => break,
}
}
for (root, watched) in &self.roots {
let mut buf = watched.buffer.lock();
if buf.overflowed() {
results
.entry(root.clone())
.or_default()
.push(GuardEvent::ReconcileSubtree(root.clone()));
buf.drain_and_reset();
} else {
let buffered: Vec<GuardEvent> = buf.drain().into_iter().map(|(_, ge)| ge).collect();
if !buffered.is_empty() {
results.entry(root.clone()).or_default().extend(buffered);
}
}
}
results.into_iter().collect()
}
fn find_root_for_path(&self, path: &std::path::Path) -> Option<PathBuf> {
for root in self.roots.keys() {
if path.starts_with(root) {
return Some(root.clone());
}
}
None
}
#[allow(dead_code)]
pub fn root_count(&self) -> usize {
self.roots.len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.roots.is_empty()
}
pub fn pending_event_count(&self, root: &std::path::Path) -> usize {
self.roots
.get(root)
.map(|r| r.buffer.lock().len())
.unwrap_or(0)
}
}
fn normalize_notify_event(event: ¬ify::Event) -> Vec<GuardEvent> {
let path = event.paths.first().cloned().unwrap_or_default();
match event.kind {
EventKind::Create(_) => vec![GuardEvent::Create(path)],
EventKind::Modify(_) => vec![GuardEvent::Modify(path)],
EventKind::Remove(_) => vec![GuardEvent::Remove(path)],
_ => {
if !event.paths.is_empty() {
vec![GuardEvent::Modify(path)]
} else {
Vec::new()
}
}
}
}
#[cfg(test)]
#[path = "../../tests/unit/daemon_guard_watcher.rs"]
mod tests;