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