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 The hidden daemon may have exited before creating the socket; run `{}` to surface startup errors.",
577 waited.as_secs(),
578 socket_path.display(),
579 diagnostic_start_server_command(socket_path)
580 ),
581 }
582 }
583}
584
585impl std::error::Error for AutoStartError {
586 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
587 match self {
588 Self::Client(error) => Some(error),
589 Self::BinaryPath(error) => Some(error),
590 Self::Launch { error, .. } => Some(error),
591 Self::IncompatibleDaemon { .. } => None,
592 Self::TimedOut { .. } => None,
593 }
594 }
595}
596
597impl From<ClientError> for AutoStartError {
598 fn from(error: ClientError) -> Self {
599 Self::Client(error)
600 }
601}
602
603#[cfg(not(any(unix, windows)))]
604fn ensure_server_running_with<ConnectFn, LaunchFn>(
605 socket_path: &Path,
606 timeout: Duration,
607 poll_interval: Duration,
608 connect: ConnectFn,
609 launch: LaunchFn,
610) -> Result<Connection, AutoStartError>
611where
612 ConnectFn: FnMut() -> Result<ConnectResult, ClientError>,
613 LaunchFn: FnMut() -> Result<(), AutoStartError>,
614{
615 ensure_server_running_with_probe(
616 socket_path,
617 timeout,
618 poll_interval,
619 connect,
620 launch,
621 probe_server_readiness,
622 )
623}
624
625#[cfg(any(all(test, unix), not(any(unix, windows))))]
626fn ensure_server_running_with_probe<ConnectFn, LaunchFn, ProbeFn>(
627 socket_path: &Path,
628 timeout: Duration,
629 poll_interval: Duration,
630 mut connect: ConnectFn,
631 mut launch: LaunchFn,
632 mut probe: ProbeFn,
633) -> Result<Connection, AutoStartError>
634where
635 ConnectFn: FnMut() -> Result<ConnectResult, ClientError>,
636 LaunchFn: FnMut() -> Result<(), AutoStartError>,
637 ProbeFn: FnMut(&mut Connection) -> Result<(), ClientError>,
638{
639 match connect().map_err(AutoStartError::Client)? {
640 ConnectResult::Connected(mut connection) => {
641 probe(&mut connection).map_err(AutoStartError::Client)?;
642 return Ok(connection);
643 }
644 ConnectResult::Absent => {}
645 }
646
647 launch()?;
648 wait_for_server(
649 socket_path,
650 timeout,
651 poll_interval,
652 &mut connect,
653 &mut probe,
654 )
655}
656
657#[cfg(any(all(test, unix), not(any(unix, windows))))]
658fn wait_for_server<ConnectFn, ProbeFn>(
659 socket_path: &Path,
660 timeout: Duration,
661 poll_interval: Duration,
662 connect: &mut ConnectFn,
663 probe: &mut ProbeFn,
664) -> Result<Connection, AutoStartError>
665where
666 ConnectFn: FnMut() -> Result<ConnectResult, ClientError>,
667 ProbeFn: FnMut(&mut Connection) -> Result<(), ClientError>,
668{
669 let start = Instant::now();
670 let deadline = start + timeout;
671
672 loop {
673 match connect() {
674 Ok(crate::ConnectResult::Connected(mut connection)) => match probe(&mut connection) {
675 Ok(()) => return Ok(connection),
676 Err(error) if is_transient_connect_error(&error) => {}
677 Err(error) => return Err(AutoStartError::Client(error)),
678 },
679 Ok(crate::ConnectResult::Absent) => {}
680 Err(error) if is_transient_connect_error(&error) => {}
681 Err(error) => return Err(AutoStartError::Client(error)),
682 }
683
684 let now = Instant::now();
685 if now >= deadline {
686 return Err(AutoStartError::TimedOut {
687 socket_path: socket_path.to_path_buf(),
688 waited: timeout,
689 });
690 }
691
692 std::thread::sleep(poll_interval.min(deadline.saturating_duration_since(now)));
693 }
694}
695
696fn is_transient_connect_error(error: &ClientError) -> bool {
697 matches!(
698 error,
699 ClientError::Io(io_error)
700 if matches!(
701 io_error.kind(),
702 io::ErrorKind::WouldBlock
703 | io::ErrorKind::Interrupted
704 | io::ErrorKind::TimedOut
705 )
706 )
707}
708
709fn incompatible_daemon_kill_server_command(socket_path: &Path) -> String {
710 if default_socket_path()
711 .ok()
712 .as_deref()
713 .is_some_and(|default_path| default_path == socket_path)
714 {
715 return "rmux kill-server".to_owned();
716 }
717
718 format!("rmux -S {} kill-server", shell_quote_path(socket_path))
719}
720
721fn diagnostic_start_server_command(socket_path: &Path) -> String {
722 if default_socket_path()
723 .ok()
724 .as_deref()
725 .is_some_and(|default_path| default_path == socket_path)
726 {
727 return "rmux start-server".to_owned();
728 }
729
730 format!("rmux -S {} start-server", shell_quote_path(socket_path))
731}
732
733fn probe_server_readiness(connection: &mut Connection) -> Result<(), ClientError> {
734 let response = connection.daemon_status()?;
735 match response {
736 Response::DaemonStatus(status) if status.config_loading => {
737 Err(ClientError::Io(io::Error::new(
738 io::ErrorKind::WouldBlock,
739 "daemon is still loading startup config",
740 )))
741 }
742 Response::DaemonStatus(_) => Ok(()),
743 Response::Error(_) => Ok(()),
744 other => Err(ClientError::Protocol(rmux_proto::RmuxError::Server(
745 format!("unexpected readiness response: {other:?}"),
746 ))),
747 }
748}
749
750#[cfg(not(any(unix, windows)))]
751fn launch_hidden_daemon(
752 socket_path: &Path,
753 config: &AutoStartConfig,
754) -> Result<(), AutoStartError> {
755 let binary_path = rmux_binary_path(config).map_err(AutoStartError::BinaryPath)?;
756 spawn_hidden_daemon_for(&binary_path, socket_path, config).map_err(|error| {
757 AutoStartError::Launch {
758 path: binary_path,
759 error,
760 }
761 })
762}
763
764fn spawn_hidden_daemon_for(
765 binary_path: &Path,
766 socket_path: &Path,
767 config: &AutoStartConfig,
768) -> io::Result<()> {
769 #[cfg(target_os = "linux")]
770 {
771 spawn_hidden_daemon_for_linux(binary_path, socket_path, config)
772 }
773
774 #[cfg(not(target_os = "linux"))]
775 {
776 spawn_hidden_daemon_for_polling(binary_path, socket_path, config)
777 }
778}
779
780#[cfg(not(target_os = "linux"))]
781fn spawn_hidden_daemon_for_polling(
782 binary_path: &Path,
783 socket_path: &Path,
784 config: &AutoStartConfig,
785) -> io::Result<()> {
786 #[cfg(windows)]
787 {
788 spawn_hidden_daemon_for_windows(binary_path, socket_path, config)
789 }
790
791 #[cfg(not(windows))]
792 {
793 let command = hidden_daemon_command(binary_path, socket_path, config, true);
794 match spawn_hidden_daemon(command) {
795 Ok(()) => Ok(()),
796 Err(error) if rmux_os::daemon::should_retry_hidden_daemon_without_breakaway(&error) => {
797 let command = hidden_daemon_command(binary_path, socket_path, config, false);
798 spawn_hidden_daemon(command)
799 }
800 Err(error) => Err(error),
801 }
802 }
803}
804
805#[cfg(windows)]
806fn spawn_hidden_daemon_for_windows(
807 binary_path: &Path,
808 socket_path: &Path,
809 config: &AutoStartConfig,
810) -> io::Result<()> {
811 let ready = rmux_os::daemon::StartupReadyEvent::new()?;
812 let mut command = hidden_daemon_command(binary_path, socket_path, config, true);
813 append_startup_ready_event(&mut command, &ready);
814 match spawn_hidden_daemon(command) {
815 Ok(()) => {
816 let _ = ready.wait(STARTUP_READY_EVENT_TIMEOUT);
817 Ok(())
818 }
819 Err(error) if rmux_os::daemon::should_retry_hidden_daemon_without_breakaway(&error) => {
820 let ready = rmux_os::daemon::StartupReadyEvent::new()?;
821 let mut command = hidden_daemon_command(binary_path, socket_path, config, false);
822 append_startup_ready_event(&mut command, &ready);
823 spawn_hidden_daemon(command)?;
824 let _ = ready.wait(STARTUP_READY_EVENT_TIMEOUT);
825 Ok(())
826 }
827 Err(error) => Err(error),
828 }
829}
830
831#[cfg(windows)]
832fn append_startup_ready_event(command: &mut Command, ready: &rmux_os::daemon::StartupReadyEvent) {
833 command.arg("--startup-ready-event").arg(ready.name());
834}
835
836#[cfg(target_os = "linux")]
837fn spawn_hidden_daemon_for_linux(
838 binary_path: &Path,
839 socket_path: &Path,
840 config: &AutoStartConfig,
841) -> io::Result<()> {
842 let mut ready = StartupReadyEvent::new()?;
843 let mut command =
844 hidden_daemon_command_preserving_fd(binary_path, socket_path, config, true, ready.raw_fd());
845 ready.append_hidden_daemon_args(&mut command);
846 match spawn_hidden_daemon(command) {
847 Ok(()) => {
848 ready.wait_for_signal(STARTUP_READY_EVENT_TIMEOUT);
849 Ok(())
850 }
851 Err(error) if rmux_os::daemon::should_retry_hidden_daemon_without_breakaway(&error) => {
852 let mut ready = StartupReadyEvent::new()?;
853 let mut command = hidden_daemon_command_preserving_fd(
854 binary_path,
855 socket_path,
856 config,
857 false,
858 ready.raw_fd(),
859 );
860 ready.append_hidden_daemon_args(&mut command);
861 spawn_hidden_daemon(command)?;
862 ready.wait_for_signal(STARTUP_READY_EVENT_TIMEOUT);
863 Ok(())
864 }
865 Err(error) => Err(error),
866 }
867}
868
869#[cfg(target_os = "linux")]
870struct StartupReadyEvent {
871 file: File,
872}
873
874#[cfg(target_os = "linux")]
875impl StartupReadyEvent {
876 fn new() -> io::Result<Self> {
877 let fd = rustix::event::eventfd(
878 0,
879 rustix::event::EventfdFlags::NONBLOCK | rustix::event::EventfdFlags::CLOEXEC,
880 )
881 .map_err(io::Error::from)?;
882 Ok(Self { file: fd.into() })
883 }
884
885 fn append_hidden_daemon_args(&self, command: &mut Command) {
886 command
887 .arg("--startup-ready-fd")
888 .arg(self.file.as_raw_fd().to_string());
889 }
890
891 fn raw_fd(&self) -> i32 {
892 self.file.as_raw_fd()
893 }
894
895 fn wait_for_signal(&mut self, timeout: Duration) {
896 let deadline = Instant::now() + timeout;
897 let mut bytes = [0_u8; 8];
898 loop {
899 match self.file.read_exact(&mut bytes) {
900 Ok(()) => return,
901 Err(error)
902 if matches!(
903 error.kind(),
904 io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
905 ) && Instant::now() < deadline =>
906 {
907 std::thread::sleep(Duration::from_millis(1));
908 }
909 Err(_) => return,
910 }
911 }
912 }
913}
914
915#[cfg(target_os = "linux")]
916fn hidden_daemon_command_preserving_fd(
917 binary_path: &Path,
918 socket_path: &Path,
919 config: &AutoStartConfig,
920 allow_job_breakaway: bool,
921 preserved_fd: i32,
922) -> Command {
923 let mut command = hidden_daemon_command_base(binary_path, socket_path, config);
924 rmux_os::daemon::configure_hidden_daemon_command_preserving_fds(
925 &mut command,
926 allow_job_breakaway,
927 &[preserved_fd],
928 );
929 command
930}
931
932#[cfg(not(target_os = "linux"))]
933fn hidden_daemon_command(
934 binary_path: &Path,
935 socket_path: &Path,
936 config: &AutoStartConfig,
937 allow_job_breakaway: bool,
938) -> Command {
939 let mut command = hidden_daemon_command_base(binary_path, socket_path, config);
940 rmux_os::daemon::configure_hidden_daemon_command(&mut command, allow_job_breakaway);
941 command
942}
943
944fn hidden_daemon_command_base(
945 binary_path: &Path,
946 socket_path: &Path,
947 config: &AutoStartConfig,
948) -> Command {
949 let mut command = Command::new(binary_path);
950 command
951 .arg(INTERNAL_DAEMON_FLAG)
952 .arg(socket_path)
953 .stdin(Stdio::null())
954 .stdout(Stdio::null())
955 .stderr(Stdio::null());
956 config.append_hidden_daemon_args(&mut command);
957 command
958}
959
960fn spawn_hidden_daemon(mut command: Command) -> io::Result<()> {
961 let child = rmux_os::daemon::spawn_hidden_daemon_command(&mut command)?;
962 drop(child);
965 Ok(())
966}
967
968fn rmux_binary_path(config: &AutoStartConfig) -> io::Result<PathBuf> {
969 if let Some(path) = &config.binary_override {
970 return Ok(path.clone());
971 }
972
973 let current_exe = env::current_exe()?;
974 let resolved_exe = std::fs::canonicalize(¤t_exe).ok();
975 match env::var_os(BINARY_OVERRIDE_ENV).filter(|_| binary_override_enabled_for_tests()) {
976 Some(path) => Ok(PathBuf::from(path)),
977 None => Ok(hidden_daemon_binary_path_for_executable_paths(
978 ¤t_exe,
979 resolved_exe.as_deref(),
980 config,
981 )
982 .unwrap_or(current_exe)),
983 }
984}
985
986fn binary_override_enabled_for_tests() -> bool {
987 cfg!(debug_assertions)
988 && env::var_os(BINARY_OVERRIDE_TEST_OPT_IN_ENV).is_some_and(|value| value == "1")
989}
990
991#[cfg(all(test, unix))]
992fn hidden_daemon_binary_path(current_exe: &Path) -> Option<PathBuf> {
993 hidden_daemon_binary_path_for_executable_paths(current_exe, None, &AutoStartConfig::disabled())
994}
995
996fn hidden_daemon_binary_path_for_executable_paths(
997 current_exe: &Path,
998 resolved_exe: Option<&Path>,
999 config: &AutoStartConfig,
1000) -> Option<PathBuf> {
1001 hidden_daemon_binary_path_for_config(current_exe, config).or_else(|| {
1002 resolved_exe.and_then(|path| hidden_daemon_binary_path_for_config(path, config))
1003 })
1004}
1005
1006fn hidden_daemon_binary_path_for_config(
1007 current_exe: &Path,
1008 config: &AutoStartConfig,
1009) -> Option<PathBuf> {
1010 if config.web_required {
1011 return None;
1012 }
1013 let file_stem = current_exe.file_stem()?.to_str()?;
1014 if file_stem == "rmux-daemon" {
1015 return None;
1016 }
1017
1018 let mut candidate = current_exe.to_path_buf();
1019 let daemon_file_name = match current_exe
1020 .extension()
1021 .and_then(|extension| extension.to_str())
1022 {
1023 Some(extension) if !extension.is_empty() => format!("rmux-daemon.{extension}"),
1024 _ => "rmux-daemon".to_owned(),
1025 };
1026 candidate.set_file_name(daemon_file_name);
1027 candidate.is_file().then_some(candidate)
1028}
1029
1030#[cfg(all(test, unix))]
1031#[path = "auto_start/tests.rs"]
1032mod tests;
1033
1034#[cfg(all(test, windows))]
1035mod windows_tests {
1036 use std::time::Duration;
1037
1038 use super::{startup_readiness_poll_sleep, STARTUP_POLL_INTERVAL};
1039
1040 #[test]
1041 fn windows_startup_readiness_poll_uses_short_backoff() {
1042 let mut attempt = 0;
1043 let remaining = Duration::from_secs(1);
1044
1045 let sleeps = (0..8)
1046 .map(|_| startup_readiness_poll_sleep(&mut attempt, remaining))
1047 .collect::<Vec<_>>();
1048
1049 assert_eq!(
1050 sleeps,
1051 [
1052 Duration::from_millis(1),
1053 Duration::from_millis(2),
1054 Duration::from_millis(4),
1055 Duration::from_millis(8),
1056 Duration::from_millis(16),
1057 Duration::from_millis(32),
1058 STARTUP_POLL_INTERVAL,
1059 STARTUP_POLL_INTERVAL,
1060 ]
1061 );
1062 }
1063
1064 #[test]
1065 fn windows_startup_readiness_poll_respects_remaining_deadline() {
1066 let mut attempt = 6;
1067
1068 assert_eq!(
1069 startup_readiness_poll_sleep(&mut attempt, Duration::from_millis(7)),
1070 Duration::from_millis(7)
1071 );
1072 }
1073}