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_selected_with, StartupError, StartupOutcome,
23 DEFAULT_STARTUP_DEADLINE, 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
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub enum ServerConnectionProvenance {
183 StartedByCaller,
185 JoinedExisting,
187}
188
189pub struct EnsuredServerConnection {
191 connection: Connection,
192 provenance: ServerConnectionProvenance,
193 socket_path: PathBuf,
194}
195
196impl EnsuredServerConnection {
197 fn new(
198 connection: Connection,
199 provenance: ServerConnectionProvenance,
200 socket_path: PathBuf,
201 ) -> Self {
202 Self {
203 connection,
204 provenance,
205 socket_path,
206 }
207 }
208
209 #[must_use]
211 pub const fn provenance(&self) -> ServerConnectionProvenance {
212 self.provenance
213 }
214
215 #[must_use]
220 pub fn socket_path(&self) -> &Path {
221 &self.socket_path
222 }
223
224 #[must_use]
226 pub fn into_connection(self) -> Connection {
227 self.connection
228 }
229
230 #[must_use]
232 pub fn into_connection_and_socket_path(self) -> (Connection, PathBuf) {
233 (self.connection, self.socket_path)
234 }
235}
236
237pub fn ensure_server_running(socket_path: &Path) -> Result<Connection, AutoStartError> {
244 ensure_server_running_with_config(socket_path, AutoStartConfig::disabled())
245}
246
247pub fn ensure_server_running_with_config(
249 socket_path: &Path,
250 config: AutoStartConfig,
251) -> Result<Connection, AutoStartError> {
252 ensure_server_running_with_config_outcome(socket_path, config)
253 .map(EnsuredServerConnection::into_connection)
254}
255
256#[cfg(windows)]
263pub fn ensure_server_running_with_config_outcome(
264 socket_path: &Path,
265 config: AutoStartConfig,
266) -> Result<EnsuredServerConnection, AutoStartError> {
267 ensure_server_running_windows(socket_path, config)
268}
269
270#[cfg(unix)]
277pub fn ensure_server_running_with_config_outcome(
278 socket_path: &Path,
279 config: AutoStartConfig,
280) -> Result<EnsuredServerConnection, AutoStartError> {
281 ensure_server_running_unix(socket_path, config)
282}
283
284#[cfg(not(any(unix, windows)))]
286pub fn ensure_server_running_with_config_outcome(
287 socket_path: &Path,
288 config: AutoStartConfig,
289) -> Result<EnsuredServerConnection, AutoStartError> {
290 ensure_server_running_polling(socket_path, config)
291}
292
293#[cfg(unix)]
294fn ensure_server_running_unix(
295 socket_path: &Path,
296 config: AutoStartConfig,
297) -> Result<EnsuredServerConnection, AutoStartError> {
298 let binary_path = rmux_binary_path(&config).map_err(AutoStartError::BinaryPath)?;
299 let launcher_binary_path = binary_path.clone();
300 let launcher_socket_path = socket_path.to_path_buf();
301 let launcher_config = config.clone();
302
303 let runtime = tokio::runtime::Builder::new_current_thread()
304 .enable_all()
305 .build()
306 .map_err(|error| AutoStartError::Client(ClientError::Io(error)))?;
307 let outcome = runtime.block_on(connect_or_start_with(
308 socket_path,
309 move || async move {
310 spawn_hidden_daemon_for(
311 &launcher_binary_path,
312 &launcher_socket_path,
313 &launcher_config,
314 )
315 },
316 DEFAULT_STARTUP_DEADLINE,
317 STARTUP_POLL_INTERVAL,
318 ));
319
320 let outcome =
321 outcome.map_err(|error| auto_start_error_from_startup(error, &binary_path, socket_path))?;
322 let provenance = startup_outcome_provenance(&outcome);
323 let connection = startup_outcome_into_connection(outcome)?;
324
325 let connection = probe_connected_server(connection, &config, socket_path)?;
326 let connected = upgrade_restart::ensure_daemon_fresh_or_restart(
327 connection,
328 socket_path,
329 &binary_path,
330 &config,
331 )?;
332 Ok(EnsuredServerConnection::new(
333 connected.connection,
334 provenance,
335 connected.socket_path,
336 ))
337}
338
339#[cfg(unix)]
340fn startup_outcome_into_connection(outcome: StartupOutcome) -> Result<Connection, AutoStartError> {
341 let stream = outcome
342 .into_stream()
343 .into_std()
344 .map_err(|error| AutoStartError::Client(ClientError::Io(error)))?;
345 stream
346 .set_nonblocking(false)
347 .map_err(|error| AutoStartError::Client(ClientError::Io(error)))?;
348 Connection::new(stream).map_err(AutoStartError::Client)
349}
350
351#[cfg(windows)]
352fn ensure_server_running_windows(
353 socket_path: &Path,
354 config: AutoStartConfig,
355) -> Result<EnsuredServerConnection, AutoStartError> {
356 let binary_path = rmux_binary_path(&config).map_err(AutoStartError::BinaryPath)?;
357 let launcher_binary_path = binary_path.clone();
358 let launcher_config = config.clone();
359
360 let (outcome, selected_endpoint) = connect_or_start_blocking_selected_with(
361 socket_path,
362 move |reserved_socket_path| {
363 spawn_hidden_daemon_for(
364 &launcher_binary_path,
365 reserved_socket_path,
366 &launcher_config,
367 )
368 },
369 DEFAULT_STARTUP_DEADLINE,
370 STARTUP_POLL_INTERVAL,
371 )
372 .map_err(|error| auto_start_error_from_startup(error, &binary_path, socket_path))?;
373 let selected_socket_path = selected_endpoint.into_path();
374 let provenance = startup_outcome_provenance(&outcome);
375 let connection = startup_outcome_into_connection(outcome)?;
376 let (connection, readiness_status) =
377 probe_connected_server_windows(connection, &config, &selected_socket_path)?;
378 let connected = upgrade_restart::ensure_daemon_fresh_or_restart_after_windows_readiness(
379 connection,
380 &selected_socket_path,
381 &binary_path,
382 &config,
383 readiness_status,
384 )?;
385 Ok(EnsuredServerConnection::new(
386 connected.connection,
387 provenance,
388 connected.socket_path,
389 ))
390}
391
392#[cfg(any(unix, windows))]
393fn startup_outcome_provenance(outcome: &StartupOutcome) -> ServerConnectionProvenance {
394 if outcome.is_owner() {
395 ServerConnectionProvenance::StartedByCaller
396 } else {
397 ServerConnectionProvenance::JoinedExisting
398 }
399}
400
401#[cfg(windows)]
402fn startup_outcome_into_connection(outcome: StartupOutcome) -> Result<Connection, AutoStartError> {
403 Connection::new(outcome.into_stream()).map_err(AutoStartError::Client)
404}
405
406#[cfg(windows)]
407fn probe_connected_server_windows(
408 mut connection: Connection,
409 _config: &AutoStartConfig,
410 socket_path: &Path,
411) -> Result<(Connection, Option<DaemonStatusResponse>), AutoStartError> {
412 let deadline = Instant::now() + DEFAULT_STARTUP_DEADLINE;
413 let mut poll_attempt = 0_u32;
414 loop {
415 match probe_server_readiness_status(&mut connection) {
416 Ok(status) => return Ok((connection, status)),
417 Err(ClientError::Protocol(RmuxError::UnsupportedWireVersion { got, .. })) => {
418 return Err(AutoStartError::IncompatibleDaemon {
419 socket_path: socket_path.to_path_buf(),
420 message: upgrade::incompatible_daemon_message(&upgrade::IncompatibleDaemon {
421 daemon_version: None,
422 daemon_wire_version: Some(got),
423 }),
424 });
425 }
426 Err(error) if is_transient_connect_error(&error) && Instant::now() < deadline => {
427 let remaining = deadline.saturating_duration_since(Instant::now());
428 std::thread::sleep(startup_readiness_poll_sleep(&mut poll_attempt, remaining));
429 }
430 Err(error) => return Err(AutoStartError::Client(error)),
431 }
432 }
433}
434
435#[cfg(windows)]
436fn probe_server_readiness_status(
437 connection: &mut Connection,
438) -> Result<Option<DaemonStatusResponse>, ClientError> {
439 let response = connection.daemon_status()?;
440 match response {
441 Response::DaemonStatus(status) if status.config_loading => {
442 Err(ClientError::Io(io::Error::new(
443 io::ErrorKind::WouldBlock,
444 "daemon is still loading startup config",
445 )))
446 }
447 Response::DaemonStatus(status) => Ok(Some(status)),
448 Response::Error(_) => Ok(None),
449 other => Err(ClientError::Protocol(rmux_proto::RmuxError::Server(
450 format!("unexpected readiness response: {other:?}"),
451 ))),
452 }
453}
454
455fn probe_connected_server(
456 mut connection: Connection,
457 _config: &AutoStartConfig,
458 socket_path: &Path,
459) -> Result<Connection, AutoStartError> {
460 let deadline = Instant::now() + DEFAULT_STARTUP_DEADLINE;
461 let mut poll_attempt = 0_u32;
462 loop {
463 match probe_server_readiness(&mut connection) {
464 Ok(()) => return Ok(connection),
465 Err(ClientError::Protocol(RmuxError::UnsupportedWireVersion { got, .. })) => {
466 return Err(AutoStartError::IncompatibleDaemon {
467 socket_path: socket_path.to_path_buf(),
468 message: upgrade::incompatible_daemon_message(&upgrade::IncompatibleDaemon {
469 daemon_version: None,
470 daemon_wire_version: Some(got),
471 }),
472 });
473 }
474 Err(error) if is_transient_connect_error(&error) && Instant::now() < deadline => {
475 let remaining = deadline.saturating_duration_since(Instant::now());
476 std::thread::sleep(startup_readiness_poll_sleep(&mut poll_attempt, remaining));
477 }
478 Err(error) => return Err(AutoStartError::Client(error)),
479 }
480 }
481}
482
483fn startup_readiness_poll_sleep(poll_attempt: &mut u32, remaining: Duration) -> Duration {
484 #[cfg(windows)]
485 {
486 const INITIAL_POLL_MILLIS: u64 = 1;
487
488 let shift = (*poll_attempt).min(6);
489 *poll_attempt = (*poll_attempt).saturating_add(1);
490 let millis = INITIAL_POLL_MILLIS
491 .checked_shl(shift)
492 .unwrap_or(u64::MAX)
493 .min(STARTUP_POLL_INTERVAL.as_millis() as u64);
494 Duration::from_millis(millis).min(remaining)
495 }
496
497 #[cfg(not(windows))]
498 {
499 const INITIAL_POLL_MILLIS: u64 = 1;
500
501 let shift = (*poll_attempt).min(6);
502 *poll_attempt = (*poll_attempt).saturating_add(1);
503 let millis = INITIAL_POLL_MILLIS
504 .checked_shl(shift)
505 .unwrap_or(u64::MAX)
506 .min(STARTUP_POLL_INTERVAL.as_millis() as u64);
507 Duration::from_millis(millis).min(remaining)
508 }
509}
510
511#[cfg(unix)]
512fn auto_start_error_from_startup(
513 error: StartupError,
514 binary_path: &Path,
515 socket_path: &Path,
516) -> AutoStartError {
517 match error {
518 StartupError::Launcher { source } => AutoStartError::Launch {
519 path: binary_path.to_path_buf(),
520 error: source,
521 },
522 StartupError::StartupTimeout { waited, .. } => AutoStartError::TimedOut {
523 socket_path: socket_path.to_path_buf(),
524 waited,
525 },
526 error => AutoStartError::Client(ClientError::Io(io::Error::new(
527 startup_error_kind(&error),
528 error.to_string(),
529 ))),
530 }
531}
532
533#[cfg(windows)]
534fn auto_start_error_from_startup(
535 error: StartupError,
536 binary_path: &Path,
537 socket_path: &Path,
538) -> AutoStartError {
539 match error {
540 StartupError::Launcher { source } => AutoStartError::Launch {
541 path: binary_path.to_path_buf(),
542 error: source,
543 },
544 StartupError::StartupTimeout { waited, .. } => AutoStartError::TimedOut {
545 socket_path: socket_path.to_path_buf(),
546 waited,
547 },
548 error => AutoStartError::Client(ClientError::Io(io::Error::new(
549 startup_error_kind(&error),
550 error.to_string(),
551 ))),
552 }
553}
554
555#[cfg(unix)]
556fn startup_error_kind(error: &StartupError) -> io::ErrorKind {
557 match error {
558 StartupError::InvalidPath { .. } | StartupError::SymlinkRejected { .. } => {
559 io::ErrorKind::InvalidInput
560 }
561 StartupError::UnsafeOwner { .. }
562 | StartupError::UnsafePermissions { .. }
563 | StartupError::PeerCredentialMismatch { .. } => io::ErrorKind::PermissionDenied,
564 StartupError::Lock { source, .. } | StartupError::Filesystem { source, .. } => {
565 source.kind()
566 }
567 StartupError::Launcher { source } => source.kind(),
568 StartupError::StartupTimeout { .. } => io::ErrorKind::TimedOut,
569 }
570}
571
572#[cfg(windows)]
573fn startup_error_kind(error: &StartupError) -> io::ErrorKind {
574 match error {
575 StartupError::InvalidPipeName { .. } | StartupError::InvalidMutexName { .. } => {
576 io::ErrorKind::InvalidInput
577 }
578 StartupError::MutexAccessDenied { .. } | StartupError::PipeAccessDenied { .. } => {
579 io::ErrorKind::PermissionDenied
580 }
581 StartupError::MutexTimeout { .. }
582 | StartupError::PipeBusy { .. }
583 | StartupError::StartupTimeout { .. } => io::ErrorKind::TimedOut,
584 StartupError::PipeNotFound { .. } | StartupError::PipeNoData { .. } => {
585 io::ErrorKind::NotFound
586 }
587 StartupError::Mutex { source, .. } | StartupError::PipeIo { source, .. } => source.kind(),
588 StartupError::Launcher { source } => source.kind(),
589 }
590}
591
592#[cfg(not(any(unix, windows)))]
593fn ensure_server_running_polling(
594 socket_path: &Path,
595 config: AutoStartConfig,
596) -> Result<EnsuredServerConnection, AutoStartError> {
597 if config.loads_startup_config() {
598 return ensure_server_running_with_probe_outcome(
599 socket_path,
600 AUTO_START_TIMEOUT,
601 POLL_INTERVAL,
602 || crate::connect_or_absent(socket_path),
603 || launch_hidden_daemon(socket_path, &config),
604 |_| Ok(()),
605 );
606 }
607
608 ensure_server_running_with_probe_outcome(
609 socket_path,
610 AUTO_START_TIMEOUT,
611 POLL_INTERVAL,
612 || crate::connect_or_absent(socket_path),
613 || launch_hidden_daemon(socket_path, &config),
614 probe_server_readiness,
615 )
616}
617
618#[derive(Debug)]
620pub enum AutoStartError {
621 Client(ClientError),
623 BinaryPath(io::Error),
625 Launch {
627 path: PathBuf,
629 error: io::Error,
631 },
632 IncompatibleDaemon {
634 socket_path: PathBuf,
636 message: String,
638 },
639 TimedOut {
641 socket_path: PathBuf,
643 waited: Duration,
645 },
646}
647
648impl fmt::Display for AutoStartError {
649 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
650 match self {
651 Self::Client(error) => write!(formatter, "{error}"),
652 Self::BinaryPath(error) => {
653 write!(formatter, "failed to resolve rmux binary path: {error}")
654 }
655 Self::Launch { path, error } => {
656 write!(
657 formatter,
658 "failed to launch hidden rmux daemon '{}': {error}",
659 path.display()
660 )
661 }
662 Self::IncompatibleDaemon {
663 socket_path,
664 message,
665 } => write!(
666 formatter,
667 "rmux: {message} on '{}'.\nrmux: run `{}` to stop it, then retry.",
668 socket_path.display(),
669 incompatible_daemon_kill_server_command(socket_path)
670 ),
671 Self::TimedOut {
672 socket_path,
673 waited,
674 } => write!(
675 formatter,
676 "timed out after {}s waiting for rmux server socket '{}'. \
677 The hidden daemon may have exited before creating the socket; run `{}` to surface startup errors.",
678 waited.as_secs(),
679 socket_path.display(),
680 diagnostic_start_server_command(socket_path)
681 ),
682 }
683 }
684}
685
686impl std::error::Error for AutoStartError {
687 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
688 match self {
689 Self::Client(error) => Some(error),
690 Self::BinaryPath(error) => Some(error),
691 Self::Launch { error, .. } => Some(error),
692 Self::IncompatibleDaemon { .. } => None,
693 Self::TimedOut { .. } => None,
694 }
695 }
696}
697
698impl From<ClientError> for AutoStartError {
699 fn from(error: ClientError) -> Self {
700 Self::Client(error)
701 }
702}
703
704#[cfg(not(any(unix, windows)))]
705fn ensure_server_running_with<ConnectFn, LaunchFn>(
706 socket_path: &Path,
707 timeout: Duration,
708 poll_interval: Duration,
709 connect: ConnectFn,
710 launch: LaunchFn,
711) -> Result<Connection, AutoStartError>
712where
713 ConnectFn: FnMut() -> Result<ConnectResult, ClientError>,
714 LaunchFn: FnMut() -> Result<(), AutoStartError>,
715{
716 ensure_server_running_with_probe(
717 socket_path,
718 timeout,
719 poll_interval,
720 connect,
721 launch,
722 probe_server_readiness,
723 )
724}
725
726#[cfg(any(all(test, unix), not(any(unix, windows))))]
727fn ensure_server_running_with_probe<ConnectFn, LaunchFn, ProbeFn>(
728 socket_path: &Path,
729 timeout: Duration,
730 poll_interval: Duration,
731 connect: ConnectFn,
732 launch: LaunchFn,
733 probe: ProbeFn,
734) -> Result<Connection, AutoStartError>
735where
736 ConnectFn: FnMut() -> Result<ConnectResult, ClientError>,
737 LaunchFn: FnMut() -> Result<(), AutoStartError>,
738 ProbeFn: FnMut(&mut Connection) -> Result<(), ClientError>,
739{
740 ensure_server_running_with_probe_outcome(
741 socket_path,
742 timeout,
743 poll_interval,
744 connect,
745 launch,
746 probe,
747 )
748 .map(EnsuredServerConnection::into_connection)
749}
750
751#[cfg(any(all(test, unix), not(any(unix, windows))))]
752fn ensure_server_running_with_probe_outcome<ConnectFn, LaunchFn, ProbeFn>(
753 socket_path: &Path,
754 timeout: Duration,
755 poll_interval: Duration,
756 mut connect: ConnectFn,
757 mut launch: LaunchFn,
758 mut probe: ProbeFn,
759) -> Result<EnsuredServerConnection, AutoStartError>
760where
761 ConnectFn: FnMut() -> Result<ConnectResult, ClientError>,
762 LaunchFn: FnMut() -> Result<(), AutoStartError>,
763 ProbeFn: FnMut(&mut Connection) -> Result<(), ClientError>,
764{
765 match connect().map_err(AutoStartError::Client)? {
766 ConnectResult::Connected(mut connection) => {
767 probe(&mut connection).map_err(AutoStartError::Client)?;
768 return Ok(EnsuredServerConnection::new(
769 connection,
770 ServerConnectionProvenance::JoinedExisting,
771 socket_path.to_path_buf(),
772 ));
773 }
774 ConnectResult::Absent => {}
775 }
776
777 launch()?;
778 let connection = wait_for_server(
779 socket_path,
780 timeout,
781 poll_interval,
782 &mut connect,
783 &mut probe,
784 )?;
785 Ok(EnsuredServerConnection::new(
786 connection,
787 ServerConnectionProvenance::StartedByCaller,
788 socket_path.to_path_buf(),
789 ))
790}
791
792#[cfg(any(all(test, unix), not(any(unix, windows))))]
793fn wait_for_server<ConnectFn, ProbeFn>(
794 socket_path: &Path,
795 timeout: Duration,
796 poll_interval: Duration,
797 connect: &mut ConnectFn,
798 probe: &mut ProbeFn,
799) -> Result<Connection, AutoStartError>
800where
801 ConnectFn: FnMut() -> Result<ConnectResult, ClientError>,
802 ProbeFn: FnMut(&mut Connection) -> Result<(), ClientError>,
803{
804 let start = Instant::now();
805 let deadline = start + timeout;
806
807 loop {
808 match connect() {
809 Ok(crate::ConnectResult::Connected(mut connection)) => match probe(&mut connection) {
810 Ok(()) => return Ok(connection),
811 Err(error) if is_transient_connect_error(&error) => {}
812 Err(error) => return Err(AutoStartError::Client(error)),
813 },
814 Ok(crate::ConnectResult::Absent) => {}
815 Err(error) if is_transient_connect_error(&error) => {}
816 Err(error) => return Err(AutoStartError::Client(error)),
817 }
818
819 let now = Instant::now();
820 if now >= deadline {
821 return Err(AutoStartError::TimedOut {
822 socket_path: socket_path.to_path_buf(),
823 waited: timeout,
824 });
825 }
826
827 std::thread::sleep(poll_interval.min(deadline.saturating_duration_since(now)));
828 }
829}
830
831fn is_transient_connect_error(error: &ClientError) -> bool {
832 matches!(
833 error,
834 ClientError::Io(io_error)
835 if matches!(
836 io_error.kind(),
837 io::ErrorKind::WouldBlock
838 | io::ErrorKind::Interrupted
839 | io::ErrorKind::TimedOut
840 )
841 )
842}
843
844fn incompatible_daemon_kill_server_command(socket_path: &Path) -> String {
845 if default_socket_path()
846 .ok()
847 .as_deref()
848 .is_some_and(|default_path| default_path == socket_path)
849 {
850 return "rmux kill-server".to_owned();
851 }
852
853 format!("rmux -S {} kill-server", shell_quote_path(socket_path))
854}
855
856fn diagnostic_start_server_command(socket_path: &Path) -> String {
857 if default_socket_path()
858 .ok()
859 .as_deref()
860 .is_some_and(|default_path| default_path == socket_path)
861 {
862 return "rmux start-server".to_owned();
863 }
864
865 format!("rmux -S {} start-server", shell_quote_path(socket_path))
866}
867
868fn probe_server_readiness(connection: &mut Connection) -> Result<(), ClientError> {
869 let response = connection.daemon_status()?;
870 match response {
871 Response::DaemonStatus(status) if status.config_loading => {
872 Err(ClientError::Io(io::Error::new(
873 io::ErrorKind::WouldBlock,
874 "daemon is still loading startup config",
875 )))
876 }
877 Response::DaemonStatus(_) => Ok(()),
878 Response::Error(_) => Ok(()),
879 other => Err(ClientError::Protocol(rmux_proto::RmuxError::Server(
880 format!("unexpected readiness response: {other:?}"),
881 ))),
882 }
883}
884
885#[cfg(not(any(unix, windows)))]
886fn launch_hidden_daemon(
887 socket_path: &Path,
888 config: &AutoStartConfig,
889) -> Result<(), AutoStartError> {
890 let binary_path = rmux_binary_path(config).map_err(AutoStartError::BinaryPath)?;
891 spawn_hidden_daemon_for(&binary_path, socket_path, config).map_err(|error| {
892 AutoStartError::Launch {
893 path: binary_path,
894 error,
895 }
896 })
897}
898
899fn spawn_hidden_daemon_for(
900 binary_path: &Path,
901 socket_path: &Path,
902 config: &AutoStartConfig,
903) -> io::Result<()> {
904 #[cfg(target_os = "linux")]
905 {
906 spawn_hidden_daemon_for_linux(binary_path, socket_path, config)
907 }
908
909 #[cfg(not(target_os = "linux"))]
910 {
911 spawn_hidden_daemon_for_polling(binary_path, socket_path, config)
912 }
913}
914
915#[cfg(not(target_os = "linux"))]
916fn spawn_hidden_daemon_for_polling(
917 binary_path: &Path,
918 socket_path: &Path,
919 config: &AutoStartConfig,
920) -> io::Result<()> {
921 #[cfg(windows)]
922 {
923 spawn_hidden_daemon_for_windows(binary_path, socket_path, config)
924 }
925
926 #[cfg(not(windows))]
927 {
928 let command = hidden_daemon_command(binary_path, socket_path, config, true);
929 spawn_hidden_daemon(command)
930 }
931}
932
933#[cfg(windows)]
934fn spawn_hidden_daemon_for_windows(
935 binary_path: &Path,
936 socket_path: &Path,
937 config: &AutoStartConfig,
938) -> io::Result<()> {
939 let ready = rmux_os::daemon::StartupReadyEvent::new()?;
940 let mut command = hidden_daemon_command(binary_path, socket_path, config, true);
941 append_startup_ready_event(&mut command, &ready);
942 spawn_hidden_daemon(command)?;
943 let _ = ready.wait(STARTUP_READY_EVENT_TIMEOUT);
944 Ok(())
945}
946
947#[cfg(windows)]
948fn append_startup_ready_event(command: &mut Command, ready: &rmux_os::daemon::StartupReadyEvent) {
949 command.arg("--startup-ready-event").arg(ready.name());
950}
951
952#[cfg(target_os = "linux")]
953fn spawn_hidden_daemon_for_linux(
954 binary_path: &Path,
955 socket_path: &Path,
956 config: &AutoStartConfig,
957) -> io::Result<()> {
958 let mut ready = StartupReadyEvent::new()?;
959 let mut command =
960 hidden_daemon_command_preserving_fd(binary_path, socket_path, config, true, ready.raw_fd());
961 ready.append_hidden_daemon_args(&mut command);
962 spawn_hidden_daemon(command)?;
963 ready.wait_for_signal(STARTUP_READY_EVENT_TIMEOUT);
964 Ok(())
965}
966
967#[cfg(target_os = "linux")]
968struct StartupReadyEvent {
969 file: File,
970}
971
972#[cfg(target_os = "linux")]
973impl StartupReadyEvent {
974 fn new() -> io::Result<Self> {
975 let fd = rustix::event::eventfd(
976 0,
977 rustix::event::EventfdFlags::NONBLOCK | rustix::event::EventfdFlags::CLOEXEC,
978 )
979 .map_err(io::Error::from)?;
980 Ok(Self { file: fd.into() })
981 }
982
983 fn append_hidden_daemon_args(&self, command: &mut Command) {
984 command
985 .arg("--startup-ready-fd")
986 .arg(self.file.as_raw_fd().to_string());
987 }
988
989 fn raw_fd(&self) -> i32 {
990 self.file.as_raw_fd()
991 }
992
993 fn wait_for_signal(&mut self, timeout: Duration) {
994 let deadline = Instant::now() + timeout;
995 let mut bytes = [0_u8; 8];
996 loop {
997 match self.file.read_exact(&mut bytes) {
998 Ok(()) => return,
999 Err(error)
1000 if matches!(
1001 error.kind(),
1002 io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
1003 ) && Instant::now() < deadline =>
1004 {
1005 std::thread::sleep(Duration::from_millis(1));
1006 }
1007 Err(_) => return,
1008 }
1009 }
1010 }
1011}
1012
1013#[cfg(target_os = "linux")]
1014fn hidden_daemon_command_preserving_fd(
1015 binary_path: &Path,
1016 socket_path: &Path,
1017 config: &AutoStartConfig,
1018 allow_job_breakaway: bool,
1019 preserved_fd: i32,
1020) -> Command {
1021 let mut command = hidden_daemon_command_base(binary_path, socket_path, config);
1022 rmux_os::daemon::configure_hidden_daemon_command_preserving_fds(
1023 &mut command,
1024 allow_job_breakaway,
1025 &[preserved_fd],
1026 );
1027 command
1028}
1029
1030#[cfg(not(target_os = "linux"))]
1031fn hidden_daemon_command(
1032 binary_path: &Path,
1033 socket_path: &Path,
1034 config: &AutoStartConfig,
1035 allow_job_breakaway: bool,
1036) -> Command {
1037 let mut command = hidden_daemon_command_base(binary_path, socket_path, config);
1038 rmux_os::daemon::configure_hidden_daemon_command(&mut command, allow_job_breakaway);
1039 command
1040}
1041
1042fn hidden_daemon_command_base(
1043 binary_path: &Path,
1044 socket_path: &Path,
1045 config: &AutoStartConfig,
1046) -> Command {
1047 let mut command = Command::new(binary_path);
1048 command
1049 .arg(INTERNAL_DAEMON_FLAG)
1050 .arg(socket_path)
1051 .stdin(Stdio::null())
1052 .stdout(Stdio::null())
1053 .stderr(Stdio::null());
1054 config.append_hidden_daemon_args(&mut command);
1055 command
1056}
1057
1058fn spawn_hidden_daemon(mut command: Command) -> io::Result<()> {
1059 let child = rmux_os::daemon::spawn_hidden_daemon_command_requiring_job_breakaway(&mut command)?;
1060 drop(child);
1063 Ok(())
1064}
1065
1066fn rmux_binary_path(config: &AutoStartConfig) -> io::Result<PathBuf> {
1067 if let Some(path) = &config.binary_override {
1068 return Ok(path.clone());
1069 }
1070
1071 let current_exe = env::current_exe()?;
1072 let resolved_exe = std::fs::canonicalize(¤t_exe).ok();
1073 match env::var_os(BINARY_OVERRIDE_ENV).filter(|_| binary_override_enabled_for_tests()) {
1074 Some(path) => Ok(PathBuf::from(path)),
1075 None => Ok(hidden_daemon_binary_path_for_executable_paths(
1076 ¤t_exe,
1077 resolved_exe.as_deref(),
1078 config,
1079 )
1080 .unwrap_or(current_exe)),
1081 }
1082}
1083
1084fn binary_override_enabled_for_tests() -> bool {
1085 cfg!(debug_assertions)
1086 && env::var_os(BINARY_OVERRIDE_TEST_OPT_IN_ENV).is_some_and(|value| value == "1")
1087}
1088
1089#[cfg(all(test, unix))]
1090fn hidden_daemon_binary_path(current_exe: &Path) -> Option<PathBuf> {
1091 hidden_daemon_binary_path_for_executable_paths(current_exe, None, &AutoStartConfig::disabled())
1092}
1093
1094fn hidden_daemon_binary_path_for_executable_paths(
1095 current_exe: &Path,
1096 resolved_exe: Option<&Path>,
1097 config: &AutoStartConfig,
1098) -> Option<PathBuf> {
1099 hidden_daemon_binary_path_for_config(current_exe, config).or_else(|| {
1100 resolved_exe.and_then(|path| hidden_daemon_binary_path_for_config(path, config))
1101 })
1102}
1103
1104fn hidden_daemon_binary_path_for_config(
1105 current_exe: &Path,
1106 config: &AutoStartConfig,
1107) -> Option<PathBuf> {
1108 if config.web_required {
1109 return None;
1110 }
1111 let file_stem = current_exe.file_stem()?.to_str()?;
1112 if file_stem == "rmux-daemon" {
1113 return None;
1114 }
1115
1116 let mut candidate = current_exe.to_path_buf();
1117 let daemon_file_name = match current_exe
1118 .extension()
1119 .and_then(|extension| extension.to_str())
1120 {
1121 Some(extension) if !extension.is_empty() => format!("rmux-daemon.{extension}"),
1122 _ => "rmux-daemon".to_owned(),
1123 };
1124 candidate.set_file_name(daemon_file_name);
1125 candidate.is_file().then_some(candidate)
1126}
1127
1128#[cfg(all(test, unix))]
1129#[path = "auto_start/tests.rs"]
1130mod tests;
1131
1132#[cfg(all(test, windows))]
1133mod windows_tests {
1134 use std::time::Duration;
1135
1136 use super::{startup_readiness_poll_sleep, STARTUP_POLL_INTERVAL};
1137
1138 #[test]
1139 fn windows_startup_readiness_poll_uses_short_backoff() {
1140 let mut attempt = 0;
1141 let remaining = Duration::from_secs(1);
1142
1143 let sleeps = (0..8)
1144 .map(|_| startup_readiness_poll_sleep(&mut attempt, remaining))
1145 .collect::<Vec<_>>();
1146
1147 assert_eq!(
1148 sleeps,
1149 [
1150 Duration::from_millis(1),
1151 Duration::from_millis(2),
1152 Duration::from_millis(4),
1153 Duration::from_millis(8),
1154 Duration::from_millis(16),
1155 Duration::from_millis(32),
1156 STARTUP_POLL_INTERVAL,
1157 STARTUP_POLL_INTERVAL,
1158 ]
1159 );
1160 }
1161
1162 #[test]
1163 fn windows_startup_readiness_poll_respects_remaining_deadline() {
1164 let mut attempt = 6;
1165
1166 assert_eq!(
1167 startup_readiness_poll_sleep(&mut attempt, Duration::from_millis(7)),
1168 Duration::from_millis(7)
1169 );
1170 }
1171}