# Daemon Lifecycle: Start, Attach, and Re-Exec

> When a tmux-style command (e.g. `new-session -d`) finds no running daemon, `rmux-client`'s `auto_start` module re-execs the current binary with the hidden flag `--__internal-daemon` plus an optional socket path and config-forwarding flags. `src/main.rs` detects this flag and branches into `run_hidden_daemon`, which builds a `DaemonConfig`, binds a `ServerDaemon`, and blocks on `server.wait()`. On Unix this runs a single-thread Tokio runtime; on Windows it uses a multi-thread runtime with a floor of 4 workers. The socket path defaults to a per-user directory derived from `$RMUX_TMPDIR` or `/tmp`; the `$RMUX` env var lets nested clients detect they are already inside a session and short-circuit.

- Repository: Helvesec/rmux
- GitHub: https://github.com/Helvesec/rmux
- Human wiki: https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181
- Complete Markdown: https://grok-wiki.com/public/wiki/helvesec-rmux-ea7220d1f181/llms-full.txt

## Source Files

- `src/main.rs`
- `crates/rmux-client/src/auto_start.rs`
- `crates/rmux-server/src/daemon.rs`
- `crates/rmux-ipc/src/endpoint.rs`
- `crates/rmux-client/src/nested.rs`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [src/main.rs](src/main.rs)
- [crates/rmux-client/src/auto_start.rs](crates/rmux-client/src/auto_start.rs)
- [crates/rmux-server/src/daemon.rs](crates/rmux-server/src/daemon.rs)
- [crates/rmux-ipc/src/endpoint.rs](crates/rmux-ipc/src/endpoint.rs)
- [crates/rmux-client/src/nested.rs](crates/rmux-client/src/nested.rs)
- [crates/rmux-server/src/unix_socket.rs](crates/rmux-server/src/unix_socket.rs)
- [crates/rmux-server/src/signals.rs](crates/rmux-server/src/signals.rs)
- [crates/rmux-sdk/src/bootstrap/startup_unix.rs](crates/rmux-sdk/src/bootstrap/startup_unix.rs)
</details>

# Daemon Lifecycle: Start, Attach, and Re-Exec

`rmux` ships as a single binary with two personalities: the public client CLI and a hidden long-running daemon. When a tmux-style command such as `new-session -d` needs a running server and finds none, the client re-executes itself with the hidden flag `--__internal-daemon`. `src/main.rs` detects this flag at startup and branches into the daemon path instead of the normal CLI path, eliminating the need for a separate server binary.

This page explains how the two personalities co-exist in one binary, how `auto_start` decides to spawn the daemon, what arguments are forwarded across the re-exec boundary, how the Tokio runtime is configured per platform, how the socket path is resolved, and how the `$RMUX` environment variable lets nested clients short-circuit the whole flow.

---

## The Single-Binary Dual-Entry Design

`src/main.rs` owns both entry points. The routing decision in `try_main` is a single `match` on the second command-line argument:

```rust
// src/main.rs:65-75
match args.get(1) {
    Some(argument) if argument == INTERNAL_DAEMON_FLAG => {
        let internal = parse_internal_daemon_args(args.into_iter().skip(2))
            .map_err(|error| cli::ExitFailure::new(1, error))?;
        run_hidden_daemon(internal)
            ...
    }
    _ => cli::run(args),
}
```

`INTERNAL_DAEMON_FLAG` is the string `"--__internal-daemon"`, defined in `rmux-client` and re-exported so both sides of the protocol share the exact constant:

```rust
// crates/rmux-client/src/auto_start.rs:42
pub const INTERNAL_DAEMON_FLAG: &str = "--__internal-daemon";
```

Sources: [src/main.rs:65-75](), [crates/rmux-client/src/auto_start.rs:42]()

---

## When Auto-Start Triggers

`ensure_server_running` (and its config-forwarding variant `ensure_server_running_with_config`) is explicitly reserved for commands that map to tmux's `CMD_STARTSERVER` inventory — commands like `new-session -d`, `start-server`, and `new-window`. Other commands use `connect` or `connect_or_absent` directly, so they never spawn a daemon as a side effect.

```rust
// crates/rmux-client/src/auto_start.rs:130-132
pub fn ensure_server_running(socket_path: &Path) -> Result<Connection, AutoStartError> {
    ensure_server_running_with_config(socket_path, AutoStartConfig::disabled())
}
```

Sources: [crates/rmux-client/src/auto_start.rs:124-132]()

---

## The Re-Exec Protocol

### Spawning the Hidden Daemon

`spawn_hidden_daemon_for` builds the child `Command`, then immediately drops the `Child` handle without waiting, so the daemon outlives the short-lived client process:

