diff options
Diffstat (limited to 'src/main.rs')
| -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() |
