spotify_cli/cli/commands/
daemon.rs

1//! Daemon management commands
2
3use std::fs;
4use std::path::PathBuf;
5use std::process::Command;
6
7use crate::io::output::{ErrorKind, Response};
8use crate::rpc::events::EventPoller;
9use crate::rpc::{Server, ServerConfig};
10use crate::storage::paths;
11
12use tokio::signal;
13use tracing::info;
14
15/// Get the PID file path
16fn pid_file_path() -> PathBuf {
17    paths::pid_file().unwrap_or_else(|_| PathBuf::from("/tmp/spotify-cli.pid"))
18}
19
20/// Start the daemon in the background
21pub async fn daemon_start() -> Response {
22    let pid_file = pid_file_path();
23
24    // Check if already running
25    if let Some(pid) = read_pid(&pid_file) {
26        if is_process_running(pid) {
27            return Response::err(
28                409,
29                format!("Daemon already running (PID {})", pid),
30                ErrorKind::Validation,
31            );
32        }
33        // Stale PID file, remove it
34        let _ = fs::remove_file(&pid_file);
35    }
36
37    // Get the path to the current executable
38    let exe = match std::env::current_exe() {
39        Ok(path) => path,
40        Err(e) => {
41            return Response::err(
42                500,
43                format!("Failed to get executable path: {}", e),
44                ErrorKind::Storage,
45            );
46        }
47    };
48
49    // Spawn the daemon process
50    match Command::new(&exe)
51        .args(["daemon", "run"])
52        .stdin(std::process::Stdio::null())
53        .stdout(std::process::Stdio::null())
54        .stderr(std::process::Stdio::null())
55        .spawn()
56    {
57        Ok(child) => {
58            let pid = child.id();
59            if let Err(e) = fs::write(&pid_file, pid.to_string()) {
60                return Response::err(
61                    500,
62                    format!("Failed to write PID file: {}", e),
63                    ErrorKind::Storage,
64                );
65            }
66
67            let config = ServerConfig::default();
68            Response::success_with_payload(
69                200,
70                "Daemon started",
71                serde_json::json!({
72                    "pid": pid,
73                    "socket": config.socket_path.display().to_string(),
74                }),
75            )
76        }
77        Err(e) => Response::err(
78            500,
79            format!("Failed to start daemon: {}", e),
80            ErrorKind::Storage,
81        ),
82    }
83}
84
85/// Stop the running daemon
86pub async fn daemon_stop() -> Response {
87    let pid_file = pid_file_path();
88
89    let pid = match read_pid(&pid_file) {
90        Some(p) => p,
91        None => return Response::err(404, "Daemon not running (no PID file)", ErrorKind::NotFound),
92    };
93
94    if !is_process_running(pid) {
95        let _ = fs::remove_file(&pid_file);
96        return Response::err(
97            404,
98            "Daemon not running (stale PID file removed)",
99            ErrorKind::NotFound,
100        );
101    }
102
103    // Send SIGTERM to the process
104    #[cfg(unix)]
105    {
106        unsafe {
107            libc::kill(pid as i32, libc::SIGTERM);
108        }
109    }
110
111    #[cfg(not(unix))]
112    {
113        let _ = Command::new("taskkill")
114            .args(["/PID", &pid.to_string(), "/F"])
115            .output();
116    }
117
118    // Remove PID file
119    let _ = fs::remove_file(&pid_file);
120
121    Response::success_with_payload(200, "Daemon stopped", serde_json::json!({ "pid": pid }))
122}
123
124/// Check daemon status
125pub async fn daemon_status() -> Response {
126    let pid_file = pid_file_path();
127    let config = ServerConfig::default();
128
129    let pid = read_pid(&pid_file);
130    let running = pid.map(is_process_running).unwrap_or(false);
131    let socket_exists = config.socket_path.exists();
132
133    Response::success_with_payload(
134        200,
135        if running {
136            "Daemon running"
137        } else {
138            "Daemon not running"
139        },
140        serde_json::json!({
141            "running": running,
142            "pid": pid,
143            "socket": config.socket_path.display().to_string(),
144            "socket_exists": socket_exists,
145        }),
146    )
147}
148
149/// Run the daemon in the foreground
150pub async fn daemon_run() -> Response {
151    let pid_file = pid_file_path();
152
153    // Write our PID
154    let pid = std::process::id();
155    if let Err(e) = fs::write(&pid_file, pid.to_string()) {
156        return Response::err(
157            500,
158            format!("Failed to write PID file: {}", e),
159            ErrorKind::Storage,
160        );
161    }
162
163    let config = ServerConfig::default();
164    let server = Server::new(config);
165    let event_tx = server.event_sender();
166
167    info!(pid = pid, socket = %server.socket_path().display(), "Starting daemon");
168
169    // Spawn the event poller
170    let event_poller = EventPoller::new(event_tx);
171    tokio::spawn(async move {
172        event_poller.run().await;
173    });
174
175    // Run the server until interrupted
176    tokio::select! {
177        result = server.run() => {
178            if let Err(e) = result {
179                let _ = fs::remove_file(&pid_file);
180                return Response::err(500, format!("Server error: {}", e), ErrorKind::Storage);
181            }
182        }
183        _ = signal::ctrl_c() => {
184            info!("Received shutdown signal");
185        }
186    }
187
188    // Cleanup
189    let _ = fs::remove_file(&pid_file);
190
191    Response::success(200, "Daemon stopped")
192}
193
194fn read_pid(path: &PathBuf) -> Option<u32> {
195    fs::read_to_string(path)
196        .ok()
197        .and_then(|s| s.trim().parse().ok())
198}
199
200fn is_process_running(pid: u32) -> bool {
201    #[cfg(unix)]
202    {
203        // On Unix, sending signal 0 checks if process exists
204        unsafe { libc::kill(pid as i32, 0) == 0 }
205    }
206
207    #[cfg(not(unix))]
208    {
209        // On Windows, try to open the process
210        use std::process::Command;
211        Command::new("tasklist")
212            .args(["/FI", &format!("PID eq {}", pid)])
213            .output()
214            .map(|o| String::from_utf8_lossy(&o.stdout).contains(&pid.to_string()))
215            .unwrap_or(false)
216    }
217}