Files
alpine-router/mihomo/mihomo.go

322 lines
6.1 KiB
Go
Raw Normal View History

2026-04-13 09:46:02 +03:00
package mihomo
import (
"embed"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"sync"
"time"
"gopkg.in/yaml.v3"
)
var (
mu sync.Mutex
process *os.Process
running bool
configDir string
logMu sync.Mutex
logRing [500]string
logLen int
logPos int
)
//go:embed default.yaml
var defaultConfigFS embed.FS
func init() {
configDir = defaultConfigDir()
}
func defaultConfigDir() string {
exe, err := os.Executable()
if err != nil {
return "/lib/mihomo"
}
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
exe = resolved
}
base := filepath.Dir(exe)
if base == "/usr/bin" || base == "/usr/sbin" || base == "/sbin" || base == "/bin" {
return "/lib/mihomo"
}
return filepath.Join(base, "mihomo")
}
func SetConfigDir(dir string) {
mu.Lock()
defer mu.Unlock()
configDir = dir
}
func ConfigDir() string {
return configDir
}
func DataDir() string {
return filepath.Join(ConfigDir(), "data")
}
func CoresDir() string {
return filepath.Join(ConfigDir(), "cores")
}
func ConfigPath() string {
return filepath.Join(DataDir(), "config.yaml")
}
func CorePath() string {
arch := runtime.GOARCH
switch arch {
case "amd64":
arch = "amd64"
case "arm64":
arch = "arm64"
case "arm":
arch = "armv7"
default:
arch = "amd64"
}
return filepath.Join(CoresDir(), fmt.Sprintf("mihomo-linux-%s", arch))
}
func EnsureDefaultConfig() error {
cfgPath := ConfigPath()
if _, err := os.Stat(cfgPath); err == nil {
return nil
}
if err := os.MkdirAll(DataDir(), 0755); err != nil {
return fmt.Errorf("mkdir mihomo data dir: %w", err)
}
data, err := defaultConfigFS.ReadFile("default.yaml")
if err != nil {
return fmt.Errorf("read default config: %w", err)
}
tmp := cfgPath + ".tmp"
if err := os.WriteFile(tmp, data, 0644); err != nil {
return fmt.Errorf("write default config: %w", err)
}
if err := os.Rename(tmp, cfgPath); err != nil {
return fmt.Errorf("rename default config: %w", err)
}
return nil
}
func appendLog(line string) {
logMu.Lock()
defer logMu.Unlock()
logRing[logPos%len(logRing)] = line
logPos++
if logLen < len(logRing) {
logLen++
}
}
func Logs() []string {
logMu.Lock()
defer logMu.Unlock()
result := make([]string, 0, logLen)
start := logPos - logLen
for i := start; i < logPos; i++ {
result = append(result, logRing[i%len(logRing)])
}
return result
}
type lineWriter struct{}
func (lineWriter) Write(p []byte) (int, error) {
start := 0
for i, b := range p {
if b == '\n' {
line := string(p[start:i])
if line != "" {
appendLog(line)
}
start = i + 1
}
}
if start < len(p) {
line := string(p[start:])
if line != "" {
appendLog(line)
}
}
return len(p), nil
}
func Status() map[string]interface{} {
mu.Lock()
defer mu.Unlock()
corePath := CorePath()
_, err := os.Stat(corePath)
coreExists := err == nil
_, cfgErr := os.Stat(ConfigPath())
cfgExists := cfgErr == nil
status := map[string]interface{}{
"running": running,
"core_exists": coreExists,
"core_path": corePath,
"config_dir": DataDir(),
"config_file": ConfigPath(),
"config_exists": cfgExists,
}
if running && process != nil {
status["pid"] = process.Pid
}
return status
}
func Start() error {
mu.Lock()
defer mu.Unlock()
if running {
return fmt.Errorf("mihomo is already running")
}
corePath := CorePath()
if _, err := os.Stat(corePath); err != nil {
return fmt.Errorf("mihomo core not found at %s: %w", corePath, err)
}
cfgPath := ConfigPath()
if _, err := os.Stat(cfgPath); err != nil {
return fmt.Errorf("mihomo config not found at %s: %w", cfgPath, err)
}
cmd := exec.Command(corePath, "-d", DataDir())
lw := lineWriter{}
w := io.MultiWriter(os.Stdout, lw)
cmd.Stdout = w
cmd.Stderr = io.MultiWriter(os.Stderr, lw)
if err := cmd.Start(); err != nil {
return fmt.Errorf("start mihomo: %w", err)
}
process = cmd.Process
running = true
go func() {
err := cmd.Wait()
mu.Lock()
running = false
process = nil
mu.Unlock()
if err != nil {
fmt.Fprintf(os.Stderr, "mihomo process exited: %v\n", err)
}
}()
time.Sleep(500 * time.Millisecond)
if !running {
return fmt.Errorf("mihomo exited immediately")
}
return nil
}
func Stop() error {
mu.Lock()
defer mu.Unlock()
if !running || process == nil {
running = false
process = nil
return nil
}
if err := process.Signal(os.Interrupt); err != nil {
_ = process.Kill()
}
running = false
process = nil
return nil
}
func Restart() error {
if err := Stop(); err != nil {
return err
}
time.Sleep(500 * time.Millisecond)
return Start()
}
func IsRunning() bool {
mu.Lock()
defer mu.Unlock()
return running
}
func InstallCore(srcPath string) error {
arch := runtime.GOARCH
switch arch {
case "amd64":
arch = "amd64"
case "arm64":
arch = "arm64"
case "arm":
arch = "armv7"
default:
arch = "amd64"
}
dstPath := filepath.Join(CoresDir(), fmt.Sprintf("mihomo-linux-%s", arch))
if err := os.MkdirAll(CoresDir(), 0755); err != nil {
return fmt.Errorf("mkdir cores: %w", err)
}
data, err := os.ReadFile(srcPath)
if err != nil {
return fmt.Errorf("read core source: %w", err)
}
if err := os.WriteFile(dstPath, data, 0755); err != nil {
return fmt.Errorf("write core: %w", err)
}
return nil
}
func LoadConfig() (map[string]interface{}, error) {
data, err := os.ReadFile(ConfigPath())
if err != nil {
if os.IsNotExist(err) {
return map[string]interface{}{}, nil
}
return nil, fmt.Errorf("read mihomo config: %w", err)
}
var cfg map[string]interface{}
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse mihomo config: %w", err)
}
return cfg, nil
}
func SaveConfig(cfg map[string]interface{}) error {
data, err := yaml.Marshal(cfg)
if err != nil {
return fmt.Errorf("marshal mihomo config: %w", err)
}
if err := os.MkdirAll(DataDir(), 0755); err != nil {
return fmt.Errorf("mkdir mihomo data dir: %w", err)
}
tmp := ConfigPath() + ".tmp"
if err := os.WriteFile(tmp, data, 0644); err != nil {
return fmt.Errorf("write mihomo config: %w", err)
}
if err := os.Rename(tmp, ConfigPath()); err != nil {
return fmt.Errorf("rename mihomo config: %w", err)
}
return nil
}