rss/src/main.rs
Niko Marmeladkov 27c3d339c6
add --state-dir (-D) flag to persist enabled/logs/pid across reboots
socket stays in /tmp/rss/rss.sock so client commands need no extra args;
state dir is controlled by RSS_STATE_DIR or --state-dir and stores
enabled flags, logs, and daemon.pid in a persistent location.
2026-07-05 22:31:48 +03:00

267 lines
7.2 KiB
Rust

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<usize>, 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,
}
}
}