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