Files
ss/internal/service/wrapper_darwin.go
2026-07-19 21:23:44 +08:00

185 lines
4.9 KiB
Go

//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 `ssm 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
}