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 }