diff options
| author | diogo464 <[email protected]> | 2025-06-19 09:26:48 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-06-19 09:26:48 +0100 |
| commit | 000607fdd40887f3735281e0383d8f6c80a3d382 (patch) | |
| tree | 18c33da32cf7cfe5a7efb8340b9bdbdb26827956 | |
| parent | 1c2e20c56d7fdbb0f7b21d12137ec7d58cd839c8 (diff) | |
Store command in PID file and display in list/status output
- Add PidFileData struct to represent PID file contents
- Store both PID and command in PID file (first line: PID, remaining lines: command args)
- Implement write_to_file() and read_from_file() methods for structured data handling
- Update run_daemon() to write command alongside PID
- Update all functions to use PidFileData instead of raw PID parsing:
- is_process_running()
- stop_daemon()
- list_daemons() - now shows actual command instead of "N/A"
- status_daemon() - shows command in detailed output
- clean_orphaned_files()
- Add command_string() method for formatted display
- Maintain backward compatibility with error handling for invalid PID files
- All tests pass, command display works correctly
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <[email protected]>
| -rw-r--r-- | src/main.rs | 296 |
1 files changed, 151 insertions, 145 deletions
diff --git a/src/main.rs b/src/main.rs index ac6e305..c82208b 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -9,6 +9,60 @@ use std::sync::mpsc::channel; | |||
| 9 | use std::thread; | 9 | use std::thread; |
| 10 | use std::time::Duration; | 10 | use std::time::Duration; |
| 11 | 11 | ||
| 12 | /// Represents the contents of a PID file | ||
| 13 | #[derive(Debug, Clone)] | ||
| 14 | struct PidFileData { | ||
| 15 | /// Process ID | ||
| 16 | pid: u32, | ||
| 17 | /// Command that was executed (program + arguments) | ||
| 18 | command: Vec<String>, | ||
| 19 | } | ||
| 20 | |||
| 21 | impl PidFileData { | ||
| 22 | /// Create a new PidFileData instance | ||
| 23 | fn new(pid: u32, command: Vec<String>) -> Self { | ||
| 24 | Self { pid, command } | ||
| 25 | } | ||
| 26 | |||
| 27 | /// Write PID file data to a file | ||
| 28 | fn write_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> { | ||
| 29 | let mut file = File::create(path)?; | ||
| 30 | writeln!(file, "{}", self.pid)?; | ||
| 31 | for arg in &self.command { | ||
| 32 | writeln!(file, "{}", arg)?; | ||
| 33 | } | ||
| 34 | Ok(()) | ||
| 35 | } | ||
| 36 | |||
| 37 | /// Read PID file data from a file | ||
| 38 | fn read_from_file<P: AsRef<Path>>(path: P) -> Result<Self> { | ||
| 39 | let contents = std::fs::read_to_string(path)?; | ||
| 40 | let lines: Vec<&str> = contents.lines().collect(); | ||
| 41 | |||
| 42 | if lines.is_empty() { | ||
| 43 | return Err(anyhow::anyhow!("PID file is empty")); | ||
| 44 | } | ||
| 45 | |||
| 46 | let pid = lines[0] | ||
| 47 | .trim() | ||
| 48 | .parse::<u32>() | ||
| 49 | .context("Failed to parse PID from first line")?; | ||
| 50 | |||
| 51 | let command: Vec<String> = lines[1..].iter().map(|line| line.to_string()).collect(); | ||
| 52 | |||
| 53 | if command.is_empty() { | ||
| 54 | return Err(anyhow::anyhow!("No command found in PID file")); | ||
| 55 | } | ||
| 56 | |||
| 57 | Ok(Self { pid, command }) | ||
| 58 | } | ||
| 59 | |||
| 60 | /// Get the command as a formatted string for display | ||
| 61 | fn command_string(&self) -> String { | ||
| 62 | self.command.join(" ") | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 12 | #[derive(Parser)] | 66 | #[derive(Parser)] |
| 13 | #[command(name = "demon")] | 67 | #[command(name = "demon")] |
| 14 | #[command(about = "A daemon process management CLI", long_about = None)] | 68 | #[command(about = "A daemon process management CLI", long_about = None)] |
| @@ -189,9 +243,9 @@ fn run_daemon(id: &str, command: &[String]) -> Result<()> { | |||
| 189 | .spawn() | 243 | .spawn() |
| 190 | .with_context(|| format!("Failed to start process '{}' with args {:?}", program, args))?; | 244 | .with_context(|| format!("Failed to start process '{}' with args {:?}", program, args))?; |
| 191 | 245 | ||
| 192 | // Write PID to file | 246 | // Write PID and command to file |
| 193 | let mut pid_file_handle = File::create(&pid_file)?; | 247 | let pid_data = PidFileData::new(child.id(), command.to_vec()); |
| 194 | writeln!(pid_file_handle, "{}", child.id())?; | 248 | pid_data.write_to_file(&pid_file)?; |
| 195 | 249 | ||
| 196 | // Don't wait for the child - let it run detached | 250 | // Don't wait for the child - let it run detached |
| 197 | std::mem::forget(child); | 251 | std::mem::forget(child); |
| @@ -203,22 +257,18 @@ fn run_daemon(id: &str, command: &[String]) -> Result<()> { | |||
| 203 | 257 | ||
| 204 | fn is_process_running(pid_file: &str) -> Result<bool> { | 258 | fn is_process_running(pid_file: &str) -> Result<bool> { |
| 205 | // Try to read the PID file | 259 | // Try to read the PID file |
| 206 | let mut file = match File::open(pid_file) { | 260 | if !Path::new(pid_file).exists() { |
| 207 | Ok(f) => f, | 261 | return Ok(false); // No PID file means no running process |
| 208 | Err(_) => return Ok(false), // No PID file means no running process | 262 | } |
| 209 | }; | ||
| 210 | |||
| 211 | let mut contents = String::new(); | ||
| 212 | file.read_to_string(&mut contents)?; | ||
| 213 | 263 | ||
| 214 | let pid: u32 = match contents.trim().parse() { | 264 | let pid_data = match PidFileData::read_from_file(pid_file) { |
| 215 | Ok(p) => p, | 265 | Ok(data) => data, |
| 216 | Err(_) => return Ok(false), // Invalid PID file | 266 | Err(_) => return Ok(false), // Invalid PID file |
| 217 | }; | 267 | }; |
| 218 | 268 | ||
| 219 | // Check if process is still running using kill -0 | 269 | // Check if process is still running using kill -0 |
| 220 | let output = Command::new("kill") | 270 | let output = Command::new("kill") |
| 221 | .args(&["-0", &pid.to_string()]) | 271 | .args(&["-0", &pid_data.pid.to_string()]) |
| 222 | .output()?; | 272 | .output()?; |
| 223 | 273 | ||
| 224 | Ok(output.status.success()) | 274 | Ok(output.status.success()) |
| @@ -227,27 +277,21 @@ fn is_process_running(pid_file: &str) -> Result<bool> { | |||
| 227 | fn stop_daemon(id: &str, timeout: u64) -> Result<()> { | 277 | fn stop_daemon(id: &str, timeout: u64) -> Result<()> { |
| 228 | let pid_file = format!("{}.pid", id); | 278 | let pid_file = format!("{}.pid", id); |
| 229 | 279 | ||
| 230 | // Check if PID file exists | 280 | // Check if PID file exists and read PID data |
| 231 | let mut file = match File::open(&pid_file) { | 281 | let pid_data = match PidFileData::read_from_file(&pid_file) { |
| 232 | Ok(f) => f, | 282 | Ok(data) => data, |
| 233 | Err(_) => { | 283 | Err(_) => { |
| 234 | println!("Process '{}' is not running (no PID file found)", id); | 284 | if Path::new(&pid_file).exists() { |
| 285 | println!("Process '{}': invalid PID file, removing it", id); | ||
| 286 | std::fs::remove_file(&pid_file)?; | ||
| 287 | } else { | ||
| 288 | println!("Process '{}' is not running (no PID file found)", id); | ||
| 289 | } | ||
| 235 | return Ok(()); | 290 | return Ok(()); |
| 236 | } | 291 | } |
| 237 | }; | 292 | }; |
| 238 | 293 | ||
| 239 | // Read PID | 294 | let pid = pid_data.pid; |
| 240 | let mut contents = String::new(); | ||
| 241 | file.read_to_string(&mut contents)?; | ||
| 242 | |||
| 243 | let pid: u32 = match contents.trim().parse() { | ||
| 244 | Ok(p) => p, | ||
| 245 | Err(_) => { | ||
| 246 | println!("Process '{}': invalid PID file, removing it", id); | ||
| 247 | std::fs::remove_file(&pid_file)?; | ||
| 248 | return Ok(()); | ||
| 249 | } | ||
| 250 | }; | ||
| 251 | 295 | ||
| 252 | tracing::info!( | 296 | tracing::info!( |
| 253 | "Stopping daemon '{}' (PID: {}) with timeout {}s", | 297 | "Stopping daemon '{}' (PID: {}) with timeout {}s", |
| @@ -554,49 +598,29 @@ fn list_daemons(quiet: bool) -> Result<()> { | |||
| 554 | // Extract ID from filename (remove .pid extension) | 598 | // Extract ID from filename (remove .pid extension) |
| 555 | let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); | 599 | let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); |
| 556 | 600 | ||
| 557 | // Read PID from file | 601 | // Read PID data from file |
| 558 | match std::fs::read_to_string(&path) { | 602 | match PidFileData::read_from_file(&path) { |
| 559 | Ok(contents) => { | 603 | Ok(pid_data) => { |
| 560 | let pid_str = contents.trim(); | 604 | let status = if is_process_running_by_pid(pid_data.pid) { |
| 561 | match pid_str.parse::<u32>() { | 605 | "RUNNING" |
| 562 | Ok(pid) => { | 606 | } else { |
| 563 | let status = if is_process_running_by_pid(pid) { | 607 | "DEAD" |
| 564 | "RUNNING" | 608 | }; |
| 565 | } else { | ||
| 566 | "DEAD" | ||
| 567 | }; | ||
| 568 | 609 | ||
| 569 | if quiet { | 610 | if quiet { |
| 570 | println!("{}:{}:{}", id, pid, status); | 611 | println!("{}:{}:{}", id, pid_data.pid, status); |
| 571 | } else { | 612 | } else { |
| 572 | // Try to read command from a hypothetical command file | 613 | let command = pid_data.command_string(); |
| 573 | // For now, we'll just show "N/A" since we don't store the command | 614 | println!("{:<20} {:<8} {:<10} {}", id, pid_data.pid, status, command); |
| 574 | let command = "N/A"; | ||
| 575 | println!("{:<20} {:<8} {:<10} {}", id, pid, status, command); | ||
| 576 | } | ||
| 577 | } | ||
| 578 | Err(_) => { | ||
| 579 | if quiet { | ||
| 580 | println!("{}:INVALID:ERROR", id); | ||
| 581 | } else { | ||
| 582 | println!( | ||
| 583 | "{:<20} {:<8} {:<10} {}", | ||
| 584 | id, "INVALID", "ERROR", "Invalid PID file" | ||
| 585 | ); | ||
| 586 | } | ||
| 587 | } | ||
| 588 | } | 615 | } |
| 589 | } | 616 | } |
| 590 | Err(e) => { | 617 | Err(_) => { |
| 591 | if quiet { | 618 | if quiet { |
| 592 | println!("{}:ERROR:ERROR", id); | 619 | println!("{}:INVALID:ERROR", id); |
| 593 | } else { | 620 | } else { |
| 594 | println!( | 621 | println!( |
| 595 | "{:<20} {:<8} {:<10} {}", | 622 | "{:<20} {:<8} {:<10} {}", |
| 596 | id, | 623 | id, "INVALID", "ERROR", "Invalid PID file" |
| 597 | "ERROR", | ||
| 598 | "ERROR", | ||
| 599 | format!("Cannot read: {}", e) | ||
| 600 | ); | 624 | ); |
| 601 | } | 625 | } |
| 602 | } | 626 | } |
| @@ -624,39 +648,32 @@ fn status_daemon(id: &str) -> Result<()> { | |||
| 624 | return Ok(()); | 648 | return Ok(()); |
| 625 | } | 649 | } |
| 626 | 650 | ||
| 627 | // Read PID from file | 651 | // Read PID data from file |
| 628 | match std::fs::read_to_string(&pid_file) { | 652 | match PidFileData::read_from_file(&pid_file) { |
| 629 | Ok(contents) => { | 653 | Ok(pid_data) => { |
| 630 | let pid_str = contents.trim(); | 654 | println!("PID: {}", pid_data.pid); |
| 631 | match pid_str.parse::<u32>() { | 655 | println!("Command: {}", pid_data.command_string()); |
| 632 | Ok(pid) => { | ||
| 633 | println!("PID: {}", pid); | ||
| 634 | |||
| 635 | if is_process_running_by_pid(pid) { | ||
| 636 | println!("Status: RUNNING"); | ||
| 637 | |||
| 638 | // Show file information | ||
| 639 | if Path::new(&stdout_file).exists() { | ||
| 640 | let metadata = std::fs::metadata(&stdout_file)?; | ||
| 641 | println!("Stdout file: {} ({} bytes)", stdout_file, metadata.len()); | ||
| 642 | } else { | ||
| 643 | println!("Stdout file: {} (not found)", stdout_file); | ||
| 644 | } | ||
| 645 | 656 | ||
| 646 | if Path::new(&stderr_file).exists() { | 657 | if is_process_running_by_pid(pid_data.pid) { |
| 647 | let metadata = std::fs::metadata(&stderr_file)?; | 658 | println!("Status: RUNNING"); |
| 648 | println!("Stderr file: {} ({} bytes)", stderr_file, metadata.len()); | 659 | |
| 649 | } else { | 660 | // Show file information |
| 650 | println!("Stderr file: {} (not found)", stderr_file); | 661 | if Path::new(&stdout_file).exists() { |
| 651 | } | 662 | let metadata = std::fs::metadata(&stdout_file)?; |
| 652 | } else { | 663 | println!("Stdout file: {} ({} bytes)", stdout_file, metadata.len()); |
| 653 | println!("Status: DEAD (process not running)"); | 664 | } else { |
| 654 | println!("Note: Use 'demon clean' to remove orphaned files"); | 665 | println!("Stdout file: {} (not found)", stdout_file); |
| 655 | } | ||
| 656 | } | 666 | } |
| 657 | Err(_) => { | 667 | |
| 658 | println!("Status: ERROR (invalid PID in file)"); | 668 | if Path::new(&stderr_file).exists() { |
| 669 | let metadata = std::fs::metadata(&stderr_file)?; | ||
| 670 | println!("Stderr file: {} ({} bytes)", stderr_file, metadata.len()); | ||
| 671 | } else { | ||
| 672 | println!("Stderr file: {} (not found)", stderr_file); | ||
| 659 | } | 673 | } |
| 674 | } else { | ||
| 675 | println!("Status: DEAD (process not running)"); | ||
| 676 | println!("Note: Use 'demon clean' to remove orphaned files"); | ||
| 660 | } | 677 | } |
| 661 | } | 678 | } |
| 662 | Err(e) => { | 679 | Err(e) => { |
| @@ -678,69 +695,58 @@ fn clean_orphaned_files() -> Result<()> { | |||
| 678 | let path_str = path.to_string_lossy(); | 695 | let path_str = path.to_string_lossy(); |
| 679 | let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); | 696 | let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); |
| 680 | 697 | ||
| 681 | // Read PID from file | 698 | // Read PID data from file |
| 682 | match std::fs::read_to_string(&path) { | 699 | match PidFileData::read_from_file(&path) { |
| 683 | Ok(contents) => { | 700 | Ok(pid_data) => { |
| 684 | let pid_str = contents.trim(); | 701 | // Check if process is still running |
| 685 | match pid_str.parse::<u32>() { | 702 | if !is_process_running_by_pid(pid_data.pid) { |
| 686 | Ok(pid) => { | 703 | println!( |
| 687 | // Check if process is still running | 704 | "Cleaning up orphaned files for '{}' (PID: {})", |
| 688 | if !is_process_running_by_pid(pid) { | 705 | id, pid_data.pid |
| 689 | println!("Cleaning up orphaned files for '{}' (PID: {})", id, pid); | 706 | ); |
| 690 | |||
| 691 | // Remove PID file | ||
| 692 | if let Err(e) = std::fs::remove_file(&path) { | ||
| 693 | tracing::warn!("Failed to remove {}: {}", path_str, e); | ||
| 694 | } else { | ||
| 695 | tracing::info!("Removed {}", path_str); | ||
| 696 | } | ||
| 697 | |||
| 698 | // Remove stdout file if it exists | ||
| 699 | let stdout_file = format!("{}.stdout", id); | ||
| 700 | if Path::new(&stdout_file).exists() { | ||
| 701 | if let Err(e) = std::fs::remove_file(&stdout_file) { | ||
| 702 | tracing::warn!("Failed to remove {}: {}", stdout_file, e); | ||
| 703 | } else { | ||
| 704 | tracing::info!("Removed {}", stdout_file); | ||
| 705 | } | ||
| 706 | } | ||
| 707 | 707 | ||
| 708 | // Remove stderr file if it exists | 708 | // Remove PID file |
| 709 | let stderr_file = format!("{}.stderr", id); | 709 | if let Err(e) = std::fs::remove_file(&path) { |
| 710 | if Path::new(&stderr_file).exists() { | 710 | tracing::warn!("Failed to remove {}: {}", path_str, e); |
| 711 | if let Err(e) = std::fs::remove_file(&stderr_file) { | 711 | } else { |
| 712 | tracing::warn!("Failed to remove {}: {}", stderr_file, e); | 712 | tracing::info!("Removed {}", path_str); |
| 713 | } else { | 713 | } |
| 714 | tracing::info!("Removed {}", stderr_file); | ||
| 715 | } | ||
| 716 | } | ||
| 717 | 714 | ||
| 718 | cleaned_count += 1; | 715 | // Remove stdout file if it exists |
| 716 | let stdout_file = format!("{}.stdout", id); | ||
| 717 | if Path::new(&stdout_file).exists() { | ||
| 718 | if let Err(e) = std::fs::remove_file(&stdout_file) { | ||
| 719 | tracing::warn!("Failed to remove {}: {}", stdout_file, e); | ||
| 719 | } else { | 720 | } else { |
| 720 | tracing::info!( | 721 | tracing::info!("Removed {}", stdout_file); |
| 721 | "Skipping '{}' (PID: {}) - process is still running", | ||
| 722 | id, | ||
| 723 | pid | ||
| 724 | ); | ||
| 725 | } | 722 | } |
| 726 | } | 723 | } |
| 727 | Err(_) => { | 724 | |
| 728 | println!("Cleaning up invalid PID file: {}", path_str); | 725 | // Remove stderr file if it exists |
| 729 | if let Err(e) = std::fs::remove_file(&path) { | 726 | let stderr_file = format!("{}.stderr", id); |
| 730 | tracing::warn!("Failed to remove invalid PID file {}: {}", path_str, e); | 727 | if Path::new(&stderr_file).exists() { |
| 728 | if let Err(e) = std::fs::remove_file(&stderr_file) { | ||
| 729 | tracing::warn!("Failed to remove {}: {}", stderr_file, e); | ||
| 731 | } else { | 730 | } else { |
| 732 | tracing::info!("Removed invalid PID file {}", path_str); | 731 | tracing::info!("Removed {}", stderr_file); |
| 733 | cleaned_count += 1; | ||
| 734 | } | 732 | } |
| 735 | } | 733 | } |
| 734 | |||
| 735 | cleaned_count += 1; | ||
| 736 | } else { | ||
| 737 | tracing::info!( | ||
| 738 | "Skipping '{}' (PID: {}) - process is still running", | ||
| 739 | id, | ||
| 740 | pid_data.pid | ||
| 741 | ); | ||
| 736 | } | 742 | } |
| 737 | } | 743 | } |
| 738 | Err(_) => { | 744 | Err(_) => { |
| 739 | println!("Cleaning up unreadable PID file: {}", path_str); | 745 | println!("Cleaning up invalid PID file: {}", path_str); |
| 740 | if let Err(e) = std::fs::remove_file(&path) { | 746 | if let Err(e) = std::fs::remove_file(&path) { |
| 741 | tracing::warn!("Failed to remove unreadable PID file {}: {}", path_str, e); | 747 | tracing::warn!("Failed to remove invalid PID file {}: {}", path_str, e); |
| 742 | } else { | 748 | } else { |
| 743 | tracing::info!("Removed unreadable PID file {}", path_str); | 749 | tracing::info!("Removed invalid PID file {}", path_str); |
| 744 | cleaned_count += 1; | 750 | cleaned_count += 1; |
| 745 | } | 751 | } |
| 746 | } | 752 | } |
