Files
ss/internal/core/manager.go

459 lines
12 KiB
Go
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.

// Package core manages the mihomo core binary (download/update) and its
// system service lifecycle (install/start/stop/reload/status).
package core
import (
"archive/zip"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"gitea.epss.net.cn/klesh/ss/internal/corecfg"
"gitea.epss.net.cn/klesh/ss/internal/service"
"gitea.epss.net.cn/klesh/ss/internal/yamlutil"
)
// Manager manages the mihomo core binary and its OS service.
type Manager struct {
CoreConfigManager *corecfg.Manager
}
// New creates a core Manager.
func New(coreConfigManager *corecfg.Manager) *Manager {
return &Manager{CoreConfigManager: coreConfigManager}
}
func (m *Manager) binDir() string {
return filepath.Join(m.CoreConfigManager.Storage.ConfigDir, "bin")
}
// GetBinaryPath returns the path to the mihomo executable for this platform.
func (m *Manager) GetBinaryPath() string {
return binaryPath(m.binDir())
}
type githubAsset struct {
Name string `json:"name"`
Size int64 `json:"size"`
BrowserDownloadURL string `json:"browser_download_url"`
}
type githubRelease struct {
TagName string `json:"tag_name"`
Assets []githubAsset `json:"assets"`
}
// Update downloads and installs the mihomo binary from GitHub releases.
// Direct port of CoreManager.update.
func (m *Manager) Update(version string, force bool) bool {
system, arch, binaryName, ok := platformInfo()
if !ok {
fmt.Printf("❌ Unsupported operating system/architecture: %s/%s\n", runtime.GOOS, runtime.GOARCH)
return false
}
binDir := m.binDir()
if err := os.MkdirAll(binDir, 0o755); err != nil {
fmt.Printf("[ERROR] Failed to create bin directory: %v\n", err)
return false
}
path := binaryPath(binDir)
if _, err := os.Stat(path); err == nil && !force {
fmt.Printf(" Binary already exists at: %s\n", path)
fmt.Println(" Use --force to overwrite")
return true
}
var releaseURL string
if version != "" {
releaseURL = fmt.Sprintf("https://api.github.com/repos/MetaCubeX/mihomo/releases/tags/%s", version)
} else {
releaseURL = "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest"
}
fmt.Printf("[INFO] Fetching release info from: %s\n", releaseURL)
release, err := fetchRelease(releaseURL)
if err != nil {
fmt.Printf("[ERROR] %v\n", err)
return false
}
fmt.Printf("[INFO] Found release: %s\n", release.TagName)
fileExtension := ".gz"
if system == "windows" {
fileExtension = ".zip"
}
expectedFilename := fmt.Sprintf("%s-%s%s", binaryName, release.TagName, fileExtension)
var targetAsset *githubAsset
for i := range release.Assets {
if release.Assets[i].Name == expectedFilename {
targetAsset = &release.Assets[i]
break
}
}
if targetAsset == nil {
prefix := fmt.Sprintf("%s-%s", binaryName, release.TagName)
for i := range release.Assets {
name := release.Assets[i].Name
if strings.HasPrefix(name, prefix) && (strings.HasSuffix(name, ".gz") || strings.HasSuffix(name, ".zip")) {
targetAsset = &release.Assets[i]
break
}
}
}
if targetAsset == nil {
fmt.Printf("[ERROR] Binary not found for %s/%s: %s\n", system, arch, expectedFilename)
fmt.Println("Available binaries:")
for _, asset := range release.Assets {
if strings.Contains(asset.Name, "mihomo") && (strings.HasSuffix(asset.Name, ".gz") || strings.HasSuffix(asset.Name, ".zip")) {
fmt.Printf(" - %s\n", asset.Name)
}
}
return false
}
fmt.Printf("[DOWNLOAD] Downloading: %s\n", targetAsset.Name)
fmt.Printf(" Size: %d bytes\n", targetAsset.Size)
tempCompressedPath := path + ".tmp" + fileExtension
tempExtractedPath := path + ".tmp"
if err := downloadFile(targetAsset.BrowserDownloadURL, tempCompressedPath); err != nil {
fmt.Printf("[ERROR] %v\n", err)
return false
}
if info, err := os.Stat(tempCompressedPath); err != nil || info.Size() != targetAsset.Size {
os.Remove(tempCompressedPath)
fmt.Println("[ERROR] Download verification failed - size mismatch")
return false
}
if err := extractBinary(tempCompressedPath, tempExtractedPath, fileExtension); err != nil {
os.Remove(tempCompressedPath)
os.Remove(tempExtractedPath)
fmt.Printf("[ERROR] Failed to extract binary: %v\n", err)
return false
}
os.Remove(tempCompressedPath)
if system != "windows" {
os.Chmod(tempExtractedPath, 0o755)
}
if _, err := os.Stat(path); err == nil {
backupPath := path + ".backup"
if err := os.Rename(path, backupPath); err != nil {
fmt.Printf("[ERROR] Failed to back up existing binary: %v\n", err)
return false
}
fmt.Printf("[INFO] Backed up existing binary to: %s\n", backupPath)
}
if err := os.Rename(tempExtractedPath, path); err != nil {
fmt.Printf("[ERROR] Failed to install binary: %v\n", err)
return false
}
fmt.Printf("[SUCCESS] Successfully updated mihomo %s\n", release.TagName)
fmt.Printf(" Location: %s\n", path)
if info, err := os.Stat(path); err == nil {
fmt.Printf(" Size: %d bytes\n", info.Size())
}
return true
}
func fetchRelease(url string) (*githubRelease, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/vnd.github.v3+json")
req.Header.Set("User-Agent", "scientific-surfing/1.0")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("network error: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("network error: HTTP %d", resp.StatusCode)
}
var release githubRelease
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return nil, fmt.Errorf("failed to parse release info: %w", err)
}
return &release, nil
}
func downloadFile(url, destPath string) error {
client := &http.Client{Timeout: 5 * time.Minute}
resp, err := client.Get(url)
if err != nil {
return fmt.Errorf("network error: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("network error: HTTP %d", resp.StatusCode)
}
out, err := os.Create(destPath)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return err
}
func extractBinary(compressedPath, extractedPath, fileExtension string) error {
switch fileExtension {
case ".gz":
f, err := os.Open(compressedPath)
if err != nil {
return err
}
defer f.Close()
gz, err := gzip.NewReader(f)
if err != nil {
return err
}
defer gz.Close()
out, err := os.Create(extractedPath)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, gz)
return err
case ".zip":
zr, err := zip.OpenReader(compressedPath)
if err != nil {
return err
}
defer zr.Close()
if len(zr.File) == 0 {
return fmt.Errorf("zip archive is empty")
}
fileInfo := zr.File[0]
rc, err := fileInfo.Open()
if err != nil {
return err
}
defer rc.Close()
out, err := os.Create(extractedPath)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, rc)
return err
default:
return fmt.Errorf("unsupported file format: %s", fileExtension)
}
}
// InstallService installs mihomo as a system service.
func (m *Manager) InstallService(serviceName, description string) bool {
binaryPath := m.GetBinaryPath()
if _, err := os.Stat(binaryPath); err != nil {
fmt.Printf("❌ Mihomo binary not found at: %s\n", binaryPath)
fmt.Println(" Please run 'core update' first to download the binary")
return false
}
configDir := m.CoreConfigManager.Storage.ConfigDir
configFile := filepath.Join(configDir, "generated_config.yaml")
if _, err := os.Stat(configFile); err != nil {
m.CoreConfigManager.Apply()
fmt.Printf("✅ Generated initial configuration: %s\n", configFile)
}
serviceArgs := fmt.Sprintf("-d %s -f %s", configDir, configFile)
svcMgr, err := service.New(configDir)
if err != nil {
fmt.Printf("❌ Failed to install service: %v\n", err)
return false
}
if err := svcMgr.Install(serviceName, binaryPath, description, serviceArgs); err != nil {
fmt.Printf("❌ Failed to install service: %v\n", err)
return false
}
fmt.Printf("✅ Service '%s' installed successfully\n", serviceName)
fmt.Printf(" Config directory: %s\n", configDir)
fmt.Printf(" Config file: %s\n", configFile)
return true
}
// UninstallService uninstalls the mihomo system service.
func (m *Manager) UninstallService(serviceName string) bool {
svcMgr, err := service.New(m.CoreConfigManager.Storage.ConfigDir)
if err != nil {
fmt.Printf("❌ Failed to uninstall service: %v\n", err)
return false
}
if err := svcMgr.Uninstall(serviceName); err != nil {
fmt.Printf("❌ Failed to uninstall service: %v\n", err)
return false
}
fmt.Printf("✅ Service '%s' uninstalled successfully\n", serviceName)
return true
}
// StartService starts the mihomo system service.
func (m *Manager) StartService(serviceName string) bool {
svcMgr, err := service.New(m.CoreConfigManager.Storage.ConfigDir)
if err != nil {
fmt.Printf("❌ Failed to start service: %v\n", err)
return false
}
if err := svcMgr.Start(serviceName); err != nil {
fmt.Printf("❌ Failed to start service: %v\n", err)
return false
}
fmt.Printf("✅ Service '%s' started successfully\n", serviceName)
return true
}
// StopService stops the mihomo system service.
func (m *Manager) StopService(serviceName string) bool {
svcMgr, err := service.New(m.CoreConfigManager.Storage.ConfigDir)
if err != nil {
fmt.Printf("❌ Failed to stop service: %v\n", err)
return false
}
if err := svcMgr.Stop(serviceName); err != nil {
fmt.Printf("❌ Failed to stop service: %v\n", err)
return false
}
fmt.Printf("✅ Service '%s' stopped successfully\n", serviceName)
return true
}
// RestartService restarts the mihomo system service.
func (m *Manager) RestartService(serviceName string) bool {
svcMgr, err := service.New(m.CoreConfigManager.Storage.ConfigDir)
if err != nil {
fmt.Printf("❌ Failed to restart service: %v\n", err)
return false
}
if err := svcMgr.Restart(serviceName); err != nil {
fmt.Printf("❌ Failed to restart service: %v\n", err)
return false
}
fmt.Printf("✅ Service '%s' restarted successfully\n", serviceName)
return true
}
// GetServiceStatus returns a human-readable status string for the service.
func (m *Manager) GetServiceStatus(serviceName string) string {
return service.Status(serviceName)
}
// ReloadService reloads mihomo configuration via its external-controller API
// without restarting the service.
func (m *Manager) ReloadService() bool {
configPath := filepath.Join(m.CoreConfigManager.Storage.ConfigDir, "generated_config.yaml")
if _, err := os.Stat(configPath); err != nil {
fmt.Printf("❌ Configuration file not found: %s\n", configPath)
return false
}
raw, err := os.ReadFile(configPath)
if err != nil {
fmt.Printf("❌ Error reloading service: %v\n", err)
return false
}
var config map[string]interface{}
if err := yamlutil.Unmarshal(raw, &config); err != nil {
fmt.Printf("❌ Error reloading service: %v\n", err)
return false
}
externalController := "127.0.0.1:9090"
if v, ok := config["external-controller"].(string); ok && v != "" {
externalController = v
}
secret, _ := config["secret"].(string)
baseURL := externalController
if !strings.HasPrefix(baseURL, "http") {
baseURL = "http://" + baseURL
}
url := baseURL + "/configs?force=true"
payload := map[string]interface{}{
"path": configPath,
"payload": "",
}
body, err := json.Marshal(payload)
if err != nil {
fmt.Printf("❌ Error reloading service: %v\n", err)
return false
}
req, err := http.NewRequest(http.MethodPut, url, strings.NewReader(string(body)))
if err != nil {
fmt.Printf("❌ Error reloading service: %v\n", err)
return false
}
req.Header.Set("Content-Type", "application/json")
if secret != "" {
req.Header.Set("Authorization", "Bearer "+secret)
}
fmt.Printf("🔄 Reloading configuration via API: %s\n", url)
client := &http.Client{Timeout: 2 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("❌ Failed to connect to external controller: %v\n", err)
fmt.Println(" Is the service running?")
return false
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNoContent {
fmt.Println("✅ Configuration reloaded successfully")
return true
}
respBody, _ := io.ReadAll(resp.Body)
fmt.Printf("❌ Failed to reload configuration: HTTP %d\n", resp.StatusCode)
fmt.Printf(" Response: %s\n", string(respBody))
return false
}