1use std::env;
4use std::fmt;
5use std::io;
6use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio};
8use std::time::{Duration, Instant};
9#[cfg(target_os = "linux")]
10use std::{fs::File, io::Read, os::fd::AsRawFd};
11
12#[cfg(windows)]
13use rmux_proto::DaemonStatusResponse;
14use rmux_proto::{Response, RmuxError};
15#[cfg(unix)]
16use rmux_sdk::bootstrap::startup_unix::{
17 connect_or_start_with, StartupError, StartupOutcome, DEFAULT_STARTUP_DEADLINE,
18 STARTUP_POLL_INTERVAL,
19};
20#[cfg(windows)]
21use rmux_sdk::bootstrap::startup_windows::{
22 connect_or_start_blocking_with, StartupError, StartupOutcome, DEFAULT_STARTUP_DEADLINE,
23 STARTUP_POLL_INTERVAL,
24};
25
26use crate::shell_quote::shell_quote_path;
27#[cfg(any(all(test, unix), not(any(unix, windows))))]
28use crate::ConnectResult;
29use crate::{default_socket_path, upgrade, ClientError, Connection};
30
31mod upgrade_restart;
32
33#[cfg(target_os = "linux")]
34const STARTUP_READY_EVENT_TIMEOUT: Duration = Duration::from_millis(20);
35#[cfg(windows)]
36const STARTUP_READY_EVENT_TIMEOUT: Duration = Duration::from_secs(2);
37#[cfg(not(any(unix, windows)))]
38const AUTO_START_TIMEOUT: Duration = Duration::from_secs(5);
39#[cfg(not(any(unix, windows)))]
40const POLL_INTERVAL: Duration = Duration::from_millis(50);
41
42pub const INTERNAL_DAEMON_FLAG: &str = "--__internal-daemon";
47
48const BINARY_OVERRIDE_ENV: &str = "RMUX_INTERNAL_BINARY_PATH";
49const BINARY_OVERRIDE_TEST_OPT_IN_ENV: &str = "RMUX_ALLOW_INTERNAL_BINARY_OVERRIDE";
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct AutoStartConfig {
53 selection: AutoStartConfigSelection,
54 quiet: bool,
55 cwd: Option<PathBuf>,
56 web_frontend: Option<String>,
57 web_port: Option<u16>,
58 web_required: bool,
59 binary_override: Option<PathBuf>,
60}
61
62impl AutoStartConfig {
63 #[must_use]
65 pub const fn disabled() -> Self {
66 Self {
67 selection: AutoStartConfigSelection::Disabled,
68 quiet: true,
69 cwd: None,
70 web_frontend: None,
71 web_port: None,
72 web_required: false,
73 binary_override: None,
74 }
75 }
76
77 #[must_use]
79 pub fn default_files(quiet: bool, cwd: Option<PathBuf>) -> Self {
80 Self {
81 selection: AutoStartConfigSelection::Default,
82 quiet,
83 cwd,
84 web_frontend: None,
85 web_port: None,
86 web_required: false,
87 binary_override: None,
88 }
89 }
90
91 #[must_use]
93 pub fn custom_files(files: Vec<PathBuf>, quiet: bool, cwd: Option<PathBuf>) -> Self {
94 Self {
95 selection: AutoStartConfigSelection::Files(files),
96 quiet,
97 cwd,
98 web_frontend: None,
99 web_port: None,
100 web_required: false,
101 binary_override: None,
102 }
103 }
104
105 #[must_use]
107 pub const fn with_web_port(mut self, port: u16) -> Self {
108 self.web_port = Some(port);
109 self.web_required = true;
110 self
111 }
112
113 #[must_use]
115 pub fn with_web_frontend(mut self, frontend: String) -> Self {
116 self.web_frontend = Some(frontend);
117 self.web_required = true;
118 self
119 }
120
121 #[must_use]
123 pub const fn with_web_required(mut self) -> Self {
124 self.web_required = true;
125 self
126 }
127
128 #[must_use]
130 pub fn with_binary_override(mut self, binary_path: PathBuf) -> Self {
131 self.binary_override = Some(binary_path);
132 self
133 }
134
135 #[cfg(not(windows))]
136 #[cfg(not(any(unix, windows)))]
137 fn loads_startup_config(&self) -> bool {
138 !matches!(self.selection, AutoStartConfigSelection::Disabled)
139 }
140
141 fn append_hidden_daemon_args(&self, command: &mut Command) {
142 match &self.selection {
143 AutoStartConfigSelection::Disabled => {}
144 AutoStartConfigSelection::Default => {
145 command.arg("--config-default");
146 }
147 AutoStartConfigSelection::Files(files) => {
148 for file in files {
149 command.arg("--config-file").arg(file);
150 }
151 }
152 }
153
154 if self.quiet {
155 command.arg("--config-quiet");
156 }
157 if let Some(cwd) = &self.cwd {
158 command.arg("--config-cwd").arg(cwd);
159 }
160 if let Some(port) = self.web_port {
161 command.arg("--web-port").arg(port.to_string());
162 }
163 if let Some(frontend) = &self.web_frontend {
164 command.arg("--frontend-url").arg(frontend);
165 }
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
171pub enum AutoStartConfigSelection {
172 Disabled,
174 Default,
176 Files(Vec<PathBuf>),
178}
179
180pub fn ensure_server_running(socket_path: &Path) -> Result<Connection, AutoStartError> {
187 ensure_server_running_with_config(socket_path, AutoStartConfig::disabled())
188}
189
190#[cfg(unix)]
192pub fn ensure_server_running_with_config(
193 socket_path: &Path,
194 config: AutoStartConfig,
195) -> Result<Connection, AutoStartError> {
196 ensure_server_running_unix(socket_path, config)
197}
198
199#[cfg(windows)]
201pub fn ensure_server_running_with_config(
202 socket_path: &Path,
203 config: AutoStartConfig,
204) -> Result<Connection, AutoStartError> {
205 ensure_server_running_windows(socket_path, config)
206}
207
208#[cfg(not(any(unix, windows)))]
210pub fn ensure_server_running_with_config(
211 socket_path: &Path,
212 config: AutoStartConfig,
213) -> Result<Connection, AutoStartError> {
214 ensure_server_running_polling(socket_path, config)
215}
216
217#[cfg(unix)]
218fn ensure_server_running_unix(
219 socket_path: &Path,
220 config: AutoStartConfig,
221) -> Result<Connection, AutoStartError> {
222 let binary_path = rmux_binary_path(&config).map_err(AutoStartError::BinaryPath)?;
223 let launcher_binary_path = binary_path.clone();
224 let launcher_socket_path = socket_path.to_path_buf();
225 let launcher_config = config.clone();
226
227 let runtime = tokio::runtime::Builder::new_current_thread()
228 .enable_all()
229 .build()
230 .map_err(|error| AutoStartError::Client(ClientError::Io(error)))?;
231 let outcome = runtime.block_on(connect_or_start_with(
232 socket_path,
233 move || async move {
234 spawn_hidden_daemon_for(
235 &launcher_binary_path,
236 &launcher_socket_path,
237 &launcher_config,
238 )
239 },
240 DEFAULT_STARTUP_DEADLINE,
241 STARTUP_POLL_INTERVAL,
242 ));
243
244 let connection = startup_outcome_into_connection(
245 outcome.map_err(|error| auto_start_error_from_startup(error, &binary_path, socket_path))?,
246 )?;
247
248 let connection = probe_connected_server(connection, &config, socket_path)?;
249 upgrade_restart::ensure_daemon_fresh_or_restart(connection, socket_path, &binary_path, &config)
250}
251
252#[cfg(unix)]
253fn startup_outcome_into_connection(outcome: StartupOutcome) -> Result<Connection, AutoStartError> {
254 let stream = outcome
255 .into_stream()
256 .into_std()
257 .map_err(|error| AutoStartError::Client(ClientError::Io(error)))?;
258 stream
259 .set_nonblocking(false)
260 .map_err(|error| AutoStartError::Client(ClientError::Io(error)))?;
261 Connection::new(stream).map_err(AutoStartError::Client)
262}
263
264#[cfg(windows)]
265fn ensure_server_running_windows(
266 socket_path: &Path,
267 config: AutoStartConfig,
268) -> Result<Connection, AutoStartError> {
269 let binary_path = rmux_binary_path(&config).map_err(AutoStartError::BinaryPath)?;
270 let launcher_binary_path = binary_path.clone();
271 let launcher_socket_path = socket_path.to_path_buf();
272 let launcher_config = config.clone();
273
274 let outcome = connect_or_start_blocking_with(
275 socket_path,
276 move || {
277 spawn_hidden_daemon_for(
278 &launcher_binary_path,
279 &launcher_socket_path,
280 &launcher_config,
281 )
282 },
283 DEFAULT_STARTUP_DEADLINE,
284 STARTUP_POLL_INTERVAL,
285 );
286
287 let connection = startup_outcome_into_connection(
288 outcome.map_err(|error| auto_start_error_from_startup(error, &binary_path, socket_path))?,
289 )?;
290 let (connection, readiness_status) =
291 probe_connected_server_windows(connection, &config, socket_path)?;
292 upgrade_restart::ensure_daemon_fresh_or_restart_after_windows_readiness(
293 connection,
294 socket_path,
295 &binary_path,
296 &config,
297 readiness_status,
298 )
299}
300
301#[cfg(windows)]
302fn startup_outcome_into_connection(outcome: StartupOutcome) -> Result<Connection, AutoStartError> {
303 Connection::new(outcome.into_stream()).map_err(AutoStartError::Client)
304}
305
306#[cfg(windows)]
307fn probe_connected_server_windows(
308 mut connection: Connection,
309 _config: &AutoStartConfig,
310 socket_path: &Path,
311) -> Result<(Connection, Option<DaemonStatusResponse>), AutoStartError> {
312 let deadline = Instant::now() + DEFAULT_STARTUP_DEADLINE;
313 let mut poll_attempt = 0_u32;
314 loop {
315 match probe_server_readiness_status(&mut connection) {
316 Ok(status) => return Ok((connection, status)),
317 Err(ClientError::Protocol(RmuxError::UnsupportedWireVersion { got, .. })) => {
318 return Err(AutoStartError::IncompatibleDaemon {
319 socket_path: socket_path.to_path_buf(),
320 message: upgrade::incompatible_daemon_message(&upgrade::IncompatibleDaemon {
321 daemon_version: None,
322 daemon_wire_version: Some(got),
323 }),
324 });
325 }
326 Err(error) if is_transient_connect_error(&error) && Instant::now() < deadline => {
327 let remaining = deadline.saturating_duration_since(Instant::now());
328 std::thread::sleep(startup_readiness_poll_sleep(&mut poll_attempt, remaining));
329 }
330 Err(error) => return Err(AutoStartError::Client(error)),
331 }
332 }
333}
334
335#[cfg(windows)]
336fn probe_server_readiness_status(
337 connection: &mut Connection,
338) -> Result<Option<DaemonStatusResponse>, ClientError> {
339 let response = connection.daemon_status()?;
340 match response {
341 Response::DaemonStatus(status) if status.config_loading => {
342 Err(ClientError::Io(io::Error::new(
343 io::ErrorKind::WouldBlock,
344 "daemon is still loading startup config",
345 )))
346 }
347 Response::DaemonStatus(status) => Ok(Some(status)),
348 Response::Error(_) => Ok(None),
349 other => Err(ClientError::Protocol(rmux_proto::RmuxError::Server(
350 format!("unexpected readiness response: {other:?}"),
351 ))),
352 }
353}
354
355fn probe_connected_server(
356 mut connection: Connection,
357 _config: &AutoStartConfig,
358 socket_path: &Path,
359) -> Result<Connection, AutoStartError> {
360 let deadline = Instant::now() + DEFAULT_STARTUP_DEADLINE;
361 let mut poll_attempt = 0_u32;
362 loop {
363 match probe_server_readiness(&mut connection) {
364 Ok(()) => return Ok(connection),
365 Err(ClientError::Protocol(RmuxError::UnsupportedWireVersion { got, .. })) => {
366 return Err(AutoStartError::IncompatibleDaemon {
367 socket_path: socket_path.to_path_buf(),
368 message: upgrade::incompatible_daemon_message(&upgrade::IncompatibleDaemon {
369 daemon_version: None,
370 daemon_wire_version: Some(got),
371 }),
372 });
373 }
374 Err(error) if is_transient_connect_error(&error) && Instant::now() < deadline => {
375 let remaining = deadline.saturating_duration_since(Instant::now());
376 std::thread::sleep(startup_readiness_poll_sleep(&mut poll_attempt, remaining));
377 }
378 Err(error) => return Err(AutoStartError::Client(error)),
379 }
380 }
381}
382
383fn startup_readiness_poll_sleep(poll_attempt: &mut u32, remaining: Duration) -> Duration {
384 #[cfg(windows)]
385 {
386 const INITIAL_POLL_MILLIS: u64 = 1;
387
388 let shift = (*poll_attempt).min(6);
389 *poll_attempt = (*poll_attempt).saturating_add(1);
390 let millis = INITIAL_POLL_MILLIS
391 .checked_shl(shift)
392 .unwrap_or(u64::MAX)
393 .min(STARTUP_POLL_INTERVAL.as_millis() as u64);
394 Duration::from_millis(millis).min(remaining)
395 }
396
397 #[cfg(not(windows))]
398 {
399 const INITIAL_POLL_MILLIS: u64 = 1;
400
401 let shift = (*poll_attempt).min(6);
402 *poll_attempt = (*poll_attempt).saturating_add(1);
403 let millis = INITIAL_POLL_MILLIS
404 .checked_shl(shift)
405 .unwrap_or(u64::MAX)
406 .min(STARTUP_POLL_INTERVAL.as_millis() as u64);
407 Duration::from_millis(millis).min(remaining)
408 }
409}
410
411#[cfg(unix)]
412fn auto_start_error_from_startup(
413 error: StartupError,
414 binary_path: &Path,
415 socket_path: &Path,
416) -> AutoStartError {
417 match error {
418 StartupError::Launcher { source } => AutoStartError::Launch {
419 path: binary_path.to_path_buf(),
420 error: source,
421 },
422 StartupError::StartupTimeout { waited, .. } => AutoStartError::TimedOut {
423 socket_path: socket_path.to_path_buf(),
424 waited,
425 },
426 error => AutoStartError::Client(ClientError::Io(io::Error::new(
427 startup_error_kind(&error),
428 error.to_string(),
429 ))),
430 }
431}
432
433#[cfg(windows)]
434fn auto_start_error_from_startup(
435 error: StartupError,
436 binary_path: &Path,
437 socket_path: &Path,
438) -> AutoStartError {
439 match error {
440 StartupError::Launcher { source } => AutoStartError::Launch {
441 path: binary_path.to_path_buf(),
442 error: source,
443 },
444 StartupError::StartupTimeout { waited, .. } => AutoStartError::TimedOut {
445 socket_path: socket_path.to_path_buf(),
446 waited,
447 },
448 error => AutoStartError::Client(ClientError::Io(io::Error::new(
449 startup_error_kind(&error),
450 error.to_string(),
451 ))),
452 }
453}
454
455#[cfg(unix)]
456fn startup_error_kind(error: &StartupError) -> io::ErrorKind {
457 match error {
458 StartupError::InvalidPath { .. } | StartupError::SymlinkRejected { .. } => {
459 io::ErrorKind::InvalidInput
460 }
461 StartupError::UnsafeOwner { .. }
462 | StartupError::UnsafePermissions { .. }
463 | StartupError::PeerCredentialMismatch { .. } => io::ErrorKind::PermissionDenied,
464 StartupError::Lock { source, .. } | StartupError::Filesystem { source, .. } => {
465 source.kind()
466 }
467 StartupError::Launcher { source } => source.kind(),
468 StartupError::StartupTimeout { .. } => io::ErrorKind::TimedOut,
469 }
470}
471
472#[cfg(windows)]
473fn startup_error_kind(error: &StartupError) -> io::ErrorKind {
474 match error {
475 StartupError::InvalidPipeName { .. } | StartupError::InvalidMutexName { .. } => {
476 io::ErrorKind::InvalidInput
477 }
478 StartupError::MutexAccessDenied { .. } | StartupError::PipeAccessDenied { .. } => {
479 io::ErrorKind::PermissionDenied
480 }
481 StartupError::MutexTimeout { .. }
482 | StartupError::PipeBusy { .. }
483 | StartupError::StartupTimeout { .. } => io::ErrorKind::TimedOut,
484 StartupError::PipeNotFound { .. } | StartupError::PipeNoData { .. } => {
485 io::ErrorKind::NotFound
486 }
487 StartupError::Mutex { source, .. } | StartupError::PipeIo { source, .. } => source.kind(),
488 StartupError::Launcher { source } => source.kind(),
489 }
490}
491
492#[cfg(not(any(unix, windows)))]
493fn ensure_server_running_polling(
494 socket_path: &Path,
495 config: AutoStartConfig,
496) -> Result<Connection, AutoStartError> {
497 if config.loads_startup_config() {
498 return ensure_server_running_with_probe(
499 socket_path,
500 AUTO_START_TIMEOUT,
501 POLL_INTERVAL,
502 || crate::connect_or_absent(socket_path),
503 || launch_hidden_daemon(socket_path, &config),
504 |_| Ok(()),
505 );
506 }
507
508 ensure_server_running_with(
509 socket_path,
510 AUTO_START_TIMEOUT,
511 POLL_INTERVAL,
512 || crate::connect_or_absent(socket_path),
513 || launch_hidden_daemon(socket_path, &config),
514 )
515}
516
517#[derive(Debug)]
519pub enum AutoStartError {
520 Client(ClientError),
522 BinaryPath(io::Error),
524 Launch {
526 path: PathBuf,
528 error: io::Error,
530 },
531 IncompatibleDaemon {
533 socket_path: PathBuf,
535 message: String,
537 },
538 TimedOut {
540 socket_path: PathBuf,
542 waited: Duration,
544 },
545}
546
547impl fmt::Display for AutoStartError {
548 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
549 match self {
550 Self::Client(error) => write!(formatter, "{error}"),
551 Self::BinaryPath(error) => {
552 write!(formatter, "failed to resolve rmux binary path: {error}")
553 }
554 Self::Launch { path, error } => {
555 write!(
556 formatter,
557 "failed to launch hidden rmux daemon '{}': {error}",
558 path.display()
559 )
560 }
561 Self::IncompatibleDaemon {
562 socket_path,
563 message,
564 } => write!(
565 formatter,
566 "rmux: {message} on '{}'.\nrmux: run `{}` to stop it, then retry.",
567 socket_path.display(),
568 incompatible_daemon_kill_server_command(socket_path)
569 ),
570 Self::TimedOut {
571 socket_path,
572 waited,
573 } => write!(
574 formatter,
575 "timed out after {}s waiting for rmux server socket '{}'",
576 waited.as_secs(),
577 socket_path.display()
578 ),
579 }
580 }
581}
582
583impl std::error::Error for AutoStartError {
584 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
585 match self {
586 Self::Client(error) => Some(error),
587 Self::BinaryPath(error) => Some(error),
588 Self::Launch { error, .. } => Some(error),
589 Self::IncompatibleDaemon { .. } => None,
590 Self::TimedOut { .. } => None,
591 }
592 }
593}
594
595impl From<ClientError> for AutoStartError {
596 fn from(error: ClientError) -> Self {
597 Self::Client(error)
598 }
599}
600
601#[cfg(not(any(unix, windows)))]
602fn ensure_server_running_with<ConnectFn, LaunchFn>(
603 socket_path: &Path,
604 timeout: Duration,
605 poll_interval: Duration,
606 connect: ConnectFn,
607 launch: LaunchFn,
608) -> Result<Connection, AutoStartError>
609where
610 ConnectFn: FnMut() -> Result<ConnectResult, ClientError>,
611 LaunchFn: FnMut() -> Result<(), AutoStartError>,
612{
613 ensure_server_running_with_probe(
614 socket_path,
615 timeout,
616 poll_interval,
617 connect,
618 launch,
619 probe_server_readiness,
620 )
621}
622
623#[cfg(any(all(test, unix), not(any(unix, windows))))]
624fn ensure_server_running_with_probe<ConnectFn, LaunchFn, ProbeFn>(
625 socket_path: &Path,
626 timeout: Duration,
627 poll_interval: Duration,
628 mut connect: ConnectFn,
629 mut launch: LaunchFn,
630 mut probe: ProbeFn,
631) -> Result<Connection, AutoStartError>
632where
633 ConnectFn: FnMut() -> Result<ConnectResult, ClientError>,
634 LaunchFn: FnMut() -> Result<(), AutoStartError>,
635 ProbeFn: FnMut(&mut Connection) -> Result<(), ClientError>,
636{
637 match connect().map_err(AutoStartError::Client)? {
638 ConnectResult::Connected(mut connection) => {
639 probe(&mut connection).map_err(AutoStartError::Client)?;
640 return Ok(connection);
641 }
642 ConnectResult::Absent => {}
643 }
644
645 launch()?;
646 wait_for_server(
647 socket_path,
648 timeout,
649 poll_interval,
650 &mut connect,
651 &mut probe,
652 )
653}
654
655#[cfg(any(all(test, unix), not(any(unix, windows))))]
656fn wait_for_server<ConnectFn, ProbeFn>(
657 socket_path: &Path,
658 timeout: Duration,
659 poll_interval: Duration,
660 connect: &mut ConnectFn,
661 probe: &mut ProbeFn,
662) -> Result<Connection, AutoStartError>
663where
664 ConnectFn: FnMut() -> Result<ConnectResult, ClientError>,
665 ProbeFn: FnMut(&mut Connection) -> Result<(), ClientError>,
666{
667 let start = Instant::now();
668 let deadline = start + timeout;
669
670 loop {
671 match connect() {
672 Ok(crate::ConnectResult::Connected(mut connection)) => match probe(&mut connection) {
673 Ok(()) => return Ok(connection),
674 Err(error) if is_transient_connect_error(&error) => {}
675 Err(error) => return Err(AutoStartError::Client(error)),
676 },
677 Ok(crate::ConnectResult::Absent) => {}
678 Err(error) if is_transient_connect_error(&error) => {}
679 Err(error) => return Err(AutoStartError::Client(error)),
680 }
681
682 let now = Instant::now();
683 if now >= deadline {
684 return Err(AutoStartError::TimedOut {
685 socket_path: socket_path.to_path_buf(),
686 waited: timeout,
687 });
688 }
689
690 std::thread::sleep(poll_interval.min(deadline.saturating_duration_since(now)));
691 }
692}
693
694fn is_transient_connect_error(error: &ClientError) -> bool {
695 matches!(
696 error,
697 ClientError::Io(io_error)
698 if matches!(
699 io_error.kind(),
700 io::ErrorKind::WouldBlock
701 | io::ErrorKind::Interrupted
702 | io::ErrorKind::TimedOut
703 )
704 )
705}
706
707fn incompatible_daemon_kill_server_command(socket_path: &Path) -> String {
708 if default_socket_path()
709 .ok()
710 .as_deref()
711 .is_some_and(|default_path| default_path == socket_path)
712 {
713 return "rmux kill-server".to_owned();
714 }
715
716 format!("rmux -S {} kill-server", shell_quote_path(socket_path))
717}
718
719fn probe_server_readiness(connection: &mut Connection) -> Result<(), ClientError> {
720 let response = connection.daemon_status()?;
721 match response {
722 Response::DaemonStatus(status) if status.config_loading => {
723 Err(ClientError::Io(io::Error::new(
724 io::ErrorKind::WouldBlock,
725 "daemon is still loading startup config",
726 )))
727 }
728 Response::DaemonStatus(_) => Ok(()),
729 Response::Error(_) => Ok(()),
730 other => Err(ClientError::Protocol(rmux_proto::RmuxError::Server(
731 format!("unexpected readiness response: {other:?}"),
732 ))),
733 }
734}
735
736#[cfg(not(any(unix, windows)))]
737fn launch_hidden_daemon(
738 socket_path: &Path,
739 config: &AutoStartConfig,
740) -> Result<(), AutoStartError> {
741 let binary_path = rmux_binary_path(config).map_err(AutoStartError::BinaryPath)?;
742 spawn_hidden_daemon_for(&binary_path, socket_path, config).map_err(|error| {
743 AutoStartError::Launch {
744 path: binary_path,
745 error,
746 }
747 })
748}
749
750fn spawn_hidden_daemon_for(
751 binary_path: &Path,
752 socket_path: &Path,
753 config: &AutoStartConfig,
754) -> io::Result<()> {
755 #[cfg(target_os = "linux")]
756 {
757 spawn_hidden_daemon_for_linux(binary_path, socket_path, config)
758 }
759
760 #[cfg(not(target_os = "linux"))]
761 {
762 spawn_hidden_daemon_for_polling(binary_path, socket_path, config)
763 }
764}
765
766#[cfg(not(target_os = "linux"))]
767fn spawn_hidden_daemon_for_polling(
768 binary_path: &Path,
769 socket_path: &Path,
770 config: &AutoStartConfig,
771) -> io::Result<()> {
772 #[cfg(windows)]
773 {
774 spawn_hidden_daemon_for_windows(binary_path, socket_path, config)
775 }
776
777 #[cfg(not(windows))]
778 {
779 let command = hidden_daemon_command(binary_path, socket_path, config, true);
780 match spawn_hidden_daemon(command) {
781 Ok(()) => Ok(()),
782 Err(error) if rmux_os::daemon::should_retry_hidden_daemon_without_breakaway(&error) => {
783 let command = hidden_daemon_command(binary_path, socket_path, config, false);
784 spawn_hidden_daemon(command)
785 }
786 Err(error) => Err(error),
787 }
788 }
789}
790
791#[cfg(windows)]
792fn spawn_hidden_daemon_for_windows(
793 binary_path: &Path,
794 socket_path: &Path,
795 config: &AutoStartConfig,
796) -> io::Result<()> {
797 let ready = rmux_os::daemon::StartupReadyEvent::new()?;
798 let mut command = hidden_daemon_command(binary_path, socket_path, config, true);
799 append_startup_ready_event(&mut command, &ready);
800 match spawn_hidden_daemon(command) {
801 Ok(()) => {
802 let _ = ready.wait(STARTUP_READY_EVENT_TIMEOUT);
803 Ok(())
804 }
805 Err(error) if rmux_os::daemon::should_retry_hidden_daemon_without_breakaway(&error) => {
806 let ready = rmux_os::daemon::StartupReadyEvent::new()?;
807 let mut command = hidden_daemon_command(binary_path, socket_path, config, false);
808 append_startup_ready_event(&mut command, &ready);
809 spawn_hidden_daemon(command)?;
810 let _ = ready.wait(STARTUP_READY_EVENT_TIMEOUT);
811 Ok(())
812 }
813 Err(error) => Err(error),
814 }
815}
816
817#[cfg(windows)]
818fn append_startup_ready_event(command: &mut Command, ready: &rmux_os::daemon::StartupReadyEvent) {
819 command.arg("--startup-ready-event").arg(ready.name());
820}
821
822#[cfg(target_os = "linux")]
823fn spawn_hidden_daemon_for_linux(
824 binary_path: &Path,
825 socket_path: &Path,
826 config: &AutoStartConfig,
827) -> io::Result<()> {
828 let mut ready = StartupReadyEvent::new()?;
829 let mut command =
830 hidden_daemon_command_preserving_fd(binary_path, socket_path, config, true, ready.raw_fd());
831 ready.append_hidden_daemon_args(&mut command);
832 match spawn_hidden_daemon(command) {
833 Ok(()) => {
834 ready.wait_for_signal(STARTUP_READY_EVENT_TIMEOUT);
835 Ok(())
836 }
837 Err(error) if rmux_os::daemon::should_retry_hidden_daemon_without_breakaway(&error) => {
838 let mut ready = StartupReadyEvent::new()?;
839 let mut command = hidden_daemon_command_preserving_fd(
840 binary_path,
841 socket_path,
842 config,
843 false,
844 ready.raw_fd(),
845 );
846 ready.append_hidden_daemon_args(&mut command);
847 spawn_hidden_daemon(command)?;
848 ready.wait_for_signal(STARTUP_READY_EVENT_TIMEOUT);
849 Ok(())
850 }
851 Err(error) => Err(error),
852 }
853}
854
855#[cfg(target_os = "linux")]
856struct StartupReadyEvent {
857 file: File,
858}
859
860#[cfg(target_os = "linux")]
861impl StartupReadyEvent {
862 fn new() -> io::Result<Self> {
863 let fd = rustix::event::eventfd(
864 0,
865 rustix::event::EventfdFlags::NONBLOCK | rustix::event::EventfdFlags::CLOEXEC,
866 )
867 .map_err(io::Error::from)?;
868 Ok(Self { file: fd.into() })
869 }
870
871 fn append_hidden_daemon_args(&self, command: &mut Command) {
872 command
873 .arg("--startup-ready-fd")
874 .arg(self.file.as_raw_fd().to_string());
875 }
876
877 fn raw_fd(&self) -> i32 {
878 self.file.as_raw_fd()
879 }
880
881 fn wait_for_signal(&mut self, timeout: Duration) {
882 let deadline = Instant::now() + timeout;
883 let mut bytes = [0_u8; 8];
884 loop {
885 match self.file.read_exact(&mut bytes) {
886 Ok(()) => return,
887 Err(error)
888 if matches!(
889 error.kind(),
890 io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
891 ) && Instant::now() < deadline =>
892 {
893 std::thread::sleep(Duration::from_millis(1));
894 }
895 Err(_) => return,
896 }
897 }
898 }
899}
900
901#[cfg(target_os = "linux")]
902fn hidden_daemon_command_preserving_fd(
903 binary_path: &Path,
904 socket_path: &Path,
905 config: &AutoStartConfig,
906 allow_job_breakaway: bool,
907 preserved_fd: i32,
908) -> Command {
909 let mut command = hidden_daemon_command_base(binary_path, socket_path, config);
910 rmux_os::daemon::configure_hidden_daemon_command_preserving_fds(
911 &mut command,
912 allow_job_breakaway,
913 &[preserved_fd],
914 );
915 command
916}
917
918#[cfg(not(target_os = "linux"))]
919fn hidden_daemon_command(
920 binary_path: &Path,
921 socket_path: &Path,
922 config: &AutoStartConfig,
923 allow_job_breakaway: bool,
924) -> Command {
925 let mut command = hidden_daemon_command_base(binary_path, socket_path, config);
926 rmux_os::daemon::configure_hidden_daemon_command(&mut command, allow_job_breakaway);
927 command
928}
929
930fn hidden_daemon_command_base(
931 binary_path: &Path,
932 socket_path: &Path,
933 config: &AutoStartConfig,
934) -> Command {
935 let mut command = Command::new(binary_path);
936 command
937 .arg(INTERNAL_DAEMON_FLAG)
938 .arg(socket_path)
939 .stdin(Stdio::null())
940 .stdout(Stdio::null())
941 .stderr(Stdio::null());
942 config.append_hidden_daemon_args(&mut command);
943 command
944}
945
946fn spawn_hidden_daemon(mut command: Command) -> io::Result<()> {
947 let child = rmux_os::daemon::spawn_hidden_daemon_command(&mut command)?;
948 drop(child);
951 Ok(())
952}
953
954fn rmux_binary_path(config: &AutoStartConfig) -> io::Result<PathBuf> {
955 if let Some(path) = &config.binary_override {
956 return Ok(path.clone());
957 }
958
959 let current_exe = env::current_exe()?;
960 match env::var_os(BINARY_OVERRIDE_ENV).filter(|_| binary_override_enabled_for_tests()) {
961 Some(path) => Ok(PathBuf::from(path)),
962 None => {
963 Ok(hidden_daemon_binary_path_for_config(¤t_exe, config).unwrap_or(current_exe))
964 }
965 }
966}
967
968fn binary_override_enabled_for_tests() -> bool {
969 cfg!(debug_assertions)
970 && env::var_os(BINARY_OVERRIDE_TEST_OPT_IN_ENV).is_some_and(|value| value == "1")
971}
972
973#[cfg(all(test, unix))]
974fn hidden_daemon_binary_path(current_exe: &Path) -> Option<PathBuf> {
975 hidden_daemon_binary_path_for_config(current_exe, &AutoStartConfig::disabled())
976}
977
978fn hidden_daemon_binary_path_for_config(
979 current_exe: &Path,
980 config: &AutoStartConfig,
981) -> Option<PathBuf> {
982 if config.web_required {
983 return None;
984 }
985 let file_stem = current_exe.file_stem()?.to_str()?;
986 if file_stem == "rmux-daemon" {
987 return None;
988 }
989
990 let mut candidate = current_exe.to_path_buf();
991 let daemon_file_name = match current_exe
992 .extension()
993 .and_then(|extension| extension.to_str())
994 {
995 Some(extension) if !extension.is_empty() => format!("rmux-daemon.{extension}"),
996 _ => "rmux-daemon".to_owned(),
997 };
998 candidate.set_file_name(daemon_file_name);
999 candidate.is_file().then_some(candidate)
1000}
1001
1002#[cfg(all(test, unix))]
1003#[path = "auto_start/tests.rs"]
1004mod tests;
1005
1006#[cfg(all(test, windows))]
1007mod windows_tests {
1008 use std::time::Duration;
1009
1010 use super::{startup_readiness_poll_sleep, STARTUP_POLL_INTERVAL};
1011
1012 #[test]
1013 fn windows_startup_readiness_poll_uses_short_backoff() {
1014 let mut attempt = 0;
1015 let remaining = Duration::from_secs(1);
1016
1017 let sleeps = (0..8)
1018 .map(|_| startup_readiness_poll_sleep(&mut attempt, remaining))
1019 .collect::<Vec<_>>();
1020
1021 assert_eq!(
1022 sleeps,
1023 [
1024 Duration::from_millis(1),
1025 Duration::from_millis(2),
1026 Duration::from_millis(4),
1027 Duration::from_millis(8),
1028 Duration::from_millis(16),
1029 Duration::from_millis(32),
1030 STARTUP_POLL_INTERVAL,
1031 STARTUP_POLL_INTERVAL,
1032 ]
1033 );
1034 }
1035
1036 #[test]
1037 fn windows_startup_readiness_poll_respects_remaining_deadline() {
1038 let mut attempt = 6;
1039
1040 assert_eq!(
1041 startup_readiness_poll_sleep(&mut attempt, Duration::from_millis(7)),
1042 Duration::from_millis(7)
1043 );
1044 }
1045}