diff options
| author | diogo464 <[email protected]> | 2025-06-19 09:18:18 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-06-19 09:18:18 +0100 |
| commit | 1c2e20c56d7fdbb0f7b21d12137ec7d58cd839c8 (patch) | |
| tree | 9ea6d12018eacb4b9928fb7b667333682f3b5491 /src/main.rs | |
| parent | a8e1212c07fb489cfa430ef64fb3a1c8df464a22 (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/main.rs')
| -rw-r--r-- | src/main.rs | 302 |
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 @@ | |||
| 1 | use clap::{Parser, Subcommand, Args}; | 1 | use anyhow::{Context, Result}; |
| 2 | use clap::{Args, Parser, Subcommand}; | ||
| 3 | use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; | ||
| 2 | use std::fs::File; | 4 | use std::fs::File; |
| 3 | use std::io::{Write, Read, Seek, SeekFrom}; | 5 | use std::io::{Read, Seek, SeekFrom, Write}; |
| 6 | use std::path::Path; | ||
| 4 | use std::process::{Command, Stdio}; | 7 | use std::process::{Command, Stdio}; |
| 8 | use std::sync::mpsc::channel; | ||
| 5 | use std::thread; | 9 | use std::thread; |
| 6 | use std::time::Duration; | 10 | use std::time::Duration; |
| 7 | use std::path::Path; | ||
| 8 | use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; | ||
| 9 | use std::sync::mpsc::channel; | ||
| 10 | use 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 { | |||
| 23 | enum Commands { | 23 | enum 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 | ||
| 231 | fn stop_daemon(id: &str, timeout: u64) -> Result<()> { | 227 | fn 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 { | |||
| 324 | fn cat_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { | 335 | fn 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 | ||
| 365 | fn tail_logs(id: &str, show_stdout: bool, show_stderr: bool) -> Result<()> { | 376 | fn 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 | ||
| 487 | fn handle_file_change( | 510 | fn 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 | ||
| 638 | fn clean_orphaned_files() -> Result<()> { | 670 | fn 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 | ||
| 723 | fn print_llm_guide() { | 759 | fn 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 |
| 727 | Demon 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. | 764 | Demon 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 | ||
| 951 | This tool is designed for Linux environments and provides a simple interface for managing background processes with persistent logging."#); | 988 | This tool is designed for Linux environments and provides a simple interface for managing background processes with persistent logging."# |
| 989 | ); | ||
| 952 | } | 990 | } |
| 953 | 991 | ||
| 954 | fn find_pid_files() -> Result<Vec<std::fs::DirEntry>> { | 992 | fn find_pid_files() -> Result<Vec<std::fs::DirEntry>> { |
