aboutsummaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-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()