feat: migrate to golang

This commit is contained in:
2026-07-19 20:01:38 +08:00
parent 302d4e6bb5
commit a2630df9e0
69 changed files with 4750 additions and 3369 deletions

458
internal/core/manager.go Normal file
View File

@ -0,0 +1,458 @@
// 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(serviceName string) 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
}