tempo_cli/services/
daemon_service.rs1use anyhow::{Context, Result};
2use std::path::PathBuf;
3use std::process::{Command, Stdio};
4
5use crate::utils::ipc::{get_socket_path, is_daemon_running, IpcClient, IpcMessage, IpcResponse};
6use crate::utils::paths::get_data_dir;
7
8#[cfg(test)]
9use std::env;
10
11pub struct DaemonService;
13
14impl DaemonService {
15 pub async fn start_daemon() -> Result<()> {
17 if is_daemon_running() {
18 return Err(anyhow::anyhow!("Daemon is already running"));
19 }
20
21 println!("Starting tempo daemon...");
22
23 let daemon_path = Self::find_daemon_binary()?;
25
26 let mut cmd = Command::new(daemon_path);
28 cmd.stdout(Stdio::null())
29 .stderr(Stdio::null())
30 .stdin(Stdio::null());
31
32 if let Ok(data_dir) = get_data_dir() {
34 cmd.env("TEMPO_DATA_DIR", data_dir);
35 }
36
37 let child = cmd.spawn().context("Failed to start daemon process")?;
38
39 println!("Daemon started with PID: {}", child.id());
40
41 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
43
44 if !is_daemon_running() {
46 return Err(anyhow::anyhow!("Failed to start daemon - not responding"));
47 }
48
49 println!("✓ Daemon started successfully");
50 Ok(())
51 }
52
53 pub async fn stop_daemon() -> Result<()> {
55 if !is_daemon_running() {
56 println!("Daemon is not running");
57 return Ok(());
58 }
59
60 println!("Stopping tempo daemon...");
61
62 let socket_path = get_socket_path()?;
63 let mut client = IpcClient::connect(&socket_path)
64 .await
65 .context("Failed to connect to daemon")?;
66
67 let response = client.send_message(&IpcMessage::Shutdown).await?;
68
69 match response {
70 IpcResponse::Success => {
71 println!("✓ Daemon stopped successfully");
72 Ok(())
73 }
74 IpcResponse::Error(e) => Err(anyhow::anyhow!("Failed to stop daemon: {}", e)),
75 _ => Err(anyhow::anyhow!("Unexpected response from daemon")),
76 }
77 }
78
79 pub async fn restart_daemon() -> Result<()> {
81 println!("Restarting tempo daemon...");
82
83 if is_daemon_running() {
84 Self::stop_daemon().await?;
85
86 for _ in 0..10 {
88 if !is_daemon_running() {
89 break;
90 }
91 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
92 }
93 }
94
95 Self::start_daemon().await?;
96 println!("✓ Daemon restarted successfully");
97 Ok(())
98 }
99
100 pub async fn get_daemon_status() -> Result<DaemonStatus> {
102 if !is_daemon_running() {
103 return Ok(DaemonStatus {
104 running: false,
105 uptime_seconds: 0,
106 active_session: None,
107 version: None,
108 socket_path: get_socket_path().ok(),
109 });
110 }
111
112 let socket_path = get_socket_path()?;
113 let mut client = IpcClient::connect(&socket_path).await?;
114
115 let response = client.send_message(&IpcMessage::GetStatus).await?;
116
117 match response {
118 IpcResponse::Status {
119 daemon_running,
120 active_session,
121 uptime,
122 } => Ok(DaemonStatus {
123 running: daemon_running,
124 uptime_seconds: uptime,
125 active_session,
126 version: Some(env!("CARGO_PKG_VERSION").to_string()),
127 socket_path: Some(socket_path),
128 }),
129 IpcResponse::Error(e) => Err(anyhow::anyhow!("Failed to get daemon status: {}", e)),
130 _ => Err(anyhow::anyhow!("Unexpected response from daemon")),
131 }
132 }
133
134 pub async fn send_activity_heartbeat() -> Result<()> {
136 if !is_daemon_running() {
137 return Ok(()); }
139
140 let socket_path = get_socket_path()?;
141 let mut client = IpcClient::connect(&socket_path).await?;
142 let _response = client.send_message(&IpcMessage::ActivityHeartbeat).await?;
143 Ok(())
144 }
145
146 pub async fn get_pool_stats() -> Result<PoolStatistics> {
148 Ok(PoolStatistics {
151 total_connections: 5,
152 active_connections: 2,
153 idle_connections: 3,
154 max_connections: 10,
155 connection_requests: 150,
156 connection_timeouts: 0,
157 })
158 }
159
160 fn find_daemon_binary() -> Result<PathBuf> {
163 let possible_names = ["tempo-daemon", "tempo_daemon"];
165 let possible_paths = [
166 std::env::current_exe()?.parent().map(|p| p.to_path_buf()),
167 Some(PathBuf::from("/usr/local/bin")),
168 Some(PathBuf::from("/usr/bin")),
169 std::env::var("CARGO_TARGET_DIR")
170 .ok()
171 .map(|p| PathBuf::from(p).join("debug")),
172 std::env::var("CARGO_TARGET_DIR")
173 .ok()
174 .map(|p| PathBuf::from(p).join("release")),
175 ];
176
177 for path_opt in possible_paths.iter().flatten() {
178 for name in &possible_names {
179 let full_path = path_opt.join(name);
180 if full_path.exists() && full_path.is_file() {
181 return Ok(full_path);
182 }
183
184 #[cfg(windows)]
186 {
187 let exe_path = path_opt.join(format!("{}.exe", name));
188 if exe_path.exists() && exe_path.is_file() {
189 return Ok(exe_path);
190 }
191 }
192 }
193 }
194
195 Ok(PathBuf::from("tempo-daemon"))
197 }
198}
199
200#[derive(Debug, Clone)]
202pub struct DaemonStatus {
203 pub running: bool,
204 pub uptime_seconds: u64,
205 pub active_session: Option<crate::utils::ipc::SessionInfo>,
206 pub version: Option<String>,
207 pub socket_path: Option<PathBuf>,
208}
209
210#[derive(Debug, Clone)]
212pub struct PoolStatistics {
213 pub total_connections: u32,
214 pub active_connections: u32,
215 pub idle_connections: u32,
216 pub max_connections: u32,
217 pub connection_requests: u64,
218 pub connection_timeouts: u64,
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use std::env;
225
226 #[test]
227 fn test_find_daemon_binary() {
228 let result = DaemonService::find_daemon_binary();
229 assert!(result.is_ok());
231 let path = result.unwrap();
232 assert!(!path.as_os_str().is_empty());
233 }
234
235 #[tokio::test]
236 async fn test_daemon_status_when_not_running() {
237 let status = DaemonService::get_daemon_status().await.unwrap();
239 if !status.running {
240 assert_eq!(status.uptime_seconds, 0);
241 assert!(status.active_session.is_none());
242 }
243 }
244
245 #[test]
246 fn test_daemon_binary_search_paths() {
247 let result = DaemonService::find_daemon_binary();
248 assert!(result.is_ok());
249
250 let path = result.unwrap();
251
252 assert!(
254 path.is_absolute() || path == PathBuf::from("tempo-daemon"),
255 "Daemon path should be absolute or fallback binary name: {:?}",
256 path
257 );
258 }
259
260 #[tokio::test]
261 async fn test_pool_stats_placeholder() {
262 let stats = DaemonService::get_pool_stats().await.unwrap();
263
264 assert_eq!(stats.total_connections, 5);
266 assert_eq!(stats.active_connections, 2);
267 assert_eq!(stats.idle_connections, 3);
268 assert_eq!(stats.max_connections, 10);
269 assert_eq!(stats.connection_requests, 150);
270 assert_eq!(stats.connection_timeouts, 0);
271
272 assert_eq!(
274 stats.active_connections + stats.idle_connections,
275 stats.total_connections
276 );
277 assert!(stats.total_connections <= stats.max_connections);
278 }
279
280 #[tokio::test]
281 async fn test_daemon_operations_when_not_running() {
282 let _stop_result = DaemonService::stop_daemon().await;
286 let heartbeat_result = DaemonService::send_activity_heartbeat().await;
291 assert!(heartbeat_result.is_ok()); }
293
294 #[test]
295 fn test_daemon_status_structure() {
296 let status = DaemonStatus {
297 running: true,
298 uptime_seconds: 3600,
299 active_session: None,
300 version: Some("0.2.0".to_string()),
301 socket_path: Some(PathBuf::from("/tmp/tempo.sock")),
302 };
303
304 assert!(status.running);
305 assert_eq!(status.uptime_seconds, 3600);
306 assert!(status.active_session.is_none());
307 assert_eq!(status.version, Some("0.2.0".to_string()));
308 assert!(status.socket_path.is_some());
309 }
310
311 #[test]
312 fn test_pool_statistics_structure() {
313 let pool_stats = PoolStatistics {
314 total_connections: 10,
315 active_connections: 6,
316 idle_connections: 4,
317 max_connections: 20,
318 connection_requests: 500,
319 connection_timeouts: 2,
320 };
321
322 assert_eq!(pool_stats.total_connections, 10);
323 assert_eq!(pool_stats.active_connections, 6);
324 assert_eq!(pool_stats.idle_connections, 4);
325 assert_eq!(pool_stats.max_connections, 20);
326 assert_eq!(pool_stats.connection_requests, 500);
327 assert_eq!(pool_stats.connection_timeouts, 2);
328
329 assert_eq!(
331 pool_stats.active_connections + pool_stats.idle_connections,
332 pool_stats.total_connections
333 );
334 }
335
336 #[test]
337 fn test_version_info() {
338 let version = env!("CARGO_PKG_VERSION");
339 assert!(!version.is_empty());
340 assert!(version.starts_with("0."));
341 }
342}