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.
267 lines
7.2 KiB
Rust
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,
|
|
}
|
|
}
|
|
}
|