1use std::io::{BufRead, BufReader};
9use std::net::TcpListener;
10use std::path::PathBuf;
11use std::process::Command;
12
13use anyhow::Result;
14use console::style;
15
16#[derive(Debug, Clone)]
18pub enum DaemonSubcommand {
19 Start {
21 foreground: bool,
23 port: u16,
25 },
26 Stop,
28 Status,
30 Logs {
32 lines: usize,
34 follow: bool,
36 },
37}
38
39pub fn execute_daemon(cmd: DaemonSubcommand) -> Result<()> {
41 let acp_dir = PathBuf::from(".acp");
42 let pid_file = acp_dir.join("daemon.pid");
43 let log_file = acp_dir.join("daemon.log");
44
45 match cmd {
46 DaemonSubcommand::Start { foreground, port } => {
47 if let Some(pid) = read_pid_file(&pid_file) {
49 if is_process_running(pid) {
50 println!(
51 "{} Daemon already running with PID {}",
52 style("!").yellow(),
53 pid
54 );
55 return Ok(());
56 }
57 let _ = std::fs::remove_file(&pid_file);
59 }
60
61 if is_port_in_use(port) {
63 eprintln!("{} Port {} is already in use", style("✗").red(), port);
64 eprintln!(" Another process may be using this port.");
65 eprintln!(" Try a different port with: acp daemon start --port <PORT>");
66 return Err(anyhow::anyhow!("Port {} is already in use", port));
67 }
68
69 if !acp_dir.exists() {
71 std::fs::create_dir_all(&acp_dir)?;
72 }
73
74 let acpd_path = find_acpd_binary()?;
76
77 if foreground {
78 println!(
80 "{} Starting daemon in foreground mode...",
81 style("→").cyan()
82 );
83 let status = Command::new(&acpd_path)
84 .arg("--port")
85 .arg(port.to_string())
86 .arg("run")
87 .status()?;
88
89 if !status.success() {
90 eprintln!("{} Daemon exited with error", style("✗").red());
91 std::process::exit(1);
92 }
93 } else {
94 let log = std::fs::File::create(&log_file)?;
96 let log_err = log.try_clone()?;
97
98 let child = Command::new(&acpd_path)
99 .arg("--port")
100 .arg(port.to_string())
101 .arg("run")
102 .stdout(log)
103 .stderr(log_err)
104 .spawn()?;
105
106 let pid = child.id();
107 std::fs::write(&pid_file, format!("{}:{}", pid, port))?;
109
110 println!(
111 "{} Daemon started with PID {} (port {})",
112 style("✓").green(),
113 pid,
114 port
115 );
116 println!(" Log file: {}", log_file.display());
117 println!(" API: http://127.0.0.1:{}/health", port);
118 }
119 }
120
121 DaemonSubcommand::Stop => match read_pid_file(&pid_file) {
122 Some(pid) => {
123 if is_process_running(pid) {
124 #[cfg(unix)]
126 {
127 let _ = Command::new("kill")
128 .arg("-TERM")
129 .arg(pid.to_string())
130 .status();
131 }
132
133 #[cfg(not(unix))]
134 {
135 eprintln!(
136 "{} Stopping daemon not supported on this platform",
137 style("✗").red()
138 );
139 }
140
141 println!(
142 "{} Sent stop signal to daemon (PID {})",
143 style("✓").green(),
144 pid
145 );
146 } else {
147 println!(
148 "{} Daemon not running (stale PID file)",
149 style("!").yellow()
150 );
151 }
152 let _ = std::fs::remove_file(&pid_file);
153 }
154 None => {
155 println!("{} No daemon running", style("•").dim());
156 }
157 },
158
159 DaemonSubcommand::Status => match read_pid_file(&pid_file) {
160 Some(pid) => {
161 if is_process_running(pid) {
162 let port = read_port_from_pid_file(&pid_file).unwrap_or(9222);
164 println!(
165 "{} Daemon is running (PID {}, port {})",
166 style("✓").green(),
167 pid,
168 port
169 );
170
171 if let Ok(health) = check_daemon_health(port) {
173 println!(" Health: {}", health);
174 }
175 } else {
176 println!(
177 "{} Daemon not running (stale PID file)",
178 style("!").yellow()
179 );
180 let _ = std::fs::remove_file(&pid_file);
181 }
182 }
183 None => {
184 println!("{} Daemon not running", style("•").dim());
185 }
186 },
187
188 DaemonSubcommand::Logs { lines, follow } => {
189 if !log_file.exists() {
190 println!(
191 "{} No log file found at {}",
192 style("!").yellow(),
193 log_file.display()
194 );
195 return Ok(());
196 }
197
198 if follow {
199 let mut child = Command::new("tail")
201 .arg("-f")
202 .arg("-n")
203 .arg(lines.to_string())
204 .arg(&log_file)
205 .spawn()?;
206
207 child.wait()?;
208 } else {
209 let file = std::fs::File::open(&log_file)?;
211 let reader = BufReader::new(file);
212 let all_lines: Vec<String> = reader.lines().map_while(Result::ok).collect();
213 let start = if all_lines.len() > lines {
214 all_lines.len() - lines
215 } else {
216 0
217 };
218
219 for line in &all_lines[start..] {
220 println!("{}", line);
221 }
222 }
223 }
224 }
225
226 Ok(())
227}
228
229fn read_pid_file(path: &PathBuf) -> Option<u32> {
231 std::fs::read_to_string(path).ok().and_then(|s| {
232 let content = s.trim();
233 content
235 .split(':')
236 .next()
237 .and_then(|pid_str| pid_str.parse().ok())
238 })
239}
240
241fn read_port_from_pid_file(path: &PathBuf) -> Option<u16> {
243 std::fs::read_to_string(path).ok().and_then(|s| {
244 let parts: Vec<&str> = s.trim().split(':').collect();
245 if parts.len() >= 2 {
246 parts[1].parse().ok()
247 } else {
248 None
249 }
250 })
251}
252
253fn is_process_running(pid: u32) -> bool {
254 #[cfg(unix)]
255 {
256 Command::new("kill")
257 .arg("-0")
258 .arg(pid.to_string())
259 .status()
260 .map(|s| s.success())
261 .unwrap_or(false)
262 }
263
264 #[cfg(not(unix))]
265 {
266 true }
268}
269
270fn is_port_in_use(port: u16) -> bool {
271 TcpListener::bind(("127.0.0.1", port)).is_err()
272}
273
274fn find_acpd_binary() -> Result<PathBuf> {
275 if let Ok(output) = Command::new("which").arg("acpd").output() {
277 if output.status.success() {
278 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
279 if !path.is_empty() {
280 return Ok(PathBuf::from(path));
281 }
282 }
283 }
284
285 let current_exe = std::env::current_exe()?;
287 if let Some(bin_dir) = current_exe.parent() {
288 let acpd_path = bin_dir.join("acpd");
289 if acpd_path.exists() {
290 return Ok(acpd_path);
291 }
292 }
293
294 for dir in &["target/debug/acpd", "target/release/acpd"] {
296 let path = PathBuf::from(dir);
297 if path.exists() {
298 return Ok(path);
299 }
300 }
301
302 Err(anyhow::anyhow!(
303 "Could not find acpd binary. Make sure it's installed or built.\n\
304 Try: cargo build -p acpd"
305 ))
306}
307
308fn check_daemon_health(port: u16) -> std::result::Result<String, Box<dyn std::error::Error>> {
309 let output = Command::new("curl")
310 .arg("-s")
311 .arg("-m")
312 .arg("2") .arg(format!("http://127.0.0.1:{}/health", port))
314 .output()?;
315
316 if output.status.success() {
317 Ok(String::from_utf8_lossy(&output.stdout).to_string())
318 } else {
319 Err("Failed to connect".into())
320 }
321}