diff options
| -rw-r--r-- | CLAUDE.md | 161 | ||||
| -rw-r--r-- | IMPROVEMENT_PLAN.md | 166 | ||||
| -rw-r--r-- | src/main.rs | 35 |
3 files changed, 345 insertions, 17 deletions
| @@ -8,3 +8,164 @@ do not add dependencies manually, instead, use the following tools: | |||
| 8 | + for logging, prefer the `tracing` crate with `tracing-subscriber` and fully qualify the log macros (ex: `tracing::info!`) | 8 | + for logging, prefer the `tracing` crate with `tracing-subscriber` and fully qualify the log macros (ex: `tracing::info!`) |
| 9 | + for cli use the `clap` crate. when implementing subcommands use an `enum` and separate structs for each subcommand's arguments | 9 | + for cli use the `clap` crate. when implementing subcommands use an `enum` and separate structs for each subcommand's arguments |
| 10 | + use the `anyhow` crate for error handling | 10 | + use the `anyhow` crate for error handling |
| 11 | |||
| 12 | ## testing guidelines | ||
| 13 | for testing cli applications, use the `assert_cmd` crate for integration testing | ||
| 14 | |||
| 15 | ## assert_cmd crate reference | ||
| 16 | |||
| 17 | ### Overview | ||
| 18 | `assert_cmd` is a Rust testing library designed to simplify integration testing of command-line applications. It provides easy command initialization, simplified configuration, and robust assertion mechanisms. | ||
| 19 | |||
| 20 | ### Key Features | ||
| 21 | - Easy command initialization and execution | ||
| 22 | - Cargo binary testing support | ||
| 23 | - Flexible output validation with predicates | ||
| 24 | - Environment variable and stdin management | ||
| 25 | - Comprehensive assertion mechanisms | ||
| 26 | |||
| 27 | ### Basic Usage Patterns | ||
| 28 | |||
| 29 | #### 1. Basic Command Testing | ||
| 30 | ```rust | ||
| 31 | use assert_cmd::Command; | ||
| 32 | |||
| 33 | // Run a Cargo binary | ||
| 34 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 35 | cmd.assert().success(); // Basic success assertion | ||
| 36 | ``` | ||
| 37 | |||
| 38 | #### 2. Command with Arguments | ||
| 39 | ```rust | ||
| 40 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 41 | cmd.args(&["run", "--id", "test", "sleep", "5"]) | ||
| 42 | .assert() | ||
| 43 | .success(); | ||
| 44 | ``` | ||
| 45 | |||
| 46 | #### 3. Output Validation | ||
| 47 | ```rust | ||
| 48 | use predicates::prelude::*; | ||
| 49 | |||
| 50 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 51 | cmd.args(&["list"]) | ||
| 52 | .assert() | ||
| 53 | .success() | ||
| 54 | .stdout(predicate::str::contains("ID")) | ||
| 55 | .stderr(predicate::str::is_empty()); | ||
| 56 | ``` | ||
| 57 | |||
| 58 | #### 4. Testing Failures | ||
| 59 | ```rust | ||
| 60 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 61 | cmd.args(&["run", "--id", "test"]) // Missing command | ||
| 62 | .assert() | ||
| 63 | .failure() | ||
| 64 | .stderr(predicate::str::contains("Command cannot be empty")); | ||
| 65 | ``` | ||
| 66 | |||
| 67 | ### Key Methods | ||
| 68 | |||
| 69 | #### Command Configuration | ||
| 70 | - `Command::cargo_bin("binary_name")`: Find and initialize a Cargo project binary | ||
| 71 | - `arg(arg)` / `args(&[args])`: Add command arguments | ||
| 72 | - `env(key, value)` / `envs(vars)`: Set environment variables | ||
| 73 | - `current_dir(path)`: Set working directory | ||
| 74 | - `write_stdin(input)`: Provide stdin input | ||
| 75 | |||
| 76 | #### Assertions | ||
| 77 | - `assert()`: Start assertion chain | ||
| 78 | - `success()`: Check for successful execution (exit code 0) | ||
| 79 | - `failure()`: Check for command failure (exit code != 0) | ||
| 80 | - `code(expected)`: Validate specific exit code | ||
| 81 | - `stdout(predicate)`: Validate stdout content | ||
| 82 | - `stderr(predicate)`: Validate stderr content | ||
| 83 | |||
| 84 | ### Predicates for Output Validation | ||
| 85 | ```rust | ||
| 86 | use predicates::prelude::*; | ||
| 87 | |||
| 88 | // Exact match | ||
| 89 | .stdout("exact output") | ||
| 90 | |||
| 91 | // Contains text | ||
| 92 | .stdout(predicate::str::contains("partial")) | ||
| 93 | |||
| 94 | // Regex match | ||
| 95 | .stdout(predicate::str::is_match(r"PID: \d+").unwrap()) | ||
| 96 | |||
| 97 | // Empty output | ||
| 98 | .stderr(predicate::str::is_empty()) | ||
| 99 | |||
| 100 | // Multiple conditions | ||
| 101 | .stdout(predicate::str::contains("SUCCESS").and(predicate::str::contains("ID"))) | ||
| 102 | ``` | ||
| 103 | |||
| 104 | ### Testing File I/O | ||
| 105 | For testing CLI tools that create/modify files, combine with `tempfile` and `assert_fs`: | ||
| 106 | |||
| 107 | ```rust | ||
| 108 | use tempfile::TempDir; | ||
| 109 | use std::fs; | ||
| 110 | |||
| 111 | #[test] | ||
| 112 | fn test_file_creation() { | ||
| 113 | let temp_dir = TempDir::new().unwrap(); | ||
| 114 | |||
| 115 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 116 | cmd.current_dir(temp_dir.path()) | ||
| 117 | .args(&["run", "--id", "test", "echo", "hello"]) | ||
| 118 | .assert() | ||
| 119 | .success(); | ||
| 120 | |||
| 121 | // Verify files were created | ||
| 122 | assert!(temp_dir.path().join("test.pid").exists()); | ||
| 123 | assert!(temp_dir.path().join("test.stdout").exists()); | ||
| 124 | } | ||
| 125 | ``` | ||
| 126 | |||
| 127 | ### Best Practices | ||
| 128 | |||
| 129 | 1. **Use `cargo_bin()`**: Automatically locate project binaries | ||
| 130 | 2. **Chain configuration**: Configure all arguments/env before calling `assert()` | ||
| 131 | 3. **Test various scenarios**: Success, failure, edge cases | ||
| 132 | 4. **Use predicates**: More flexible than exact string matching | ||
| 133 | 5. **Isolate tests**: Use temporary directories for file-based tests | ||
| 134 | 6. **Test error conditions**: Verify proper error handling and messages | ||
| 135 | |||
| 136 | ### Common Patterns for CLI Testing | ||
| 137 | |||
| 138 | #### Testing Help Output | ||
| 139 | ```rust | ||
| 140 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 141 | cmd.args(&["--help"]) | ||
| 142 | .assert() | ||
| 143 | .success() | ||
| 144 | .stdout(predicate::str::contains("daemon process management")); | ||
| 145 | ``` | ||
| 146 | |||
| 147 | #### Testing Subcommands | ||
| 148 | ```rust | ||
| 149 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 150 | cmd.args(&["list"]) | ||
| 151 | .assert() | ||
| 152 | .success() | ||
| 153 | .stdout(predicate::str::contains("ID")); | ||
| 154 | ``` | ||
| 155 | |||
| 156 | #### Testing with Timeouts | ||
| 157 | ```rust | ||
| 158 | use std::time::Duration; | ||
| 159 | |||
| 160 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 161 | cmd.timeout(Duration::from_secs(30)) // Prevent hanging tests | ||
| 162 | .args(&["run", "--id", "long", "sleep", "10"]) | ||
| 163 | .assert() | ||
| 164 | .success(); | ||
| 165 | ``` | ||
| 166 | |||
| 167 | ### Integration with Other Test Crates | ||
| 168 | - **`assert_fs`**: Filesystem testing utilities | ||
| 169 | - **`predicates`**: Advanced output validation | ||
| 170 | - **`tempfile`**: Temporary file/directory management | ||
| 171 | - **`serial_test`**: Serialize tests that can't run concurrently | ||
diff --git a/IMPROVEMENT_PLAN.md b/IMPROVEMENT_PLAN.md new file mode 100644 index 0000000..a553884 --- /dev/null +++ b/IMPROVEMENT_PLAN.md | |||
| @@ -0,0 +1,166 @@ | |||
| 1 | # Demon CLI Improvement Plan | ||
| 2 | |||
| 3 | ## Overview | ||
| 4 | This document outlines the planned improvements to the demon CLI tool based on feedback and best practices. | ||
| 5 | |||
| 6 | ## Improvement Tasks | ||
| 7 | |||
| 8 | ### 1. Switch to `anyhow` for Error Handling | ||
| 9 | **Priority**: High | ||
| 10 | **Status**: Pending | ||
| 11 | |||
| 12 | **Goal**: Replace `Box<dyn std::error::Error>` with `anyhow::Result` throughout the codebase for better error handling. | ||
| 13 | |||
| 14 | **Tasks**: | ||
| 15 | - Replace all `Result<(), Box<dyn std::error::Error>>` with `anyhow::Result<()>` | ||
| 16 | - Use `anyhow::Context` for better error context | ||
| 17 | - Simplify error handling code | ||
| 18 | - Update imports and error propagation | ||
| 19 | |||
| 20 | **Benefits**: | ||
| 21 | - Better error messages with context | ||
| 22 | - Simpler error handling | ||
| 23 | - More idiomatic Rust error handling | ||
| 24 | |||
| 25 | ### 2. Implement CLI Testing with `assert_cmd` | ||
| 26 | **Priority**: High | ||
| 27 | **Status**: Pending | ||
| 28 | |||
| 29 | **Goal**: Create comprehensive integration tests for all CLI commands using the `assert_cmd` crate. | ||
| 30 | |||
| 31 | **Prerequisites**: | ||
| 32 | - Research and document `assert_cmd` usage in CLAUDE.md | ||
| 33 | - Add `assert_cmd` dependency | ||
| 34 | - Create test infrastructure | ||
| 35 | |||
| 36 | **Test Coverage Required**: | ||
| 37 | - `demon run`: Process spawning, file creation, duplicate detection | ||
| 38 | - `demon stop`: Process termination, timeout handling, cleanup | ||
| 39 | - `demon tail`: File watching behavior (basic scenarios) | ||
| 40 | - `demon cat`: File content display, flag handling | ||
| 41 | - `demon list`: Process listing, status detection | ||
| 42 | - `demon status`: Individual process status checks | ||
| 43 | - `demon clean`: Orphaned file cleanup | ||
| 44 | - Error scenarios: missing files, invalid PIDs, etc. | ||
| 45 | |||
| 46 | **Test Structure**: | ||
| 47 | ``` | ||
| 48 | tests/ | ||
| 49 | ├── cli.rs # Main CLI integration tests | ||
| 50 | ├── fixtures/ # Test data and helper files | ||
| 51 | └── common/ # Shared test utilities | ||
| 52 | ``` | ||
| 53 | |||
| 54 | ### 3. Add Quiet Flag to List Command | ||
| 55 | **Priority**: Medium | ||
| 56 | **Status**: Pending | ||
| 57 | |||
| 58 | **Goal**: Add `-q/--quiet` flag to the `demon list` command for machine-readable output. | ||
| 59 | |||
| 60 | **Requirements**: | ||
| 61 | - Add `quiet` field to `ListArgs` struct (if needed, since `List` currently has no args) | ||
| 62 | - Convert `List` command to use `ListArgs` struct | ||
| 63 | - When quiet flag is used: | ||
| 64 | - No headers | ||
| 65 | - One line per process: `id:pid:status` | ||
| 66 | - No "No daemon processes found" message when empty | ||
| 67 | |||
| 68 | **Example Output**: | ||
| 69 | ```bash | ||
| 70 | # Normal mode | ||
| 71 | $ demon list | ||
| 72 | ID PID STATUS COMMAND | ||
| 73 | -------------------------------------------------- | ||
| 74 | my-app 12345 RUNNING N/A | ||
| 75 | |||
| 76 | # Quiet mode | ||
| 77 | $ demon list -q | ||
| 78 | my-app:12345:RUNNING | ||
| 79 | ``` | ||
| 80 | |||
| 81 | ### 4. Add LLM Command | ||
| 82 | **Priority**: Medium | ||
| 83 | **Status**: Pending | ||
| 84 | |||
| 85 | **Goal**: Add a `demon llm` command that outputs a comprehensive usage guide for other LLMs. | ||
| 86 | |||
| 87 | **Requirements**: | ||
| 88 | - Add `Llm` variant to `Commands` enum | ||
| 89 | - No arguments needed | ||
| 90 | - Output to stdout (not stderr like other messages) | ||
| 91 | - Include all commands with examples | ||
| 92 | - Assume the reader is an LLM that needs to understand how to use the tool | ||
| 93 | |||
| 94 | **Content Structure**: | ||
| 95 | - Tool overview and purpose | ||
| 96 | - All available commands with syntax | ||
| 97 | - Practical examples for each command | ||
| 98 | - Common workflows | ||
| 99 | - File structure explanation | ||
| 100 | - Error handling tips | ||
| 101 | |||
| 102 | ### 5. Remove `glob` Dependency | ||
| 103 | **Priority**: Low | ||
| 104 | **Status**: Pending | ||
| 105 | |||
| 106 | **Goal**: Replace the `glob` crate with standard library `std::fs` functionality. | ||
| 107 | |||
| 108 | **Implementation**: | ||
| 109 | - Remove `glob` from Cargo.toml | ||
| 110 | - Replace `glob("*.pid")` with `std::fs::read_dir()` + filtering | ||
| 111 | - Update imports | ||
| 112 | - Ensure same functionality is maintained | ||
| 113 | |||
| 114 | **Functions to Update**: | ||
| 115 | - `list_daemons()`: Find all .pid files | ||
| 116 | - `clean_orphaned_files()`: Find all .pid files | ||
| 117 | |||
| 118 | **Implementation Pattern**: | ||
| 119 | ```rust | ||
| 120 | // Replace glob("*.pid") with: | ||
| 121 | std::fs::read_dir(".")? | ||
| 122 | .filter_map(|entry| entry.ok()) | ||
| 123 | .filter(|entry| { | ||
| 124 | entry.path().extension() | ||
| 125 | .and_then(|ext| ext.to_str()) | ||
| 126 | .map(|ext| ext == "pid") | ||
| 127 | .unwrap_or(false) | ||
| 128 | }) | ||
| 129 | ``` | ||
| 130 | |||
| 131 | ## Implementation Order | ||
| 132 | |||
| 133 | 1. **Document assert_cmd** - Add understanding to CLAUDE.md | ||
| 134 | 2. **Switch to anyhow** - Foundation for better error handling | ||
| 135 | 3. **Implement tests** - Ensure current functionality works correctly | ||
| 136 | 4. **Add quiet flag** - Small feature addition | ||
| 137 | 5. **Add LLM command** - Documentation feature | ||
| 138 | 6. **Remove glob** - Cleanup and reduce dependencies | ||
| 139 | |||
| 140 | ## Success Criteria | ||
| 141 | |||
| 142 | - [ ] All existing functionality remains intact | ||
| 143 | - [ ] Comprehensive test coverage (>80% of CLI scenarios) | ||
| 144 | - [ ] Better error messages with context | ||
| 145 | - [ ] Machine-readable list output option | ||
| 146 | - [ ] LLM-friendly documentation command | ||
| 147 | - [ ] Reduced dependency footprint | ||
| 148 | - [ ] All changes committed with proper messages | ||
| 149 | |||
| 150 | ## Risk Assessment | ||
| 151 | |||
| 152 | **Low Risk**: | ||
| 153 | - anyhow migration (straightforward replacement) | ||
| 154 | - quiet flag addition (additive change) | ||
| 155 | - LLM command (new, isolated feature) | ||
| 156 | |||
| 157 | **Medium Risk**: | ||
| 158 | - glob removal (need to ensure exact same behavior) | ||
| 159 | - CLI testing (need to handle file system interactions carefully) | ||
| 160 | |||
| 161 | ## Notes | ||
| 162 | |||
| 163 | - Each improvement should be implemented, tested, and committed separately | ||
| 164 | - Maintain backward compatibility for all existing commands | ||
| 165 | - Update IMPLEMENTATION_PLAN.md as work progresses | ||
| 166 | - Consider adding integration tests that verify the actual daemon functionality \ No newline at end of file | ||
diff --git a/src/main.rs b/src/main.rs index e0545e6..12a08b6 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -8,6 +8,7 @@ use std::path::Path; | |||
| 8 | use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; | 8 | use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; |
| 9 | use std::sync::mpsc::channel; | 9 | use std::sync::mpsc::channel; |
| 10 | use glob::glob; | 10 | use glob::glob; |
| 11 | use anyhow::{Result, Context}; | ||
| 11 | 12 | ||
| 12 | #[derive(Parser)] | 13 | #[derive(Parser)] |
| 13 | #[command(name = "demon")] | 14 | #[command(name = "demon")] |
| @@ -114,11 +115,11 @@ fn main() { | |||
| 114 | } | 115 | } |
| 115 | } | 116 | } |
| 116 | 117 | ||
| 117 | fn run_command(command: Commands) -> Result<(), Box<dyn std::error::Error>> { | 118 | fn run_command(command: Commands) -> Result<()> { |
| 118 | match command { | 119 | match command { |
| 119 | Commands::Run(args) => { | 120 | Commands::Run(args) => { |
| 120 | if args.command.is_empty() { | 121 | if args.command.is_empty() { |
| 121 | return Err("Command cannot be empty".into()); | 122 | return Err(anyhow::anyhow!("Command cannot be empty")); |
| 122 | } | 123 | } |
| 123 | run_daemon(&args.id, &args.command) | 124 | run_daemon(&args.id, &args.command) |
| 124 | } | 125 | } |
| @@ -147,14 +148,14 @@ fn run_command(command: Commands) -> Result<(), Box<dyn std::error::Error>> { | |||
| 147 | } | 148 | } |
| 148 | } | 149 | } |
| 149 | 150 | ||
| 150 | fn run_daemon(id: &str, command: &[String]) -> Result<(), Box<dyn std::error::Error>> { | 151 | fn run_daemon(id: &str, command: &[String]) -> Result<()> { |
| 151 | let pid_file = format!("{}.pid", id); | 152 | let pid_file = format!("{}.pid", id); |
| 152 | let stdout_file = format!("{}.stdout", id); | 153 | let stdout_file = format!("{}.stdout", id); |
| 153 | let stderr_file = format!("{}.stderr", id); | 154 | let stderr_file = format!("{}.stderr", id); |
| 154 | 155 | ||
| 155 | // Check if process is already running | 156 | // Check if process is already running |
| 156 | if is_process_running(&pid_file)? { | 157 | if is_process_running(&pid_file)? { |
| 157 | return Err(format!("Process '{}' is already running", id).into()); | 158 | return Err(anyhow::anyhow!("Process '{}' is already running", id)); |
| 158 | } | 159 | } |
| 159 | 160 | ||
| 160 | tracing::info!("Starting daemon '{}' with command: {:?}", id, command); | 161 | tracing::info!("Starting daemon '{}' with command: {:?}", id, command); |
| @@ -177,7 +178,7 @@ fn run_daemon(id: &str, command: &[String]) -> Result<(), Box<dyn std::error::Er | |||
| 177 | .stderr(Stdio::from(stderr_redirect)) | 178 | .stderr(Stdio::from(stderr_redirect)) |
| 178 | .stdin(Stdio::null()) | 179 | .stdin(Stdio::null()) |
| 179 | .spawn() | 180 | .spawn() |
| 180 | .map_err(|e| format!("Failed to start process '{}': {}", program, e))?; | 181 | .with_context(|| format!("Failed to start process '{}' with args {:?}", program, args))?; |
| 181 | 182 | ||
| 182 | // Write PID to file | 183 | // Write PID to file |
| 183 | let mut pid_file_handle = File::create(&pid_file)?; | 184 | let mut pid_file_handle = File::create(&pid_file)?; |
| @@ -191,7 +192,7 @@ fn run_daemon(id: &str, command: &[String]) -> Result<(), Box<dyn std::error::Er | |||
| 191 | Ok(()) | 192 | Ok(()) |
| 192 | } | 193 | } |
| 193 | 194 | ||
| 194 | fn is_process_running(pid_file: &str) -> Result<bool, Box<dyn std::error::Error>> { | 195 | fn is_process_running(pid_file: &str) -> Result<bool> { |
| 195 | // Try to read the PID file | 196 | // Try to read the PID file |
| 196 | let mut file = match File::open(pid_file) { | 197 | let mut file = match File::open(pid_file) { |
| 197 | Ok(f) => f, | 198 | Ok(f) => f, |
| @@ -214,7 +215,7 @@ fn is_process_running(pid_file: &str) -> Result<bool, Box<dyn std::error::Error> | |||
| 214 | Ok(output.status.success()) | 215 | Ok(output.status.success()) |
| 215 | } | 216 | } |
| 216 | 217 | ||
| 217 | fn stop_daemon(id: &str, timeout: u64) -> Result<(), Box<dyn std::error::Error>> { | 218 | fn stop_daemon(id: &str, timeout: u64) -> Result<()> { |
| 218 | let pid_file = format!("{}.pid", id); | 219 | let pid_file = format!("{}.pid", id); |
| 219 | 220 | ||
| 220 | // Check if PID file exists | 221 | // Check if PID file exists |
| @@ -255,7 +256,7 @@ fn stop_daemon(id: &str, timeout: u64) -> Result<(), Box<dyn std::error::Error>> | |||
| 255 | .output()?; | 256 | .output()?; |
| 256 | 257 | ||
| 257 | if !output.status.success() { | 258 | if !output.status.success() { |
| 258 | return Err(format!("Failed to send SIGTERM to PID {}", pid).into()); | 259 | return Err(anyhow::anyhow!("Failed to send SIGTERM to PID {}", pid)); |
| 259 | } | 260 | } |
| 260 | 261 | ||
| 261 | // Wait for the process to terminate | 262 | // Wait for the process to terminate |
| @@ -280,14 +281,14 @@ fn stop_daemon(id: &str, timeout: u64) -> Result<(), Box<dyn std::error::Error>> | |||
| 280 | .output()?; | 281 | .output()?; |
| 281 | 282 | ||
| 282 | if !output.status.success() { | 283 | if !output.status.success() { |
| 283 | return Err(format!("Failed to send SIGKILL to PID {}", pid).into()); | 284 | return Err(anyhow::anyhow!("Failed to send SIGKILL to PID {}", pid)); |
| 284 | } | 285 | } |
| 285 | 286 | ||
| 286 | // Wait a bit more for SIGKILL to take effect | 287 | // Wait a bit more for SIGKILL to take effect |
| 287 | thread::sleep(Duration::from_secs(1)); | 288 | thread::sleep(Duration::from_secs(1)); |
| 288 | 289 | ||
| 289 | if is_process_running_by_pid(pid) { | 290 | if is_process_running_by_pid(pid) { |
| 290 | return Err(format!("Process {} is still running after SIGKILL", pid).into()); | 291 | return Err(anyhow::anyhow!("Process {} is still running after SIGKILL", pid)); |
| 291 | } | 292 | } |
| 292 | 293 | ||
| 293 | println!("Process '{}' (PID: {}) terminated forcefully", id, pid); | 294 | println!("Process '{}' (PID: {}) terminated forcefully", id, pid); |
| @@ -307,7 +308,7 @@ fn is_process_running_by_pid(pid: u32) -> bool { | |||
| 307 | } | 308 | } |
| 308 | } | 309 | } |
| 309 | 310 | ||
| 310 | fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<(), Box<dyn std::error::Error>> { | 311 | fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { |
| 311 | let stdout_file = format!("{}.stdout", id); | 312 | let stdout_file = format!("{}.stdout", id); |
| 312 | let stderr_file = format!("{}.stderr", id); | 313 | let stderr_file = format!("{}.stderr", id); |
| 313 | 314 | ||
| @@ -348,7 +349,7 @@ fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<(), Box<dy | |||
| 348 | Ok(()) | 349 | Ok(()) |
| 349 | } | 350 | } |
| 350 | 351 | ||
| 351 | fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<(), Box<dyn std::error::Error>> { | 352 | fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { |
| 352 | let stdout_file = format!("{}.stdout", id); | 353 | let stdout_file = format!("{}.stdout", id); |
| 353 | let stderr_file = format!("{}.stderr", id); | 354 | let stderr_file = format!("{}.stderr", id); |
| 354 | 355 | ||
| @@ -464,7 +465,7 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<(), Box<d | |||
| 464 | Ok(()) | 465 | Ok(()) |
| 465 | } | 466 | } |
| 466 | 467 | ||
| 467 | fn read_file_content(file: &mut File) -> Result<String, Box<dyn std::error::Error>> { | 468 | fn read_file_content(file: &mut File) -> Result<String> { |
| 468 | let mut content = String::new(); | 469 | let mut content = String::new(); |
| 469 | file.read_to_string(&mut content)?; | 470 | file.read_to_string(&mut content)?; |
| 470 | Ok(content) | 471 | Ok(content) |
| @@ -474,7 +475,7 @@ fn handle_file_change( | |||
| 474 | file_path: &str, | 475 | file_path: &str, |
| 475 | positions: &mut std::collections::HashMap<String, u64>, | 476 | positions: &mut std::collections::HashMap<String, u64>, |
| 476 | show_headers: bool | 477 | show_headers: bool |
| 477 | ) -> Result<(), Box<dyn std::error::Error>> { | 478 | ) -> Result<()> { |
| 478 | let mut file = File::open(file_path)?; | 479 | let mut file = File::open(file_path)?; |
| 479 | let current_pos = positions.get(file_path).copied().unwrap_or(0); | 480 | let current_pos = positions.get(file_path).copied().unwrap_or(0); |
| 480 | 481 | ||
| @@ -500,7 +501,7 @@ fn handle_file_change( | |||
| 500 | Ok(()) | 501 | Ok(()) |
| 501 | } | 502 | } |
| 502 | 503 | ||
| 503 | fn list_daemons() -> Result<(), Box<dyn std::error::Error>> { | 504 | fn list_daemons() -> Result<()> { |
| 504 | println!("{:<20} {:<8} {:<10} {}", "ID", "PID", "STATUS", "COMMAND"); | 505 | println!("{:<20} {:<8} {:<10} {}", "ID", "PID", "STATUS", "COMMAND"); |
| 505 | println!("{}", "-".repeat(50)); | 506 | println!("{}", "-".repeat(50)); |
| 506 | 507 | ||
| @@ -557,7 +558,7 @@ fn list_daemons() -> Result<(), Box<dyn std::error::Error>> { | |||
| 557 | Ok(()) | 558 | Ok(()) |
| 558 | } | 559 | } |
| 559 | 560 | ||
| 560 | fn status_daemon(id: &str) -> Result<(), Box<dyn std::error::Error>> { | 561 | fn status_daemon(id: &str) -> Result<()> { |
| 561 | let pid_file = format!("{}.pid", id); | 562 | let pid_file = format!("{}.pid", id); |
| 562 | let stdout_file = format!("{}.stdout", id); | 563 | let stdout_file = format!("{}.stdout", id); |
| 563 | let stderr_file = format!("{}.stderr", id); | 564 | let stderr_file = format!("{}.stderr", id); |
| @@ -614,7 +615,7 @@ fn status_daemon(id: &str) -> Result<(), Box<dyn std::error::Error>> { | |||
| 614 | Ok(()) | 615 | Ok(()) |
| 615 | } | 616 | } |
| 616 | 617 | ||
| 617 | fn clean_orphaned_files() -> Result<(), Box<dyn std::error::Error>> { | 618 | fn clean_orphaned_files() -> Result<()> { |
| 618 | tracing::info!("Scanning for orphaned daemon files..."); | 619 | tracing::info!("Scanning for orphaned daemon files..."); |
| 619 | 620 | ||
| 620 | let mut cleaned_count = 0; | 621 | let mut cleaned_count = 0; |
