Эх сурвалжийг харах

更新退出码类型并添加后台服务支持

将 `MainV1` 函数的返回值类型从 `int` 更改为 `exitutils.ExitCode`,并在日志中更新了部分信息。新增了对 Windows 服务的支持,包括安装、卸载、启动、停止和重启功能,并在 `README.md` 和 `CHANGELOG.md` 中添加了相关说明。
SongZihuan 1 долоо хоног өмнө
parent
commit
3e16bcf03e

+ 2 - 1
CHANGELOG.md

@@ -11,7 +11,8 @@
 
 - 新增版本号获取功能(仅输出版本号,不输出其他任何内容,不以字母v或V开头)。
 - 加入对 `Windows Console` 的支持。
-- 添加对机器可读日志的支持(`Json`格式)。
+- 添加对机器可读日志的支持(`json`格式)。
+- 添加对`Windows`服务的支持。
 
 ### 修复
 

+ 41 - 1
README.md

@@ -19,6 +19,7 @@
 
 * `lionv1` 是使用控制单元的多服务演示程序。
 * `tigerv1` 是直接运行服务的单服务演示程序。
+* `catv1` 是服务安装演示程序。
 
 入口程序不直接包含太多的实际代码,真正的`main`函数位于`src\mainfunc`下。
 程序的返回值代表程序的`Exit Code`。
@@ -165,11 +166,50 @@ server:  # 系统执行服务所需要的参数
   * 最后优先级:使用随机版本号,版本号为`0.0.0+dev-时间戳-随机值`,随机值在执行`go generate`时生成(位于文件`random_data.txt`中),`go build`后固定。
 * `Version` 版本号:`SemanticVersioning`前添加`v`的字符串。
 
+## 后台服务
+
+虽然`lionv1`和`tigerv1`也可以作为后台服务,但是我使用了`catv1`进行了更高层次的抽象,使得在`Windows`和`Linux`上可以安装服务程序。
+
+### 安装
+
+```shell
+$ catv1 install <命令行参数列表>
+```
+
+使用此命令可以在`Windows`中或`Linux`中注册一个服务,服务名称为:`<resource.Name>-cat-v1`。
+
+注意:安装后可执行程序`catv1`仍需保留在原来位置,不可移动。
+
+### 卸载
+
+```shell
+$ catv1 uninstall
+```
+
+### 启动
+
+```shell
+$ catv1 start
+```
+
+启动不需要指定命令行参数,命令行参数在`install`时即确定。
+
+### 停止
+
+```shell
+$ catv1 stop
+```
+
+### 重启
+
+```shell
+$ catv1 restart
+```
+
 ## 日后升级计划
 
 1. 单元测试
 2. GitHub Action
-3. 对`Windows`服务的支持。
 
 ## 协议
 

+ 1 - 0
go.mod

@@ -5,6 +5,7 @@ go 1.23.0
 toolchain go1.23.4
 
 require (
+	github.com/kardianos/service v1.2.2 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	golang.org/x/sys v0.31.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect

+ 3 - 0
go.sum

@@ -1,5 +1,8 @@
+github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60=
+github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
 golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

+ 15 - 0
src/cmd/catv1/main.go

@@ -0,0 +1,15 @@
+// Copyright 2025 BackendServerTemplate Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	_ "github.com/SongZihuan/BackendServerTemplate/src/global"
+	catv1 "github.com/SongZihuan/BackendServerTemplate/src/mainfunc/cat/v1"
+	"os"
+)
+
+func main() {
+	os.Exit(int(catv1.MainV1()))
+}

+ 1 - 1
src/cmd/lionv1/main.go

@@ -11,5 +11,5 @@ import (
 )
 
 func main() {
-	os.Exit(lionv1.MainV1())
+	os.Exit(int(lionv1.MainV1()))
 }

+ 1 - 1
src/cmd/tigerv1/main.go

@@ -11,5 +11,5 @@ import (
 )
 
 func main() {
-	os.Exit(tigerv1.MainV1())
+	os.Exit(int(tigerv1.MainV1()))
 }

