1use std::fmt;
2use std::io;
3use std::thread;
4use std::time::{Duration, Instant};
5
6use term_session_muxio_service_definitions::{
7 ChannelName, gateway_channel_name, probe_ipc_endpoint,
8};
9
10#[cfg(unix)]
11use std::process::{Child, Command, Stdio};
12
13pub fn resolve_gateway() -> ChannelName {
17 gateway_channel_name()
18}
19
20enum DaemonChild {
30 #[cfg(unix)]
31 Unix(Child),
32 #[cfg(windows)]
33 Windows(WindowsDaemonProcess),
34}
35
36enum DaemonExitStatus {
38 #[cfg(unix)]
39 Unix(std::process::ExitStatus),
40 #[cfg(windows)]
41 Windows(u32),
42}
43
44impl fmt::Display for DaemonExitStatus {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 #[cfg(unix)]
48 DaemonExitStatus::Unix(status) => write!(f, "{status}"),
49 #[cfg(windows)]
50 DaemonExitStatus::Windows(code) => write!(f, "exit code: {code}"),
51 }
52 }
53}
54
55impl DaemonChild {
56 fn try_wait(&mut self) -> io::Result<Option<DaemonExitStatus>> {
59 match self {
60 #[cfg(unix)]
61 DaemonChild::Unix(child) => Ok(child.try_wait()?.map(DaemonExitStatus::Unix)),
62 #[cfg(windows)]
63 DaemonChild::Windows(proc) => proc.try_wait(),
64 }
65 }
66}
67
68#[cfg(windows)]
71struct WindowsDaemonProcess {
72 process: windows_sys::Win32::Foundation::HANDLE,
73 thread: windows_sys::Win32::Foundation::HANDLE,
74}
75
76#[cfg(windows)]
77impl WindowsDaemonProcess {
78 fn try_wait(&mut self) -> io::Result<Option<DaemonExitStatus>> {
79 use windows_sys::Win32::Foundation::{WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT};
80 use windows_sys::Win32::System::Threading::{GetExitCodeProcess, WaitForSingleObject};
81 unsafe {
82 match WaitForSingleObject(self.process, 0) {
83 WAIT_TIMEOUT => Ok(None),
84 WAIT_OBJECT_0 => {
85 let mut code = 0u32;
86 if GetExitCodeProcess(self.process, &mut code) == 0 {
87 return Err(io::Error::last_os_error());
88 }
89 Ok(Some(DaemonExitStatus::Windows(code)))
90 }
91 WAIT_FAILED => Err(io::Error::last_os_error()),
92 _ => Err(io::Error::last_os_error()),
93 }
94 }
95 }
96}
97
98#[cfg(windows)]
99impl Drop for WindowsDaemonProcess {
100 fn drop(&mut self) {
101 use windows_sys::Win32::Foundation::CloseHandle;
102 unsafe {
103 let _ = CloseHandle(self.process);
104 let _ = CloseHandle(self.thread);
105 }
106 }
107}
108
109fn spawn_detached_server(bin: &std::path::Path) -> io::Result<DaemonChild> {
110 #[cfg(unix)]
111 {
112 unix_spawn_detached_server(bin)
113 }
114 #[cfg(windows)]
115 {
116 windows_spawn_detached_server(bin)
117 }
118 #[cfg(not(any(unix, windows)))]
119 {
120 Err(io::Error::new(
121 io::ErrorKind::Unsupported,
122 "daemon detachment is not supported on this platform",
123 ))
124 }
125}
126
127#[cfg(unix)]
128fn unix_spawn_detached_server(bin: &std::path::Path) -> io::Result<DaemonChild> {
129 use std::os::unix::process::CommandExt;
130 let mut cmd = Command::new(bin);
131 cmd.arg("--daemon");
132 cmd.stdin(Stdio::null())
137 .stdout(Stdio::null())
138 .stderr(Stdio::null());
139 unsafe {
146 cmd.pre_exec(|| {
147 if libc::setsid() == -1 {
148 return Err(std::io::Error::last_os_error());
149 }
150 Ok(())
151 });
152 }
153 cmd.spawn().map(DaemonChild::Unix)
154}
155
156#[cfg(windows)]
157fn windows_spawn_detached_server(bin: &std::path::Path) -> io::Result<DaemonChild> {
158 use std::os::windows::ffi::OsStrExt;
159 use windows_sys::Win32::Foundation::{
160 CloseHandle, GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE,
161 };
162 use windows_sys::Win32::Storage::FileSystem::{
163 CreateFileW, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
164 };
165 use windows_sys::Win32::System::Threading::{
166 CREATE_NEW_PROCESS_GROUP, CreateProcessW, DETACHED_PROCESS, PROCESS_INFORMATION,
167 STARTF_USESTDHANDLES, STARTUPINFOW,
168 };
169
170 let nul_path: Vec<u16> = "\\\\.\\NUL\0".encode_utf16().collect();
178 let nul_handle = unsafe {
179 CreateFileW(
180 nul_path.as_ptr(),
181 GENERIC_READ | GENERIC_WRITE,
182 FILE_SHARE_READ | FILE_SHARE_WRITE,
183 std::ptr::null(),
184 OPEN_EXISTING,
185 0,
186 std::ptr::null_mut(),
187 )
188 };
189 if nul_handle == INVALID_HANDLE_VALUE {
190 return Err(io::Error::last_os_error());
191 }
192
193 let si = STARTUPINFOW {
198 cb: std::mem::size_of::<STARTUPINFOW>() as u32,
199 lpReserved: std::ptr::null_mut(),
200 lpDesktop: std::ptr::null_mut(),
201 lpTitle: std::ptr::null_mut(),
202 dwX: 0,
203 dwY: 0,
204 dwXSize: 0,
205 dwYSize: 0,
206 dwXCountChars: 0,
207 dwYCountChars: 0,
208 dwFillAttribute: 0,
209 dwFlags: STARTF_USESTDHANDLES,
210 wShowWindow: 0,
211 cbReserved2: 0,
212 lpReserved2: std::ptr::null_mut(),
213 hStdInput: nul_handle,
214 hStdOutput: nul_handle,
215 hStdError: nul_handle,
216 };
217
218 let mut program: Vec<u16> = bin.as_os_str().encode_wide().collect();
219 program.push(0);
220 let mut command_line: Vec<u16> = Vec::with_capacity(program.len() + 16);
225 command_line.push(b'"' as u16);
226 command_line.extend_from_slice(&program[..program.len() - 1]);
227 command_line.push(b'"' as u16);
228 command_line.extend(" --daemon".encode_utf16());
229 command_line.push(0);
230
231 let mut pi = PROCESS_INFORMATION {
232 hProcess: std::ptr::null_mut(),
233 hThread: std::ptr::null_mut(),
234 dwProcessId: 0,
235 dwThreadId: 0,
236 };
237
238 let ok = unsafe {
239 CreateProcessW(
240 program.as_ptr(),
241 command_line.as_mut_ptr(),
242 std::ptr::null(),
243 std::ptr::null(),
244 0, DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP,
246 std::ptr::null(),
247 std::ptr::null(),
248 &si as *const STARTUPINFOW,
249 &mut pi,
250 )
251 };
252 unsafe {
253 let _ = CloseHandle(nul_handle);
254 }
255 if ok == 0 {
256 return Err(io::Error::last_os_error());
257 }
258
259 Ok(DaemonChild::Windows(WindowsDaemonProcess {
260 process: pi.hProcess,
261 thread: pi.hThread,
262 }))
263}
264
265pub fn connect_or_spawn_server(bin: Option<&std::path::Path>) -> io::Result<String> {
272 let gateway = resolve_gateway();
273 let socket_name = gateway.to_string();
274
275 if probe_ipc_endpoint(&gateway) {
276 return Ok(socket_name);
277 }
278
279 let bin = bin
280 .map(|b| b.to_path_buf())
281 .unwrap_or_else(|| std::env::current_exe().expect("current exe path"));
282 let mut child = spawn_detached_server(&bin)?;
283 let start = Instant::now();
284 let timeout = Duration::from_secs(3);
285 let poll_interval = Duration::from_millis(50);
286
287 while start.elapsed() < timeout {
288 if probe_ipc_endpoint(&gateway) {
289 return Ok(socket_name);
290 }
291 if let Ok(Some(status)) = child.try_wait() {
292 if probe_ipc_endpoint(&gateway) {
295 return Ok(socket_name);
296 }
297 return Err(io::Error::new(
298 io::ErrorKind::ConnectionRefused,
299 format!("Gateway exited during startup with status: {status}"),
300 ));
301 }
302 thread::sleep(poll_interval);
303 }
304
305 Err(io::Error::new(
306 io::ErrorKind::TimedOut,
307 format!("Timed out waiting for gateway on channel '{gateway}'"),
308 ))
309}