use std::path::{Path, PathBuf};
use std::time::Duration;
use tracing::warn;
use super::classify::{self, NO_SERVER_SIGNATURE, ServerHealth, SocketProbe};
#[cfg(unix)]
const SOCKET_CONNECT_TIMEOUT: Duration = Duration::from_millis(500);
#[cfg(unix)]
const SOCKET_SILENCE_WINDOW: Duration = Duration::from_millis(500);
#[derive(Debug, Clone)]
pub struct CommandObservation {
pub success: bool,
pub exit_code: Option<i32>,
pub stdout: String,
pub stderr: String,
}
#[derive(Debug, Clone)]
pub struct ProbeResult {
pub health: ServerHealth,
pub command: Option<CommandObservation>,
pub socket: Option<SocketProbe>,
}
pub async fn probe() -> ProbeResult {
let output = tokio::process::Command::new("tmux").arg("list-sessions").output().await;
let out = match output {
Ok(out) => out,
Err(e) => {
warn!(stage = "probe", error = %e, "tmux list-sessions 无法执行,判为 other(不触发自愈)");
return ProbeResult { health: ServerHealth::Other, command: None, socket: None };
}
};
let command = CommandObservation {
success: out.status.success(),
exit_code: out.status.code(),
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
};
let socket = if !command.success && !command.stderr.contains(NO_SERVER_SIGNATURE) {
Some(socket_probe(socket_path().as_deref()).await)
} else {
None
};
let health = classify::classify(command.success, &command.stdout, &command.stderr, socket);
ProbeResult { health, command: Some(command), socket }
}
pub fn socket_path() -> Option<PathBuf> {
platform::socket_path_impl()
}
async fn socket_probe(path: Option<&Path>) -> SocketProbe {
let Some(path) = path else {
warn_platform_degraded();
return SocketProbe::Inconclusive;
};
socket_probe_impl(path).await
}
fn warn_platform_degraded() {
static WARN_ONCE: std::sync::Once = std::sync::Once::new();
WARN_ONCE.call_once(|| {
warn!("socket 健康探针在本平台不可用(无 unix domain socket 路径语义),降级为 Inconclusive(不触发自愈)");
});
}
#[cfg(unix)]
mod platform {
use std::path::PathBuf;
pub fn socket_path_impl() -> Option<PathBuf> {
let uid = unsafe { libc::getuid() };
let base = std::env::var_os("TMUX_TMPDIR")
.map(PathBuf::from)
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| PathBuf::from("/tmp"));
Some(base.join(format!("tmux-{uid}")).join("default"))
}
}
#[cfg(not(unix))]
mod platform {
use std::path::PathBuf;
pub fn socket_path_impl() -> Option<PathBuf> {
None
}
}
#[cfg(unix)]
async fn socket_probe_impl(path: &Path) -> SocketProbe {
use tokio::io::AsyncReadExt;
let connected =
tokio::time::timeout(SOCKET_CONNECT_TIMEOUT, tokio::net::UnixStream::connect(path)).await;
let mut stream = match connected {
Err(_) => return SocketProbe::Inconclusive, Ok(Err(e)) => {
return match e.kind() {
std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::NotFound => {
SocketProbe::ConnectRefused
}
_ => SocketProbe::Inconclusive,
};
}
Ok(Ok(stream)) => stream,
};
let mut buf = [0u8; 1];
match tokio::time::timeout(SOCKET_SILENCE_WINDOW, stream.read(&mut buf)).await {
Err(_) => SocketProbe::ConnectAlive, Ok(Ok(0)) => SocketProbe::ConnectThenEof, Ok(Ok(_)) => SocketProbe::ConnectAlive, Ok(Err(_)) => SocketProbe::Inconclusive,
}
}
#[cfg(not(unix))]
async fn socket_probe_impl(_path: &Path) -> SocketProbe {
warn_platform_degraded();
SocketProbe::Inconclusive
}
#[cfg(test)]
mod tests {
use super::*;
use crate::health::test_support::{FakeSocketHolder, temp_dir};
#[cfg(unix)]
#[tokio::test]
async fn live_listener_probes_as_alive() {
let holder = FakeSocketHolder::spawn("probe_alive", "tmux");
let result = socket_probe(Some(&holder.socket_path)).await;
assert_eq!(result, SocketProbe::ConnectAlive, "阻塞无输出 = 正常");
}
#[cfg(unix)]
#[tokio::test]
async fn missing_socket_probes_as_refused() {
let dir = temp_dir("probe_refused");
let result = socket_probe(Some(&dir.join("missing.sock"))).await;
assert_eq!(result, SocketProbe::ConnectRefused);
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[tokio::test]
async fn accept_then_close_probes_as_eof() {
let dir = temp_dir("probe_eof");
let path = dir.join("s.sock");
let listener = tokio::net::UnixListener::bind(&path).expect("bind");
let accept = tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("accept");
drop(stream);
});
let result = socket_probe(Some(&path)).await;
assert_eq!(result, SocketProbe::ConnectThenEof, "connect 成功后立即 EOF = Deaf");
let _ = accept.await;
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn absent_socket_path_probes_inconclusive() {
assert_eq!(socket_probe(None).await, SocketProbe::Inconclusive);
}
#[cfg(unix)]
#[test]
fn socket_path_shape_matches_tmux_convention() {
let path = socket_path().expect("unix 有路径");
let uid = unsafe { libc::getuid() };
assert!(path.ends_with(format!("tmux-{uid}/default")), "got {}", path.display());
}
}