1use std::env;
4use std::fmt;
5use std::io;
6use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio};
8use std::time::Duration;
9#[cfg(any(all(test, unix), not(any(unix, windows))))]
10use std::time::Instant;
11
12#[cfg(not(windows))]
13use rmux_proto::{ListSessionsRequest, Response};
14#[cfg(unix)]
15use rmux_sdk::bootstrap::startup_unix::{
16 connect_or_start_with, StartupError, StartupOutcome, DEFAULT_STARTUP_DEADLINE,
17 STARTUP_POLL_INTERVAL,
18};
19#[cfg(windows)]
20use rmux_sdk::bootstrap::startup_windows::{
21 connect_or_start_with, StartupError, StartupOutcome, DEFAULT_STARTUP_DEADLINE,
22 STARTUP_POLL_INTERVAL,
23};
24
25use crate::shell_quote::shell_quote_path;
26#[cfg(any(all(test, unix), not(any(unix, windows))))]
27use crate::ConnectResult;
28use crate::{ClientError, Connection};
29
30mod upgrade_restart;
31
32#[cfg(not(any(unix, windows)))]
33const AUTO_START_TIMEOUT: Duration = Duration::from_secs(5);
34#[cfg(not(any(unix, windows)))]
35const POLL_INTERVAL: Duration = Duration::from_millis(50);
36
37pub const INTERNAL_DAEMON_FLAG: &str = "--__internal-daemon";
42
43const BINARY_OVERRIDE_ENV: &str = "RMUX_INTERNAL_BINARY_PATH";
44const BINARY_OVERRIDE_TEST_OPT_IN_ENV: &str = "RMUX_ALLOW_INTERNAL_BINARY_OVERRIDE";
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct AutoStartConfig {
49 selection: AutoStartConfigSelection,
50 quiet: bool,
51 cwd: Option<PathBuf>,
52}
53
54impl AutoStartConfig {
55 #[must_use]
57 pub const fn disabled() -> Self {
58 Self {
59 selection: AutoStartConfigSelection::Disabled,
60 quiet: true,
61 cwd: None,
62 }
63 }
64
65 #[must_use]
67 pub fn default_files(quiet: bool, cwd: Option<PathBuf>) -> Self {
68 Self {
69 selection: AutoStartConfigSelection::Default,
70 quiet,
71 cwd,
72 }
73 }
74
75 #[must_use]
77 pub fn custom_files(files: Vec<PathBuf>, quiet: bool, cwd: Option<PathBuf>) -> Self {
78 Self {
79 selection: AutoStartConfigSelection::Files(files),
80 quiet,
81 cwd,
82 }
83 }
84
85 #[cfg(not(windows))]
86 fn loads_startup_config(&self) -> bool {
87 !matches!(self.selection, AutoStartConfigSelection::Disabled)
88 }
89
90 fn append_hidden_daemon_args(&self, command: &mut Command) {
91 match &self.selection {
92 AutoStartConfigSelection::Disabled => {}
93 AutoStartConfigSelection::Default => {
94 command.arg("--config-default");
95 }
96 AutoStartConfigSelection::Files(files) => {
97 for file in files {
98 command.arg("--config-file").arg(file);
99 }
100 }
101 }
102
103 if self.quiet {
104 command.arg("--config-quiet");
105 }
106 if let Some(cwd) = &self.cwd {
107 command.arg("--config-cwd").arg(cwd);
108 }
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum AutoStartConfigSelection {
115 Disabled,
117 Default,
119 Files(Vec<PathBuf>),
121}
122
123pub fn ensure_server_running(socket_path: &Path) -> Result<Connection, AutoStartError> {
130 ensure_server_running_with_config(socket_path, AutoStartConfig::disabled())
131}
132
133#[cfg(unix)]
135pub fn ensure_server_running_with_config(
136 socket_path: &Path,
137 config: AutoStartConfig,
138) -> Result<Connection, AutoStartError> {
139 ensure_server_running_unix(socket_path, config)
140}
141
142#[cfg(windows)]
144pub fn ensure_server_running_with_config(
145 socket_path: &Path,
146 config: AutoStartConfig,
147) -> Result<Connection, AutoStartError> {
148 ensure_server_running_windows(socket_path, config)
149}
150
151#[cfg(not(any(unix, windows)))]
153pub fn ensure_server_running_with_config(
154 socket_path: &Path,
155 config: AutoStartConfig,
156) -> Result<Connection, AutoStartError> {
157 ensure_server_running_polling(socket_path, config)
158}
159
160#[cfg(unix)]
161fn ensure_server_running_unix(
162 socket_path: &Path,
163 config: AutoStartConfig,
164) -> Result<Connection, AutoStartError> {
165 let binary_path = rmux_binary_path().map_err(AutoStartError::BinaryPath)?;
166 let launcher_binary_path = binary_path.clone();
167 let launcher_socket_path = socket_path.to_path_buf();
168 let launcher_config = config.clone();
169
170 let runtime = tokio::runtime::Builder::new_current_thread()
171 .enable_all()
172 .build()
173 .map_err(|error| AutoStartError::Client(ClientError::Io(error)))?;
174 let outcome = runtime.block_on(connect_or_start_with(
175 socket_path,
176 move || async move {
177 spawn_hidden_daemon_for(
178 &launcher_binary_path,
179 &launcher_socket_path,
180 &launcher_config,
181 )
182 },
183 DEFAULT_STARTUP_DEADLINE,
184 STARTUP_POLL_INTERVAL,
185 ));
186
187 let connection = startup_outcome_into_connection(
188 outcome.map_err(|error| auto_start_error_from_startup(error, &binary_path, socket_path))?,
189 )?;
190
191 let connection = probe_connected_server(connection, &config)?;
192 upgrade_restart::ensure_daemon_fresh_or_restart(connection, socket_path, &binary_path, &config)
193}
194
195#[cfg(unix)]
196fn startup_outcome_into_connection(outcome: StartupOutcome) -> Result<Connection, AutoStartError> {
197 let stream = outcome
198 .into_stream()
199 .into_std()
200 .map_err(|error| AutoStartError::Client(ClientError::Io(error)))?;
201 stream
202 .set_nonblocking(false)
203 .map_err(|error| AutoStartError::Client(ClientError::Io(error)))?;
204 Connection::new(stream).map_err(AutoStartError::Client)
205}
206
207#[cfg(windows)]
208fn ensure_server_running_windows(
209 socket_path: &Path,
210 config: AutoStartConfig,
211) -> Result<Connection, AutoStartError> {
212 let binary_path = rmux_binary_path().map_err(AutoStartError::BinaryPath)?;
213 let launcher_binary_path = binary_path.clone();
214 let launcher_socket_path = socket_path.to_path_buf();
215 let launcher_config = config.clone();
216
217 let runtime = tokio::runtime::Builder::new_current_thread()
218 .enable_all()
219 .build()
220 .map_err(|error| AutoStartError::Client(ClientError::Io(error)))?;
221 let outcome = runtime.block_on(connect_or_start_with(
222 socket_path,
223 move || async move {
224 spawn_hidden_daemon_for(
225 &launcher_binary_path,
226 &launcher_socket_path,
227 &launcher_config,
228 )
229 },
230 DEFAULT_STARTUP_DEADLINE,
231 STARTUP_POLL_INTERVAL,
232 ));
233
234 let connection = startup_outcome_into_connection(
235 outcome.map_err(|error| auto_start_error_from_startup(error, &binary_path, socket_path))?,
236 )?;
237 upgrade_restart::ensure_daemon_fresh_or_restart(connection, socket_path, &binary_path, &config)
238}
239
240#[cfg(windows)]
241fn startup_outcome_into_connection(outcome: StartupOutcome) -> Result<Connection, AutoStartError> {
242 Connection::new(outcome.into_stream()).map_err(AutoStartError::Client)
243}
244
245#[cfg(not(windows))]
246fn probe_connected_server(
247 mut connection: Connection,
248 config: &AutoStartConfig,
249) -> Result<Connection, AutoStartError> {
250 if !config.loads_startup_config() {
251 probe_server_readiness(&mut connection).map_err(AutoStartError::Client)?;
252 }
253 Ok(connection)
254}
255
256#[cfg(windows)]
257fn probe_connected_server(
258 connection: Connection,
259 _config: &AutoStartConfig,
260) -> Result<Connection, AutoStartError> {
261 Ok(connection)
262}
263
264#[cfg(unix)]
265fn auto_start_error_from_startup(
266 error: StartupError,
267 binary_path: &Path,
268 socket_path: &Path,
269) -> AutoStartError {
270 match error {
271 StartupError::Launcher { source } => AutoStartError::Launch {
272 path: binary_path.to_path_buf(),
273 error: source,
274 },
275 StartupError::StartupTimeout { waited, .. } => AutoStartError::TimedOut {
276 socket_path: socket_path.to_path_buf(),
277 waited,
278 },
279 error => AutoStartError::Client(ClientError::Io(io::Error::new(
280 startup_error_kind(&error),
281 error.to_string(),
282 ))),
283 }
284}
285
286#[cfg(windows)]
287fn auto_start_error_from_startup(
288 error: StartupError,
289 binary_path: &Path,
290 socket_path: &Path,
291) -> AutoStartError {
292 match error {
293 StartupError::Launcher { source } => AutoStartError::Launch {
294 path: binary_path.to_path_buf(),
295 error: source,
296 },
297 StartupError::StartupTimeout { waited, .. } => AutoStartError::TimedOut {
298 socket_path: socket_path.to_path_buf(),
299 waited,
300 },
301 error => AutoStartError::Client(ClientError::Io(io::Error::new(
302 startup_error_kind(&error),
303 error.to_string(),
304 ))),
305 }
306}
307
308#[cfg(unix)]
309fn startup_error_kind(error: &StartupError) -> io::ErrorKind {
310 match error {
311 StartupError::InvalidPath { .. } | StartupError::SymlinkRejected { .. } => {
312 io::ErrorKind::InvalidInput
313 }
314 StartupError::UnsafeOwner { .. }
315 | StartupError::UnsafePermissions { .. }
316 | StartupError::PeerCredentialMismatch { .. } => io::ErrorKind::PermissionDenied,
317 StartupError::Lock { source, .. } | StartupError::Filesystem { source, .. } => {
318 source.kind()
319 }
320 StartupError::Launcher { source } => source.kind(),
321 StartupError::StartupTimeout { .. } => io::ErrorKind::TimedOut,
322 }
323}
324
325#[cfg(windows)]
326fn startup_error_kind(error: &StartupError) -> io::ErrorKind {
327 match error {
328 StartupError::InvalidPipeName { .. } | StartupError::InvalidMutexName { .. } => {
329 io::ErrorKind::InvalidInput
330 }
331 StartupError::MutexAccessDenied { .. } | StartupError::PipeAccessDenied { .. } => {
332 io::ErrorKind::PermissionDenied
333 }
334 StartupError::MutexTimeout { .. }
335 | StartupError::PipeBusy { .. }
336 | StartupError::StartupTimeout { .. } => io::ErrorKind::TimedOut,
337 StartupError::PipeNotFound { .. } | StartupError::PipeNoData { .. } => {
338 io::ErrorKind::NotFound
339 }
340 StartupError::Mutex { source, .. } | StartupError::PipeIo { source, .. } => source.kind(),
341 StartupError::Launcher { source } => source.kind(),
342 }
343}
344
345#[cfg(not(any(unix, windows)))]
346fn ensure_server_running_polling(
347 socket_path: &Path,
348 config: AutoStartConfig,
349) -> Result<Connection, AutoStartError> {
350 if config.loads_startup_config() {
351 return ensure_server_running_with_probe(
352 socket_path,
353 AUTO_START_TIMEOUT,
354 POLL_INTERVAL,
355 || crate::connect_or_absent(socket_path),
356 || launch_hidden_daemon(socket_path, &config),
357 |_| Ok(()),
358 );
359 }
360
361 ensure_server_running_with(
362 socket_path,
363 AUTO_START_TIMEOUT,
364 POLL_INTERVAL,
365 || crate::connect_or_absent(socket_path),
366 || launch_hidden_daemon(socket_path, &config),
367 )
368}
369
370#[derive(Debug)]
372pub enum AutoStartError {
373 Client(ClientError),
375 BinaryPath(io::Error),
377 Launch {
379 path: PathBuf,
381 error: io::Error,
383 },
384 IncompatibleDaemon {
386 socket_path: PathBuf,
388 message: String,
390 },
391 TimedOut {
393 socket_path: PathBuf,
395 waited: Duration,
397 },
398}
399
400impl fmt::Display for AutoStartError {
401 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
402 match self {
403 Self::Client(error) => write!(formatter, "{error}"),
404 Self::BinaryPath(error) => {
405 write!(formatter, "failed to resolve rmux binary path: {error}")
406 }
407 Self::Launch { path, error } => {
408 write!(
409 formatter,
410 "failed to launch hidden rmux daemon '{}': {error}",
411 path.display()
412 )
413 }
414 Self::IncompatibleDaemon {
415 socket_path,
416 message,
417 } => write!(
418 formatter,
419 "{message} on '{}'; detach existing clients, then run `rmux -S {} kill-server` before retrying",
420 socket_path.display(),
421 shell_quote_path(socket_path)
422 ),
423 Self::TimedOut {
424 socket_path,
425 waited,
426 } => write!(
427 formatter,
428 "timed out after {}s waiting for rmux server socket '{}'",
429 waited.as_secs(),
430 socket_path.display()
431 ),
432 }
433 }
434}
435
436impl std::error::Error for AutoStartError {
437 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
438 match self {
439 Self::Client(error) => Some(error),
440 Self::BinaryPath(error) => Some(error),
441 Self::Launch { error, .. } => Some(error),
442 Self::IncompatibleDaemon { .. } => None,
443 Self::TimedOut { .. } => None,
444 }
445 }
446}
447
448impl From<ClientError> for AutoStartError {
449 fn from(error: ClientError) -> Self {
450 Self::Client(error)
451 }
452}
453
454#[cfg(not(any(unix, windows)))]
455fn ensure_server_running_with<ConnectFn, LaunchFn>(
456 socket_path: &Path,
457 timeout: Duration,
458 poll_interval: Duration,
459 connect: ConnectFn,
460 launch: LaunchFn,
461) -> Result<Connection, AutoStartError>
462where
463 ConnectFn: FnMut() -> Result<ConnectResult, ClientError>,
464 LaunchFn: FnMut() -> Result<(), AutoStartError>,
465{
466 ensure_server_running_with_probe(
467 socket_path,
468 timeout,
469 poll_interval,
470 connect,
471 launch,
472 probe_server_readiness,
473 )
474}
475
476#[cfg(any(all(test, unix), not(any(unix, windows))))]
477fn ensure_server_running_with_probe<ConnectFn, LaunchFn, ProbeFn>(
478 socket_path: &Path,
479 timeout: Duration,
480 poll_interval: Duration,
481 mut connect: ConnectFn,
482 mut launch: LaunchFn,
483 mut probe: ProbeFn,
484) -> Result<Connection, AutoStartError>
485where
486 ConnectFn: FnMut() -> Result<ConnectResult, ClientError>,
487 LaunchFn: FnMut() -> Result<(), AutoStartError>,
488 ProbeFn: FnMut(&mut Connection) -> Result<(), ClientError>,
489{
490 match connect().map_err(AutoStartError::Client)? {
491 ConnectResult::Connected(mut connection) => {
492 probe(&mut connection).map_err(AutoStartError::Client)?;
493 return Ok(connection);
494 }
495 ConnectResult::Absent => {}
496 }
497
498 launch()?;
499 wait_for_server(
500 socket_path,
501 timeout,
502 poll_interval,
503 &mut connect,
504 &mut probe,
505 )
506}
507
508#[cfg(any(all(test, unix), not(any(unix, windows))))]
509fn wait_for_server<ConnectFn, ProbeFn>(
510 socket_path: &Path,
511 timeout: Duration,
512 poll_interval: Duration,
513 connect: &mut ConnectFn,
514 probe: &mut ProbeFn,
515) -> Result<Connection, AutoStartError>
516where
517 ConnectFn: FnMut() -> Result<ConnectResult, ClientError>,
518 ProbeFn: FnMut(&mut Connection) -> Result<(), ClientError>,
519{
520 let start = Instant::now();
521 let deadline = start + timeout;
522
523 loop {
524 match connect() {
525 Ok(crate::ConnectResult::Connected(mut connection)) => match probe(&mut connection) {
526 Ok(()) => return Ok(connection),
527 Err(error) if is_transient_connect_error(&error) => {}
528 Err(error) => return Err(AutoStartError::Client(error)),
529 },
530 Ok(crate::ConnectResult::Absent) => {}
531 Err(error) if is_transient_connect_error(&error) => {}
532 Err(error) => return Err(AutoStartError::Client(error)),
533 }
534
535 let now = Instant::now();
536 if now >= deadline {
537 return Err(AutoStartError::TimedOut {
538 socket_path: socket_path.to_path_buf(),
539 waited: timeout,
540 });
541 }
542
543 std::thread::sleep(poll_interval.min(deadline.saturating_duration_since(now)));
544 }
545}
546
547fn is_transient_connect_error(error: &ClientError) -> bool {
548 matches!(
549 error,
550 ClientError::Io(io_error)
551 if matches!(
552 io_error.kind(),
553 io::ErrorKind::WouldBlock
554 | io::ErrorKind::Interrupted
555 | io::ErrorKind::TimedOut
556 )
557 )
558}
559
560#[cfg(not(windows))]
561fn probe_server_readiness(connection: &mut Connection) -> Result<(), ClientError> {
562 let response = connection.list_sessions(ListSessionsRequest {
563 format: None,
564 filter: None,
565 sort_order: None,
566 reversed: false,
567 })?;
568 match response {
569 Response::ListSessions(_) => Ok(()),
570 other => Err(ClientError::Protocol(rmux_proto::RmuxError::Server(
571 format!("unexpected readiness response: {other:?}"),
572 ))),
573 }
574}
575
576#[cfg(not(any(unix, windows)))]
577fn launch_hidden_daemon(
578 socket_path: &Path,
579 config: &AutoStartConfig,
580) -> Result<(), AutoStartError> {
581 let binary_path = rmux_binary_path().map_err(AutoStartError::BinaryPath)?;
582 spawn_hidden_daemon_for(&binary_path, socket_path, config).map_err(|error| {
583 AutoStartError::Launch {
584 path: binary_path,
585 error,
586 }
587 })
588}
589
590fn spawn_hidden_daemon_for(
591 binary_path: &Path,
592 socket_path: &Path,
593 config: &AutoStartConfig,
594) -> io::Result<()> {
595 let command = hidden_daemon_command(binary_path, socket_path, config, true);
596 match spawn_hidden_daemon(command) {
597 Ok(()) => Ok(()),
598 Err(error) if rmux_os::daemon::should_retry_hidden_daemon_without_breakaway(&error) => {
599 let command = hidden_daemon_command(binary_path, socket_path, config, false);
600 spawn_hidden_daemon(command)
601 }
602 Err(error) => Err(error),
603 }
604}
605
606fn hidden_daemon_command(
607 binary_path: &Path,
608 socket_path: &Path,
609 config: &AutoStartConfig,
610 allow_job_breakaway: bool,
611) -> Command {
612 let mut command = Command::new(binary_path);
613 command
614 .arg(INTERNAL_DAEMON_FLAG)
615 .arg(socket_path)
616 .stdin(Stdio::null())
617 .stdout(Stdio::null())
618 .stderr(Stdio::null());
619 config.append_hidden_daemon_args(&mut command);
620 rmux_os::daemon::configure_hidden_daemon_command(&mut command, allow_job_breakaway);
621 command
622}
623
624fn spawn_hidden_daemon(mut command: Command) -> io::Result<()> {
625 let child = rmux_os::daemon::spawn_hidden_daemon_command(&mut command)?;
626 drop(child);
629 Ok(())
630}
631
632fn rmux_binary_path() -> io::Result<PathBuf> {
633 let current_exe = env::current_exe()?;
634 match env::var_os(BINARY_OVERRIDE_ENV).filter(|_| binary_override_enabled_for_tests()) {
635 Some(path) => Ok(PathBuf::from(path)),
636 None => Ok(current_exe),
637 }
638}
639
640fn binary_override_enabled_for_tests() -> bool {
641 cfg!(debug_assertions)
642 && env::var_os(BINARY_OVERRIDE_TEST_OPT_IN_ENV).is_some_and(|value| value == "1")
643}
644
645#[cfg(all(test, unix))]
646#[path = "auto_start/tests.rs"]
647mod tests;