Skip to main content

rmux_client/
lib.rs

1#![deny(missing_docs)]
2
3//! Blocking local client for the RMUX detached RPC protocol.
4//!
5//! This crate provides the transport layer for sending [`rmux_proto::Request`]
6//! frames and receiving [`rmux_proto::Response`] frames over a blocking
7//! local stream. It also exposes nested-session detection through the `$RMUX`
8//! environment variable and raw-terminal lifecycle management for attach-mode
9//! clients.
10
11#[cfg(unix)]
12pub mod attach;
13#[cfg(windows)]
14#[path = "attach_windows.rs"]
15pub mod attach;
16pub mod auto_start;
17pub(crate) mod commands;
18pub mod connection;
19pub mod control;
20pub mod nested;
21
22#[cfg(unix)]
23pub use attach::attach_terminal_with_initial_bytes_and_resize_geometry;
24pub use attach::{
25    attach_terminal, attach_terminal_with_initial_bytes, attach_with_terminal, drive_attach_stream,
26    AttachError, RawTerminal,
27};
28pub use auto_start::{
29    ensure_server_running, ensure_server_running_with_config, AutoStartConfig,
30    AutoStartConfigSelection, AutoStartError, INTERNAL_DAEMON_FLAG,
31};
32pub use commands::server::StartServerError;
33pub use commands::window::SplitWindowOptions;
34pub use connection::{
35    connect, connect_or_absent, default_socket_path, resolve_socket_path, socket_path_for_label,
36    AttachSessionUpgrade, AttachTransition, ConnectResult, Connection, ControlModeUpgrade,
37    ControlTransition,
38};
39pub use control::{drive_control_mode, drive_control_mode_with_stdio};
40pub use nested::{
41    detect_context, ensure_nested_context, require_nested_context, ClientContext,
42    NestedContextError,
43};
44
45use rmux_proto::RmuxError;
46use std::fmt;
47
48/// Client-side errors for transport and protocol failures.
49#[derive(Debug)]
50pub enum ClientError {
51    /// An I/O error occurred on the local client stream.
52    Io(std::io::Error),
53    /// A protocol framing or encoding error occurred.
54    Protocol(RmuxError),
55    /// Entering or restoring raw terminal mode failed.
56    Attach(AttachError),
57    /// The server closed the connection before sending a complete response frame.
58    UnexpectedEof,
59}
60
61impl fmt::Display for ClientError {
62    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            Self::Io(error) => write!(formatter, "i/o error: {error}"),
65            Self::Protocol(error) => write!(formatter, "protocol error: {error}"),
66            Self::Attach(error) => write!(formatter, "attach error: {error}"),
67            Self::UnexpectedEof => formatter
68                .write_str("server closed connection before a complete response frame arrived"),
69        }
70    }
71}
72
73impl std::error::Error for ClientError {
74    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
75        match self {
76            Self::Io(error) => Some(error),
77            Self::Protocol(error) => Some(error),
78            Self::Attach(error) => Some(error),
79            Self::UnexpectedEof => None,
80        }
81    }
82}
83
84impl From<std::io::Error> for ClientError {
85    fn from(error: std::io::Error) -> Self {
86        Self::Io(error)
87    }
88}
89
90impl From<RmuxError> for ClientError {
91    fn from(error: RmuxError) -> Self {
92        Self::Protocol(error)
93    }
94}
95
96impl From<AttachError> for ClientError {
97    fn from(error: AttachError) -> Self {
98        Self::Attach(error)
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use std::error::Error as _;
105    use std::io;
106
107    use super::{AttachError, ClientError};
108
109    #[test]
110    fn client_error_wraps_attach_errors() {
111        let error = ClientError::from(AttachError::Io(io::Error::other("dup failed")));
112
113        assert!(
114            matches!(error, ClientError::Attach(AttachError::Io(_))),
115            "attach errors should preserve their variant information"
116        );
117        assert_eq!(
118            error.to_string(),
119            expected_attach_error_display("dup failed")
120        );
121        assert!(
122            error.source().is_some(),
123            "wrapped attach error should chain"
124        );
125    }
126
127    #[cfg(unix)]
128    fn expected_attach_error_display(message: &str) -> String {
129        format!("attach error: terminal descriptor operation failed: {message}")
130    }
131
132    #[cfg(windows)]
133    fn expected_attach_error_display(message: &str) -> String {
134        format!("attach error: terminal console operation failed: {message}")
135    }
136
137    #[cfg(not(any(unix, windows)))]
138    fn expected_attach_error_display(message: &str) -> String {
139        format!("attach error: terminal descriptor operation failed: {message}")
140    }
141}