use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock, Mutex};
use notify::{Config as NotifyConfig, RecommendedWatcher, RecursiveMode, Watcher};
use super::types::DEFAULT_MAX_DIR_WATCHES;
pub trait WatchHandle: Send {
fn close(&mut self);
fn set_error_handler(&mut self, _handler: WatchErrorHandler) {}
fn take_events(&mut self) -> Vec<PathBuf> {
Vec::new()
}
}
pub type WatchErrorHandler = Box<dyn Fn(WatchStartError) + Send + Sync>;
struct NativeWatchHandle {
watcher: Option<RecommendedWatcher>,
events: Arc<Mutex<Vec<PathBuf>>>,
error_handler: Arc<Mutex<Option<WatchErrorHandler>>>,
}
impl NativeWatchHandle {
fn new(dir: &Path, recursive: bool) -> Result<Self, WatchStartError> {
let events = Arc::new(Mutex::new(Vec::new()));
let error_handler: Arc<Mutex<Option<WatchErrorHandler>>> = Arc::new(Mutex::new(None));
let event_queue = Arc::clone(&events);
let error_sink = Arc::clone(&error_handler);
let mut watcher = RecommendedWatcher::new(
move |result: notify::Result<notify::Event>| match result {
Ok(event) => {
if let Ok(mut queue) = event_queue.lock() {
queue.extend(event.paths);
}
}
Err(err) => {
if let Ok(handler) = error_sink.lock()
&& let Some(handler) = handler.as_ref()
{
handler(watch_error_from_message(err.to_string()));
}
}
},
NotifyConfig::default(),
)
.map_err(|err| watch_error_from_message(err.to_string()))?;
watcher
.watch(
dir,
if recursive {
RecursiveMode::Recursive
} else {
RecursiveMode::NonRecursive
},
)
.map_err(|err| watch_error_from_message(err.to_string()))?;
Ok(Self {
watcher: Some(watcher),
events,
error_handler,
})
}
}
impl WatchHandle for NativeWatchHandle {
fn close(&mut self) {
self.watcher.take();
}
fn set_error_handler(&mut self, handler: WatchErrorHandler) {
if let Ok(mut slot) = self.error_handler.lock() {
*slot = Some(handler);
}
}
fn take_events(&mut self) -> Vec<PathBuf> {
let Ok(mut events) = self.events.lock() else {
return Vec::new();
};
std::mem::take(&mut *events)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WatchStartError {
ResourceExhaustion(String),
InotifyExhaustion(String),
Other(String),
}
impl WatchStartError {
pub fn resource_exhaustion(message: impl Into<String>) -> Self {
Self::ResourceExhaustion(message.into())
}
pub fn inotify_exhaustion(message: impl Into<String>) -> Self {
Self::InotifyExhaustion(message.into())
}
pub fn other(message: impl Into<String>) -> Self {
Self::Other(message.into())
}
pub(super) fn message(&self) -> &str {
match self {
Self::ResourceExhaustion(message)
| Self::InotifyExhaustion(message)
| Self::Other(message) => message,
}
}
}
fn watch_error_from_message(message: String) -> WatchStartError {
if message.contains("ENOSPC") || message.contains("inotify") {
WatchStartError::inotify_exhaustion(message)
} else if message.contains("EMFILE")
|| message.contains("ENFILE")
|| message.to_ascii_lowercase().contains("too many open files")
{
WatchStartError::resource_exhaustion(message)
} else {
WatchStartError::other(message)
}
}
pub type WatchFactory =
Arc<dyn Fn(&Path) -> Result<Box<dyn WatchHandle>, WatchStartError> + Send + Sync>;
static WATCH_FACTORY: LazyLock<Mutex<Option<WatchFactory>>> = LazyLock::new(|| Mutex::new(None));
static RECURSIVE_WATCH_OVERRIDE: LazyLock<Mutex<Option<bool>>> = LazyLock::new(|| Mutex::new(None));
pub fn __set_fs_watch_for_tests(factory: Option<WatchFactory>) {
if let Ok(mut slot) = WATCH_FACTORY.lock() {
*slot = factory;
}
}
pub fn __set_supports_recursive_watch_for_tests(value: Option<bool>) {
if let Ok(mut slot) = RECURSIVE_WATCH_OVERRIDE.lock() {
*slot = value;
}
}
pub(super) fn create_watch_handle(
dir: &Path,
recursive: bool,
) -> Result<Box<dyn WatchHandle>, WatchStartError> {
let factory = WATCH_FACTORY
.lock()
.ok()
.and_then(|slot| slot.as_ref().map(Arc::clone));
if let Some(factory) = factory {
factory(dir)
} else {
NativeWatchHandle::new(dir, recursive)
.map(|handle| Box::new(handle) as Box<dyn WatchHandle>)
}
}
pub(super) fn supports_recursive_watch() -> bool {
if let Ok(slot) = RECURSIVE_WATCH_OVERRIDE.lock()
&& let Some(value) = *slot
{
return value;
}
cfg!(target_os = "macos") || cfg!(target_os = "windows")
}
pub(super) fn max_dir_watches() -> usize {
std::env::var("RUSTCODEGRAPH_MAX_DIR_WATCHES")
.ok()
.and_then(|raw| raw.parse::<usize>().ok())
.filter(|value| *value > 0)
.unwrap_or(DEFAULT_MAX_DIR_WATCHES)
}