From f6df164e5147f6ecce612d9542482eb10f6480de Mon Sep 17 00:00:00 2001 From: andrussal Date: Fri, 1 May 2026 05:07:12 +0200 Subject: [PATCH] fix(local): wait for stopped child processes --- .../deployers/local/src/node_control/mod.rs | 2 +- .../deployers/local/src/process.rs | 96 ++++++++++++++++++- 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/testing-framework/deployers/local/src/node_control/mod.rs b/testing-framework/deployers/local/src/node_control/mod.rs index 0b82550..24a9a73 100644 --- a/testing-framework/deployers/local/src/node_control/mod.rs +++ b/testing-framework/deployers/local/src/node_control/mod.rs @@ -163,7 +163,7 @@ impl NodeManager { pub fn stop_all(&self) { let mut state = self.lock_state(); for node in &mut state.nodes { - node.start_kill(); + node.stop_blocking(); } state.nodes.clear(); diff --git a/testing-framework/deployers/local/src/process.rs b/testing-framework/deployers/local/src/process.rs index 53b8766..485de73 100644 --- a/testing-framework/deployers/local/src/process.rs +++ b/testing-framework/deployers/local/src/process.rs @@ -7,7 +7,7 @@ use std::{ path::{Path, PathBuf}, process::Stdio, thread, - time::Duration, + time::{Duration, Instant}, }; use fs_extra::dir::{CopyOptions, copy as copy_dir}; @@ -18,6 +18,9 @@ use tokio::{ time::timeout, }; +const PROCESS_CLEANUP_WAIT_TIMEOUT: Duration = Duration::from_secs(5); +const PROCESS_CLEANUP_POLL_INTERVAL: Duration = Duration::from_millis(20); + #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub enum NodeEndpointPort { TestingApi, @@ -197,6 +200,28 @@ impl bool { + let deadline = Instant::now() + wait_timeout; + + loop { + match self.child.try_wait() { + Ok(Some(_)) | Err(_) => return true, + Ok(None) => {} + } + + if Instant::now() >= deadline { + return false; + } + + thread::sleep(PROCESS_CLEANUP_POLL_INTERVAL); + } + } + pub fn keep_tempdir(&mut self) -> io::Result<()> { let dir = mem::replace(&mut self.tempdir, tempfile::tempdir()?); let _ = dir.keep(); @@ -294,7 +319,8 @@ fn build_process_command(tempdir: &Path, launch: &LaunchSpec) -> Command { .current_dir(tempdir) .stdin(Stdio::null()) .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()); + .stderr(Stdio::inherit()) + .kill_on_drop(true); command } @@ -310,7 +336,7 @@ impl) -> Result::spawn( + "node", + (), + |_, _, _| { + Ok(LaunchSpec { + binary: "/bin/sleep".into(), + args: vec!["60".into()], + ..LaunchSpec::default() + }) + }, + |_| Ok(NodeEndpoints::default()), + false, + None, + None, + |_| Ok(()), + ) + .await + .expect("process should spawn"); + + let pid = node.pid(); + drop(node); + + assert!( + wait_until_process_exits(pid, Duration::from_secs(1)), + "process {pid} should exit on drop" + ); + } + + #[cfg(unix)] + fn wait_until_process_exits(pid: u32, timeout: Duration) -> bool { + let deadline = Instant::now() + timeout; + + while Instant::now() < deadline { + if !process_exists(pid) { + return true; + } + + std::thread::sleep(Duration::from_millis(20)); + } + + !process_exists(pid) + } + + #[cfg(unix)] + fn process_exists(pid: u32) -> bool { + StdCommand::new("kill") + .arg("-0") + .arg(pid.to_string()) + .stderr(Stdio::null()) + .status() + .is_ok_and(|status| status.success()) + } }