launch.rs
raw
//! 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());
}
}