+ 133 - 0
src/mainfunc/cat/v1/main.go

@@ -0,0 +1,133 @@
+// Copyright 2025 BackendServerTemplate Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package v1
+
+import (
+	"fmt"
+	"github.com/SongZihuan/BackendServerTemplate/src/global"
+	"github.com/SongZihuan/BackendServerTemplate/src/logger"
+	"github.com/SongZihuan/BackendServerTemplate/src/logger/loglevel"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/consoleutils"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/exitutils"
+	"github.com/kardianos/service"
+	"os"
+	"strings"
+)
+
+func MainV1() (exitCode exitutils.ExitCode) {
+	var err error
+
+	err = consoleutils.SetConsoleCPSafe(consoleutils.CodePageUTF8)
+	if err != nil {
+		return exitutils.InitFailedErrorForWin32ConsoleModule(err.Error())
+	}
+
+	err = logger.InitBaseLogger(loglevel.LevelDebug, true, nil, nil, nil, nil)
+	if err != nil {
+		return exitutils.InitFailedErrorForLoggerModule(err.Error())
+	}
+	defer logger.CloseLogger()
+	defer logger.Recover()
+
+	// 定义服务配置
+	svcConfig := &service.Config{
+		Name:        fmt.Sprintf("%s-cat-v1", global.Name),
+		DisplayName: fmt.Sprintf("%s cat v1", global.Name),
+		Description: "简单的Go模板程序",
+	}
+
+	// 解析命令行参数
+	if len(os.Args) > 1 {
+		cmd := os.Args[1]
+		switch strings.ToLower(cmd) {
+		case "install":
+			if len(os.Args) > 2 {
+				svcConfig.Arguments = os.Args[2:]
+			}
+
+			prg := NewProgram()
+			s, err := service.New(prg, svcConfig)
+			if err != nil {
+				return exitutils.InitFailedError("Service New", err.Error())
+			}
+
+			// 安装服务
+			err = s.Install()
+			if err != nil {
+				return exitutils.InitFailedError("Service Install", err.Error())
+			}
+
+			return exitutils.SuccessExit("Service Install Success")
+		case "remove", "uninstall":
+			prg := NewProgram()
+			s, err := service.New(prg, svcConfig)
+			if err != nil {
+				return exitutils.InitFailedError("Service New", err.Error())
+			}
+
+			// 卸载服务
+			err = s.Uninstall()
+			if err != nil {
+				return exitutils.InitFailedError("Service Install", err.Error())
+			}
+
+			return exitutils.SuccessExit("Service Install Success")
+		case "start":
+			prg := NewProgram()
+			s, err := service.New(prg, svcConfig)
+			if err != nil {
+				return exitutils.InitFailedError("Service New", err.Error())
+			}
+
+			// 启动服务
+			err = s.Start()
+			if err != nil {
+				return exitutils.InitFailedError("Service Start", err.Error())
+			}
+
+			return exitutils.SuccessExit("Service Start Success")
+		case "stop":
+			prg := NewProgram()
+			s, err := service.New(prg, svcConfig)
+			if err != nil {
+				return exitutils.InitFailedError("Service New", err.Error())
+			}
+
+			// 停止服务
+			err = s.Stop()
+			if err != nil {
+				return exitutils.InitFailedError("Service Stop", err.Error())
+			}
+
+			return exitutils.SuccessExit("Service Stop Success")
+		case "restart":
+			prg := NewProgram()
+			s, err := service.New(prg, svcConfig)
+			if err != nil {
+				return exitutils.InitFailedError("Service New", err.Error())
+			}
+
+			// 停止服务
+			err = s.Restart()
+			if err != nil {
+				return exitutils.InitFailedError("Service Restart", err.Error())
+			}
+
+			return exitutils.SuccessExit("Service Restart Success")
+		default:
+			// pass
+		}
+	}
+
+	prg := NewProgram()
+	s, err := service.New(prg, svcConfig)
+	if err != nil {
+		return exitutils.InitFailedError("Service New", err.Error())
+	}
+
+	_ = s.Run()
+
+	return prg.ExitCode()
+}

