use std::any::TypeId;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::{mpsc, Mutex};
use std::thread;
use std::time::Duration;
use notify::{Event, RecursiveMode, Watcher};
use crate::error::Error;
use crate::log::warning;
use super::debounce::run;
use super::{WatchMode, Watched};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum WatchKey {
Type(TypeId),
Instance(u64),
}
pub(super) static STARTED: Mutex<BTreeMap<WatchKey, &'static str>> = Mutex::new(BTreeMap::new());
#[must_use = "dropping the handle stops the watcher; bind it, or call `.detach()` \
to watch for the rest of the process"]
pub struct WatchHandle {
key: WatchKey,
name: &'static str,
watcher: Option<Backend>,
}
enum Backend {
Native(notify::RecommendedWatcher),
Poll(notify::PollWatcher),
}
impl WatchHandle {
pub fn detach(mut self) {
if let Some(watcher) = self.watcher.take() {
std::mem::forget(watcher);
}
std::mem::forget(self);
}
pub fn stop(self) {}
#[must_use]
pub fn name(&self) -> &'static str {
self.name
}
}
impl Drop for WatchHandle {
fn drop(&mut self) {
let Some(watcher) = self.watcher.take() else {
return;
};
drop(watcher);
STARTED
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&self.key);
}
}
impl std::fmt::Debug for WatchHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WatchHandle")
.field("name", &self.name)
.finish_non_exhaustive()
}
}
pub fn spawn(
key: WatchKey,
name: &'static str,
watched: Watched,
debounce: Duration,
reload: impl Fn() -> Result<Option<String>, Error> + Send + 'static,
) -> std::io::Result<WatchHandle> {
spawn_with(key, name, watched, debounce, WatchMode::default(), reload)
}
pub fn spawn_with(
key: WatchKey,
name: &'static str,
watched: Watched,
debounce: Duration,
mode: WatchMode,
reload: impl Fn() -> Result<Option<String>, Error> + Send + 'static,
) -> std::io::Result<WatchHandle> {
if STARTED
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(key, name)
.is_some()
{
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!(
"`{name}` is already being watched; hold on to the handle the \
first `start_watch()` returned, or drop it before starting \
another"
),
));
}
let registered = Registered { key, armed: true };
let (sender, receiver) = mpsc::channel::<notify::Result<Event>>();
let mut backend = match mode {
WatchMode::Native => Backend::Native(notify::recommended_watcher(sender).map_err(to_io)?),
WatchMode::Poll { interval } => Backend::Poll(
notify::PollWatcher::new(
sender,
notify::Config::default().with_poll_interval(interval),
)
.map_err(to_io)?,
),
};
match &mut backend {
Backend::Native(watcher) => watch_directories(name, watcher, &watched)?,
Backend::Poll(watcher) => watch_directories(name, watcher, &watched)?,
}
thread::Builder::new()
.name(format!("config-watch-{name}"))
.spawn(move || run(name, &watched, debounce, reload, &receiver))?;
registered.defuse();
Ok(WatchHandle {
key,
name,
watcher: Some(backend),
})
}
struct Registered {
key: WatchKey,
armed: bool,
}
impl Registered {
fn defuse(mut self) {
self.armed = false;
}
}
impl Drop for Registered {
fn drop(&mut self) {
if self.armed {
STARTED
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&self.key);
}
}
}
fn to_io(error: notify::Error) -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::Other, error)
}
fn watch_directories(
name: &'static str,
watcher: &mut impl Watcher,
watched: &Watched,
) -> std::io::Result<()> {
let mut directories = Vec::<PathBuf>::new();
{
let mut push = |directory: PathBuf| {
if !directories.contains(&directory) {
directories.push(directory);
}
};
for file in &watched.files {
push(
file.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."))
.to_path_buf(),
);
}
for directory in &watched.search_directories {
push(directory.clone());
}
}
let mut watched = 0usize;
let mut last_error = None;
for directory in &directories {
match watcher.watch(directory, RecursiveMode::NonRecursive) {
Ok(()) => watched += 1,
Err(error) => {
warning!("{name}: could not watch {}: {error}", directory.display());
last_error = Some(error);
}
}
}
if watched == 0 {
return Err(last_error.map_or_else(
|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("{name}: no configuration file to watch"),
)
},
to_io,
));
}
Ok(())
}