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",
|
"-S", "--proxy-source", metavar="URL",
|
||||||
help="proxy source API 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(
|
p.add_argument(
|
||||||
"--cprofile", metavar="FILE", nargs="?", const="s5p.prof",
|
"--cprofile", metavar="FILE", nargs="?", const="s5p.prof",
|
||||||
help="enable cProfile, dump stats to FILE (default: 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:
|
if args.max_connections is not None:
|
||||||
config.max_connections = args.max_connections
|
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:
|
if args.proxy_source:
|
||||||
config.proxy_pool = ProxyPoolConfig(
|
config.proxy_pool = ProxyPoolConfig(
|
||||||
sources=[PoolSourceConfig(url=args.proxy_source)],
|
sources=[PoolSourceConfig(url=args.proxy_source)],
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ class Config:
|
|||||||
max_connections: int = 256
|
max_connections: int = 256
|
||||||
pool_size: int = 0
|
pool_size: int = 0
|
||||||
pool_max_idle: float = 30.0
|
pool_max_idle: float = 30.0
|
||||||
|
api_host: str = ""
|
||||||
|
api_port: int = 0
|
||||||
proxy_pool: ProxyPoolConfig | None = None
|
proxy_pool: ProxyPoolConfig | None = None
|
||||||
config_file: str = ""
|
config_file: str = ""
|
||||||
|
|
||||||
@@ -148,6 +150,15 @@ def load_config(path: str | Path) -> Config:
|
|||||||
if "pool_max_idle" in raw:
|
if "pool_max_idle" in raw:
|
||||||
config.pool_max_idle = float(raw["pool_max_idle"])
|
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:
|
if "chain" in raw:
|
||||||
for entry in raw["chain"]:
|
for entry in raw["chain"]:
|
||||||
if isinstance(entry, str):
|
if isinstance(entry, str):
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import signal
|
|||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from .api import start_api
|
||||||
from .config import Config, load_config
|
from .config import Config, load_config
|
||||||
from .connpool import FirstHopPool
|
from .connpool import FirstHopPool
|
||||||
from .metrics import Metrics
|
from .metrics import Metrics
|
||||||
@@ -259,6 +260,16 @@ async def serve(config: Config) -> None:
|
|||||||
)
|
)
|
||||||
logger.info(" retries: %d", config.retries)
|
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)
|
# SIGHUP: hot-reload config (timeout, retries, log_level, pool settings)
|
||||||
async def _reload() -> None:
|
async def _reload() -> None:
|
||||||
if not config.config_file:
|
if not config.config_file:
|
||||||
@@ -289,6 +300,10 @@ async def serve(config: Config) -> None:
|
|||||||
|
|
||||||
loop.add_signal_handler(signal.SIGHUP, _on_sighup)
|
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()
|
metrics_stop = asyncio.Event()
|
||||||
pool_ref = proxy_pool
|
pool_ref = proxy_pool
|
||||||
metrics_task = asyncio.create_task(_metrics_logger(metrics, metrics_stop, pool_ref))
|
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:
|
async with srv:
|
||||||
sig = await stop
|
sig = await stop
|
||||||
logger.info("received %s, shutting down", signal.Signals(sig).name)
|
logger.info("received %s, shutting down", signal.Signals(sig).name)
|
||||||
|
if api_srv:
|
||||||
|
api_srv.close()
|
||||||
|
await api_srv.wait_closed()
|
||||||
if hop_pool:
|
if hop_pool:
|
||||||
await hop_pool.stop()
|
await hop_pool.stop()
|
||||||
if proxy_pool:
|
if proxy_pool:
|
||||||
|
|||||||
Reference in New Issue
Block a user