+ 122 - 0
src/mainfunc/cat/v1/service.go

@@ -0,0 +1,122 @@
+// Copyright 2025 BackendServerTemplate Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package v1
+
+import (
+	"errors"
+	"github.com/SongZihuan/BackendServerTemplate/src/commandlineargs"
+	"github.com/SongZihuan/BackendServerTemplate/src/config"
+	"github.com/SongZihuan/BackendServerTemplate/src/config/configparser"
+	"github.com/SongZihuan/BackendServerTemplate/src/consolewatcher"
+	"github.com/SongZihuan/BackendServerTemplate/src/logger"
+	"github.com/SongZihuan/BackendServerTemplate/src/server/example3"
+	"github.com/SongZihuan/BackendServerTemplate/src/server/servercontext"
+	"github.com/SongZihuan/BackendServerTemplate/src/server/serverinterface"
+	"github.com/SongZihuan/BackendServerTemplate/src/signalwatcher"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/consoleutils"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/exitutils"
+	"github.com/kardianos/service"
+	"os"
+)
+
+type Program struct {
+	sigchan             chan os.Signal
+	consolechan         chan consoleutils.Event
+	consolewaitexitchan chan any
+	stopErr             error
+	ser                 serverinterface.Server
+	exitCode            exitutils.ExitCode
+}
+
+func NewProgram() *Program {
+	return &Program{}
+}
+
+func (p *Program) Start(s service.Service) error {
+	err := commandlineargs.InitCommandLineArgsParser(nil)
+	if err != nil {
+		if errors.Is(err, commandlineargs.StopRun) {
+			p.exitCode = exitutils.SuccessExitQuite()
+			return err
+		}
+
+		p.exitCode = exitutils.InitFailedError("Command Line Args Parser", err.Error())
+		return err
+	}
+
+	err = config.InitConfig(&config.ConfigOption{
+		ConfigFilePath: commandlineargs.ConfigFile(),
+		OutputFilePath: commandlineargs.OutputConfigFile(),
+		Provider:       configparser.NewYamlProvider(),
+	})
+	if err != nil {
+		p.exitCode = exitutils.InitFailedError("Config file read and parser", err.Error())
+		return err
+	}
+
+	p.sigchan = signalwatcher.NewSignalExitChannel()
+
+	p.consolechan, p.consolewaitexitchan, err = consolewatcher.NewWin32ConsoleExitChannel()
+	if err != nil {
+		p.exitCode = exitutils.InitFailedError("Win32 console channel", err.Error())
+		return err
+	}
+
+	p.ser, _, err = example3.NewServerExample3(&example3.ServerExample3Option{
+		StopWaitTime: config.Data().Server.StopWaitTimeDuration,
+	})
+	if err != nil {
+		return exitutils.InitFailedError("Server Example1", err.Error())
+	}
+
+	logger.Infof("Start to run server example 3")
+	go p.ser.Run()
+	go func() {
+		select {
+		case sig := <-p.sigchan:
+			logger.Warnf("stop by signal (%s)", sig.String())
+			err = nil
+			p.stopErr = nil
+		case event := <-p.consolechan:
+			logger.Infof("stop by console event (%s)", event.String())
+			err = nil
+			p.stopErr = nil
+		case <-p.ser.GetCtx().Listen():
+			err = p.ser.GetCtx().Error()
+			if err == nil || errors.Is(err, servercontext.StopAllTask) {
+				logger.Infof("stop by server")
+				err = nil
+				p.stopErr = nil
+			} else {
+				logger.Errorf("stop by server with error: %s", err.Error())
+				p.stopErr = err
+			}
+		}
+
+		p.stopErr = s.Stop()
+		if p.stopErr != nil {
+			p.exitCode = exitutils.RunErrorQuite()
+		}
+	}()
+
+	return nil
+}
+
+func (p *Program) Stop(s service.Service) error {
+	p.ser.Stop()
+	close(p.consolewaitexitchan)
+
+	if p.stopErr != nil {
+		p.exitCode = exitutils.RunError(p.stopErr.Error())
+		return p.stopErr
+	}
+
+	p.exitCode = exitutils.SuccessExit("all tasks are completed and the main go routine exits")
+	return nil
+}
+
+func (p *Program) ExitCode() exitutils.ExitCode {
+	return p.exitCode
+}

