#[cfg(not(unix))]
fn main() {
eprintln!("procserv-rs is Unix-only (requires forkpty)");
std::process::exit(2);
}
#[cfg(unix)]
mod app {
use std::path::PathBuf;
use std::process::ExitCode;
use std::time::Duration;
use clap::Parser;
use epics_tools_rs::procserv::{
ProcServ, ProcServConfig,
config::{ChildConfig, KeyBindings, ListenConfig, LoggingConfig},
daemon::{fork_and_go, install_signal_handlers},
restart::{RestartMode, RestartPolicy},
};
#[derive(Parser, Debug)]
#[command(
name = "procserv-rs",
about = "Pure-Rust port of the EPICS procServ process supervisor",
version
)]
struct Args {
#[arg(short = 'p', long)]
port: Option<u16>,
#[arg(long)]
allow: bool,
#[arg(long = "unixpath")]
unix_path: Option<PathBuf>,
#[arg(short = 'f', long)]
foreground: bool,
#[arg(short = 'L', long)]
logfile: Option<PathBuf>,
#[arg(long)]
pidfile: Option<PathBuf>,
#[arg(long)]
info_file: Option<PathBuf>,
#[arg(long, default_value_t = 15)]
holdoff: u64,
#[arg(short = 'w', long)]
wait: bool,
#[arg(long)]
chdir: Option<PathBuf>,
#[arg(long)]
name: Option<String>,
#[arg(long, default_value_t = 10)]
max_restarts: u32,
#[arg(long, default_value_t = 600)]
restart_window: u64,
#[arg(long, default_value_t = 24)]
kill_char: u8,
#[arg(long, default_value_t = 20)]
toggle_restart_char: u8,
#[arg(long, default_value_t = 29)]
logout_char: u8,
#[arg(trailing_var_arg = true, allow_hyphen_values = true, required = true)]
cmd: Vec<String>,
}
fn build_config(args: Args) -> Result<ProcServConfig, String> {
if args.cmd.is_empty() {
return Err("missing child command".into());
}
let mut iter = args.cmd.into_iter();
let program = PathBuf::from(iter.next().unwrap());
let argv: Vec<String> = iter.collect();
let display_name = args.name.unwrap_or_else(|| {
program
.file_name()
.map_or("child".into(), |s| s.to_string_lossy().into())
});
let listen = ListenConfig {
tcp_port: args.port,
tcp_bind: args.port.map(|p| {
if args.allow {
std::net::SocketAddr::from(([0, 0, 0, 0], p))
} else {
std::net::SocketAddr::from(([127, 0, 0, 1], p))
}
}),
unix_path: args.unix_path,
};
let child = ChildConfig {
name: display_name,
program,
args: argv,
cwd: args.chdir,
kill_signal: 9, ignore_chars: Vec::new(),
};
let logging = LoggingConfig {
log_path: args.logfile,
pid_path: args.pidfile,
info_path: args.info_file,
time_format: "%Y-%m-%d %H:%M:%S".into(),
};
let nz = |c: u8| if c == 0 { None } else { Some(c) };
Ok(ProcServConfig {
foreground: args.foreground,
listen,
keys: KeyBindings {
kill: nz(args.kill_char),
toggle_restart: nz(args.toggle_restart_char),
restart: Some(0x12), quit: None,
logout: nz(args.logout_char),
},
child,
logging,
restart: RestartPolicy {
max_restarts: args.max_restarts,
window: Duration::from_secs(args.restart_window),
delay: Duration::from_secs(1),
},
restart_mode: RestartMode::OnExit,
holdoff: Duration::from_secs(args.holdoff),
wait_for_manual_start: args.wait,
})
}
pub fn entry() -> ExitCode {
let _ = tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.try_init();
let args = Args::parse();
let foreground = args.foreground;
let cfg = match build_config(args) {
Ok(c) => c,
Err(e) => {
tracing::error!(error = %e, "procserv-rs: invalid config");
return ExitCode::FAILURE;
}
};
if !foreground && let Err(e) = fork_and_go() {
eprintln!("procserv-rs: daemonize failed: {e}");
return ExitCode::FAILURE;
}
let runtime = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(r) => r,
Err(e) => {
tracing::error!(error = %e, "procserv-rs: tokio runtime build failed");
return ExitCode::FAILURE;
}
};
runtime.block_on(async move {
let server = match ProcServ::new(cfg) {
Ok(s) => s,
Err(e) => {
tracing::error!(error = %e, "procserv-rs: build failed");
return ExitCode::FAILURE;
}
};
let shutdown = match install_signal_handlers().await {
Ok(s) => s,
Err(e) => {
tracing::error!(error = %e, "procserv-rs: signal handler install failed");
return ExitCode::FAILURE;
}
};
tokio::select! {
res = server.run() => match res {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
tracing::error!(error = %e, "procserv-rs: runtime error");
ExitCode::FAILURE
}
},
reason = shutdown.wait() => {
tracing::info!(reason = ?reason.ok(), "procserv-rs: shutdown signal");
ExitCode::SUCCESS
}
}
})
}
}
#[cfg(unix)]
fn main() -> std::process::ExitCode {
app::entry()
}