diff options
| author | diogo464 <[email protected]> | 2025-06-19 17:11:09 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-06-19 17:11:09 +0100 |
| commit | a365f12a6c6e7775a5bd3c6177050b74826c608c (patch) | |
| tree | a940d89c741250aedacb4dc8d8f9e92ce9810fc4 /src | |
| parent | 9c3d24b08649ebf6d4a3614f3506ce2702aafd74 (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.rs | 317 |
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}; | |||
| 3 | use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; | 3 | use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; |
| 4 | use std::fs::File; | 4 | use std::fs::File; |
| 5 | use std::io::{Read, Seek, SeekFrom, Write}; | 5 | use std::io::{Read, Seek, SeekFrom, Write}; |
| 6 | use std::path::Path; | 6 | use std::path::{Path, PathBuf}; |
| 7 | use std::process::{Command, Stdio}; | 7 | use std::process::{Command, Stdio}; |
| 8 | use std::sync::mpsc::channel; | 8 | use std::sync::mpsc::channel; |
| 9 | use std::thread; | 9 | use std::thread; |
| @@ -117,6 +117,13 @@ struct Cli { | |||
| 117 | command: Commands, | 117 | command: Commands, |
| 118 | } | 118 | } |
| 119 | 119 | ||
| 120 | #[derive(Args)] | ||
| 121 | struct 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)] |
| 121 | enum Commands { | 128 | enum 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)] |
| 151 | struct RunArgs { | 158 | struct 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)] |
| 160 | struct StopArgs { | 170 | struct 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)] |
| 170 | struct TailArgs { | 183 | struct 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)] |
| 192 | struct CatArgs { | 208 | struct 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)] |
| 206 | struct ListArgs { | 225 | struct 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)] |
| 213 | struct StatusArgs { | 235 | struct 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)] |
| 244 | struct CleanArgs { | ||
| 245 | #[clap(flatten)] | ||
| 246 | global: Global, | ||
| 247 | } | ||
| 248 | |||
| 249 | #[derive(Args)] | ||
| 219 | struct WaitArgs { | 250 | struct 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 | ||
| 275 | fn run_daemon(id: &str, command: &[String]) -> Result<()> { | 327 | fn 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 | |||
| 345 | fn 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 | |||
| 360 | fn build_file_path(root_dir: &Path, id: &str, extension: &str) -> PathBuf { | ||
| 361 | root_dir.join(format!("{}.{}", id, extension)) | ||
| 362 | } | ||
| 363 | |||
| 364 | fn 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 | ||
| 323 | fn is_process_running(pid_file: &str) -> Result<bool> { | 412 | fn 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 | ||
| 339 | fn stop_daemon(id: &str, timeout: u64) -> Result<()> { | 428 | fn 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 | ||
| 444 | fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { | 533 | fn 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 | ||
| 485 | fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool, follow: bool, lines: usize) -> Result<()> { | 574 | fn 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 | ||
| 652 | fn read_last_n_lines(file_path: &str, n: usize) -> Result<String> { | 744 | fn 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 | ||
| 669 | fn handle_file_change( | 757 | fn 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 | ||
| 699 | fn list_daemons(quiet: bool) -> Result<()> { | 787 | fn 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 | ||
| 773 | fn status_daemon(id: &str) -> Result<()> { | 863 | fn 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 | ||
| 823 | fn clean_orphaned_files() -> Result<()> { | 913 | fn 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 | ||
| 1167 | fn wait_daemon(id: &str, timeout: u64, interval: u64) -> Result<()> { | 1259 | fn 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 | ||
| 1220 | fn find_pid_files() -> Result<Vec<std::fs::DirEntry>> { | 1323 | fn 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() |
