diff options
| author | diogo464 <[email protected]> | 2025-06-19 10:03:56 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-06-19 10:03:56 +0100 |
| commit | 5101d9c410a7c901ea20636d2a4e56b3282a1c14 (patch) | |
| tree | 4b229f6ced108f14957a7e80c2c30880f7174811 | |
| parent | 7b7dbf8948fa063d040a744a4903be1df75ca943 (diff) | |
Add wait subcommand for blocking until process termination
Implements a new 'wait' subcommand that blocks until a specified daemon process terminates, with configurable timeout and polling interval.
Features:
- Default 30-second timeout, configurable with --timeout flag
- Infinite wait with --timeout 0
- Configurable polling interval with --interval flag (default 1 second)
- Quiet operation - only shows errors on failure
- Preserves PID files (doesn't clean up)
- Exit codes: 0 for success, 1 for failure
Usage examples:
- demon wait my-process # Wait 30s
- demon wait my-process --timeout 0 # Wait indefinitely
- demon wait my-process --timeout 60 --interval 2 # Custom timeout/interval
Added comprehensive test suite covering:
- Non-existent processes
- Already terminated processes
- Normal process termination
- Timeout scenarios
- Infinite timeout behavior
- Custom polling intervals
Updated documentation:
- README.md with wait command reference and usage examples
- LLM guide with detailed wait command documentation
- Integration examples for development workflows
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <[email protected]>
| -rw-r--r-- | IMPLEMENTATION_PLAN.md | 140 | ||||
| -rw-r--r-- | README.md | 24 | ||||
| -rw-r--r-- | src/main.rs | 100 | ||||
| -rw-r--r-- | tests/cli.rs | 123 |
4 files changed, 382 insertions, 5 deletions
diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index eaa1e64..56c3caf 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md | |||
| @@ -172,4 +172,142 @@ The implementation follows the planned modular structure: | |||
| 172 | - **File Operations**: Manages PID files and log redirection ✅ | 172 | - **File Operations**: Manages PID files and log redirection ✅ |
| 173 | - **Output Display**: Implements both cat and tail functionality ✅ | 173 | - **Output Display**: Implements both cat and tail functionality ✅ |
| 174 | 174 | ||
| 175 | The tool is ready for production use! \ No newline at end of file | 175 | --- |
| 176 | |||
| 177 | # Wait Subcommand Implementation Plan | ||
| 178 | |||
| 179 | ## Overview | ||
| 180 | Add a `wait` subcommand to the demon CLI that blocks until a specified process terminates, with configurable timeout and polling interval. | ||
| 181 | |||
| 182 | ## Requirements Summary | ||
| 183 | - **Default timeout**: 30 seconds | ||
| 184 | - **Infinite timeout**: Use `--timeout 0` | ||
| 185 | - **Exit codes**: 0 for success, 1 for failure | ||
| 186 | - **PID file**: Leave untouched (don't clean up) | ||
| 187 | - **Output**: Quiet operation, only show error messages | ||
| 188 | - **Polling interval**: 1 second default, configurable with `--interval` flag | ||
| 189 | |||
| 190 | ## Implementation Details | ||
| 191 | |||
| 192 | ### 1. Command Structure | ||
| 193 | ```rust | ||
| 194 | /// Wait for a daemon process to terminate | ||
| 195 | Wait(WaitArgs), | ||
| 196 | ``` | ||
| 197 | |||
| 198 | ### 2. Arguments Structure | ||
| 199 | ```rust | ||
| 200 | #[derive(Args)] | ||
| 201 | struct WaitArgs { | ||
| 202 | /// Process identifier | ||
| 203 | id: String, | ||
| 204 | |||
| 205 | /// Timeout in seconds (0 = infinite) | ||
| 206 | #[arg(long, default_value = "30")] | ||
| 207 | timeout: u64, | ||
| 208 | |||
| 209 | /// Polling interval in seconds | ||
| 210 | #[arg(long, default_value = "1")] | ||
| 211 | interval: u64, | ||
| 212 | } | ||
| 213 | ``` | ||
| 214 | |||
| 215 | ### 3. Core Function Implementation | ||
| 216 | ```rust | ||
| 217 | fn wait_daemon(id: &str, timeout: u64, interval: u64) -> Result<()> { | ||
| 218 | // 1. Check if PID file exists | ||
| 219 | // 2. Read PID from file | ||
| 220 | // 3. Check if process exists initially | ||
| 221 | // 4. If timeout == 0, loop indefinitely | ||
| 222 | // 5. Otherwise, loop with timeout tracking | ||
| 223 | // 6. Poll every `interval` seconds | ||
| 224 | // 7. Return appropriate exit codes | ||
| 225 | } | ||
| 226 | ``` | ||
| 227 | |||
| 228 | ### 4. Logic Flow | ||
| 229 | 1. **Initial validation**: | ||
| 230 | - Check if PID file exists → error if not | ||
| 231 | - Read PID from file → error if invalid | ||
| 232 | - Check if process is running → error if already dead | ||
| 233 | |||
| 234 | 2. **Waiting loop**: | ||
| 235 | - If timeout = 0: infinite loop | ||
| 236 | - Otherwise: track elapsed time | ||
| 237 | - Poll every `interval` seconds using `is_process_running_by_pid()` | ||
| 238 | - Break when process terminates or timeout reached | ||
| 239 | |||
| 240 | 3. **Exit conditions**: | ||
| 241 | - Process terminates → exit 0 | ||
| 242 | - Timeout reached → error message + exit 1 | ||
| 243 | - Initial errors → error message + exit 1 | ||
| 244 | |||
| 245 | ### 5. Error Messages | ||
| 246 | - "Process '{id}' not found (no PID file)" | ||
| 247 | - "Process '{id}' is not running" | ||
| 248 | - "Timeout reached waiting for process '{id}' to terminate" | ||
| 249 | |||
| 250 | ## Testing Strategy | ||
| 251 | |||
| 252 | ### New Tests | ||
| 253 | 1. **test_wait_nonexistent_process**: Should fail with appropriate error | ||
| 254 | 2. **test_wait_already_dead_process**: Should fail when process already terminated | ||
| 255 | 3. **test_wait_process_terminates**: Should succeed when process terminates normally | ||
| 256 | 4. **test_wait_timeout**: Should fail when timeout is reached | ||
| 257 | 5. **test_wait_infinite_timeout**: Test with timeout=0 (use short-lived process) | ||
| 258 | 6. **test_wait_custom_interval**: Test with different polling intervals | ||
| 259 | |||
| 260 | ### Updated Tests | ||
| 261 | Replace `std::thread::sleep(Duration::from_millis(100))` with `demon wait` in: | ||
| 262 | - `test_run_creates_files` → `demon wait test --timeout 5` | ||
| 263 | - `test_run_with_complex_command` → `demon wait complex --timeout 5` | ||
| 264 | - Similar tests that wait for process completion | ||
| 265 | |||
| 266 | ## Files to Modify | ||
| 267 | |||
| 268 | ### 1. src/main.rs | ||
| 269 | - Add `Wait(WaitArgs)` to `Commands` enum (around line 146) | ||
| 270 | - Add `WaitArgs` struct after other Args structs (around line 206) | ||
| 271 | - Add `Commands::Wait(args) => wait_daemon(&args.id, args.timeout, args.interval)` to match statement (around line 246) | ||
| 272 | - Implement `wait_daemon()` function (add after other daemon functions) | ||
| 273 | |||
| 274 | ### 2. tests/cli.rs | ||
| 275 | - Add new test functions for wait subcommand | ||
| 276 | - Update existing tests to use wait instead of sleep where appropriate | ||
| 277 | |||
| 278 | ### 3. README.md | ||
| 279 | - Add wait command to command reference section | ||
| 280 | - Add examples showing wait usage | ||
| 281 | |||
| 282 | ### 4. LLM Guide (print_llm_guide function) | ||
| 283 | - Add wait command documentation | ||
| 284 | - Add to available commands list | ||
| 285 | - Add usage examples | ||
| 286 | |||
| 287 | ## Command Usage Examples | ||
| 288 | |||
| 289 | ```bash | ||
| 290 | # Wait with default 30s timeout | ||
| 291 | demon wait my-process | ||
| 292 | |||
| 293 | # Wait indefinitely | ||
| 294 | demon wait my-process --timeout 0 | ||
| 295 | |||
| 296 | # Wait with custom timeout and interval | ||
| 297 | demon wait my-process --timeout 60 --interval 2 | ||
| 298 | ``` | ||
| 299 | |||
| 300 | ## Implementation Order | ||
| 301 | 1. Implement core functionality in main.rs | ||
| 302 | 2. Add comprehensive tests | ||
| 303 | 3. Update existing tests to use wait | ||
| 304 | 4. Update documentation (README + LLM guide) | ||
| 305 | 5. Test full integration | ||
| 306 | |||
| 307 | ## Key Implementation Notes | ||
| 308 | - Use existing `is_process_running_by_pid()` function for consistency | ||
| 309 | - Use existing `PidFile::read_from_file()` for PID file handling | ||
| 310 | - Follow existing error handling patterns with anyhow | ||
| 311 | - Use `std::thread::sleep(Duration::from_secs(interval))` for polling | ||
| 312 | - Track elapsed time for timeout implementation | ||
| 313 | - Maintain quiet operation - no progress messages \ No newline at end of file | ||
| @@ -117,6 +117,20 @@ demon cat web-server | |||
| 117 | demon cat web-server --stdout | 117 | demon cat web-server --stdout |
| 118 | ``` | 118 | ``` |
| 119 | 119 | ||
| 120 | ### `demon wait <id> [--timeout <seconds>] [--interval <seconds>]` | ||
| 121 | Wait for a daemon process to terminate. | ||
| 122 | |||
| 123 | ```bash | ||
| 124 | # Wait with default 30-second timeout | ||
| 125 | demon wait web-server | ||
| 126 | |||
| 127 | # Wait indefinitely | ||
| 128 | demon wait web-server --timeout 0 | ||
| 129 | |||
| 130 | # Wait with custom timeout and polling interval | ||
| 131 | demon wait web-server --timeout 60 --interval 2 | ||
| 132 | ``` | ||
| 133 | |||
| 120 | ### `demon clean` | 134 | ### `demon clean` |
| 121 | Remove orphaned files from processes that are no longer running. | 135 | Remove orphaned files from processes that are no longer running. |
| 122 | 136 | ||
| @@ -145,6 +159,9 @@ demon run db-server docker run -p 5432:5432 postgres | |||
| 145 | # Monitor everything | 159 | # Monitor everything |
| 146 | demon list | 160 | demon list |
| 147 | demon tail api-server --stderr # Watch for errors | 161 | demon tail api-server --stderr # Watch for errors |
| 162 | |||
| 163 | # Wait for a specific service to finish | ||
| 164 | demon wait api-server | ||
| 148 | ``` | 165 | ``` |
| 149 | 166 | ||
| 150 | ### LLM Agent Integration | 167 | ### LLM Agent Integration |
| @@ -154,6 +171,9 @@ Designed for seamless automation and LLM agent workflows: | |||
| 154 | # Agents can start long-running processes | 171 | # Agents can start long-running processes |
| 155 | demon run data-processor python process_large_dataset.py | 172 | demon run data-processor python process_large_dataset.py |
| 156 | 173 | ||
| 174 | # Wait for the process to complete | ||
| 175 | demon wait data-processor --timeout 3600 # 1 hour timeout | ||
| 176 | |||
| 157 | # Check status programmatically | 177 | # Check status programmatically |
| 158 | if demon status data-processor | grep -q "RUNNING"; then | 178 | if demon status data-processor | grep -q "RUNNING"; then |
| 159 | echo "Processing is still running" | 179 | echo "Processing is still running" |
| @@ -223,9 +243,7 @@ demon list --quiet | |||
| 223 | demon list --quiet | grep -q "web-server:" || demon run web-server python -m http.server | 243 | demon list --quiet | grep -q "web-server:" || demon run web-server python -m http.server |
| 224 | 244 | ||
| 225 | # Wait for process to finish | 245 | # Wait for process to finish |
| 226 | while demon list --quiet | grep -q "backup-job:.*:RUNNING"; do | 246 | demon wait backup-job --timeout 0 # Wait indefinitely |
| 227 | sleep 5 | ||
| 228 | done | ||
| 229 | 247 | ||
| 230 | # Get all running processes | 248 | # Get all running processes |
| 231 | demon list --quiet | grep ":RUNNING" | cut -d: -f1 | 249 | demon list --quiet | grep ":RUNNING" | cut -d: -f1 |
diff --git a/src/main.rs b/src/main.rs index e90bfed..5547d9a 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -142,6 +142,9 @@ enum Commands { | |||
| 142 | 142 | ||
| 143 | /// Output comprehensive usage guide for LLMs | 143 | /// Output comprehensive usage guide for LLMs |
| 144 | Llm, | 144 | Llm, |
| 145 | |||
| 146 | /// Wait for a daemon process to terminate | ||
| 147 | Wait(WaitArgs), | ||
| 145 | } | 148 | } |
| 146 | 149 | ||
| 147 | #[derive(Args)] | 150 | #[derive(Args)] |
| @@ -204,6 +207,20 @@ struct StatusArgs { | |||
| 204 | id: String, | 207 | id: String, |
| 205 | } | 208 | } |
| 206 | 209 | ||
| 210 | #[derive(Args)] | ||
| 211 | struct WaitArgs { | ||
| 212 | /// Process identifier | ||
| 213 | id: String, | ||
| 214 | |||
| 215 | /// Timeout in seconds (0 = infinite) | ||
| 216 | #[arg(long, default_value = "30")] | ||
| 217 | timeout: u64, | ||
| 218 | |||
| 219 | /// Polling interval in seconds | ||
| 220 | #[arg(long, default_value = "1")] | ||
| 221 | interval: u64, | ||
| 222 | } | ||
| 223 | |||
| 207 | fn main() { | 224 | fn main() { |
| 208 | tracing_subscriber::fmt() | 225 | tracing_subscriber::fmt() |
| 209 | .with_writer(std::io::stderr) | 226 | .with_writer(std::io::stderr) |
| @@ -243,6 +260,7 @@ fn run_command(command: Commands) -> Result<()> { | |||
| 243 | print_llm_guide(); | 260 | print_llm_guide(); |
| 244 | Ok(()) | 261 | Ok(()) |
| 245 | } | 262 | } |
| 263 | Commands::Wait(args) => wait_daemon(&args.id, args.timeout, args.interval), | ||
| 246 | } | 264 | } |
| 247 | } | 265 | } |
| 248 | 266 | ||
| @@ -954,6 +972,28 @@ demon tail web-server # Follow both logs | |||
| 954 | demon tail web-server --stdout # Follow only stdout | 972 | demon tail web-server --stdout # Follow only stdout |
| 955 | ``` | 973 | ``` |
| 956 | 974 | ||
| 975 | ### demon wait <id> [--timeout <seconds>] [--interval <seconds>] | ||
| 976 | Blocks until a daemon process terminates. | ||
| 977 | |||
| 978 | **Syntax**: `demon wait <id> [--timeout <seconds>] [--interval <seconds>]` | ||
| 979 | |||
| 980 | **Behavior**: | ||
| 981 | - Checks if PID file exists and process is running | ||
| 982 | - Polls the process every `interval` seconds (default: 1 second) | ||
| 983 | - Waits for up to `timeout` seconds (default: 30 seconds) | ||
| 984 | - Use `--timeout 0` for infinite wait | ||
| 985 | - Exits successfully when process terminates | ||
| 986 | - Fails with error if process doesn't exist or timeout is reached | ||
| 987 | - Does not clean up PID files (use `demon clean` for that) | ||
| 988 | |||
| 989 | **Examples**: | ||
| 990 | ```bash | ||
| 991 | demon wait web-server # Wait 30s for termination | ||
| 992 | demon wait backup-job --timeout 0 # Wait indefinitely | ||
| 993 | demon wait data-processor --timeout 3600 # Wait up to 1 hour | ||
| 994 | demon wait short-task --interval 2 # Poll every 2 seconds | ||
| 995 | ``` | ||
| 996 | |||
| 957 | ### demon clean | 997 | ### demon clean |
| 958 | Removes orphaned files from processes that are no longer running. | 998 | Removes orphaned files from processes that are no longer running. |
| 959 | 999 | ||
| @@ -996,6 +1036,13 @@ demon status my-web-server # Check if it started | |||
| 996 | demon tail my-web-server # Monitor logs | 1036 | demon tail my-web-server # Monitor logs |
| 997 | ``` | 1037 | ``` |
| 998 | 1038 | ||
| 1039 | ### Waiting for Process Completion | ||
| 1040 | ```bash | ||
| 1041 | demon run batch-job python process_data.py | ||
| 1042 | demon wait batch-job --timeout 600 # Wait up to 10 minutes | ||
| 1043 | demon cat batch-job # Check output after completion | ||
| 1044 | ``` | ||
| 1045 | |||
| 999 | ### Running a Backup Job | 1046 | ### Running a Backup Job |
| 1000 | ```bash | 1047 | ```bash |
| 1001 | demon run nightly-backup -- rsync -av /data/ /backup/ | 1048 | demon run nightly-backup -- rsync -av /data/ /backup/ |
| @@ -1059,6 +1106,59 @@ This tool is designed for Linux environments and provides a simple interface for | |||
| 1059 | ); | 1106 | ); |
| 1060 | } | 1107 | } |
| 1061 | 1108 | ||
| 1109 | fn wait_daemon(id: &str, timeout: u64, interval: u64) -> Result<()> { | ||
| 1110 | let pid_file = format!("{}.pid", id); | ||
| 1111 | |||
| 1112 | // Check if PID file exists and read PID data | ||
| 1113 | let pid_file_data = match PidFile::read_from_file(&pid_file) { | ||
| 1114 | Ok(data) => data, | ||
| 1115 | Err(PidFileReadError::FileNotFound) => { | ||
| 1116 | return Err(anyhow::anyhow!("Process '{}' not found (no PID file)", id)); | ||
| 1117 | } | ||
| 1118 | Err(PidFileReadError::FileInvalid(reason)) => { | ||
| 1119 | return Err(anyhow::anyhow!("Process '{}' has invalid PID file: {}", id, reason)); | ||
| 1120 | } | ||
| 1121 | Err(PidFileReadError::IoError(err)) => { | ||
| 1122 | return Err(anyhow::anyhow!("Failed to read PID file for '{}': {}", id, err)); | ||
| 1123 | } | ||
| 1124 | }; | ||
| 1125 | |||
| 1126 | let pid = pid_file_data.pid; | ||
| 1127 | |||
| 1128 | // Check if process is currently running | ||
| 1129 | if !is_process_running_by_pid(pid) { | ||
| 1130 | return Err(anyhow::anyhow!("Process '{}' is not running", id)); | ||
| 1131 | } | ||
| 1132 | |||
| 1133 | tracing::info!("Waiting for process '{}' (PID: {}) to terminate", id, pid); | ||
| 1134 | |||
| 1135 | // Handle infinite timeout case | ||
| 1136 | if timeout == 0 { | ||
| 1137 | loop { | ||
| 1138 | if !is_process_running_by_pid(pid) { | ||
| 1139 | tracing::info!("Process '{}' (PID: {}) has terminated", id, pid); | ||
| 1140 | return Ok(()); | ||
| 1141 | } | ||
| 1142 | thread::sleep(Duration::from_secs(interval)); | ||
| 1143 | } | ||
| 1144 | } | ||
| 1145 | |||
| 1146 | // Handle timeout case | ||
| 1147 | let mut elapsed = 0; | ||
| 1148 | while elapsed < timeout { | ||
| 1149 | if !is_process_running_by_pid(pid) { | ||
| 1150 | tracing::info!("Process '{}' (PID: {}) has terminated", id, pid); | ||
| 1151 | return Ok(()); | ||
| 1152 | } | ||
| 1153 | |||
| 1154 | thread::sleep(Duration::from_secs(interval)); | ||
| 1155 | elapsed += interval; | ||
| 1156 | } | ||
| 1157 | |||
| 1158 | // Timeout reached | ||
| 1159 | Err(anyhow::anyhow!("Timeout reached waiting for process '{}' to terminate", id)) | ||
| 1160 | } | ||
| 1161 | |||
| 1062 | fn find_pid_files() -> Result<Vec<std::fs::DirEntry>> { | 1162 | fn find_pid_files() -> Result<Vec<std::fs::DirEntry>> { |
| 1063 | let entries = std::fs::read_dir(".")? | 1163 | let entries = std::fs::read_dir(".")? |
| 1064 | .filter_map(|entry| { | 1164 | .filter_map(|entry| { |
diff --git a/tests/cli.rs b/tests/cli.rs index 5e72dad..285eb70 100644 --- a/tests/cli.rs +++ b/tests/cli.rs | |||
| @@ -17,7 +17,8 @@ fn test_help_output() { | |||
| 17 | .stdout(predicate::str::contains("cat")) | 17 | .stdout(predicate::str::contains("cat")) |
| 18 | .stdout(predicate::str::contains("list")) | 18 | .stdout(predicate::str::contains("list")) |
| 19 | .stdout(predicate::str::contains("status")) | 19 | .stdout(predicate::str::contains("status")) |
| 20 | .stdout(predicate::str::contains("clean")); | 20 | .stdout(predicate::str::contains("clean")) |
| 21 | .stdout(predicate::str::contains("wait")); | ||
| 21 | } | 22 | } |
| 22 | 23 | ||
| 23 | #[test] | 24 | #[test] |
| @@ -410,7 +411,127 @@ fn test_llm_command() { | |||
| 410 | .stdout(predicate::str::contains("demon cat")) | 411 | .stdout(predicate::str::contains("demon cat")) |
| 411 | .stdout(predicate::str::contains("demon status")) | 412 | .stdout(predicate::str::contains("demon status")) |
| 412 | .stdout(predicate::str::contains("demon clean")) | 413 | .stdout(predicate::str::contains("demon clean")) |
| 414 | .stdout(predicate::str::contains("demon wait")) | ||
| 413 | .stdout(predicate::str::contains("Common Workflows")) | 415 | .stdout(predicate::str::contains("Common Workflows")) |
| 414 | .stdout(predicate::str::contains("Best Practices")) | 416 | .stdout(predicate::str::contains("Best Practices")) |
| 415 | .stdout(predicate::str::contains("Integration Tips")); | 417 | .stdout(predicate::str::contains("Integration Tips")); |
| 416 | } | 418 | } |
| 419 | |||
| 420 | #[test] | ||
| 421 | fn test_wait_nonexistent_process() { | ||
| 422 | let temp_dir = TempDir::new().unwrap(); | ||
| 423 | |||
| 424 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 425 | cmd.current_dir(temp_dir.path()) | ||
| 426 | .args(&["wait", "nonexistent"]) | ||
| 427 | .assert() | ||
| 428 | .failure() | ||
| 429 | .stderr(predicate::str::contains("not found")); | ||
| 430 | } | ||
| 431 | |||
| 432 | #[test] | ||
| 433 | fn test_wait_already_dead_process() { | ||
| 434 | let temp_dir = TempDir::new().unwrap(); | ||
| 435 | |||
| 436 | // Create a short-lived process | ||
| 437 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 438 | cmd.current_dir(temp_dir.path()) | ||
| 439 | .args(&["run", "dead", "echo", "hello"]) | ||
| 440 | .assert() | ||
| 441 | .success(); | ||
| 442 | |||
| 443 | // Give it time to finish | ||
| 444 | std::thread::sleep(Duration::from_millis(100)); | ||
| 445 | |||
| 446 | // Try to wait for it (should fail since it's already dead) | ||
| 447 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 448 | cmd.current_dir(temp_dir.path()) | ||
| 449 | .args(&["wait", "dead"]) | ||
| 450 | .assert() | ||
| 451 | .failure() | ||
| 452 | .stderr(predicate::str::contains("not running")); | ||
| 453 | } | ||
| 454 | |||
| 455 | #[test] | ||
| 456 | fn test_wait_process_terminates() { | ||
| 457 | let temp_dir = TempDir::new().unwrap(); | ||
| 458 | |||
| 459 | // Start a process that will run for 2 seconds | ||
| 460 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 461 | cmd.current_dir(temp_dir.path()) | ||
| 462 | .args(&["run", "short", "sleep", "2"]) | ||
| 463 | .assert() | ||
| 464 | .success(); | ||
| 465 | |||
| 466 | // Wait for it with a 5-second timeout (should succeed) | ||
| 467 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 468 | cmd.current_dir(temp_dir.path()) | ||
| 469 | .args(&["wait", "short", "--timeout", "5"]) | ||
| 470 | .assert() | ||
| 471 | .success(); | ||
| 472 | } | ||
| 473 | |||
| 474 | #[test] | ||
| 475 | fn test_wait_timeout() { | ||
| 476 | let temp_dir = TempDir::new().unwrap(); | ||
| 477 | |||
| 478 | // Start a long-running process | ||
| 479 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 480 | cmd.current_dir(temp_dir.path()) | ||
| 481 | .args(&["run", "long", "sleep", "10"]) | ||
| 482 | .assert() | ||
| 483 | .success(); | ||
| 484 | |||
| 485 | // Wait with a very short timeout (should fail) | ||
| 486 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 487 | cmd.current_dir(temp_dir.path()) | ||
| 488 | .args(&["wait", "long", "--timeout", "2"]) | ||
| 489 | .assert() | ||
| 490 | .failure() | ||
| 491 | .stderr(predicate::str::contains("Timeout reached")); | ||
| 492 | |||
| 493 | // Clean up the still-running process | ||
| 494 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 495 | cmd.current_dir(temp_dir.path()) | ||
| 496 | .args(&["stop", "long"]) | ||
| 497 | .assert() | ||
| 498 | .success(); | ||
| 499 | } | ||
| 500 | |||
| 501 | #[test] | ||
| 502 | fn test_wait_infinite_timeout() { | ||
| 503 | let temp_dir = TempDir::new().unwrap(); | ||
| 504 | |||
| 505 | // Start a short process that will finish quickly | ||
| 506 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 507 | cmd.current_dir(temp_dir.path()) | ||
| 508 | .args(&["run", "quick", "sleep", "1"]) | ||
| 509 | .assert() | ||
| 510 | .success(); | ||
| 511 | |||
| 512 | // Wait with infinite timeout (should succeed quickly) | ||
| 513 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 514 | cmd.current_dir(temp_dir.path()) | ||
| 515 | .args(&["wait", "quick", "--timeout", "0"]) | ||
| 516 | .assert() | ||
| 517 | .success(); | ||
| 518 | } | ||
| 519 | |||
| 520 | #[test] | ||
| 521 | fn test_wait_custom_interval() { | ||
| 522 | let temp_dir = TempDir::new().unwrap(); | ||
| 523 | |||
| 524 | // Start a short process | ||
| 525 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 526 | cmd.current_dir(temp_dir.path()) | ||
| 527 | .args(&["run", "interval-test", "sleep", "2"]) | ||
| 528 | .assert() | ||
| 529 | .success(); | ||
| 530 | |||
| 531 | // Wait with custom interval (should still succeed) | ||
| 532 | let mut cmd = Command::cargo_bin("demon").unwrap(); | ||
| 533 | cmd.current_dir(temp_dir.path()) | ||
| 534 | .args(&["wait", "interval-test", "--timeout", "5", "--interval", "2"]) | ||
| 535 | .assert() | ||
| 536 | .success(); | ||
| 537 | } | ||
