Skip to main content

imsg_proc/
lib.rs

1//! Self-respawn: detach the current binary as a background child.
2//!
3//! One shared spawn/log-redirect/detach primitive for every "re-invoke myself as a background
4//! process" call site in the workspace — the CLI's backgrounded `daemon start`, the CLI's
5//! ephemeral one-shot broker, and the GUI's headless self-provisioned daemon — previously three
6//! independent, near-identical copies differing only in argv verb and stdio/detach behavior.
7
8use std::path::Path;
9use std::process::Stdio;
10
11use anyhow::{Context, Result};
12use tokio::fs::OpenOptions;
13use tokio::process::{Child, Command};
14
15// errors only if config_path is set and isn't valid UTF-8
16fn respawn_args(verb: &[&str], addr: &str, config_path: Option<&Path>) -> Result<Vec<String>> {
17    let mut args: Vec<String> = verb.iter().map(|s| (*s).to_owned()).collect();
18    args.push("--device".to_owned());
19    args.push(addr.to_owned());
20    if let Some(p) = config_path {
21        args.push("--config".to_owned());
22        args.push(p.to_str().context("config path is not valid UTF-8")?.to_owned());
23    }
24    Ok(args)
25}
26
27// 0o600 on unix: these logs outlive this process and may carry message content
28async fn open_log(log_path: &Path) -> Result<std::fs::File> {
29    if let Some(parent) = log_path.parent() {
30        tokio::fs::create_dir_all(parent).await.context("creating log directory")?;
31    }
32    let mut open_opts = OpenOptions::new();
33    open_opts.create(true).write(true).truncate(true);
34    #[cfg(unix)]
35    open_opts.mode(0o600);
36    Ok(open_opts
37        .open(log_path)
38        .await
39        .with_context(|| format!("opening log file: {}", log_path.display()))?
40        .into_std()
41        .await)
42}
43
44/// Re-execs `current_exe()` with `verb --device <addr> [--config <config_path>]`.
45///
46/// Stdio redirected to `log_path`. Stdout is discarded unless `capture_stdout` is set (stderr
47/// always goes to the log); if `detach` is set, the child is moved into its own process group
48/// (Unix) so it survives this process exiting.
49///
50/// # Errors
51///
52/// Returns an error if the current executable path can't be resolved, `config_path` isn't
53/// valid UTF-8, the log file can't be created/opened, or spawning fails.
54pub async fn respawn_self(
55    verb: &[&str],
56    addr: &str,
57    config_path: Option<&Path>,
58    log_path: &Path,
59    capture_stdout: bool,
60    detach: bool,
61) -> Result<Child> {
62    let args = respawn_args(verb, addr, config_path)?;
63    let log_file = open_log(log_path).await?;
64    let exe = std::env::current_exe().context("resolving current executable path")?;
65
66    let mut cmd = Command::new(exe);
67    cmd.args(args);
68    cmd.stdin(Stdio::null());
69    cmd.stdout(if capture_stdout {
70        Stdio::from(log_file.try_clone().context("duplicating log file handle")?)
71    } else {
72        Stdio::null()
73    });
74    cmd.stderr(Stdio::from(log_file));
75    #[cfg(unix)]
76    if detach {
77        cmd.process_group(0);
78    }
79    cmd.spawn().context("spawning detached subprocess")
80}
81
82#[cfg(test)]
83mod tests;