mod cli; mod config; mod daemon; mod ipc; mod log; mod state; mod supervisor; use clap::Parser; use cli::{Cli, Commands}; use std::io::Write; use std::sync::atomic::{AtomicBool, Ordering}; fn main() { let cli = Cli::parse(); if let Some(ref dir) = cli.state_dir { std::env::set_var("RSS_STATE_DIR", dir); } match cli.command { Commands::Daemon => { let config_path = resolve_config_path(cli.config.as_deref()); daemon::run(config_path); } Commands::Start { ref names } => cmd_start(names), Commands::Stop { ref names } => cmd_stop(names), Commands::Restart { ref names } => cmd_restart(names), Commands::Status { ref name } => cmd_status(name.as_deref()), Commands::List => cmd_list(), Commands::Logs { ref name, lines, follow } => cmd_logs(name, lines, follow), Commands::Enable { ref names, now } => cmd_enable(names, now), Commands::Disable { ref names } => cmd_disable(names), Commands::Send { ref name, ref message } => cmd_send(name, message), Commands::Attach { ref name, full } => cmd_attach(name, full), Commands::Supervise { .. } => { eprintln!("error: supervise is internal, use daemon"); std::process::exit(1); } } } fn resolve_config_path(custom: Option<&str>) -> String { if let Some(path) = custom { return path.to_string(); } if let Ok(path) = std::env::var("RSS_CONFIG") { return path; } state::config_dir().join("config.toml").to_string_lossy().to_string() } fn cmd_start(names: &[String]) { let cmd = format!("start {}", names.join(" ")); match ipc::send_cmd(&cmd) { Ok(lines) => { for line in lines { println!("{}", line); } } Err(e) => { eprintln!("error: {}", e); std::process::exit(1); } } } fn cmd_stop(names: &[String]) { let cmd = format!("stop {}", names.join(" ")); match ipc::send_cmd(&cmd) { Ok(lines) => { for line in lines { println!("{}", line); } } Err(e) => { eprintln!("error: {}", e); std::process::exit(1); } } } fn cmd_restart(names: &[String]) { let cmd = format!("restart {}", names.join(" ")); match ipc::send_cmd(&cmd) { Ok(lines) => { for line in lines { println!("{}", line); } } Err(e) => { eprintln!("error: {}", e); std::process::exit(1); } } } fn cmd_status(name: Option<&str>) { let cmd = match name { Some(n) => format!("status {}", n), None => "status".to_string(), }; match ipc::send_cmd(&cmd) { Ok(lines) => { for line in lines { println!("{}", line); } } Err(e) => { eprintln!("error: {}", e); std::process::exit(1); } } } fn cmd_list() { match ipc::send_cmd("list") { Ok(lines) => { if lines.is_empty() { println!("no services configured"); return; } for line in &lines { let parts: Vec<&str> = line.splitn(4, ' ').collect(); if parts.len() >= 4 { println!("{:<12} {:<7} {:>6} {}", parts[0], parts[1], parts[2], parts[3]); } else { println!("{}", line); } } } Err(e) => { eprintln!("error: {}", e); std::process::exit(1); } } } fn cmd_logs(name: &str, mut lines: Option, follow: bool) { if lines.is_none() && follow { lines = Some(10); } let log_path = state::log_path(name); if !log_path.exists() { eprintln!("error: no logs for '{}'", name); std::process::exit(1); } if let Err(e) = log::show_logs(log_path.to_str().unwrap_or(""), lines, follow) { eprintln!("error: reading logs: {}", e); std::process::exit(1); } } fn cmd_enable(names: &[String], now: bool) { let cmd = if now { format!("enable {} now", names.join(" ")) } else { format!("enable {}", names.join(" ")) }; match ipc::send_cmd(&cmd) { Ok(lines) => { for line in lines { println!("{}", line); } } Err(e) => { eprintln!("error: {}", e); std::process::exit(1); } } } fn cmd_disable(names: &[String]) { let cmd = format!("disable {}", names.join(" ")); match ipc::send_cmd(&cmd) { Ok(lines) => { for line in lines { println!("{}", line); } } Err(e) => { eprintln!("error: {}", e); std::process::exit(1); } } } fn cmd_send(name: &str, message: &[String]) { let msg = message.join(" "); let cmd = format!("stdin {} {}", name, msg); match ipc::send_cmd(&cmd) { Ok(_) => println!("ok"), Err(e) => { eprintln!("error: {}", e); std::process::exit(1); } } } static ATTACH_INTERRUPT: AtomicBool = AtomicBool::new(false); extern "C" fn handle_attach_int(_: i32) { ATTACH_INTERRUPT.store(true, Ordering::Relaxed); } fn cmd_attach(name: &str, full: bool) { let sig_action = nix::sys::signal::SigAction::new( nix::sys::signal::SigHandler::Handler(handle_attach_int), nix::sys::signal::SaFlags::empty(), nix::sys::signal::SigSet::empty(), ); unsafe { let _ = nix::sys::signal::sigaction(nix::sys::signal::Signal::SIGINT, &sig_action); } let stream = match ipc::send_cmd_get_stream(&format!("stdin {}", name)) { Ok(s) => s, Err(e) => { eprintln!("error: {}", e); std::process::exit(1); } }; // Tail the log file in a background thread let log_path = crate::state::log_path(name); if log_path.exists() { let log_path_str = log_path.to_string_lossy().to_string(); std::thread::spawn(move || { let _ = crate::log::show_logs(&log_path_str, Some(0), true); }); } // Main loop: read stdin line by line, send to daemon let stdin = std::io::stdin(); loop { if ATTACH_INTERRUPT.load(Ordering::Relaxed) { if full { let _ = ipc::send_cmd(&format!("signal {} INT", name)); } break; } let mut line = String::new(); match stdin.read_line(&mut line) { Ok(0) => break, Ok(_) => { let trimmed = line.trim_end_matches('\n').trim_end_matches('\r'); let bytes = trimmed.as_bytes(); if bytes.len() >= 2 && bytes[0] == 0x10 && (bytes[1] == b'd' || bytes[1] == b'D') { break; } if !trimmed.is_empty() { let _ = writeln!(&stream, "{}", trimmed); let _ = (&stream).flush(); } } Err(e) if e.kind() == std::io::ErrorKind::Interrupted => { continue; } Err(_) => break, } } }