//! Launching a resolved command as a detached process. use std::io; use std::os::unix::process::CommandExt; use std::process::{Command, Stdio}; use crate::query::Invocation; /// Spawn `invocation` as an independent process and return immediately. /// /// The child is placed in its own session (`setsid`) so that it survives the /// launcher exiting and is not tied to the launcher's controlling terminal. /// Standard streams are redirected to `/dev/null`: a GUI program has no use for /// the launcher's stdio, and leaving them attached would keep pipes open. /// /// Returns once the child has been spawned; we do not wait for it. An error is /// returned only if the spawn itself fails (which, since the command name comes /// from the `$PATH` scan, should be rare). pub fn launch(invocation: &Invocation) -> io::Result<()> { let mut command = Command::new(&invocation.command); command .args(&invocation.args) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()); // Detach into a new session between fork and exec so the launcher can exit // without signalling or reaping the child. unsafe { command.pre_exec(|| { if libc::setsid() == -1 { return Err(io::Error::last_os_error()); } Ok(()) }); } command.spawn().map(|_child| ()) } #[cfg(test)] mod tests { use super::*; /// A successful spawn returns without error and does not block. We launch /// `true`, which exits immediately, and confirm the call itself succeeds. #[test] fn spawns_a_real_command() { let inv = Invocation { command: "true".to_string(), args: vec![], }; assert!(launch(&inv).is_ok()); } /// Spawning a genuinely absent command surfaces the spawn error rather than /// panicking. (The live launcher only ever passes names drawn from `$PATH`, /// so this path is defensive.) #[test] fn missing_command_is_an_error() { let inv = Invocation { command: "liftoff-no-such-command-xyzzy".to_string(), args: vec![], }; assert!(launch(&inv).is_err()); } }