feat: wire control API into server and config

Add api_host/api_port to Config dataclass, parse api_listen key in
load_config(), add --api [HOST:]PORT CLI flag. Start/stop API server
in serve() alongside the SOCKS5 listener.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-16 19:03:44 +01:00
parent ecf9a840e4
commit b72d083f56
3 changed files with 41 additions and 0 deletions

View File

@@ -54,6 +54,10 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"-S", "--proxy-source", metavar="URL",
help="proxy source API URL",
)
p.add_argument(
"--api", metavar="[HOST:]PORT",
help="enable control API on address (e.g. 127.0.0.1:1081)",
)
p.add_argument(
"--cprofile", metavar="FILE", nargs="?", const="s5p.prof",
help="enable cProfile, dump stats to FILE (default: s5p.prof)",
@@ -89,6 +93,14 @@ def main(argv: list[str] | None = None) -> int:
if args.max_connections is not None:
config.max_connections = args.max_connections
if args.api:
if ":" in args.api:
host, port_str = args.api.rsplit(":", 1)
config.api_host = host
config.api_port = int(port_str)
else:
config.api_port = int(args.api)
if args.proxy_source:
config.proxy_pool = ProxyPoolConfig(
sources=[PoolSourceConfig(url=args.proxy_source)],

View File

@@ -66,6 +66,8 @@ class Config:
max_connections: int = 256
pool_size: int = 0
pool_max_idle: float = 30.0
api_host: str = ""
api_port: int = 0
proxy_pool: ProxyPoolConfig | None = None
config_file: str = ""
@@ -148,6 +150,15 @@ def load_config(path: str | Path) -> Config:
if "pool_max_idle" in raw:
config.pool_max_idle = float(raw["pool_max_idle"])
if "api_listen" in raw:
api = raw["api_listen"]
if isinstance(api, str) and ":" in api:
host, port_str = api.rsplit(":", 1)
config.api_host = host
config.api_port = int(port_str)
elif isinstance(api, (str, int)):
config.api_port = int(api)
if "chain" in raw:
for entry in raw["chain"]:
if isinstance(entry, str):

View File

@@ -8,6 +8,7 @@ import signal
import struct
import time
from .api import start_api
from .config import Config, load_config
from .connpool import FirstHopPool
from .metrics import Metrics
@@ -259,6 +260,16 @@ async def serve(config: Config) -> None:
)
logger.info(" retries: %d", config.retries)
# -- control API ---------------------------------------------------------
api_srv: asyncio.Server | None = None
if config.api_port:
api_ctx: dict = {
"config": config,
"metrics": metrics,
"pool": proxy_pool,
"hop_pool": hop_pool,
}
# SIGHUP: hot-reload config (timeout, retries, log_level, pool settings)
async def _reload() -> None:
if not config.config_file:
@@ -289,6 +300,10 @@ async def serve(config: Config) -> None:
loop.add_signal_handler(signal.SIGHUP, _on_sighup)
if config.api_port:
api_ctx["reload_fn"] = _reload
api_srv = await start_api(config.api_host, config.api_port, api_ctx)
metrics_stop = asyncio.Event()
pool_ref = proxy_pool
metrics_task = asyncio.create_task(_metrics_logger(metrics, metrics_stop, pool_ref))
@@ -296,6 +311,9 @@ async def serve(config: Config) -> None:
async with srv:
sig = await stop
logger.info("received %s, shutting down", signal.Signals(sig).name)
if api_srv:
api_srv.close()
await api_srv.wait_closed()
if hop_pool:
await hop_pool.stop()
if proxy_pool: