use std::collections::{BTreeMap, HashMap};
use std::time::{Duration, Instant};
use serde_json::json;
use crate::errors::log_debug;
use crate::extraction::index::build_scope_ignore;
use crate::utils::normalize_path;
use super::backend::supports_recursive_watch;
use super::state::FileWatcher;
use super::types::{
PendingFile, SyncRunResult, WatchMode, WatchOptions, WatchSyncError, resolve_max_debounce_ms,
resolve_min_sync_interval_ms,
};
use super::util::{context, is_test_runtime, path_string};
impl FileWatcher {
pub fn new<F>(
project_root: impl Into<std::path::PathBuf>,
sync_fn: F,
options: WatchOptions,
) -> Self
where
F: FnMut() -> Result<SyncRunResult, WatchSyncError> + Send + 'static,
{
let debounce_ms = options.debounce_ms.unwrap_or(2000);
let min_sync_interval_ms =
resolve_min_sync_interval_ms(options.min_sync_interval_ms, debounce_ms);
let max_debounce_ms = resolve_max_debounce_ms(options.max_debounce_ms, debounce_ms);
Self {
recursive_watcher: None,
dir_watchers: HashMap::new(),
dir_cap_warned: false,
inotify_limit_warned: false,
degraded_reason: None,
lock_retry_count: 0,
inert: false,
scheduled_at: None,
scheduled_delay_ms: None,
scheduled_kind: None,
debounce_started_at: None,
pending_files: BTreeMap::new(),
poll_snapshot: BTreeMap::new(),
last_poll_at: None,
poll_interval_ms: debounce_ms.clamp(250, 2000),
sync_started_ms: 0,
syncing: false,
last_sync_finished_at: None,
min_sync_interval_ms,
max_debounce_ms,
stopped: false,
ready: false,
ignore_matcher: None,
project_root: project_root.into(),
debounce_ms,
sync_fn: Box::new(sync_fn),
on_sync_complete: options.on_sync_complete,
on_sync_error: options.on_sync_error,
on_degraded: options.on_degraded,
inert_for_tests: options.inert_for_tests,
}
}
pub fn start(&mut self) -> bool {
if self.recursive_watcher.is_some() || !self.dir_watchers.is_empty() || self.inert {
return true;
}
self.stopped = false;
self.degraded_reason = None;
self.lock_retry_count = 0;
self.scheduled_at = None;
self.scheduled_delay_ms = None;
self.scheduled_kind = None;
self.debounce_started_at = None;
self.last_sync_finished_at = None;
self.last_poll_at = None;
if let Some(disabled_reason) =
super::super::watch_policy::watch_disabled_reason(&self.project_root, None)
{
let ctx = context([
("reason", json!(disabled_reason)),
("projectRoot", json!(path_string(&self.project_root))),
]);
log_debug("File watcher disabled", Some(&ctx));
return false;
}
self.ignore_matcher = Some(build_scope_ignore(&self.project_root, None::<Vec<String>>));
self.poll_snapshot = self.collect_source_snapshot();
self.last_poll_at = Some(Instant::now());
let mode = if self.inert_for_tests {
self.inert = true;
WatchMode::Inert
} else if supports_recursive_watch() {
if let Err(err) = self.start_recursive() {
self.handle_start_error(err);
return false;
}
WatchMode::Recursive
} else {
self.start_per_directory();
WatchMode::PerDirectory
};
if self.degraded_reason.is_some() {
return false;
}
self.pending_files.clear();
self.ready = true;
if is_test_runtime() {
self.register_live_watcher_for_tests();
}
let mode = match mode {
WatchMode::Inert => "inert",
WatchMode::Recursive => "recursive",
WatchMode::PerDirectory => "per-directory",
};
let mut ctx = context([
("projectRoot", json!(path_string(&self.project_root))),
("debounceMs", json!(self.debounce_ms)),
("mode", json!(mode)),
]);
if !self.dir_watchers.is_empty() {
ctx.insert("watchedDirs".to_owned(), json!(self.dir_watchers.len()));
}
log_debug("File watcher started", Some(&ctx));
true
}
pub fn stop(&mut self) {
self.stopped = true;
self.scheduled_at = None;
self.scheduled_delay_ms = None;
self.scheduled_kind = None;
self.debounce_started_at = None;
self.last_sync_finished_at = None;
self.last_poll_at = None;
self.poll_snapshot.clear();
if let Some(mut watcher) = self.recursive_watcher.take() {
watcher.close();
}
for watcher in self.dir_watchers.values_mut() {
watcher.close();
}
self.dir_watchers.clear();
self.dir_cap_warned = false;
self.inotify_limit_warned = false;
self.lock_retry_count = 0;
self.inert = false;
self.pending_files.clear();
self.ready = false;
self.ignore_matcher = None;
if is_test_runtime() {
self.unregister_live_watcher_for_tests();
}
log_debug("File watcher stopped", None);
}
pub fn ingest_event_for_tests(&mut self, rel_path: &str) {
self.handle_change(&normalize_path(rel_path));
}
pub fn is_active(&self) -> bool {
(self.recursive_watcher.is_some() || !self.dir_watchers.is_empty() || self.inert)
&& !self.stopped
}
pub fn is_degraded(&self) -> bool {
self.degraded_reason.is_some()
}
pub fn get_degraded_reason(&self) -> Option<&str> {
self.degraded_reason.as_deref()
}
pub fn wait_until_ready(&self, timeout_ms: u64) -> Result<(), String> {
if self.ready {
return Ok(());
}
let start = Instant::now();
let timeout = Duration::from_millis(timeout_ms);
while start.elapsed() < timeout {
if self.ready {
return Ok(());
}
std::thread::sleep(Duration::from_millis(10));
}
Err(format!(
"FileWatcher.waitUntilReady timed out after {timeout_ms}ms"
))
}
pub fn get_pending_files(&self) -> Vec<PendingFile> {
self.pending_files
.iter()
.map(|(file_path, info)| PendingFile {
path: file_path.clone(),
first_seen_ms: info.first_seen_ms,
last_seen_ms: info.last_seen_ms,
indexing: self.syncing && self.sync_started_ms >= info.last_seen_ms,
})
.collect()
}
pub fn flush_due(&mut self) {
let Some(started_at) = self.scheduled_at else {
return;
};
let Some(delay_ms) = self.scheduled_delay_ms else {
return;
};
if started_at.elapsed() >= Duration::from_millis(delay_ms) {
if self.scheduled_kind == Some(super::types::ScheduledSyncKind::Debounce) {
if let Some(remaining_ms) = self.throttle_remaining_ms() {
self.scheduled_at = Some(Instant::now());
self.scheduled_delay_ms = Some(remaining_ms);
self.scheduled_kind = Some(super::types::ScheduledSyncKind::Debounce);
return;
}
}
self.scheduled_at = None;
self.scheduled_delay_ms = None;
self.scheduled_kind = None;
self.flush();
}
}
pub fn tick(&mut self) {
self.drain_watch_events();
self.poll_for_changes();
self.flush_due();
}
pub fn flush_now(&mut self) {
self.scheduled_at = None;
self.scheduled_delay_ms = None;
self.scheduled_kind = None;
self.flush();
}
#[doc(hidden)]
pub fn __scheduled_delay_ms_for_tests(&self) -> Option<u64> {
self.scheduled_delay_ms
}
}
impl Drop for FileWatcher {
fn drop(&mut self) {
if self.is_active() || self.ready {
self.stop();
}
}
}