""" Cross-platform service manager for installing, uninstalling, and restarting services. This module provides a unified interface for managing system services across Windows, Linux, and macOS operating systems. """ from dataclasses import asdict import os import platform import subprocess import ctypes import sys from pathlib import Path from typing import Optional, Protocol from pydantic import BaseModel, Field, field_validator from scientific_surfing.storage import StorageManager class ServiceConfig(BaseModel): """Configuration model for service installation.""" name: str = Field(..., description="Name of the service") executable_path: Path = Field(..., description="Path to the service executable") description: Optional[str] = Field(None, description="Service description") args: Optional[str] = Field(None, description="Command line arguments for the service") @field_validator('name') @classmethod def validate_name(cls, v: str) -> str: """Validate service name format.""" if not v or not v.strip(): raise ValueError("Service name cannot be empty") if ' ' in v.strip(): raise ValueError("Service name cannot contain spaces") return v.strip() @field_validator('executable_path') @classmethod def validate_executable_path(cls, v: Path) -> Path: """Validate executable path exists and is executable.""" if not v.exists(): raise ValueError(f"Executable path does not exist: {v}") if not v.is_file(): raise ValueError(f"Path is not a file: {v}") if not os.access(v, os.X_OK): raise ValueError(f"File is not executable: {v}") return v.resolve() class ServiceManagerProtocol(Protocol): """Protocol defining the interface for service managers.""" config_dir: str def __init__(self, config_dir: str): self.config_dir = config_dir def install(self, config: ServiceConfig) -> None: """Install a service with the given configuration.""" ... def uninstall(self, name: str) -> None: """Uninstall a service by name.""" ... def start(self, name: str) -> None: """Start a service by name.""" ... def stop(self, name: str) -> None: """Stop a service by name.""" ... def restart(self, name: str) -> None: """Restart a service by name.""" ... class WindowsServiceManager(ServiceManagerProtocol): """Windows-specific service manager using sc.exe.""" @staticmethod def _is_admin() -> bool: """Check if the current process has administrator privileges.""" try: return ctypes.windll.shell32.IsUserAnAdmin() except: return False @staticmethod def _format_error_message(operation: str, service_name: str, error: str) -> str: """Format a user-friendly error message for service operations.""" if "access is denied" in error.lower() or "5" in error: return ( f"Failed to {operation} service '{service_name}': Access denied.\n\n" f"Administrator privileges are required to {operation} Windows services.\n\n" f"Solutions:\n" f"• Run this script as administrator (right-click → 'Run as administrator')\n" f"• Open an elevated Command Prompt and run the command manually\n" f"• Ensure User Account Control (UAC) is enabled and accept the prompt" ) elif "1060" in error: return ( f"Failed to {operation} service '{service_name}': Service not found.\n\n" f"The specified service does not exist. Check the service name and try again." ) elif "1062" in error and operation in ["stop", "restart"]: return ( f"Service '{service_name}' is not currently running.\n\n" f"This is not an error - the service was already stopped." ) else: return f"Failed to {operation} service '{service_name}': {error}" @staticmethod def _run_as_admin(cmd: list[str], description: str = "Service Management") -> None: """Run a command with administrator privileges using UAC elevation.""" if WindowsServiceManager._is_admin(): # Already running as admin, execute directly result = subprocess.run(cmd, capture_output=True, text=True, check=True) if result.returncode != 0: raise RuntimeError(f"Failed to {description.lower()}: {result.stderr}") else: # Provide clear instructions for manual elevation command_str = " ".join(cmd) raise RuntimeError( f"Administrator privileges required to {description.lower()}.\n\n" f"Command: {command_str}\n\n" f"Please do one of the following:\n" f"1. Run this script as administrator (right-click → 'Run as administrator')\n" f"2. Open an elevated Command Prompt and run: {command_str}\n" f"3. Accept the UAC prompt when it appears" ) def install(self, config: ServiceConfig) -> None: """Install a Windows service using Python service wrapper.""" import tempfile import json from pathlib import Path from .windows_service_wrapper import WindowServiceConfig # Check for pywin32 dependency try: import win32serviceutil import win32service import win32event import servicemanager except ImportError: raise RuntimeError( "pywin32 package is required for Windows service support. " "Install it with: pip install pywin32" ) # Create service configuration for the wrapper windows_service_config = WindowServiceConfig( name = "mihomo", display_name = "Scentific Surfing Service", description = "Surfing the Internal scientifically", working_dir = str(config.executable_path.parent), bin_path = str(config.executable_path), args = config.args or '', ) # Create permanent config file in a stable location config_dir = self.config_dir config_dir.mkdir(parents=True, exist_ok=True) config_file = config_dir / f"{config.name}_config.json" with open(config_file, 'w') as f: json.dump(asdict(windows_service_config), f, indent=2) # Path to the wrapper script wrapper_script = Path(__file__).parent / "windows_service_wrapper.py" if not wrapper_script.exists(): raise RuntimeError(f"Windows service wrapper not found: {wrapper_script}") # Build the command to run the Python service wrapper using the proper service class python_exe = sys.executable service_cmd = [ python_exe, str(wrapper_script) ] # Quote the path to handle spaces properly escaped_cmd = " ".join(service_cmd) if ' ' in escaped_cmd: escaped_cmd = f'"{escaped_cmd}"' # Create service using sc.exe with the Python wrapper - using proper command format python_exe = sys.executable wrapper_script = Path(__file__).parent / "windows_service_wrapper.py" # Build the command with proper quoting for Windows service_cmd = f'"{python_exe}" "{wrapper_script}" "{config_file}"' # Create service using sc.exe cmd = [ "sc", "create", config.name, "binPath=", service_cmd, "start=", "auto" ] if config.description: cmd.extend(["DisplayName=", config.description]) try: self._run_as_admin(cmd, f"install service '{config.name}'") except RuntimeError as e: # Clean up config file on failure try: config_file.unlink(missing_ok=True) except: pass raise RuntimeError(self._format_error_message("install", config.name, str(e))) def uninstall(self, name: str) -> None: """Uninstall a Windows service.""" import json from pathlib import Path try: # Stop the service first try: self._run_as_admin(["sc", "stop", name], f"stop service '{name}'") except: # Ignore if service is not running pass # Delete the service self._run_as_admin(["sc", "delete", name], f"uninstall service '{name}'") # Clean up configuration file config_dir = Path.home() / ".scientific_surfing" / "service_configs" config_file = config_dir / f"{name}_config.json" try: config_file.unlink(missing_ok=True) # Remove directory if empty try: config_dir.rmdir() except OSError: pass # Directory not empty except: pass # Ignore cleanup errors except RuntimeError as e: raise RuntimeError(self._format_error_message("uninstall", name, str(e))) def start(self, name: str) -> None: """Start a Windows service.""" try: self._run_as_admin(["sc", "start", name], f"start service '{name}'") except RuntimeError as e: raise RuntimeError(self._format_error_message("start", name, str(e))) def stop(self, name: str) -> None: """Stop a Windows service.""" try: self._run_as_admin(["sc", "stop", name], f"stop service '{name}'") except RuntimeError as e: raise RuntimeError(self._format_error_message("stop", name, str(e))) def restart(self, name: str) -> None: """Restart a Windows service.""" try: try: self._run_as_admin(["sc", "stop", name], f"stop service '{name}'") except RuntimeError as e: # Ignore if service is not running (error 1062) if "1062" not in str(e).lower(): raise self._run_as_admin(["sc", "start", name], f"start service '{name}'") except RuntimeError as e: raise RuntimeError(self._format_error_message("restart", name, str(e))) class LinuxServiceManager(ServiceManagerProtocol): """Linux-specific service manager using systemd.""" def _get_service_file_path(self, name: str) -> Path: """Get the path to the systemd service file.""" return Path("/etc/systemd/system") / f"{name}.service" def _create_service_file(self, config: ServiceConfig) -> None: """Create a systemd service file.""" exec_start = f"{config.executable_path}" if config.args: exec_start += f" {config.args}" service_content = f"""[Unit] Description={config.description or config.name} After=network.target [Service] Type=simple ExecStart={exec_start} Restart=always RestartSec=10 User=root [Install] WantedBy=multi-user.target """ service_path = self._get_service_file_path(config.name) try: with open(service_path, 'w') as f: f.write(service_content) # Set appropriate permissions os.chmod(service_path, 0o644) except OSError as e: raise RuntimeError(f"Failed to create service file: {e}") def install(self, config: ServiceConfig) -> None: """Install a Linux service using systemd.""" self._create_service_file(config) try: # Reload systemd subprocess.run(["systemctl", "daemon-reload"], capture_output=True, text=True, check=True) # Enable and start the service subprocess.run(["systemctl", "enable", config.name], capture_output=True, text=True, check=True) subprocess.run(["systemctl", "start", config.name], capture_output=True, text=True, check=True) except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to install service: {e.stderr}") def uninstall(self, name: str) -> None: """Uninstall a Linux service.""" try: # Stop and disable the service subprocess.run(["systemctl", "stop", name], capture_output=True) subprocess.run(["systemctl", "disable", name], capture_output=True, text=True, check=True) # Remove service file service_path = self._get_service_file_path(name) if service_path.exists(): service_path.unlink() # Reload systemd subprocess.run(["systemctl", "daemon-reload"], capture_output=True, text=True, check=True) except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to uninstall service: {e.stderr}") def start(self, name: str) -> None: """Start a Linux service.""" try: result = subprocess.run(["systemctl", "start", name], capture_output=True, text=True, check=True) if result.returncode != 0: raise RuntimeError(f"Failed to start service: {result.stderr}") except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to start service: {e.stderr}") def stop(self, name: str) -> None: """Stop a Linux service.""" try: result = subprocess.run(["systemctl", "stop", name], capture_output=True, text=True, check=True) if result.returncode != 0: raise RuntimeError(f"Failed to stop service: {result.stderr}") except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to stop service: {e.stderr}") def restart(self, name: str) -> None: """Restart a Linux service.""" try: result = subprocess.run(["systemctl", "restart", name], capture_output=True, text=True, check=True) if result.returncode != 0: raise RuntimeError(f"Failed to restart service: {result.stderr}") except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to restart service: {e.stderr}") class MacOSServiceManager(ServiceManagerProtocol): """macOS-specific service manager using launchd.""" def _get_launchd_path(self, name: str) -> Path: """Get the path to the launchd plist file.""" return Path("/Library/LaunchDaemons") / f"com.{name}.plist" def _create_launchd_plist(self, config: ServiceConfig) -> None: """Create a launchd plist file.""" program_args = [str(config.executable_path)] if config.args: program_args.extend(config.args.split()) program_args_xml = "\n".join([f" {arg}" for arg in program_args]) plist_content = f""" Label com.{config.name} ProgramArguments {program_args_xml} RunAtLoad KeepAlive StandardOutPath /var/log/{config.name}.log StandardErrorPath /var/log/{config.name}.error.log """ plist_path = self._get_launchd_path(config.name) try: with open(plist_path, 'w') as f: f.write(plist_content) # Set appropriate permissions os.chmod(plist_path, 0o644) except OSError as e: raise RuntimeError(f"Failed to create launchd plist: {e}") def install(self, config: ServiceConfig) -> None: """Install a macOS service using launchd.""" self._create_launchd_plist(config) try: # Load the service plist_path = self._get_launchd_path(config.name) subprocess.run(["launchctl", "load", str(plist_path)], capture_output=True, text=True, check=True) # Start the service subprocess.run(["launchctl", "start", f"com.{config.name}"], capture_output=True, text=True, check=True) except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to install service: {e.stderr}") def uninstall(self, name: str) -> None: """Uninstall a macOS service.""" try: # Stop and unload the service subprocess.run(["launchctl", "stop", f"com.{name}"], capture_output=True) plist_path = self._get_launchd_path(name) if plist_path.exists(): subprocess.run(["launchctl", "unload", str(plist_path)], capture_output=True, text=True, check=True) plist_path.unlink() except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to uninstall service: {e.stderr}") def start(self, name: str) -> None: """Start a macOS service.""" try: subprocess.run(["launchctl", "start", f"com.{name}"], capture_output=True, text=True, check=True) except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to start service: {e.stderr}") def stop(self, name: str) -> None: """Stop a macOS service.""" try: subprocess.run(["launchctl", "stop", f"com.{name}"], capture_output=True, text=True, check=True) except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to stop service: {e.stderr}") def restart(self, name: str) -> None: """Restart a macOS service.""" try: subprocess.run(["launchctl", "stop", f"com.{name}"], capture_output=True) subprocess.run(["launchctl", "start", f"com.{name}"], capture_output=True, text=True, check=True) except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to restart service: {e.stderr}") class ServiceManager: """Main service manager that delegates to platform-specific implementations.""" def __init__(self, config_dir: str) -> None: """Initialize the service manager with the appropriate platform implementation.""" system = platform.system().lower() if system == "windows": self._manager: ServiceManagerProtocol = WindowsServiceManager(config_dir) elif system == "linux": self._manager = LinuxServiceManager(config_dir) elif system == "darwin": self._manager = MacOSServiceManager(config_dir) else: raise RuntimeError(f"Unsupported operating system: {system}") def install(self, name: str, executable_path: str, description: Optional[str] = None, args: Optional[str] = None) -> None: """ Install a service with the given name and executable path. Args: name: Name of the service to install executable_path: Path to the service executable description: Optional description for the service args: Optional command line arguments for the service Raises: ValueError: If parameters are invalid RuntimeError: If installation fails """ config = ServiceConfig( name=name, executable_path=Path(executable_path), description=description, args=args ) self._manager.install(config) def uninstall(self, name: str) -> None: """ Uninstall a service by name. Args: name: Name of the service to uninstall Raises: RuntimeError: If uninstallation fails """ if not name or not name.strip(): raise ValueError("Service name cannot be empty") self._manager.uninstall(name.strip()) def start(self, name: str) -> None: """ Start a service by name. Args: name: Name of the service to start Raises: ValueError: If service name is invalid RuntimeError: If start fails """ if not name or not name.strip(): raise ValueError("Service name cannot be empty") self._manager.start(name.strip()) def stop(self, name: str) -> None: """ Stop a service by name. Args: name: Name of the service to stop Raises: ValueError: If service name is invalid RuntimeError: If stop fails """ if not name or not name.strip(): raise ValueError("Service name cannot be empty") self._manager.stop(name.strip()) def restart(self, name: str) -> None: """ Restart a service by name. Args: name: Name of the service to restart Raises: ValueError: If service name is invalid RuntimeError: If restart fails """ if not name or not name.strip(): raise ValueError("Service name cannot be empty") self._manager.restart(name.strip()) if __name__ == "__main__": # Example usage import sys if len(sys.argv) < 3: print("Usage: python service_manager.py [executable_path] [description] [args]") sys.exit(1) action = sys.argv[1] service_name = sys.argv[2] storage = StorageManager() service_manager = ServiceManager(storage.config_dir) try: if action == "install": if len(sys.argv) < 4: print("Error: install requires executable_path") sys.exit(1) executable_path = sys.argv[3] description = sys.argv[4] if len(sys.argv) > 4 else None args = sys.argv[5] if len(sys.argv) > 5 else None service_manager.install(service_name, executable_path, description, args) print(f"Service '{service_name}' installed successfully") elif action == "uninstall": service_manager.uninstall(service_name) print(f"Service '{service_name}' uninstalled successfully") elif action == "start": service_manager.start(service_name) print(f"Service '{service_name}' started successfully") elif action == "stop": service_manager.stop(service_name) print(f"Service '{service_name}' stopped successfully") elif action == "restart": service_manager.restart(service_name) print(f"Service '{service_name}' restarted successfully") else: print(f"Error: Unknown action '{action}'") sys.exit(1) except Exception as e: print(f"Error: {e}") sys.exit(1)