Skip to main content

hyperdb_mcp/daemon/
spawn.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Spawn the daemon as a detached background process.
5//!
6//! When an MCP client starts and no daemon is running, it spawns one using the
7//! current binary with the `daemon` subcommand. The spawned process is fully
8//! detached so it outlives the parent MCP session.
9
10use std::io;
11use std::process::Command;
12use std::time::{Duration, Instant};
13
14use tracing::{debug, info};
15
16use super::discovery::{self, DaemonInfo};
17
18/// Maximum time to wait for the daemon to write its discovery file after spawning.
19const SPAWN_TIMEOUT: Duration = Duration::from_secs(10);
20
21/// Polling interval while waiting for the discovery file.
22const POLL_INTERVAL: Duration = Duration::from_millis(100);
23
24/// Ensure a daemon is running and return its info.
25/// If no daemon is detected, spawn one and wait for it to become ready.
26///
27/// # Errors
28/// Returns an error if the daemon cannot be spawned or does not become ready
29/// within the timeout period.
30pub fn ensure_daemon(port: u16) -> io::Result<DaemonInfo> {
31    // Check if already running
32    if let Some(info) = discovery::discover() {
33        debug!(endpoint = %info.hyperd_endpoint, "daemon already running");
34        return Ok(info);
35    }
36
37    info!("no running daemon detected, spawning one");
38    spawn_detached(port)?;
39    wait_for_daemon()
40}
41
42/// Spawn `hyperdb-mcp daemon` as a fully detached background process.
43fn spawn_detached(port: u16) -> io::Result<()> {
44    let exe = std::env::current_exe()?;
45    let port_str = port.to_string();
46
47    let mut cmd = Command::new(&exe);
48    cmd.arg("daemon").arg("--port").arg(&port_str);
49
50    // Detach from parent: redirect stdio to null
51    cmd.stdin(std::process::Stdio::null());
52    cmd.stdout(std::process::Stdio::null());
53    cmd.stderr(std::process::Stdio::null());
54
55    // Platform-specific detach flags
56    #[cfg(unix)]
57    {
58        use std::os::unix::process::CommandExt;
59        // SAFETY: setsid() is async-signal-safe per POSIX. Called in pre_exec
60        // (between fork and exec) to create a new session so the daemon isn't
61        // killed when the parent terminal/process exits.
62        unsafe {
63            cmd.pre_exec(|| {
64                libc::setsid();
65                Ok(())
66            });
67        }
68    }
69
70    #[cfg(windows)]
71    {
72        use std::os::windows::process::CommandExt;
73        const CREATE_NO_WINDOW: u32 = 0x0800_0000;
74        const DETACHED_PROCESS: u32 = 0x0000_0008;
75        cmd.creation_flags(CREATE_NO_WINDOW | DETACHED_PROCESS);
76    }
77
78    let child = cmd.spawn()?;
79    info!(pid = child.id(), "daemon process spawned");
80    Ok(())
81}
82
83/// Poll for the discovery file to appear (daemon is ready).
84fn wait_for_daemon() -> io::Result<DaemonInfo> {
85    let start = Instant::now();
86    loop {
87        if let Some(info) = discovery::discover() {
88            info!(endpoint = %info.hyperd_endpoint, "daemon is ready");
89            return Ok(info);
90        }
91
92        if start.elapsed() >= SPAWN_TIMEOUT {
93            return Err(io::Error::new(
94                io::ErrorKind::TimedOut,
95                format!(
96                    "daemon did not become ready within {} seconds",
97                    SPAWN_TIMEOUT.as_secs()
98                ),
99            ));
100        }
101
102        std::thread::sleep(POLL_INTERVAL);
103    }
104}