feat: Windows 服务 安装,启动,停止 功能

This commit is contained in:
2025-10-18 22:16:54 +08:00
parent b4ce2046a9
commit 6a54381310
14 changed files with 1882 additions and 504 deletions

View File

@ -15,12 +15,14 @@ import requests
from pathlib import Path
from scientific_surfing.corecfg_manager import CoreConfigManager
from scientific_surfing.service_manager import ServiceManager
class CoreManager:
"""Manages user configuration with import, export, and edit operations."""
def __init__(self, core_config_manager: CoreConfigManager):
self.storage = core_config_manager.storage
self.core_config_manager = core_config_manager
def update(self, version: Optional[str] = None, force: bool = False) -> bool:
@ -222,207 +224,181 @@ class CoreManager:
print(f"[ERROR] Upgrade failed: {e}")
return False
def daemon(self, config_path: Optional[str] = None) -> bool:
def get_binary_path(self) -> Path:
"""Get the path to the mihomo executable."""
system = platform.system().lower()
binary_dir = self.core_config_manager.storage.config_dir / "bin"
binary_path = binary_dir / ("mihomo.exe" if system == "windows" else "mihomo")
return binary_path
def install_service(self, service_name: str = "mihomo", description: str = "Mihomo proxy service") -> bool:
"""
Run the mihomo executable as a daemon with the generated configuration.
Install mihomo as a system service.
Args:
config_path: Path to the configuration file. If None, uses generated_config.yaml
service_name: Name for the service (default: "mihomo")
description: Service description
Returns:
bool: True if daemon started successfully, False otherwise.
bool: True if installation successful, False otherwise
"""
try:
# Determine binary path
system = platform.system().lower()
binary_dir = self.core_config_manager.storage.config_dir / "bin"
binary_path = binary_dir / ("mihomo.exe" if system == "windows" else "mihomo")
binary_path = self.get_binary_path()
if not binary_path.exists():
print(f"❌ Mihomo binary not found at: {binary_path}")
print(" Run 'core update' to download the binary first.")
print(" Please run 'core update' first to download the binary")
return False
# Determine config path
if config_path is None:
config_file = self.core_config_manager.storage.config_dir / "generated_config.yaml"
else:
config_file = Path(config_path)
# Setup service arguments
config_dir = self.core_config_manager.storage.config_dir
config_file = config_dir / "generated_config.yaml"
# Ensure config file exists
if not config_file.exists():
print(f"❌ Configuration file not found: {config_file}")
print(" Run 'core-config apply' to generate the configuration first.")
return False
self.core_config_manager.generate_config()
print(f"✅ Generated initial configuration: {config_file}")
print(f"[INFO] Starting mihomo daemon...")
print(f" Binary: {binary_path}")
print(f" Config: {config_file}")
# Build service arguments
service_args = f"-d \"{config_dir}\" -f \"{config_file}\""
# Prepare command
cmd = [
str(binary_path),
"-f", str(config_file),
"-d", str(self.core_config_manager.storage.config_dir)
]
# Start the process
if system == "windows":
# Windows: Use CREATE_NEW_PROCESS_GROUP to avoid console window
creation_flags = subprocess.CREATE_NEW_PROCESS_GROUP if hasattr(subprocess, 'CREATE_NEW_PROCESS_GROUP') else 0
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=creation_flags,
cwd=str(self.core_config_manager.storage.config_dir)
)
else:
# Unix-like systems
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
preexec_fn=os.setsid if hasattr(os, 'setsid') else None,
cwd=str(self.core_config_manager.storage.config_dir)
)
# Check if process started successfully
try:
return_code = process.poll()
if return_code is not None:
stdout, stderr = process.communicate(timeout=2)
print(f"❌ Failed to start daemon (exit code: {return_code})")
if stderr:
print(f" Error: {stderr.decode().strip()}")
return False
except subprocess.TimeoutExpired:
# Process is still running, which is good
pass
# Save PID for later management
pid_file = self.core_config_manager.storage.config_dir / "mihomo.pid"
with open(pid_file, 'w') as f:
f.write(str(process.pid))
print(f"✅ Daemon started successfully (PID: {process.pid})")
print(f" PID file: {pid_file}")
service_manager = ServiceManager(self.storage.config_dir)
service_manager.install(service_name, str(binary_path), description, service_args)
print(f"✅ Service '{service_name}' installed successfully")
print(f" Config directory: {config_dir}")
print(f" Config file: {config_file}")
return True
except Exception as e:
print(f"❌ Failed to start daemon: {e}")
print(f"❌ Failed to install service: {e}")
return False
def stop_daemon(self) -> bool:
def uninstall_service(self, service_name: str = "mihomo") -> bool:
"""
Stop the running mihomo daemon.
Uninstall mihomo system service.
Args:
service_name: Name of the service to uninstall (default: "mihomo")
Returns:
bool: True if daemon stopped successfully, False otherwise.
bool: True if uninstallation successful, False otherwise
"""
try:
pid_file = self.core_config_manager.storage.config_dir / "mihomo.pid"
if not pid_file.exists():
print("❌ No daemon appears to be running (PID file not found)")
return False
with open(pid_file, 'r') as f:
pid = int(f.read().strip())
system = platform.system().lower()
try:
if system == "windows":
# Windows: Use taskkill
subprocess.run(["taskkill", "/F", "/PID", str(pid)],
check=True, capture_output=True, text=True)
else:
# Unix-like systems: Use kill
os.kill(pid, signal.SIGTERM)
# Wait a bit and check if process is still running
try:
os.kill(pid, 0) # Signal 0 just checks if process exists
# Process still exists, try SIGKILL
os.kill(pid, signal.SIGKILL)
except ProcessLookupError:
# Process already terminated
pass
pid_file.unlink()
print(f"✅ Daemon stopped successfully (PID: {pid})")
return True
except (ProcessLookupError, subprocess.CalledProcessError):
# Process not found, clean up PID file
pid_file.unlink()
print(" Daemon was not running, cleaned up PID file")
return True
service_manager = ServiceManager(self.storage.config_dir)
service_manager.uninstall(service_name)
print(f"✅ Service '{service_name}' uninstalled successfully")
return True
except Exception as e:
print(f"❌ Failed to stop daemon: {e}")
print(f"❌ Failed to uninstall service: {e}")
return False
def daemon_status(self) -> Dict[str, Any]:
def start_service(self, service_name: str = "mihomo") -> bool:
"""
Get the status of the mihomo daemon.
Start mihomo system service.
Args:
service_name: Name of the service to start (default: "mihomo")
Returns:
Dict containing daemon status information.
bool: True if start successful, False otherwise
"""
status = {
"running": False,
"pid": None,
"binary_path": None,
"config_path": None,
"error": None
}
try:
pid_file = self.core_config_manager.storage.config_dir / "mihomo.pid"
service_manager = ServiceManager(self.storage.config_dir)
service_manager.start(service_name)
print(f"✅ Service '{service_name}' started successfully")
return True
if not pid_file.exists():
status["error"] = "PID file not found"
return status
except Exception as e:
print(f"❌ Failed to start service: {e}")
return False
with open(pid_file, 'r') as f:
pid = int(f.read().strip())
def stop_service(self, service_name: str = "mihomo") -> bool:
"""
Stop mihomo system service.
# Check if process is running
Args:
service_name: Name of the service to stop (default: "mihomo")
Returns:
bool: True if stop successful, False otherwise
"""
try:
service_manager = ServiceManager(self.storage.config_dir)
service_manager.stop(service_name)
print(f"✅ Service '{service_name}' stopped successfully")
return True
except Exception as e:
print(f"❌ Failed to stop service: {e}")
return False
def restart_service(self, service_name: str = "mihomo") -> bool:
"""
Restart mihomo system service.
Args:
service_name: Name of the service to restart (default: "mihomo")
Returns:
bool: True if restart successful, False otherwise
"""
try:
service_manager = ServiceManager(self.storage.config_dir)
service_manager.restart(service_name)
print(f"✅ Service '{service_name}' restarted successfully")
return True
except Exception as e:
print(f"❌ Failed to restart service: {e}")
return False
def get_service_status(self, service_name: str = "mihomo") -> str:
"""
Get the status of mihomo system service.
Args:
service_name: Name of the service to check (default: "mihomo")
Returns:
str: Service status description
"""
try:
system = platform.system().lower()
try:
if system == "windows":
# Windows: Use tasklist
result = subprocess.run(["tasklist", "/FI", f"PID eq {pid}"],
capture_output=True, text=True)
if str(pid) in result.stdout:
status["running"] = True
status["pid"] = pid
if system == "windows":
result = subprocess.run(["sc", "query", service_name], capture_output=True, text=True)
if result.returncode == 0:
for line in result.stdout.split('\n'):
if "STATE" in line:
return line.strip()
return "Service exists but status unknown"
else:
return "Service not installed or not found"
elif system == "linux":
result = subprocess.run(["systemctl", "is-active", service_name], capture_output=True, text=True)
if result.returncode == 0:
status = result.stdout.strip()
if status == "active":
return "Service is running"
else:
status["error"] = "Process not found"
pid_file.unlink() # Clean up stale PID file
return f"Service is {status}"
else:
# Unix-like systems: Use kill signal 0
os.kill(pid, 0) # Signal 0 just checks if process exists
status["running"] = True
status["pid"] = pid
return "Service not installed or not found"
except (ProcessLookupError, subprocess.CalledProcessError):
status["error"] = "Process not found"
pid_file.unlink() # Clean up stale PID file
elif system == "darwin":
result = subprocess.run(["launchctl", "list"], capture_output=True, text=True)
if service_name in result.stdout or f"com.{service_name}" in result.stdout:
return "Service is loaded (check launchctl status for details)"
else:
return "Service not installed or not loaded"
else:
return f"Unsupported system: {system}"
except Exception as e:
status["error"] = str(e)
return f"Error checking service status: {e}"
# Add binary and config paths
system = platform.system().lower()
binary_path = self.core_config_manager.storage.config_dir / "bin" / ("mihomo.exe" if system == "windows" else "mihomo")
config_path = self.core_config_manager.storage.config_dir / "generated_config.yaml"
status["binary_path"] = str(binary_path) if binary_path.exists() else None
status["config_path"] = str(config_path) if config_path.exists() else None
return status
def deep_merge(dict1, dict2):
for k, v in dict2.items():