Skip to main content

vs_cli/
spawn.rs

1//! Daemon auto-spawn.
2//!
3//! `vs` and the daemon are the same binary. When the CLI runs and the
4//! daemon socket is missing, [`spawn_daemon`] re-execs `vs serve`
5//! detached from the current process. The CLI then waits for the
6//! socket to appear (with a timeout). Failure to spawn or connect
7//! within the budget yields a local error (the user sees
8//! `! DAEMON_START_FAILED`-equivalent stderr).
9
10use std::path::Path;
11use std::process::{Command, Stdio};
12use std::time::{Duration, Instant};
13
14use anyhow::{Context as _, Result};
15
16/// Path to the `vs` binary that should run the daemon. Resolution
17/// order:
18///
19/// 1. `$VS_DAEMON_BIN` — explicit override (tests use this).
20/// 2. `current_exe()` — re-exec ourselves with `vs serve`. The CLI is
21///    a single binary, so this is the production path.
22/// 3. `vs` on `$PATH` — last-resort fallback if `current_exe()` fails.
23#[must_use]
24pub fn daemon_binary() -> std::path::PathBuf {
25    if let Ok(p) = std::env::var("VS_DAEMON_BIN") {
26        return p.into();
27    }
28    if let Ok(self_exe) = std::env::current_exe() {
29        return self_exe;
30    }
31    "vs".into()
32}
33
34/// Spawn `vs serve` detached. Inherits no stdio so the CLI process is
35/// not kept alive by the daemon's pipes.
36///
37/// `extra_args` are passed *after* `serve` (e.g. `--home=/tmp/x`).
38pub fn spawn_daemon(extra_args: &[&str]) -> Result<()> {
39    let bin = daemon_binary();
40    Command::new(&bin)
41        .arg("serve")
42        .args(extra_args)
43        .stdin(Stdio::null())
44        .stdout(Stdio::null())
45        .stderr(Stdio::null())
46        .spawn()
47        .with_context(|| format!("spawn {} serve", bin.display()))?;
48    Ok(())
49}
50
51/// Wait until a daemon is reachable at `socket`, polling at ~50ms.
52/// On Unix this checks for the AF_UNIX socket file; on Windows it
53/// connect-probes the named pipe (pipes don't appear on the
54/// filesystem). Returns `Ok(())` on success, error on timeout.
55pub fn wait_for_socket(socket: impl AsRef<Path>, timeout: Duration) -> Result<()> {
56    let socket = socket.as_ref();
57    let deadline = Instant::now() + timeout;
58    while Instant::now() < deadline {
59        if vs_daemon::transport::is_listening(socket) {
60            return Ok(());
61        }
62        std::thread::sleep(Duration::from_millis(50));
63    }
64    anyhow::bail!(
65        "daemon socket {} did not appear within {:?}",
66        socket.display(),
67        timeout
68    )
69}