Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cmd/data/linux_service.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[Unit]
Description=API Testing

[Service]
ExecStart=/usr/bin/env atest server

[Install]
WantedBy=multi-user.target
17 changes: 17 additions & 0 deletions cmd/data/macos_service.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?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.github.linuxsuren.atest</string>
<key>ServiceDescription</key>
<string>API Testing Server</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/atest</string>
<string>server</string>
</array>
<key>RunAtLoad</key>
<false/>
</dict>
</plist>
148 changes: 128 additions & 20 deletions cmd/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"os"

_ "embed"

fakeruntime "github.com/linuxsuren/go-fake-runtime"
"github.com/spf13/cobra"
)
Expand All @@ -22,22 +24,25 @@ func createServiceCommand(execer fakeruntime.Execer) (c *cobra.Command) {
}
flags := c.Flags()
flags.StringVarP(&opt.action, "action", "a", "", "The action of service, support actions: install, start, stop, restart, status")
flags.StringVarP(&opt.scriptPath, "script-path", "", "/lib/systemd/system/atest.service", "The service script file path")
flags.StringVarP(&opt.scriptPath, "script-path", "", "", "The service script file path")
return
}

type serviceOption struct {
action string
scriptPath string
service Service
fakeruntime.Execer
}

func (o *serviceOption) preRunE(c *cobra.Command, args []string) (err error) {
if o.Execer.OS() != "linux" {
err = fmt.Errorf("only support on Linux")
}
if o.action == "" && len(args) > 0 {
o.action = args[0]
if o.Execer.OS() != fakeruntime.OSLinux && o.Execer.OS() != fakeruntime.OSDarwin {
err = fmt.Errorf("only support on Linux/Darwin instead of %s", o.Execer.OS())
} else {
if o.action == "" && len(args) > 0 {
o.action = args[0]
}
o.service = newService(o.Execer, o.scriptPath)
}
return
}
Expand All @@ -46,17 +51,15 @@ func (o *serviceOption) runE(c *cobra.Command, args []string) (err error) {
var output string
switch o.action {
case "install", "i":
if err = os.WriteFile(o.scriptPath, []byte(script), os.ModeAppend); err == nil {
output, err = o.Execer.RunCommandAndReturn("systemctl", "", "enable", "atest")
}
output, err = o.service.Install()
case "start":
output, err = o.Execer.RunCommandAndReturn("systemctl", "", "start", "atest")
output, err = o.service.Start()
case "stop":
output, err = o.Execer.RunCommandAndReturn("systemctl", "", "stop", "atest")
output, err = o.service.Stop()
case "restart":
output, err = o.Execer.RunCommandAndReturn("systemctl", "", "restart", "atest")
output, err = o.service.Restart()
case "status":
output, err = o.Execer.RunCommandAndReturn("systemctl", "", "status", "atest")
output, err = o.service.Status()
default:
err = fmt.Errorf("not support action: '%s'", o.action)
}
Expand All @@ -67,12 +70,117 @@ func (o *serviceOption) runE(c *cobra.Command, args []string) (err error) {
return
}

var script = `[Unit]
Description=API Testing
// Service is the interface of service
type Service interface {
Start() (string, error) // start the service
Stop() (string, error) // stop the service gracefully
Restart() (string, error) // restart the service gracefully
Status() (string, error) // status of the service
Install() (string, error) // install the service
}

func emptyThenDefault(value, defaultValue string) string {
if value == "" {
value = defaultValue
}
return value
}

func newService(execer fakeruntime.Execer, scriptPath string) (service Service) {
switch execer.OS() {
case fakeruntime.OSDarwin:
service = &macOSService{
commonService: commonService{
Execer: execer,
scriptPath: emptyThenDefault(scriptPath, "/Library/LaunchDaemons/com.github.linuxsuren.atest.plist"),
script: macOSServiceScript,
},
}
case fakeruntime.OSLinux:
service = &linuxService{
commonService: commonService{
Execer: execer,
scriptPath: emptyThenDefault(scriptPath, "/lib/systemd/system/atest.service"),
script: linuxServiceScript,
},
}
}
return
}

type commonService struct {
fakeruntime.Execer
scriptPath string
script string
}

type macOSService struct {
commonService
}

var (
//go:embed data/macos_service.xml
macOSServiceScript string
//go:embed data/linux_service.txt
linuxServiceScript string
)

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

[Install]
WantedBy=multi-user.target
`
func (s *macOSService) Stop() (output string, err error) {
output, err = s.Execer.RunCommandAndReturn("sudo", "", "launchctl", "stop", "com.github.linuxsuren.atest")
return
}

func (s *macOSService) Restart() (output string, err error) {
if output, err = s.Stop(); err == nil {
output, err = s.Start()
}
return
}

func (s *macOSService) Status() (output string, err error) {
output, err = s.Execer.RunCommandAndReturn("sudo", "", "launchctl", "runstats", "system/com.github.linuxsuren.atest")
return
}

func (s *macOSService) Install() (output string, err error) {
if err = os.WriteFile(s.scriptPath, []byte(s.script), os.ModeAppend); err == nil {
output, err = s.Execer.RunCommandAndReturn("sudo", "", "launchctl", "enable", "system/com.github.linuxsuren.atest")
}
return
}

type linuxService struct {
commonService
}

func (s *linuxService) Start() (output string, err error) {
output, err = s.Execer.RunCommandAndReturn("systemctl", "", "start", "atest")
return
}

func (s *linuxService) Stop() (output string, err error) {
output, err = s.Execer.RunCommandAndReturn("systemctl", "", "stop", "atest")
return
}

func (s *linuxService) Restart() (output string, err error) {
output, err = s.Execer.RunCommandAndReturn("systemctl", "", "restart", "atest")
return
}

func (s *linuxService) Status() (output string, err error) {
output, err = s.Execer.RunCommandAndReturn("systemctl", "", "status", "atest")
return
}

func (s *linuxService) Install() (output string, err error) {
if err = os.WriteFile(s.scriptPath, []byte(s.script), os.ModeAppend); err == nil {
output, err = s.Execer.RunCommandAndReturn("systemctl", "", "enable", "atest")
}
return
}
49 changes: 39 additions & 10 deletions cmd/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import (
func TestService(t *testing.T) {
root := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer())
root.SetArgs([]string{"service", "fake"})
root.SetOut(new(bytes.Buffer))
err := root.Execute()
assert.NotNil(t, err)

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

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

targetScript := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer())
targetScript.SetArgs([]string{"service", paramAction, "install", "--script-path", tmpFile.Name()})
err = targetScript.Execute()
assert.Nil(t, err)
data, err := os.ReadFile(tmpFile.Name())
assert.Nil(t, err)
assert.Equal(t, script, string(data))

tests := []struct {
name string
action string
targetOS string
expectOutput string
}{{
name: "action: start",
action: "start",
targetOS: "linux",
expectOutput: "output1",
}, {
name: "action: stop",
action: "stop",
targetOS: "linux",
expectOutput: "output2",
}, {
name: "action: restart",
action: "restart",
targetOS: "linux",
expectOutput: "output3",
}, {
name: "action: status",
action: "status",
targetOS: "linux",
expectOutput: "output4",
}, {
name: "action: install",
action: "install",
targetOS: "linux",
expectOutput: "output4",
}, {
name: "action: start, macos",
action: "start",
targetOS: fakeruntime.OSDarwin,
expectOutput: "output4",
}, {
name: "action: stop, macos",
action: "stop",
targetOS: fakeruntime.OSDarwin,
expectOutput: "output4",
}, {
name: "action: restart, macos",
action: "restart",
targetOS: fakeruntime.OSDarwin,
expectOutput: "output4",
}, {
name: "action: status, macos",
action: "status",
targetOS: fakeruntime.OSDarwin,
expectOutput: "output4",
}, {
name: "action: install, macos",
action: "install",
targetOS: fakeruntime.OSDarwin,
expectOutput: "output4",
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := new(bytes.Buffer)
normalRoot := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux", ExpectOutput: tt.expectOutput}, NewFakeGRPCServer())
normalRoot := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: tt.targetOS, ExpectOutput: tt.expectOutput}, NewFakeGRPCServer())
normalRoot.SetOut(buf)
normalRoot.SetArgs([]string{"service", "--action", tt.action})
normalRoot.SetArgs([]string{"service", "--action", tt.action, "--script-path", tmpFile.Name()})
err = normalRoot.Execute()
assert.Nil(t, err)
assert.Equal(t, tt.expectOutput+"\n", buf.String())
Expand Down