```rust
// crates/rmux-client/src/auto_start.rs:557-597
fn spawn_hidden_daemon_for(binary_path, socket_path, config) -> io::Result<()> {
    let command = hidden_daemon_command(binary_path, socket_path, config, true);
    match spawn_hidden_daemon(command) {
        Ok(()) => Ok(()),
        Err(error) if should_retry_hidden_daemon_without_breakaway(&error) => {
            let command = hidden_daemon_command(binary_path, socket_path, config, false);
            spawn_hidden_daemon(command)
        }
        Err(error) => Err(error),
    }
}

fn spawn_hidden_daemon(mut command: Command) -> io::Result<()> {
    let child = command.spawn()?;
    drop(child);  // intentional: daemon must outlive the client
    Ok(())
}
```

The child command is always constructed as:

```
rmux --__internal-daemon <socket_path> [--config-default | --config-file <path>]... [--config-quiet] [--config-cwd <path>]
```

stdin, stdout, and stderr are all redirected to `/dev/null` (`Stdio::null()`).

On Windows, if the initial spawn fails with a specific error, `rmux_os::daemon::should_retry_hidden_daemon_without_breakaway` triggers a retry without job-object breakaway — this is a Windows process group inheritance workaround.

Sources: [crates/rmux-client/src/auto_start.rs:557-597]()

### Binary Path Resolution

The re-exec target is always `env::current_exe()`. In debug builds with the `RMUX_ALLOW_INTERNAL_BINARY_OVERRIDE=1` environment variable set, the `RMUX_INTERNAL_BINARY_PATH` env var overrides the path — this is a test-only escape hatch:

```rust
// crates/rmux-client/src/auto_start.rs:599-610
fn rmux_binary_path() -> io::Result<PathBuf> {
    let current_exe = env::current_exe()?;
    match env::var_os(BINARY_OVERRIDE_ENV).filter(|_| binary_override_enabled_for_tests()) {
        Some(path) => Ok(PathBuf::from(path)),
        None => Ok(current_exe),
    }
}
```

Sources: [crates/rmux-client/src/auto_start.rs:599-610]()

### Argument Parsing in the Daemon

`parse_internal_daemon_args` in `src/main.rs` is a simple hand-written parser (no clap). The first non-`--` argument is treated as the socket path. Remaining `--` arguments carry config-forwarding flags:

| Flag | Effect |
|---|---|
| `<path>` (positional) | Explicit socket path for this daemon instance |
| `--config-default` | Load rmux's default startup config search path |
| `--config-file <path>` | Load this explicit config file (may repeat) |
| `--config-quiet` | Suppress "file not found" warnings |
| `--config-cwd <path>` | Working directory for relative config paths |

`--config-default` and `--config-file` are mutually exclusive; duplicate `--config-default` also errors out. These flags are constructed by `AutoStartConfig::append_hidden_daemon_args` on the client side.

Sources: [src/main.rs:94-183](), [crates/rmux-client/src/auto_start.rs:91-110]()

---

## Running the Daemon: `run_hidden_daemon`

Once `parse_internal_daemon_args` succeeds, `run_hidden_daemon` builds a `DaemonConfig`, selects the Tokio runtime, and blocks:

```rust
// src/main.rs:185-211
fn run_hidden_daemon(args: InternalDaemonArgs) -> io::Result<()> {
    let mut config = match args.socket_path {
        Some(socket_path) => DaemonConfig::new(socket_path),
        None => DaemonConfig::with_default_socket_path()?,
    };
    config = match args.config_selection {
        ServerConfigFileSelection::Disabled => config,
        ServerConfigFileSelection::Default => {
            config.with_default_config_load(args.config_quiet, args.config_cwd)
        }
        ServerConfigFileSelection::Files(files) => {
            config.with_config_files(files, args.config_quiet, args.config_cwd)
        }
    };
    #[cfg(unix)]
    let runtime = Builder::new_current_thread().enable_all().build()?;
    #[cfg(windows)]
    let runtime = Builder::new_multi_thread()
        .worker_threads(hidden_daemon_worker_threads())
        .enable_all()
        .build()?;

    runtime.block_on(async move {
        let server = ServerDaemon::new(config).bind().await?;
        server.wait().await
    })
}
```

Sources: [src/main.rs:185-211]()

### Platform Runtime Differences

| Platform | Runtime type | Thread count |
|---|---|---|
| Unix | `current_thread` (single-threaded) | 1 |
| Windows | `multi_thread` | `max(available_parallelism, 4)` |

The Windows floor of 4 workers is enforced by:

```rust
// src/main.rs:214-219
fn hidden_daemon_worker_threads() -> usize {
    std::thread::available_parallelism()
        .map(usize::from)
        .unwrap_or(4)
        .max(4)
}
```

Sources: [src/main.rs:199-219]()

---

## Daemon Bind and Shutdown Lifecycle

