command_stream/detach.rs
1//! Fire-and-forget spawning (issue #232).
2//!
3//! [`crate::StreamingRunner::detach`] and [`crate::ProcessRunner::spawn_detached`]
4//! start a command that outlives the caller and hand back its pid:
5//!
6//! * stdin, stdout and stderr are the null device, so nothing ties the child
7//! to the caller's pipes or terminal;
8//! * on Unix the child leads a new process group (`process_group(0)`), so a
9//! CTRL+C or a group-wide signal aimed at the caller does not reach it; on
10//! Windows it is created with `CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS`;
11//! * there is no `kill_on_drop`: nothing the caller drops stops the child.
12//!
13//! A background thread waits for the child, so it is reaped instead of being
14//! left as a zombie while the caller keeps running. If the caller exits first
15//! the child is reparented and keeps running.
16
17use std::path::Path;
18use std::process::Stdio;
19
20use crate::stream_process::{ChildEnv, StreamingCommand};
21use crate::trace::trace_lazy;
22use crate::Result;
23
24/// `CREATE_NEW_PROCESS_GROUP`: the child does not receive the caller's CTRL+C.
25#[cfg(windows)]
26const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
27/// `DETACHED_PROCESS`: the child does not inherit the caller's console.
28#[cfg(windows)]
29const DETACHED_PROCESS: u32 = 0x0000_0008;
30
31/// Spawn `command` detached from the caller and return its pid.
32pub(crate) fn spawn_detached(
33 command: &StreamingCommand,
34 cwd: Option<&Path>,
35 env: &ChildEnv,
36) -> Result<Option<u32>> {
37 let mut cmd = command.command(env.vars.as_ref()).into_std();
38 cmd.stdin(Stdio::null())
39 .stdout(Stdio::null())
40 .stderr(Stdio::null());
41 if let Some(cwd) = cwd {
42 cmd.current_dir(cwd);
43 }
44 env.apply(&mut cmd);
45
46 #[cfg(unix)]
47 {
48 use std::os::unix::process::CommandExt;
49 cmd.process_group(0);
50 }
51 #[cfg(windows)]
52 {
53 use std::os::windows::process::CommandExt;
54 cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS);
55 }
56
57 let mut child = cmd.spawn()?;
58 let pid = child.id();
59 trace_lazy("detach", || format!("Spawned detached child | pid={pid}"));
60 // Reap the child when it exits. If the thread cannot be started the child
61 // still runs; it is merely left unreaped until the caller exits.
62 let _ = std::thread::Builder::new()
63 .name("command-stream-detached".to_string())
64 .spawn(move || {
65 let _ = child.wait();
66 });
67 Ok(Some(pid))
68}
69
70impl crate::ProcessRunner {
71 /// Start the command detached from the caller and return its pid, without
72 /// waiting for it (issue #232).
73 ///
74 /// The command runs through the platform shell with the runner's `cwd`,
75 /// `env`, [`env_remove`](crate::RunOptions::env_remove) and `prefer_local`
76 /// options, with null stdio, in a process group of its own and without
77 /// `kill_on_drop`, so it keeps running after the runner is dropped or the
78 /// caller exits. Built-in virtual commands are not used, since they run
79 /// inside the caller. The runner itself is left unstarted.
80 ///
81 /// ```no_run
82 /// use command_stream::{ProcessRunner, RunOptions};
83 ///
84 /// let runner = ProcessRunner::new("sleep 60 && touch done", RunOptions::default());
85 /// let pid = runner.spawn_detached()?;
86 /// println!("started in the background as pid {pid:?}");
87 /// # Ok::<(), command_stream::Error>(())
88 /// ```
89 pub fn spawn_detached(&self) -> Result<Option<u32>> {
90 let options = self.options();
91 let runner = match &self.argv {
92 Some(argv) => crate::StreamingRunner::with_command(argv.clone()),
93 None => crate::StreamingRunner::new(self.command()),
94 };
95 let mut runner = runner
96 .prefer_local(options.prefer_local.clone())
97 .env_remove_all(options.env_remove.iter().cloned());
98 if let Some(cwd) = crate::resolve_spawn_cwd(options.cwd.as_ref()) {
99 runner = runner.cwd(cwd);
100 }
101 if let Some(env) = &options.env {
102 runner = runner.env(env.clone());
103 }
104 runner.detach()
105 }
106}