tempo_cli/services/
daemon_service.rs

1use 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
11/// Service layer for daemon-related operations
12pub struct DaemonService;
13
14impl DaemonService {
15    /// Start the tempo daemon
16    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        // Find the daemon binary
24        let daemon_path = Self::find_daemon_binary()?;
25        
26        // Start daemon as background process
27        let mut cmd = Command::new(daemon_path);
28        cmd.stdout(Stdio::null())
29           .stderr(Stdio::null())
30           .stdin(Stdio::null());
31
32        // Set environment variables for daemon
33        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        // Wait a moment for daemon to initialize
43        tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
44
45        // Verify daemon is running
46        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    /// Stop the tempo daemon
55    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    /// Restart the tempo daemon
84    pub async fn restart_daemon() -> Result<()> {
85        println!("Restarting tempo daemon...");
86        
87        if is_daemon_running() {
88            Self::stop_daemon().await?;
89            
90            // Wait for daemon to fully stop
91            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    /// Get daemon status and information
105    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    /// Send activity heartbeat to daemon
141    pub async fn send_activity_heartbeat() -> Result<()> {
142        if !is_daemon_running() {
143            return Ok(()); // Silently ignore if daemon not running
144        }
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    /// Get connection pool statistics
153    pub async fn get_pool_stats() -> Result<PoolStatistics> {
154        // This would interact with the daemon to get pool stats
155        // For now, return a placeholder
156        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    // Private helper methods
167
168    fn find_daemon_binary() -> Result<PathBuf> {
169        // Try to find the daemon binary in common locations
170        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                // Try with .exe extension on Windows
187                #[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        // Fall back to assuming it's in PATH
198        Ok(PathBuf::from("tempo-daemon"))
199    }
200}
201
202/// Information about daemon status
203#[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/// Database connection pool statistics
213#[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        // Should at least return a path, even if file doesn't exist
232        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        // This test assumes daemon is not running
240        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        // Should either be a full path or just the binary name
255        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        // Test placeholder values
267        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        // Validate relationship
275        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        // Test that daemon operations handle "not running" state gracefully
285        
286        // Stop daemon when not running should succeed silently
287        let stop_result = DaemonService::stop_daemon().await;
288        // This may succeed (daemon not running) or fail (can't connect)
289        // Either is acceptable behavior
290        
291        // Activity heartbeat should handle daemon not running
292        let heartbeat_result = DaemonService::send_activity_heartbeat().await;
293        assert!(heartbeat_result.is_ok()); // Should silently ignore
294    }
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        // Validate internal consistency
332        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}