diff --git a/Cargo.lock b/Cargo.lock index 423882bf..5275a63f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4080,7 +4080,6 @@ dependencies = [ "dirs 6.0.0", "dunce", "ide-core", - "libc", "serde", "serde_json", "specta", @@ -4104,6 +4103,7 @@ version = "0.1.33" dependencies = [ "dunce", "encoding_rs", + "libc", "serde", "serde_json", "specta", @@ -4173,7 +4173,6 @@ name = "ide-docker" version = "0.1.33" dependencies = [ "ide-core", - "libc", "serde", "serde_json", "specta", @@ -4200,7 +4199,6 @@ version = "0.1.33" dependencies = [ "ide-core", "ide-vcs", - "libc", "serde", "serde_json", "specta", diff --git a/crates/ide-build/Cargo.toml b/crates/ide-build/Cargo.toml index 94686372..bda630cf 100644 --- a/crates/ide-build/Cargo.toml +++ b/crates/ide-build/Cargo.toml @@ -15,8 +15,5 @@ specta.workspace = true tracing.workspace = true dirs.workspace = true -[target.'cfg(unix)'.dependencies] -libc = "0.2" - [dev-dependencies] tempfile.workspace = true diff --git a/crates/ide-build/src/process.rs b/crates/ide-build/src/process.rs index 8177089f..db169b4c 100644 --- a/crates/ide-build/src/process.rs +++ b/crates/ide-build/src/process.rs @@ -2,11 +2,12 @@ //! line to a callback, and cancellation kills the whole process group (the //! `mvnw` / `gradlew` wrapper scripts start the JVM as a child). +use ide_core::process::kill_tree; use ide_core::{CancellationToken, IdeError, IdeResult}; use std::collections::BTreeMap; use std::io::{BufRead, BufReader, Read}; use std::path::Path; -use std::process::{Child, Command, Stdio}; +use std::process::{Command, Stdio}; use std::sync::mpsc; use std::time::Duration; @@ -22,18 +23,6 @@ pub(crate) struct Output { pub text: String, } -fn kill_tree(child: &mut Child) { - #[cfg(unix)] - if let Ok(pid) = i32::try_from(child.id()) { - // SAFETY: plain syscall; the group was created by `process_group(0)` at spawn. - unsafe { - libc::kill(-pid, libc::SIGKILL); - } - } - let _ = child.kill(); - let _ = child.wait(); -} - /// `program` as spawnable: a bare name (`mvn`, `mvn.cmd`) resolves on the IDE's /// search path — the login shell's `PATH` plus version-manager shims — since the /// app's own `PATH` can be minimal and `Command` would not see `mvn` there. @@ -80,12 +69,8 @@ pub(crate) fn run_with_env( let mut command = Command::new(&program); command.args(args).current_dir(cwd).envs(env).envs(encoding.env.iter().map(|(k, v)| (k, v))).stdin(Stdio::null()); command.stdout(Stdio::piped()).stderr(Stdio::piped()); - #[cfg(unix)] - { - use std::os::unix::process::CommandExt; - command.process_group(0); - } - ide_core::process::prepare(&mut command); + // Its own group: `mvnw` / `gradlew` start the JVM as a child, and cancelling kills both. + ide_core::process::prepare(ide_core::process::own_group(&mut command)); if clear_env { command.env_clear().envs(env).envs(encoding.env); } diff --git a/crates/ide-core/Cargo.toml b/crates/ide-core/Cargo.toml index 81e06570..4f9b43a6 100644 --- a/crates/ide-core/Cargo.toml +++ b/crates/ide-core/Cargo.toml @@ -18,5 +18,8 @@ tokio-util.workspace = true [dev-dependencies] tempfile.workspace = true +[target.'cfg(unix)'.dependencies] +libc = "0.2" + [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61", features = ["Win32_Globalization", "Win32_Security", "Win32_System_Registry", "Win32_Foundation"] } diff --git a/crates/ide-core/src/process.rs b/crates/ide-core/src/process.rs index 9aaaff28..b7a74e44 100644 --- a/crates/ide-core/src/process.rs +++ b/crates/ide-core/src/process.rs @@ -1,8 +1,11 @@ //! Child-process helpers shared by every crate that spawns tools. use std::ffi::{OsStr, OsString}; -use std::process::Command; +#[cfg(windows)] +use std::process::Stdio; +use std::process::{Child, Command}; use std::sync::{Arc, RwLock}; +use std::time::Duration; /// Windows `CREATE_NO_WINDOW` creation flag, for call sites that combine it /// with other flags (and then call [`host_env`] themselves). Prefer [`prepare`] otherwise. @@ -25,6 +28,70 @@ pub fn prepare(cmd: &mut Command) -> &mut Command { host_env(cmd) } +/// Starts the child as the leader of its own process group on Unix, so [`kill_tree`] and +/// [`terminate_tree`] reach what it starts in turn (`gh` → `git`, `npm` → `node`, `mvnw` → +/// the JVM). A no-op elsewhere: on Windows the tree is found from the child's process id. +pub fn own_group(cmd: &mut Command) -> &mut Command { + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + cmd.process_group(0); + } + cmd +} + +/// Kills the child and everything it started, then reaps it. +/// +/// - Unix: `SIGKILL` to its process group, so spawn it with [`own_group`]. +/// - Windows: `taskkill /T /F`, which walks the tree from the child's process id. +pub fn kill_tree(child: &mut Child) { + #[cfg(unix)] + signal_group(child, libc::SIGKILL); + #[cfg(windows)] + taskkill_tree(child); + let _ = child.kill(); + let _ = child.wait(); +} + +/// [`kill_tree`] that first asks the group to exit: `SIGTERM`, then `grace`, then `SIGKILL`. +/// Git removes its `index.lock` on `SIGTERM`, where `SIGKILL` leaves it behind and the next +/// git command fails. Windows has no such signal to send from outside the child's console, +/// so there it is [`kill_tree`]. +pub fn terminate_tree(child: &mut Child, grace: Duration) { + #[cfg(unix)] + { + signal_group(child, libc::SIGTERM); + // Sleep rather than `try_wait`: the child stays unreaped, so its process id (and the + // group id) cannot be reused before the `SIGKILL` below. + std::thread::sleep(grace); + } + #[cfg(not(unix))] + let _ = grace; + kill_tree(child); +} + +#[cfg(unix)] +fn signal_group(child: &Child, signal: libc::c_int) { + if let Ok(pid) = libc::pid_t::try_from(child.id()) { + // SAFETY: plain syscall. The child is not reaped yet, so `-pid` is still its group (or + // no group at all when it was not spawned with `own_group`, and the call fails). + unsafe { + libc::kill(-pid, signal); + } + } +} + +#[cfg(windows)] +fn taskkill_tree(child: &Child) { + let mut taskkill = Command::new("taskkill"); + prepare(&mut taskkill) + .args(["/T", "/F", "/PID", &child.id().to_string()]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + let _ = taskkill.status(); +} + /// Gives `cmd` the host's environment back when the IDE runs from an AppImage. `AppRun` and /// its hooks point `LD_LIBRARY_PATH`, `PATH`, `XDG_DATA_DIRS`, `GIO_MODULE_DIR`, `GTK_PATH`, /// `GST_PLUGIN_*`, ... into the mounted image for the IDE itself; a host program that @@ -597,3 +664,71 @@ mod tests { assert_eq!(find_executable("nope", &search), None); } } + +#[cfg(all(test, unix))] +mod kill_tests { + use super::*; + use std::path::Path; + use std::time::Instant; + + /// Runs `script` in `sh` as its own group's leader and waits for it to write `ready`. + fn start(script: &str, ready: &Path) -> Child { + let mut cmd = Command::new("sh"); + cmd.arg("-c").arg(script); + let child = own_group(&mut cmd).spawn().unwrap(); + let deadline = Instant::now() + Duration::from_secs(5); + while !ready.exists() { + assert!(Instant::now() < deadline, "the script never got ready"); + std::thread::sleep(Duration::from_millis(10)); + } + child + } + + fn assert_gone(pid: libc::pid_t) { + let deadline = Instant::now() + Duration::from_secs(5); + // SAFETY: signal 0 only checks that the process exists. + while unsafe { libc::kill(pid, 0) } == 0 { + assert!(Instant::now() < deadline, "process {pid} survived"); + std::thread::sleep(Duration::from_millis(20)); + } + } + + #[test] + fn kill_tree_kills_what_the_child_started() { + let dir = tempfile::tempdir().unwrap(); + let pid_file = dir.path().join("pid"); + let script = format!("sleep 30 & echo $! > '{0}.tmp' && mv '{0}.tmp' '{0}'; wait", pid_file.display()); + let mut child = start(&script, &pid_file); + let grandchild: libc::pid_t = std::fs::read_to_string(&pid_file).unwrap().trim().parse().unwrap(); + kill_tree(&mut child); + assert!(child.try_wait().unwrap().is_some(), "the child is reaped"); + assert_gone(grandchild); + } + + #[test] + fn terminate_tree_lets_the_child_clean_up() { + let dir = tempfile::tempdir().unwrap(); + let (ready, cleaned) = (dir.path().join("ready"), dir.path().join("cleaned")); + let script = format!( + "trap 'touch \"{}\"; exit 0' TERM; touch \"{}\"; sleep 30 & wait", + cleaned.display(), + ready.display() + ); + let mut child = start(&script, &ready); + terminate_tree(&mut child, Duration::from_millis(500)); + assert!(cleaned.exists(), "the SIGTERM handler ran before the SIGKILL"); + assert!(child.try_wait().unwrap().is_some(), "the child is reaped"); + } + + #[test] + fn terminate_tree_still_kills_a_child_that_ignores_sigterm() { + let dir = tempfile::tempdir().unwrap(); + let ready = dir.path().join("ready"); + let script = format!("trap '' TERM; touch \"{}\"; while :; do sleep 1; done", ready.display()); + let mut child = start(&script, &ready); + let started = Instant::now(); + terminate_tree(&mut child, Duration::from_millis(50)); + assert!(child.try_wait().unwrap().is_some(), "the child is reaped"); + assert!(started.elapsed() < Duration::from_secs(5)); + } +} diff --git a/crates/ide-docker/Cargo.toml b/crates/ide-docker/Cargo.toml index 1431fa54..ae06863d 100644 --- a/crates/ide-docker/Cargo.toml +++ b/crates/ide-docker/Cargo.toml @@ -13,8 +13,5 @@ specta.workspace = true tracing.workspace = true which = "8" -[target.'cfg(unix)'.dependencies] -libc = "0.2" - [dev-dependencies] tempfile.workspace = true diff --git a/crates/ide-docker/src/process.rs b/crates/ide-docker/src/process.rs index 6c08c005..3941991d 100644 --- a/crates/ide-docker/src/process.rs +++ b/crates/ide-docker/src/process.rs @@ -1,11 +1,16 @@ //! Child processes of the docker CLI: bounded runs and cancellable line streams. +use ide_core::process::terminate_tree; use ide_core::{CancellationToken, IdeError, IdeResult}; use std::io::Read; use std::process::{Child, Command, Stdio}; use std::sync::mpsc; use std::time::{Duration, Instant}; +/// How long a cancelled or timed-out child's process group (CLI plugins such as compose) +/// gets to exit on `SIGTERM`, and clean up, before `SIGKILL`. +const KILL_GRACE: Duration = Duration::from_millis(20); + pub(crate) struct Output { pub stdout: String, pub stderr: String, @@ -20,36 +25,13 @@ pub(crate) enum Stream { fn prepare(cmd: &mut Command) { cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()); - #[cfg(unix)] - { - use std::os::unix::process::CommandExt; - cmd.process_group(0); - } - ide_core::process::prepare(cmd); + ide_core::process::prepare(ide_core::process::own_group(cmd)); } fn spawn(cmd: &mut Command) -> IdeResult { cmd.spawn().map_err(|e| IdeError::unavailable(format!("could not run docker: {e}"))) } -/// Kills the child and, on Unix, its whole process group (CLI plugins such as compose). -pub(crate) fn kill(child: &mut Child) { - #[cfg(unix)] - { - let pid = child.id() as libc::pid_t; - // SAFETY: plain syscall; the child leads its own process group (`prepare`). - unsafe { - libc::kill(-pid, libc::SIGTERM); - } - std::thread::sleep(Duration::from_millis(20)); - unsafe { - libc::kill(-pid, libc::SIGKILL); - } - } - let _ = child.kill(); - let _ = child.wait(); -} - fn read_all(mut pipe: impl Read + Send + 'static) -> std::thread::JoinHandle { std::thread::spawn(move || { let mut buf = Vec::new(); @@ -88,11 +70,11 @@ pub(crate) fn run_with_input( break status; } if cancel.is_some_and(CancellationToken::is_cancelled) { - kill(&mut child); + terminate_tree(&mut child, KILL_GRACE); return Err(IdeError::cancelled()); } if Instant::now() >= deadline { - kill(&mut child); + terminate_tree(&mut child, KILL_GRACE); return Err(IdeError::unavailable("docker did not respond in time")); } std::thread::sleep(Duration::from_millis(10)); @@ -166,7 +148,7 @@ pub(crate) fn stream( let mut stderr_tail: Vec = Vec::new(); while open > 0 { if cancel.is_cancelled() { - kill(&mut child); + terminate_tree(&mut child, KILL_GRACE); return Err(IdeError::cancelled()); } match rx.recv_timeout(Duration::from_millis(50)) { @@ -189,7 +171,7 @@ pub(crate) fn stream( break status; } if cancel.is_cancelled() { - kill(&mut child); + terminate_tree(&mut child, KILL_GRACE); return Err(IdeError::cancelled()); } std::thread::sleep(Duration::from_millis(10)); diff --git a/crates/ide-github/Cargo.toml b/crates/ide-github/Cargo.toml index 0e33a8e8..5d67e56a 100644 --- a/crates/ide-github/Cargo.toml +++ b/crates/ide-github/Cargo.toml @@ -13,6 +13,3 @@ serde_json.workspace = true specta.workspace = true tracing.workspace = true which = "8" - -[target.'cfg(unix)'.dependencies] -libc = "0.2" diff --git a/crates/ide-github/src/process.rs b/crates/ide-github/src/process.rs index d5eec750..e945645b 100644 --- a/crates/ide-github/src/process.rs +++ b/crates/ide-github/src/process.rs @@ -1,11 +1,16 @@ //! Child processes of the gh CLI: bounded runs and cancellable line streams. +use ide_core::process::terminate_tree; use ide_core::{CancellationToken, IdeError, IdeResult}; use std::io::Read; use std::process::{Child, Command, Stdio}; use std::sync::mpsc; use std::time::{Duration, Instant}; +/// How long a cancelled or timed-out child's process group (gh spawns git and helpers) +/// gets to exit on `SIGTERM` before `SIGKILL`: git removes its lock files on `SIGTERM`. +const KILL_GRACE: Duration = Duration::from_millis(20); + pub(crate) struct Output { pub stdout: String, pub stderr: String, @@ -20,36 +25,13 @@ pub(crate) enum Stream { fn prepare(cmd: &mut Command) { cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()); - #[cfg(unix)] - { - use std::os::unix::process::CommandExt; - cmd.process_group(0); - } - ide_core::process::prepare(cmd); + ide_core::process::prepare(ide_core::process::own_group(cmd)); } fn spawn(cmd: &mut Command) -> IdeResult { cmd.spawn().map_err(|e| IdeError::unavailable(format!("could not run gh: {e}"))) } -/// Kills the child and, on Unix, its whole process group (gh spawns git and helpers). -pub(crate) fn kill(child: &mut Child) { - #[cfg(unix)] - { - let pid = child.id() as libc::pid_t; - // SAFETY: plain syscall; the child leads its own process group (`prepare`). - unsafe { - libc::kill(-pid, libc::SIGTERM); - } - std::thread::sleep(Duration::from_millis(20)); - unsafe { - libc::kill(-pid, libc::SIGKILL); - } - } - let _ = child.kill(); - let _ = child.wait(); -} - fn read_all(mut pipe: impl Read + Send + 'static) -> std::thread::JoinHandle { std::thread::spawn(move || { let mut buf = Vec::new(); @@ -92,11 +74,11 @@ pub(crate) fn run_with_input( break status; } if cancel.is_some_and(CancellationToken::is_cancelled) { - kill(&mut child); + terminate_tree(&mut child, KILL_GRACE); return Err(IdeError::cancelled()); } if Instant::now() >= deadline { - kill(&mut child); + terminate_tree(&mut child, KILL_GRACE); return Err(IdeError::unavailable("gh did not respond in time")); } std::thread::sleep(Duration::from_millis(10)); @@ -170,7 +152,7 @@ pub(crate) fn stream( let mut stderr_tail: Vec = Vec::new(); while open > 0 { if cancel.is_cancelled() { - kill(&mut child); + terminate_tree(&mut child, KILL_GRACE); return Err(IdeError::cancelled()); } match rx.recv_timeout(Duration::from_millis(50)) { @@ -193,7 +175,7 @@ pub(crate) fn stream( break status; } if cancel.is_cancelled() { - kill(&mut child); + terminate_tree(&mut child, KILL_GRACE); return Err(IdeError::cancelled()); } std::thread::sleep(Duration::from_millis(10)); diff --git a/crates/ide-tools/Cargo.toml b/crates/ide-tools/Cargo.toml index dadce894..dea069bd 100644 --- a/crates/ide-tools/Cargo.toml +++ b/crates/ide-tools/Cargo.toml @@ -20,8 +20,8 @@ zip.workspace = true sha2 = "0.10" hex = "0.4" -[target.'cfg(unix)'.dependencies] -libc = "0.2" - [dev-dependencies] tempfile.workspace = true + +[target.'cfg(unix)'.dev-dependencies] +libc = "0.2" diff --git a/crates/ide-tools/src/process.rs b/crates/ide-tools/src/process.rs index 11bc37b4..8a7694fe 100644 --- a/crates/ide-tools/src/process.rs +++ b/crates/ide-tools/src/process.rs @@ -4,48 +4,16 @@ use crate::InstallEvent; use ide_core::error::ErrorKind; use ide_core::output_encoding::decode_tool_output; +use ide_core::process::{kill_tree, own_group}; use ide_core::{CancellationToken, IdeError, IdeResult}; use std::io::{BufRead, BufReader, Read}; use std::path::Path; -use std::process::{Child, Command, Stdio}; +use std::process::{Command, Stdio}; use std::sync::mpsc; use std::time::{Duration, Instant}; pub type Emit<'a> = &'a dyn Fn(InstallEvent); -/// Starts the child as the leader of its own process group, so [`kill`] reaches what it -/// starts in turn (`npm` → `node`, `go install` → the compiler, `pip` → build backends). -fn own_group(cmd: &mut Command) -> &mut Command { - #[cfg(unix)] - { - use std::os::unix::process::CommandExt; - cmd.process_group(0); - } - cmd -} - -/// Kills the child and everything it started. -fn kill(child: &mut Child) { - #[cfg(unix)] - if let Ok(pid) = i32::try_from(child.id()) { - // SAFETY: plain syscall; the child leads its own process group (`own_group`) and is not reaped yet. - unsafe { - libc::kill(-pid, libc::SIGKILL); - } - } - #[cfg(windows)] - { - let mut taskkill = Command::new("taskkill"); - ide_core::process::prepare(&mut taskkill) - .args(["/T", "/F", "/PID", &child.id().to_string()]) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - let _ = taskkill.status(); - } - let _ = child.kill(); - let _ = child.wait(); -} - fn describe(cmd: &Command) -> String { let mut s = Path::new(cmd.get_program()).file_name().unwrap_or_default().to_string_lossy().into_owned(); for a in cmd.get_args() { @@ -100,7 +68,7 @@ pub fn run_collect(mut cmd: Command, cancel: &CancellationToken, emit: Emit) -> let mut output = String::new(); let status = loop { if cancel.is_cancelled() { - kill(&mut child); + kill_tree(&mut child); return Err(IdeError::cancelled()); } match rx.recv_timeout(Duration::from_millis(100)) { @@ -144,7 +112,7 @@ pub fn run_quiet(mut cmd: Command, timeout: Duration) -> Option<(bool, String)> Ok(Some(s)) => break s, Ok(None) if Instant::now() < deadline => std::thread::sleep(Duration::from_millis(15)), _ => { - kill(&mut child); + kill_tree(&mut child); return None; } } @@ -256,7 +224,7 @@ impl Http { }); let status = loop { if cancel.is_cancelled() { - kill(&mut child); + kill_tree(&mut child); return Err(IdeError::cancelled()); } match child.try_wait()? { @@ -308,7 +276,7 @@ impl Http { let mut last_bytes = 0; let status = loop { if cancel.is_cancelled() { - kill(&mut child); + kill_tree(&mut child); let _ = std::fs::remove_file(dest); return Err(IdeError::cancelled()); } @@ -413,7 +381,7 @@ pub(crate) fn run_input( } }; if result.is_err() { - kill(&mut child); + kill_tree(&mut child); } result?; let output = output.unwrap()?; diff --git a/crates/ide-vcs/src/cli.rs b/crates/ide-vcs/src/cli.rs index 051984ed..0f865cea 100644 --- a/crates/ide-vcs/src/cli.rs +++ b/crates/ide-vcs/src/cli.rs @@ -1,13 +1,18 @@ //! Thin wrapper around the `git` executable used for every mutating operation. +use ide_core::process::{own_group, terminate_tree}; use ide_core::{CancellationToken, IdeError, IdeResult}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; -use std::process::{Child, Command, Stdio}; +use std::process::{Command, Stdio}; use std::sync::OnceLock; use std::sync::mpsc; use std::time::{Duration, Instant}; +/// How long a cancelled git's process group (remote helpers, `ssh`, credential helpers) gets +/// to exit on `SIGTERM` before `SIGKILL`: git removes its `index.lock` on `SIGTERM`. +const KILL_GRACE: Duration = Duration::from_millis(20); + /// Explicit git executable from Settings › Version Control › Git (`None` = auto-detect). static CONFIGURED: std::sync::RwLock> = std::sync::RwLock::new(None); @@ -361,11 +366,7 @@ pub(crate) fn run_cancellable_raw(root: &Path, args: &[&str], cancel: &Cancellat } let mut cmd = command(root)?; cmd.args(args).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()); - #[cfg(unix)] - { - use std::os::unix::process::CommandExt; - cmd.process_group(0); - } + own_group(&mut cmd); let mut session = CommandSession::start(&mut cmd); let mut child = cmd.spawn().map_err(spawn_error)?; let capture = |mut pipe: Box| { @@ -378,8 +379,7 @@ pub(crate) fn run_cancellable_raw(root: &Path, args: &[&str], cancel: &Cancellat let stderr = capture(Box::new(child.stderr.take().expect("piped stderr"))); let status = loop { if cancel.is_cancelled() { - kill(&mut child); - let _ = child.wait(); + terminate_tree(&mut child, KILL_GRACE); let _ = stdout.join(); let _ = stderr.join(); return Err(IdeError::cancelled()); @@ -504,11 +504,7 @@ pub(crate) fn run_streaming_env( cmd.env(k, v); } cmd.args(args).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()); - #[cfg(unix)] - { - use std::os::unix::process::CommandExt; - cmd.process_group(0); - } + own_group(&mut cmd); tracing::debug!(?args, "git (streaming)"); let mut session = CommandSession::start(&mut cmd); let mut child = cmd.spawn().map_err(spawn_error)?; @@ -529,8 +525,7 @@ pub(crate) fn run_streaming_env( let mut open = readers.len(); while open > 0 { if cancel.is_cancelled() { - kill(&mut child); - let _ = child.wait(); + terminate_tree(&mut child, KILL_GRACE); for r in readers { let _ = r.join(); } @@ -559,8 +554,7 @@ pub(crate) fn run_streaming_env( break status; } if cancel.is_cancelled() { - kill(&mut child); - let _ = child.wait(); + terminate_tree(&mut child, KILL_GRACE); return Err(IdeError::cancelled()); } std::thread::sleep(Duration::from_millis(10)); @@ -644,22 +638,6 @@ fn spawn_reader( }) } -fn kill(child: &mut Child) { - #[cfg(unix)] - { - let pid = child.id() as libc::pid_t; - // SAFETY: plain syscall; the child was spawned as leader of its own process group. - unsafe { - libc::kill(-pid, libc::SIGTERM); - } - std::thread::sleep(Duration::from_millis(20)); - unsafe { - libc::kill(-pid, libc::SIGKILL); - } - } - let _ = child.kill(); -} - #[cfg(test)] mod command_session_tests { use super::*;