aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-06-19 09:18:18 +0100
committerdiogo464 <[email protected]>2025-06-19 09:18:18 +0100
commit1c2e20c56d7fdbb0f7b21d12137ec7d58cd839c8 (patch)
tree9ea6d12018eacb4b9928fb7b667333682f3b5491
parenta8e1212c07fb489cfa430ef64fb3a1c8df464a22 (diff)
Format code with rustfmt
- Apply standard Rust formatting conventions - Improve code readability and consistency - Reorganize imports alphabetically - Fix line lengths and indentation - All tests continue to pass 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
-rw-r--r--src/main.rs302
-rw-r--r--tests/cli.rs105
2 files changed, 231 insertions, 176 deletions
diff --git a/src/main.rs b/src/main.rs
index 37a7cdb..ac6e305 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,13 +1,13 @@
1use clap::{Parser, Subcommand, Args}; 1use anyhow::{Context, Result};
2use clap::{Args, Parser, Subcommand};
3use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
2use std::fs::File; 4use std::fs::File;
3use std::io::{Write, Read, Seek, SeekFrom}; 5use std::io::{Read, Seek, SeekFrom, Write};
6use std::path::Path;
4use std::process::{Command, Stdio}; 7use std::process::{Command, Stdio};
8use std::sync::mpsc::channel;
5use std::thread; 9use std::thread;
6use std::time::Duration; 10use std::time::Duration;
7use std::path::Path;
8use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
9use std::sync::mpsc::channel;
10use anyhow::{Result, Context};
11 11
12#[derive(Parser)] 12#[derive(Parser)]
13#[command(name = "demon")] 13#[command(name = "demon")]
@@ -23,25 +23,25 @@ struct Cli {
23enum Commands { 23enum Commands {
24 /// Spawn a background process and redirect stdout/stderr to files 24 /// Spawn a background process and redirect stdout/stderr to files
25 Run(RunArgs), 25 Run(RunArgs),
26 26
27 /// Stop a running daemon process 27 /// Stop a running daemon process
28 Stop(StopArgs), 28 Stop(StopArgs),
29 29
30 /// Tail daemon logs in real-time 30 /// Tail daemon logs in real-time
31 Tail(TailArgs), 31 Tail(TailArgs),
32 32
33 /// Display daemon log contents 33 /// Display daemon log contents
34 Cat(CatArgs), 34 Cat(CatArgs),
35 35
36 /// List all running daemon processes 36 /// List all running daemon processes
37 List(ListArgs), 37 List(ListArgs),
38 38
39 /// Check status of a daemon process 39 /// Check status of a daemon process
40 Status(StatusArgs), 40 Status(StatusArgs),
41 41
42 /// Clean up orphaned pid and log files 42 /// Clean up orphaned pid and log files
43 Clean, 43 Clean,
44 44
45 /// Output comprehensive usage guide for LLMs 45 /// Output comprehensive usage guide for LLMs
46 Llm, 46 Llm,
47} 47}
@@ -51,7 +51,7 @@ struct RunArgs {
51 /// Process identifier 51 /// Process identifier
52 #[arg(long)] 52 #[arg(long)]
53 id: String, 53 id: String,
54 54
55 /// Command and arguments to execute 55 /// Command and arguments to execute
56 command: Vec<String>, 56 command: Vec<String>,
57} 57}
@@ -61,7 +61,7 @@ struct StopArgs {
61 /// Process identifier 61 /// Process identifier
62 #[arg(long)] 62 #[arg(long)]
63 id: String, 63 id: String,
64 64
65 /// Timeout in seconds before sending SIGKILL after SIGTERM 65 /// Timeout in seconds before sending SIGKILL after SIGTERM
66 #[arg(long, default_value = "10")] 66 #[arg(long, default_value = "10")]
67 timeout: u64, 67 timeout: u64,
@@ -72,11 +72,11 @@ struct TailArgs {
72 /// Process identifier 72 /// Process identifier
73 #[arg(long)] 73 #[arg(long)]
74 id: String, 74 id: String,
75 75
76 /// Only tail stdout 76 /// Only tail stdout
77 #[arg(long)] 77 #[arg(long)]
78 stdout: bool, 78 stdout: bool,
79 79
80 /// Only tail stderr 80 /// Only tail stderr
81 #[arg(long)] 81 #[arg(long)]
82 stderr: bool, 82 stderr: bool,
@@ -87,11 +87,11 @@ struct CatArgs {
87 /// Process identifier 87 /// Process identifier
88 #[arg(long)] 88 #[arg(long)]
89 id: String, 89 id: String,
90 90
91 /// Only show stdout 91 /// Only show stdout
92 #[arg(long)] 92 #[arg(long)]
93 stdout: bool, 93 stdout: bool,
94 94
95 /// Only show stderr 95 /// Only show stderr
96 #[arg(long)] 96 #[arg(long)]
97 stderr: bool, 97 stderr: bool,
@@ -117,7 +117,7 @@ fn main() {
117 .init(); 117 .init();
118 118
119 let cli = Cli::parse(); 119 let cli = Cli::parse();
120 120
121 if let Err(e) = run_command(cli.command) { 121 if let Err(e) = run_command(cli.command) {
122 tracing::error!("Error: {}", e); 122 tracing::error!("Error: {}", e);
123 std::process::exit(1); 123 std::process::exit(1);
@@ -132,9 +132,7 @@ fn run_command(command: Commands) -> Result<()> {
132 } 132 }
133 run_daemon(&args.id, &args.command) 133 run_daemon(&args.id, &args.command)
134 } 134 }
135 Commands::Stop(args) => { 135 Commands::Stop(args) => stop_daemon(&args.id, args.timeout),
136 stop_daemon(&args.id, args.timeout)
137 }
138 Commands::Tail(args) => { 136 Commands::Tail(args) => {
139 let show_stdout = !args.stderr || args.stdout; 137 let show_stdout = !args.stderr || args.stdout;
140 let show_stderr = !args.stdout || args.stderr; 138 let show_stderr = !args.stdout || args.stderr;
@@ -145,15 +143,9 @@ fn run_command(command: Commands) -> Result<()> {
145 let show_stderr = !args.stdout || args.stderr; 143 let show_stderr = !args.stdout || args.stderr;
146 cat_logs(&args.id, show_stdout, show_stderr) 144 cat_logs(&args.id, show_stdout, show_stderr)
147 } 145 }
148 Commands::List(args) => { 146 Commands::List(args) => list_daemons(args.quiet),
149 list_daemons(args.quiet) 147 Commands::Status(args) => status_daemon(&args.id),
150 } 148 Commands::Clean => clean_orphaned_files(),
151 Commands::Status(args) => {
152 status_daemon(&args.id)
153 }
154 Commands::Clean => {
155 clean_orphaned_files()
156 }
157 Commands::Llm => { 149 Commands::Llm => {
158 print_llm_guide(); 150 print_llm_guide();
159 Ok(()) 151 Ok(())
@@ -165,26 +157,30 @@ fn run_daemon(id: &str, command: &[String]) -> Result<()> {
165 let pid_file = format!("{}.pid", id); 157 let pid_file = format!("{}.pid", id);
166 let stdout_file = format!("{}.stdout", id); 158 let stdout_file = format!("{}.stdout", id);
167 let stderr_file = format!("{}.stderr", id); 159 let stderr_file = format!("{}.stderr", id);
168 160
169 // Check if process is already running 161 // Check if process is already running
170 if is_process_running(&pid_file)? { 162 if is_process_running(&pid_file)? {
171 return Err(anyhow::anyhow!("Process '{}' is already running", id)); 163 return Err(anyhow::anyhow!("Process '{}' is already running", id));
172 } 164 }
173 165
174 tracing::info!("Starting daemon '{}' with command: {:?}", id, command); 166 tracing::info!("Starting daemon '{}' with command: {:?}", id, command);
175 167
176 // Truncate/create output files 168 // Truncate/create output files
177 File::create(&stdout_file)?; 169 File::create(&stdout_file)?;
178 File::create(&stderr_file)?; 170 File::create(&stderr_file)?;
179 171
180 // Open files for redirection 172 // Open files for redirection
181 let stdout_redirect = File::create(&stdout_file)?; 173 let stdout_redirect = File::create(&stdout_file)?;
182 let stderr_redirect = File::create(&stderr_file)?; 174 let stderr_redirect = File::create(&stderr_file)?;
183 175
184 // Spawn the process 176 // Spawn the process
185 let program = &command[0]; 177 let program = &command[0];
186 let args = if command.len() > 1 { &command[1..] } else { &[] }; 178 let args = if command.len() > 1 {
187 179 &command[1..]
180 } else {
181 &[]
182 };
183
188 let child = Command::new(program) 184 let child = Command::new(program)
189 .args(args) 185 .args(args)
190 .stdout(Stdio::from(stdout_redirect)) 186 .stdout(Stdio::from(stdout_redirect))
@@ -192,16 +188,16 @@ fn run_daemon(id: &str, command: &[String]) -> Result<()> {
192 .stdin(Stdio::null()) 188 .stdin(Stdio::null())
193 .spawn() 189 .spawn()
194 .with_context(|| format!("Failed to start process '{}' with args {:?}", program, args))?; 190 .with_context(|| format!("Failed to start process '{}' with args {:?}", program, args))?;
195 191
196 // Write PID to file 192 // Write PID to file
197 let mut pid_file_handle = File::create(&pid_file)?; 193 let mut pid_file_handle = File::create(&pid_file)?;
198 writeln!(pid_file_handle, "{}", child.id())?; 194 writeln!(pid_file_handle, "{}", child.id())?;
199 195
200 // Don't wait for the child - let it run detached 196 // Don't wait for the child - let it run detached
201 std::mem::forget(child); 197 std::mem::forget(child);
202 198
203 println!("Started daemon '{}' with PID written to {}", id, pid_file); 199 println!("Started daemon '{}' with PID written to {}", id, pid_file);
204 200
205 Ok(()) 201 Ok(())
206} 202}
207 203
@@ -211,26 +207,26 @@ fn is_process_running(pid_file: &str) -> Result<bool> {
211 Ok(f) => f, 207 Ok(f) => f,
212 Err(_) => return Ok(false), // No PID file means no running process 208 Err(_) => return Ok(false), // No PID file means no running process
213 }; 209 };
214 210
215 let mut contents = String::new(); 211 let mut contents = String::new();
216 file.read_to_string(&mut contents)?; 212 file.read_to_string(&mut contents)?;
217 213
218 let pid: u32 = match contents.trim().parse() { 214 let pid: u32 = match contents.trim().parse() {
219 Ok(p) => p, 215 Ok(p) => p,
220 Err(_) => return Ok(false), // Invalid PID file 216 Err(_) => return Ok(false), // Invalid PID file
221 }; 217 };
222 218
223 // Check if process is still running using kill -0 219 // Check if process is still running using kill -0
224 let output = Command::new("kill") 220 let output = Command::new("kill")
225 .args(&["-0", &pid.to_string()]) 221 .args(&["-0", &pid.to_string()])
226 .output()?; 222 .output()?;
227 223
228 Ok(output.status.success()) 224 Ok(output.status.success())
229} 225}
230 226
231fn stop_daemon(id: &str, timeout: u64) -> Result<()> { 227fn stop_daemon(id: &str, timeout: u64) -> Result<()> {
232 let pid_file = format!("{}.pid", id); 228 let pid_file = format!("{}.pid", id);
233 229
234 // Check if PID file exists 230 // Check if PID file exists
235 let mut file = match File::open(&pid_file) { 231 let mut file = match File::open(&pid_file) {
236 Ok(f) => f, 232 Ok(f) => f,
@@ -239,11 +235,11 @@ fn stop_daemon(id: &str, timeout: u64) -> Result<()> {
239 return Ok(()); 235 return Ok(());
240 } 236 }
241 }; 237 };
242 238
243 // Read PID 239 // Read PID
244 let mut contents = String::new(); 240 let mut contents = String::new();
245 file.read_to_string(&mut contents)?; 241 file.read_to_string(&mut contents)?;
246 242
247 let pid: u32 = match contents.trim().parse() { 243 let pid: u32 = match contents.trim().parse() {
248 Ok(p) => p, 244 Ok(p) => p,
249 Err(_) => { 245 Err(_) => {
@@ -252,26 +248,34 @@ fn stop_daemon(id: &str, timeout: u64) -> Result<()> {
252 return Ok(()); 248 return Ok(());
253 } 249 }
254 }; 250 };
255 251
256 tracing::info!("Stopping daemon '{}' (PID: {}) with timeout {}s", id, pid, timeout); 252 tracing::info!(
257 253 "Stopping daemon '{}' (PID: {}) with timeout {}s",
254 id,
255 pid,
256 timeout
257 );
258
258 // Check if process is running 259 // Check if process is running
259 if !is_process_running_by_pid(pid) { 260 if !is_process_running_by_pid(pid) {
260 println!("Process '{}' (PID: {}) is not running, cleaning up PID file", id, pid); 261 println!(
262 "Process '{}' (PID: {}) is not running, cleaning up PID file",
263 id, pid
264 );
261 std::fs::remove_file(&pid_file)?; 265 std::fs::remove_file(&pid_file)?;
262 return Ok(()); 266 return Ok(());
263 } 267 }
264 268
265 // Send SIGTERM 269 // Send SIGTERM
266 tracing::info!("Sending SIGTERM to PID {}", pid); 270 tracing::info!("Sending SIGTERM to PID {}", pid);
267 let output = Command::new("kill") 271 let output = Command::new("kill")
268 .args(&["-TERM", &pid.to_string()]) 272 .args(&["-TERM", &pid.to_string()])
269 .output()?; 273 .output()?;
270 274
271 if !output.status.success() { 275 if !output.status.success() {
272 return Err(anyhow::anyhow!("Failed to send SIGTERM to PID {}", pid)); 276 return Err(anyhow::anyhow!("Failed to send SIGTERM to PID {}", pid));
273 } 277 }
274 278
275 // Wait for the process to terminate 279 // Wait for the process to terminate
276 for i in 0..timeout { 280 for i in 0..timeout {
277 if !is_process_running_by_pid(pid) { 281 if !is_process_running_by_pid(pid) {
@@ -279,34 +283,41 @@ fn stop_daemon(id: &str, timeout: u64) -> Result<()> {
279 std::fs::remove_file(&pid_file)?; 283 std::fs::remove_file(&pid_file)?;
280 return Ok(()); 284 return Ok(());
281 } 285 }
282 286
283 if i == 0 { 287 if i == 0 {
284 tracing::info!("Waiting for process to terminate gracefully..."); 288 tracing::info!("Waiting for process to terminate gracefully...");
285 } 289 }
286 290
287 thread::sleep(Duration::from_secs(1)); 291 thread::sleep(Duration::from_secs(1));
288 } 292 }
289 293
290 // Process didn't terminate, send SIGKILL 294 // Process didn't terminate, send SIGKILL
291 tracing::warn!("Process {} didn't terminate after {}s, sending SIGKILL", pid, timeout); 295 tracing::warn!(
296 "Process {} didn't terminate after {}s, sending SIGKILL",
297 pid,
298 timeout
299 );
292 let output = Command::new("kill") 300 let output = Command::new("kill")
293 .args(&["-KILL", &pid.to_string()]) 301 .args(&["-KILL", &pid.to_string()])
294 .output()?; 302 .output()?;
295 303
296 if !output.status.success() { 304 if !output.status.success() {
297 return Err(anyhow::anyhow!("Failed to send SIGKILL to PID {}", pid)); 305 return Err(anyhow::anyhow!("Failed to send SIGKILL to PID {}", pid));
298 } 306 }
299 307
300 // Wait a bit more for SIGKILL to take effect 308 // Wait a bit more for SIGKILL to take effect
301 thread::sleep(Duration::from_secs(1)); 309 thread::sleep(Duration::from_secs(1));
302 310
303 if is_process_running_by_pid(pid) { 311 if is_process_running_by_pid(pid) {
304 return Err(anyhow::anyhow!("Process {} is still running after SIGKILL", pid)); 312 return Err(anyhow::anyhow!(
313 "Process {} is still running after SIGKILL",
314 pid
315 ));
305 } 316 }
306 317
307 println!("Process '{}' (PID: {}) terminated forcefully", id, pid); 318 println!("Process '{}' (PID: {}) terminated forcefully", id, pid);
308 std::fs::remove_file(&pid_file)?; 319 std::fs::remove_file(&pid_file)?;
309 320
310 Ok(()) 321 Ok(())
311} 322}
312 323
@@ -314,7 +325,7 @@ fn is_process_running_by_pid(pid: u32) -> bool {
314 let output = Command::new("kill") 325 let output = Command::new("kill")
315 .args(&["-0", &pid.to_string()]) 326 .args(&["-0", &pid.to_string()])
316 .output(); 327 .output();
317 328
318 match output { 329 match output {
319 Ok(output) => output.status.success(), 330 Ok(output) => output.status.success(),
320 Err(_) => false, 331 Err(_) => false,
@@ -324,9 +335,9 @@ fn is_process_running_by_pid(pid: u32) -> bool {
324fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { 335fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> {
325 let stdout_file = format!("{}.stdout", id); 336 let stdout_file = format!("{}.stdout", id);
326 let stderr_file = format!("{}.stderr", id); 337 let stderr_file = format!("{}.stderr", id);
327 338
328 let mut files_found = false; 339 let mut files_found = false;
329 340
330 if show_stdout { 341 if show_stdout {
331 if let Ok(contents) = std::fs::read_to_string(&stdout_file) { 342 if let Ok(contents) = std::fs::read_to_string(&stdout_file) {
332 if !contents.is_empty() { 343 if !contents.is_empty() {
@@ -340,7 +351,7 @@ fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> {
340 tracing::warn!("Could not read {}", stdout_file); 351 tracing::warn!("Could not read {}", stdout_file);
341 } 352 }
342 } 353 }
343 354
344 if show_stderr { 355 if show_stderr {
345 if let Ok(contents) = std::fs::read_to_string(&stderr_file) { 356 if let Ok(contents) = std::fs::read_to_string(&stderr_file) {
346 if !contents.is_empty() { 357 if !contents.is_empty() {
@@ -354,21 +365,22 @@ fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> {
354 tracing::warn!("Could not read {}", stderr_file); 365 tracing::warn!("Could not read {}", stderr_file);
355 } 366 }
356 } 367 }
357 368
358 if !files_found { 369 if !files_found {
359 println!("No log files found for daemon '{}'", id); 370 println!("No log files found for daemon '{}'", id);
360 } 371 }
361 372
362 Ok(()) 373 Ok(())
363} 374}
364 375
365fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { 376fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> {
366 let stdout_file = format!("{}.stdout", id); 377 let stdout_file = format!("{}.stdout", id);
367 let stderr_file = format!("{}.stderr", id); 378 let stderr_file = format!("{}.stderr", id);
368 379
369 // First, display existing content and set up initial positions 380 // First, display existing content and set up initial positions
370 let mut file_positions: std::collections::HashMap<String, u64> = std::collections::HashMap::new(); 381 let mut file_positions: std::collections::HashMap<String, u64> =
371 382 std::collections::HashMap::new();
383
372 if show_stdout && Path::new(&stdout_file).exists() { 384 if show_stdout && Path::new(&stdout_file).exists() {
373 let mut file = File::open(&stdout_file)?; 385 let mut file = File::open(&stdout_file)?;
374 let initial_content = read_file_content(&mut file)?; 386 let initial_content = read_file_content(&mut file)?;
@@ -381,7 +393,7 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> {
381 let position = file.seek(SeekFrom::Current(0))?; 393 let position = file.seek(SeekFrom::Current(0))?;
382 file_positions.insert(stdout_file.clone(), position); 394 file_positions.insert(stdout_file.clone(), position);
383 } 395 }
384 396
385 if show_stderr && Path::new(&stderr_file).exists() { 397 if show_stderr && Path::new(&stderr_file).exists() {
386 let mut file = File::open(&stderr_file)?; 398 let mut file = File::open(&stderr_file)?;
387 let initial_content = read_file_content(&mut file)?; 399 let initial_content = read_file_content(&mut file)?;
@@ -396,28 +408,31 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> {
396 let position = file.seek(SeekFrom::Current(0))?; 408 let position = file.seek(SeekFrom::Current(0))?;
397 file_positions.insert(stderr_file.clone(), position); 409 file_positions.insert(stderr_file.clone(), position);
398 } 410 }
399 411
400 if file_positions.is_empty() { 412 if file_positions.is_empty() {
401 println!("No log files found for daemon '{}'. Watching for new files...", id); 413 println!(
414 "No log files found for daemon '{}'. Watching for new files...",
415 id
416 );
402 } 417 }
403 418
404 tracing::info!("Watching for changes to log files... Press Ctrl+C to stop."); 419 tracing::info!("Watching for changes to log files... Press Ctrl+C to stop.");
405 420
406 // Set up file watcher 421 // Set up file watcher
407 let (tx, rx) = channel(); 422 let (tx, rx) = channel();
408 let mut watcher = RecommendedWatcher::new(tx, Config::default())?; 423 let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
409 424
410 // Watch the current directory for new files and changes 425 // Watch the current directory for new files and changes
411 watcher.watch(Path::new("."), RecursiveMode::NonRecursive)?; 426 watcher.watch(Path::new("."), RecursiveMode::NonRecursive)?;
412 427
413 // Handle Ctrl+C gracefully 428 // Handle Ctrl+C gracefully
414 let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); 429 let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
415 let r = running.clone(); 430 let r = running.clone();
416 431
417 ctrlc::set_handler(move || { 432 ctrlc::set_handler(move || {
418 r.store(false, std::sync::atomic::Ordering::SeqCst); 433 r.store(false, std::sync::atomic::Ordering::SeqCst);
419 })?; 434 })?;
420 435
421 while running.load(std::sync::atomic::Ordering::SeqCst) { 436 while running.load(std::sync::atomic::Ordering::SeqCst) {
422 match rx.recv_timeout(Duration::from_millis(100)) { 437 match rx.recv_timeout(Duration::from_millis(100)) {
423 Ok(res) => { 438 Ok(res) => {
@@ -429,11 +444,15 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> {
429 }) => { 444 }) => {
430 for path in paths { 445 for path in paths {
431 let path_str = path.to_string_lossy().to_string(); 446 let path_str = path.to_string_lossy().to_string();
432 447
433 if (show_stdout && path_str == stdout_file) || 448 if (show_stdout && path_str == stdout_file)
434 (show_stderr && path_str == stderr_file) { 449 || (show_stderr && path_str == stderr_file)
435 450 {
436 if let Err(e) = handle_file_change(&path_str, &mut file_positions, show_stdout && show_stderr) { 451 if let Err(e) = handle_file_change(
452 &path_str,
453 &mut file_positions,
454 show_stdout && show_stderr,
455 ) {
437 tracing::error!("Error handling file change: {}", e); 456 tracing::error!("Error handling file change: {}", e);
438 } 457 }
439 } 458 }
@@ -447,14 +466,18 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> {
447 // Handle file creation 466 // Handle file creation
448 for path in paths { 467 for path in paths {
449 let path_str = path.to_string_lossy().to_string(); 468 let path_str = path.to_string_lossy().to_string();
450 469
451 if (show_stdout && path_str == stdout_file) || 470 if (show_stdout && path_str == stdout_file)
452 (show_stderr && path_str == stderr_file) { 471 || (show_stderr && path_str == stderr_file)
453 472 {
454 tracing::info!("New file detected: {}", path_str); 473 tracing::info!("New file detected: {}", path_str);
455 file_positions.insert(path_str.clone(), 0); 474 file_positions.insert(path_str.clone(), 0);
456 475
457 if let Err(e) = handle_file_change(&path_str, &mut file_positions, show_stdout && show_stderr) { 476 if let Err(e) = handle_file_change(
477 &path_str,
478 &mut file_positions,
479 show_stdout && show_stderr,
480 ) {
458 tracing::error!("Error handling new file: {}", e); 481 tracing::error!("Error handling new file: {}", e);
459 } 482 }
460 } 483 }
@@ -473,7 +496,7 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> {
473 } 496 }
474 } 497 }
475 } 498 }
476 499
477 println!("\nTailing stopped."); 500 println!("\nTailing stopped.");
478 Ok(()) 501 Ok(())
479} 502}
@@ -485,32 +508,32 @@ fn read_file_content(file: &mut File) -> Result<String> {
485} 508}
486 509
487fn handle_file_change( 510fn handle_file_change(
488 file_path: &str, 511 file_path: &str,
489 positions: &mut std::collections::HashMap<String, u64>, 512 positions: &mut std::collections::HashMap<String, u64>,
490 show_headers: bool 513 show_headers: bool,
491) -> Result<()> { 514) -> Result<()> {
492 let mut file = File::open(file_path)?; 515 let mut file = File::open(file_path)?;
493 let current_pos = positions.get(file_path).copied().unwrap_or(0); 516 let current_pos = positions.get(file_path).copied().unwrap_or(0);
494 517
495 // Seek to the last read position 518 // Seek to the last read position
496 file.seek(SeekFrom::Start(current_pos))?; 519 file.seek(SeekFrom::Start(current_pos))?;
497 520
498 // Read new content 521 // Read new content
499 let mut new_content = String::new(); 522 let mut new_content = String::new();
500 file.read_to_string(&mut new_content)?; 523 file.read_to_string(&mut new_content)?;
501 524
502 if !new_content.is_empty() { 525 if !new_content.is_empty() {
503 if show_headers { 526 if show_headers {
504 println!("==> {} <==", file_path); 527 println!("==> {} <==", file_path);
505 } 528 }
506 print!("{}", new_content); 529 print!("{}", new_content);
507 std::io::Write::flush(&mut std::io::stdout())?; 530 std::io::Write::flush(&mut std::io::stdout())?;
508 531
509 // Update position 532 // Update position
510 let new_pos = file.seek(SeekFrom::Current(0))?; 533 let new_pos = file.seek(SeekFrom::Current(0))?;
511 positions.insert(file_path.to_string(), new_pos); 534 positions.insert(file_path.to_string(), new_pos);
512 } 535 }
513 536
514 Ok(()) 537 Ok(())
515} 538}
516 539
@@ -519,18 +542,18 @@ fn list_daemons(quiet: bool) -> Result<()> {
519 println!("{:<20} {:<8} {:<10} {}", "ID", "PID", "STATUS", "COMMAND"); 542 println!("{:<20} {:<8} {:<10} {}", "ID", "PID", "STATUS", "COMMAND");
520 println!("{}", "-".repeat(50)); 543 println!("{}", "-".repeat(50));
521 } 544 }
522 545
523 let mut found_any = false; 546 let mut found_any = false;
524 547
525 // Find all .pid files in current directory 548 // Find all .pid files in current directory
526 for entry in find_pid_files()? { 549 for entry in find_pid_files()? {
527 found_any = true; 550 found_any = true;
528 let path = entry.path(); 551 let path = entry.path();
529 let path_str = path.to_string_lossy(); 552 let path_str = path.to_string_lossy();
530 553
531 // Extract ID from filename (remove .pid extension) 554 // Extract ID from filename (remove .pid extension)
532 let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); 555 let id = path_str.strip_suffix(".pid").unwrap_or(&path_str);
533 556
534 // Read PID from file 557 // Read PID from file
535 match std::fs::read_to_string(&path) { 558 match std::fs::read_to_string(&path) {
536 Ok(contents) => { 559 Ok(contents) => {
@@ -542,7 +565,7 @@ fn list_daemons(quiet: bool) -> Result<()> {
542 } else { 565 } else {
543 "DEAD" 566 "DEAD"
544 }; 567 };
545 568
546 if quiet { 569 if quiet {
547 println!("{}:{}:{}", id, pid, status); 570 println!("{}:{}:{}", id, pid, status);
548 } else { 571 } else {
@@ -556,7 +579,10 @@ fn list_daemons(quiet: bool) -> Result<()> {
556 if quiet { 579 if quiet {
557 println!("{}:INVALID:ERROR", id); 580 println!("{}:INVALID:ERROR", id);
558 } else { 581 } else {
559 println!("{:<20} {:<8} {:<10} {}", id, "INVALID", "ERROR", "Invalid PID file"); 582 println!(
583 "{:<20} {:<8} {:<10} {}",
584 id, "INVALID", "ERROR", "Invalid PID file"
585 );
560 } 586 }
561 } 587 }
562 } 588 }
@@ -565,16 +591,22 @@ fn list_daemons(quiet: bool) -> Result<()> {
565 if quiet { 591 if quiet {
566 println!("{}:ERROR:ERROR", id); 592 println!("{}:ERROR:ERROR", id);
567 } else { 593 } else {
568 println!("{:<20} {:<8} {:<10} {}", id, "ERROR", "ERROR", format!("Cannot read: {}", e)); 594 println!(
595 "{:<20} {:<8} {:<10} {}",
596 id,
597 "ERROR",
598 "ERROR",
599 format!("Cannot read: {}", e)
600 );
569 } 601 }
570 } 602 }
571 } 603 }
572 } 604 }
573 605
574 if !found_any && !quiet { 606 if !found_any && !quiet {
575 println!("No daemon processes found."); 607 println!("No daemon processes found.");
576 } 608 }
577 609
578 Ok(()) 610 Ok(())
579} 611}
580 612
@@ -582,16 +614,16 @@ fn status_daemon(id: &str) -> Result<()> {
582 let pid_file = format!("{}.pid", id); 614 let pid_file = format!("{}.pid", id);
583 let stdout_file = format!("{}.stdout", id); 615 let stdout_file = format!("{}.stdout", id);
584 let stderr_file = format!("{}.stderr", id); 616 let stderr_file = format!("{}.stderr", id);
585 617
586 println!("Daemon: {}", id); 618 println!("Daemon: {}", id);
587 println!("PID file: {}", pid_file); 619 println!("PID file: {}", pid_file);
588 620
589 // Check if PID file exists 621 // Check if PID file exists
590 if !Path::new(&pid_file).exists() { 622 if !Path::new(&pid_file).exists() {
591 println!("Status: NOT FOUND (no PID file)"); 623 println!("Status: NOT FOUND (no PID file)");
592 return Ok(()); 624 return Ok(());
593 } 625 }
594 626
595 // Read PID from file 627 // Read PID from file
596 match std::fs::read_to_string(&pid_file) { 628 match std::fs::read_to_string(&pid_file) {
597 Ok(contents) => { 629 Ok(contents) => {
@@ -599,10 +631,10 @@ fn status_daemon(id: &str) -> Result<()> {
599 match pid_str.parse::<u32>() { 631 match pid_str.parse::<u32>() {
600 Ok(pid) => { 632 Ok(pid) => {
601 println!("PID: {}", pid); 633 println!("PID: {}", pid);
602 634
603 if is_process_running_by_pid(pid) { 635 if is_process_running_by_pid(pid) {
604 println!("Status: RUNNING"); 636 println!("Status: RUNNING");
605 637
606 // Show file information 638 // Show file information
607 if Path::new(&stdout_file).exists() { 639 if Path::new(&stdout_file).exists() {
608 let metadata = std::fs::metadata(&stdout_file)?; 640 let metadata = std::fs::metadata(&stdout_file)?;
@@ -610,7 +642,7 @@ fn status_daemon(id: &str) -> Result<()> {
610 } else { 642 } else {
611 println!("Stdout file: {} (not found)", stdout_file); 643 println!("Stdout file: {} (not found)", stdout_file);
612 } 644 }
613 645
614 if Path::new(&stderr_file).exists() { 646 if Path::new(&stderr_file).exists() {
615 let metadata = std::fs::metadata(&stderr_file)?; 647 let metadata = std::fs::metadata(&stderr_file)?;
616 println!("Stderr file: {} ({} bytes)", stderr_file, metadata.len()); 648 println!("Stderr file: {} ({} bytes)", stderr_file, metadata.len());
@@ -631,21 +663,21 @@ fn status_daemon(id: &str) -> Result<()> {
631 println!("Status: ERROR (cannot read PID file: {})", e); 663 println!("Status: ERROR (cannot read PID file: {})", e);
632 } 664 }
633 } 665 }
634 666
635 Ok(()) 667 Ok(())
636} 668}
637 669
638fn clean_orphaned_files() -> Result<()> { 670fn clean_orphaned_files() -> Result<()> {
639 tracing::info!("Scanning for orphaned daemon files..."); 671 tracing::info!("Scanning for orphaned daemon files...");
640 672
641 let mut cleaned_count = 0; 673 let mut cleaned_count = 0;
642 674
643 // Find all .pid files in current directory 675 // Find all .pid files in current directory
644 for entry in find_pid_files()? { 676 for entry in find_pid_files()? {
645 let path = entry.path(); 677 let path = entry.path();
646 let path_str = path.to_string_lossy(); 678 let path_str = path.to_string_lossy();
647 let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); 679 let id = path_str.strip_suffix(".pid").unwrap_or(&path_str);
648 680
649 // Read PID from file 681 // Read PID from file
650 match std::fs::read_to_string(&path) { 682 match std::fs::read_to_string(&path) {
651 Ok(contents) => { 683 Ok(contents) => {
@@ -655,14 +687,14 @@ fn clean_orphaned_files() -> Result<()> {
655 // Check if process is still running 687 // Check if process is still running
656 if !is_process_running_by_pid(pid) { 688 if !is_process_running_by_pid(pid) {
657 println!("Cleaning up orphaned files for '{}' (PID: {})", id, pid); 689 println!("Cleaning up orphaned files for '{}' (PID: {})", id, pid);
658 690
659 // Remove PID file 691 // Remove PID file
660 if let Err(e) = std::fs::remove_file(&path) { 692 if let Err(e) = std::fs::remove_file(&path) {
661 tracing::warn!("Failed to remove {}: {}", path_str, e); 693 tracing::warn!("Failed to remove {}: {}", path_str, e);
662 } else { 694 } else {
663 tracing::info!("Removed {}", path_str); 695 tracing::info!("Removed {}", path_str);
664 } 696 }
665 697
666 // Remove stdout file if it exists 698 // Remove stdout file if it exists
667 let stdout_file = format!("{}.stdout", id); 699 let stdout_file = format!("{}.stdout", id);
668 if Path::new(&stdout_file).exists() { 700 if Path::new(&stdout_file).exists() {
@@ -672,7 +704,7 @@ fn clean_orphaned_files() -> Result<()> {
672 tracing::info!("Removed {}", stdout_file); 704 tracing::info!("Removed {}", stdout_file);
673 } 705 }
674 } 706 }
675 707
676 // Remove stderr file if it exists 708 // Remove stderr file if it exists
677 let stderr_file = format!("{}.stderr", id); 709 let stderr_file = format!("{}.stderr", id);
678 if Path::new(&stderr_file).exists() { 710 if Path::new(&stderr_file).exists() {
@@ -682,10 +714,14 @@ fn clean_orphaned_files() -> Result<()> {
682 tracing::info!("Removed {}", stderr_file); 714 tracing::info!("Removed {}", stderr_file);
683 } 715 }
684 } 716 }
685 717
686 cleaned_count += 1; 718 cleaned_count += 1;
687 } else { 719 } else {
688 tracing::info!("Skipping '{}' (PID: {}) - process is still running", id, pid); 720 tracing::info!(
721 "Skipping '{}' (PID: {}) - process is still running",
722 id,
723 pid
724 );
689 } 725 }
690 } 726 }
691 Err(_) => { 727 Err(_) => {
@@ -710,18 +746,19 @@ fn clean_orphaned_files() -> Result<()> {
710 } 746 }
711 } 747 }
712 } 748 }
713 749
714 if cleaned_count == 0 { 750 if cleaned_count == 0 {
715 println!("No orphaned files found."); 751 println!("No orphaned files found.");
716 } else { 752 } else {
717 println!("Cleaned up {} orphaned daemon(s).", cleaned_count); 753 println!("Cleaned up {} orphaned daemon(s).", cleaned_count);
718 } 754 }
719 755
720 Ok(()) 756 Ok(())
721} 757}
722 758
723fn print_llm_guide() { 759fn print_llm_guide() {
724 println!(r#"# Demon - Daemon Process Management CLI 760 println!(
761 r#"# Demon - Daemon Process Management CLI
725 762
726## Overview 763## Overview
727Demon is a command-line tool for spawning, managing, and monitoring background processes (daemons) on Linux systems. It redirects process stdout/stderr to files and provides commands to control and observe these processes. 764Demon is a command-line tool for spawning, managing, and monitoring background processes (daemons) on Linux systems. It redirects process stdout/stderr to files and provides commands to control and observe these processes.
@@ -948,7 +985,8 @@ demon list --quiet > process_status.txt
948- Use standard Unix signals for process control 985- Use standard Unix signals for process control
949- Log rotation should be handled by the application itself 986- Log rotation should be handled by the application itself
950 987
951This tool is designed for Linux environments and provides a simple interface for managing background processes with persistent logging."#); 988This tool is designed for Linux environments and provides a simple interface for managing background processes with persistent logging."#
989 );
952} 990}
953 991
954fn find_pid_files() -> Result<Vec<std::fs::DirEntry>> { 992fn find_pid_files() -> Result<Vec<std::fs::DirEntry>> {
diff --git a/tests/cli.rs b/tests/cli.rs
index 37398e6..e99e876 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -32,7 +32,7 @@ fn test_version_output() {
32#[test] 32#[test]
33fn test_run_missing_command() { 33fn test_run_missing_command() {
34 let temp_dir = TempDir::new().unwrap(); 34 let temp_dir = TempDir::new().unwrap();
35 35
36 let mut cmd = Command::cargo_bin("demon").unwrap(); 36 let mut cmd = Command::cargo_bin("demon").unwrap();
37 cmd.current_dir(temp_dir.path()) 37 cmd.current_dir(temp_dir.path())
38 .args(&["run", "--id", "test"]) 38 .args(&["run", "--id", "test"])
@@ -44,19 +44,19 @@ fn test_run_missing_command() {
44#[test] 44#[test]
45fn test_run_creates_files() { 45fn test_run_creates_files() {
46 let temp_dir = TempDir::new().unwrap(); 46 let temp_dir = TempDir::new().unwrap();
47 47
48 let mut cmd = Command::cargo_bin("demon").unwrap(); 48 let mut cmd = Command::cargo_bin("demon").unwrap();
49 cmd.current_dir(temp_dir.path()) 49 cmd.current_dir(temp_dir.path())
50 .args(&["run", "--id", "test", "echo", "hello"]) 50 .args(&["run", "--id", "test", "echo", "hello"])
51 .assert() 51 .assert()
52 .success() 52 .success()
53 .stdout(predicate::str::contains("Started daemon 'test'")); 53 .stdout(predicate::str::contains("Started daemon 'test'"));
54 54
55 // Verify files were created 55 // Verify files were created
56 assert!(temp_dir.path().join("test.pid").exists()); 56 assert!(temp_dir.path().join("test.pid").exists());
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 // Check that stdout contains our output 60 // Check that stdout contains our output
61 let stdout_content = fs::read_to_string(temp_dir.path().join("test.stdout")).unwrap(); 61 let stdout_content = fs::read_to_string(temp_dir.path().join("test.stdout")).unwrap();
62 assert_eq!(stdout_content.trim(), "hello"); 62 assert_eq!(stdout_content.trim(), "hello");
@@ -65,14 +65,14 @@ fn test_run_creates_files() {
65#[test] 65#[test]
66fn test_run_duplicate_process() { 66fn test_run_duplicate_process() {
67 let temp_dir = TempDir::new().unwrap(); 67 let temp_dir = TempDir::new().unwrap();
68 68
69 // Start a long-running process 69 // Start a long-running process
70 let mut cmd = Command::cargo_bin("demon").unwrap(); 70 let mut cmd = Command::cargo_bin("demon").unwrap();
71 cmd.current_dir(temp_dir.path()) 71 cmd.current_dir(temp_dir.path())
72 .args(&["run", "--id", "long", "sleep", "30"]) 72 .args(&["run", "--id", "long", "sleep", "30"])
73 .assert() 73 .assert()
74 .success(); 74 .success();
75 75
76 // Try to start another with the same ID 76 // Try to start another with the same ID
77 let mut cmd = Command::cargo_bin("demon").unwrap(); 77 let mut cmd = Command::cargo_bin("demon").unwrap();
78 cmd.current_dir(temp_dir.path()) 78 cmd.current_dir(temp_dir.path())
@@ -80,7 +80,7 @@ fn test_run_duplicate_process() {
80 .assert() 80 .assert()
81 .failure() 81 .failure()
82 .stderr(predicate::str::contains("already running")); 82 .stderr(predicate::str::contains("already running"));
83 83
84 // Clean up the running process 84 // Clean up the running process
85 let mut cmd = Command::cargo_bin("demon").unwrap(); 85 let mut cmd = Command::cargo_bin("demon").unwrap();
86 cmd.current_dir(temp_dir.path()) 86 cmd.current_dir(temp_dir.path())
@@ -92,7 +92,7 @@ fn test_run_duplicate_process() {
92#[test] 92#[test]
93fn test_list_empty() { 93fn test_list_empty() {
94 let temp_dir = TempDir::new().unwrap(); 94 let temp_dir = TempDir::new().unwrap();
95 95
96 let mut cmd = Command::cargo_bin("demon").unwrap(); 96 let mut cmd = Command::cargo_bin("demon").unwrap();
97 cmd.current_dir(temp_dir.path()) 97 cmd.current_dir(temp_dir.path())
98 .args(&["list"]) 98 .args(&["list"])
@@ -107,14 +107,14 @@ fn test_list_empty() {
107#[test] 107#[test]
108fn test_list_with_processes() { 108fn test_list_with_processes() {
109 let temp_dir = TempDir::new().unwrap(); 109 let temp_dir = TempDir::new().unwrap();
110 110
111 // Start a process 111 // Start a process
112 let mut cmd = Command::cargo_bin("demon").unwrap(); 112 let mut cmd = Command::cargo_bin("demon").unwrap();
113 cmd.current_dir(temp_dir.path()) 113 cmd.current_dir(temp_dir.path())
114 .args(&["run", "--id", "test", "echo", "done"]) 114 .args(&["run", "--id", "test", "echo", "done"])
115 .assert() 115 .assert()
116 .success(); 116 .success();
117 117
118 // List processes 118 // List processes
119 let mut cmd = Command::cargo_bin("demon").unwrap(); 119 let mut cmd = Command::cargo_bin("demon").unwrap();
120 cmd.current_dir(temp_dir.path()) 120 cmd.current_dir(temp_dir.path())
@@ -128,17 +128,22 @@ fn test_list_with_processes() {
128#[test] 128#[test]
129fn test_cat_output() { 129fn test_cat_output() {
130 let temp_dir = TempDir::new().unwrap(); 130 let temp_dir = TempDir::new().unwrap();
131 131
132 // Create a process with output 132 // Create a process with output
133 let mut cmd = Command::cargo_bin("demon").unwrap(); 133 let mut cmd = Command::cargo_bin("demon").unwrap();
134 cmd.current_dir(temp_dir.path()) 134 cmd.current_dir(temp_dir.path())
135 .args(&[ 135 .args(&[
136 "run", "--id", "test", "--", 136 "run",
137 "sh", "-c", "echo 'stdout line'; echo 'stderr line' >&2" 137 "--id",
138 "test",
139 "--",
140 "sh",
141 "-c",
142 "echo 'stdout line'; echo 'stderr line' >&2",
138 ]) 143 ])
139 .assert() 144 .assert()
140 .success(); 145 .success();
141 146
142 // Cat the output 147 // Cat the output
143 let mut cmd = Command::cargo_bin("demon").unwrap(); 148 let mut cmd = Command::cargo_bin("demon").unwrap();
144 cmd.current_dir(temp_dir.path()) 149 cmd.current_dir(temp_dir.path())
@@ -152,17 +157,22 @@ fn test_cat_output() {
152#[test] 157#[test]
153fn test_cat_stdout_only() { 158fn test_cat_stdout_only() {
154 let temp_dir = TempDir::new().unwrap(); 159 let temp_dir = TempDir::new().unwrap();
155 160
156 // Create a process with output 161 // Create a process with output
157 let mut cmd = Command::cargo_bin("demon").unwrap(); 162 let mut cmd = Command::cargo_bin("demon").unwrap();
158 cmd.current_dir(temp_dir.path()) 163 cmd.current_dir(temp_dir.path())
159 .args(&[ 164 .args(&[
160 "run", "--id", "test", "--", 165 "run",
161 "sh", "-c", "echo 'stdout line'; echo 'stderr line' >&2" 166 "--id",
167 "test",
168 "--",
169 "sh",
170 "-c",
171 "echo 'stdout line'; echo 'stderr line' >&2",
162 ]) 172 ])
163 .assert() 173 .assert()
164 .success(); 174 .success();
165 175
166 // Cat only stdout 176 // Cat only stdout
167 let mut cmd = Command::cargo_bin("demon").unwrap(); 177 let mut cmd = Command::cargo_bin("demon").unwrap();
168 cmd.current_dir(temp_dir.path()) 178 cmd.current_dir(temp_dir.path())
@@ -176,7 +186,7 @@ fn test_cat_stdout_only() {
176#[test] 186#[test]
177fn test_status_nonexistent() { 187fn test_status_nonexistent() {
178 let temp_dir = TempDir::new().unwrap(); 188 let temp_dir = TempDir::new().unwrap();
179 189
180 let mut cmd = Command::cargo_bin("demon").unwrap(); 190 let mut cmd = Command::cargo_bin("demon").unwrap();
181 cmd.current_dir(temp_dir.path()) 191 cmd.current_dir(temp_dir.path())
182 .args(&["status", "--id", "nonexistent"]) 192 .args(&["status", "--id", "nonexistent"])
@@ -188,14 +198,14 @@ fn test_status_nonexistent() {
188#[test] 198#[test]
189fn test_status_dead_process() { 199fn test_status_dead_process() {
190 let temp_dir = TempDir::new().unwrap(); 200 let temp_dir = TempDir::new().unwrap();
191 201
192 // Create a short-lived process 202 // Create a short-lived process
193 let mut cmd = Command::cargo_bin("demon").unwrap(); 203 let mut cmd = Command::cargo_bin("demon").unwrap();
194 cmd.current_dir(temp_dir.path()) 204 cmd.current_dir(temp_dir.path())
195 .args(&["run", "--id", "dead", "echo", "hello"]) 205 .args(&["run", "--id", "dead", "echo", "hello"])
196 .assert() 206 .assert()
197 .success(); 207 .success();
198 208
199 // Check its status (should be dead) 209 // Check its status (should be dead)
200 let mut cmd = Command::cargo_bin("demon").unwrap(); 210 let mut cmd = Command::cargo_bin("demon").unwrap();
201 cmd.current_dir(temp_dir.path()) 211 cmd.current_dir(temp_dir.path())
@@ -208,7 +218,7 @@ fn test_status_dead_process() {
208#[test] 218#[test]
209fn test_stop_nonexistent() { 219fn test_stop_nonexistent() {
210 let temp_dir = TempDir::new().unwrap(); 220 let temp_dir = TempDir::new().unwrap();
211 221
212 let mut cmd = Command::cargo_bin("demon").unwrap(); 222 let mut cmd = Command::cargo_bin("demon").unwrap();
213 cmd.current_dir(temp_dir.path()) 223 cmd.current_dir(temp_dir.path())
214 .args(&["stop", "--id", "nonexistent"]) 224 .args(&["stop", "--id", "nonexistent"])
@@ -220,14 +230,14 @@ fn test_stop_nonexistent() {
220#[test] 230#[test]
221fn test_stop_process() { 231fn test_stop_process() {
222 let temp_dir = TempDir::new().unwrap(); 232 let temp_dir = TempDir::new().unwrap();
223 233
224 // Start a long-running process 234 // Start a long-running process
225 let mut cmd = Command::cargo_bin("demon").unwrap(); 235 let mut cmd = Command::cargo_bin("demon").unwrap();
226 cmd.current_dir(temp_dir.path()) 236 cmd.current_dir(temp_dir.path())
227 .args(&["run", "--id", "long", "sleep", "10"]) 237 .args(&["run", "--id", "long", "sleep", "10"])
228 .assert() 238 .assert()
229 .success(); 239 .success();
230 240
231 // Stop it 241 // Stop it
232 let mut cmd = Command::cargo_bin("demon").unwrap(); 242 let mut cmd = Command::cargo_bin("demon").unwrap();
233 cmd.current_dir(temp_dir.path()) 243 cmd.current_dir(temp_dir.path())
@@ -235,7 +245,7 @@ fn test_stop_process() {
235 .assert() 245 .assert()
236 .success() 246 .success()
237 .stdout(predicate::str::contains("terminated gracefully")); 247 .stdout(predicate::str::contains("terminated gracefully"));
238 248
239 // Verify PID file is gone 249 // Verify PID file is gone
240 assert!(!temp_dir.path().join("long.pid").exists()); 250 assert!(!temp_dir.path().join("long.pid").exists());
241} 251}
@@ -243,7 +253,7 @@ fn test_stop_process() {
243#[test] 253#[test]
244fn test_clean_no_orphans() { 254fn test_clean_no_orphans() {
245 let temp_dir = TempDir::new().unwrap(); 255 let temp_dir = TempDir::new().unwrap();
246 256
247 let mut cmd = Command::cargo_bin("demon").unwrap(); 257 let mut cmd = Command::cargo_bin("demon").unwrap();
248 cmd.current_dir(temp_dir.path()) 258 cmd.current_dir(temp_dir.path())
249 .args(&["clean"]) 259 .args(&["clean"])
@@ -255,14 +265,14 @@ fn test_clean_no_orphans() {
255#[test] 265#[test]
256fn test_clean_with_orphans() { 266fn test_clean_with_orphans() {
257 let temp_dir = TempDir::new().unwrap(); 267 let temp_dir = TempDir::new().unwrap();
258 268
259 // Create a dead process 269 // Create a dead process
260 let mut cmd = Command::cargo_bin("demon").unwrap(); 270 let mut cmd = Command::cargo_bin("demon").unwrap();
261 cmd.current_dir(temp_dir.path()) 271 cmd.current_dir(temp_dir.path())
262 .args(&["run", "--id", "dead", "echo", "hello"]) 272 .args(&["run", "--id", "dead", "echo", "hello"])
263 .assert() 273 .assert()
264 .success(); 274 .success();
265 275
266 // Clean up orphaned files 276 // Clean up orphaned files
267 let mut cmd = Command::cargo_bin("demon").unwrap(); 277 let mut cmd = Command::cargo_bin("demon").unwrap();
268 cmd.current_dir(temp_dir.path()) 278 cmd.current_dir(temp_dir.path())
@@ -271,7 +281,7 @@ fn test_clean_with_orphans() {
271 .success() 281 .success()
272 .stdout(predicate::str::contains("Cleaned up")) 282 .stdout(predicate::str::contains("Cleaned up"))
273 .stdout(predicate::str::contains("orphaned")); 283 .stdout(predicate::str::contains("orphaned"));
274 284
275 // Verify files are gone 285 // Verify files are gone
276 assert!(!temp_dir.path().join("dead.pid").exists()); 286 assert!(!temp_dir.path().join("dead.pid").exists());
277 assert!(!temp_dir.path().join("dead.stdout").exists()); 287 assert!(!temp_dir.path().join("dead.stdout").exists());
@@ -281,19 +291,24 @@ fn test_clean_with_orphans() {
281#[test] 291#[test]
282fn test_run_with_complex_command() { 292fn test_run_with_complex_command() {
283 let temp_dir = TempDir::new().unwrap(); 293 let temp_dir = TempDir::new().unwrap();
284 294
285 let mut cmd = Command::cargo_bin("demon").unwrap(); 295 let mut cmd = Command::cargo_bin("demon").unwrap();
286 cmd.current_dir(temp_dir.path()) 296 cmd.current_dir(temp_dir.path())
287 .args(&[ 297 .args(&[
288 "run", "--id", "complex", "--", 298 "run",
289 "sh", "-c", "for i in 1 2 3; do echo \"line $i\"; done" 299 "--id",
300 "complex",
301 "--",
302 "sh",
303 "-c",
304 "for i in 1 2 3; do echo \"line $i\"; done",
290 ]) 305 ])
291 .assert() 306 .assert()
292 .success(); 307 .success();
293 308
294 // Give the process a moment to complete 309 // Give the process a moment to complete
295 std::thread::sleep(Duration::from_millis(100)); 310 std::thread::sleep(Duration::from_millis(100));
296 311
297 // Check the output contains all lines 312 // Check the output contains all lines
298 let stdout_content = fs::read_to_string(temp_dir.path().join("complex.stdout")).unwrap(); 313 let stdout_content = fs::read_to_string(temp_dir.path().join("complex.stdout")).unwrap();
299 assert!(stdout_content.contains("line 1")); 314 assert!(stdout_content.contains("line 1"));
@@ -304,14 +319,14 @@ fn test_run_with_complex_command() {
304#[test] 319#[test]
305fn test_timeout_configuration() { 320fn test_timeout_configuration() {
306 let temp_dir = TempDir::new().unwrap(); 321 let temp_dir = TempDir::new().unwrap();
307 322
308 // Start a process 323 // Start a process
309 let mut cmd = Command::cargo_bin("demon").unwrap(); 324 let mut cmd = Command::cargo_bin("demon").unwrap();
310 cmd.current_dir(temp_dir.path()) 325 cmd.current_dir(temp_dir.path())
311 .args(&["run", "--id", "timeout-test", "sleep", "5"]) 326 .args(&["run", "--id", "timeout-test", "sleep", "5"])
312 .assert() 327 .assert()
313 .success(); 328 .success();
314 329
315 // Stop with custom timeout (should work normally since sleep responds to SIGTERM) 330 // Stop with custom timeout (should work normally since sleep responds to SIGTERM)
316 let mut cmd = Command::cargo_bin("demon").unwrap(); 331 let mut cmd = Command::cargo_bin("demon").unwrap();
317 cmd.current_dir(temp_dir.path()) 332 cmd.current_dir(temp_dir.path())
@@ -324,10 +339,10 @@ fn test_timeout_configuration() {
324#[test] 339#[test]
325fn test_invalid_process_id() { 340fn test_invalid_process_id() {
326 let temp_dir = TempDir::new().unwrap(); 341 let temp_dir = TempDir::new().unwrap();
327 342
328 // Create an invalid PID file 343 // Create an invalid PID file
329 fs::write(temp_dir.path().join("invalid.pid"), "not-a-number").unwrap(); 344 fs::write(temp_dir.path().join("invalid.pid"), "not-a-number").unwrap();
330 345
331 // Status should handle it gracefully 346 // Status should handle it gracefully
332 let mut cmd = Command::cargo_bin("demon").unwrap(); 347 let mut cmd = Command::cargo_bin("demon").unwrap();
333 cmd.current_dir(temp_dir.path()) 348 cmd.current_dir(temp_dir.path())
@@ -335,7 +350,7 @@ fn test_invalid_process_id() {
335 .assert() 350 .assert()
336 .success() 351 .success()
337 .stdout(predicate::str::contains("ERROR")); 352 .stdout(predicate::str::contains("ERROR"));
338 353
339 // Clean should remove it 354 // Clean should remove it
340 let mut cmd = Command::cargo_bin("demon").unwrap(); 355 let mut cmd = Command::cargo_bin("demon").unwrap();
341 cmd.current_dir(temp_dir.path()) 356 cmd.current_dir(temp_dir.path())
@@ -348,7 +363,7 @@ fn test_invalid_process_id() {
348#[test] 363#[test]
349fn test_list_quiet_mode() { 364fn test_list_quiet_mode() {
350 let temp_dir = TempDir::new().unwrap(); 365 let temp_dir = TempDir::new().unwrap();
351 366
352 // Test quiet mode with no processes 367 // Test quiet mode with no processes
353 let mut cmd = Command::cargo_bin("demon").unwrap(); 368 let mut cmd = Command::cargo_bin("demon").unwrap();
354 cmd.current_dir(temp_dir.path()) 369 cmd.current_dir(temp_dir.path())
@@ -356,14 +371,14 @@ fn test_list_quiet_mode() {
356 .assert() 371 .assert()
357 .success() 372 .success()
358 .stdout(predicate::str::is_empty()); 373 .stdout(predicate::str::is_empty());
359 374
360 // Create a process 375 // Create a process
361 let mut cmd = Command::cargo_bin("demon").unwrap(); 376 let mut cmd = Command::cargo_bin("demon").unwrap();
362 cmd.current_dir(temp_dir.path()) 377 cmd.current_dir(temp_dir.path())
363 .args(&["run", "--id", "quiet-test", "echo", "done"]) 378 .args(&["run", "--id", "quiet-test", "echo", "done"])
364 .assert() 379 .assert()
365 .success(); 380 .success();
366 381
367 // Test quiet mode with process - should output colon-separated format 382 // Test quiet mode with process - should output colon-separated format
368 let mut cmd = Command::cargo_bin("demon").unwrap(); 383 let mut cmd = Command::cargo_bin("demon").unwrap();
369 cmd.current_dir(temp_dir.path()) 384 cmd.current_dir(temp_dir.path())
@@ -384,7 +399,9 @@ fn test_llm_command() {
384 cmd.args(&["llm"]) 399 cmd.args(&["llm"])
385 .assert() 400 .assert()
386 .success() 401 .success()
387 .stdout(predicate::str::contains("# Demon - Daemon Process Management CLI")) 402 .stdout(predicate::str::contains(
403 "# Demon - Daemon Process Management CLI",
404 ))
388 .stdout(predicate::str::contains("## Available Commands")) 405 .stdout(predicate::str::contains("## Available Commands"))
389 .stdout(predicate::str::contains("demon run")) 406 .stdout(predicate::str::contains("demon run"))
390 .stdout(predicate::str::contains("demon stop")) 407 .stdout(predicate::str::contains("demon stop"))
@@ -396,4 +413,4 @@ fn test_llm_command() {
396 .stdout(predicate::str::contains("Common Workflows")) 413 .stdout(predicate::str::contains("Common Workflows"))
397 .stdout(predicate::str::contains("Best Practices")) 414 .stdout(predicate::str::contains("Best Practices"))
398 .stdout(predicate::str::contains("Integration Tips")); 415 .stdout(predicate::str::contains("Integration Tips"));
399} \ No newline at end of file 416}