diff --git a/src/lib.rs b/src/lib.rs index 895bb03..e563b55 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -304,9 +304,8 @@ impl Watch { } } - let mut current_child = SharedChild::new(); + let mut exec = Executor::new(tx.clone()); let mut lock_guard = Some(self.watch_lock.write()); - let mut generation: u64 = 0; // `pending_build` tracks whether a change has arrived that has not yet // been translated into a spawned command. It starts as `true` so the @@ -324,36 +323,7 @@ impl Watch { pending_build = false; has_file_changes = false; log::info!("Running command"); - let mut current_child = current_child.clone(); - let mut list = list.clone(); - let tx = tx.clone(); - let build_id = generation; - thread::spawn(move || { - let mut status = ExitStatus::default(); - - list.spawn(|res| match res { - Err(err) => { - log::error!("Could not execute command: {err}"); - false - } - Ok(child) => { - log::trace!("Child spawned PID: {}", child.id()); - current_child.replace(child); - status = current_child.wait(); - status.success() - } - }); - - if status.success() { - log::info!("Command succeeded."); - tx.send(Event::CommandSucceeded(build_id)) - .expect("can send"); - } else if let Some(code) = status.code() { - log::error!("Command failed (exit code: {code})"); - } else { - log::error!("Command failed."); - } - }); + exec.spawn(list.clone()); } // Drain all events that arrive within the debounce window. Each @@ -361,7 +331,7 @@ impl Watch { // been quiet for `debounce`. loop { match rx.recv_timeout(self.debounce) { - Ok(Event::ChangeDetected { git }) => { + Ok(WatchEvent::ChangeDetected { git }) => { if git { log::trace!("Git directory change detected, resetting debounce timer"); } else { @@ -369,23 +339,20 @@ impl Watch { has_file_changes = true; } if !pending_build { - current_child.terminate(); - generation += 1; + exec.cancel(); if lock_guard.is_none() { lock_guard = Some(self.watch_lock.write()); } pending_build = true; } } - Ok(Event::CommandSucceeded(build_id)) if build_id == generation => { - log::trace!("Command succeeded, releasing lock"); - lock_guard.take(); - // Continue waiting for the next change. - } - Ok(Event::CommandSucceeded(build_id)) => { - log::trace!( - "Ignoring stale success from build {build_id} (current: {generation})" - ); + Ok(WatchEvent::BuildFinished) => { + if let Some(succeeded) = exec.take_result() { + if succeeded { + log::trace!("Command succeeded, releasing lock"); + lock_guard.take(); + } + } } Err(mpsc::RecvTimeoutError::Timeout) => { // Quiet for `debounce` — time to build if there is a @@ -418,7 +385,7 @@ impl Watch { } } Err(mpsc::RecvTimeoutError::Disconnected) => { - current_child.terminate(); + exec.cancel(); return Ok(()); } } @@ -559,7 +526,7 @@ impl Watch { struct WatchEventHandler { watch: Watch, - tx: mpsc::Sender, + tx: mpsc::Sender, git_dirs: Vec, } @@ -584,14 +551,14 @@ impl notify::EventHandler for WatchEventHandler { { log::trace!("Git directory change detected in {event:?}"); self.tx - .send(Event::ChangeDetected { git: true }) + .send(WatchEvent::ChangeDetected { git: true }) .expect("can send"); return; } log::trace!("Changes detected in {event:?}"); self.tx - .send(Event::ChangeDetected { git: false }) + .send(WatchEvent::ChangeDetected { git: false }) .expect("can send"); } else { log::trace!("Ignoring non-create/modify event: {event:?}"); @@ -684,6 +651,77 @@ impl SharedChild { } } +/// Encapsulates a build running on a background thread. +/// +/// Tracks the child process and [`JoinHandle`] so the +/// main loop can poll for completion without a dedicated channel. +struct Executor { + child: SharedChild, + build_handle: Option>, + tx: mpsc::Sender, +} + +impl Executor { + fn new(tx: mpsc::Sender) -> Self { + Self { + child: SharedChild::new(), + build_handle: None, + tx, + } + } + + /// Spawn a build on a background thread. + fn spawn(&mut self, mut commands: CommandList) { + let mut child = self.child.clone(); + let tx = self.tx.clone(); + + self.build_handle = Some(thread::spawn(move || { + let mut status = ExitStatus::default(); + + commands.spawn(|res| match res { + Err(err) => { + log::error!("Could not execute command: {err}"); + false + } + Ok(process) => { + log::trace!("Child spawned PID: {}", process.id()); + child.replace(process); + status = child.wait(); + status.success() + } + }); + + if status.success() { + log::info!("Command succeeded."); + } else if let Some(code) = status.code() { + log::error!("Command failed (exit code: {code})"); + } else { + log::error!("Command failed."); + } + + let _ = tx.send(WatchEvent::BuildFinished); + status.success() + })); + } + + /// Terminate the current build (if any). + fn cancel(&mut self) { + self.child.terminate(); + self.build_handle.take(); + } + + /// If the current build has finished, return `Some(success)`, + /// otherwise return `None`. + fn take_result(&mut self) -> Option { + let handle = self.build_handle.as_ref()?; + if handle.is_finished() { + Some(self.build_handle.take().unwrap().join().unwrap_or(false)) + } else { + None + } + } +} + /// A list of commands to run. #[derive(Debug, Clone)] pub struct CommandList { @@ -800,9 +838,9 @@ impl WatchLock { } #[derive(Debug)] -enum Event { - CommandSucceeded(u64), +enum WatchEvent { ChangeDetected { git: bool }, + BuildFinished, } #[cfg(test)]