intent_engine/dashboard/
daemon.rs

1use anyhow::{Context, Result};
2use std::fs;
3use std::path::PathBuf;
4
5/// Get PID file path for a Dashboard instance
6pub fn pid_file_path(port: u16) -> PathBuf {
7    // Use temp directory for PID files
8    let temp_dir = std::env::temp_dir();
9    temp_dir.join(format!("ie-dashboard-{}.pid", port))
10}
11
12/// Write PID to file
13pub fn write_pid_file(port: u16, pid: u32) -> Result<()> {
14    let path = pid_file_path(port);
15    fs::write(&path, pid.to_string())
16        .with_context(|| format!("Failed to write PID file: {}", path.display()))?;
17    Ok(())
18}
19
20/// Read PID from file
21pub fn read_pid_file(port: u16) -> Result<Option<u32>> {
22    let path = pid_file_path(port);
23
24    if !path.exists() {
25        return Ok(None);
26    }
27
28    let content = fs::read_to_string(&path)
29        .with_context(|| format!("Failed to read PID file: {}", path.display()))?;
30
31    let pid = content
32        .trim()
33        .parse::<u32>()
34        .context("Invalid PID in file")?;
35
36    Ok(Some(pid))
37}
38
39/// Delete PID file
40pub fn delete_pid_file(port: u16) -> Result<()> {
41    let path = pid_file_path(port);
42
43    if path.exists() {
44        fs::remove_file(&path)
45            .with_context(|| format!("Failed to delete PID file: {}", path.display()))?;
46    }
47
48    Ok(())
49}
50
51/// Check if a process is running
52#[cfg(unix)]
53pub fn is_process_running(pid: u32) -> bool {
54    use std::process::Command;
55
56    // Use kill -0 to check if process exists
57    Command::new("kill")
58        .args(["-0", &pid.to_string()])
59        .output()
60        .map(|output| output.status.success())
61        .unwrap_or(false)
62}
63
64#[cfg(windows)]
65pub fn is_process_running(pid: u32) -> bool {
66    use std::process::Command;
67
68    // Use tasklist to check if process exists
69    Command::new("tasklist")
70        .args(["/FI", &format!("PID eq {}", pid)])
71        .output()
72        .map(|output| String::from_utf8_lossy(&output.stdout).contains(&pid.to_string()))
73        .unwrap_or(false)
74}
75
76/// Stop a process by PID
77#[cfg(unix)]
78pub fn stop_process(pid: u32) -> Result<()> {
79    use std::process::Command;
80
81    // Send SIGTERM
82    let output = Command::new("kill")
83        .arg(pid.to_string())
84        .output()
85        .context("Failed to send SIGTERM")?;
86
87    if !output.status.success() {
88        anyhow::bail!(
89            "Failed to stop process {}: {}",
90            pid,
91            String::from_utf8_lossy(&output.stderr)
92        );
93    }
94
95    Ok(())
96}
97
98#[cfg(windows)]
99pub fn stop_process(pid: u32) -> Result<()> {
100    use std::process::Command;
101
102    // Use taskkill
103    let output = Command::new("taskkill")
104        .args(["/PID", &pid.to_string(), "/F"])
105        .output()
106        .context("Failed to kill process")?;
107
108    if !output.status.success() {
109        anyhow::bail!(
110            "Failed to stop process {}: {}",
111            pid,
112            String::from_utf8_lossy(&output.stderr)
113        );
114    }
115
116    Ok(())
117}
118
119/// Open URL in default browser
120pub fn open_browser(url: &str) -> Result<()> {
121    #[cfg(target_os = "windows")]
122    {
123        std::process::Command::new("cmd")
124            .args(["/C", "start", url])
125            .spawn()
126            .context("Failed to open browser")?;
127    }
128
129    #[cfg(target_os = "macos")]
130    {
131        std::process::Command::new("open")
132            .arg(url)
133            .spawn()
134            .context("Failed to open browser")?;
135    }
136
137    #[cfg(target_os = "linux")]
138    {
139        std::process::Command::new("xdg-open")
140            .arg(url)
141            .spawn()
142            .context("Failed to open browser")?;
143    }
144
145    Ok(())
146}
147
148/// Daemonize the current process (Unix only)
149/// NOTE: This is a placeholder for Step 3. Actual daemon implementation
150/// will be done when we implement the HTTP server.
151#[cfg(unix)]
152pub fn daemonize() -> Result<()> {
153    // TODO: Step 3 will implement proper daemonization
154    // For now, this is just a placeholder
155    Ok(())
156}
157
158/// On Windows, daemonize placeholder
159#[cfg(windows)]
160pub fn daemonize() -> Result<()> {
161    // TODO: Step 3 will implement Windows service/detach
162    Ok(())
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_pid_file_operations() {
171        let port = 9999;
172        let pid = std::process::id();
173
174        // Write PID
175        write_pid_file(port, pid).unwrap();
176
177        // Read PID
178        let read_pid = read_pid_file(port).unwrap();
179        assert_eq!(read_pid, Some(pid));
180
181        // Delete PID
182        delete_pid_file(port).unwrap();
183
184        // Verify deleted
185        let read_pid_after = read_pid_file(port).unwrap();
186        assert_eq!(read_pid_after, None);
187    }
188
189    #[test]
190    fn test_is_process_running() {
191        let current_pid = std::process::id();
192
193        // Current process should be running
194        assert!(is_process_running(current_pid));
195
196        // Invalid PID should not be running
197        assert!(!is_process_running(999999));
198    }
199}