tempo_cli/services/
daemon_service.rs

1use 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
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().context("Failed to start daemon process")?;
38
39        println!("Daemon started with PID: {}", child.id());
40
41        // Wait a moment for daemon to initialize
42        tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
43
44        // Verify daemon is running
45        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    /// Stop the tempo daemon
54    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    /// Restart the tempo daemon
80    pub async fn restart_daemon() -> Result<()> {
81        println!("Restarting tempo daemon...");
82
83        if is_daemon_running() {
84            Self::stop_daemon().await?;
85
86            // Wait for daemon to fully stop
87            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    /// Get daemon status and information
101    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    /// Send activity heartbeat to daemon
135    pub async fn send_activity_heartbeat() -> Result<()> {
136        if !is_daemon_running() {
137            return Ok(()); // Silently ignore if daemon not running
138        }
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    /// Get connection pool statistics
147    pub async fn get_pool_stats() -> Result<PoolStatistics> {
148        // This would interact with the daemon to get pool stats
149        // For now, return a placeholder
150        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    // Private helper methods
161
162    fn find_daemon_binary() -> Result<PathBuf> {
163        // Try to find the daemon binary in common locations
164        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                // Try with .exe extension on Windows
185                #[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        // Fall back to assuming it's in PATH
196        Ok(PathBuf::from("tempo-daemon"))
197    }
198}
199
200/// Information about daemon status
201#[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/// Database connection pool statistics
211#[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        // Should at least return a path, even if file doesn't exist
230        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        // This test assumes daemon is not running
238        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        // Should either be a full path or just the binary name
253        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        // Test placeholder values
265        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        // Validate relationship
273        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        // Test that daemon operations handle "not running" state gracefully
283
284        // Stop daemon when not running should succeed silently
285        let _stop_result = DaemonService::stop_daemon().await;
286        // This may succeed (daemon not running) or fail (can't connect)
287        // Either is acceptable behavior
288
289        // Activity heartbeat should handle daemon not running
290        let heartbeat_result = DaemonService::send_activity_heartbeat().await;
291        assert!(heartbeat_result.is_ok()); // Should silently ignore
292    }
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        // Validate internal consistency
330        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}