use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
const HOOK_TIMEOUT: Duration = Duration::from_secs(30);
pub struct HookEvent<'a> {
pub event: &'a str,
pub device: &'a str,
pub util: f64,
pub message: &'a str,
}
pub fn run_hook(command: &str, ev: &HookEvent) {
let spawned = Command::new("sh")
.arg("-c")
.arg(command)
.env("IOWATCHD_EVENT", ev.event)
.env("IOWATCHD_DEVICE", ev.device)
.env("IOWATCHD_UTIL", format!("{:.0}", ev.util))
.env("IOWATCHD_MESSAGE", ev.message)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
let mut child = match spawned {
Ok(c) => c,
Err(e) => return eprintln!("warning: could not run notify_command: {e}"),
};
std::thread::spawn(move || {
let start = Instant::now();
loop {
match child.try_wait() {
Ok(Some(_)) | Err(_) => return,
Ok(None) if start.elapsed() > HOOK_TIMEOUT => {
let _ = child.kill();
let _ = child.wait();
return;
}
Ok(None) => std::thread::sleep(Duration::from_millis(200)),
}
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hook_receives_the_environment() {
let out = std::env::temp_dir().join(format!("iowatchd-hook-{}", std::process::id()));
let cmd = format!(
"echo \"$IOWATCHD_EVENT $IOWATCHD_DEVICE $IOWATCHD_UTIL $IOWATCHD_MESSAGE\" > {}",
out.display()
);
run_hook(
&cmd,
&HookEvent {
event: "alert",
device: "sda",
util: 97.4,
message: "hello world",
},
);
for _ in 0..50 {
if out.exists() {
std::thread::sleep(Duration::from_millis(100));
break;
}
std::thread::sleep(Duration::from_millis(100));
}
let text = std::fs::read_to_string(&out).unwrap_or_default();
let _ = std::fs::remove_file(&out);
assert_eq!(text.trim(), "alert sda 97 hello world");
}
fn ev() -> HookEvent<'static> {
HookEvent {
event: "normal",
device: "sda",
util: 5.0,
message: "ok",
}
}
#[test]
fn failing_and_missing_commands_do_not_panic() {
run_hook("exit 3", &ev());
run_hook("this-command-does-not-exist-iowatchd", &ev());
run_hook("", &ev());
}
#[test]
fn hook_does_not_block_the_caller() {
let start = Instant::now();
run_hook("sleep 5", &ev());
assert!(start.elapsed() < Duration::from_secs(2));
}
}