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