//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 ss 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 ss 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(" %s\n", xmlEscape(arg))
}
plistContent := fmt.Sprintf(`
Label
com.%s
ProgramArguments
%s
RunAtLoad
KeepAlive
StandardOutPath
/var/log/%s.log
StandardErrorPath
/var/log/%s.error.log
`, 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(
"&", "&",
"<", "<",
">", ">",
`"`, """,
"'", "'",
)
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
}