1use clap::Subcommand;
11use colored::Colorize;
12use std::time::Duration;
13
14use nika_daemon::lifecycle::{
15 check_pid_file, cleanup_stale_socket, daemonize, is_process_alive, remove_pid_file,
16 send_sigterm, wait_for_shutdown_signal, write_pid_file,
17};
18use nika_daemon::{
19 daemon_dir, daemon_log_path, daemon_pid_path, daemon_socket_path, DaemonClient, DaemonConfig,
20 DaemonResponse, DaemonServer,
21};
22use nika_engine::error::NikaError;
23
24#[derive(Subcommand)]
26pub enum DaemonAction {
27 Start {
29 #[arg(long)]
31 foreground: bool,
32 },
33
34 Stop,
36
37 Restart {
39 #[arg(long)]
41 foreground: bool,
42 },
43
44 Status,
46
47 Install,
49
50 Uninstall,
52
53 Logs {
55 #[arg(short, long)]
57 follow: bool,
58
59 #[arg(short = 'n', long, default_value = "20")]
61 lines: usize,
62 },
63}
64
65pub async fn handle_daemon_command(action: DaemonAction) -> Result<(), NikaError> {
66 match action {
67 DaemonAction::Start { foreground } => start_daemon(foreground).await,
68 DaemonAction::Stop => stop_daemon().await,
69 DaemonAction::Restart { foreground } => {
70 let _ = stop_daemon().await;
71 tokio::time::sleep(Duration::from_millis(500)).await;
72 start_daemon(foreground).await
73 }
74 DaemonAction::Status => show_status().await,
75 DaemonAction::Install => {
76 nika_daemon::install::install().map_err(daemon_err)?;
77 eprintln!("{} daemon service installed", "✓".green().bold());
78 Ok(())
79 }
80 DaemonAction::Uninstall => {
81 nika_daemon::install::uninstall().map_err(daemon_err)?;
82 eprintln!("{} daemon service removed", "✓".green().bold());
83 Ok(())
84 }
85 DaemonAction::Logs { follow, lines } => show_logs(follow, lines).await,
86 }
87}
88
89async fn start_daemon(foreground: bool) -> Result<(), NikaError> {
90 let socket_path = daemon_socket_path();
91 let pid_path = daemon_pid_path();
92
93 if let Some(pid) = check_pid_file(&pid_path).map_err(daemon_err)? {
95 eprintln!("{} daemon already running (pid {})", "✗".red().bold(), pid);
96 return Ok(());
97 }
98
99 let _ = cleanup_stale_socket(&socket_path, &pid_path);
101
102 std::fs::create_dir_all(daemon_dir()).ok();
104
105 if !foreground {
106 let log_path = daemon_log_path();
109 daemonize(&log_path).map_err(daemon_err)?;
110
111 tokio::time::sleep(Duration::from_millis(500)).await;
113 let client = DaemonClient::new(&socket_path).with_timeout(Duration::from_secs(3));
114 match client.ping().await {
115 Ok((version, _)) => {
116 eprintln!("{} daemon started (v{version})", "✓".green().bold());
117 eprintln!(" socket: {}", socket_path.display());
118 eprintln!(" logs: {}", daemon_log_path().display());
119 }
120 Err(_) => {
121 eprintln!(
122 "{} daemon spawned but not responding yet — check logs",
123 "⚠".yellow().bold()
124 );
125 eprintln!(" logs: {}", daemon_log_path().display());
126 }
127 }
128 return Ok(());
129 }
130
131 write_pid_file(&pid_path).map_err(daemon_err)?;
133
134 eprintln!(
135 "{} daemon starting in foreground (pid {})",
136 "▸".cyan().bold(),
137 std::process::id()
138 );
139 eprintln!(" socket: {}", socket_path.display());
140 eprintln!(" press Ctrl-C to stop");
141
142 let config = DaemonConfig {
144 socket_path: socket_path.clone(),
145 ..DaemonConfig::default()
146 };
147 let server = DaemonServer::new(config);
148 let shutdown = server.shutdown_handle();
149
150 tokio::spawn(async move {
152 wait_for_shutdown_signal().await;
153 let _ = shutdown.send(true);
154 });
155
156 server.run().await.map_err(daemon_err)?;
158
159 remove_pid_file(&pid_path);
161
162 if foreground {
163 eprintln!("{} daemon stopped", "✓".green().bold());
164 }
165
166 Ok(())
167}
168
169async fn stop_daemon() -> Result<(), NikaError> {
170 let pid_path = daemon_pid_path();
171
172 match check_pid_file(&pid_path).map_err(daemon_err)? {
173 Some(pid) => {
174 eprintln!("{} stopping daemon (pid {})", "▸".cyan().bold(), pid);
175
176 send_sigterm(pid).map_err(daemon_err)?;
178
179 for _ in 0..50 {
181 if !is_process_alive(pid) {
182 break;
183 }
184 tokio::time::sleep(Duration::from_millis(100)).await;
185 }
186
187 remove_pid_file(&pid_path);
189 let _ = std::fs::remove_file(daemon_socket_path());
190
191 eprintln!("{} daemon stopped", "✓".green().bold());
192 Ok(())
193 }
194 None => {
195 eprintln!("{} daemon is not running", "⚠".yellow().bold());
196 Ok(())
197 }
198 }
199}
200
201async fn show_status() -> Result<(), NikaError> {
202 let socket_path = daemon_socket_path();
203 let pid_path = daemon_pid_path();
204
205 match check_pid_file(&pid_path).map_err(daemon_err)? {
207 None => {
208 println!("{} daemon is not running", "●".red().bold());
209 println!(" socket: {}", socket_path.display());
210 println!(" start: nika daemon start");
211 return Ok(());
212 }
213 Some(pid) => {
214 let client = DaemonClient::new(&socket_path).with_timeout(Duration::from_secs(2));
216
217 match client.send(nika_daemon::DaemonRequest::Status).await {
218 Ok(DaemonResponse::StatusInfo {
219 pid,
220 uptime_secs,
221 services,
222 }) => {
223 println!("{} daemon is running", "●".green().bold());
224 println!(" pid: {}", pid);
225 println!(" uptime: {}", format_uptime(uptime_secs));
226 println!(" socket: {}", socket_path.display());
227 println!(" services: {}", services.join(", "));
228 }
229 Ok(other) => {
230 println!(
231 "{} daemon responded unexpectedly: {:?}",
232 "●".yellow().bold(),
233 other
234 );
235 }
236 Err(_) => {
237 println!(
238 "{} daemon PID {} exists but not responding",
239 "●".yellow().bold(),
240 pid
241 );
242 println!(" socket: {}", socket_path.display());
243 println!(" try: nika daemon restart");
244 }
245 }
246 }
247 }
248
249 Ok(())
250}
251
252async fn show_logs(follow: bool, lines: usize) -> Result<(), NikaError> {
253 let log_path = daemon_log_path();
254
255 if !log_path.exists() {
256 eprintln!(
257 "{} no daemon log file found at {}",
258 "⚠".yellow().bold(),
259 log_path.display()
260 );
261 return Ok(());
262 }
263
264 if follow {
265 let mut child = tokio::process::Command::new("tail")
267 .args(["-f", "-n", &lines.to_string()])
268 .arg(&log_path)
269 .spawn()
270 .map_err(|e| NikaError::Execution(format!("failed to spawn tail: {e}")))?;
271
272 child
273 .wait()
274 .await
275 .map_err(|e| NikaError::Execution(format!("tail failed: {e}")))?;
276 } else {
277 let content = tokio::fs::read_to_string(&log_path)
278 .await
279 .map_err(|e| NikaError::Execution(format!("failed to read log: {e}")))?;
280
281 let all_lines: Vec<&str> = content.lines().collect();
282 let start = all_lines.len().saturating_sub(lines);
283 for line in &all_lines[start..] {
284 println!("{}", line);
285 }
286 }
287
288 Ok(())
289}
290
291fn format_uptime(secs: u64) -> String {
292 let hours = secs / 3600;
293 let mins = (secs % 3600) / 60;
294 let secs = secs % 60;
295 if hours > 0 {
296 format!("{}h {}m {}s", hours, mins, secs)
297 } else if mins > 0 {
298 format!("{}m {}s", mins, secs)
299 } else {
300 format!("{}s", secs)
301 }
302}
303
304fn daemon_err(e: nika_daemon::DaemonError) -> NikaError {
305 NikaError::Execution(format!("daemon: {e}"))
306}
307
308#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn format_uptime_seconds() {
318 assert_eq!(format_uptime(42), "42s");
319 }
320
321 #[test]
322 fn format_uptime_minutes() {
323 assert_eq!(format_uptime(125), "2m 5s");
324 }
325
326 #[test]
327 fn format_uptime_hours() {
328 assert_eq!(format_uptime(3661), "1h 1m 1s");
329 }
330
331 #[test]
332 fn daemon_action_variants_exist() {
333 let _ = DaemonAction::Start { foreground: false };
335 let _ = DaemonAction::Stop;
336 let _ = DaemonAction::Restart { foreground: false };
337 let _ = DaemonAction::Status;
338 let _ = DaemonAction::Logs {
339 follow: false,
340 lines: 20,
341 };
342 }
343}