Files
scientific-surfing/scientific_surfing/core_manager.py

411 lines
15 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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