Skip to main content

term_session/
auto_spawn.rs

1use std::io;
2use std::process::{Child, Command, Stdio};
3use std::thread;
4use std::time::{Duration, Instant};
5
6use term_session_muxio_service_definitions::{ChannelName, probe_ipc_endpoint};
7
8/// Parameters forwarded to the auto-spawned server process.
9#[derive(Clone, Debug)]
10pub struct ServerSpawnConfig<'a> {
11    pub channel: &'a ChannelName,
12    pub cmd: &'a [String],
13}
14
15fn spawn_detached_server(cfg: &ServerSpawnConfig<'_>) -> io::Result<Child> {
16    let bin = std::env::current_exe()?;
17    let mut cmd = Command::new(bin);
18    cmd.arg("--server")
19        .arg("--channel")
20        .arg(cfg.channel.to_string());
21    if !cfg.cmd.is_empty() {
22        cmd.arg("--").args(cfg.cmd);
23    }
24    // All stdio is detached: a daemon must not rely on the parent reading its
25    // pipes. In particular, a piped stderr that is never drained lets the OS
26    // pipe buffer fill, blocking the server's stderr writes and deadlocking
27    // startup on every platform. Discard it instead.
28    cmd.stdin(Stdio::null())
29        .stdout(Stdio::null())
30        .stderr(Stdio::null());
31    #[cfg(unix)]
32    {
33        use std::os::unix::process::CommandExt;
34        // Start the server in its own session and process group via setsid().
35        // This is the only process-group manipulation done here: a child that
36        // already became a process-group leader (e.g. via setpgid) would have
37        // setsid() fail with EPERM. Detaching from the launching terminal means
38        // the daemon can never freeze its input, and terminal Ctrl+C / Ctrl+Z /
39        // SIGHUP-on-close are never delivered to it.
40        unsafe {
41            cmd.pre_exec(|| {
42                if libc::setsid() == -1 {
43                    return Err(std::io::Error::last_os_error());
44                }
45                Ok(())
46            });
47        }
48    }
49    #[cfg(windows)]
50    {
51        use std::os::windows::process::CommandExt;
52        cmd.creation_flags(0x08000000);
53    }
54    cmd.spawn()
55}
56
57/// Wait for a session server to become reachable on the channel, spawning one
58/// via `current_exe() --server` if none is running.
59///
60/// Returns the channel name string, which the caller passes to the muxio IPC
61/// client. The client and server both route it through `GenericNamespaced`, so
62/// no filesystem path is involved.
63pub fn connect_or_spawn_server(
64    channel: &ChannelName,
65    cfg: &ServerSpawnConfig<'_>,
66) -> io::Result<String> {
67    let socket_name = channel.to_string();
68
69    if probe_ipc_endpoint(channel) {
70        return Ok(socket_name);
71    }
72
73    let mut child = spawn_detached_server(cfg)?;
74    let start = Instant::now();
75    let timeout = Duration::from_secs(3);
76    let poll_interval = Duration::from_millis(50);
77
78    while start.elapsed() < timeout {
79        if probe_ipc_endpoint(channel) {
80            return Ok(socket_name);
81        }
82        if let Ok(Some(status)) = child.try_wait() {
83            // The spawned server died before the socket came up. Another racer
84            // may have won the bind; re-probe before surfacing the failure.
85            if probe_ipc_endpoint(channel) {
86                return Ok(socket_name);
87            }
88            return Err(io::Error::new(
89                io::ErrorKind::ConnectionRefused,
90                format!("Session server exited during startup with status: {status}"),
91            ));
92        }
93        thread::sleep(poll_interval);
94    }
95
96    Err(io::Error::new(
97        io::ErrorKind::TimedOut,
98        format!("Timed out waiting for server on channel '{channel}'"),
99    ))
100}