use std::{fmt, sync::mpsc, thread, time::Duration};
use serde::Serialize;
use crate::ipc::{self, CommandAck, ControlCommand, ControlResponse};
pub const CLI_VERSION: &str = env!("CARGO_PKG_VERSION");
const PROBE_TIMEOUT: Duration = Duration::from_secs(2);
#[derive(Debug, Clone, Serialize)]
pub struct VersionReport {
pub cli: String,
pub supervisor: Option<String>,
pub supervisor_pid: Option<u32>,
pub supervisor_binary: Option<String>,
}
impl VersionReport {
pub fn collect() -> Self {
let (supervisor, supervisor_pid) = probe();
Self {
cli: CLI_VERSION.to_string(),
supervisor,
supervisor_pid,
supervisor_binary: supervisor_pid.and_then(running_binary),
}
}
pub fn drifted(&self) -> bool {
self.supervisor
.as_deref()
.is_some_and(|version| version != self.cli)
}
}
impl fmt::Display for VersionReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "sysg {} (this binary)", self.cli)?;
match (&self.supervisor, self.supervisor_pid) {
(Some(version), Some(pid)) => {
write!(f, "\nsupervisor {version} (pid {pid})")?
}
(Some(version), None) => write!(f, "\nsupervisor {version}")?,
(None, Some(pid)) => {
write!(f, "\nsupervisor unreachable (pid {pid} holds the socket)")?;
}
(None, None) => return write!(f, "\nsupervisor not running"),
}
if let Some(binary) = &self.supervisor_binary {
write!(f, "\n executing {binary}")?;
}
if self.drifted() {
write!(
f,
"\n drift: the supervisor is still serving {}; it adopts {} when it is upgraded in place or stopped",
self.supervisor.as_deref().unwrap_or(""),
self.cli
)?;
}
Ok(())
}
}
fn probe() -> (Option<String>, Option<u32>) {
let (tx, rx) = mpsc::channel();
if thread::Builder::new()
.name("sysg-version-probe".into())
.spawn(move || {
let _ = tx.send(ipc::send_command_with_peer(
&ControlCommand::Version,
PROBE_TIMEOUT / 2,
));
})
.is_err()
{
return (None, None);
}
match rx.recv_timeout(PROBE_TIMEOUT) {
Ok(Ok((CommandAck::Response(ControlResponse::DaemonVersion(version)), pid))) => {
(Some(version), pid)
}
Ok(Ok((_, pid))) => (None, pid),
_ => (None, None),
}
}
#[cfg(target_os = "linux")]
fn running_binary(pid: u32) -> Option<String> {
std::fs::read_link(format!("/proc/{pid}/exe"))
.ok()
.map(|path| path.to_string_lossy().into_owned())
}
#[cfg(not(target_os = "linux"))]
fn running_binary(_pid: u32) -> Option<String> {
None
}