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());
    }
}