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

148 lines
4.0 KiB
Go

//go:build darwin
package service
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
func newPlatformManager(configDir string) (platformManager, error) {
return &macosServiceManager{configDir: configDir}, nil
}
// macosServiceManager manages services via launchd. Direct port of
// MacOSServiceManager in service_manager.py. Unlike the Python version
// (which shells out to a separate macos_service_wrapper.py for DNS
// handling), the generated plist invokes this same ssm binary in
// "service run-macos" mode (see wrapper_darwin.go) — no second script file
// is needed.
type macosServiceManager struct {
configDir string
}
func (d *macosServiceManager) launchdPath(name string) string {
return filepath.Join("/Library/LaunchDaemons", "com."+name+".plist")
}
func (d *macosServiceManager) createLaunchdPlist(cfg Config) error {
selfPath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to determine ssm executable path: %w", err)
}
workingDir := filepath.Dir(cfg.ExecutablePath)
programArgs := []string{
selfPath,
"service", "run-macos",
cfg.ExecutablePath,
workingDir,
}
if cfg.Args != "" {
extra, err := shlexSplit(cfg.Args)
if err != nil {
return fmt.Errorf("failed to parse service args: %w", err)
}
programArgs = append(programArgs, extra...)
}
programArgsXML := ""
for _, arg := range programArgs {
programArgsXML += fmt.Sprintf(" <string>%s</string>\n", xmlEscape(arg))
}
plistContent := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.%s</string>
<key>ProgramArguments</key>
<array>
%s </array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/%s.log</string>
<key>StandardErrorPath</key>
<string>/var/log/%s.error.log</string>
</dict>
</plist>
`, cfg.Name, programArgsXML, cfg.Name, cfg.Name)
plistPath := d.launchdPath(cfg.Name)
if err := os.WriteFile(plistPath, []byte(plistContent), 0o644); err != nil {
return fmt.Errorf("failed to create launchd plist: %w", err)
}
return nil
}
var xmlReplacer = strings.NewReplacer(
"&", "&amp;",
"<", "&lt;",
">", "&gt;",
`"`, "&quot;",
"'", "&apos;",
)
func xmlEscape(s string) string {
return xmlReplacer.Replace(s)
}
func (d *macosServiceManager) Install(cfg Config) error {
if err := d.createLaunchdPlist(cfg); err != nil {
return err
}
plistPath := d.launchdPath(cfg.Name)
if out, err := exec.Command("launchctl", "load", plistPath).CombinedOutput(); err != nil {
return fmt.Errorf("failed to install service: %s", out)
}
if out, err := exec.Command("launchctl", "start", "com."+cfg.Name).CombinedOutput(); err != nil {
return fmt.Errorf("failed to install service: %s", out)
}
return nil
}
func (d *macosServiceManager) Uninstall(name string) error {
exec.Command("launchctl", "stop", "com."+name).Run()
plistPath := d.launchdPath(name)
if _, err := os.Stat(plistPath); err == nil {
if out, err := exec.Command("launchctl", "unload", plistPath).CombinedOutput(); err != nil {
return fmt.Errorf("failed to uninstall service: %s", out)
}
os.Remove(plistPath)
}
return nil
}
func (d *macosServiceManager) Start(name string) error {
if out, err := exec.Command("launchctl", "start", "com."+name).CombinedOutput(); err != nil {
return fmt.Errorf("failed to start service: %s", out)
}
return nil
}
func (d *macosServiceManager) Stop(name string) error {
if out, err := exec.Command("launchctl", "stop", "com."+name).CombinedOutput(); err != nil {
return fmt.Errorf("failed to stop service: %s", out)
}
return nil
}
func (d *macosServiceManager) Restart(name string) error {
exec.Command("launchctl", "stop", "com."+name).Run()
if out, err := exec.Command("launchctl", "start", "com."+name).CombinedOutput(); err != nil {
return fmt.Errorf("failed to restart service: %s", out)
}
return nil
}