spotify_cli/cli/commands/
daemon.rs1use 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
15fn pid_file_path() -> PathBuf {
17 paths::pid_file().unwrap_or_else(|_| PathBuf::from("/tmp/spotify-cli.pid"))
18}
19
20pub async fn daemon_start() -> Response {
22 let pid_file = pid_file_path();
23
24 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 let _ = fs::remove_file(&pid_file);
35 }
36
37 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 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
85pub 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 #[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 let _ = fs::remove_file(&pid_file);
120
121 Response::success_with_payload(200, "Daemon stopped", serde_json::json!({ "pid": pid }))
122}
123
124pub 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
149pub async fn daemon_run() -> Response {
151 let pid_file = pid_file_path();
152
153 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 let event_poller = EventPoller::new(event_tx);
171 tokio::spawn(async move {
172 event_poller.run().await;
173 });
174
175 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 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 unsafe { libc::kill(pid as i32, 0) == 0 }
205 }
206
207 #[cfg(not(unix))]
208 {
209 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}