""" User configuration manager for scientific-surfing. Handles user preferences with import, export, and edit operations. """ import os import platform import gzip import zipfile import shutil import subprocess import signal from typing import Optional, Dict, Any 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: """ Download and update mihomo binary from GitHub releases. Args: version: Specific version to download (e.g., 'v1.18.5'). If None, downloads latest. force: Force download even if binary already exists. Returns: bool: True if update successful, False otherwise. """ try: # Determine current OS and architecture system = platform.system().lower() machine = platform.machine().lower() # Map platform to mihomo binary naming (base name without extension) platform_map = { 'windows': { 'amd64': 'mihomo-windows-amd64', '386': 'mihomo-windows-386', 'arm64': 'mihomo-windows-arm64', 'arm': 'mihomo-windows-arm32v7' }, 'linux': { 'amd64': 'mihomo-linux-amd64', '386': 'mihomo-linux-386', 'arm64': 'mihomo-linux-arm64', 'arm': 'mihomo-linux-armv7' }, 'darwin': { 'amd64': 'mihomo-darwin-amd64', 'arm64': 'mihomo-darwin-arm64' } } # Normalize architecture names arch_map = { 'x86_64': 'amd64', 'amd64': 'amd64', 'i386': '386', 'i686': '386', 'arm64': 'arm64', 'aarch64': 'arm64', 'armv7l': 'arm', 'arm': 'arm' } if system not in platform_map: print(f"❌ Unsupported operating system: {system}") return False normalized_arch = arch_map.get(machine, machine) if normalized_arch not in platform_map[system]: print(f"❌ Unsupported architecture: {machine} ({normalized_arch})") return False binary_name = platform_map[system][normalized_arch] # Setup directories binary_dir = self.core_config_manager.storage.config_dir / "bin" binary_dir.mkdir(parents=True, exist_ok=True) binary_path = binary_dir / ("mihomo.exe" if system == "windows" else "mihomo") # Check if binary already exists if binary_path.exists() and not force: print(f"ℹ️ Binary already exists at: {binary_path}") print(" Use --force to overwrite") return True # Get release info if version: # Specific version release_url = f"https://api.github.com/repos/MetaCubeX/mihomo/releases/tags/{version}" else: # Latest release release_url = "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest" print(f"[INFO] Fetching release info from: {release_url}") headers = { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'scientific-surfing/1.0' } response = requests.get(release_url, headers=headers, timeout=30) response.raise_for_status() release_data = response.json() release_version = release_data['tag_name'] print(f"[INFO] Found release: {release_version}") # Find the correct asset assets = release_data.get('assets', []) target_asset = None # Determine file extension based on system file_extension = '.zip' if system == 'windows' else '.gz' expected_filename = f"{binary_name}-{release_version}{file_extension}" # Look for exact match first for asset in assets: if asset['name'] == expected_filename: target_asset = asset break # Fallback to prefix matching if exact match not found if not target_asset: binary_name_prefix = f"{binary_name}-{release_version}" for asset in assets: if asset['name'].startswith(binary_name_prefix) and (asset['name'].endswith('.gz') or asset['name'].endswith('.zip')): target_asset = asset break if not target_asset: print(f"[ERROR] Binary not found for {system}/{normalized_arch}: {expected_filename}") print("Available binaries:") for asset in assets: if 'mihomo' in asset['name'] and (asset['name'].endswith('.gz') or asset['name'].endswith('.zip')): print(f" - {asset['name']}") return False # Download the compressed file download_url = target_asset['browser_download_url'] compressed_filename = target_asset['name'] print(f"[DOWNLOAD] Downloading: {compressed_filename}") print(f" Size: {target_asset['size']:,} bytes") download_response = requests.get(download_url, stream=True, timeout=60) download_response.raise_for_status() # Download to temporary file temp_compressed_path = binary_path.with_suffix(f".tmp{file_extension}") temp_extracted_path = binary_path.with_suffix('.tmp') with open(temp_compressed_path, 'wb') as f: for chunk in download_response.iter_content(chunk_size=8192): if chunk: f.write(chunk) # Verify download if temp_compressed_path.stat().st_size != target_asset['size']: temp_compressed_path.unlink() print("[ERROR] Download verification failed - size mismatch") return False # Extract the binary try: if file_extension == '.gz': # Extract .gz file with gzip.open(temp_compressed_path, 'rb') as f_in: with open(temp_extracted_path, 'wb') as f_out: shutil.copyfileobj(f_in, f_out) elif file_extension == '.zip': # Extract .zip file with zipfile.ZipFile(temp_compressed_path, 'r') as zip_ref: # Find the executable file in the zip file_info = zip_ref.filelist[0] with zip_ref.open(file_info.filename) as f_in: with open(temp_extracted_path, 'wb') as f_out: shutil.copyfileobj(f_in, f_out) else: raise ValueError(f"Unsupported file format: {file_extension}") except Exception as e: temp_compressed_path.unlink() if temp_extracted_path.exists(): temp_extracted_path.unlink() print(f"[ERROR] Failed to extract binary: {e}") return False # Clean up compressed file temp_compressed_path.unlink() # Make executable on Unix-like systems if system != 'windows': os.chmod(temp_extracted_path, 0o755) # Move to final location if binary_path.exists(): backup_path = binary_path.with_suffix('.backup') binary_path.rename(backup_path) print(f"[INFO] Backed up existing binary to: {backup_path}") temp_extracted_path.rename(binary_path) print(f"[SUCCESS] Successfully updated mihomo {release_version}") print(f" Location: {binary_path}") print(f" Size: {binary_path.stat().st_size:,} bytes") return True except requests.exceptions.RequestException as e: print(f"[ERROR] Network error: {e}") return False except Exception as e: print(f"[ERROR] Upgrade failed: {e}") return False 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: """ Install mihomo as a system service. Args: service_name: Name for the service (default: "mihomo") description: Service description Returns: bool: True if installation successful, False otherwise """ try: binary_path = self.get_binary_path() if not binary_path.exists(): print(f"❌ Mihomo binary not found at: {binary_path}") print(" Please run 'core update' first to download the binary") return False # 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(): self.core_config_manager.generate_config() print(f"✅ Generated initial configuration: {config_file}") # Build service arguments service_args = f"-d \"{config_dir}\" -f \"{config_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 install service: {e}") return False def uninstall_service(self, service_name: str = "mihomo") -> bool: """ Uninstall mihomo system service. Args: service_name: Name of the service to uninstall (default: "mihomo") Returns: bool: True if uninstallation successful, False otherwise """ try: 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 uninstall service: {e}") return False def start_service(self, service_name: str = "mihomo") -> bool: """ Start mihomo system service. Args: service_name: Name of the service to start (default: "mihomo") Returns: bool: True if start successful, False otherwise """ try: service_manager = ServiceManager(self.storage.config_dir) service_manager.start(service_name) print(f"✅ Service '{service_name}' started successfully") return True except Exception as e: print(f"❌ Failed to start service: {e}") return False def stop_service(self, service_name: str = "mihomo") -> bool: """ Stop mihomo system service. 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() 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: return f"Service is {status}" else: return "Service not installed or not found" 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: return f"Error checking service status: {e}" def deep_merge(dict1, dict2): for k, v in dict2.items(): if k in dict1 and isinstance(dict1[k], dict) and isinstance(v, dict): dict1[k] = deep_merge(dict1[k], v) elif k in dict1 and isinstance(dict1[k], list) and isinstance(v, list): dict1[k].extend(v) # Example: extend lists. Adjust logic for other list merging needs. else: dict1[k] = v return dict1