use std::path::Path;
use std::time::Duration;
use super::super::protocol::{CommsRequest, CommsResponse};
use super::{PROBE_ATTEMPTS, PROBE_RETRY_BACKOFF, roundtrip};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DaemonProbe {
Serving {
threads: u32,
},
Failing {
code: String,
message: String,
},
Unreachable,
}
#[cfg(any(unix, windows))]
pub fn probe_serving(socket_path: &Path) -> DaemonProbe {
for attempt in 0..PROBE_ATTEMPTS {
match roundtrip(socket_path, &CommsRequest::Status) {
Some(CommsResponse::Status(report)) => {
return DaemonProbe::Serving {
threads: report.threads,
};
}
Some(CommsResponse::Error { code, message }) => return DaemonProbe::Failing { code, message },
Some(other) => {
return DaemonProbe::Failing {
code: "unexpected_response".to_string(),
message: format!("daemon answered Status with {other:?}"),
};
}
None if attempt + 1 < PROBE_ATTEMPTS => std::thread::sleep(PROBE_RETRY_BACKOFF),
None => {}
}
}
DaemonProbe::Unreachable
}
#[cfg(not(any(unix, windows)))]
pub fn probe_serving(_socket_path: &Path) -> DaemonProbe {
DaemonProbe::Unreachable
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum StopOutcome {
Accepted,
Refused {
code: String,
message: String,
},
Unreachable,
}
impl StopOutcome {
pub fn permits_reclaim(&self) -> bool {
match self {
Self::Accepted | Self::Unreachable => true,
Self::Refused { code, .. } => code == "store_error",
}
}
}
pub fn request_stop_classified(socket_path: &Path) -> StopOutcome {
match roundtrip(socket_path, &CommsRequest::Stop) {
Some(CommsResponse::Ok) => StopOutcome::Accepted,
Some(CommsResponse::Error { code, message }) => StopOutcome::Refused { code, message },
Some(other) => StopOutcome::Refused {
code: "unexpected_response".to_string(),
message: format!("daemon answered Stop with {other:?}"),
},
None => StopOutcome::Unreachable,
}
}
#[cfg(unix)]
pub fn force_terminate(pid: u32, grace: Duration) -> bool {
use crate::daemon_lock::pid_is_live;
if !pid_is_live(pid) {
return true;
}
unsafe { libc::kill(pid as libc::pid_t, libc::SIGTERM) };
let deadline = std::time::Instant::now() + grace;
while std::time::Instant::now() < deadline {
if !pid_is_live(pid) {
return true;
}
std::thread::sleep(Duration::from_millis(50));
}
unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL) };
let deadline = std::time::Instant::now() + Duration::from_secs(2);
while std::time::Instant::now() < deadline {
if !pid_is_live(pid) {
return true;
}
std::thread::sleep(Duration::from_millis(50));
}
!pid_is_live(pid)
}
#[cfg(windows)]
pub fn force_terminate(pid: u32, _grace: Duration) -> bool {
use crate::daemon_lock::pid_is_live;
if !pid_is_live(pid) {
return true;
}
let _ = std::process::Command::new("taskkill")
.args(["/PID", &pid.to_string(), "/F"])
.output();
let deadline = std::time::Instant::now() + Duration::from_secs(2);
while std::time::Instant::now() < deadline {
if !pid_is_live(pid) {
return true;
}
std::thread::sleep(Duration::from_millis(50));
}
!pid_is_live(pid)
}
#[cfg(all(test, unix))]
mod tests {
use std::path::PathBuf;
use super::*;
use crate::comms::protocol::PROTO_VER;
fn serve_one_reply(
dir: &std::path::Path,
name: &str,
reply: crate::comms::protocol::CommsOut,
) -> (PathBuf, std::thread::JoinHandle<()>) {
use std::io::{Read, Write};
use std::os::unix::net::UnixListener;
let socket = dir.join(name);
let listener = UnixListener::bind(&socket).expect("bind");
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("accept");
let mut prefix = [0u8; 4];
stream.read_exact(&mut prefix).expect("read len");
let len = u32::from_be_bytes(prefix) as usize;
let mut req = vec![0u8; len];
stream.read_exact(&mut req).expect("read req");
let body = rmp_serde::to_vec_named(&reply).expect("encode");
let out_len = u32::try_from(body.len()).expect("len fits");
stream.write_all(&out_len.to_be_bytes()).expect("write len");
stream.write_all(&body).expect("write body");
});
(socket, handle)
}
#[test]
fn probe_serving_separates_a_serving_daemon_from_one_that_only_holds_the_socket() {
use crate::comms::protocol::{CommsOut, CommsResponse, StatusReport};
let dir = tempfile::tempdir().expect("tempdir");
let (ok_socket, ok_server) = serve_one_reply(
dir.path(),
"serving.sock",
CommsOut::Response(CommsResponse::Status(StatusReport {
pid: 1,
version: "0.25.1".to_string(),
build_id: "abc".to_string(),
proto_ver: PROTO_VER,
uptime_secs: 1,
threads: 7,
subscribers: 0,
})),
);
assert_eq!(
probe_serving(&ok_socket),
DaemonProbe::Serving { threads: 7 },
"a daemon that answers Status is really serving"
);
ok_server.join().expect("server thread");
let (bad_socket, bad_server) = serve_one_reply(
dir.path(),
"wedged.sock",
CommsOut::Response(CommsResponse::Error {
code: "store_error".to_string(),
message: "fjall error: FjallError: Poisoned".to_string(),
}),
);
let verdict = probe_serving(&bad_socket);
bad_server.join().expect("server thread");
match verdict {
DaemonProbe::Failing { code, message } => {
assert_eq!(code, "store_error");
assert!(
message.contains("Poisoned"),
"the daemon's own error is carried through"
);
}
other => panic!("a daemon refusing every request must not read as healthy: {other:?}"),
}
assert_eq!(
probe_serving(&dir.path().join("nothing-here.sock")),
DaemonProbe::Unreachable,
"no endpoint is not reachable"
);
}
}