myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

commit 7ea5a73494875aa02eb2ee9603699caac620d86a
parent 565b1c6b147dbff6712df07e4688665b1bc1bf2d
Author: triesap <tyson@radroots.org>
Date:   Thu,  2 Apr 2026 01:41:19 +0000

tests: prove timeout helper is reaped

- record the helper pid before the timeout completes
- run the real process executor against a live helper script
- assert the observed helper pid is gone after timeout
- keep the reaping proof stable under the full nix lanes

Diffstat:
Msrc/custody.rs | 66+++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
1 file changed, 57 insertions(+), 9 deletions(-)

diff --git a/src/custody.rs b/src/custody.rs @@ -1374,6 +1374,7 @@ mod tests { #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; + use std::process::Command; use std::sync::Mutex; use std::time::Instant; @@ -1403,14 +1404,34 @@ mod tests { } #[cfg(unix)] - fn write_timeout_helper(path: &Path) { - let script = "#!/bin/sh\nwhile :; do\n :\ndone\n"; + fn shell_single_quote(value: &str) -> String { + format!("'{}'", value.replace('\'', "'\"'\"'")) + } + + #[cfg(unix)] + fn write_timeout_helper(path: &Path, pid_path: &Path) { + let script = format!( + "#!/bin/sh\nprintf '%s\\n' \"$$\" > {}\nwhile :; do\n :\ndone\n", + shell_single_quote(&pid_path.display().to_string()) + ); fs::write(path, script).expect("write helper"); let mut permissions = fs::metadata(path).expect("helper metadata").permissions(); permissions.set_mode(0o755); fs::set_permissions(path, permissions).expect("helper permissions"); } + #[cfg(unix)] + fn process_exists(pid: u32) -> bool { + Command::new("kill") + .arg("-0") + .arg(pid.to_string()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .expect("kill probe") + .success() + } + #[derive(Debug)] struct FakeExternalCommandExecutor { identity: RadrootsIdentity, @@ -1934,20 +1955,47 @@ mod tests { #[cfg(unix)] #[test] fn process_executor_times_out_and_kills_real_helper() { + let timeout = Duration::from_secs(2); let temp = tempfile::tempdir().expect("tempdir"); let helper_path = temp.path().join("timeout-helper.sh"); - write_timeout_helper(&helper_path); + let pid_path = temp.path().join("timeout-helper.pid"); + write_timeout_helper(&helper_path, &pid_path); + + let helper_path_for_thread = helper_path.clone(); + let handle = std::thread::spawn(move || { + let executor = MycProcessCommandExecutor; + let started_at = Instant::now(); + let err = executor + .execute( + &helper_path_for_thread, + b"{\"operation\":\"describe\"}", + timeout, + ) + .expect_err("timeout"); + (started_at.elapsed(), err) + }); - let executor = MycProcessCommandExecutor; - let started_at = Instant::now(); - let err = executor - .execute(&helper_path, b"{\"operation\":\"describe\"}", Duration::from_millis(100)) - .expect_err("timeout"); + let pid_deadline = Instant::now() + timeout + Duration::from_secs(1); + let pid = loop { + match fs::read_to_string(&pid_path) { + Ok(value) => break value.trim().parse::<u32>().expect("pid"), + Err(error) + if error.kind() == std::io::ErrorKind::NotFound + && Instant::now() < pid_deadline => + { + std::thread::sleep(Duration::from_millis(10)); + } + Err(error) => panic!("helper pid: {error}"), + } + }; + + let (elapsed, err) = handle.join().expect("executor thread"); assert!(matches!(err, MycExternalCommandExecuteError::TimedOut)); assert!( - started_at.elapsed() < Duration::from_secs(2), + elapsed < timeout + Duration::from_secs(2), "timeout path should stay bounded" ); + assert!(!process_exists(pid), "helper process should be terminated"); } }