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}