aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-06-19 10:03:56 +0100
committerdiogo464 <[email protected]>2025-06-19 10:03:56 +0100
commit5101d9c410a7c901ea20636d2a4e56b3282a1c14 (patch)
tree4b229f6ced108f14957a7e80c2c30880f7174811
parent7b7dbf8948fa063d040a744a4903be1df75ca943 (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.md140
-rw-r--r--README.md24
-rw-r--r--src/main.rs100
-rw-r--r--tests/cli.rs123
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
175The tool is ready for production use! \ No newline at end of file 175---
176
177# Wait Subcommand Implementation Plan
178
179## Overview
180Add 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
195Wait(WaitArgs),
196```
197
198### 2. Arguments Structure
199```rust
200#[derive(Args)]
201struct 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
217fn 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
2291. **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
2342. **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
2403. **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
2531. **test_wait_nonexistent_process**: Should fail with appropriate error
2542. **test_wait_already_dead_process**: Should fail when process already terminated
2553. **test_wait_process_terminates**: Should succeed when process terminates normally
2564. **test_wait_timeout**: Should fail when timeout is reached
2575. **test_wait_infinite_timeout**: Test with timeout=0 (use short-lived process)
2586. **test_wait_custom_interval**: Test with different polling intervals
259
260### Updated Tests
261Replace `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
291demon wait my-process
292
293# Wait indefinitely
294demon wait my-process --timeout 0
295
296# Wait with custom timeout and interval
297demon wait my-process --timeout 60 --interval 2
298```
299
300## Implementation Order
3011. Implement core functionality in main.rs
3022. Add comprehensive tests
3033. Update existing tests to use wait
3044. Update documentation (README + LLM guide)
3055. 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
diff --git a/README.md b/README.md
index cbc8fe5..99fb135 100644
--- a/README.md
+++ b/README.md
@@ -117,6 +117,20 @@ demon cat web-server
117demon cat web-server --stdout 117demon cat web-server --stdout
118``` 118```
119 119
120### `demon wait <id> [--timeout <seconds>] [--interval <seconds>]`
121Wait for a daemon process to terminate.
122
123```bash
124# Wait with default 30-second timeout
125demon wait web-server
126
127# Wait indefinitely
128demon wait web-server --timeout 0
129
130# Wait with custom timeout and polling interval
131demon wait web-server --timeout 60 --interval 2
132```
133
120### `demon clean` 134### `demon clean`
121Remove orphaned files from processes that are no longer running. 135Remove 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
146demon list 160demon list
147demon tail api-server --stderr # Watch for errors 161demon tail api-server --stderr # Watch for errors
162
163# Wait for a specific service to finish
164demon 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
155demon run data-processor python process_large_dataset.py 172demon run data-processor python process_large_dataset.py
156 173
174# Wait for the process to complete
175demon wait data-processor --timeout 3600 # 1 hour timeout
176
157# Check status programmatically 177# Check status programmatically
158if demon status data-processor | grep -q "RUNNING"; then 178if 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
223demon list --quiet | grep -q "web-server:" || demon run web-server python -m http.server 243demon 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
226while demon list --quiet | grep -q "backup-job:.*:RUNNING"; do 246demon wait backup-job --timeout 0 # Wait indefinitely
227 sleep 5
228done
229 247
230# Get all running processes 248# Get all running processes
231demon list --quiet | grep ":RUNNING" | cut -d: -f1 249demon 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)]
211struct 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
207fn main() { 224fn 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
954demon tail web-server --stdout # Follow only stdout 972demon tail web-server --stdout # Follow only stdout
955``` 973```
956 974
975### demon wait <id> [--timeout <seconds>] [--interval <seconds>]
976Blocks 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
991demon wait web-server # Wait 30s for termination
992demon wait backup-job --timeout 0 # Wait indefinitely
993demon wait data-processor --timeout 3600 # Wait up to 1 hour
994demon wait short-task --interval 2 # Poll every 2 seconds
995```
996
957### demon clean 997### demon clean
958Removes orphaned files from processes that are no longer running. 998Removes orphaned files from processes that are no longer running.
959 999
@@ -996,6 +1036,13 @@ demon status my-web-server # Check if it started
996demon tail my-web-server # Monitor logs 1036demon tail my-web-server # Monitor logs
997``` 1037```
998 1038
1039### Waiting for Process Completion
1040```bash
1041demon run batch-job python process_data.py
1042demon wait batch-job --timeout 600 # Wait up to 10 minutes
1043demon cat batch-job # Check output after completion
1044```
1045
999### Running a Backup Job 1046### Running a Backup Job
1000```bash 1047```bash
1001demon run nightly-backup -- rsync -av /data/ /backup/ 1048demon 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
1109fn 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
1062fn find_pid_files() -> Result<Vec<std::fs::DirEntry>> { 1162fn 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]
421fn 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]
433fn 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]
456fn 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]
475fn 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]
502fn 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]
521fn 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}