Skip to content

Commit eee87cc

Browse files
authored
feat: support to install service on macos (#75)
Co-authored-by: Rick <[email protected]>
1 parent 8e832ee commit eee87cc

File tree

4 files changed

+192
-30
lines changed

4 files changed

+192
-30
lines changed

cmd/data/linux_service.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[Unit]
2+
Description=API Testing
3+
4+
[Service]
5+
ExecStart=/usr/bin/env atest server
6+
7+
[Install]
8+
WantedBy=multi-user.target

cmd/data/macos_service.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>Label</key>
6+
<string>com.github.linuxsuren.atest</string>
7+
<key>ServiceDescription</key>
8+
<string>API Testing Server</string>
9+
<key>ProgramArguments</key>
10+
<array>
11+
<string>/usr/local/bin/atest</string>
12+
<string>server</string>
13+
</array>
14+
<key>RunAtLoad</key>
15+
<false/>
16+
</dict>
17+
</plist>

cmd/service.go

Lines changed: 128 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"os"
77

8+
_ "embed"
9+
810
fakeruntime "github.com/linuxsuren/go-fake-runtime"
911
"github.com/spf13/cobra"
1012
)
@@ -22,22 +24,25 @@ func createServiceCommand(execer fakeruntime.Execer) (c *cobra.Command) {
2224
}
2325
flags := c.Flags()
2426
flags.StringVarP(&opt.action, "action", "a", "", "The action of service, support actions: install, start, stop, restart, status")
25-
flags.StringVarP(&opt.scriptPath, "script-path", "", "/lib/systemd/system/atest.service", "The service script file path")
27+
flags.StringVarP(&opt.scriptPath, "script-path", "", "", "The service script file path")
2628
return
2729
}
2830

2931
type serviceOption struct {
3032
action string
3133
scriptPath string
34+
service Service
3235
fakeruntime.Execer
3336
}
3437

