Skip to main content

rmux_client/commands/
server.rs

1use std::fmt;
2use std::path::Path;
3#[cfg(windows)]
4use std::path::PathBuf;
5
6use rmux_proto::{
7    DaemonStatusRequest, KillServerRequest, LockClientRequest, LockServerRequest,
8    LockSessionRequest, Request, Response, ServerAccessRequest, SessionName, ShutdownIfIdleRequest,
9};
10
11use crate::{
12    auto_start::{ensure_server_running_with_config, AutoStartConfig, AutoStartError},
13    connection::{connect, Connection},
14    ClientError,
15};
16
17/// Connects to the exact endpoint eligible for a `kill-server` request.
18///
19/// On Windows, a pre-rotation daemon may still own the static endpoint while
20/// the current client has resolved a private managed generation. Only an
21/// absent managed endpoint may fall back to the legacy endpoint authenticated
22/// by the private discovery record. Ordinary commands never use this path.
23#[cfg(windows)]
24pub fn connect_for_server_shutdown(
25    socket_path: &Path,
26) -> Result<(Connection, PathBuf), ClientError> {
27    let primary_error = match connect(socket_path) {
28        Ok(connection) => return Ok((connection, socket_path.to_path_buf())),
29        Err(error) if shutdown_endpoint_is_absent(&error) => error,
30        Err(error) => return Err(error),
31    };
32    let Some(legacy_endpoint) = rmux_ipc::legacy_shutdown_endpoint(socket_path)? else {
33        return Err(primary_error);
34    };
35    let legacy_path = legacy_endpoint.into_path();
36    connect(&legacy_path).map(|connection| (connection, legacy_path))
37}
38
39#[cfg(windows)]
40fn shutdown_endpoint_is_absent(error: &ClientError) -> bool {
41    matches!(
42        error,
43        ClientError::Io(error)
44            if matches!(
45                error.kind(),
46                std::io::ErrorKind::NotFound | std::io::ErrorKind::ConnectionRefused
47            )
48    )
49}
50
51impl Connection {
52    /// Ensures the server is available, honouring top-level no-start-server behavior.
53    pub fn start_server(
54        socket_path: &Path,
55        no_start_server: bool,
56        config: AutoStartConfig,
57    ) -> Result<Self, StartServerError> {
58        if no_start_server {
59            return connect(socket_path).map_err(StartServerError::Client);
60        }
61
62        ensure_server_running_with_config(socket_path, config).map_err(StartServerError::AutoStart)
63    }
64
65    /// Sends a `kill-server` request over the detached RPC channel.
66    pub fn kill_server(&mut self) -> Result<Response, ClientError> {
67        self.roundtrip(&Request::KillServer(KillServerRequest))
68    }
69
70    /// Sends a `kill-server` request and returns after the frame is written.
71    ///
72    /// Windows package clients use this because the daemon can close its reply
73    /// pipe during shutdown. Callers must then drop this connection and wait
74    /// for endpoint release with [`crate::wait_for_server_endpoint_cleanup`].
75    pub fn kill_server_after_write(&mut self) -> Result<(), ClientError> {
76        self.write_request(&Request::KillServer(KillServerRequest))
77    }
78
79    /// Sends a legacy-wire `kill-server` request for stale daemon cleanup.
80    pub fn kill_server_legacy_wire(&mut self, wire_version: u32) -> Result<(), ClientError> {
81        self.write_legacy_wire_request(&Request::KillServer(KillServerRequest), wire_version)
82    }
83
84    /// Sends an internal daemon status request over the detached RPC channel.
85    pub fn daemon_status(&mut self) -> Result<Response, ClientError> {
86        self.roundtrip(&Request::DaemonStatus(DaemonStatusRequest))
87    }
88
89    /// Sends an internal idle-only daemon shutdown request.
90    pub fn shutdown_if_idle(&mut self) -> Result<Response, ClientError> {
91        self.roundtrip(&Request::ShutdownIfIdle(ShutdownIfIdleRequest))
92    }
93
94    /// Sends a `lock-server` request over the detached RPC channel.
95    pub fn lock_server(&mut self) -> Result<Response, ClientError> {
96        self.roundtrip(&Request::LockServer(LockServerRequest))
97    }
98
99    /// Sends a `lock-session` request over the detached RPC channel.
100    pub fn lock_session(&mut self, target: SessionName) -> Result<Response, ClientError> {
101        self.roundtrip(&Request::LockSession(LockSessionRequest { target }))
102    }
103
104    /// Sends a `lock-client` request over the detached RPC channel.
105    pub fn lock_client(&mut self, target_client: String) -> Result<Response, ClientError> {
106        self.roundtrip(&Request::LockClient(LockClientRequest { target_client }))
107    }
108
109    /// Sends a `server-access` request over the detached RPC channel.
110    pub fn server_access(&mut self, request: ServerAccessRequest) -> Result<Response, ClientError> {
111        self.roundtrip(&Request::ServerAccess(request))
112    }
113}
114
115/// Client-side `start-server` failure surface.
116#[derive(Debug)]
117pub enum StartServerError {
118    /// Connecting to an already-running server failed.
119    Client(ClientError),
120    /// Auto-starting the server failed.
121    AutoStart(AutoStartError),
122}
123
124impl fmt::Display for StartServerError {
125    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
126        match self {
127            Self::Client(error) => fmt::Display::fmt(error, formatter),
128            Self::AutoStart(error) => fmt::Display::fmt(error, formatter),
129        }
130    }
131}
132
133impl std::error::Error for StartServerError {
134    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
135        match self {
136            Self::Client(error) => Some(error),
137            Self::AutoStart(error) => Some(error),
138        }
139    }
140}