//! SIGTERM cancels a world tool and preserves its durable invocation for restart. use sausage::{ cred::Cred, harness::{input, run_module_with_input}, world::World, }; use std::{ path::Path, process::{Command, Stdio}, sync::Arc, time::{Duration, Instant}, }; #[tokio::test(flavor = "current_thread")] async fn shutdown_child() -> anyhow::Result<()> { let Ok(path) = std::env::var("SHUTDOWN_WORLD") else { return Ok(()); }; let world = World::open(Path::new(&path))?; let processes = world.processes.clone(); let cred = Arc::new(Cred::open(&world.host_dir)?); let exec = sausage::process::exec(World { root: world.root.clone(), host_dir: world.host_dir.clone(), processes: processes.clone(), })?; let (sender, receiver) = input::channel(); let signal = tokio::spawn(input::stop_signal(sender)?); run_module_with_input( world, exec, cred, receiver, concat!( "globalThis.foundationCase = 'shutdown';\n", include_str!("../../js/foundation-test-bundle.js") ), ) .await?; signal.abort(); tokio::time::timeout(Duration::from_secs(1), async { while !processes.lock().unwrap().is_empty() { tokio::task::yield_now().await; } }) .await?; Ok(()) } #[test] fn sigterm_cancels_tool_and_resumes() -> anyhow::Result<()> { let source = Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap(); assert!( Command::new("cargo") .args([ "build", "--release", "--locked", "-p", "sausage-coreutils", "--bin", "sleep", "--manifest-path", ]) .arg(source.join("Cargo.toml")) .args(["--target", "wasm32-wasip2", "--target-dir"]) .arg(target) .args(["-j", "2"]) .status()? .success() ); let dir = tempfile::tempdir()?; World::open(dir.path())?; std::fs::create_dir_all(dir.path().join("world/bin"))?; std::fs::copy( target.join("wasm32-wasip2/release/sleep.wasm"), dir.path().join("world/bin/sleep.wasm"), )?; let spawn = || { Command::new(std::env::current_exe().unwrap()) .args(["--exact", "harness_shutdown::shutdown_child", "--nocapture"]) .env("SHUTDOWN_WORLD", dir.path()) .stdin(Stdio::null()) .spawn() }; let mut child = spawn()?; let deadline = Instant::now() + Duration::from_secs(60); while !dir.path().join("world/sleep-ready").exists() { if let Some(status) = child.try_wait()? { anyhow::bail!("Tool child exited early: {status}"); } if Instant::now() > deadline { child.kill()?; child.wait()?; anyhow::bail!("World sleep did not start"); } std::thread::yield_now(); } assert!( Command::new("kill") .args(["-TERM", &child.id().to_string()]) .status()? .success() ); let deadline = Instant::now() + Duration::from_secs(5); loop { if let Some(status) = child.try_wait()? { assert!(status.success()); break; } if Instant::now() > deadline { child.kill()?; child.wait()?; anyhow::bail!("SIGTERM exceeded five seconds"); } std::thread::yield_now(); } std::fs::write(dir.path().join("world/resume"), "yes")?; let mut child = spawn()?; let deadline = Instant::now() + Duration::from_secs(20); loop { if let Some(status) = child.try_wait()? { assert!(status.success()); break; } if Instant::now() > deadline { child.kill()?; child.wait()?; anyhow::bail!("Restart did not complete"); } std::thread::yield_now(); } Ok(()) }