diff options
| -rw-r--r-- | src/main.rs | 130 | ||||
| -rw-r--r-- | tests/cli.rs | 3 |
2 files changed, 101 insertions, 32 deletions
diff --git a/src/main.rs b/src/main.rs index 4ab1dd9..246af67 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -9,6 +9,36 @@ 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 | /// Error types for reading PID files | ||
| 13 | #[derive(Debug)] | ||
| 14 | pub enum PidFileReadError { | ||
| 15 | /// The PID file does not exist | ||
| 16 | FileNotFound, | ||
| 17 | /// The PID file exists but has invalid content | ||
| 18 | FileInvalid(String), | ||
| 19 | /// IO error occurred while reading | ||
| 20 | IoError(std::io::Error), | ||
| 21 | } | ||
| 22 | |||
| 23 | impl std::fmt::Display for PidFileReadError { | ||
| 24 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 25 | match self { | ||
| 26 | PidFileReadError::FileNotFound => write!(f, "PID file not found"), | ||
| 27 | PidFileReadError::FileInvalid(reason) => write!(f, "PID file invalid: {}", reason), | ||
| 28 | PidFileReadError::IoError(err) => write!(f, "IO error reading PID file: {}", err), | ||
| 29 | } | ||
| 30 | } | ||
| 31 | } | ||
| 32 | |||
| 33 | impl std::error::Error for PidFileReadError { | ||
| 34 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { | ||
| 35 | match self { | ||
| 36 | PidFileReadError::IoError(err) => Some(err), | ||
| 37 | _ => None, | ||
| 38 | } | ||
| 39 | } | ||
| 40 | } | ||
| 41 | |||
| 12 | /// Represents the contents of a PID file | 42 | /// Represents the contents of a PID file |
| 13 | #[derive(Debug, Clone)] | 43 | #[derive(Debug, Clone)] |
| 14 | struct PidFile { | 44 | struct PidFile { |
| @@ -35,23 +65,34 @@ impl PidFile { | |||
| 35 | } | 65 | } |
| 36 | 66 | ||
| 37 | /// Read PID file from a file | 67 | /// Read PID file from a file |
| 38 | fn read_from_file<P: AsRef<Path>>(path: P) -> Result<Self> { | 68 | fn read_from_file<P: AsRef<Path>>(path: P) -> Result<Self, PidFileReadError> { |
| 39 | let contents = std::fs::read_to_string(path)?; | 69 | let contents = match std::fs::read_to_string(&path) { |
| 70 | Ok(contents) => contents, | ||
| 71 | Err(err) => { | ||
| 72 | return if err.kind() == std::io::ErrorKind::NotFound { | ||
| 73 | Err(PidFileReadError::FileNotFound) | ||
| 74 | } else { | ||
| 75 | Err(PidFileReadError::IoError(err)) | ||
| 76 | }; | ||
| 77 | } | ||
| 78 | }; | ||
| 79 | |||
| 40 | let lines: Vec<&str> = contents.lines().collect(); | 80 | let lines: Vec<&str> = contents.lines().collect(); |
| 41 | 81 | ||
| 42 | if lines.is_empty() { | 82 | if lines.is_empty() { |
| 43 | return Err(anyhow::anyhow!("PID file is empty")); | 83 | return Err(PidFileReadError::FileInvalid("PID file is empty".to_string())); |
| 44 | } | 84 | } |
| 45 | 85 | ||
| 46 | let pid = lines[0] | 86 | let pid = lines[0].trim().parse::<u32>().map_err(|_| { |
| 47 | .trim() | 87 | PidFileReadError::FileInvalid("Invalid PID on first line".to_string()) |
| 48 | .parse::<u32>() | 88 | })?; |
| 49 | .context("Failed to parse PID from first line")?; | ||
| 50 | 89 | ||
| 51 | let command: Vec<String> = lines[1..].iter().map(|line| line.to_string()).collect(); | 90 | let command: Vec<String> = lines[1..].iter().map(|line| line.to_string()).collect(); |
| 52 | 91 | ||
| 53 | if command.is_empty() { | 92 | if command.is_empty() { |
| 54 | return Err(anyhow::anyhow!("No command found in PID file")); | 93 | return Err(PidFileReadError::FileInvalid( |
| 94 | "No command found in PID file".to_string(), | ||
| 95 | )); | ||
| 55 | } | 96 | } |
| 56 | 97 | ||
| 57 | Ok(Self { pid, command }) | 98 | Ok(Self { pid, command }) |
| @@ -256,14 +297,11 @@ fn run_daemon(id: &str, command: &[String]) -> Result<()> { | |||
| 256 | } | 297 | } |
| 257 | 298 | ||
| 258 | fn is_process_running(pid_file: &str) -> Result<bool> { | 299 | fn is_process_running(pid_file: &str) -> Result<bool> { |
| 259 | // Try to read the PID file | ||
| 260 | if !Path::new(pid_file).exists() { | ||
| 261 | return Ok(false); // No PID file means no running process | ||
| 262 | } | ||
| 263 | |||
| 264 | let pid_file_data = match PidFile::read_from_file(pid_file) { | 300 | let pid_file_data = match PidFile::read_from_file(pid_file) { |
| 265 | Ok(data) => data, | 301 | Ok(data) => data, |
| 266 | Err(_) => return Ok(false), // Invalid PID file | 302 | Err(PidFileReadError::FileNotFound) => return Ok(false), // No PID file means no running process |
| 303 | Err(PidFileReadError::FileInvalid(_)) => return Ok(false), // Invalid PID file means no running process | ||
| 304 | Err(PidFileReadError::IoError(err)) => return Err(err.into()), // Propagate IO errors | ||
| 267 | }; | 305 | }; |
| 268 | 306 | ||
| 269 | // Check if process is still running using kill -0 | 307 | // Check if process is still running using kill -0 |
| @@ -280,15 +318,18 @@ fn stop_daemon(id: &str, timeout: u64) -> Result<()> { | |||
| 280 | // Check if PID file exists and read PID data | 318 | // Check if PID file exists and read PID data |
| 281 | let pid_file_data = match PidFile::read_from_file(&pid_file) { | 319 | let pid_file_data = match PidFile::read_from_file(&pid_file) { |
| 282 | Ok(data) => data, | 320 | Ok(data) => data, |
| 283 | Err(_) => { | 321 | Err(PidFileReadError::FileNotFound) => { |
| 284 | if Path::new(&pid_file).exists() { | 322 | println!("Process '{}' is not running (no PID file found)", id); |
| 285 | println!("Process '{}': invalid PID file, removing it", id); | 323 | return Ok(()); |
| 286 | std::fs::remove_file(&pid_file)?; | 324 | } |
| 287 | } else { | 325 | Err(PidFileReadError::FileInvalid(_)) => { |
| 288 | println!("Process '{}' is not running (no PID file found)", id); | 326 | println!("Process '{}': invalid PID file, removing it", id); |
| 289 | } | 327 | std::fs::remove_file(&pid_file)?; |
| 290 | return Ok(()); | 328 | return Ok(()); |
| 291 | } | 329 | } |
| 330 | Err(PidFileReadError::IoError(err)) => { | ||
| 331 | return Err(anyhow::anyhow!("Failed to read PID file: {}", err)); | ||
| 332 | } | ||
| 292 | }; | 333 | }; |
| 293 | 334 | ||
| 294 | let pid = pid_file_data.pid; | 335 | let pid = pid_file_data.pid; |
| @@ -614,13 +655,34 @@ fn list_daemons(quiet: bool) -> Result<()> { | |||
| 614 | println!("{:<20} {:<8} {:<10} {}", id, pid_file_data.pid, status, command); | 655 | println!("{:<20} {:<8} {:<10} {}", id, pid_file_data.pid, status, command); |
| 615 | } | 656 | } |
| 616 | } | 657 | } |
| 617 | Err(_) => { | 658 | Err(PidFileReadError::FileNotFound) => { |
| 659 | // This shouldn't happen since we found the file, but handle gracefully | ||
| 660 | if quiet { | ||
| 661 | println!("{}:NOTFOUND:ERROR", id); | ||
| 662 | } else { | ||
| 663 | println!( | ||
| 664 | "{:<20} {:<8} {:<10} {}", | ||
| 665 | id, "NOTFOUND", "ERROR", "PID file disappeared" | ||
| 666 | ); | ||
| 667 | } | ||
| 668 | } | ||
| 669 | Err(PidFileReadError::FileInvalid(reason)) => { | ||
| 618 | if quiet { | 670 | if quiet { |
| 619 | println!("{}:INVALID:ERROR", id); | 671 | println!("{}:INVALID:ERROR", id); |
| 620 | } else { | 672 | } else { |
| 621 | println!( | 673 | println!( |
| 622 | "{:<20} {:<8} {:<10} {}", | 674 | "{:<20} {:<8} {:<10} {}", |
| 623 | id, "INVALID", "ERROR", "Invalid PID file" | 675 | id, "INVALID", "ERROR", reason |
| 676 | ); | ||
| 677 | } | ||
| 678 | } | ||
| 679 | Err(PidFileReadError::IoError(_)) => { | ||
| 680 | if quiet { | ||
| 681 | println!("{}:ERROR:ERROR", id); | ||
| 682 | } else { | ||
| 683 | println!( | ||
| 684 | "{:<20} {:<8} {:<10} {}", | ||
| 685 | id, "ERROR", "ERROR", "Cannot read PID file" | ||
| 624 | ); | 686 | ); |
| 625 | } | 687 | } |
| 626 | } | 688 | } |
| @@ -642,12 +704,6 @@ fn status_daemon(id: &str) -> Result<()> { | |||
| 642 | println!("Daemon: {}", id); | 704 | println!("Daemon: {}", id); |
| 643 | println!("PID file: {}", pid_file); | 705 | println!("PID file: {}", pid_file); |
| 644 | 706 | ||
| 645 | // Check if PID file exists | ||
| 646 | if !Path::new(&pid_file).exists() { | ||
| 647 | println!("Status: NOT FOUND (no PID file)"); | ||
| 648 | return Ok(()); | ||
| 649 | } | ||
| 650 | |||
| 651 | // Read PID data from file | 707 | // Read PID data from file |
| 652 | match PidFile::read_from_file(&pid_file) { | 708 | match PidFile::read_from_file(&pid_file) { |
| 653 | Ok(pid_file_data) => { | 709 | Ok(pid_file_data) => { |
| @@ -676,8 +732,14 @@ fn status_daemon(id: &str) -> Result<()> { | |||
| 676 | println!("Note: Use 'demon clean' to remove orphaned files"); | 732 | println!("Note: Use 'demon clean' to remove orphaned files"); |
| 677 | } | 733 | } |
| 678 | } | 734 | } |
| 679 | Err(e) => { | 735 | Err(PidFileReadError::FileNotFound) => { |
| 680 | println!("Status: ERROR (cannot read PID file: {})", e); | 736 | println!("Status: NOT FOUND (no PID file)"); |
| 737 | } | ||
| 738 | Err(PidFileReadError::FileInvalid(reason)) => { | ||
| 739 | println!("Status: ERROR (invalid PID file: {})", reason); | ||
| 740 | } | ||
| 741 | Err(PidFileReadError::IoError(err)) => { | ||
| 742 | println!("Status: ERROR (cannot read PID file: {})", err); | ||
| 681 | } | 743 | } |
| 682 | } | 744 | } |
| 683 | 745 | ||
| @@ -741,7 +803,11 @@ fn clean_orphaned_files() -> Result<()> { | |||
| 741 | ); | 803 | ); |
| 742 | } | 804 | } |
| 743 | } | 805 | } |
| 744 | Err(_) => { | 806 | Err(PidFileReadError::FileNotFound) => { |
| 807 | // This shouldn't happen since we found the file, but handle gracefully | ||
| 808 | tracing::warn!("PID file {} disappeared during processing", path_str); | ||
| 809 | } | ||
| 810 | Err(PidFileReadError::FileInvalid(_)) | Err(PidFileReadError::IoError(_)) => { | ||
| 745 | println!("Cleaning up invalid PID file: {}", path_str); | 811 | println!("Cleaning up invalid PID file: {}", path_str); |
| 746 | if let Err(e) = std::fs::remove_file(&path) { | 812 | if let Err(e) = std::fs::remove_file(&path) { |
| 747 | tracing::warn!("Failed to remove invalid PID file {}: {}", path_str, e); | 813 | tracing::warn!("Failed to remove invalid PID file {}: {}", path_str, e); |
diff --git a/tests/cli.rs b/tests/cli.rs index e99e876..2903f61 100644 --- a/tests/cli.rs +++ b/tests/cli.rs | |||
| @@ -57,6 +57,9 @@ fn test_run_creates_files() { | |||
| 57 | assert!(temp_dir.path().join("test.stdout").exists()); | 57 | assert!(temp_dir.path().join("test.stdout").exists()); |
| 58 | assert!(temp_dir.path().join("test.stderr").exists()); | 58 | assert!(temp_dir.path().join("test.stderr").exists()); |
| 59 | 59 | ||
| 60 | // Give the process a moment to complete | ||
| 61 | std::thread::sleep(Duration::from_millis(100)); | ||
| 62 | |||
| 60 | // Check that stdout contains our output | 63 | // Check that stdout contains our output |
| 61 | let stdout_content = fs::read_to_string(temp_dir.path().join("test.stdout")).unwrap(); | 64 | let stdout_content = fs::read_to_string(temp_dir.path().join("test.stdout")).unwrap(); |
| 62 | assert_eq!(stdout_content.trim(), "hello"); | 65 | assert_eq!(stdout_content.trim(), "hello"); |
