use std::{
ffi::OsString,
hash::Hasher,
path::Path,
process::ExitStatus,
};
use nix::{sys::signal::Signal, unistd::Pid};
use tokio::{
process::{Child, Command},
signal::unix::{signal, SignalKind},
task::JoinSet,
};
use crate::config::Config;
const SUPERVISOR_ENV: &str = "PROKSI_SUPERVISOR_PID";
pub fn is_worker() -> bool {
std::env::var_os(SUPERVISOR_ENV).is_some()
}
pub fn request_upgrade() -> bool {
signal_supervisor(Signal::SIGHUP)
}
pub fn notify_ready() {
signal_supervisor(Signal::SIGUSR1);
}
fn signal_supervisor(signal: Signal) -> bool {
let Some(pid) = std::env::var(SUPERVISOR_ENV)
.ok()
.and_then(|pid| pid.parse::<i32>().ok())
else {
return false;
};
nix::sys::signal::kill(Pid::from_raw(pid), signal).is_ok()
}
pub fn upgrade_socket(config: &Config) -> String {
let mut hash = Fnv(0xcbf2_9ce4_8422_2325);
hash.write(config.config_path.as_deref().unwrap_or_default().as_bytes());
std::env::temp_dir()
.join(format!("proksi-upgrade-{:016x}.sock", hash.finish()))
.to_string_lossy()
.into_owned()
}
struct Fnv(u64);
impl Hasher for Fnv {
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, bytes: &[u8]) {
for byte in bytes {
self.0 = (self.0 ^ u64::from(*byte)).wrapping_mul(0x0100_0000_01b3);
}
}
}
pub fn run() -> Result<i32, anyhow::Error> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
runtime.block_on(supervise())
}
async fn supervise() -> Result<i32, anyhow::Error> {
let exe = std::env::current_exe()?;
let args = std::env::args_os().skip(1).collect::<Vec<_>>();
let mut hangup = signal(SignalKind::hangup())?;
let mut ready = signal(SignalKind::user_defined1())?;
let mut terminate = signal(SignalKind::terminate())?;
let mut interrupt = signal(SignalKind::interrupt())?;
let mut workers = Workers::default();
workers.start(&exe, &args)?;
loop {
tokio::select! {
_ = hangup.recv() => {
if workers.upgrading.is_some() {
log("upgrade requested while the new worker is starting: it follows once ready");
workers.pending = true;
} else {
workers.upgrade(&exe, &args);
}
}
_ = ready.recv() => {
workers.upgrading = None;
workers.upgrade_if_pending(&exe, &args);
}
_ = terminate.recv() => workers.signal(Signal::SIGTERM),
_ = interrupt.recv() => workers.signal(Signal::SIGINT),
exited = workers.exited() => {
let Some(code) = exited else { continue };
if workers.running.is_empty() {
return Ok(code);
}
workers.upgrade_if_pending(&exe, &args);
}
}
}
}
#[derive(Default)]
struct Workers {
running: Vec<u32>,
current: Option<u32>,
upgrading: Option<u32>,
pending: bool,
waiting: JoinSet<(u32, std::io::Result<ExitStatus>)>,
}
impl Workers {
fn start(&mut self, exe: &Path, args: &[OsString]) -> Result<u32, anyhow::Error> {
let mut child: Child = Command::new(exe)
.args(args)
.env(SUPERVISOR_ENV, std::process::id().to_string())
.spawn()?;
let pid = child
.id()
.ok_or_else(|| anyhow::anyhow!("the worker exited right away"))?;
self.waiting.spawn(async move { (pid, child.wait().await) });
self.running.push(pid);
self.current = Some(pid);
Ok(pid)
}
fn upgrade(&mut self, exe: &Path, args: &[OsString]) {
match self.start_upgrade(exe, args) {
Ok(pid) => self.upgrading = Some(pid),
Err(err) => log(&format!("could not upgrade: {err}")),
}
}
fn upgrade_if_pending(&mut self, exe: &Path, args: &[OsString]) {
if self.pending && self.upgrading.is_none() {
self.pending = false;
self.upgrade(exe, args);
}
}
fn start_upgrade(&mut self, exe: &Path, args: &[OsString]) -> Result<u32, anyhow::Error> {
let config = crate::config::load("/etc/proksi/configs")
.map_err(|err| anyhow::anyhow!("invalid configuration, keeping the current one: {err}"))?;
crate::config::listeners::listeners(&config).map_err(anyhow::Error::msg)?;
let previous = self.current;
let mut args = args.to_vec();
if !args.iter().any(|arg| arg == "--upgrade" || arg == "-u") {
args.push("--upgrade".into());
}
let pid = self.start(exe, &args)?;
log(&format!("upgrading: started worker {pid}"));
if let Some(previous) = previous {
signal_worker(previous, Signal::SIGQUIT);
}
Ok(pid)
}
fn signal(&self, signal: Signal) {
for pid in &self.running {
signal_worker(*pid, signal);
}
}
async fn exited(&mut self) -> Option<i32> {
if self.waiting.is_empty() {
std::future::pending::<()>().await;
}
let (pid, status) = self.waiting.join_next().await?.ok()?;
self.running.retain(|running| *running != pid);
if self.upgrading == Some(pid) {
self.upgrading = None;
}
if self.current == Some(pid) {
self.current = self.running.last().copied();
}
let code = status.ok().and_then(|status| status.code()).unwrap_or(1);
log(&format!("worker {pid} stopped (exit code {code})"));
Some(code)
}
}
fn signal_worker(pid: u32, signal: Signal) {
if let Ok(pid) = i32::try_from(pid) {
let _ = nix::sys::signal::kill(Pid::from_raw(pid), signal);
}
}
fn log(message: &str) {
eprintln!("proksi supervisor: {message}");
}