From a7ae0d86a95ac10f5a1fa1d7568ed4e394c4ab3c Mon Sep 17 00:00:00 2001 From: reverse Date: Sun, 23 Aug 2026 02:10:41 +0000 Subject: [PATCH] rehash: 'echoircd rehash' CLI + SIGHUP reload the running config in place via a pidfile --- .gitignore | 3 +++ Cargo.toml | 2 ++ src/ircd.rs | 21 +++++++++++++++ src/main.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 96 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 550b223..7e71ae5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ bin/ # local backups / scratch config copies *.bak-* echoircd.conf.* + +# runtime pidfile +*.pid diff --git a/Cargo.toml b/Cargo.toml index 6e797ca..ef312ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ name = "echoircd" path = "src/lib.rs" [dependencies] +# SIGHUP → config rehash, without unsafe (the crate keeps signal setup internal). +signal-hook = "0.3" # TLS backend: openssl. The crate keeps all `unsafe` internal, so the daemon # stays `#![forbid(unsafe_code)]`. A pure-Rust `rustls` backend can slot in beside it. openssl = "0.10" diff --git a/src/ircd.rs b/src/ircd.rs index f870d9b..2b472c0 100644 --- a/src/ircd.rs +++ b/src/ircd.rs @@ -101,6 +101,8 @@ pub enum Event { }, /// Background timer tick — drives ping/idle timeouts. Tick, + /// Re-read the config file and apply it live (from SIGHUP / the `rehash` CLI). + Rehash, } /// Insert an extra IRCv3 tag into a wire line's tag block, creating the `@…` @@ -323,6 +325,7 @@ impl Ircd { let _ = reply.send(resp); } Event::Tick => self.on_tick(), + Event::Rehash => self.on_rehash(), } self.drain_hooks(); } @@ -643,6 +646,24 @@ impl Ircd { /// Background timer: PING idle clients, reap the unresponsive and the /// never-registered. + /// SIGHUP / `rehash` CLI: re-read the config and apply it live, keeping the + /// running config if the file can't be read (same policy as /REHASH). + fn on_rehash(&mut self) { + let path = self.server.conf_path.clone(); + eprintln!("rehashing server config file."); + match Config::try_load(&path) { + Some(fresh) => { + self.server.announce("The server is rehashing its configuration."); + self.server.apply_config(fresh); + self.server.announce("Server configuration reloaded."); + eprintln!("server configuration is reloaded."); + } + None => { + eprintln!("rehash: could not read {path} — the running configuration was kept."); + } + } + } + fn on_tick(&mut self) { self.server.ping_links(); // keepalive on every server link self.server.purge_xlines(); // drop expired server bans diff --git a/src/main.rs b/src/main.rs index 33124dd..84fce2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,10 +20,51 @@ use echoircd::ircd::{Event, Ircd}; use echoircd::socketengine; use echoircd::tls::{OpensslBackend, TlsBackend}; +/// `echoircd rehash [config]`: read the running server's pidfile and send it +/// SIGHUP so it reloads its config in place, then exit. No unsafe — the signal is +/// sent via the `kill` command. +fn rehash_cli(cfgpath: &str) -> i32 { + let cfg = Config::load(cfgpath); + let pidfile = cfg + .raw + .get("pidfile") + .and_then(|v| v.first()) + .cloned() + .unwrap_or_else(|| "echoircd.pid".to_string()); + let pid = match std::fs::read_to_string(&pidfile) { + Ok(s) => s.trim().to_string(), + Err(_) => { + eprintln!("echoircd: no pidfile at {pidfile} — is the server running?"); + return 1; + } + }; + if pid.parse::().is_err() { + eprintln!("echoircd: bad pidfile {pidfile} (contents: {pid:?})"); + return 1; + } + println!("rehashing server config file."); + let sent = std::process::Command::new("kill") + .args(["-s", "HUP", &pid]) + .status() + .map(|st| st.success()) + .unwrap_or(false); + if !sent { + eprintln!("echoircd: could not signal pid {pid} — is the server running?"); + return 1; + } + println!("server configuration is reloaded."); + 0 +} + fn main() { - let path = std::env::args() - .nth(1) - .unwrap_or_else(|| "echoircd.conf".to_string()); + let mut args = std::env::args().skip(1); + let first = args.next(); + // `echoircd rehash [config]` signals a running server instead of booting one. + if first.as_deref() == Some("rehash") { + let cfgpath = args.next().unwrap_or_else(|| "echoircd.conf".to_string()); + std::process::exit(rehash_cli(&cfgpath)); + } + let path = first.unwrap_or_else(|| "echoircd.conf".to_string()); let cfg = Config::load(&path); // precompute the bcrypt constants off-thread so the first hash never stalls the core @@ -61,6 +102,17 @@ fn main() { std::process::exit(1); } + // Write a pidfile (default echoircd.pid) so `echoircd rehash` can find us. + let pidfile = cfg + .raw + .get("pidfile") + .and_then(|v| v.first()) + .cloned() + .unwrap_or_else(|| "echoircd.pid".to_string()); + if let Err(e) = std::fs::write(&pidfile, format!("{}\n", std::process::id())) { + eprintln!("echoircd: could not write pidfile {pidfile}: {e}"); + } + // global queue limits (per-class overrides layer on top of these in the reactor) let raw_num = |k: &str, d: usize| { cfg.raw @@ -122,6 +174,21 @@ fn main() { } }); + // SIGHUP → live config rehash (the mechanism the `rehash` CLI uses). + match signal_hook::iterator::Signals::new([signal_hook::consts::SIGHUP]) { + Ok(mut signals) => { + let sig_tx = tx.clone(); + thread::spawn(move || { + for _ in signals.forever() { + if sig_tx.send(Event::Rehash).is_err() { + break; + } + } + }); + } + Err(e) => eprintln!("echoircd: SIGHUP handler unavailable: {e}"), + } + // reactor worker pool: shared by the plaintext acceptor and the direct-TLS // acceptor, so client I/O (framing + TLS crypto) spreads across cores. let reactors =