From 818169758b2b4619664f1a4e9fbd422cae2cb976 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 15 Feb 2026 16:02:57 +0100 Subject: [PATCH] feat: add SIGHUP hot config reload On SIGHUP, re-read the YAML config file and update mutable runtime settings: timeout, retries, log_level, and pool config (sources, intervals, thresholds). Pool triggers an immediate source re-fetch. Listen address and chain require restart. --- src/s5p/cli.py | 2 ++ src/s5p/config.py | 1 + src/s5p/pool.py | 7 +++++++ src/s5p/server.py | 28 +++++++++++++++++++++++++++- 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/s5p/cli.py b/src/s5p/cli.py index 7422c81..6c46027 100644 --- a/src/s5p/cli.py +++ b/src/s5p/cli.py @@ -62,6 +62,8 @@ def main(argv: list[str] | None = None) -> int: args = _parse_args(argv) config = load_config(args.config) if args.config else Config() + if args.config: + config.config_file = args.config if args.listen: if ":" in args.listen: diff --git a/src/s5p/config.py b/src/s5p/config.py index 690819e..16fe35d 100644 --- a/src/s5p/config.py +++ b/src/s5p/config.py @@ -74,6 +74,7 @@ class Config: log_level: str = "info" proxy_source: ProxySourceConfig | None = None proxy_pool: ProxyPoolConfig | None = None + config_file: str = "" def parse_proxy_url(url: str) -> ChainHop: diff --git a/src/s5p/pool.py b/src/s5p/pool.py index 0fb44ce..1c5d1ba 100644 --- a/src/s5p/pool.py +++ b/src/s5p/pool.py @@ -90,6 +90,13 @@ class ProxyPool: self._tasks.append(asyncio.create_task(self._refresh_loop())) self._tasks.append(asyncio.create_task(self._health_loop())) + async def reload(self, cfg: ProxyPoolConfig) -> None: + """Update pool config and trigger source re-fetch.""" + self._cfg = cfg + logger.info("pool: config reloaded, re-fetching sources") + await self._fetch_all_sources() + self._save_state() + async def stop(self) -> None: """Cancel background tasks and save state.""" self._stop.set() diff --git a/src/s5p/server.py b/src/s5p/server.py index 444b3d5..9f19ff2 100644 --- a/src/s5p/server.py +++ b/src/s5p/server.py @@ -8,7 +8,7 @@ import signal import struct import time -from .config import Config +from .config import Config, load_config from .metrics import Metrics from .pool import ProxyPool from .proto import ProtoError, Socks5Reply, build_chain, read_socks5_address @@ -253,6 +253,32 @@ async def serve(config: Config) -> None: for sig in (signal.SIGTERM, signal.SIGINT): loop.add_signal_handler(sig, lambda s=sig: stop.set_result(s)) + # SIGHUP: hot-reload config (timeout, retries, log_level, pool settings) + async def _reload() -> None: + if not config.config_file: + logger.warning("reload: no config file specified, ignoring SIGHUP") + return + try: + new = load_config(config.config_file) + except Exception as e: + logger.warning("reload: failed to read config: %s", e) + return + config.timeout = new.timeout + config.retries = new.retries + if new.log_level != config.log_level: + config.log_level = new.log_level + logging.getLogger("s5p").setLevel( + getattr(logging, new.log_level.upper(), logging.INFO), + ) + if isinstance(proxy_pool, ProxyPool) and new.proxy_pool: + await proxy_pool.reload(new.proxy_pool) + logger.info("reload: config reloaded") + + def _on_sighup() -> None: + asyncio.ensure_future(_reload()) + + loop.add_signal_handler(signal.SIGHUP, _on_sighup) + metrics_stop = asyncio.Event() pool_ref = proxy_pool if isinstance(proxy_pool, ProxyPool) else None metrics_task = asyncio.create_task(_metrics_logger(metrics, metrics_stop, pool_ref))