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