use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, RecvTimeoutError};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
pub fn start(rx: Receiver<()>, command: String, debounce_ms: u64) {
thread::spawn(move || run_loop(rx, command, debounce_ms));
}
fn run_loop(rx: Receiver<()>, command: String, debounce_ms: u64) {
let is_running = Arc::new(AtomicBool::new(false));
let debounce = Duration::from_millis(debounce_ms);
let mut deadline: Option<Instant> = None;
loop {
let timeout = deadline
.map(|d| d.saturating_duration_since(Instant::now()))
.unwrap_or(Duration::from_secs(60));
match rx.recv_timeout(timeout) {
Ok(()) => {
deadline = Some(Instant::now() + debounce);
}
Err(RecvTimeoutError::Timeout) => {
if deadline.map_or(false, |d| Instant::now() >= d) {
deadline = None;
maybe_run(&command, &is_running);
}
}
Err(RecvTimeoutError::Disconnected) => break,
}
}
}
fn maybe_run(command: &str, is_running: &Arc<AtomicBool>) -> bool {
if is_running.swap(true, Ordering::SeqCst) {
return false;
}
let is_running = Arc::clone(is_running);
let cmd = command.to_string();
thread::spawn(move || {
execute_command(&cmd);
is_running.store(false, Ordering::SeqCst);
});
true
}
pub fn execute_command(command: &str) {
match spawn_shell(command) {
Ok(mut child) => {
let _ = child.wait();
}
Err(e) => {
eprintln!("[iwatchr] Failed to run command: {e}");
}
}
}
fn spawn_shell(command: &str) -> std::io::Result<std::process::Child> {
#[cfg(unix)]
{
Command::new("sh").arg("-c").arg(command).spawn()
}
#[cfg(windows)]
{
Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", command])
.spawn()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::mpsc;
#[test]
fn execute_command_succeeds_for_no_op() {
#[cfg(unix)]
execute_command("true");
#[cfg(windows)]
execute_command("exit 0");
}
#[test]
fn execute_command_handles_bad_command_gracefully() {
execute_command("__iwatchr_nonexistent_xyz_command__");
}
#[test]
fn execute_command_runs_echo() {
#[cfg(unix)]
execute_command("echo iwatchr_unit_test");
#[cfg(windows)]
execute_command("echo iwatchr_unit_test");
}
#[test]
fn maybe_run_starts_when_not_running() {
let flag = Arc::new(AtomicBool::new(false));
let started = maybe_run("echo hi", &flag);
assert!(started);
#[cfg(unix)]
thread::sleep(Duration::from_millis(300));
#[cfg(windows)]
thread::sleep(Duration::from_secs(5));
assert!(!flag.load(Ordering::SeqCst));
}
#[test]
fn maybe_run_skips_when_already_running() {
let flag = Arc::new(AtomicBool::new(true)); let started = maybe_run("echo hi", &flag);
assert!(!started);
assert!(flag.load(Ordering::SeqCst));
}
#[test]
fn maybe_run_resets_flag_after_completion() {
let flag = Arc::new(AtomicBool::new(false));
maybe_run("echo hi", &flag);
#[cfg(unix)]
thread::sleep(Duration::from_millis(300));
#[cfg(windows)]
thread::sleep(Duration::from_secs(5));
assert!(!flag.load(Ordering::SeqCst));
}
#[test]
fn runner_processes_event_without_panic() {
let (tx, rx) = mpsc::channel();
start(rx, "echo iwatchr_debounce_test".to_string(), 50);
tx.send(()).unwrap();
thread::sleep(Duration::from_millis(400));
}
#[test]
fn runner_exits_cleanly_when_sender_drops() {
let (tx, rx) = mpsc::channel::<()>();
let handle = thread::spawn(move || run_loop(rx, "echo hi".to_string(), 50));
drop(tx); handle.join().expect("runner thread should exit cleanly");
}
#[test]
fn multiple_rapid_events_produce_single_run() {
let (tx, rx) = mpsc::channel();
start(rx, "echo iwatchr_burst_test".to_string(), 100);
for _ in 0..20 {
tx.send(()).unwrap();
thread::sleep(Duration::from_millis(5));
}
thread::sleep(Duration::from_millis(500));
}
}