diff --git a/cmd/data/linux_service.txt b/cmd/data/linux_service.txt new file mode 100644 index 00000000..0c0a9aed --- /dev/null +++ b/cmd/data/linux_service.txt @@ -0,0 +1,8 @@ +[Unit] +Description=API Testing + +[Service] +ExecStart=/usr/bin/env atest server + +[Install] +WantedBy=multi-user.target diff --git a/cmd/data/macos_service.xml b/cmd/data/macos_service.xml new file mode 100644 index 00000000..7d26ffaf --- /dev/null +++ b/cmd/data/macos_service.xml @@ -0,0 +1,17 @@ + + + + + Label + com.github.linuxsuren.atest + ServiceDescription + API Testing Server + ProgramArguments + + /usr/local/bin/atest + server + + RunAtLoad + + + diff --git a/cmd/service.go b/cmd/service.go index 6a731eed..a02d18dc 100644 --- a/cmd/service.go +++ b/cmd/service.go @@ -5,6 +5,8 @@ import ( "fmt" "os" + _ "embed" + fakeruntime "github.com/linuxsuren/go-fake-runtime" "github.com/spf13/cobra" ) @@ -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 } @@ -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) } @@ -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 +} diff --git a/cmd/service_test.go b/cmd/service_test.go index 83d1430a..bef696e4 100644 --- a/cmd/service_test.go +++ b/cmd/service_test.go @@ -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) @@ -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())