`ServerDaemon::bind` creates the IPC endpoint, wires up shutdown signaling, and spawns the accept loop as a Tokio task:

```rust
// crates/rmux-server/src/daemon.rs:232-293
pub async fn bind(self) -> io::Result<ServerHandle> {
    // Unix path:
    let bound_listener = bind_unix_listener_at(self.config.socket_path())?;
    let (shutdown_handle, shutdown_receiver) = ShutdownHandle::new();
    let (server_signal_tx, server_signal_rx) = tokio::sync::mpsc::unbounded_channel();
    let signal_watcher = SignalWatcher::install(shutdown_handle.clone(), server_signal_tx)?;
    let task = tokio::spawn(listener::serve(...));
    Ok(ServerHandle { socket_path, shutdown_handle, task, signal_watcher })
}
```

`ServerHandle::wait` (called in `run_hidden_daemon`) simply awaits the `JoinHandle`. The daemon stays alive until the task completes — which only happens after a shutdown signal fires.

Sources: [crates/rmux-server/src/daemon.rs:232-293](), [crates/rmux-server/src/daemon.rs:384-390]()

### Signal Handling (Unix)

`SignalWatcher` runs a dedicated OS thread (not a Tokio task) that blocks on `Signals::forever()`. Signal disposition:

| Signal | Action |
|---|---|
| `SIGINT`, `SIGTERM` | `shutdown_handle.request_shutdown()` → daemon exits |
| `SIGCHLD` | Sends `ServerSignal::ChildChanged` to server |
| `SIGUSR1` | Sends `ServerSignal::RecreateSocket` to server |
| `SIGHUP`, `SIGQUIT`, `SIGUSR2` | Logged and ignored |

Dropping `SignalWatcher` closes the signal iterator handle and joins the thread.

Sources: [crates/rmux-server/src/signals.rs:19-94]()

### Shutdown Propagation

`ShutdownHandle` wraps a `oneshot::Sender<()>` behind `Arc<Mutex<Option<...>>>` so any clone can fire shutdown exactly once:

```rust
// crates/rmux-server/src/daemon.rs:207-221
pub(crate) fn request_shutdown(&self) {
    if let Some(sender) = self.sender.lock().expect("shutdown sender").take() {
        let _ = sender.send(());
    }
}
```

`Drop for ServerHandle` calls `request_shutdown` automatically, so tests or out-of-band drops cannot leave a zombie server.

Sources: [crates/rmux-server/src/daemon.rs:207-221](), [crates/rmux-server/src/daemon.rs:412-416]()

---

## Socket Path Resolution

### Default Path Computation

When no socket path is supplied to the daemon, `DaemonConfig::with_default_socket_path` calls `default_socket_path`, which calls `rmux_ipc::default_endpoint`:

```rust
// crates/rmux-server/src/daemon.rs:46-47
pub fn default_socket_path() -> io::Result<PathBuf> {
    rmux_ipc::default_endpoint().map(LocalEndpoint::into_path)
}
```

On Unix, the endpoint is computed as:

```
<root>/rmux-<uid>/default
```

Where `<root>` is `$RMUX_TMPDIR` if set and resolvable, falling back to `/tmp`:

```rust
// crates/rmux-ipc/src/endpoint.rs:81-84
fn endpoint_for_label_impl(label: &OsStr) -> io::Result<LocalEndpoint> {
    let user_id = rmux_os::identity::real_user_id();
    endpoint_from_parts(std::env::var_os(RMUX_TMPDIR_ENV).as_deref(), user_id, label)
}
```

On Windows, the endpoint is a named pipe:

```
\\.\pipe\rmux-<SID>-il-<integrity>-<label>
```

Sources: [crates/rmux-ipc/src/endpoint.rs:71-114](), [crates/rmux-server/src/daemon.rs:89-91]()

### Socket Ownership Validation

On Unix, `bind_unix_listener_at` in `rmux-server` prepares the socket path, creates the parent directory with mode `0o700` if needed, removes a stale socket, and enforces `0o600` permissions on the bound socket file. Symlinks on any path component are rejected.

On Windows, if `LocalListener::bind` fails and a protocol probe succeeds on the existing pipe, the error is promoted to `AddrInUse` with a human-readable message.

Sources: [crates/rmux-server/src/unix_socket.rs:25-45](), [crates/rmux-server/src/daemon.rs:297-323]()

---

## Nested-Session Detection via `$RMUX`

The `$RMUX` environment variable is the mechanism clients use to detect they are already running inside an rmux session. This allows commands like `switch-client` to require a nested context, and prevents spurious daemon spawning from nested shells.

