aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-06-19 17:11:09 +0100
committerdiogo464 <[email protected]>2025-06-19 17:11:09 +0100
commita365f12a6c6e7775a5bd3c6177050b74826c608c (patch)
treea940d89c741250aedacb4dc8d8f9e92ce9810fc4 /src
parent9c3d24b08649ebf6d4a3614f3506ce2702aafd74 (diff)
Add configurable root directory for daemon files with git root discovery
Implement --root-dir global option to specify where daemon files (PID, stdout, stderr) are created. When not specified, automatically searches upward for git repository root. This addresses the issue of daemon files being scattered across various working directories. Key features: - Global --root-dir option available on all commands - Automatic git root discovery when --root-dir not specified - Proper error handling for invalid directories - Updated all file operations to use configurable root directory - Optimized HashMap usage with PathBuf for better performance - Fixed ID extraction to show daemon names instead of full paths - Updated tests to work with new directory structure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/main.rs317
1 files changed, 210 insertions, 107 deletions
diff --git a/src/main.rs b/src/main.rs
index 89a32b4..05208ea 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,7 +3,7 @@ use clap::{Args, Parser, Subcommand};
3use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; 3use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
4use std::fs::File; 4use std::fs::File;
5use std::io::{Read, Seek, SeekFrom, Write}; 5use std::io::{Read, Seek, SeekFrom, Write};
6use std::path::Path; 6use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio}; 7use std::process::{Command, Stdio};
8use std::sync::mpsc::channel; 8use std::sync::mpsc::channel;
9use std::thread; 9use std::thread;
@@ -117,6 +117,13 @@ struct Cli {
117 command: Commands, 117 command: Commands,
118} 118}
119 119
120#[derive(Args)]
121struct Global {
122 /// Root directory for daemon files (pid, logs). If not specified, searches for git root.
123 #[arg(long, global = true)]
124 root_dir: Option<PathBuf>,
125}
126
120#[derive(Subcommand)] 127#[derive(Subcommand)]
121enum Commands { 128enum Commands {
122 /// Spawn a background process and redirect stdout/stderr to files 129 /// Spawn a background process and redirect stdout/stderr to files
@@ -138,7 +145,7 @@ enum Commands {
138 Status(StatusArgs), 145 Status(StatusArgs),
139 146
140 /// Clean up orphaned pid and log files 147 /// Clean up orphaned pid and log files
141 Clean, 148 Clean(CleanArgs),
142 149
143 /// Output comprehensive usage guide for LLMs 150 /// Output comprehensive usage guide for LLMs
144 Llm, 151 Llm,
@@ -149,6 +156,9 @@ enum Commands {
149 156
150#[derive(Args)] 157#[derive(Args)]
151struct RunArgs { 158struct RunArgs {
159 #[clap(flatten)]
160 global: Global,
161
152 /// Process identifier 162 /// Process identifier
153 id: String, 163 id: String,
154 164
@@ -158,6 +168,9 @@ struct RunArgs {
158 168
159#[derive(Args)] 169#[derive(Args)]
160struct StopArgs { 170struct StopArgs {
171 #[clap(flatten)]
172 global: Global,
173
161 /// Process identifier 174 /// Process identifier
162 id: String, 175 id: String,
163 176
@@ -168,6 +181,9 @@ struct StopArgs {
168 181
169#[derive(Args)] 182#[derive(Args)]
170struct TailArgs { 183struct TailArgs {
184 #[clap(flatten)]
185 global: Global,
186
171 /// Process identifier 187 /// Process identifier
172 id: String, 188 id: String,
173 189
@@ -190,6 +206,9 @@ struct TailArgs {
190 206
191#[derive(Args)] 207#[derive(Args)]
192struct CatArgs { 208struct CatArgs {
209 #[clap(flatten)]
210 global: Global,
211
193 /// Process identifier 212 /// Process identifier
194 id: String, 213 id: String,
195 214
@@ -204,6 +223,9 @@ struct CatArgs {
204 223
205#[derive(Args)] 224#[derive(Args)]
206struct ListArgs { 225struct ListArgs {
226 #[clap(flatten)]
227 global: Global,
228
207 /// Quiet mode - output only process data without headers 229 /// Quiet mode - output only process data without headers
208 #[arg(short, long)] 230 #[arg(short, long)]
209 quiet: bool, 231 quiet: bool,
@@ -211,12 +233,24 @@ struct ListArgs {
211 233
212#[derive(Args)] 234#[derive(Args)]
213struct StatusArgs { 235struct StatusArgs {
236 #[clap(flatten)]
237 global: Global,
238
214 /// Process identifier 239 /// Process identifier
215 id: String, 240 id: String,
216} 241}
217 242
218#[derive(Args)] 243#[derive(Args)]
244struct CleanArgs {
245 #[clap(flatten)]
246 global: Global,
247}
248
249#[derive(Args)]
219struct WaitArgs { 250struct WaitArgs {
251 #[clap(flatten)]
252 global: Global,
253
220 /// Process identifier 254 /// Process identifier
221 id: String, 255 id: String,
222 256
@@ -248,34 +282,89 @@ fn run_command(command: Commands) -> Result<()> {
248 if args.command.is_empty() { 282 if args.command.is_empty() {
249 return Err(anyhow::anyhow!("Command cannot be empty")); 283 return Err(anyhow::anyhow!("Command cannot be empty"));
250 } 284 }
251 run_daemon(&args.id, &args.command) 285 let root_dir = resolve_root_dir(&args.global)?;
286 run_daemon(&args.id, &args.command, &root_dir)
287 }
288 Commands::Stop(args) => {
289 let root_dir = resolve_root_dir(&args.global)?;
290 stop_daemon(&args.id, args.timeout, &root_dir)
252 } 291 }
253 Commands::Stop(args) => stop_daemon(&args.id, args.timeout),
254 Commands::Tail(args) => { 292 Commands::Tail(args) => {
255 let show_stdout = !args.stderr || args.stdout; 293 let show_stdout = !args.stderr || args.stdout;
256 let show_stderr = !args.stdout || args.stderr; 294 let show_stderr = !args.stdout || args.stderr;
257 tail_logs(&args.id, show_stdout, show_stderr, args.follow, args.lines) 295 let root_dir = resolve_root_dir(&args.global)?;
296 tail_logs(&args.id, show_stdout, show_stderr, args.follow, args.lines, &root_dir)
258 } 297 }
259 Commands::Cat(args) => { 298 Commands::Cat(args) => {
260 let show_stdout = !args.stderr || args.stdout; 299 let show_stdout = !args.stderr || args.stdout;
261 let show_stderr = !args.stdout || args.stderr; 300 let show_stderr = !args.stdout || args.stderr;
262 cat_logs(&args.id, show_stdout, show_stderr) 301 let root_dir = resolve_root_dir(&args.global)?;
302 cat_logs(&args.id, show_stdout, show_stderr, &root_dir)
303 }
304 Commands::List(args) => {
305 let root_dir = resolve_root_dir(&args.global)?;
306 list_daemons(args.quiet, &root_dir)
307 }
308 Commands::Status(args) => {
309 let root_dir = resolve_root_dir(&args.global)?;
310 status_daemon(&args.id, &root_dir)
311 }
312 Commands::Clean(args) => {
313 let root_dir = resolve_root_dir(&args.global)?;
314 clean_orphaned_files(&root_dir)
263 } 315 }
264 Commands::List(args) => list_daemons(args.quiet),
265 Commands::Status(args) => status_daemon(&args.id),
266 Commands::Clean => clean_orphaned_files(),
267 Commands::Llm => { 316 Commands::Llm => {
268 print_llm_guide(); 317 print_llm_guide();
269 Ok(()) 318 Ok(())
270 } 319 }
271 Commands::Wait(args) => wait_daemon(&args.id, args.timeout, args.interval), 320 Commands::Wait(args) => {
321 let root_dir = resolve_root_dir(&args.global)?;
322 wait_daemon(&args.id, args.timeout, args.interval, &root_dir)
323 }
272 } 324 }
273} 325}
274 326
275fn run_daemon(id: &str, command: &[String]) -> Result<()> { 327fn find_git_root() -> Result<PathBuf> {
276 let pid_file = format!("{}.pid", id); 328 let mut current = std::env::current_dir()?;
277 let stdout_file = format!("{}.stdout", id); 329
278 let stderr_file = format!("{}.stderr", id); 330 loop {
331 let git_path = current.join(".git");
332 if git_path.exists() {
333 return Ok(current);
334 }
335
336 match current.parent() {
337 Some(parent) => current = parent.to_path_buf(),
338 None => return Err(anyhow::anyhow!(
339 "No git repository found. Please specify --root-dir or run from within a git repository"
340 )),
341 }
342 }
343}
344
345fn resolve_root_dir(global: &Global) -> Result<PathBuf> {
346 match &global.root_dir {
347 Some(dir) => {
348 if !dir.exists() {
349 return Err(anyhow::anyhow!("Specified root directory does not exist: {}", dir.display()));
350 }
351 if !dir.is_dir() {
352 return Err(anyhow::anyhow!("Specified root path is not a directory: {}", dir.display()));
353 }
354 Ok(dir.clone())
355 },
356 None => find_git_root(),
357 }
358}
359
360fn build_file_path(root_dir: &Path, id: &str, extension: &str) -> PathBuf {
361 root_dir.join(format!("{}.{}", id, extension))
362}
363
364fn run_daemon(id: &str, command: &[String], root_dir: &Path) -> Result<()> {
365 let pid_file = build_file_path(root_dir, id, "pid");
366 let stdout_file = build_file_path(root_dir, id, "stdout");
367 let stderr_file = build_file_path(root_dir, id, "stderr");
279 368
280 // Check if process is already running 369 // Check if process is already running
281 if is_process_running(&pid_file)? { 370 if is_process_running(&pid_file)? {
@@ -315,12 +404,12 @@ fn run_daemon(id: &str, command: &[String]) -> Result<()> {
315 // Don't wait for the child - let it run detached 404 // Don't wait for the child - let it run detached
316 std::mem::forget(child); 405 std::mem::forget(child);
317 406
318 println!("Started daemon '{}' with PID written to {}", id, pid_file); 407 println!("Started daemon '{}' with PID written to {}", id, pid_file.display());
319 408
320 Ok(()) 409 Ok(())
321} 410}
322 411
323fn is_process_running(pid_file: &str) -> Result<bool> { 412fn is_process_running<P: AsRef<Path>>(pid_file: P) -> Result<bool> {
324 let pid_file_data = match PidFile::read_from_file(pid_file) { 413 let pid_file_data = match PidFile::read_from_file(pid_file) {
325 Ok(data) => data, 414 Ok(data) => data,
326 Err(PidFileReadError::FileNotFound) => return Ok(false), // No PID file means no running process 415 Err(PidFileReadError::FileNotFound) => return Ok(false), // No PID file means no running process
@@ -336,8 +425,8 @@ fn is_process_running(pid_file: &str) -> Result<bool> {
336 Ok(output.status.success()) 425 Ok(output.status.success())
337} 426}
338 427
339fn stop_daemon(id: &str, timeout: u64) -> Result<()> { 428fn stop_daemon(id: &str, timeout: u64, root_dir: &Path) -> Result<()> {
340 let pid_file = format!("{}.pid", id); 429 let pid_file = build_file_path(root_dir, id, "pid");
341 430
342 // Check if PID file exists and read PID data 431 // Check if PID file exists and read PID data
343 let pid_file_data = match PidFile::read_from_file(&pid_file) { 432 let pid_file_data = match PidFile::read_from_file(&pid_file) {
@@ -441,9 +530,9 @@ fn is_process_running_by_pid(pid: u32) -> bool {
441 } 530 }
442} 531}
443 532
444fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { 533fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool, root_dir: &Path) -> Result<()> {
445 let stdout_file = format!("{}.stdout", id); 534 let stdout_file = build_file_path(root_dir, id, "stdout");
446 let stderr_file = format!("{}.stderr", id); 535 let stderr_file = build_file_path(root_dir, id, "stderr");
447 536
448 let mut files_found = false; 537 let mut files_found = false;
449 538
@@ -452,12 +541,12 @@ fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> {
452 if !contents.is_empty() { 541 if !contents.is_empty() {
453 files_found = true; 542 files_found = true;
454 if show_stderr { 543 if show_stderr {
455 println!("==> {} <==", stdout_file); 544 println!("==> {} <==", stdout_file.display());
456 } 545 }
457 print!("{}", contents); 546 print!("{}", contents);
458 } 547 }
459 } else { 548 } else {
460 tracing::warn!("Could not read {}", stdout_file); 549 tracing::warn!("Could not read {}", stdout_file.display());
461 } 550 }
462 } 551 }
463 552
@@ -466,12 +555,12 @@ fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> {
466 if !contents.is_empty() { 555 if !contents.is_empty() {
467 files_found = true; 556 files_found = true;
468 if show_stdout { 557 if show_stdout {
469 println!("==> {} <==", stderr_file); 558 println!("==> {} <==", stderr_file.display());
470 } 559 }
471 print!("{}", contents); 560 print!("{}", contents);
472 } 561 }
473 } else { 562 } else {
474 tracing::warn!("Could not read {}", stderr_file); 563 tracing::warn!("Could not read {}", stderr_file.display());
475 } 564 }
476 } 565 }
477 566
@@ -482,31 +571,38 @@ fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> {
482 Ok(()) 571 Ok(())
483} 572}
484 573
485fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool, follow: bool, lines: usize) -> Result<()> { 574fn tail_logs(
486 let stdout_file = format!("{}.stdout", id); 575 id: &str,
487 let stderr_file = format!("{}.stderr", id); 576 show_stdout: bool,
577 show_stderr: bool,
578 follow: bool,
579 lines: usize,
580 root_dir: &Path,
581) -> Result<()> {
582 let stdout_file = build_file_path(root_dir, id, "stdout");
583 let stderr_file = build_file_path(root_dir, id, "stderr");
488 584
489 if !follow { 585 if !follow {
490 // Non-follow mode: just show the last n lines and exit 586 // Non-follow mode: just show the last n lines and exit
491 let mut files_found = false; 587 let mut files_found = false;
492 588
493 if show_stdout && Path::new(&stdout_file).exists() { 589 if show_stdout && stdout_file.exists() {
494 let content = read_last_n_lines(&stdout_file, lines)?; 590 let content = read_last_n_lines(&stdout_file, lines)?;
495 if !content.is_empty() { 591 if !content.is_empty() {
496 files_found = true; 592 files_found = true;
497 if show_stderr { 593 if show_stderr {
498 println!("==> {} <==", stdout_file); 594 println!("==> {} <==", stdout_file.display());
499 } 595 }
500 print!("{}", content); 596 print!("{}", content);
501 } 597 }
502 } 598 }
503 599
504 if show_stderr && Path::new(&stderr_file).exists() { 600 if show_stderr && stderr_file.exists() {
505 let content = read_last_n_lines(&stderr_file, lines)?; 601 let content = read_last_n_lines(&stderr_file, lines)?;
506 if !content.is_empty() { 602 if !content.is_empty() {
507 files_found = true; 603 files_found = true;
508 if show_stdout { 604 if show_stdout {
509 println!("==> {} <==", stderr_file); 605 println!("==> {} <==", stderr_file.display());
510 } 606 }
511 print!("{}", content); 607 print!("{}", content);
512 } 608 }
@@ -520,15 +616,15 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool, follow: bool, lines
520 } 616 }
521 617
522 // Follow mode: original real-time monitoring behavior 618 // Follow mode: original real-time monitoring behavior
523 let mut file_positions: std::collections::HashMap<String, u64> = 619 let mut file_positions: std::collections::HashMap<PathBuf, u64> =
524 std::collections::HashMap::new(); 620 std::collections::HashMap::new();
525 621
526 if show_stdout && Path::new(&stdout_file).exists() { 622 if show_stdout && stdout_file.exists() {
527 let mut file = File::open(&stdout_file)?; 623 let mut file = File::open(&stdout_file)?;
528 let initial_content = read_file_content(&mut file)?; 624 let initial_content = read_file_content(&mut file)?;
529 if !initial_content.is_empty() { 625 if !initial_content.is_empty() {
530 if show_stderr { 626 if show_stderr {
531 println!("==> {} <==", stdout_file); 627 println!("==> {} <==", stdout_file.display());
532 } 628 }
533 print!("{}", initial_content); 629 print!("{}", initial_content);
534 } 630 }
@@ -536,14 +632,14 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool, follow: bool, lines
536 file_positions.insert(stdout_file.clone(), position); 632 file_positions.insert(stdout_file.clone(), position);
537 } 633 }
538 634
539 if show_stderr && Path::new(&stderr_file).exists() { 635 if show_stderr && stderr_file.exists() {
540 let mut file = File::open(&stderr_file)?; 636 let mut file = File::open(&stderr_file)?;
541 let initial_content = read_file_content(&mut file)?; 637 let initial_content = read_file_content(&mut file)?;
542 if !initial_content.is_empty() { 638 if !initial_content.is_empty() {
543 if show_stdout && file_positions.len() > 0 { 639 if show_stdout && file_positions.len() > 0 {
544 println!("\n==> {} <==", stderr_file); 640 println!("\n==> {} <==", stderr_file.display());
545 } else if show_stdout { 641 } else if show_stdout {
546 println!("==> {} <==", stderr_file); 642 println!("==> {} <==", stderr_file.display());
547 } 643 }
548 print!("{}", initial_content); 644 print!("{}", initial_content);
549 } 645 }
@@ -564,8 +660,8 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool, follow: bool, lines
564 let (tx, rx) = channel(); 660 let (tx, rx) = channel();
565 let mut watcher = RecommendedWatcher::new(tx, Config::default())?; 661 let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
566 662
567 // Watch the current directory for new files and changes 663 // Watch the root directory for new files and changes
568 watcher.watch(Path::new("."), RecursiveMode::NonRecursive)?; 664 watcher.watch(root_dir, RecursiveMode::NonRecursive)?;
569 665
570 // Handle Ctrl+C gracefully 666 // Handle Ctrl+C gracefully
571 let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); 667 let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
@@ -585,13 +681,11 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool, follow: bool, lines
585 .. 681 ..
586 }) => { 682 }) => {
587 for path in paths { 683 for path in paths {
588 let path_str = path.to_string_lossy().to_string(); 684 if (show_stdout && path == stdout_file)
589 685 || (show_stderr && path == stderr_file)
590 if (show_stdout && path_str == stdout_file)
591 || (show_stderr && path_str == stderr_file)
592 { 686 {
593 if let Err(e) = handle_file_change( 687 if let Err(e) = handle_file_change(
594 &path_str, 688 &path,
595 &mut file_positions, 689 &mut file_positions,
596 show_stdout && show_stderr, 690 show_stdout && show_stderr,
597 ) { 691 ) {
@@ -607,16 +701,14 @@ fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool, follow: bool, lines
607 }) => { 701 }) => {
608 // Handle file creation 702 // Handle file creation
609 for path in paths { 703 for path in paths {
610 let path_str = path.to_string_lossy().to_string(); 704 if (show_stdout && path == stdout_file)
611 705 || (show_stderr && path == stderr_file)
612 if (show_stdout && path_str == stdout_file)
613 || (show_stderr && path_str == stderr_file)
614 { 706 {
615 tracing::info!("New file detected: {}", path_str); 707 tracing::info!("New file detected: {}", path.display());
616 file_positions.insert(path_str.clone(), 0); 708 file_positions.insert(path.clone(), 0);
617 709
618 if let Err(e) = handle_file_change( 710 if let Err(e) = handle_file_change(
619 &path_str, 711 &path,
620 &mut file_positions, 712 &mut file_positions,
621 show_stdout && show_stderr, 713 show_stdout && show_stderr,
622 ) { 714 ) {
@@ -649,26 +741,22 @@ fn read_file_content(file: &mut File) -> Result<String> {
649 Ok(content) 741 Ok(content)
650} 742}
651 743
652fn read_last_n_lines(file_path: &str, n: usize) -> Result<String> { 744fn read_last_n_lines<P: AsRef<Path>>(file_path: P, n: usize) -> Result<String> {
653 let content = std::fs::read_to_string(file_path)?; 745 let content = std::fs::read_to_string(file_path)?;
654 if content.is_empty() { 746 if content.is_empty() {
655 return Ok(String::new()); 747 return Ok(String::new());
656 } 748 }
657 749
658 let lines: Vec<&str> = content.lines().collect(); 750 let lines: Vec<&str> = content.lines().collect();
659 let start_index = if lines.len() > n { 751 let start_index = if lines.len() > n { lines.len() - n } else { 0 };
660 lines.len() - n 752
661 } else {
662 0
663 };
664
665 let last_lines: Vec<&str> = lines[start_index..].to_vec(); 753 let last_lines: Vec<&str> = lines[start_index..].to_vec();
666 Ok(last_lines.join("\n") + if content.ends_with('\n') { "\n" } else { "" }) 754 Ok(last_lines.join("\n") + if content.ends_with('\n') { "\n" } else { "" })
667} 755}
668 756
669fn handle_file_change( 757fn handle_file_change(
670 file_path: &str, 758 file_path: &Path,
671 positions: &mut std::collections::HashMap<String, u64>, 759 positions: &mut std::collections::HashMap<PathBuf, u64>,
672 show_headers: bool, 760 show_headers: bool,
673) -> Result<()> { 761) -> Result<()> {
674 let mut file = File::open(file_path)?; 762 let mut file = File::open(file_path)?;
@@ -683,20 +771,20 @@ fn handle_file_change(
683 771
684 if !new_content.is_empty() { 772 if !new_content.is_empty() {
685 if show_headers { 773 if show_headers {
686 println!("==> {} <==", file_path); 774 println!("==> {} <==", file_path.display());
687 } 775 }
688 print!("{}", new_content); 776 print!("{}", new_content);
689 std::io::Write::flush(&mut std::io::stdout())?; 777 std::io::Write::flush(&mut std::io::stdout())?;
690 778
691 // Update position 779 // Update position
692 let new_pos = file.seek(SeekFrom::Current(0))?; 780 let new_pos = file.seek(SeekFrom::Current(0))?;
693 positions.insert(file_path.to_string(), new_pos); 781 positions.insert(file_path.to_path_buf(), new_pos);
694 } 782 }
695 783
696 Ok(()) 784 Ok(())
697} 785}
698 786
699fn list_daemons(quiet: bool) -> Result<()> { 787fn list_daemons(quiet: bool, root_dir: &Path) -> Result<()> {
700 if !quiet { 788 if !quiet {
701 println!("{:<20} {:<8} {:<10} {}", "ID", "PID", "STATUS", "COMMAND"); 789 println!("{:<20} {:<8} {:<10} {}", "ID", "PID", "STATUS", "COMMAND");
702 println!("{}", "-".repeat(50)); 790 println!("{}", "-".repeat(50));
@@ -704,14 +792,16 @@ fn list_daemons(quiet: bool) -> Result<()> {
704 792
705 let mut found_any = false; 793 let mut found_any = false;
706 794
707 // Find all .pid files in current directory 795 // Find all .pid files in root directory
708 for entry in find_pid_files()? { 796 for entry in find_pid_files(root_dir)? {
709 found_any = true; 797 found_any = true;
710 let path = entry.path(); 798 let path = entry.path();
711 let path_str = path.to_string_lossy(); 799 let filename = path.file_name()
800 .and_then(|name| name.to_str())
801 .unwrap_or_default();
712 802
713 // Extract ID from filename (remove .pid extension) 803 // Extract ID from filename (remove .pid extension)
714 let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); 804 let id = filename.strip_suffix(".pid").unwrap_or(filename);
715 805
716 // Read PID data from file 806 // Read PID data from file
717 match PidFile::read_from_file(&path) { 807 match PidFile::read_from_file(&path) {
@@ -770,13 +860,13 @@ fn list_daemons(quiet: bool) -> Result<()> {
770 Ok(()) 860 Ok(())
771} 861}
772 862
773fn status_daemon(id: &str) -> Result<()> { 863fn status_daemon(id: &str, root_dir: &Path) -> Result<()> {
774 let pid_file = format!("{}.pid", id); 864 let pid_file = build_file_path(root_dir, id, "pid");
775 let stdout_file = format!("{}.stdout", id); 865 let stdout_file = build_file_path(root_dir, id, "stdout");
776 let stderr_file = format!("{}.stderr", id); 866 let stderr_file = build_file_path(root_dir, id, "stderr");
777 867
778 println!("Daemon: {}", id); 868 println!("Daemon: {}", id);
779 println!("PID file: {}", pid_file); 869 println!("PID file: {}", pid_file.display());
780 870
781 // Read PID data from file 871 // Read PID data from file
782 match PidFile::read_from_file(&pid_file) { 872 match PidFile::read_from_file(&pid_file) {
@@ -788,18 +878,18 @@ fn status_daemon(id: &str) -> Result<()> {
788 println!("Status: RUNNING"); 878 println!("Status: RUNNING");
789 879
790 // Show file information 880 // Show file information
791 if Path::new(&stdout_file).exists() { 881 if stdout_file.exists() {
792 let metadata = std::fs::metadata(&stdout_file)?; 882 let metadata = std::fs::metadata(&stdout_file)?;
793 println!("Stdout file: {} ({} bytes)", stdout_file, metadata.len()); 883 println!("Stdout file: {} ({} bytes)", stdout_file.display(), metadata.len());
794 } else { 884 } else {
795 println!("Stdout file: {} (not found)", stdout_file); 885 println!("Stdout file: {} (not found)", stdout_file.display());
796 } 886 }
797 887
798 if Path::new(&stderr_file).exists() { 888 if stderr_file.exists() {
799 let metadata = std::fs::metadata(&stderr_file)?; 889 let metadata = std::fs::metadata(&stderr_file)?;
800 println!("Stderr file: {} ({} bytes)", stderr_file, metadata.len()); 890 println!("Stderr file: {} ({} bytes)", stderr_file.display(), metadata.len());
801 } else { 891 } else {
802 println!("Stderr file: {} (not found)", stderr_file); 892 println!("Stderr file: {} (not found)", stderr_file.display());
803 } 893 }
804 } else { 894 } else {
805 println!("Status: DEAD (process not running)"); 895 println!("Status: DEAD (process not running)");
@@ -820,16 +910,18 @@ fn status_daemon(id: &str) -> Result<()> {
820 Ok(()) 910 Ok(())
821} 911}
822 912
823fn clean_orphaned_files() -> Result<()> { 913fn clean_orphaned_files(root_dir: &Path) -> Result<()> {
824 tracing::info!("Scanning for orphaned daemon files..."); 914 tracing::info!("Scanning for orphaned daemon files...");
825 915
826 let mut cleaned_count = 0; 916 let mut cleaned_count = 0;
827 917
828 // Find all .pid files in current directory 918 // Find all .pid files in root directory
829 for entry in find_pid_files()? { 919 for entry in find_pid_files(root_dir)? {
830 let path = entry.path(); 920 let path = entry.path();
831 let path_str = path.to_string_lossy(); 921 let filename = path.file_name()
832 let id = path_str.strip_suffix(".pid").unwrap_or(&path_str); 922 .and_then(|name| name.to_str())
923 .unwrap_or_default();
924 let id = filename.strip_suffix(".pid").unwrap_or(filename);
833 925
834 // Read PID data from file 926 // Read PID data from file
835 match PidFile::read_from_file(&path) { 927 match PidFile::read_from_file(&path) {
@@ -843,28 +935,28 @@ fn clean_orphaned_files() -> Result<()> {
843 935
844 // Remove PID file 936 // Remove PID file
845 if let Err(e) = std::fs::remove_file(&path) { 937 if let Err(e) = std::fs::remove_file(&path) {
846 tracing::warn!("Failed to remove {}: {}", path_str, e); 938 tracing::warn!("Failed to remove {}: {}", path.display(), e);
847 } else { 939 } else {
848 tracing::info!("Removed {}", path_str); 940 tracing::info!("Removed {}", path.display());
849 } 941 }
850 942
851 // Remove stdout file if it exists 943 // Remove stdout file if it exists
852 let stdout_file = format!("{}.stdout", id); 944 let stdout_file = build_file_path(root_dir, id, "stdout");
853 if Path::new(&stdout_file).exists() { 945 if stdout_file.exists() {
854 if let Err(e) = std::fs::remove_file(&stdout_file) { 946 if let Err(e) = std::fs::remove_file(&stdout_file) {
855 tracing::warn!("Failed to remove {}: {}", stdout_file, e); 947 tracing::warn!("Failed to remove {}: {}", stdout_file.display(), e);
856 } else { 948 } else {
857 tracing::info!("Removed {}", stdout_file); 949 tracing::info!("Removed {}", stdout_file.display());
858 } 950 }
859 } 951 }
860 952
861 // Remove stderr file if it exists 953 // Remove stderr file if it exists
862 let stderr_file = format!("{}.stderr", id); 954 let stderr_file = build_file_path(root_dir, id, "stderr");
863 if Path::new(&stderr_file).exists() { 955 if stderr_file.exists() {
864 if let Err(e) = std::fs::remove_file(&stderr_file) { 956 if let Err(e) = std::fs::remove_file(&stderr_file) {
865 tracing::warn!("Failed to remove {}: {}", stderr_file, e); 957 tracing::warn!("Failed to remove {}: {}", stderr_file.display(), e);
866 } else { 958 } else {
867 tracing::info!("Removed {}", stderr_file); 959 tracing::info!("Removed {}", stderr_file.display());
868 } 960 }
869 } 961 }
870 962
@@ -879,14 +971,14 @@ fn clean_orphaned_files() -> Result<()> {
879 } 971 }
880 Err(PidFileReadError::FileNotFound) => { 972 Err(PidFileReadError::FileNotFound) => {
881 // This shouldn't happen since we found the file, but handle gracefully 973 // This shouldn't happen since we found the file, but handle gracefully
882 tracing::warn!("PID file {} disappeared during processing", path_str); 974 tracing::warn!("PID file {} disappeared during processing", path.display());
883 } 975 }
884 Err(PidFileReadError::FileInvalid(_)) | Err(PidFileReadError::IoError(_)) => { 976 Err(PidFileReadError::FileInvalid(_)) | Err(PidFileReadError::IoError(_)) => {
885 println!("Cleaning up invalid PID file: {}", path_str); 977 println!("Cleaning up invalid PID file: {}", path.display());
886 if let Err(e) = std::fs::remove_file(&path) { 978 if let Err(e) = std::fs::remove_file(&path) {
887 tracing::warn!("Failed to remove invalid PID file {}: {}", path_str, e); 979 tracing::warn!("Failed to remove invalid PID file {}: {}", path.display(), e);
888 } else { 980 } else {
889 tracing::info!("Removed invalid PID file {}", path_str); 981 tracing::info!("Removed invalid PID file {}", path.display());
890 cleaned_count += 1; 982 cleaned_count += 1;
891 } 983 }
892 } 984 }
@@ -1164,8 +1256,8 @@ This tool is designed for Linux environments and provides a simple interface for
1164 ); 1256 );
1165} 1257}
1166 1258
1167fn wait_daemon(id: &str, timeout: u64, interval: u64) -> Result<()> { 1259fn wait_daemon(id: &str, timeout: u64, interval: u64, root_dir: &Path) -> Result<()> {
1168 let pid_file = format!("{}.pid", id); 1260 let pid_file = build_file_path(root_dir, id, "pid");
1169 1261
1170 // Check if PID file exists and read PID data 1262 // Check if PID file exists and read PID data
1171 let pid_file_data = match PidFile::read_from_file(&pid_file) { 1263 let pid_file_data = match PidFile::read_from_file(&pid_file) {
@@ -1174,10 +1266,18 @@ fn wait_daemon(id: &str, timeout: u64, interval: u64) -> Result<()> {
1174 return Err(anyhow::anyhow!("Process '{}' not found (no PID file)", id)); 1266 return Err(anyhow::anyhow!("Process '{}' not found (no PID file)", id));
1175 } 1267 }
1176 Err(PidFileReadError::FileInvalid(reason)) => { 1268 Err(PidFileReadError::FileInvalid(reason)) => {
1177 return Err(anyhow::anyhow!("Process '{}' has invalid PID file: {}", id, reason)); 1269 return Err(anyhow::anyhow!(
1270 "Process '{}' has invalid PID file: {}",
1271 id,
1272 reason
1273 ));
1178 } 1274 }
1179 Err(PidFileReadError::IoError(err)) => { 1275 Err(PidFileReadError::IoError(err)) => {
1180 return Err(anyhow::anyhow!("Failed to read PID file for '{}': {}", id, err)); 1276 return Err(anyhow::anyhow!(
1277 "Failed to read PID file for '{}': {}",
1278 id,
1279 err
1280 ));
1181 } 1281 }
1182 }; 1282 };
1183 1283
@@ -1208,17 +1308,20 @@ fn wait_daemon(id: &str, timeout: u64, interval: u64) -> Result<()> {
1208 tracing::info!("Process '{}' (PID: {}) has terminated", id, pid); 1308 tracing::info!("Process '{}' (PID: {}) has terminated", id, pid);
1209 return Ok(()); 1309 return Ok(());
1210 } 1310 }
1211 1311
1212 thread::sleep(Duration::from_secs(interval)); 1312 thread::sleep(Duration::from_secs(interval));
1213 elapsed += interval; 1313 elapsed += interval;
1214 } 1314 }
1215 1315
1216 // Timeout reached 1316 // Timeout reached
1217 Err(anyhow::anyhow!("Timeout reached waiting for process '{}' to terminate", id)) 1317 Err(anyhow::anyhow!(
1318 "Timeout reached waiting for process '{}' to terminate",
1319 id
1320 ))
1218} 1321}
1219 1322
1220fn find_pid_files() -> Result<Vec<std::fs::DirEntry>> { 1323fn find_pid_files(root_dir: &Path) -> Result<Vec<std::fs::DirEntry>> {
1221 let entries = std::fs::read_dir(".")? 1324 let entries = std::fs::read_dir(root_dir)?
1222 .filter_map(|entry| { 1325 .filter_map(|entry| {
1223 entry.ok().and_then(|e| { 1326 entry.ok().and_then(|e| {
1224 e.path() 1327 e.path()