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