feat: migrate to golang
This commit is contained in:
184
internal/service/wrapper_darwin.go
Normal file
184
internal/service/wrapper_darwin.go
Normal file
@ -0,0 +1,184 @@
|
||||
//go:build darwin
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// dnsManager manages macOS DNS settings via networksetup(8). Direct port of
|
||||
// MacOSDNSManager in macos_service_wrapper.py.
|
||||
type dnsManager struct{}
|
||||
|
||||
func (dnsManager) getNetworkServices() []string {
|
||||
out, err := exec.Command("networksetup", "-listallnetworkservices").CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to list network services: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
lines := strings.Split(string(out), "\n")
|
||||
var services []string
|
||||
for _, line := range lines[1:] { // skip header
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" && !strings.HasPrefix(line, "*") {
|
||||
services = append(services, line)
|
||||
}
|
||||
}
|
||||
return services
|
||||
}
|
||||
|
||||
func (dnsManager) getCurrentDNS(service string) []string {
|
||||
out, err := exec.Command("networksetup", "-getdnsservers", service).CombinedOutput()
|
||||
if err != nil {
|
||||
return []string{"Empty"}
|
||||
}
|
||||
var servers []string
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" && !strings.Contains(line, " ") {
|
||||
servers = append(servers, line)
|
||||
}
|
||||
}
|
||||
return servers
|
||||
}
|
||||
|
||||
func (dnsManager) setDNS(service string, servers []string) bool {
|
||||
var err error
|
||||
if len(servers) == 0 {
|
||||
_, err = exec.Command("networksetup", "-setdnsservers", service, "empty").CombinedOutput()
|
||||
} else {
|
||||
args := append([]string{"-setdnsservers", service}, servers...)
|
||||
_, err = exec.Command("networksetup", args...).CombinedOutput()
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to set DNS for %s: %v\n", service, err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// mihomoWrapper wraps the mihomo process on macOS, managing DNS settings
|
||||
// around its lifecycle. Direct port of MihomoServiceWrapper in
|
||||
// macos_service_wrapper.py.
|
||||
type mihomoWrapper struct {
|
||||
executable string
|
||||
args []string
|
||||
configDir string
|
||||
process *exec.Cmd
|
||||
dns dnsManager
|
||||
originalDNS map[string][]string
|
||||
shuttingDown bool
|
||||
}
|
||||
|
||||
func (w *mihomoWrapper) start() bool {
|
||||
cmd := exec.Command(w.executable, w.args...)
|
||||
cmd.Dir = w.configDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to start service: %v\n", err)
|
||||
return false
|
||||
}
|
||||
w.process = cmd
|
||||
fmt.Fprintf(os.Stderr, "Started mihomo service (PID: %d)\n", cmd.Process.Pid)
|
||||
|
||||
w.originalDNS = make(map[string][]string)
|
||||
dnsServer := "223.6.6.6"
|
||||
for _, svc := range w.dns.getNetworkServices() {
|
||||
original := w.dns.getCurrentDNS(svc)
|
||||
w.originalDNS[svc] = original
|
||||
|
||||
if w.dns.setDNS(svc, []string{dnsServer}) {
|
||||
fmt.Fprintf(os.Stderr, "Set DNS for %s: %s\n", svc, dnsServer)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Warning: Failed to set DNS for %s\n", svc)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *mihomoWrapper) stop() bool {
|
||||
for svc, servers := range w.originalDNS {
|
||||
if w.dns.setDNS(svc, servers) {
|
||||
if len(servers) > 0 && servers[0] != "Empty" {
|
||||
fmt.Fprintf(os.Stderr, "Restored DNS for %s: %s\n", svc, strings.Join(servers, ", "))
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Reset DNS for %s to automatic\n", svc)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Warning: Failed to restore DNS for %s\n", svc)
|
||||
}
|
||||
}
|
||||
|
||||
if w.process != nil && w.process.Process != nil {
|
||||
w.process.Process.Signal(syscall.SIGTERM)
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- w.process.Wait() }()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
w.process.Process.Kill()
|
||||
<-done
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Stopped mihomo service (PID: %d)\n", w.process.Process.Pid)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *mihomoWrapper) run() {
|
||||
w.shuttingDown = false
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
|
||||
|
||||
if !w.start() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "Service started, press Ctrl+C to stop")
|
||||
|
||||
waitCh := make(chan error, 1)
|
||||
go func() { waitCh <- w.process.Wait() }()
|
||||
|
||||
select {
|
||||
case <-waitCh:
|
||||
fmt.Fprintf(os.Stderr, "Process exited with code: %d\n", w.process.ProcessState.ExitCode())
|
||||
w.stop()
|
||||
case sig := <-sigCh:
|
||||
fmt.Fprintf(os.Stderr, "Received signal %v, shutting down...\n", sig)
|
||||
w.shuttingDown = true
|
||||
w.stop()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// RunMacOSWrapper runs the mihomo process with DNS management, blocking
|
||||
// until it exits or a termination signal is received. Invoked by the
|
||||
// hidden `ss service run-macos <executable> <configDir> [args...]` command
|
||||
// that the generated launchd plist points at (see service_darwin.go),
|
||||
// replacing the standalone macos_service_wrapper.py script.
|
||||
func RunMacOSWrapper(executable, configDir string, args []string) error {
|
||||
if _, err := os.Stat(executable); err != nil {
|
||||
return fmt.Errorf("executable not found: %s", executable)
|
||||
}
|
||||
if _, err := os.Stat(configDir); err != nil {
|
||||
return fmt.Errorf("config directory not found: %s", configDir)
|
||||
}
|
||||
|
||||
w := &mihomoWrapper{
|
||||
executable: executable,
|
||||
args: args,
|
||||
configDir: configDir,
|
||||
}
|
||||
w.run()
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user