use std::path::PathBuf;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use tracing::{info, warn};
use crate::error::{Result, SkadooshError};
const WATCH_CAP: usize = 16;
const PROCESS_POLL: Duration = Duration::from_millis(200);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WatchEvent {
FileChanged(PathBuf),
ProcessExited(u32),
TimerElapsed(String),
}
impl WatchEvent {
pub fn message(&self) -> String {
match self {
WatchEvent::FileChanged(path) => {
format!("The file {} has changed.", path.display())
}
WatchEvent::ProcessExited(pid) => format!("Process {pid} has exited."),
WatchEvent::TimerElapsed(secs) => format!("Your {secs}-second timer is up."),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct WatchConfig {
pub files: Vec<PathBuf>,
pub processes: Vec<u32>,
pub timers: Vec<u64>,
}
impl WatchConfig {
pub fn is_empty(&self) -> bool {
self.files.is_empty() && self.processes.is_empty() && self.timers.is_empty()
}
}
pub struct WatchManager {
tasks: JoinSet<()>,
shutdown: CancellationToken,
}
impl WatchManager {
pub fn start(
config: &WatchConfig,
shutdown: CancellationToken,
) -> (Self, mpsc::Receiver<WatchEvent>) {
let (tx, rx) = mpsc::channel(WATCH_CAP);
let mut tasks = JoinSet::new();
for path in &config.files {
let tx = tx.clone();
let path = path.clone();
let shutdown = shutdown.clone();
tasks.spawn(async move {
if let Err(err) = watch_file(path, tx, shutdown).await {
warn!(error = %err, "file watcher exited with error");
}
});
}
for &pid in &config.processes {
let tx = tx.clone();
let shutdown = shutdown.clone();
tasks.spawn(async move {
watch_process(pid, tx, shutdown).await;
});
}
for &secs in &config.timers {
let tx = tx.clone();
let shutdown = shutdown.clone();
let label = secs.to_string();
tasks.spawn(async move {
watch_timer(secs, label, tx, shutdown).await;
});
}
drop(tx);
(Self { tasks, shutdown }, rx)
}
pub async fn shutdown(mut self) {
self.shutdown.cancel();
while let Some(res) = self.tasks.join_next().await {
if let Err(err) = res {
warn!(error = %err, "watch task panicked");
}
}
}
}
impl Drop for WatchManager {
fn drop(&mut self) {
self.shutdown.cancel();
}
}
async fn watch_file(
path: PathBuf,
tx: mpsc::Sender<WatchEvent>,
shutdown: CancellationToken,
) -> Result<()> {
use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
let (ev_tx, mut ev_rx) =
mpsc::unbounded_channel::<std::result::Result<notify::Event, notify::Error>>();
let mut watcher = RecommendedWatcher::new(
move |res| {
let _ = ev_tx.send(res);
},
notify::Config::default(),
)
.map_err(|e| SkadooshError::Other(anyhow::anyhow!("file watcher init: {e}")))?;
let parent = path.parent();
let (watch_target, filter): (PathBuf, bool) = match parent {
Some(p) if !p.as_os_str().is_empty() => (p.to_path_buf(), true),
_ => (path.clone(), false),
};
watcher
.watch(&watch_target, RecursiveMode::NonRecursive)
.map_err(|e| SkadooshError::Other(anyhow::anyhow!("file watch start: {e}")))?;
info!(path = %path.display(), "watching file for changes");
loop {
let res = tokio::select! {
biased;
_ = shutdown.cancelled() => break,
res = ev_rx.recv() => match res {
Some(res) => res,
None => break, },
};
let event = match res {
Ok(event) => event,
Err(err) => {
warn!(path = %path.display(), error = %err, "file watcher error");
continue;
}
};
let is_change = matches!(
event.kind,
EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
);
if !is_change {
continue;
}
if filter && !event.paths.iter().any(|p| p.as_path() == path.as_path()) {
continue;
}
let _ = tx.send(WatchEvent::FileChanged(path.clone())).await;
}
Ok(())
}
async fn watch_process(pid: u32, tx: mpsc::Sender<WatchEvent>, shutdown: CancellationToken) {
let proc_path = PathBuf::from(format!("/proc/{pid}"));
if !proc_path.exists() {
warn!(pid, "watched process not found in /proc; not watching");
return;
}
info!(pid, "watching process for exit");
loop {
if !proc_path.exists() {
info!(pid, "watched process exited");
let _ = tx.send(WatchEvent::ProcessExited(pid)).await;
return;
}
tokio::select! {
biased;
_ = shutdown.cancelled() => (),
_ = tokio::time::sleep(PROCESS_POLL) => {}
}
}
}
async fn watch_timer(
secs: u64,
label: String,
tx: mpsc::Sender<WatchEvent>,
shutdown: CancellationToken,
) {
info!(secs, "watch timer armed");
tokio::select! {
biased;
_ = shutdown.cancelled() => (),
_ = tokio::time::sleep(Duration::from_secs(secs)) => {
let _ = tx.send(WatchEvent::TimerElapsed(label)).await;
}
}
}