use std::fmt;
pub const DEAF_SIGNATURE: &str = "server exited unexpectedly";
pub const NO_SERVER_SIGNATURE: &str = "no server running";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServerHealth {
Healthy,
NoServer,
Deaf,
Other,
}
impl ServerHealth {
pub fn as_str(self) -> &'static str {
match self {
ServerHealth::Healthy => "healthy",
ServerHealth::NoServer => "no_server",
ServerHealth::Deaf => "deaf",
ServerHealth::Other => "other",
}
}
}
impl fmt::Display for ServerHealth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SocketProbe {
ConnectThenEof,
ConnectAlive,
ConnectRefused,
Inconclusive,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListSessionsFailure {
Deaf,
NoServer,
EmptyStdout,
Other,
}
pub fn classify_list_sessions_failure(stdout: &str, stderr: &str) -> ListSessionsFailure {
if stderr.contains(DEAF_SIGNATURE) {
return ListSessionsFailure::Deaf;
}
if stderr.contains(NO_SERVER_SIGNATURE) {
return ListSessionsFailure::NoServer;
}
if stdout.trim().is_empty() {
return ListSessionsFailure::EmptyStdout;
}
ListSessionsFailure::Other
}
pub fn classify(
success: bool,
stdout: &str,
stderr: &str,
socket: Option<SocketProbe>,
) -> ServerHealth {
if success {
return ServerHealth::Healthy;
}
match classify_list_sessions_failure(stdout, stderr) {
ListSessionsFailure::Deaf => ServerHealth::Deaf,
ListSessionsFailure::NoServer => ServerHealth::NoServer,
ListSessionsFailure::EmptyStdout | ListSessionsFailure::Other => match socket {
Some(SocketProbe::ConnectThenEof) => ServerHealth::Deaf,
Some(SocketProbe::ConnectRefused) => ServerHealth::NoServer,
_ => ServerHealth::Other,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn four_states_are_pairwise_distinct() {
let deaf = classify(false, "", "tmux: server exited unexpectedly\n", None);
let no_server = classify(false, "", "no server running on /tmp/tmux-1000/default\n", None);
let other = classify(false, "", "error connecting to socket: Permission denied\n", None);
let healthy = classify(true, "sess: 1 windows\n", "", None);
assert_eq!(deaf, ServerHealth::Deaf);
assert_eq!(no_server, ServerHealth::NoServer);
assert_eq!(other, ServerHealth::Other);
assert_eq!(healthy, ServerHealth::Healthy);
assert_ne!(deaf, no_server);
assert_ne!(deaf, other);
assert_ne!(deaf, healthy);
assert_ne!(no_server, other);
assert_ne!(no_server, healthy);
assert_ne!(other, healthy);
}
#[test]
fn socket_probe_results_classify_as_documented() {
let f = |sock| classify(false, "", "", Some(sock));
assert_eq!(f(SocketProbe::ConnectThenEof), ServerHealth::Deaf);
assert_eq!(f(SocketProbe::ConnectRefused), ServerHealth::NoServer);
assert_eq!(f(SocketProbe::ConnectAlive), ServerHealth::Other);
assert_eq!(f(SocketProbe::Inconclusive), ServerHealth::Other);
assert_eq!(classify(false, "", "", None), ServerHealth::Other, "无任何证据 ⇒ Other");
}
#[test]
fn command_success_dominates_probe() {
assert_eq!(
classify(true, "", "", Some(SocketProbe::ConnectThenEof)),
ServerHealth::Healthy
);
}
#[test]
fn deaf_signature_with_empty_stdout_is_not_empty_state() {
assert_eq!(
classify_list_sessions_failure("", "tmux: server exited unexpectedly\n"),
ListSessionsFailure::Deaf,
"聋签名必须压过空 stdout 兜底(S2 禁吞异常)"
);
}
#[test]
fn list_sessions_failure_taxonomy() {
use ListSessionsFailure::*;
assert_eq!(classify_list_sessions_failure("", "no server running"), NoServer);
assert_eq!(
classify_list_sessions_failure("", ""),
EmptyStdout,
"psmux/Windows 空 stdout 行为保留"
);
assert_eq!(
classify_list_sessions_failure("", "weird error"),
EmptyStdout,
"空 stdout 且无签名 ⇒ 空态(历史行为保留)"
);
assert_eq!(classify_list_sessions_failure("data", "weird error"), Other);
}
#[test]
fn signature_in_stdout_data_is_not_deaf() {
assert_eq!(
classify_list_sessions_failure("s: 1 windows\nno server running\n", ""),
ListSessionsFailure::Other
);
assert_eq!(
classify(false, "x: server exited unexpectedly\n", "", None),
ServerHealth::Other
);
}
#[test]
fn as_str_matches_http_contract_words() {
assert_eq!(ServerHealth::Healthy.as_str(), "healthy");
assert_eq!(ServerHealth::NoServer.as_str(), "no_server");
assert_eq!(ServerHealth::Deaf.as_str(), "deaf");
assert_eq!(ServerHealth::Other.as_str(), "other");
}
}