From 5fcb4596e4b99b70c09b87f777994817f7211ca9 Mon Sep 17 00:00:00 2001 From: tech Date: Thu, 28 May 2026 17:52:43 +0800 Subject: [PATCH] feat: add CLI entry point with start/stop/status/config subcommands Co-Authored-By: Claude Opus 4.7 --- src/jianda_proxy/cli.py | 169 ++++++++++++++++++++++++++++++++++++++++ tests/test_cli.py | 48 ++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 src/jianda_proxy/cli.py create mode 100644 tests/test_cli.py diff --git a/src/jianda_proxy/cli.py b/src/jianda_proxy/cli.py new file mode 100644 index 0000000..1e00f00 --- /dev/null +++ b/src/jianda_proxy/cli.py @@ -0,0 +1,169 @@ +import argparse +import logging +import os +import sys + +from jianda_proxy.config import ( + load_config, + set_config_value, + get_config_value, + list_config, + get_config_path, + get_log_path, +) +from jianda_proxy.proxy import create_server +from jianda_proxy.daemon import ( + write_pid, + is_running, + stop_daemon, + get_status, + daemonize, + remove_pid, +) + + +def build_parser(): + parser = argparse.ArgumentParser(prog="jianda-proxy", description="HTTP proxy for Gitea LFS") + sub = parser.add_subparsers(dest="command") + + # start + p_start = sub.add_parser("start", help="Start the proxy daemon") + p_start.add_argument("--foreground", action="store_true", help="Run in foreground (no daemon)") + p_start.add_argument("--port", type=str, default=None, help="Override listen port") + + # stop + sub.add_parser("stop", help="Stop the proxy daemon") + + # status + sub.add_parser("status", help="Show proxy daemon status") + + # config + p_config = sub.add_parser("config", help="Manage configuration") + config_sub = p_config.add_subparsers(dest="config_command") + + p_set = config_sub.add_parser("set", help="Set a config value") + p_set.add_argument("key", help="Config key") + p_set.add_argument("value", help="Config value") + + p_get = config_sub.add_parser("get", help="Get a config value") + p_get.add_argument("key", help="Config key") + + config_sub.add_parser("list", help="List all config values") + config_sub.add_parser("path", help="Show config file path") + + return parser + + +def cmd_start(args): + if is_running(): + print("Error: jianda-proxy is already running.", file=sys.stderr) + sys.exit(1) + + config = load_config() + config_dict = dict(config["proxy"]) + + if args.port: + config_dict["listen_port"] = args.port + + if args.foreground: + _run_foreground(config_dict) + else: + _run_daemonized(config_dict) + + +def _run_foreground(config_dict): + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(name)s] %(levelname)s - %(message)s", + ) + server = create_server(config_dict) + try: + server.serve_forever() + except KeyboardInterrupt: + pass + finally: + server.server_close() + + +def _run_daemonized(config_dict): + from logging.handlers import RotatingFileHandler + + log_path = get_log_path() + handler = RotatingFileHandler(log_path, maxBytes=5 * 1024 * 1024, backupCount=3) + handler.setFormatter( + logging.Formatter("%(asctime)s [%(name)s] %(levelname)s - %(message)s") + ) + root = logging.getLogger() + root.setLevel(logging.INFO) + root.addHandler(handler) + + daemonize() + + pid = os.getpid() + write_pid(pid) + + server = create_server(config_dict) + try: + server.serve_forever() + finally: + server.server_close() + remove_pid() + + +def cmd_stop(_args): + result = stop_daemon() + if result == "not_running": + print("jianda-proxy is not running.", file=sys.stderr) + sys.exit(1) + else: + print("jianda-proxy stopped.") + + +def cmd_status(_args): + status = get_status() + if status["running"]: + print(f"jianda-proxy is running (PID: {status['pid']})") + else: + print("jianda-proxy is not running.") + + +def cmd_config(args): + if args.config_command == "set": + set_config_value(args.key, args.value) + print(f"{args.key} = {args.value}") + elif args.config_command == "get": + print(get_config_value(args.key)) + elif args.config_command == "list": + for k, v in list_config().items(): + print(f"{k} = {v}") + elif args.config_command == "path": + print(get_config_path()) + else: + print("Unknown config command. Use set/get/list/path.", file=sys.stderr) + sys.exit(1) + + +def main(): + parser = build_parser() + args = parser.parse_args() + + if not args.command: + parser.print_help() + sys.exit(1) + + commands = { + "start": cmd_start, + "stop": cmd_stop, + "status": cmd_status, + "config": cmd_config, + } + fn = commands.get(args.command) + if fn: + fn(args) + else: + parser.print_help() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..5119420 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,48 @@ +import pytest +from jianda_proxy.cli import build_parser + + +class TestParser: + def test_start_subcommand_exists(self): + parser = build_parser() + args = parser.parse_args(["start"]) + assert args.command == "start" + assert args.foreground is False + + def test_start_foreground_flag(self): + parser = build_parser() + args = parser.parse_args(["start", "--foreground"]) + assert args.foreground is True + + def test_stop_subcommand(self): + parser = build_parser() + args = parser.parse_args(["stop"]) + assert args.command == "stop" + + def test_status_subcommand(self): + parser = build_parser() + args = parser.parse_args(["status"]) + assert args.command == "status" + + def test_config_set(self): + parser = build_parser() + args = parser.parse_args(["config", "set", "listen_port", "8080"]) + assert args.config_command == "set" + assert args.key == "listen_port" + assert args.value == "8080" + + def test_config_get(self): + parser = build_parser() + args = parser.parse_args(["config", "get", "listen_port"]) + assert args.config_command == "get" + assert args.key == "listen_port" + + def test_config_list(self): + parser = build_parser() + args = parser.parse_args(["config", "list"]) + assert args.config_command == "list" + + def test_config_path(self): + parser = build_parser() + args = parser.parse_args(["config", "path"]) + assert args.config_command == "path"