+ 1 - 1
src/mainfunc/lion/v1/main.go

@@ -21,7 +21,7 @@ import (
 	"github.com/SongZihuan/BackendServerTemplate/src/utils/exitutils"
 )
 
-func MainV1() (exitCode int) {
+func MainV1() (exitCode exitutils.ExitCode) {
 	var err error
 
 	err = consoleutils.SetConsoleCPSafe(consoleutils.CodePageUTF8)

+ 2 - 2
src/mainfunc/tiger/v1/main.go

@@ -19,7 +19,7 @@ import (
 	"github.com/SongZihuan/BackendServerTemplate/src/utils/exitutils"
 )
 
-func MainV1() (exitCode int) {
+func MainV1() (exitCode exitutils.ExitCode) {
 	var err error
 
 	err = consoleutils.SetConsoleCPSafe(consoleutils.CodePageUTF8)
@@ -66,7 +66,7 @@ func MainV1() (exitCode int) {
 		return exitutils.InitFailedError("Server Example1", err.Error())
 	}
 
-	logger.Infof("Start to run server controller")
+	logger.Infof("Start to run server example 1")
 	go ser.Run()
 
 	var stopErr error

+ 121 - 0
src/server/example3/server.go

@@ -0,0 +1,121 @@
+// Copyright 2025 BackendServerTemplate Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package example3
+
+import (
+	"github.com/SongZihuan/BackendServerTemplate/src/logger"
+	"github.com/SongZihuan/BackendServerTemplate/src/server/servercontext"
+	"github.com/SongZihuan/BackendServerTemplate/src/server/serverinterface"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/strconvutils"
+	"sync"
+	"time"
+)
+
+type ServerExample3 struct {
+	running      bool
+	ctx          *servercontext.ServerContext
+	name         string
+	wg           *sync.WaitGroup
+	stopWaitTime time.Duration
+}
+
+type ServerExample3Option struct {
+	StopWaitTime time.Duration
+}
+
+func NewServerExample3(opt *ServerExample3Option) (*ServerExample3, *servercontext.ServerContext, error) {
+	ctx := servercontext.NewServerContext()
+
+	if opt == nil {
+		opt = &ServerExample3Option{
+			StopWaitTime: 10 * time.Second,
+		}
+	} else {
+		if opt.StopWaitTime == 0 {
+			opt.StopWaitTime = 10 * time.Second
+		}
+	}
+
+	server := &ServerExample3{
+		ctx:          ctx,
+		running:      false,
+		name:         "example3",
+		wg:           new(sync.WaitGroup),
+		stopWaitTime: opt.StopWaitTime,
+	}
+	err := server.init()
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return server, ctx, nil
+}
+
+func (s *ServerExample3) init() error {
+	return nil
+}
+
+func (s *ServerExample3) Name() string {
+	return s.name
+}
+
+func (s *ServerExample3) GetCtx() *servercontext.ServerContext {
+	return s.ctx
+}
+
+func (s *ServerExample3) Run() {
+	s.running = true
+	defer func() {
+		s.running = false
+	}()
+
+	s.wg = new(sync.WaitGroup)
+	s.wg.Add(1)
+	defer s.wg.Done()
+
+MainCycle:
+	for {
+		logger.Warnf("Example3: I am running!")
+
+		select {
+		case <-s.ctx.Listen():
+			logger.Warnf("Example3: I am stop!")
+			break MainCycle
+		case <-time.After(2 * time.Second):
+			continue
+		}
+	}
+}
+
+func (s *ServerExample3) Stop() {
+	s.ctx.StopTask()
+	if s.wg != nil {
+		wgchan := make(chan any)
+
+		go func() {
+			s.wg.Wait()
+			close(wgchan)
+		}()
+
+		select {
+		case <-time.After(s.stopWaitTime):
+			logger.Errorf("%s - 退出清理超时... (%s)", s.name, strconvutils.TimeDurationToString(s.stopWaitTime))
+		case <-wgchan:
+			// pass
+		}
+	}
+}
+
+func (s *ServerExample3) IsRunning() bool {
+	return s.running
+}
+
+func _test() {
+	var a serverinterface.Server
+	var b *ServerExample3
+
+	a = b
+	_ = a
+}

+ 23 - 9
src/utils/exitutils/exit.go

@@ -5,6 +5,7 @@
 package exitutils
 
 import (
+	"fmt"
 	"github.com/SongZihuan/BackendServerTemplate/src/logger"
 	"log"
 )
@@ -15,11 +16,17 @@ const (
 	exitCodeErrorLogMustBeReady = 254
 )
 
-func getExitCode(defaultExitCode int, exitCode ...int) (ec int) {
+type ExitCode int
+
+func (e ExitCode) Error() string {
+	return fmt.Sprintf("Exit with code %d", e)
+}
+
+func getExitCode(defaultExitCode int, exitCode ...int) (ec ExitCode) {
 	if len(exitCode) == 1 {
-		ec = exitCode[0]
+		ec = ExitCode(exitCode[0])
 	} else {
-		ec = defaultExitCode
+		ec = ExitCode(defaultExitCode)
 	}
 
 	if ec < exitCodeMin {
@@ -33,7 +40,7 @@ func getExitCode(defaultExitCode int, exitCode ...int) (ec int) {
 	return ec
 }
 
-func InitFailedErrorForWin32ConsoleModule(reason string, exitCode ...int) int {
+func InitFailedErrorForWin32ConsoleModule(reason string, exitCode ...int) ExitCode {
 	if reason == "" {
 		reason = "no reason"
 	}
@@ -46,7 +53,7 @@ func InitFailedErrorForWin32ConsoleModule(reason string, exitCode ...int) int {
 	return ec
 }
 
-func InitFailedErrorForLoggerModule(reason string, exitCode ...int) int {
+func InitFailedErrorForLoggerModule(reason string, exitCode ...int) ExitCode {
 	if reason == "" {
 		reason = "no reason"
 	}
@@ -59,7 +66,7 @@ func InitFailedErrorForLoggerModule(reason string, exitCode ...int) int {
 	return ec
 }
 
-func InitFailedError(module string, reason string, exitCode ...int) int {
+func InitFailedError(module string, reason string, exitCode ...int) ExitCode {
 	if !logger.IsReady() {
 		return exitCodeErrorLogMustBeReady
 	}
@@ -76,7 +83,14 @@ func InitFailedError(module string, reason string, exitCode ...int) int {
 	return ec
 }
 
-func RunError(reason string, exitCode ...int) int {
+func RunErrorQuite(exitCode ...int) ExitCode {
+	if !logger.IsReady() {
+		return exitCodeErrorLogMustBeReady
+	}
+	return getExitCode(1, exitCode...)
+}
+
+func RunError(reason string, exitCode ...int) ExitCode {
 	if !logger.IsReady() {
 		return exitCodeErrorLogMustBeReady
 	}
@@ -93,7 +107,7 @@ func RunError(reason string, exitCode ...int) int {
 	return ec
 }
 
-func SuccessExit(reason string, exitCode ...int) int {
+func SuccessExit(reason string, exitCode ...int) ExitCode {
 	if !logger.IsReady() {
 		return exitCodeErrorLogMustBeReady
 	}
@@ -109,7 +123,7 @@ func SuccessExit(reason string, exitCode ...int) int {
 	return ec
 }
 
-func SuccessExitQuite(exitCode ...int) int {
+func SuccessExitQuite(exitCode ...int) ExitCode {
 	if !logger.IsReady() {
 		return exitCodeErrorLogMustBeReady
 	}