From e799ea011f4e590a0bf584fab31cd172b7026784 Mon Sep 17 00:00:00 2001 From: Klesh Wong Date: Fri, 6 Mar 2026 11:44:23 +0800 Subject: [PATCH] feat: add reload service command and update command handling --- ss/cli.py | 36 ++++++++++++++++--------- ss/core_manager.py | 67 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 13 deletions(-) diff --git a/ss/cli.py b/ss/cli.py index 80d818f..454aafb 100644 --- a/ss/cli.py +++ b/ss/cli.py @@ -65,7 +65,7 @@ def create_parser() -> argparse.ArgumentParser: update_parser.add_argument('--force', action='store_true', help='Force update even if binary already exists') # Config commands - config_parser = core_subparsers.add_parser('config', help='Manage core configuration') + config_parser = subparsers.add_parser('config', help='Manage core configuration') config_subparsers = config_parser.add_subparsers(dest='config_command', help='Configuration operations') # Import config @@ -89,7 +89,7 @@ def create_parser() -> argparse.ArgumentParser: apply_parser = config_subparsers.add_parser('apply', help='Apply active subscription to generate final config') # Service management commands - service_parser = core_subparsers.add_parser('service', help='Manage mihomo as a system service') + service_parser = subparsers.add_parser('service', help='Manage mihomo as a system service') service_subparsers = service_parser.add_subparsers(dest='service_command', help='Service operations') # Install service command @@ -113,6 +113,10 @@ def create_parser() -> argparse.ArgumentParser: restart_service_parser = service_subparsers.add_parser('restart', help='Restart mihomo system service') restart_service_parser.add_argument('--name', default='mihomo', help='Service name (default: mihomo)') + # Reload service command + reload_service_parser = service_subparsers.add_parser('reload', help='Reload mihomo service configuration (via API)') + reload_service_parser.add_argument('--name', default='mihomo', help='Service name (default: mihomo)') + # Status service command status_service_parser = service_subparsers.add_parser('status', help='Check mihomo system service status') status_service_parser.add_argument('--name', default='mihomo', help='Service name (default: mihomo)') @@ -138,7 +142,7 @@ def create_parser() -> argparse.ArgumentParser: return parser -def handle_subscription_command(args, subscription_manager: SubscriptionManager, core_config_manager: CoreConfigManager, parser: argparse.ArgumentParser) -> None: +def handle_subscription_command(args, subscription_manager: SubscriptionManager, core_config_manager: CoreConfigManager, core_manager: CoreManager, parser: argparse.ArgumentParser) -> None: """Handle subscription related commands.""" if not hasattr(args, 'subcommand') or not args.subcommand: parser.parse_args(['subscription', '--help']) @@ -156,7 +160,9 @@ def handle_subscription_command(args, subscription_manager: SubscriptionManager, subscription_manager.set_subscription_url(args.name, args.url) elif args.subcommand == 'activate': subscription_manager.activate_subscription(args.name) - core_config_manager.apply() + if core_config_manager.apply(): + # Reload service if config applied successfully + core_manager.reload_service() elif args.subcommand == 'list': subscription_manager.list_subscriptions() elif args.subcommand == 'storage': @@ -173,10 +179,6 @@ def handle_core_command(args, core_manager: CoreManager, core_config_manager: Co if args.core_command == 'update': core_manager.update(version=args.version, force=args.force) - elif args.core_command == 'config': - handle_config_command(args, core_config_manager, parser) - elif args.core_command == 'service': - handle_service_command(args, core_manager, parser) else: parser.parse_args(['core', '--help']) @@ -184,7 +186,7 @@ def handle_core_command(args, core_manager: CoreManager, core_config_manager: Co def handle_config_command(args, core_config_manager: CoreConfigManager, parser: argparse.ArgumentParser) -> None: """Handle configuration commands.""" if not hasattr(args, 'config_command') or not args.config_command: - parser.parse_args(['core', 'config', '--help']) + parser.parse_args(['config', '--help']) return if args.config_command == 'import': @@ -200,13 +202,13 @@ def handle_config_command(args, core_config_manager: CoreConfigManager, parser: elif args.config_command == 'apply': core_config_manager.apply() else: - parser.parse_args(['core', 'config', '--help']) + parser.parse_args(['config', '--help']) def handle_service_command(args, core_manager: CoreManager, parser: argparse.ArgumentParser) -> None: """Handle service commands.""" if not hasattr(args, 'service_command') or not args.service_command: - parser.parse_args(['core', 'service', '--help']) + parser.parse_args(['service', '--help']) return if args.service_command == 'install': @@ -232,11 +234,15 @@ def handle_service_command(args, core_manager: CoreManager, parser: argparse.Arg success = core_manager.restart_service(service_name=args.name) if not success: sys.exit(1) + elif args.service_command == 'reload': + success = core_manager.reload_service(service_name=args.name) + if not success: + sys.exit(1) elif args.service_command == 'status': status = core_manager.get_service_status(service_name=args.name) print(f"Service '{args.name}' status: {status}") else: - parser.parse_args(['core', 'service', '--help']) + parser.parse_args(['service', '--help']) def handle_hook_command(args, hook_manager: HookManager, parser: argparse.ArgumentParser) -> None: @@ -274,9 +280,13 @@ def main() -> None: try: if args.command == 'subscription': - handle_subscription_command(args, subscription_manager, core_config_manager, parser) + handle_subscription_command(args, subscription_manager, core_config_manager, core_manager, parser) elif args.command == 'core': handle_core_command(args, core_manager, core_config_manager, parser) + elif args.command == 'config': + handle_config_command(args, core_config_manager, parser) + elif args.command == 'service': + handle_service_command(args, core_manager, parser) elif args.command == 'hook': handle_hook_command(args, hook_manager, parser) else: diff --git a/ss/core_manager.py b/ss/core_manager.py index 87ebdff..b6273a6 100644 --- a/ss/core_manager.py +++ b/ss/core_manager.py @@ -433,6 +433,73 @@ class CoreManager: except Exception as e: return f"Error checking service status: {e}" + def reload_service(self, service_name: str = "mihomo") -> bool: + """ + Reload mihomo configuration via external controller API without restarting service. + + Args: + service_name: Name of the service (default: "mihomo") + + Returns: + bool: True if reload successful, False otherwise. + """ + config_path = self.core_config_manager.storage.config_dir / "generated_config.yaml" + if not config_path.exists(): + print(f"❌ Configuration file not found: {config_path}") + return False + + try: + # Parse generated config to find external-controller + import yaml + with open(config_path, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + + external_controller = config.get('external-controller', '127.0.0.1:9090') + secret = config.get('secret', '') + + # Format API URL + if not external_controller.startswith('http'): + base_url = f"http://{external_controller}" + else: + base_url = external_controller + + # Prepare request + url = f"{base_url}/configs?force=true" + headers = { + 'Content-Type': 'application/json' + } + if secret: + headers['Authorization'] = f"Bearer {secret}" + + payload = { + "path": str(config_path.absolute()), + "payload": "" # Empty payload suggests reload from path + } + + # Send reload request + print(f"🔄 Reloading configuration via API: {url}") + # Short timeout as local controller should respond quickly + response = requests.put(url, json=payload, headers=headers, timeout=2) + + if response.status_code == 204: + print(f"✅ Configuration reloaded successfully") + return True + else: + print(f"❌ Failed to reload configuration: HTTP {response.status_code}") + print(f" Response: {response.text}") + return False + + except requests.exceptions.RequestException as e: + print(f"❌ Failed to connect to external controller: {e}") + print(" Is the service running?") + return False + except ImportError: + print("❌ PyYAML is required to parse configuration.") + return False + except Exception as e: + print(f"❌ Error reloading service: {e}") + return False + def deep_merge(dict1, dict2): for k, v in dict2.items():