3538
func (o *serviceOption) preRunE(c *cobra.Command, args []string) (err error) {
36-
if o.Execer.OS() != "linux" {
37-
err = fmt.Errorf("only support on Linux")
38-
}
39-
if o.action == "" && len(args) > 0 {
40-
o.action = args[0]
39+
if o.Execer.OS() != fakeruntime.OSLinux && o.Execer.OS() != fakeruntime.OSDarwin {
40+
err = fmt.Errorf("only support on Linux/Darwin instead of %s", o.Execer.OS())
41+
} else {
42+
if o.action == "" && len(args) > 0 {
43+
o.action = args[0]
44+
}
45+
o.service = newService(o.Execer, o.scriptPath)
4146
}
4247
return
4348
}
@@ -46,17 +51,15 @@ func (o *serviceOption) runE(c *cobra.Command, args []string) (err error) {
4651
var output string
4752
switch o.action {
4853
case "install", "i":
49-
if err = os.WriteFile(o.scriptPath, []byte(script), os.ModeAppend); err == nil {
50-
output, err = o.Execer.RunCommandAndReturn("systemctl", "", "enable", "atest")
51-
}
54+
output, err = o.service.Install()
5255
case "start":
53-
output, err = o.Execer.RunCommandAndReturn("systemctl", "", "start", "atest")
56+
output, err = o.service.Start()
5457
case "stop":
55-
output, err = o.Execer.RunCommandAndReturn("systemctl", "", "stop", "atest")
58+
output, err = o.service.Stop()
5659
case "restart":
57-
output, err = o.Execer.RunCommandAndReturn("systemctl", "", "restart", "atest")
60+
output, err = o.service.Restart()
5861
case "status":
59-
output, err = o.Execer.RunCommandAndReturn("systemctl", "", "status", "atest")
62+
output, err = o.service.Status()
6063
default:
6164
err = fmt.Errorf("not support action: '%s'", o.action)
6265
}
@@ -67,12 +70,117 @@ func (o *serviceOption) runE(c *cobra.Command, args []string) (err error) {
6770
return
6871
}
6972

70-
var script = `[Unit]
71-
Description=API Testing
73+
// Service is the interface of service
74+
type Service interface {
75+
Start() (string, error) // start the service
76+
Stop() (string, error) // stop the service gracefully
77+
Restart() (string, error) // restart the service gracefully
78+
Status() (string, error) // status of the service
79+
Install() (string, error) // install the service
80+
}
81+
82+
func emptyThenDefault(value, defaultValue string) string {
83+
if value == "" {
84+
value = defaultValue
85+
}
86+
return value
87+
}
88+
89+
func newService(execer fakeruntime.Execer, scriptPath string) (service Service) {
90+
switch execer.OS() {
91+
case fakeruntime.OSDarwin:
92+
service = &macOSService{
93+
commonService: commonService{
94+
Execer: execer,
95+
scriptPath: emptyThenDefault(scriptPath, "/Library/LaunchDaemons/com.github.linuxsuren.atest.plist"),
96+
script: macOSServiceScript,
97+
},
98+
}
99+
case fakeruntime.OSLinux:
100+
service = &linuxService{
101+
commonService: commonService{
102+
Execer: execer,
103+
scriptPath: emptyThenDefault(scriptPath, "/lib/systemd/system/atest.service"),
104+
script: linuxServiceScript,
105+
},
106+
}
107+
}
108+
return
109+
}
110+
111+
type commonService struct {
112+
fakeruntime.Execer
113+
scriptPath string
114+
script string
115+
}
116+
117+
type macOSService struct {
118+
commonService
119+
}
120+
121+
var (
122+
//go:embed data/macos_service.xml
123+
macOSServiceScript string
124+
//go:embed data/linux_service.txt
125+
linuxServiceScript string
126+
)
72127

73-
[Service]
74-
ExecStart=/usr/bin/env atest server
128+
func (s *macOSService) Start() (output string, err error) {
129+
output, err = s.Execer.RunCommandAndReturn("sudo", "", "launchctl", "start", "com.github.linuxsuren.atest")
130+
return
131+
}
75132

76-
[Install]
77-
WantedBy=multi-user.target
78-
`
133+
func (s *macOSService) Stop() (output string, err error) {
134+
output, err = s.Execer.RunCommandAndReturn("sudo", "", "launchctl", "stop", "com.github.linuxsuren.atest")
135+
return
136+
}
137+
138+
func (s *macOSService) Restart() (output string, err error) {
139+
if output, err = s.Stop(); err == nil {
140+
output, err = s.Start()
141+
}
142+
return
143+
}
144+
145+
func (s *macOSService) Status() (output string, err error) {
146+
output, err = s.Execer.RunCommandAndReturn("sudo", "", "launchctl", "runstats", "system/com.github.linuxsuren.atest")
147+
return
148+
}
149+
150+
func (s *macOSService) Install() (output string, err error) {
151+
if err = os.WriteFile(s.scriptPath, []byte(s.script), os.ModeAppend); err == nil {
152+
output, err = s.Execer.RunCommandAndReturn("sudo", "", "launchctl", "enable", "system/com.github.linuxsuren.atest")
153+
}
154+
return
155+
}
156+
157+
type linuxService struct {
158+
commonService
159+
}
160+
161+
func (s *linuxService) Start() (output string, err error) {
162+
output, err = s.Execer.RunCommandAndReturn("systemctl", "", "start", "atest")
163+
return
164+
}
165+
166+
func (s *linuxService) Stop() (output string, err error) {
167+
output, err = s.Execer.RunCommandAndReturn("systemctl", "", "stop", "atest")
168+
return
169+
}
170+
171+
func (s *linuxService) Restart() (output string, err error) {
172+
output, err = s.Execer.RunCommandAndReturn("systemctl", "", "restart", "atest")
173+
return
174+
}
175+
176+
func (s *linuxService) Status() (output string, err error) {
177+
output, err = s.Execer.RunCommandAndReturn("systemctl", "", "status", "atest")
178+
return
179+
}
180+
181+
func (s *linuxService) Install() (output string, err error) {
182+
if err = os.WriteFile(s.scriptPath, []byte(s.script), os.ModeAppend); err == nil {
183+
output, err = s.Execer.RunCommandAndReturn("systemctl", "", "enable", "atest")
184+
}
185+
return
186+
}

cmd/service_test.go

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ import (
1212
func TestService(t *testing.T) {
1313
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer())
1414
root.SetArgs([]string{"service", "fake"})
15+
root.SetOut(new(bytes.Buffer))
1516
err := root.Execute()
1617
assert.NotNil(t, err)
1718

1819
notLinux := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "fake"}, NewFakeGRPCServer())
1920
notLinux.SetArgs([]string{"service", paramAction, "install"})
21+
notLinux.SetOut(new(bytes.Buffer))
2022
err = notLinux.Execute()
2123
assert.NotNil(t, err)
2224

@@ -26,41 +28,68 @@ func TestService(t *testing.T) {
2628
os.RemoveAll(tmpFile.Name())
2729
}()
2830

29-
targetScript := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer())
30-
targetScript.SetArgs([]string{"service", paramAction, "install", "--script-path", tmpFile.Name()})
31-
err = targetScript.Execute()
32-
assert.Nil(t, err)
33-
data, err := os.ReadFile(tmpFile.Name())
34-
assert.Nil(t, err)
35-
assert.Equal(t, script, string(data))
36-
3731
tests := []struct {
3832
name string
3933
action string
34+
targetOS string
4035
expectOutput string
4136
}{{
4237
name: "action: start",
4338
action: "start",
39+
targetOS: "linux",
4440
expectOutput: "output1",
4541
}, {
4642
name: "action: stop",
4743
action: "stop",
44+
targetOS: "linux",
4845
expectOutput: "output2",
4946
}, {
5047
name: "action: restart",
5148
action: "restart",
49+
targetOS: "linux",
5250
expectOutput: "output3",
5351
}, {
5452
name: "action: status",
5553
action: "status",
54+
targetOS: "linux",
55+
expectOutput: "output4",
56+
}, {
57+
name: "action: install",
58+
action: "install",
59+
targetOS: "linux",
60+
expectOutput: "output4",
61+
}, {
62+
name: "action: start, macos",
63+
action: "start",
64+
targetOS: fakeruntime.OSDarwin,
65+
expectOutput: "output4",
66+
}, {
67+
name: "action: stop, macos",
68+
action: "stop",
69+
targetOS: fakeruntime.OSDarwin,
70+
expectOutput: "output4",
71+
}, {
72+
name: "action: restart, macos",
73+
action: "restart",
74+
targetOS: fakeruntime.OSDarwin,
75+
expectOutput: "output4",
76+
}, {
77+
name: "action: status, macos",
78+
action: "status",
79+
targetOS: fakeruntime.OSDarwin,
80+
expectOutput: "output4",
81+
}, {
82+
name: "action: install, macos",
83+
action: "install",
84+
targetOS: fakeruntime.OSDarwin,
5685
expectOutput: "output4",
5786
}}
5887
for _, tt := range tests {
5988
t.Run(tt.name, func(t *testing.T) {
6089
buf := new(bytes.Buffer)
61-
normalRoot := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux", ExpectOutput: tt.expectOutput}, NewFakeGRPCServer())
90+
normalRoot := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: tt.targetOS, ExpectOutput: tt.expectOutput}, NewFakeGRPCServer())
6291
normalRoot.SetOut(buf)
63-
normalRoot.SetArgs([]string{"service", "--action", tt.action})
92+
normalRoot.SetArgs([]string{"service", "--action", tt.action, "--script-path", tmpFile.Name()})
6493
err = normalRoot.Execute()
6594
assert.Nil(t, err)
6695
assert.Equal(t, tt.expectOutput+"\n", buf.String())

0 commit comments

Comments
 (0)