Skip to main content

rmux_client/
auto_start.rs

1//! Hidden daemon auto-start support for tmux `CMD_STARTSERVER` commands.
2
3use std::env;
4use std::fmt;
5use std::io;
6use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio};
8use std::time::{Duration, Instant};
9
10use rmux_proto::Response;
11#[cfg(unix)]
12use rmux_sdk::bootstrap::startup_unix::{
13    connect_or_start_with, StartupError, StartupOutcome, DEFAULT_STARTUP_DEADLINE,
14    STARTUP_POLL_INTERVAL,
15};
16#[cfg(windows)]
17use rmux_sdk::bootstrap::startup_windows::{
18    connect_or_start_with, StartupError, StartupOutcome, DEFAULT_STARTUP_DEADLINE,
19    STARTUP_POLL_INTERVAL,
20};
21
22use crate::shell_quote::shell_quote_path;
23#[cfg(any(all(test, unix), not(any(unix, windows))))]
24use crate::ConnectResult;
25use crate::{ClientError, Connection};
26
27mod upgrade_restart;
28
29#[cfg(not(any(unix, windows)))]
30const AUTO_START_TIMEOUT: Duration = Duration::from_secs(5);
31#[cfg(not(any(unix, windows)))]
32const POLL_INTERVAL: Duration = Duration::from_millis(50);
33
34/// The undocumented CLI flag that switches `rmux` into hidden daemon mode.
35///
36/// This constant is shared with `src/main.rs` so both sides of the re-exec
37/// protocol stay in sync.
38pub const INTERNAL_DAEMON_FLAG: &str = "--__internal-daemon";
39
40const BINARY_OVERRIDE_ENV: &str = "RMUX_INTERNAL_BINARY_PATH";
41const BINARY_OVERRIDE_TEST_OPT_IN_ENV: &str = "RMUX_ALLOW_INTERNAL_BINARY_OVERRIDE";
42/// Config loading policy to pass to a newly auto-started hidden daemon.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct AutoStartConfig {
45    selection: AutoStartConfigSelection,
46    quiet: bool,
47    cwd: Option<PathBuf>,
48    web_frontend: Option<String>,
49    web_port: Option<u16>,
50    web_required: bool,
51}
52
53impl AutoStartConfig {
54    /// Builds a policy that leaves startup config loading disabled.
55    #[must_use]
56    pub const fn disabled() -> Self {
57        Self {
58            selection: AutoStartConfigSelection::Disabled,
59            quiet: true,
60            cwd: None,
61            web_frontend: None,
62            web_port: None,
63            web_required: false,
64        }
65    }
66
67    /// Builds a policy that loads RMUX's default startup config search path.
68    #[must_use]
69    pub fn default_files(quiet: bool, cwd: Option<PathBuf>) -> Self {
70        Self {
71            selection: AutoStartConfigSelection::Default,
72            quiet,
73            cwd,
74            web_frontend: None,
75            web_port: None,
76            web_required: false,
77        }
78    }
79
80    /// Builds a policy that loads the explicit top-level `-f` files.
81    #[must_use]
82    pub fn custom_files(files: Vec<PathBuf>, quiet: bool, cwd: Option<PathBuf>) -> Self {
83        Self {
84            selection: AutoStartConfigSelection::Files(files),
85            quiet,
86            cwd,
87            web_frontend: None,
88            web_port: None,
89            web_required: false,
90        }
91    }
92
93    /// Overrides the web-share listener port for a newly auto-started daemon.
94    #[must_use]
95    pub const fn with_web_port(mut self, port: u16) -> Self {
96        self.web_port = Some(port);
97        self.web_required = true;
98        self
99    }
100
101    /// Overrides the frontend origin used by newly auto-started web shares.
102    #[must_use]
103    pub fn with_web_frontend(mut self, frontend: String) -> Self {
104        self.web_frontend = Some(frontend);
105        self.web_required = true;
106        self
107    }
108
109    /// Requires a daemon compiled with web-share support for this autostart.
110    #[must_use]
111    pub const fn with_web_required(mut self) -> Self {
112        self.web_required = true;
113        self
114    }
115
116    #[cfg(not(windows))]
117    #[cfg(not(any(unix, windows)))]
118    fn loads_startup_config(&self) -> bool {
119        !matches!(self.selection, AutoStartConfigSelection::Disabled)
120    }
121
122    fn append_hidden_daemon_args(&self, command: &mut Command) {
123        match &self.selection {
124            AutoStartConfigSelection::Disabled => {}
125            AutoStartConfigSelection::Default => {
126                command.arg("--config-default");
127            }
128            AutoStartConfigSelection::Files(files) => {
129                for file in files {
130                    command.arg("--config-file").arg(file);
131                }
132            }
133        }
134
135        if self.quiet {
136            command.arg("--config-quiet");
137        }
138        if let Some(cwd) = &self.cwd {
139            command.arg("--config-cwd").arg(cwd);
140        }
141        if let Some(port) = self.web_port {
142            command.arg("--web-port").arg(port.to_string());
143        }
144        if let Some(frontend) = &self.web_frontend {
145            command.arg("--frontend-url").arg(frontend);
146        }
147    }
148}
149
150/// Config file selection mode for a newly auto-started hidden daemon.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub enum AutoStartConfigSelection {
153    /// Do not load startup config files.
154    Disabled,
155    /// Load RMUX's default config search path.
156    Default,
157    /// Load these explicit config files in order.
158    Files(Vec<PathBuf>),
159}
160
161/// Ensures the RMUX server is reachable, auto-starting it when absent.
162///
163/// This boundary is reserved for command paths that match tmux's
164/// `CMD_STARTSERVER` startup inventory. Other command paths must keep using
165/// [`crate::connect`] or [`crate::connect_or_absent`] directly so they do not
166/// spawn a daemon as a side effect.
167pub fn ensure_server_running(socket_path: &Path) -> Result<Connection, AutoStartError> {
168    ensure_server_running_with_config(socket_path, AutoStartConfig::disabled())
169}
170
171/// Ensures the server is reachable, passing config load options if launched.
172#[cfg(unix)]
173pub fn ensure_server_running_with_config(
174    socket_path: &Path,
175    config: AutoStartConfig,
176) -> Result<Connection, AutoStartError> {
177    ensure_server_running_unix(socket_path, config)
178}
179
180/// Ensures the server is reachable, passing config load options if launched.
181#[cfg(windows)]
182pub fn ensure_server_running_with_config(
183    socket_path: &Path,
184    config: AutoStartConfig,
185) -> Result<Connection, AutoStartError> {
186    ensure_server_running_windows(socket_path, config)
187}
188
189/// Ensures the server is reachable, passing config load options if launched.
190#[cfg(not(any(unix, windows)))]
191pub fn ensure_server_running_with_config(
192    socket_path: &Path,
193    config: AutoStartConfig,
194) -> Result<Connection, AutoStartError> {
195    ensure_server_running_polling(socket_path, config)
196}
197
198#[cfg(unix)]
199fn ensure_server_running_unix(
200    socket_path: &Path,
201    config: AutoStartConfig,
202) -> Result<Connection, AutoStartError> {
203    let binary_path = rmux_binary_path(&config).map_err(AutoStartError::BinaryPath)?;
204    let launcher_binary_path = binary_path.clone();
205    let launcher_socket_path = socket_path.to_path_buf();
206    let launcher_config = config.clone();
207
208    let runtime = tokio::runtime::Builder::new_current_thread()
209        .enable_all()
210        .build()
211        .map_err(|error| AutoStartError::Client(ClientError::Io(error)))?;
212    let outcome = runtime.block_on(connect_or_start_with(
213        socket_path,
214        move || async move {
215            spawn_hidden_daemon_for(
216                &launcher_binary_path,
217                &launcher_socket_path,
218                &launcher_config,
219            )
220        },
221        DEFAULT_STARTUP_DEADLINE,
222        STARTUP_POLL_INTERVAL,
223    ));
224
225    let connection = startup_outcome_into_connection(
226        outcome.map_err(|error| auto_start_error_from_startup(error, &binary_path, socket_path))?,
227    )?;
228
229    let connection = probe_connected_server(connection, &config, socket_path)?;
230    upgrade_restart::ensure_daemon_fresh_or_restart(connection, socket_path, &binary_path, &config)
231}
232
233#[cfg(unix)]
234fn startup_outcome_into_connection(outcome: StartupOutcome) -> Result<Connection, AutoStartError> {
235    let stream = outcome
236        .into_stream()
237        .into_std()
238        .map_err(|error| AutoStartError::Client(ClientError::Io(error)))?;
239    stream
240        .set_nonblocking(false)
241        .map_err(|error| AutoStartError::Client(ClientError::Io(error)))?;
242    Connection::new(stream).map_err(AutoStartError::Client)
243}
244
245#[cfg(windows)]
246fn ensure_server_running_windows(
247    socket_path: &Path,
248    config: AutoStartConfig,
249) -> Result<Connection, AutoStartError> {
250    let binary_path = rmux_binary_path(&config).map_err(AutoStartError::BinaryPath)?;
251    let launcher_binary_path = binary_path.clone();
252    let launcher_socket_path = socket_path.to_path_buf();
253    let launcher_config = config.clone();
254
255    let runtime = tokio::runtime::Builder::new_current_thread()
256        .enable_all()
257        .build()
258        .map_err(|error| AutoStartError::Client(ClientError::Io(error)))?;
259    let outcome = runtime.block_on(connect_or_start_with(
260        socket_path,
261        move || async move {
262            spawn_hidden_daemon_for(
263                &launcher_binary_path,
264                &launcher_socket_path,
265                &launcher_config,
266            )
267        },
268        DEFAULT_STARTUP_DEADLINE,
269        STARTUP_POLL_INTERVAL,
270    ));
271
272    let connection = startup_outcome_into_connection(
273        outcome.map_err(|error| auto_start_error_from_startup(error, &binary_path, socket_path))?,
274    )?;
275    let connection = probe_connected_server(connection, &config, socket_path)?;
276    upgrade_restart::ensure_daemon_fresh_or_restart(connection, socket_path, &binary_path, &config)
277}
278
279#[cfg(windows)]
280fn startup_outcome_into_connection(outcome: StartupOutcome) -> Result<Connection, AutoStartError> {
281    Connection::new(outcome.into_stream()).map_err(AutoStartError::Client)
282}
283
284fn probe_connected_server(
285    mut connection: Connection,
286    _config: &AutoStartConfig,
287    _socket_path: &Path,
288) -> Result<Connection, AutoStartError> {
289    let deadline = Instant::now() + DEFAULT_STARTUP_DEADLINE;
290    loop {
291        match probe_server_readiness(&mut connection) {
292            Ok(()) => return Ok(connection),
293            Err(error) if is_transient_connect_error(&error) && Instant::now() < deadline => {
294                let remaining = deadline.saturating_duration_since(Instant::now());
295                std::thread::sleep(STARTUP_POLL_INTERVAL.min(remaining));
296            }
297            Err(error) => return Err(AutoStartError::Client(error)),
298        }
299    }
300}
301
302#[cfg(unix)]
303fn auto_start_error_from_startup(
304    error: StartupError,
305    binary_path: &Path,
306    socket_path: &Path,
307) -> AutoStartError {
308    match error {
309        StartupError::Launcher { source } => AutoStartError::Launch {
310            path: binary_path.to_path_buf(),
311            error: source,
312        },
313        StartupError::StartupTimeout { waited, .. } => AutoStartError::TimedOut {
314            socket_path: socket_path.to_path_buf(),
315            waited,
316        },
317        error => AutoStartError::Client(ClientError::Io(io::Error::new(
318            startup_error_kind(&error),
319            error.to_string(),
320        ))),
321    }
322}
323
324#[cfg(windows)]
325fn auto_start_error_from_startup(
326    error: StartupError,
327    binary_path: &Path,
328    socket_path: &Path,
329) -> AutoStartError {
330    match error {
331        StartupError::Launcher { source } => AutoStartError::Launch {
332            path: binary_path.to_path_buf(),
333            error: source,
334        },
335        StartupError::StartupTimeout { waited, .. } => AutoStartError::TimedOut {
336            socket_path: socket_path.to_path_buf(),
337            waited,
338        },
339        error => AutoStartError::Client(ClientError::Io(io::Error::new(
340            startup_error_kind(&error),
341            error.to_string(),
342        ))),
343    }
344}
345
346#[cfg(unix)]
347fn startup_error_kind(error: &StartupError) -> io::ErrorKind {
348    match error {
349        StartupError::InvalidPath { .. } | StartupError::SymlinkRejected { .. } => {
350            io::ErrorKind::InvalidInput
351        }
352        StartupError::UnsafeOwner { .. }
353        | StartupError::UnsafePermissions { .. }
354        | StartupError::PeerCredentialMismatch { .. } => io::ErrorKind::PermissionDenied,
355        StartupError::Lock { source, .. } | StartupError::Filesystem { source, .. } => {
356            source.kind()
357        }
358        StartupError::Launcher { source } => source.kind(),
359        StartupError::StartupTimeout { .. } => io::ErrorKind::TimedOut,
360    }
361}
362
363#[cfg(windows)]
364fn startup_error_kind(error: &StartupError) -> io::ErrorKind {
365    match error {
366        StartupError::InvalidPipeName { .. } | StartupError::InvalidMutexName { .. } => {
367            io::ErrorKind::InvalidInput
368        }
369        StartupError::MutexAccessDenied { .. } | StartupError::PipeAccessDenied { .. } => {
370            io::ErrorKind::PermissionDenied
371        }
372        StartupError::MutexTimeout { .. }
373        | StartupError::PipeBusy { .. }
374        | StartupError::StartupTimeout { .. } => io::ErrorKind::TimedOut,
375        StartupError::PipeNotFound { .. } | StartupError::PipeNoData { .. } => {
376            io::ErrorKind::NotFound
377        }
378        StartupError::Mutex { source, .. } | StartupError::PipeIo { source, .. } => source.kind(),
379        StartupError::Launcher { source } => source.kind(),
380    }
381}
382
383#[cfg(not(any(unix, windows)))]
384fn ensure_server_running_polling(
385    socket_path: &Path,
386    config: AutoStartConfig,
387) -> Result<Connection, AutoStartError> {
388    if config.loads_startup_config() {
389        return ensure_server_running_with_probe(
390            socket_path,
391            AUTO_START_TIMEOUT,
392            POLL_INTERVAL,
393            || crate::connect_or_absent(socket_path),
394            || launch_hidden_daemon(socket_path, &config),
395            |_| Ok(()),
396        );
397    }
398
399    ensure_server_running_with(
400        socket_path,
401        AUTO_START_TIMEOUT,
402        POLL_INTERVAL,
403        || crate::connect_or_absent(socket_path),
404        || launch_hidden_daemon(socket_path, &config),
405    )
406}
407
408/// Errors raised while auto-starting or connecting to the RMUX server.
409#[derive(Debug)]
410pub enum AutoStartError {
411    /// The client transport failed before or during readiness polling.
412    Client(ClientError),
413    /// Resolving the `rmux` binary path failed.
414    BinaryPath(io::Error),
415    /// Re-executing the hidden daemon process failed.
416    Launch {
417        /// The binary path that failed to spawn.
418        path: PathBuf,
419        /// The underlying process-spawn error.
420        error: io::Error,
421    },
422    /// A running daemon speaks an incompatible protocol version.
423    IncompatibleDaemon {
424        /// The socket path hosting the incompatible daemon.
425        socket_path: PathBuf,
426        /// Human-readable protocol mismatch detail.
427        message: String,
428    },
429    /// The socket never became reachable before the readiness deadline.
430    TimedOut {
431        /// The socket path that never became reachable.
432        socket_path: PathBuf,
433        /// The amount of time spent polling.
434        waited: Duration,
435    },
436}
437
438impl fmt::Display for AutoStartError {
439    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
440        match self {
441            Self::Client(error) => write!(formatter, "{error}"),
442            Self::BinaryPath(error) => {
443                write!(formatter, "failed to resolve rmux binary path: {error}")
444            }
445            Self::Launch { path, error } => {
446                write!(
447                    formatter,
448                    "failed to launch hidden rmux daemon '{}': {error}",
449                    path.display()
450                )
451            }
452            Self::IncompatibleDaemon {
453                socket_path,
454                message,
455            } => write!(
456                formatter,
457                "{message} on '{}'; detach existing clients, then run `rmux -S {} kill-server` before retrying",
458                socket_path.display(),
459                shell_quote_path(socket_path)
460            ),
461            Self::TimedOut {
462                socket_path,
463                waited,
464            } => write!(
465                formatter,
466                "timed out after {}s waiting for rmux server socket '{}'",
467                waited.as_secs(),
468                socket_path.display()
469            ),
470        }
471    }
472}
473
474impl std::error::Error for AutoStartError {
475    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
476        match self {
477            Self::Client(error) => Some(error),
478            Self::BinaryPath(error) => Some(error),
479            Self::Launch { error, .. } => Some(error),
480            Self::IncompatibleDaemon { .. } => None,
481            Self::TimedOut { .. } => None,
482        }
483    }
484}
485
486impl From<ClientError> for AutoStartError {
487    fn from(error: ClientError) -> Self {
488        Self::Client(error)
489    }
490}
491
492#[cfg(not(any(unix, windows)))]
493fn ensure_server_running_with<ConnectFn, LaunchFn>(
494    socket_path: &Path,
495    timeout: Duration,
496    poll_interval: Duration,
497    connect: ConnectFn,
498    launch: LaunchFn,
499) -> Result<Connection, AutoStartError>
500where
501    ConnectFn: FnMut() -> Result<ConnectResult, ClientError>,
502    LaunchFn: FnMut() -> Result<(), AutoStartError>,
503{
504    ensure_server_running_with_probe(
505        socket_path,
506        timeout,
507        poll_interval,
508        connect,
509        launch,
510        probe_server_readiness,
511    )
512}
513
514#[cfg(any(all(test, unix), not(any(unix, windows))))]
515fn ensure_server_running_with_probe<ConnectFn, LaunchFn, ProbeFn>(
516    socket_path: &Path,
517    timeout: Duration,
518    poll_interval: Duration,
519    mut connect: ConnectFn,
520    mut launch: LaunchFn,
521    mut probe: ProbeFn,
522) -> Result<Connection, AutoStartError>
523where
524    ConnectFn: FnMut() -> Result<ConnectResult, ClientError>,
525    LaunchFn: FnMut() -> Result<(), AutoStartError>,
526    ProbeFn: FnMut(&mut Connection) -> Result<(), ClientError>,
527{
528    match connect().map_err(AutoStartError::Client)? {
529        ConnectResult::Connected(mut connection) => {
530            probe(&mut connection).map_err(AutoStartError::Client)?;
531            return Ok(connection);
532        }
533        ConnectResult::Absent => {}
534    }
535
536    launch()?;
537    wait_for_server(
538        socket_path,
539        timeout,
540        poll_interval,
541        &mut connect,
542        &mut probe,
543    )
544}
545
546#[cfg(any(all(test, unix), not(any(unix, windows))))]
547fn wait_for_server<ConnectFn, ProbeFn>(
548    socket_path: &Path,
549    timeout: Duration,
550    poll_interval: Duration,
551    connect: &mut ConnectFn,
552    probe: &mut ProbeFn,
553) -> Result<Connection, AutoStartError>
554where
555    ConnectFn: FnMut() -> Result<ConnectResult, ClientError>,
556    ProbeFn: FnMut(&mut Connection) -> Result<(), ClientError>,
557{
558    let start = Instant::now();
559    let deadline = start + timeout;
560
561    loop {
562        match connect() {
563            Ok(crate::ConnectResult::Connected(mut connection)) => match probe(&mut connection) {
564                Ok(()) => return Ok(connection),
565                Err(error) if is_transient_connect_error(&error) => {}
566                Err(error) => return Err(AutoStartError::Client(error)),
567            },
568            Ok(crate::ConnectResult::Absent) => {}
569            Err(error) if is_transient_connect_error(&error) => {}
570            Err(error) => return Err(AutoStartError::Client(error)),
571        }
572
573        let now = Instant::now();
574        if now >= deadline {
575            return Err(AutoStartError::TimedOut {
576                socket_path: socket_path.to_path_buf(),
577                waited: timeout,
578            });
579        }
580
581        std::thread::sleep(poll_interval.min(deadline.saturating_duration_since(now)));
582    }
583}
584
585fn is_transient_connect_error(error: &ClientError) -> bool {
586    matches!(
587        error,
588        ClientError::Io(io_error)
589            if matches!(
590                io_error.kind(),
591                io::ErrorKind::WouldBlock
592                    | io::ErrorKind::Interrupted
593                    | io::ErrorKind::TimedOut
594            )
595    )
596}
597
598fn probe_server_readiness(connection: &mut Connection) -> Result<(), ClientError> {
599    let response = connection.daemon_status()?;
600    match response {
601        Response::DaemonStatus(status) if status.config_loading => {
602            Err(ClientError::Io(io::Error::new(
603                io::ErrorKind::WouldBlock,
604                "daemon is still loading startup config",
605            )))
606        }
607        Response::DaemonStatus(_) => Ok(()),
608        Response::Error(_) => Ok(()),
609        other => Err(ClientError::Protocol(rmux_proto::RmuxError::Server(
610            format!("unexpected readiness response: {other:?}"),
611        ))),
612    }
613}
614
615#[cfg(not(any(unix, windows)))]
616fn launch_hidden_daemon(
617    socket_path: &Path,
618    config: &AutoStartConfig,
619) -> Result<(), AutoStartError> {
620    let binary_path = rmux_binary_path(config).map_err(AutoStartError::BinaryPath)?;
621    spawn_hidden_daemon_for(&binary_path, socket_path, config).map_err(|error| {
622        AutoStartError::Launch {
623            path: binary_path,
624            error,
625        }
626    })
627}
628
629fn spawn_hidden_daemon_for(
630    binary_path: &Path,
631    socket_path: &Path,
632    config: &AutoStartConfig,
633) -> io::Result<()> {
634    let command = hidden_daemon_command(binary_path, socket_path, config, true);
635    match spawn_hidden_daemon(command) {
636        Ok(()) => Ok(()),
637        Err(error) if rmux_os::daemon::should_retry_hidden_daemon_without_breakaway(&error) => {
638            let command = hidden_daemon_command(binary_path, socket_path, config, false);
639            spawn_hidden_daemon(command)
640        }
641        Err(error) => Err(error),
642    }
643}
644
645fn hidden_daemon_command(
646    binary_path: &Path,
647    socket_path: &Path,
648    config: &AutoStartConfig,
649    allow_job_breakaway: bool,
650) -> Command {
651    let mut command = Command::new(binary_path);
652    command
653        .arg(INTERNAL_DAEMON_FLAG)
654        .arg(socket_path)
655        .stdin(Stdio::null())
656        .stdout(Stdio::null())
657        .stderr(Stdio::null());
658    config.append_hidden_daemon_args(&mut command);
659    rmux_os::daemon::configure_hidden_daemon_command(&mut command, allow_job_breakaway);
660    command
661}
662
663fn spawn_hidden_daemon(mut command: Command) -> io::Result<()> {
664    let child = rmux_os::daemon::spawn_hidden_daemon_command(&mut command)?;
665    // Intentionally drop without `wait()`: the daemon must outlive the
666    // short-lived client process that launched it.
667    drop(child);
668    Ok(())
669}
670
671fn rmux_binary_path(config: &AutoStartConfig) -> io::Result<PathBuf> {
672    let current_exe = env::current_exe()?;
673    match env::var_os(BINARY_OVERRIDE_ENV).filter(|_| binary_override_enabled_for_tests()) {
674        Some(path) => Ok(PathBuf::from(path)),
675        None => {
676            Ok(hidden_daemon_binary_path_for_config(&current_exe, config).unwrap_or(current_exe))
677        }
678    }
679}
680
681fn binary_override_enabled_for_tests() -> bool {
682    cfg!(debug_assertions)
683        && env::var_os(BINARY_OVERRIDE_TEST_OPT_IN_ENV).is_some_and(|value| value == "1")
684}
685
686#[cfg(all(test, unix))]
687fn hidden_daemon_binary_path(current_exe: &Path) -> Option<PathBuf> {
688    hidden_daemon_binary_path_for_config(current_exe, &AutoStartConfig::disabled())
689}
690
691fn hidden_daemon_binary_path_for_config(
692    current_exe: &Path,
693    config: &AutoStartConfig,
694) -> Option<PathBuf> {
695    if config.web_required {
696        return None;
697    }
698    let file_stem = current_exe.file_stem()?.to_str()?;
699    if file_stem == "rmux-daemon" {
700        return None;
701    }
702
703    let mut candidate = current_exe.to_path_buf();
704    let daemon_file_name = match current_exe
705        .extension()
706        .and_then(|extension| extension.to_str())
707    {
708        Some(extension) if !extension.is_empty() => format!("rmux-daemon.{extension}"),
709        _ => "rmux-daemon".to_owned(),
710    };
711    candidate.set_file_name(daemon_file_name);
712    candidate.is_file().then_some(candidate)
713}
714
715#[cfg(all(test, unix))]
716#[path = "auto_start/tests.rs"]
717mod tests;