From 6c836d454da0d429bd3867ad66f1de42f5212595 Mon Sep 17 00:00:00 2001 From: Eric Manganaro Date: Fri, 24 Jul 2026 22:46:19 -0400 Subject: [PATCH 1/2] fix: isolate Coatcheck host shutdown --- src/main.rs | 65 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/src/main.rs b/src/main.rs index ec7cc3e..66c550e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -478,34 +478,38 @@ impl HostGuard { }) } - fn stop(&mut self) { + fn stop(&mut self) -> Option { if self.stopped { - return; + return self.child.try_wait().ok().flatten(); } self.stopped = true; + if let Ok(Some(status)) = self.child.try_wait() { + return Some(status); + } + #[cfg(unix)] { - let group = format!("-{}", self.child.id()); - let _ = Command::new("kill") - .args(["-TERM", group.as_str()]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); + signal("-INT", &self.child.id().to_string()); } #[cfg(not(unix))] { let _ = self.child.kill(); } - for _ in 0..10 { - if matches!(self.child.try_wait(), Ok(Some(_))) { - return; + for _ in 0..100 { + if let Ok(Some(status)) = self.child.try_wait() { + return Some(status); } thread::sleep(Duration::from_millis(50)); } + + #[cfg(unix)] + { + signal("-KILL", &format!("-{}", self.child.id())); + } let _ = self.child.kill(); - let _ = self.child.wait(); + self.child.wait().ok() } } @@ -514,3 +518,40 @@ impl Drop for HostGuard { self.stop(); } } + +#[cfg(unix)] +fn signal(signal: &str, target: &str) { + let _ = Command::new("kill") + .args([signal, "--", target]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); +} + +#[cfg(all(test, unix))] +mod tests { + use std::os::unix::process::ExitStatusExt; + + use super::{Config, HostGuard}; + + #[test] + fn host_stop_interrupts_the_child_without_terminating_its_parent() { + let project = tempfile::tempdir().unwrap(); + let config: Config = toml::from_str( + r#" +version = 1 +name = "Host cleanup" + +[host] +command = ["sleep", "30"] +url = "http://127.0.0.1:3000" +"#, + ) + .unwrap(); + let mut host = HostGuard::spawn(project.path(), &config).unwrap(); + + let status = host.stop().expect("host should be reaped"); + + assert_eq!(status.signal(), Some(2)); + } +} From 218383b57e182d9d2a77968758cab3605d9e6600 Mon Sep 17 00:00:00 2001 From: Eric Manganaro Date: Fri, 24 Jul 2026 22:46:34 -0400 Subject: [PATCH 2/2] chore: release Coatcheck 0.2.1 --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a13bab7..09146bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.2.1 + +- Interrupt only the configured host process during graceful shutdown so + Topcoat can clean up its own child server without terminating the CI runner. +- Retain an isolated process-group kill as a bounded fallback for unresponsive + host commands. + ## 0.2.0 - Add additive, Serde-compatible manifest-v1 and frame-v1 contracts. diff --git a/Cargo.lock b/Cargo.lock index e505635..db4a441 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,7 +134,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "coatcheck" -version = "0.2.0" +version = "0.2.1" dependencies = [ "clap", "serde", diff --git a/Cargo.toml b/Cargo.toml index 923e9f3..ff2e7d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "coatcheck" -version = "0.2.0" +version = "0.2.1" edition = "2024" license = "MIT" rust-version = "1.95"