```rust
// crates/rmux-client/src/nested.rs:40-66
pub fn detect_context() -> ClientContext {
    detect_context_from_env(std::env::var_os("RMUX").as_deref())
}

fn detect_context_from_env(tmux_value: Option<&OsStr>) -> ClientContext {
    match tmux_value {
        Some(value) if !value.is_empty() => ClientContext::Nested,
        _ => ClientContext::Outside,
    }
}
```

Any non-empty `$RMUX` value means the client is nested. The variable is typically set by the server when it spawns a shell inside a pane, with the format `<socket_path>,<session_id>,<client_id>` (e.g. `/tmp/rmux-1000/default,12345,0`) — but the detection logic only checks emptiness, so the format is not validated client-side.

`$RMUX` is also read by `resolve_endpoint` in `rmux-ipc`: if no `-L` or `-S` flag is given, `socket_path_from_rmux_env` parses the path component before the first comma and uses it as the endpoint, provided it is in an `rmux-`-owned directory:

```rust
// crates/rmux-ipc/src/endpoint.rs:176-189
fn socket_path_from_rmux_env(rmux: Option<&OsStr>) -> Option<PathBuf> {
    let end = bytes.iter().position(|byte| *byte == b',').unwrap_or(bytes.len());
    let path = path_buf_from_bytes(bytes[..end].to_vec());
    socket_path_is_rmux_owned(&path).then_some(path)
}
```

Sources: [crates/rmux-client/src/nested.rs:40-66](), [crates/rmux-ipc/src/endpoint.rs:117-135](), [crates/rmux-ipc/src/endpoint.rs:176-189]()

---

## Startup Race Serialization (Unix)

The Unix bootstrap layer in `rmux-sdk` prevents multiple concurrent clients from each trying to start their own daemon. `connect_or_start_with` acquires a per-endpoint `flock` lock before running the launcher; losers of the race either connect to the daemon the winner created, or block until it is ready:

- Default startup deadline: **5 seconds** (`DEFAULT_STARTUP_DEADLINE`)
- Default poll interval: **25 ms** (`STARTUP_POLL_INTERVAL`)

On platforms without the `unix` or `windows` feature gates (i.e. other targets), `auto_start` falls back to a simple polling loop with a 5-second timeout and 50 ms interval.

Sources: [crates/rmux-sdk/src/bootstrap/startup_unix.rs:44-53](), [crates/rmux-client/src/auto_start.rs:33-37]()

---

## End-to-End Sequence

```mermaid
sequenceDiagram
    participant CLI as rmux (client invocation)
    participant AutoStart as auto_start::ensure_server_running
    participant Bootstrap as rmux-sdk bootstrap (connect_or_start_with)
    participant Daemon as rmux --__internal-daemon (re-exec'd child)
    participant Socket as Unix socket / Windows named pipe

    CLI->>AutoStart: CMD_STARTSERVER command
    AutoStart->>Bootstrap: connect_or_start_with(socket_path, launcher, deadline, poll)
    Bootstrap->>Socket: try connect
    Socket-->>Bootstrap: not found (Absent)
    Bootstrap->>Bootstrap: acquire startup flock (Unix)
    Bootstrap->>Daemon: spawn_hidden_daemon_for() → exec self --__internal-daemon <path>
    Bootstrap->>Bootstrap: drop Child handle (daemon lives on)
    Bootstrap->>Socket: poll every 25ms (up to 5s)
    Daemon->>Socket: bind(), accept loop ready
    Socket-->>Bootstrap: connected
    Bootstrap-->>AutoStart: StartupOutcome::Started(stream)
    AutoStart->>AutoStart: probe_server_readiness() (list-sessions ping)
    AutoStart-->>CLI: Ok(Connection)
```

Sources: [crates/rmux-client/src/auto_start.rs:161-196](), [crates/rmux-sdk/src/bootstrap/startup_unix.rs:1-18]()

---

## Summary

The daemon lifecycle is a compact re-exec protocol: a single binary detects `--__internal-daemon` as argv[1] and branches into `run_hidden_daemon` instead of the CLI, building a `DaemonConfig` from forwarded flags, selecting a platform-appropriate Tokio runtime (single-thread on Unix, multi-thread with a ≥4 worker floor on Windows), binding the IPC endpoint, and blocking on `server.wait()`. The socket path defaults to a per-user directory derived from `$RMUX_TMPDIR` or `/tmp`. The `$RMUX` environment variable short-circuits all of this for nested clients by letting `detect_context` return `ClientContext::Nested` whenever the variable is non-empty, preventing spurious daemon spawns from within existing sessions. The full lifecycle — from `ensure_server_running` through flock acquisition, spawn, readiness polling, and graceful SIGTERM shutdown — is defined across `crates/rmux-client/src/auto_start.rs`, `crates/rmux-sdk/src/bootstrap/startup_unix.rs`, `crates/rmux-server/src/daemon.rs`, and `src/main.rs`.
