148 lines
4.0 KiB
Go
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(
|
|
"&", "&",
|
|
"<", "<",
|
|
">", ">",
|
|
`"`, """,
|
|
"'", "'",
|
|
)
|
|
|
|
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
|
|
}
|