main
go 106 lines 2.69 KB
Raw
1 //go:build darwin
2
3 package service
4
5 import (
6 "context"
7 "encoding/xml"
8 "fmt"
9 "os"
10 "os/exec"
11 "path/filepath"
12 )
13
14 func Install(ctx context.Context, def Definition) error {
15 plistPath, domain, err := launchdPlistPath(def.Name)
16 if err != nil {
17 return err
18 }
19 if err := os.MkdirAll(filepath.Dir(plistPath), 0o755); err != nil {
20 return err
21 }
22 if err := os.WriteFile(plistPath, []byte(launchdPlist(def)), 0o644); err != nil {
23 return err
24 }
25 _ = exec.CommandContext(ctx, "launchctl", "bootout", domain, plistPath).Run()
26 return exec.CommandContext(ctx, "launchctl", "bootstrap", domain, plistPath).Run()
27 }
28
29 func Start(ctx context.Context, name string) error {
30 _, domain, err := launchdPlistPath(name)
31 if err != nil {
32 return err
33 }
34 return exec.CommandContext(ctx, "launchctl", "kickstart", "-k", domain+"/"+name).Run()
35 }
36
37 func Stop(ctx context.Context, name string) error {
38 plistPath, domain, err := launchdPlistPath(name)
39 if err != nil {
40 return err
41 }
42 return exec.CommandContext(ctx, "launchctl", "bootout", domain, plistPath).Run()
43 }
44
45 func StopDisable(ctx context.Context, name string) error {
46 plistPath, domain, err := launchdPlistPath(name)
47 if err != nil {
48 return err
49 }
50 _ = exec.CommandContext(ctx, "launchctl", "disable", domain+"/"+name).Run()
51 return exec.CommandContext(ctx, "launchctl", "bootout", domain, plistPath).Run()
52 }
53
54 func Run(ctx context.Context, name string, run func(context.Context) error) error {
55 return run(ctx)
56 }
57
58 func launchdPlistPath(name string) (string, string, error) {
59 if os.Geteuid() == 0 {
60 return filepath.Join("/Library/LaunchDaemons", name+".plist"), "system", nil
61 }
62 home, err := os.UserHomeDir()
63 if err != nil {
64 return "", "", err
65 }
66 return filepath.Join(home, "Library", "LaunchAgents", name+".plist"), fmt.Sprintf("gui/%d", os.Getuid()), nil
67 }
68
69 func launchdPlist(def Definition) string {
70 args := append([]string{def.Executable}, def.Args...)
71 argXML := ""
72 for _, arg := range args {
73 argXML += "\n <string>" + xmlEscape(arg) + "</string>"
74 }
75 return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
76 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
77 <plist version="1.0">
78 <dict>
79 <key>Label</key>
80 <string>%s</string>
81 <key>ProgramArguments</key>
82 <array>%s
83 </array>
84 <key>WorkingDirectory</key>
85 <string>%s</string>
86 <key>RunAtLoad</key>
87 <true/>
88 <key>KeepAlive</key>
89 <true/>
90 </dict>
91 </plist>
92 `, xmlEscape(def.Name), argXML, xmlEscape(def.WorkingDir))
93 }
94
95 func xmlEscape(value string) string {
96 var out []byte
97 xml.EscapeText((*appendWriter)(&out), []byte(value))
98 return string(out)
99 }
100
101 type appendWriter []byte
102
103 func (w *appendWriter) Write(p []byte) (int, error) {
104 *w = append(*w, p...)
105 return len(p), nil
106 }