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:
@@ -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)],
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user