diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 706 |
1 files changed, 706 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e0545e6 --- /dev/null +++ b/src/main.rs | |||
| @@ -0,0 +1,706 @@ | |||
| 1 | use clap::{Parser, Subcommand, Args}; | ||
| 2 | use std::fs::File; | ||
| 3 | use std::io::{Write, Read, Seek, SeekFrom}; | ||
| 4 | use std::process::{Command, Stdio}; | ||
| 5 | use std::thread; | ||
| 6 | use std::time::Duration; | ||
| 7 | use std::path::Path; | ||
| 8 | use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; | ||
| 9 | use std::sync::mpsc::channel; | ||
| 10 | use glob::glob; | ||
| 11 | |||
| 12 | #[derive(Parser)] | ||
| 13 | #[command(name = "demon")] | ||
| 14 | #[command(about = "A daemon process management CLI", long_about = None)] | ||
| 15 | #[command(version)] | ||
| 16 | #[command(propagate_version = true)] | ||
| 17 | struct Cli { | ||
| 18 | #[command(subcommand)] | ||
| 19 | command: Commands, | ||
| 20 | } | ||
| 21 | |||
| 22 | #[derive(Subcommand)] | ||
| 23 | enum Commands { | ||
| 24 | /// Spawn a background process and redirect stdout/stderr to files | ||
| 25 | Run(RunArgs), | ||
| 26 | |||
| 27 | /// Stop a running daemon process | ||
| 28 | Stop(StopArgs), | ||
| 29 | |||
| 30 | /// Tail daemon logs in real-time | ||
| 31 | Tail(TailArgs), | ||
| 32 | |||
| 33 | /// Display daemon log contents | ||
| 34 | Cat(CatArgs), | ||
| 35 | |||
| 36 | /// List all running daemon processes | ||
| 37 | List, | ||
| 38 | |||
| 39 | /// Check status of a daemon process | ||
| 40 | Status(StatusArgs), | ||
| 41 | |||
| 42 | /// Clean up orphaned pid and log files | ||
| 43 | Clean, | ||
| 44 | } | ||
| 45 | |||
| 46 | #[derive(Args)] | ||
| 47 | struct RunArgs { | ||
| 48 | /// Process identifier | ||
| 49 | #[arg(long)] | ||
| 50 | id: String, | ||
| 51 | |||
| 52 | /// Command and arguments to execute | ||
| 53 | command: Vec<String>, | ||
| 54 | } | ||
| 55 | |||
| 56 | #[derive(Args)] | ||
| 57 | struct StopArgs { | ||
| 58 | /// Process identifier | ||
| 59 | #[arg(long)] | ||
| 60 | id: String, | ||
| 61 | |||
| 62 | /// Timeout in seconds before sending SIGKILL after SIGTERM | ||
| 63 | #[arg(long, default_value = "10")] | ||
| 64 | timeout: u64, | ||
| 65 | } | ||
| 66 | |||
| 67 | #[derive(Args)] | ||
| 68 | struct TailArgs { | ||
| 69 | /// Process identifier | ||
| 70 | #[arg(long)] | ||
| 71 | id: String, | ||
| 72 | |||
| 73 | /// Only tail stdout | ||
| 74 | #[arg(long)] | ||
| 75 | stdout: bool, | ||
| 76 | |||
| 77 | /// Only tail stderr | ||
| 78 | #[arg(long)] | ||
| 79 | stderr: bool, | ||
| 80 | } | ||
| 81 | |||
| 82 | #[derive(Args)] | ||
| 83 | struct CatArgs { | ||
| 84 | /// Process identifier | ||
| 85 | #[arg(long)] | ||
| 86 | id: String, | ||
| 87 | |||
| 88 | /// Only show stdout | ||
| 89 | #[arg(long)] | ||
| 90 | stdout: bool, | ||
| 91 | |||
| 92 | /// Only show stderr | ||
| 93 | #[arg(long)] | ||
| 94 | stderr: bool, | ||
| 95 | } | ||
| 96 | |||
| 97 | #[derive(Args)] | ||
| 98 | struct StatusArgs { | ||
| 99 | /// Process identifier | ||
| 100 | #[arg(long)] | ||
| 101 | id: String, | ||
| 102 | } | ||
| 103 | |||
| 104 | fn main() { | ||
| 105 | tracing_subscriber::fmt() | ||
| 106 | .with_writer(std::io::stderr) | ||
| 107 | .init(); | ||
| 108 | |||
| 109 | let cli = Cli::parse(); | ||
| 110 | |||
| 111 | if let Err(e) = run_command(cli.command) { | ||
| 112 | tracing::error!("Error: {}", e); | ||
| 113 | std::process::exit(1); | ||
| 114 | } | ||
| 115 | } | ||
| 116 | |||
| 117 | fn run_command(command: Commands) -> Result<(), Box<dyn std::error::Error>> { | ||
| 118 | match command { | ||
| 119 | Commands::Run(args) => { | ||
| 120 | if args.command.is_empty() { | ||
| 121 | return Err("Command cannot be empty".into()); | ||
| 122 | } | ||
| 123 | run_daemon(&args.id, &args.command) | ||
| 124 | } | ||
| 125 | Commands::Stop(args) => { | ||
| 126 | stop_daemon(&args.id, args.timeout) | ||
| 127 | } | ||
| 128 | Commands::Tail(args) => { | ||
| 129 | let show_stdout = !args.stderr || args.stdout; | ||
| 130 | let show_stderr = !args.stdout || args.stderr; | ||
| 131 | tail_logs(&args.id, show_stdout, show_stderr) | ||
| 132 | } | ||
| 133 | Commands::Cat(args) => { | ||
| 134 | let show_stdout = !args.stderr || args.stdout; | ||
| 135 | let show_stderr = !args.stdout || args.stderr; | ||
| 136 | cat_logs(&args.id, show_stdout, show_stderr) | ||
| 137 | } | ||
| 138 | Commands::List => { | ||
| 139 | list_daemons() | ||
| 140 | } | ||
| 141 | Commands::Status(args) => { | ||
| 142 | status_daemon(&args.id) | ||
| 143 | } | ||
| 144 | Commands::Clean => { | ||
| 145 | clean_orphaned_files() | ||
| 146 | } | ||
| 147 | } | ||
| 148 | } | ||
| 149 | |||
| 150 | fn run_daemon(id: &str, command: &[String]) -> Result<(), Box<dyn std::error::Error>> { | ||
| 151 | let pid_file = format!("{}.pid", id); | ||
| 152 | let stdout_file = format!("{}.stdout", id); | ||
| 153 | let stderr_file = format!("{}.stderr", id); | ||
| 154 | |||
| 155 | // Check if process is already running | ||
| 156 | if is_process_running(&pid_file)? { | ||
| 157 | return Err(format!("Process '{}' is already running", id).into()); | ||
| 158 | } | ||
| 159 | |||
| 160 | tracing::info!("Starting daemon '{}' with command: {:?}", id, command); | ||
| 161 | |||
| 162 | // Truncate/create output files | ||
| 163 | File::create(&stdout_file)?; | ||
| 164 | File::create(&stderr_file)?; | ||
| 165 | |||
| 166 | // Open files for redirection | ||
| 167 | let stdout_redirect = File::create(&stdout_file)?; | ||
| 168 | let stderr_redirect = File::create(&stderr_file)?; | ||
| 169 | |||
| 170 | // Spawn the process | ||
| 171 | let program = &command[0]; | ||
| 172 | let args = if command.len() > 1 { &command[1..] } else { &[] }; | ||
| 173 | |||
| 174 | let child = Command::new(program) | ||
| 175 | .args(args) | ||
| 176 | .stdout(Stdio::from(stdout_redirect)) | ||
| 177 | .stderr(Stdio::from(stderr_redirect)) | ||
| 178 | .stdin(Stdio::null()) | ||
| 179 | .spawn() | ||
| 180 | .map_err(|e| format!("Failed to start process '{}': {}", program, e))?; | ||
| 181 | |||
| 182 | // Write PID to file | ||
| 183 | let mut pid_file_handle = File::create(&pid_file)?; | ||
| 184 | writeln!(pid_file_handle, "{}", child.id())?; | ||
| 185 | |||
| 186 | // Don't wait for the child - let it run detached | ||
| 187 | std::mem::forget(child); | ||
| 188 | |||
| 189 | println!("Started daemon '{}' with PID written to {}", id, pid_file); | ||
| 190 | |||
| 191 | Ok(()) | ||
| 192 | } | ||
| 193 | |||
| 194 | fn is_process_running(pid_file: &str) -> Result<bool, Box<dyn std::error::Error>> { | ||
| 195 | // Try to read the PID file | ||
| 196 | let mut file = match File::open(pid_file) { | ||
| 197 | Ok(f) => f, | ||
| 198 | Err(_) => return Ok(false), // No PID file means no running process | ||
| 199 | }; | ||
| 200 | |||
| 201 | let mut contents = String::new(); | ||
| 202 | file.read_to_string(&mut contents)?; | ||
| 203 | |||
| 204 | let pid: u32 = match contents.trim().parse() { | ||
| 205 | Ok(p) => p, | ||
| 206 | Err(_) => return Ok(false), // Invalid PID file | ||
| 207 | }; | ||
| 208 | |||
| 209 | // Check if process is still running using kill -0 | ||
| 210 | let output = Command::new("kill") | ||
| 211 | .args(&["-0", &pid.to_string()]) | ||
| 212 | .output()?; | ||
| 213 | |||
| 214 | Ok(output.status.success()) | ||
| 215 | } | ||
| 216 | |||
| 217 | fn stop_daemon(id: &str, timeout: u64) -> Result<(), Box<dyn std::error::Error>> { | ||
| 218 | let pid_file = format!("{}.pid", id); | ||
| 219 | |||
| 220 | // Check if PID file exists | ||
| 221 | let mut file = match File::open(&pid_file) { | ||
| 222 | Ok(f) => f, | ||
| 223 | Err(_) => { | ||
| 224 | println!("Process '{}' is not running (no PID file found)", id); | ||
| 225 | return Ok(()); | ||
| 226 | } | ||
| 227 | }; | ||
| 228 | |||
| 229 | // Read PID | ||
| 230 | let mut contents = String::new(); | ||
| 231 | file.read_to_string(&mut contents)?; | ||
| 232 | |||
| 233 | let pid: u32 = match contents.trim().parse() { | ||
| 234 | Ok(p) => p, | ||
| 235 | Err(_) => { | ||
| 236 | println!("Process '{}': invalid PID file, removing it", id); | ||
| 237 | std::fs::remove_file(&pid_file)?; | ||
| 238 | return Ok(()); | ||
| 239 | } | ||
| 240 | }; | ||
| 241 | |||
| 242 | tracing::info!("Stopping daemon '{}' (PID: {}) with timeout {}s", id, pid, timeout); | ||
| 243 | |||
| 244 | // Check if process is running | ||
| 245 | if !is_process_running_by_pid(pid) { | ||
| 246 | println!("Process '{}' (PID: {}) is not running, cleaning up PID file", id, pid); | ||
| 247 | std::fs::remove_file(&pid_file)?; | ||
| 248 | return Ok(()); | ||
| 249 | } | ||
| 250 | |||
| 251 | // Send SIGTERM | ||
| 252 | tracing::info!("Sending SIGTERM to PID {}", pid); | ||
| 253 | let output = Command::new("kill") | ||
| 254 | .args(&["-TERM", &pid.to_string()]) | ||
| 255 | .output()?; | ||
| 256 | |||
| 257 | if !output.status.success() { | ||
| 258 | return Err(format!("Failed to send SIGTERM to PID {}", pid).into()); | ||
| 259 | } | ||
| 260 | |||
| 261 | // Wait for the process to terminate | ||
| 262 | for i in 0..timeout { | ||
| 263 | if !is_process_running_by_pid(pid) { | ||
| 264 | println!("Process '{}' (PID: {}) terminated gracefully", id, pid); | ||
| 265 | std::fs::remove_file(&pid_file)?; | ||
| 266 | return Ok(()); | ||
| 267 | } | ||
| 268 | |||
| 269 | if i == 0 { | ||
| 270 | tracing::info!("Waiting for process to terminate gracefully..."); | ||
| 271 | } | ||
| 272 | |||
| 273 | thread::sleep(Duration::from_secs(1)); | ||
| 274 | } | ||
| 275 | |||
| 276 | // Process didn't terminate, send SIGKILL | ||
| 277 | tracing::warn!("Process {} didn't terminate after {}s, sending SIGKILL", pid, timeout); | ||
| 278 | let output = Command::new("kill") | ||
| 279 | .args(&["-KILL", &pid.to_string()]) | ||
| 280 | .output()?; | ||
| 281 | |||
| 282 | if !output.status.success() { | ||
| 283 | return Err(format!("Failed to send SIGKILL to PID {}", pid).into()); | ||
| 284 | } | ||
| 285 | |||
| 286 | // Wait a bit more for SIGKILL to take effect | ||
| 287 | thread::sleep(Duration::from_secs(1)); | ||
| 288 | |||
| 289 | if is_process_running_by_pid(pid) { | ||
| 290 | return Err(format!("Process {} is still running after SIGKILL", pid).into()); | ||
| 291 | } | ||
| 292 | |||
| 293 | println!("Process '{}' (PID: {}) terminated forcefully", id, pid); | ||
| 294 | std::fs::remove_file(&pid_file)?; | ||
| 295 | |||
| 296 | Ok(()) | ||
| 297 | } | ||
| 298 | |||
| 299 | fn is_process_running_by_pid(pid: u32) -> bool { | ||
| 300 | let output = Command::new("kill") | ||
| 301 | .args(&["-0", &pid.to_string()]) | ||
| 302 | .output(); | ||
| 303 | |||
| 304 | match output { | ||
| 305 | Ok(output) => output.status.success(), | ||
| 306 | Err(_) => false, | ||
| 307 | } | ||
| 308 | } | ||
| 309 | |||
| 310 | fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<(), Box<dyn std::error::Error>> { | ||
| 311 | let stdout_file = format!("{}.stdout", id); | ||
| 312 | let stderr_file = format!("{}.stderr", id); | ||
| 313 | |||
| 314 | let mut files_found = false; | ||
| 315 | |||
| 316 | if show_stdout { | ||
| 317 | if let Ok(contents) = std::fs::read_to_string(&stdout_file) { | ||
| 318 | if !contents.is_empty() { | ||
| 319 | files_found = true; | ||
| 320 | if show_stderr { | ||
| 321 | println!("==> {} <==", stdout_file); | ||
| 322 | } | ||
| 323 | print!("{}", contents); | ||
| 324 | } | ||
| 325 | } else { | ||
| 326 | tracing::warn!("Could not read {}", stdout_file); | ||
| 327 | } | ||
| 328 | } | ||
| 329 | |||
| 330 | if show_stderr { | ||
| 331 | if let Ok(contents) = std::fs::read_to_string(&stderr_file) { | ||
| 332 | if !contents.is_empty() { | ||
| 333 | files_found = true; | ||
| 334 | if show_stdout { | ||
| 335 | println!("==> {} <==", stderr_file); | ||
| 336 | } | ||
| 337 | print!("{}", contents); | ||
| 338 | } | ||
| 339 | } else { | ||
| 340 | tracing::warn!("Could not read {}", stderr_file); | ||
| 341 | } | ||
| 342 | } | ||
| 343 | |||
| 344 | if !files_found { | ||
| 345 | println!("No log files found for daemon '{}'", id); | ||
| 346 | } | ||
| 347 | |||
| 348 | Ok(()) | ||
| 349 | } | ||
| 350 | |||
| 351 | fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<(), Box<dyn std::error::Error>> { | ||
| 352 | let stdout_file = format!("{}.stdout", id); | ||
| 353 | let stderr_file = format!("{}.stderr", id); | ||
| 354 | |||
| 355 | // First, display existing content and set up initial positions | ||
| 356 | let mut file_positions: std::collections::HashMap<String, u64> = std::collections::HashMap::new(); | ||
| 357 | |||
| 358 | if show_stdout && Path::new(&stdout_file).exists() { | ||
| 359 | let mut file = File::open(&stdout_file)?; | ||
| 360 | let initial_content = read_file_content(&mut file)?; | ||
| 361 | if !initial_content.is_empty() { | ||
| 362 | if show_stderr { | ||
| 363 | println!("==> {} <==", stdout_file); | ||
| 364 | } | ||
| 365 | print!("{}", initial_content); | ||
| 366 | } | ||
| 367 | let position = file.seek(SeekFrom::Current(0))?; | ||
| 368 | file_positions.insert(stdout_file.clone(), position); | ||
| 369 | } | ||
| 370 | |||
| 371 | if show_stderr && Path::new(&stderr_file).exists() { | ||
| 372 | let mut file = File::open(&stderr_file)?; | ||
| 373 | let initial_content = read_file_content(&mut file)?; | ||
| 374 | if !initial_content.is_empty() { | ||
| 375 | if show_stdout && file_positions.len() > 0 { | ||
| 376 | println!("\n==> {} <==", stderr_file); | ||
| 377 | } else if show_stdout { | ||
| 378 | println!("==> {} <==", stderr_file); | ||
| 379 | } | ||
| 380 | print!("{}", initial_content); | ||
| 381 | } | ||
| 382 | let position = file.seek(SeekFrom::Current(0))?; | ||
| 383 | file_positions.insert(stderr_file.clone(), position); | ||
| 384 | } | ||
| 385 | |||
| 386 | if file_positions.is_empty() { | ||
| 387 | println!("No log files found for daemon '{}'. Watching for new files...", id); | ||
| 388 | } | ||
| 389 | |||
| 390 | tracing::info!("Watching for changes to log files... Press Ctrl+C to stop."); | ||
| 391 | |||
| 392 | // Set up file watcher | ||
| 393 | let (tx, rx) = channel(); | ||
| 394 | let mut watcher = RecommendedWatcher::new(tx, Config::default())?; | ||
| 395 | |||
| 396 | // Watch the current directory for new files and changes | ||
| 397 | watcher.watch(Path::new("."), RecursiveMode::NonRecursive)?; | ||
| 398 | |||
| 399 | // Handle Ctrl+C gracefully | ||
| 400 | let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); | ||
| 401 | let r = running.clone(); | ||
| 402 | |||
| 403 | ctrlc::set_handler(move || { | ||
| 404 | r.store(false, std::sync::atomic::Ordering::SeqCst); | ||
| 405 | })?; | ||
| 406 | |||
| 407 | while running.load(std::sync::atomic::Ordering::SeqCst) { | ||
| 408 | match rx.recv_timeout(Duration::from_millis(100)) { | ||
| 409 | Ok(res) => { | ||
| 410 | match res { | ||
| 411 | Ok(Event { | ||
| 412 | kind: EventKind::Modify(_), | ||
| 413 | paths, | ||
| 414 | .. | ||
| 415 | }) => { | ||
| 416 | for path in paths { | ||
| 417 | let path_str = path.to_string_lossy().to_string(); | ||
| 418 | |||
| 419 | if (show_stdout && path_str == stdout_file) || | ||
| 420 | (show_stderr && path_str == stderr_file) { | ||
| 421 | |||
| 422 | if let Err(e) = handle_file_change(&path_str, &mut file_positions, show_stdout && show_stderr) { | ||
| 423 | tracing::error!("Error handling file change: {}", e); | ||
| 424 | } | ||
| 425 | } | ||
| 426 | } | ||
| 427 | } | ||
| 428 | Ok(Event { | ||
| 429 | kind: EventKind::Create(_), | ||
| 430 | paths, | ||
| 431 | .. | ||
| 432 | }) => { | ||
| 433 | // Handle file creation | ||
| 434 | for path in paths { | ||
| 435 | let path_str = path.to_string_lossy().to_string(); | ||
| 436 | |||
| 437 | if (show_stdout && path_str == stdout_file) || | ||
| 438 | (show_stderr && path_str == stderr_file) { | ||
| 439 | |||
| 440 | tracing::info!("New file detected: {}", path_str); | ||
| 441 | file_positions.insert(path_str.clone(), 0); | ||
| 442 | |||
| 443 | if let Err(e) = handle_file_change(&path_str, &mut file_positions, show_stdout && show_stderr) { | ||
| 444 | tracing::error!("Error handling new file: {}", e); | ||
| 445 | } | ||
| 446 | } | ||
| 447 | } | ||
| 448 | } | ||
| 449 | Ok(_) => {} // Ignore other events | ||
| 450 | Err(e) => tracing::error!("Watch error: {:?}", e), | ||
| 451 | } | ||
| 452 | } | ||
| 453 | Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { | ||
| 454 | // Timeout is normal, just continue | ||
| 455 | } | ||
| 456 | Err(e) => { | ||
| 457 | tracing::error!("Receive error: {}", e); | ||
| 458 | break; | ||
| 459 | } | ||
| 460 | } | ||
| 461 | } | ||
| 462 | |||
| 463 | println!("\nTailing stopped."); | ||
| 464 | Ok(()) | ||
| 465 | } | ||
| 466 | |||
| 467 | fn read_file_content(file: &mut File) -> Result<String, Box<dyn std::error::Error>> { | ||
| 468 | let mut content = String::new(); | ||
| 469 | file.read_to_string(&mut content)?; | ||
| 470 | Ok(content) | ||
| 471 | } | ||
| 472 | |||
| 473 | fn handle_file_change( | ||
| 474 | file_path: &str, | ||
| 475 | positions: &mut std::collections::HashMap<String, u64>, | ||
| 476 | show_headers: bool | ||
| 477 | ) -> Result<(), Box<dyn std::error::Error>> { | ||
| 478 | let mut file = File::open(file_path)?; | ||
| 479 | let current_pos = positions.get(file_path).copied().unwrap_or(0); | ||
| 480 | |||
| 481 | // Seek to the last read position | ||
| 482 | file.seek(SeekFrom::Start(current_pos))?; | ||
| 483 | |||
| 484 | // Read new content | ||
| 485 | let mut new_content = String::new(); | ||
| 486 | file.read_to_string(&mut new_content)?; | ||
| 487 | |||
| 488 | if !new_content.is_empty() { | ||
| 489 | if show_headers { | ||
| 490 | println!("==> {} <==", file_path); | ||
| 491 | } | ||
| 492 | print!("{}", new_content); | ||
| 493 | std::io::Write::flush(&mut std::io::stdout())?; | ||
| 494 | |||
| 495 | // Update position | ||
| 496 | let new_pos = file.seek(SeekFrom::Current(0))?; | ||
| 497 | positions.insert(file_path.to_string(), new_pos); | ||
| 498 | } | ||
| 499 | |||
| 500 | Ok(()) | ||
| 501 | } | ||
| 502 | |||
| 503 | fn list_daemons() -> Result<(), Box<dyn std::error::Error>> { | ||
| 504 | println!("{:<20} {:<8} {:<10} {}", "ID", "PID", "STATUS", "COMMAND"); | ||
| 505 | println!("{}", "-".repeat(50)); | ||
| 506 | |||
| 507 | let mut found_any = false; | ||
| 508 | |||
| 509 | // Find all .pid files in current directory | ||
| 510 | for entry in glob("*.pid")? { | ||
| 511 | match entry { | ||
| 512 | Ok(path) => { | ||
| 513 | found_any = true; | ||
| 514 | let path_str = path.to_string_lossy(); | ||
| 515 | |||
| 516 | // Extract ID from filename (remove .pid extension) | ||
| 517 | let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); | ||
| 518 | |||
| 519 | // Read PID from file | ||
| 520 | match std::fs::read_to_string(&path) { | ||
| 521 | Ok(contents) => { | ||
| 522 | let pid_str = contents.trim(); | ||
| 523 | match pid_str.parse::<u32>() { | ||
| 524 | Ok(pid) => { | ||
| 525 | let status = if is_process_running_by_pid(pid) { | ||
| 526 | "RUNNING" | ||
| 527 | } else { | ||
| 528 | "DEAD" | ||
| 529 | }; | ||
| 530 | |||
| 531 | // Try to read command from a hypothetical command file | ||
| 532 | // For now, we'll just show "N/A" since we don't store the command | ||
| 533 | let command = "N/A"; | ||
| 534 | |||
| 535 | println!("{:<20} {:<8} {:<10} {}", id, pid, status, command); | ||
| 536 | } | ||
| 537 | Err(_) => { | ||
| 538 | println!("{:<20} {:<8} {:<10} {}", id, "INVALID", "ERROR", "Invalid PID file"); | ||
| 539 | } | ||
| 540 | } | ||
| 541 | } | ||
| 542 | Err(e) => { | ||
| 543 | println!("{:<20} {:<8} {:<10} {}", id, "ERROR", "ERROR", format!("Cannot read: {}", e)); | ||
| 544 | } | ||
| 545 | } | ||
| 546 | } | ||
| 547 | Err(e) => { | ||
| 548 | tracing::warn!("Error reading glob entry: {}", e); | ||
| 549 | } | ||
| 550 | } | ||
| 551 | } | ||
| 552 | |||
| 553 | if !found_any { | ||
| 554 | println!("No daemon processes found."); | ||
| 555 | } | ||
| 556 | |||
| 557 | Ok(()) | ||
| 558 | } | ||
| 559 | |||
| 560 | fn status_daemon(id: &str) -> Result<(), Box<dyn std::error::Error>> { | ||
| 561 | let pid_file = format!("{}.pid", id); | ||
| 562 | let stdout_file = format!("{}.stdout", id); | ||
| 563 | let stderr_file = format!("{}.stderr", id); | ||
| 564 | |||
| 565 | println!("Daemon: {}", id); | ||
| 566 | println!("PID file: {}", pid_file); | ||
| 567 | |||
| 568 | // Check if PID file exists | ||
| 569 | if !Path::new(&pid_file).exists() { | ||
| 570 | println!("Status: NOT FOUND (no PID file)"); | ||
| 571 | return Ok(()); | ||
| 572 | } | ||
| 573 | |||
| 574 | // Read PID from file | ||
| 575 | match std::fs::read_to_string(&pid_file) { | ||
| 576 | Ok(contents) => { | ||
| 577 | let pid_str = contents.trim(); | ||
| 578 | match pid_str.parse::<u32>() { | ||
| 579 | Ok(pid) => { | ||
| 580 | println!("PID: {}", pid); | ||
| 581 | |||
| 582 | if is_process_running_by_pid(pid) { | ||
| 583 | println!("Status: RUNNING"); | ||
| 584 | |||
| 585 | // Show file information | ||
| 586 | if Path::new(&stdout_file).exists() { | ||
| 587 | let metadata = std::fs::metadata(&stdout_file)?; | ||
| 588 | println!("Stdout file: {} ({} bytes)", stdout_file, metadata.len()); | ||
| 589 | } else { | ||
| 590 | println!("Stdout file: {} (not found)", stdout_file); | ||
| 591 | } | ||
| 592 | |||
| 593 | if Path::new(&stderr_file).exists() { | ||
| 594 | let metadata = std::fs::metadata(&stderr_file)?; | ||
| 595 | println!("Stderr file: {} ({} bytes)", stderr_file, metadata.len()); | ||
| 596 | } else { | ||
| 597 | println!("Stderr file: {} (not found)", stderr_file); | ||
| 598 | } | ||
| 599 | } else { | ||
| 600 | println!("Status: DEAD (process not running)"); | ||
| 601 | println!("Note: Use 'demon clean' to remove orphaned files"); | ||
| 602 | } | ||
| 603 | } | ||
| 604 | Err(_) => { | ||
| 605 | println!("Status: ERROR (invalid PID in file)"); | ||
| 606 | } | ||
| 607 | } | ||
| 608 | } | ||
| 609 | Err(e) => { | ||
| 610 | println!("Status: ERROR (cannot read PID file: {})", e); | ||
| 611 | } | ||
| 612 | } | ||
| 613 | |||
| 614 | Ok(()) | ||
| 615 | } | ||
| 616 | |||
| 617 | fn clean_orphaned_files() -> Result<(), Box<dyn std::error::Error>> { | ||
| 618 | tracing::info!("Scanning for orphaned daemon files..."); | ||
| 619 | |||
| 620 | let mut cleaned_count = 0; | ||
| 621 | |||
| 622 | // Find all .pid files in current directory | ||
| 623 | for entry in glob("*.pid")? { | ||
| 624 | match entry { | ||
| 625 | Ok(path) => { | ||
| 626 | let path_str = path.to_string_lossy(); | ||
| 627 | let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); | ||
| 628 | |||
| 629 | // Read PID from file | ||
| 630 | match std::fs::read_to_string(&path) { | ||
| 631 | Ok(contents) => { | ||
| 632 | let pid_str = contents.trim(); | ||
| 633 | match pid_str.parse::<u32>() { | ||
| 634 | Ok(pid) => { | ||
| 635 | // Check if process is still running | ||
| 636 | if !is_process_running_by_pid(pid) { | ||
| 637 | println!("Cleaning up orphaned files for '{}' (PID: {})", id, pid); | ||
| 638 | |||
| 639 | // Remove PID file | ||
| 640 | if let Err(e) = std::fs::remove_file(&path) { | ||
| 641 | tracing::warn!("Failed to remove {}: {}", path_str, e); | ||
| 642 | } else { | ||
| 643 | tracing::info!("Removed {}", path_str); | ||
| 644 | } | ||
| 645 | |||
| 646 | // Remove stdout file if it exists | ||
| 647 | let stdout_file = format!("{}.stdout", id); | ||
| 648 | if Path::new(&stdout_file).exists() { | ||
| 649 | if let Err(e) = std::fs::remove_file(&stdout_file) { | ||
| 650 | tracing::warn!("Failed to remove {}: {}", stdout_file, e); | ||
| 651 | } else { | ||
| 652 | tracing::info!("Removed {}", stdout_file); | ||
| 653 | } | ||
| 654 | } | ||
| 655 | |||
| 656 | // Remove stderr file if it exists | ||
| 657 | let stderr_file = format!("{}.stderr", id); | ||
| 658 | if Path::new(&stderr_file).exists() { | ||
| 659 | if let Err(e) = std::fs::remove_file(&stderr_file) { | ||
| 660 | tracing::warn!("Failed to remove {}: {}", stderr_file, e); | ||
| 661 | } else { | ||
| 662 | tracing::info!("Removed {}", stderr_file); | ||
| 663 | } | ||
| 664 | } | ||
| 665 | |||
| 666 | cleaned_count += 1; | ||
| 667 | } else { | ||
| 668 | tracing::info!("Skipping '{}' (PID: {}) - process is still running", id, pid); | ||
| 669 | } | ||
| 670 | } | ||
| 671 | Err(_) => { | ||
| 672 | println!("Cleaning up invalid PID file: {}", path_str); | ||
| 673 | if let Err(e) = std::fs::remove_file(&path) { | ||
| 674 | tracing::warn!("Failed to remove invalid PID file {}: {}", path_str, e); | ||
| 675 | } else { | ||
| 676 | tracing::info!("Removed invalid PID file {}", path_str); | ||
| 677 | cleaned_count += 1; | ||
| 678 | } | ||
| 679 | } | ||
| 680 | } | ||
| 681 | } | ||
| 682 | Err(_) => { | ||
| 683 | println!("Cleaning up unreadable PID file: {}", path_str); | ||
| 684 | if let Err(e) = std::fs::remove_file(&path) { | ||
| 685 | tracing::warn!("Failed to remove unreadable PID file {}: {}", path_str, e); | ||
| 686 | } else { | ||
| 687 | tracing::info!("Removed unreadable PID file {}", path_str); | ||
| 688 | cleaned_count += 1; | ||
| 689 | } | ||
| 690 | } | ||
| 691 | } | ||
| 692 | } | ||
| 693 | Err(e) => { | ||
| 694 | tracing::warn!("Error reading glob entry: {}", e); | ||
| 695 | } | ||
| 696 | } | ||
| 697 | } | ||
| 698 | |||
| 699 | if cleaned_count == 0 { | ||
| 700 | println!("No orphaned files found."); | ||
| 701 | } else { | ||
| 702 | println!("Cleaned up {} orphaned daemon(s).", cleaned_count); | ||
| 703 | } | ||
| 704 | |||
| 705 | Ok(()) | ||
| 706 | } | ||
