322 lines
6.1 KiB
Go
322 lines
6.1 KiB
Go
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
|
|
} |