1#[cfg(windows)]
2use std::ffi::{OsStr, OsString};
3#[cfg(all(test, unix))]
4use std::fs;
5use std::io;
6#[cfg(windows)]
7use std::io::Read;
8#[cfg(windows)]
9use std::io::Write;
10use std::path::{Path, PathBuf};
11use std::sync::{Arc, Mutex as StdMutex};
12#[cfg(windows)]
13use std::time::Duration;
14
15use tokio::sync::oneshot;
16use tokio::task::JoinHandle;
17
18use rmux_core::events::SubscriptionLimits;
19#[cfg(windows)]
20use rmux_ipc::connect_blocking;
21use rmux_ipc::LocalEndpoint;
22#[cfg(windows)]
23use rmux_ipc::LocalListener;
24#[cfg(windows)]
25use rmux_proto::{
26 encode_frame, FrameDecoder, HasSessionRequest, Request, Response, RmuxError, SessionName,
27};
28
29use crate::listener;
30use crate::listener_options::ServeOptions;
31#[cfg(windows)]
32use crate::server_access::current_owner_uid;
33#[cfg(unix)]
34use crate::unix_socket::bind_unix_listener_at;
35#[cfg(unix)]
36use crate::unix_socket::real_user_id;
37#[cfg(all(test, unix))]
38use crate::unix_socket::{
39 ensure_parent_directory, indicates_stale_socket, remove_stale_socket_if_needed,
40};
41#[cfg(unix)]
42use crate::unix_socket_access::UnixSocketAccessController;
43
44#[cfg(all(test, unix))]
45const FALLBACK_SOCKET_ROOT: &str = "/tmp";
46const DEFAULT_WEB_PORT: u16 = 9777;
47
48pub fn default_socket_path() -> io::Result<PathBuf> {
53 rmux_ipc::default_endpoint().map(LocalEndpoint::into_path)
54}
55
56#[cfg(all(test, unix))]
57fn socket_root_from_env(tmpdir: Option<&std::ffi::OsStr>) -> io::Result<PathBuf> {
58 let tmpdir = tmpdir
59 .filter(|value| !value.is_empty())
60 .map(PathBuf::from)
61 .into_iter();
62 let candidates = tmpdir.chain(std::iter::once(PathBuf::from(FALLBACK_SOCKET_ROOT)));
63
64 for candidate in candidates {
65 if let Ok(resolved) = fs::canonicalize(&candidate) {
66 return Ok(resolved);
67 }
68 }
69
70 Err(io::Error::new(
71 io::ErrorKind::NotFound,
72 "no suitable rmux socket directory",
73 ))
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct DaemonConfig {
79 socket_path: PathBuf,
80 config_load: ConfigLoadOptions,
81 subscription_limits: SubscriptionLimits,
82 web_frontend: Option<String>,
83 web_port: u16,
84 web_port_explicit: bool,
85 web_required: bool,
86 startup_ready_fd: Option<i32>,
87 #[cfg(windows)]
88 startup_ready_event: Option<OsString>,
89}
90
91impl DaemonConfig {
92 #[must_use]
94 pub fn new(socket_path: PathBuf) -> Self {
95 Self {
96 socket_path,
97 config_load: ConfigLoadOptions::disabled(),
98 subscription_limits: SubscriptionLimits::default(),
99 web_frontend: None,
100 web_port: DEFAULT_WEB_PORT,
101 web_port_explicit: false,
102 web_required: false,
103 startup_ready_fd: None,
104 #[cfg(windows)]
105 startup_ready_event: None,
106 }
107 }
108
109 pub fn with_default_socket_path() -> io::Result<Self> {
111 Ok(Self::new(default_socket_path()?))
112 }
113
114 #[must_use]
116 pub fn socket_path(&self) -> &Path {
117 &self.socket_path
118 }
119
120 #[must_use]
122 pub const fn config_load(&self) -> &ConfigLoadOptions {
123 &self.config_load
124 }
125
126 #[must_use]
128 pub fn subscription_limits(&self) -> SubscriptionLimits {
129 self.subscription_limits
130 }
131
132 #[must_use]
134 pub const fn web_port(&self) -> u16 {
135 self.web_port
136 }
137
138 #[must_use]
140 pub const fn web_port_explicit(&self) -> bool {
141 self.web_port_explicit
142 }
143
144 #[must_use]
146 pub const fn web_required(&self) -> bool {
147 self.web_required
148 }
149
150 #[must_use]
152 pub fn web_frontend(&self) -> Option<&str> {
153 self.web_frontend.as_deref()
154 }
155
156 #[must_use]
158 pub fn with_default_config_load(mut self, quiet: bool, cwd: Option<PathBuf>) -> Self {
159 self.config_load = ConfigLoadOptions {
160 selection: ConfigFileSelection::Default,
161 quiet,
162 cwd,
163 };
164 self
165 }
166
167 #[must_use]
169 pub fn with_subscription_limits(mut self, subscription_limits: SubscriptionLimits) -> Self {
170 self.subscription_limits = subscription_limits;
171 self
172 }
173
174 #[must_use]
176 pub const fn with_web_port(mut self, port: u16) -> Self {
177 self.web_port = port;
178 self.web_port_explicit = true;
179 self.web_required = true;
180 self
181 }
182
183 #[must_use]
185 pub fn with_web_frontend(mut self, frontend: String) -> Self {
186 self.web_frontend = Some(frontend);
187 self.web_required = true;
188 self
189 }
190
191 #[must_use]
193 pub fn with_config_files(
194 mut self,
195 files: Vec<PathBuf>,
196 quiet: bool,
197 cwd: Option<PathBuf>,
198 ) -> Self {
199 self.config_load = ConfigLoadOptions {
200 selection: ConfigFileSelection::Files(files),
201 quiet,
202 cwd,
203 };
204 self
205 }
206
207 #[cfg(target_os = "linux")]
209 #[must_use]
210 pub const fn with_startup_ready_fd(mut self, ready_fd: i32) -> Self {
211 self.startup_ready_fd = Some(ready_fd);
212 self
213 }
214
215 #[cfg(unix)]
216 const fn startup_ready_fd(&self) -> Option<i32> {
217 self.startup_ready_fd
218 }
219
220 #[cfg(windows)]
222 #[must_use]
223 pub fn with_startup_ready_event(mut self, ready_event: OsString) -> Self {
224 self.startup_ready_event = Some(ready_event);
225 self
226 }
227
228 #[cfg(windows)]
229 fn startup_ready_event(&self) -> Option<&OsStr> {
230 self.startup_ready_event.as_deref()
231 }
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
236pub struct ConfigLoadOptions {
237 selection: ConfigFileSelection,
238 quiet: bool,
239 cwd: Option<PathBuf>,
240}
241
242impl ConfigLoadOptions {
243 #[must_use]
245 pub const fn disabled() -> Self {
246 Self {
247 selection: ConfigFileSelection::Disabled,
248 quiet: true,
249 cwd: None,
250 }
251 }
252
253 #[must_use]
255 pub const fn selection(&self) -> &ConfigFileSelection {
256 &self.selection
257 }
258
259 #[must_use]
261 pub const fn quiet(&self) -> bool {
262 self.quiet
263 }
264
265 #[must_use]
267 pub fn cwd(&self) -> Option<&Path> {
268 self.cwd.as_deref()
269 }
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
274pub enum ConfigFileSelection {
275 Disabled,
277 Default,
279 Files(Vec<PathBuf>),
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct ServerDaemon {
286 config: DaemonConfig,
287}
288
289#[derive(Debug, Clone)]
290pub(crate) struct ShutdownHandle {
291 sender: Arc<StdMutex<Option<oneshot::Sender<()>>>>,
292}
293
294impl ShutdownHandle {
295 pub(crate) fn new() -> (Self, oneshot::Receiver<()>) {
296 let (sender, receiver) = oneshot::channel();
297 (
298 Self {
299 sender: Arc::new(StdMutex::new(Some(sender))),
300 },
301 receiver,
302 )
303 }
304
305 pub(crate) fn request_shutdown(&self) {
306 if let Some(sender) = self.sender.lock().expect("shutdown sender").take() {
307 let _ = sender.send(());
308 }
309 }
310}
311
312impl ServerDaemon {
313 #[must_use]
315 pub fn new(config: DaemonConfig) -> Self {
316 Self { config }
317 }
318
319 pub async fn bind(self) -> io::Result<ServerHandle> {
321 #[cfg(unix)]
322 {
323 let bound_listener = bind_unix_listener_at(self.config.socket_path())?;
324 let socket_access = UnixSocketAccessController::new(
325 self.config.socket_path(),
326 bound_listener.identity,
327 )?;
328 let (shutdown_handle, shutdown_receiver) = ShutdownHandle::new();
329 let signal_watcher = crate::signals::SignalWatcher::install()?;
330 let socket_path = self.config.socket_path().to_path_buf();
331 let owner_uid = real_user_id()?;
332 let serve_options = ServeOptions::new(
333 self.config.config_load().clone(),
334 self.config.subscription_limits(),
335 owner_uid,
336 )
337 .with_web_options(
338 self.config.web_port(),
339 self.config.web_frontend().map(str::to_owned),
340 self.config.web_required(),
341 self.config.web_port_explicit(),
342 )
343 .with_socket_identity(bound_listener.identity)
344 .with_socket_access(socket_access)
345 .with_server_signals(signal_watcher);
346
347 let task = tokio::spawn(listener::serve(
348 bound_listener.listener,
349 socket_path.clone(),
350 shutdown_handle.clone(),
351 shutdown_receiver,
352 serve_options,
353 ));
354 if let Some(ready_fd) = self.config.startup_ready_fd() {
355 signal_startup_ready_fd(ready_fd);
356 }
357
358 Ok(ServerHandle {
359 socket_path,
360 shutdown_handle,
361 task: Some(task),
362 })
363 }
364
365 #[cfg(windows)]
366 {
367 let endpoint = LocalEndpoint::from_path(self.config.socket_path().to_path_buf());
368 let listener = bind_windows_listener(&endpoint)?;
369 let (shutdown_handle, shutdown_receiver) = ShutdownHandle::new();
370 let socket_path = self.config.socket_path().to_path_buf();
371 let owner_uid = current_owner_uid();
372 let serve_options = ServeOptions::new(
373 self.config.config_load().clone(),
374 self.config.subscription_limits(),
375 owner_uid,
376 )
377 .with_web_options(
378 self.config.web_port(),
379 self.config.web_frontend().map(str::to_owned),
380 self.config.web_required(),
381 self.config.web_port_explicit(),
382 );
383
384 let task = tokio::spawn(listener::serve(
385 listener,
386 socket_path.clone(),
387 shutdown_handle.clone(),
388 shutdown_receiver,
389 serve_options,
390 ));
391 if let Some(ready_event) = self.config.startup_ready_event() {
392 signal_startup_ready_event(ready_event);
393 }
394
395 Ok(ServerHandle {
396 socket_path,
397 shutdown_handle,
398 task: Some(task),
399 })
400 }
401 }
402}
403
404#[cfg(unix)]
405fn signal_startup_ready_fd(_ready_fd: i32) {
406 #[cfg(target_os = "linux")]
407 {
408 let _ = rmux_os::daemon::signal_startup_ready_fd(_ready_fd);
409 }
410}
411
412#[cfg(windows)]
413fn signal_startup_ready_event(ready_event: &OsStr) {
414 let _ = rmux_os::daemon::signal_startup_ready_event(ready_event);
415}
416
417#[cfg(windows)]
418fn bind_windows_listener(endpoint: &LocalEndpoint) -> io::Result<LocalListener> {
419 match LocalListener::bind(endpoint) {
420 Ok(listener) => Ok(listener),
421 Err(bind_error) => Err(windows_bind_error(endpoint, bind_error)),
422 }
423}
424
425#[cfg(windows)]
426fn windows_bind_error(endpoint: &LocalEndpoint, bind_error: io::Error) -> io::Error {
427 if windows_pipe_responds(endpoint) {
428 return io::Error::new(
429 io::ErrorKind::AddrInUse,
430 format!(
431 "Windows named pipe '{}' is already held by a responsive rmux-compatible server",
432 endpoint.as_path().display()
433 ),
434 );
435 }
436
437 io::Error::new(
438 bind_error.kind(),
439 format!(
440 "failed to bind Windows named pipe '{}': {bind_error}. Another process may still be holding this endpoint",
441 endpoint.as_path().display()
442 ),
443 )
444}
445
446#[cfg(windows)]
447fn windows_pipe_responds(endpoint: &LocalEndpoint) -> bool {
448 let endpoint = endpoint.clone();
449 std::thread::spawn(move || windows_protocol_probe(&endpoint).unwrap_or(false))
450 .join()
451 .unwrap_or(false)
452}
453
454#[cfg(windows)]
455fn windows_protocol_probe(endpoint: &LocalEndpoint) -> io::Result<bool> {
456 let mut stream = connect_blocking(endpoint, Duration::from_millis(100))?;
457 stream.set_write_timeout(Some(Duration::from_millis(100)))?;
458 stream.set_read_timeout(Some(Duration::from_millis(100)))?;
459
460 let request = Request::HasSession(HasSessionRequest {
461 target: SessionName::new("__rmux_probe__").map_err(io::Error::other)?,
462 });
463 let frame = encode_frame(&request).map_err(io::Error::other)?;
464 stream.write_all(&frame)?;
465 stream.flush()?;
466
467 let mut decoder = FrameDecoder::new();
468 let mut buffer = [0_u8; 512];
469 loop {
470 let bytes_read = match stream.read(&mut buffer) {
471 Ok(0) => return Ok(false),
472 Ok(bytes_read) => bytes_read,
473 Err(error) if error.kind() == io::ErrorKind::TimedOut => return Ok(false),
474 Err(error) => return Err(error),
475 };
476 decoder.push_bytes(&buffer[..bytes_read]);
477 match decoder.next_frame::<Response>() {
478 Ok(Some(Response::HasSession(_))) => return Ok(true),
479 Ok(Some(_response)) => return Ok(false),
480 Ok(None) => continue,
481 Err(RmuxError::IncompleteFrame { .. }) => continue,
482 Err(_error) => return Ok(false),
483 }
484 }
485}
486
487#[derive(Debug)]
489pub struct ServerHandle {
490 socket_path: PathBuf,
491 shutdown_handle: ShutdownHandle,
492 task: Option<JoinHandle<io::Result<()>>>,
493}
494
495impl ServerHandle {
496 #[must_use]
498 pub fn socket_path(&self) -> &Path {
499 &self.socket_path
500 }
501
502 pub async fn wait(mut self) -> io::Result<()> {
504 if let Some(task) = self.task.take() {
505 return task.await.map_err(io::Error::other)?;
506 }
507
508 Ok(())
509 }
510
511 pub async fn shutdown(mut self) -> io::Result<()> {
513 self.request_shutdown();
514
515 if let Some(task) = self.task.take() {
516 return task.await.map_err(io::Error::other)?;
517 }
518
519 Ok(())
520 }
521
522 fn request_shutdown(&mut self) {
523 self.shutdown_handle.request_shutdown();
524 }
525}
526
527impl Drop for ServerHandle {
528 fn drop(&mut self) {
529 self.request_shutdown();
530 }
531}
532
533#[cfg(all(test, unix))]
534#[path = "daemon_tests/unix.rs"]
535mod tests;
536
537#[cfg(all(test, windows))]
538#[path = "daemon_tests/windows.rs"]
539mod tests;