leviath_cli/commands/
daemon.rs1use anyhow::bail;
12use leviath_runtime::control_socket::{ControlClient, ControlResponse};
13
14#[derive(clap::Args, Debug, Clone, Default)]
16pub struct DaemonArgs {
17 #[command(subcommand)]
19 pub action: Option<DaemonAction>,
20 #[arg(long, global = true)]
22 pub socket: Option<String>,
23}
24
25#[derive(clap::Subcommand, Debug, Clone, PartialEq, Eq)]
27pub enum DaemonAction {
28 Start,
30 Stop,
32 Status,
34 Restart,
36 Install,
39 Uninstall,
41}
42
43pub async fn send_shutdown(client: &ControlClient) -> anyhow::Result<()> {
45 match client.shutdown().await {
46 Ok(ControlResponse::Ok { ok: true }) => {
47 println!("daemon shutting down");
48 Ok(())
49 }
50 Ok(other) => bail!("unexpected daemon response: {other:?}"),
51 Err(e) => bail!("the leviath daemon is not reachable ({e}); is it running?"),
52 }
53}
54
55pub fn format_status(running: bool, run_count: usize) -> String {
57 if !running {
58 return "daemon not running".to_string();
59 }
60 let plural = if run_count == 1 { "" } else { "s" };
61 format!("daemon running ({run_count} agent{plural})")
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
68 use tokio::task::JoinHandle;
69
70 #[test]
71 fn format_status_covers_running_singular_plural_and_stopped() {
72 assert_eq!(format_status(false, 0), "daemon not running");
73 assert_eq!(format_status(true, 1), "daemon running (1 agent)");
74 assert_eq!(format_status(true, 3), "daemon running (3 agents)");
75 }
76
77 fn fake_daemon(
80 dir: &std::path::Path,
81 response_line: &'static str,
82 ) -> (leviath_runtime::control_socket::ControlId, JoinHandle<()>) {
83 let id = leviath_runtime::control_socket::control_id(dir);
84 let mut listener = leviath_runtime::control_socket::bind_control_listener(&id).unwrap();
85 let handle = tokio::spawn(async move {
86 let stream = listener
87 .accept()
88 .await
89 .expect("accept succeeds")
90 .expect("our own connection is admitted");
91 let (read_half, mut write_half) = tokio::io::split(stream);
92 let mut lines = BufReader::new(read_half).lines();
93 let _request = lines.next_line().await.unwrap();
94 write_half
95 .write_all(response_line.as_bytes())
96 .await
97 .unwrap();
98 write_half.write_all(b"\n").await.unwrap();
99 });
100 (id, handle)
101 }
102
103 async fn shutdown(response_line: &'static str) -> anyhow::Result<()> {
104 let dir = tempfile::tempdir().unwrap();
105 let (id, server) = fake_daemon(dir.path(), response_line);
106 let result = send_shutdown(&ControlClient::new(id)).await;
107 server.await.unwrap();
108 result
109 }
110
111 #[tokio::test]
112 async fn send_shutdown_reports_success() {
113 assert!(shutdown(r#"{"result":"ok","ok":true}"#).await.is_ok());
114 }
115
116 #[tokio::test]
117 async fn send_shutdown_rejects_unexpected_response() {
118 let err = shutdown(r#"{"result":"spawned","run_id":"x"}"#)
119 .await
120 .unwrap_err();
121 assert!(err.to_string().contains("unexpected"));
122 }
123
124 #[tokio::test]
125 async fn send_shutdown_errors_when_daemon_absent() {
126 let dir = tempfile::tempdir().unwrap();
127 let id = leviath_runtime::control_socket::control_id(&dir.path().join("no-daemon"));
128 let err = send_shutdown(&ControlClient::new(id)).await.unwrap_err();
129 assert!(err.to_string().contains("not reachable"));
130 }
131}