use std::fmt;
use std::io;
use std::time::Duration;
use crate::CommandSummary;
use crate::version::{ReleaseVersion, TmuxVersion};
mod classification;
mod refusal;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ServerConfigurationErrorKind {
ConflictingSocketSelectors,
InvalidSocketName,
InvalidSocketPath,
InvalidConfigPath,
InvalidColorMode,
WorkingDirectoryUnavailable,
SocketRootUnavailable,
NotInsideTmux,
MalformedTmuxVariable,
}
#[cfg_attr(
feature = "control-mode",
doc = r#"```
use libtmux::{ControlModeErrorKind, Error};
// `Closed` means the far side ended, often just the session going away. The
// other variants distinguish setup failures, deadline expiry, and refusals.
// The enum is `#[non_exhaustive]`, so a caller matches it rather than building
// one.
fn session_ended(failure: &Error) -> bool {
matches!(
failure,
Error::ControlMode { kind: ControlModeErrorKind::Closed, .. },
)
}
let unrelated = libtmux::Server::builder()
.socket_name("named")
.socket_path("/tmp/libtmux-rs-dev/explicit")
.build()
.expect_err("two socket selectors");
assert!(!session_ended(&unrelated));
```"#
)]
#[cfg(feature = "control-mode")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ControlModeErrorKind {
Transport,
MissingPipes,
Closed,
DispatchTimedOut,
TimedOut,
Unread,
UnrepresentableCommand,
InvalidSubscriptionName,
}
pub(crate) const NO_CURRENT_TARGET: &str = "no current target";
pub(crate) const SENSITIVE_OUTPUT_WITHHELD: &str =
"tmux output withheld because the request contained sensitive input";
pub(crate) const NO_SUCH_NEIGHBOUR: [&str; 4] = [
"no next window",
"no previous window",
"no last window",
"no last pane",
];
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum OptionErrorKind {
Unknown,
Ambiguous,
BadValue,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum ServerGoneKind {
NotRunning,
Unreachable,
Lost,
Stopped,
}
impl fmt::Display for ServerGoneKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::NotRunning => "nothing is listening",
Self::Unreachable => "the endpoint is unreachable",
Self::Lost => "the connection was lost",
Self::Stopped => "the server stopped",
})
}
}
pub(crate) const NO_CURRENT_CLIENT: &str = "no current client";
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum ErrorKind {
PartialEffect,
ObjectGone,
Refused,
ServerGone,
Timeout,
Unreachable,
UnsupportedVersion,
InvalidInput,
Transport,
Decode,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub struct IdParseError {
expected_sigil: char,
}
impl IdParseError {
pub(crate) const fn new(expected_sigil: char) -> Self {
Self { expected_sigil }
}
#[must_use]
pub const fn expected_sigil(self) -> char {
self.expected_sigil
}
}
impl fmt::Display for IdParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"invalid tmux ID: expected {} followed by an integer from 0 through {}",
self.expected_sigil,
u32::MAX,
)
}
}
impl std::error::Error for IdParseError {}
#[derive(thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[non_exhaustive]
#[error("tmux accepted an effect in {operation} before a later step failed: {source}")]
AfterEffect {
operation: &'static str,
#[source]
source: Box<Error>,
},
#[non_exhaustive]
#[error("invalid server configuration ({kind:?})")]
InvalidServerConfiguration {
kind: ServerConfigurationErrorKind,
},
#[error("invalid tmux version output")]
InvalidVersionOutput {
output_len: usize,
},
#[error("tmux {found} is below the minimum supported version {minimum}")]
UnsupportedTmuxVersion {
found: TmuxVersion,
minimum: ReleaseVersion,
},
#[error("tmux rejected the option: {detail}")]
OptionRejected {
kind: OptionErrorKind,
detail: String,
},
#[error(
"tmux keeps {option} in {declared:?}, so writing it through a \
{requested:?} handle would land there instead"
)]
OptionScopeMismatch {
option: String,
requested: crate::OptionScope,
declared: &'static [crate::OptionScope],
},
#[non_exhaustive]
#[error("tmux answered {format} with a value that is not an id: {detail}")]
UnreadableFormatValue {
format: &'static str,
detail: IdParseError,
},
#[error("the tmux server was replaced: expected {expected}, found {found}")]
ServerGenerationChanged {
expected: crate::ServerGeneration,
found: crate::ServerGeneration,
},
#[non_exhaustive]
#[error("{command} produced more than {limit} bytes on {stream} (request {request_id})")]
OutputLimitExceeded {
request_id: u64,
command: CommandSummary,
stream: &'static str,
limit: usize,
},
#[non_exhaustive]
#[error(
"work was not admitted: {in_flight} already running is this kind's limit, \
and nothing was sent, so retrying is safe (request {request_id}, {command})"
)]
Overloaded {
request_id: u64,
command: CommandSummary,
in_flight: usize,
},
#[cfg(feature = "control-mode")]
#[non_exhaustive]
#[error("a control-mode {frame} grew past its {limit} byte budget")]
ControlModeFrameTooLarge {
frame: &'static str,
limit: usize,
},
#[error("a session named {name} already exists")]
SessionExists {
name: String,
},
#[error("{capability} needs tmux {needs} or newer, and this is {found}")]
UnsupportedCapability {
capability: &'static str,
needs: ReleaseVersion,
found: TmuxVersion,
},
#[error(
"tmux {found} does not implement {capability} correctly; \
releases from {broken_in} up to but not including {fixed_in} are affected"
)]
CapabilityDefective {
capability: &'static str,
found: TmuxVersion,
broken_in: ReleaseVersion,
fixed_in: ReleaseVersion,
},
#[non_exhaustive]
#[error(
"tmux version probe request {request_id} ({command}) failed with exit code {exit_code:?} and signal {signal:?}"
)]
VersionProbeFailed {
request_id: u64,
command: CommandSummary,
exit_code: Option<i32>,
signal: Option<i32>,
},
#[non_exhaustive]
#[error("invalid {input} for tmux request {request_id}")]
InvalidCommandInput {
request_id: u64,
input: &'static str,
},
#[non_exhaustive]
#[error("{operation} requires handles from the same tmux server endpoint")]
ServerMismatch {
operation: &'static str,
},
#[cfg(feature = "plan")]
#[error("invalid plan: {source}")]
InvalidPlan {
#[source]
source: crate::plan::PlanValidationError,
},
#[non_exhaustive]
#[error("tmux executable was not found for request {request_id} ({command})")]
ExecutableNotFound {
request_id: u64,
command: CommandSummary,
#[source]
source: io::Error,
},
#[non_exhaustive]
#[error("failed to start tmux request {request_id} ({command})")]
Spawn {
request_id: u64,
command: CommandSummary,
#[source]
source: io::Error,
},
#[non_exhaustive]
#[error("failed to read {stream} for tmux request {request_id} ({command})")]
ReadOutput {
request_id: u64,
command: CommandSummary,
stream: &'static str,
kind: io::ErrorKind,
},
#[non_exhaustive]
#[error("failed to wait for tmux request {request_id} ({command})")]
WaitChild {
request_id: u64,
command: CommandSummary,
#[source]
source: io::Error,
},
#[non_exhaustive]
#[error("tmux request {request_id} ({command}) timed out after {timeout:?}")]
Timeout {
request_id: u64,
command: CommandSummary,
timeout: Duration,
},
#[non_exhaustive]
#[error("tmux executor is shut down for request {request_id} ({command})")]
ExecutorShutdown {
request_id: u64,
command: CommandSummary,
},
#[non_exhaustive]
#[error("tmux request {request_id} is already active ({command})")]
DuplicateRequest {
request_id: u64,
command: CommandSummary,
},
#[non_exhaustive]
#[error("tmux supervisor was lost for request {request_id} ({command})")]
SupervisorLost {
request_id: u64,
command: CommandSummary,
},
#[non_exhaustive]
#[error("tmux no longer has {kind} {id}")]
ObjectGone {
kind: ObjectKind,
id: String,
},
#[non_exhaustive]
#[error("tmux has no {kind} at {target}")]
LinkGone {
kind: ObjectKind,
target: String,
},
#[non_exhaustive]
#[error("client {name} is suspended, not gone")]
ClientSuspended {
name: String,
},
#[cfg(feature = "control-mode")]
#[non_exhaustive]
#[error("control mode connection failed ({kind:?})")]
ControlMode {
kind: ControlModeErrorKind,
#[source]
source: Option<io::Error>,
},
#[non_exhaustive]
#[error("could not build a runtime")]
RuntimeUnavailable {
#[source]
source: io::Error,
},
#[error("a blocking runtime cannot be driven from inside an async context")]
RuntimeNested,
#[error("tmux found no server for {command}: {kind}")]
ServerGone {
command: &'static str,
kind: ServerGoneKind,
},
#[non_exhaustive]
#[error("tmux rejected {command} (exit {exit_code:?}): {stderr}")]
CommandFailed {
command: &'static str,
exit_code: Option<i32>,
stderr: String,
},
#[non_exhaustive]
#[error("failed to decode {list_command} output: {detail}")]
DecodeListing {
list_command: &'static str,
detail: ListingDecodeError,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ObjectKind {
Session,
Window,
Pane,
Client,
}
impl fmt::Display for ObjectKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Session => "session",
Self::Window => "window",
Self::Pane => "pane",
Self::Client => "client",
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ListingDecodeError {
inner: crate::formats::FormatCodecError,
}
impl ListingDecodeError {
pub(crate) const fn new(inner: crate::formats::FormatCodecError) -> Self {
Self { inner }
}
#[must_use]
pub const fn row(&self) -> Option<usize> {
self.inner.row()
}
#[must_use]
pub const fn field_name(&self) -> Option<&'static str> {
self.inner.field_name()
}
}
impl fmt::Display for ListingDecodeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.inner.fmt(formatter)
}
}
impl std::error::Error for ListingDecodeError {}
impl Error {
#[cfg(feature = "control-mode")]
pub(crate) const fn control_mode(source: io::Error) -> Self {
Self::ControlMode {
kind: ControlModeErrorKind::Transport,
source: Some(source),
}
}
#[cfg(feature = "control-mode")]
pub(crate) const fn control_mode_pipes() -> Self {
Self::ControlMode {
kind: ControlModeErrorKind::MissingPipes,
source: None,
}
}
#[cfg(feature = "control-mode")]
pub(crate) const fn control_mode_unrepresentable() -> Self {
Self::ControlMode {
kind: ControlModeErrorKind::UnrepresentableCommand,
source: None,
}
}
#[cfg(feature = "control-mode")]
pub(crate) const fn control_mode_frame_too_large(frame: &'static str, limit: usize) -> Self {
Self::ControlModeFrameTooLarge { frame, limit }
}
#[cfg(feature = "control-mode")]
pub(crate) const fn control_mode_unread() -> Self {
Self::ControlMode {
kind: ControlModeErrorKind::Unread,
source: None,
}
}
#[cfg(feature = "control-mode")]
pub(crate) const fn control_mode_invalid_subscription() -> Self {
Self::ControlMode {
kind: ControlModeErrorKind::InvalidSubscriptionName,
source: None,
}
}
#[cfg(feature = "control-mode")]
pub(crate) const fn control_mode_closed() -> Self {
Self::ControlMode {
kind: ControlModeErrorKind::Closed,
source: None,
}
}
#[cfg(feature = "control-mode")]
pub(crate) const fn control_mode_dispatch_timeout() -> Self {
Self::ControlMode {
kind: ControlModeErrorKind::DispatchTimedOut,
source: None,
}
}
#[cfg(feature = "control-mode")]
pub(crate) const fn control_mode_timeout() -> Self {
Self::ControlMode {
kind: ControlModeErrorKind::TimedOut,
source: None,
}
}
#[cfg(feature = "blocking")]
pub(crate) const fn runtime_unavailable(source: io::Error) -> Self {
Self::RuntimeUnavailable { source }
}
pub(crate) const fn invalid_server_configuration(kind: ServerConfigurationErrorKind) -> Self {
Self::InvalidServerConfiguration { kind }
}
pub(crate) fn version_probe_failed(
request_id: u64,
command: CommandSummary,
exit_code: Option<i32>,
signal: Option<i32>,
) -> Self {
Self::VersionProbeFailed {
request_id,
command,
exit_code,
signal,
}
}
pub(crate) fn from_invalid_version_output(output_len: usize) -> Self {
Self::InvalidVersionOutput { output_len }
}
pub(crate) fn unsupported_tmux_version(found: TmuxVersion, minimum: ReleaseVersion) -> Self {
Self::UnsupportedTmuxVersion { found, minimum }
}
pub(crate) fn invalid_command_input(request_id: u64, input: &'static str) -> Self {
Self::InvalidCommandInput { request_id, input }
}
pub(crate) const fn server_mismatch(operation: &'static str) -> Self {
Self::ServerMismatch { operation }
}
pub(crate) fn spawn(
request_id: u64,
command: CommandSummary,
source: io::Error,
executable_not_found: bool,
) -> Self {
if executable_not_found {
Self::ExecutableNotFound {
request_id,
command,
source,
}
} else {
Self::Spawn {
request_id,
command,
source,
}
}
}
pub(crate) fn read_output(
request_id: u64,
command: CommandSummary,
stream: &'static str,
kind: io::ErrorKind,
) -> Self {
Self::ReadOutput {
request_id,
command,
stream,
kind,
}
}
pub(crate) fn wait_child(request_id: u64, command: CommandSummary, source: io::Error) -> Self {
Self::WaitChild {
request_id,
command,
source,
}
}
pub(crate) fn timeout(request_id: u64, command: CommandSummary, timeout: Duration) -> Self {
Self::Timeout {
request_id,
command,
timeout,
}
}
pub(crate) fn executor_shutdown(request_id: u64, command: CommandSummary) -> Self {
Self::ExecutorShutdown {
request_id,
command,
}
}
pub(crate) fn duplicate_request(request_id: u64, command: CommandSummary) -> Self {
Self::DuplicateRequest {
request_id,
command,
}
}
pub(crate) fn supervisor_lost(request_id: u64, command: CommandSummary) -> Self {
Self::SupervisorLost {
request_id,
command,
}
}
#[must_use]
pub fn invalid_version_output_len(&self) -> Option<usize> {
match self {
Self::InvalidVersionOutput { output_len } => Some(*output_len),
_ => None,
}
}
#[must_use]
pub fn found_version(&self) -> Option<&TmuxVersion> {
match self {
Self::UnsupportedTmuxVersion { found, .. }
| Self::UnsupportedCapability { found, .. } => Some(found),
_ => None,
}
}
#[must_use]
pub fn minimum_version(&self) -> Option<&ReleaseVersion> {
match self {
Self::UnsupportedTmuxVersion { minimum, .. } => Some(minimum),
Self::UnsupportedCapability { needs, .. } => Some(needs),
_ => None,
}
}
}
impl fmt::Debug for Error {
#[allow(
clippy::too_many_lines,
reason = "exhaustive safe formatting keeps every public error variant byte-free"
)]
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AfterEffect { operation, source } => formatter
.debug_struct("AfterEffect")
.field("operation", operation)
.field("source", source)
.finish(),
Self::OptionScopeMismatch {
option,
requested,
declared,
} => formatter
.debug_struct("OptionScopeMismatch")
.field("option", option)
.field("requested", requested)
.field("declared", declared)
.finish(),
Self::RuntimeNested => formatter.debug_struct("RuntimeNested").finish(),
Self::InvalidServerConfiguration { kind } => formatter
.debug_struct("InvalidServerConfiguration")
.field("kind", kind)
.finish(),
Self::UnsupportedCapability {
capability,
needs,
found,
} => formatter
.debug_struct("UnsupportedCapability")
.field("capability", capability)
.field("needs", needs)
.field("found", found)
.finish(),
Self::CapabilityDefective {
capability,
found,
broken_in,
fixed_in,
} => formatter
.debug_struct("CapabilityDefective")
.field("capability", capability)
.field("found", found)
.field("broken_in", broken_in)
.field("fixed_in", fixed_in)
.finish(),
Self::UnreadableFormatValue { format, detail } => formatter
.debug_struct("UnreadableFormatValue")
.field("format", format)
.field("detail", detail)
.finish(),
#[cfg(feature = "control-mode")]
Self::ControlModeFrameTooLarge { frame, limit } => formatter
.debug_struct("ControlModeFrameTooLarge")
.field("frame", frame)
.field("limit", limit)
.finish(),
Self::OutputLimitExceeded {
request_id,
command,
stream,
limit,
} => formatter
.debug_struct("OutputLimitExceeded")
.field("request_id", request_id)
.field("command", command)
.field("stream", stream)
.field("limit", limit)
.finish(),
Self::Overloaded {
request_id,
command,
in_flight,
} => formatter
.debug_struct("Overloaded")
.field("request_id", request_id)
.field("command", command)
.field("in_flight", in_flight)
.finish(),
Self::ServerGenerationChanged { expected, found } => formatter
.debug_struct("ServerGenerationChanged")
.field("expected", expected)
.field("found", found)
.finish(),
Self::OptionRejected { kind, detail } => formatter
.debug_struct("OptionRejected")
.field("kind", kind)
.field("detail", detail)
.finish(),
Self::SessionExists { name } => formatter
.debug_struct("SessionExists")
.field("name", name)
.finish(),
Self::InvalidVersionOutput { output_len } => formatter
.debug_struct("InvalidVersionOutput")
.field("output_len", output_len)
.finish(),
Self::UnsupportedTmuxVersion { found, minimum } => formatter
.debug_struct("UnsupportedTmuxVersion")
.field("found", found)
.field("minimum", minimum)
.finish(),
Self::VersionProbeFailed {
request_id,
command,
exit_code,
signal,
} => formatter
.debug_struct("VersionProbeFailed")
.field("request_id", request_id)
.field("command", command)
.field("exit_code", exit_code)
.field("signal", signal)
.finish_non_exhaustive(),
Self::InvalidCommandInput { request_id, input } => formatter
.debug_struct("InvalidCommandInput")
.field("request_id", request_id)
.field("input", input)
.finish(),
Self::ServerMismatch { operation } => formatter
.debug_struct("ServerMismatch")
.field("operation", operation)
.finish(),
#[cfg(feature = "plan")]
Self::InvalidPlan { source } => formatter
.debug_struct("InvalidPlan")
.field("source", source)
.finish(),
Self::ExecutableNotFound {
request_id,
command,
source,
} => formatter
.debug_struct("ExecutableNotFound")
.field("request_id", request_id)
.field("command", command)
.field("source", source)
.finish(),
Self::Spawn {
request_id,
command,
source,
} => formatter
.debug_struct("Spawn")
.field("request_id", request_id)
.field("command", command)
.field("source", source)
.finish(),
Self::ReadOutput {
request_id,
command,
stream,
kind,
} => formatter
.debug_struct("ReadOutput")
.field("request_id", request_id)
.field("command", command)
.field("stream", stream)
.field("kind", kind)
.finish(),
Self::WaitChild {
request_id,
command,
source,
} => formatter
.debug_struct("WaitChild")
.field("request_id", request_id)
.field("command", command)
.field("source", source)
.finish(),
Self::Timeout {
request_id,
command,
timeout,
} => formatter
.debug_struct("Timeout")
.field("request_id", request_id)
.field("command", command)
.field("timeout", timeout)
.finish(),
Self::ExecutorShutdown {
request_id,
command,
} => formatter
.debug_struct("ExecutorShutdown")
.field("request_id", request_id)
.field("command", command)
.finish(),
Self::DuplicateRequest {
request_id,
command,
} => formatter
.debug_struct("DuplicateRequest")
.field("request_id", request_id)
.field("command", command)
.finish(),
Self::SupervisorLost {
request_id,
command,
} => formatter
.debug_struct("SupervisorLost")
.field("request_id", request_id)
.field("command", command)
.finish(),
Self::LinkGone { kind, target } => formatter
.debug_struct("LinkGone")
.field("kind", kind)
.field("target", target)
.finish(),
Self::ClientSuspended { name } => formatter
.debug_struct("ClientSuspended")
.field("name", name)
.finish(),
#[cfg(feature = "control-mode")]
Self::ControlMode { kind, source } => formatter
.debug_struct("ControlMode")
.field("kind", kind)
.field("source_kind", &source.as_ref().map(io::Error::kind))
.finish(),
Self::RuntimeUnavailable { source } => formatter
.debug_struct("RuntimeUnavailable")
.field("kind", &source.kind())
.finish(),
Self::CommandFailed {
command,
exit_code,
stderr,
} => formatter
.debug_struct("CommandFailed")
.field("command", command)
.field("exit_code", exit_code)
.field("stderr", stderr)
.finish(),
Self::ObjectGone { kind, id } => formatter
.debug_struct("ObjectGone")
.field("kind", kind)
.field("id", id)
.finish(),
Self::ServerGone { command, kind } => formatter
.debug_struct("ServerGone")
.field("command", command)
.field("kind", kind)
.finish(),
Self::DecodeListing {
list_command,
detail,
} => formatter
.debug_struct("DecodeListing")
.field("list_command", list_command)
.field("detail", detail)
.finish(),
}
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod compat_tests;