use std::path::PathBuf;
use shep_client::Client;
use shep_core::paths::ShepPaths;
use shep_core::protocol::{Request, Response};
use crate::cli::Format;
use crate::exit::ExitCode;
use crate::output::{OutputEnvelope, SCHEMA_VERSION, Streams};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Online {
pub(crate) version: String,
pub(crate) pid: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ShepherdStatus {
pub(crate) online: Option<Online>,
pub(crate) home: PathBuf,
pub(crate) socket: PathBuf,
}
impl ShepherdStatus {
pub(crate) async fn probe(paths: &ShepPaths) -> Self {
let online = match Client::connect(&paths.socket).await {
Ok(client) => {
let ack = client.daemon();
let version = ack.daemon_version.clone();
let pid = ack.pid;
match client.request(Request::Ping).await {
Ok(Response::Pong) => Some(Online { version, pid }),
_ => None,
}
}
Err(_) => None,
};
Self {
online,
home: paths.home.clone(),
socket: paths.socket.clone(),
}
}
}
pub(crate) fn one_line(status: &ShepherdStatus) -> String {
match &status.online {
Some(Online { pid, .. }) => format!(
"shepherd online (pid {pid}), flock at {}",
status.home.display()
),
None => format!(
"no shepherd running, flock at {}. `shep start` brings one up.",
status.home.display()
),
}
}
#[derive(Debug, serde::Serialize)]
struct PingStatus {
shepherd: &'static str,
daemon_version: Option<String>,
pid: Option<u32>,
home: String,
socket: String,
}
pub(crate) fn render_ping(streams: &mut Streams<'_>, status: &ShepherdStatus) -> ExitCode {
let online = status.online.as_ref();
let payload = PingStatus {
shepherd: if online.is_some() {
"online"
} else {
"offline"
},
daemon_version: online.map(|o| o.version.clone()),
pid: online.map(|o| o.pid),
home: status.home.display().to_string(),
socket: status.socket.display().to_string(),
};
let _ = match streams.fmt {
Format::Table => {
let mut lines = vec![format!("shepherd {}", payload.shepherd)];
if let Some(o) = online {
lines.push(format!("version {}", o.version));
lines.push(format!("pid {}", o.pid));
}
lines.push(format!("home {}", payload.home));
lines.push(format!("socket {}", payload.socket));
writeln!(streams.out, "{}", lines.join("\n"))
}
Format::Json => {
let envelope = OutputEnvelope {
schema_version: SCHEMA_VERSION,
command: "ping",
data: payload,
};
serde_json::to_writer(&mut *streams.out, &envelope)
.map_err(std::io::Error::other)
.and_then(|()| writeln!(streams.out))
}
};
if online.is_some() {
ExitCode::Success
} else {
ExitCode::DaemonUnreachable
}
}
#[cfg(test)]
mod tests {
use super::*;
fn at(online: Option<Online>) -> ShepherdStatus {
ShepherdStatus {
online,
home: PathBuf::from("/home/rin/.shep"),
socket: PathBuf::from("/home/rin/.shep/run/shep.sock"),
}
}
#[test]
fn the_offline_line_names_the_home_and_the_way_out() {
let line = one_line(&at(None));
assert!(line.contains("no shepherd running"), "{line}");
assert!(line.contains("/home/rin/.shep"), "{line}");
assert!(line.contains("shep start"), "{line}");
}
#[test]
fn the_online_line_names_the_pid_and_the_home() {
let line = one_line(&at(Some(Online {
version: "0.1.0-alpha.1".to_owned(),
pid: 4823,
})));
assert!(line.contains("online"), "{line}");
assert!(line.contains("4823"), "{line}");
assert!(line.contains("/home/rin/.shep"), "{line}");
}
#[test]
fn the_status_lines_have_no_em_dashes() {
for line in [
one_line(&at(None)),
one_line(&at(Some(Online {
version: "0.1.0".to_owned(),
pid: 1,
}))),
] {
assert!(!line.contains('\u{2014}'), "em dash in {line:?}");
assert!(!line.contains('\u{2013}'), "en dash in {line:?}");
}
}
#[tokio::test]
async fn a_socket_that_handshakes_but_never_answers_is_not_online() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("wedged.sock");
let _listener = tokio::net::UnixListener::bind(&path).unwrap();
let env =
|key: &str| (key == "SHEP_HOME").then(|| dir.path().to_string_lossy().into_owned());
let mut paths = ShepPaths::resolve(&env, std::path::Path::new("/nonexistent"));
paths.socket = path;
let status = ShepherdStatus::probe(&paths).await;
assert_eq!(
status.online, None,
"a wedged socket is not an online shepherd"
);
let mut out = Vec::new();
let mut err = Vec::new();
let code = {
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: crate::style::Presentation::BARE,
fmt: Format::Table,
};
render_ping(&mut streams, &status)
};
assert_eq!(code, ExitCode::DaemonUnreachable);
assert!(
String::from_utf8(out).unwrap().contains("offline"),
"the operator is told, not errored at"
);
}
#[tokio::test]
async fn a_probe_with_nothing_listening_reports_offline() {
let dir = tempfile::tempdir().unwrap();
let env =
|key: &str| (key == "SHEP_HOME").then(|| dir.path().to_string_lossy().into_owned());
let paths = ShepPaths::resolve(&env, std::path::Path::new("/nonexistent"));
let status = ShepherdStatus::probe(&paths).await;
assert_eq!(status.online, None);
assert_eq!(status.home, dir.path());
}
}