use anyhow::bail;
use leviath_runtime::control_socket::{ControlClient, ControlResponse};
#[derive(clap::Args, Debug, Clone, Default)]
pub struct DaemonArgs {
#[command(subcommand)]
pub action: Option<DaemonAction>,
#[arg(long, global = true)]
pub socket: Option<String>,
}
#[derive(clap::Subcommand, Debug, Clone, PartialEq, Eq)]
pub enum DaemonAction {
Start,
Stop,
Status,
Restart,
Install,
Uninstall,
}
pub async fn send_shutdown(client: &ControlClient) -> anyhow::Result<()> {
match client.shutdown().await {
Ok(ControlResponse::Ok { ok: true }) => {
println!("daemon shutting down");
Ok(())
}
Ok(other) => bail!("unexpected daemon response: {other:?}"),
Err(e) => bail!("the leviath daemon is not reachable ({e}); is it running?"),
}
}
pub fn format_status(running: bool, run_count: usize) -> String {
if !running {
return "daemon not running".to_string();
}
let plural = if run_count == 1 { "" } else { "s" };
format!("daemon running ({run_count} agent{plural})")
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::task::JoinHandle;
#[test]
fn format_status_covers_running_singular_plural_and_stopped() {
assert_eq!(format_status(false, 0), "daemon not running");
assert_eq!(format_status(true, 1), "daemon running (1 agent)");
assert_eq!(format_status(true, 3), "daemon running (3 agents)");
}
fn fake_daemon(
dir: &std::path::Path,
response_line: &'static str,
) -> (leviath_runtime::control_socket::ControlId, JoinHandle<()>) {
let id = leviath_runtime::control_socket::control_id(dir);
let mut listener = leviath_runtime::control_socket::bind_control_listener(&id).unwrap();
let handle = tokio::spawn(async move {
let stream = listener
.accept()
.await
.expect("accept succeeds")
.expect("our own connection is admitted");
let (read_half, mut write_half) = tokio::io::split(stream);
let mut lines = BufReader::new(read_half).lines();
let _request = lines.next_line().await.unwrap();
write_half
.write_all(response_line.as_bytes())
.await
.unwrap();
write_half.write_all(b"\n").await.unwrap();
});
(id, handle)
}
async fn shutdown(response_line: &'static str) -> anyhow::Result<()> {
let dir = tempfile::tempdir().unwrap();
let (id, server) = fake_daemon(dir.path(), response_line);
let result = send_shutdown(&ControlClient::new(id)).await;
server.await.unwrap();
result
}
#[tokio::test]
async fn send_shutdown_reports_success() {
assert!(shutdown(r#"{"result":"ok","ok":true}"#).await.is_ok());
}
#[tokio::test]
async fn send_shutdown_rejects_unexpected_response() {
let err = shutdown(r#"{"result":"spawned","run_id":"x"}"#)
.await
.unwrap_err();
assert!(err.to_string().contains("unexpected"));
}
#[tokio::test]
async fn send_shutdown_errors_when_daemon_absent() {
let dir = tempfile::tempdir().unwrap();
let id = leviath_runtime::control_socket::control_id(&dir.path().join("no-daemon"));
let err = send_shutdown(&ControlClient::new(id)).await.unwrap_err();
assert!(err.to_string().contains("not reachable"));
}
}