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

增加对Windows控制台的支持

增加了对Windows控制台事件的监听和处理,包括CTRL+C、CTRL+BREAK以及控制台关闭等事件,并在配置文件中新增了相应的配置项。同时,优化了信号处理逻辑,使其能够更好地适应不同操作系统。
SongZihuan 1 долоо хоног өмнө
parent
commit
3879365b17

+ 1 - 0
CHANGELOG.md

@@ -10,6 +10,7 @@
 ### 新增功能
 
 - 新增版本号获取功能(仅输出版本号,不输出其他任何内容,不以字母v或V开头)。
+- 加入对 `Windows Console` 的支持。
 
 ## [0.2.0] - 2025-04-16
 

+ 14 - 7
README.md

@@ -123,10 +123,17 @@ logger:
         write-with-date-prefix: ""
 
 signal: # 信号除了机制(管理接收程序退出信号)。sigkill 等信号是不可捕获的,是强制退出的,因此此处无法控制这类信号。虽然windows本身不具有Linux这种信号机制,但是Go在信号方面做了一层模拟,使得控制它ctrl+c可以转换为相应信号。
-    sigint-exit: enable  # 收到 sigint 信号后退出
-    sigterm-exit: enable  # 收到 sigterm 信号后退出
-    sighup-exit: enable  # 收到 sighup 信号后退出
-    sigquit-exit: enable  # 收到 sigquit 信号后退出
+    use-on: not-win32  # 启动模式:any表示全平台、only-win32表示仅windows平台、not-win32表示除windows以外所有平台,never表示任何平台均不启用。
+    sigint-exit: enable  # 收到 sigint 信号后退出 (Windows中可一半呢由ctrl+c触发)
+    sigterm-exit: enable  # 收到 sigterm 信号后退出 (Windows中一般由系统欻)
+    sighup-exit: enable  # 收到 sighup 信号后退出 
+    sigquit-exit: enable  # 收到 sigquit 信号后退出(Windows中一般也ctrl+break触发)
+
+win32-console:  # 控制台管理,比起处理信号量,在Windows平台使用控制台API更接近原生且合理。
+    use-on: only-win32  # 启动方式:any或only-win32表示仅在windows平台启用。never/not-win32表示任何平台均不启用。
+    ctrl-c-exit: enable  # 接收到ctrl+c是否退出
+    ctrl-break-exit: enable  # 接收到ctrl+break是否退出
+    console-close-recovery: disable  # 当用户关闭控制台后,是否启用一个新的临时的控制台输出日志(通常不建议,因为关闭控制台即意味着程序退出,只有5000ms的时间给程序进行清理操作。同时程序一般清理时间不会太久,可能在新控制台启用前就已经完成程序退出的所有准备)
 
 server:  # 系统执行服务所需要的参数
     stop-wait-time: 10s  # 服务退出时,等待清理结束的最长时间。
@@ -148,9 +155,9 @@ server:  # 系统执行服务所需要的参数
 
 ## 日后升级计划
 
-1. Windows的`Console`机制的利用。
-2. 单元测试
-3. GitHub Action
+1. 单元测试
+2. GitHub Action
+3. 将部分`panic`转换为`logger.Panic`。
 
 ## 协议
 

+ 24 - 3
src/config/base_data.go

@@ -7,9 +7,10 @@ import (
 
 type ConfigData struct {
 	GlobalConfig `json:",inline" yaml:",inline"`
-	Logger       LoggerConfig `json:"logger" yaml:"logger"`
-	Signal       SignalConfig `json:"signal" yaml:"signal"`
-	Server       ServerConfig `json:"server" yaml:"server"`
+	Logger       LoggerConfig       `json:"logger" yaml:"logger"`
+	Signal       SignalConfig       `json:"signal" yaml:"signal"`
+	Win32Console Win32ConsoleConfig `json:"win32-console" yaml:"win32-console"`
+	Server       ServerConfig       `json:"server" yaml:"server"`
 }
 
 func (d *ConfigData) init(filePath string, provider configparser.ConfigParserProvider) (err configerror.Error) {
@@ -28,6 +29,11 @@ func (d *ConfigData) init(filePath string, provider configparser.ConfigParserPro
 		return cfgErr
 	}
 
+	cfgErr = d.Win32Console.init(filePath, provider)
+	if cfgErr != nil {
+		return cfgErr
+	}
+
 	cfgErr = d.Server.init(filePath, provider)
 	if cfgErr != nil {
 		return cfgErr
@@ -52,6 +58,11 @@ func (d *ConfigData) setDefault(c *configInfo) (err configerror.Error) {
 		return cfgErr
 	}
 
+	cfgErr = d.Win32Console.setDefault(c)
+	if cfgErr != nil {
+		return cfgErr
+	}
+
 	cfgErr = d.Server.setDefault(c)
 	if cfgErr != nil {
 		return cfgErr
@@ -76,6 +87,11 @@ func (d *ConfigData) check(c *configInfo) (err configerror.Error) {
 		return cfgErr
 	}
 
+	cfgErr = d.Win32Console.check(c)
+	if cfgErr != nil {
+		return cfgErr
+	}
+
 	cfgErr = d.Server.check(c)
 	if cfgErr != nil {
 		return cfgErr
@@ -100,6 +116,11 @@ func (d *ConfigData) process(c *configInfo) (err configerror.Error) {
 		return cfgErr
 	}
 
+	cfgErr = d.Win32Console.process(c)
+	if cfgErr != nil {
+		return cfgErr
+	}
+
 	cfgErr = d.Server.process(c)
 	if cfgErr != nil {
 		return cfgErr

+ 23 - 0
src/config/signal_config.go

@@ -3,10 +3,14 @@ package config
 import (
 	"github.com/SongZihuan/BackendServerTemplate/src/config/configerror"
 	"github.com/SongZihuan/BackendServerTemplate/src/config/configparser"
+	"github.com/SongZihuan/BackendServerTemplate/src/logger"
 	"github.com/SongZihuan/BackendServerTemplate/src/utils/typeutils"
+	"runtime"
 )
 
 type SignalConfig struct {
+	UseOn       string               `json:"use-on" yaml:"use-on"`
+	Use         bool                 `json:"-" yaml:"-"`
 	SigIntExit  typeutils.StringBool `json:"sigint-exit" yaml:"sigint-exit"`
 	SigTermExit typeutils.StringBool `json:"sigterm-exit" yaml:"sigterm-exit"`
 	SigHupExit  typeutils.StringBool `json:"sighup-exit" yaml:"sighup-exit"`
@@ -18,6 +22,10 @@ func (d *SignalConfig) init(filePath string, provider configparser.ConfigParserP
 }
 
 func (d *SignalConfig) setDefault(c *configInfo) (err configerror.Error) {
+	if d.UseOn == "" {
+		d.UseOn = "not-win32"
+	}
+
 	d.SigIntExit.SetDefaultEnable()
 	d.SigTermExit.SetDefaultEnable()
 	d.SigHupExit.SetDefaultEnable()
@@ -32,9 +40,24 @@ func (d *SignalConfig) setDefault(c *configInfo) (err configerror.Error) {
 }
 
 func (d *SignalConfig) check(c *configInfo) (err configerror.Error) {
+	if d.UseOn != "any" && d.UseOn != "not-win32" && d.UseOn != "only-win32" && d.UseOn != "never" {
+		return configerror.NewErrorf("bad use-on: %s, must be one of (any, not-win32, only-win32, never)", d.UseOn)
+	}
 	return nil
 }
 
 func (d *SignalConfig) process(c *configInfo) (cfgErr configerror.Error) {
+	switch d.UseOn {
+	case "any":
+		d.Use = true
+	case "never":
+		d.Use = false
+	case "not-win32":
+		d.Use = runtime.GOOS != "windows"
+	case "only-win32":
+		d.Use = runtime.GOOS == "windows"
+	default:
+		logger.Panic("error use-on!") // 正常情况下,非正确值应该在check步骤被返回,若此处发现错误值则可能是check的逻辑有误
+	}
 	return nil
 }

+ 52 - 0
src/config/win32_consolel_config.go

@@ -0,0 +1,52 @@
+package config
+
+import (
+	"github.com/SongZihuan/BackendServerTemplate/src/config/configerror"
+	"github.com/SongZihuan/BackendServerTemplate/src/config/configparser"
+	"github.com/SongZihuan/BackendServerTemplate/src/logger"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/typeutils"
+	"runtime"
+)
+
+type Win32ConsoleConfig struct {
+	UseOn                string               `json:"use-on" yaml:"use-on"`
+	Use                  bool                 `json:"-" yaml:"-"`
+	CtrlCExit            typeutils.StringBool `json:"ctrl-c-exit" yaml:"ctrl-c-exit"`
+	CtrlBreakExit        typeutils.StringBool `json:"ctrl-break-exit" yaml:"ctrl-break-exit"`
+	ConsoleCloseRecovery typeutils.StringBool `json:"console-close-recovery" yaml:"console-close-recovery"`
+}
+
+func (d *Win32ConsoleConfig) init(filePath string, provider configparser.ConfigParserProvider) (err configerror.Error) {
+	return nil
+}
+
+func (d *Win32ConsoleConfig) setDefault(c *configInfo) (err configerror.Error) {
+	if d.UseOn == "" {
+		d.UseOn = "only-win32"
+	}
+
+	d.CtrlCExit.SetDefaultEnable()
+	d.CtrlBreakExit.SetDefaultEnable()
+	d.ConsoleCloseRecovery.SetDefaultDisable()
+
+	return nil
+}
+
+func (d *Win32ConsoleConfig) check(c *configInfo) (err configerror.Error) {
+	if d.UseOn != "any" && d.UseOn != "not-win32" && d.UseOn != "only-win32" && d.UseOn != "never" {
+		return configerror.NewErrorf("bad use-on: %s, must be one of (any, not-win32, only-win32, never)", d.UseOn)
+	}
+	return nil
+}
+
+func (d *Win32ConsoleConfig) process(c *configInfo) (cfgErr configerror.Error) {
+	switch d.UseOn {
+	case "any", "only-win32":
+		d.Use = runtime.GOOS == "windows"
+	case "never", "not-win32":
+		d.Use = false
+	default:
+		logger.Panic("error use-on!") // 正常情况下,非正确值应该在check步骤被返回,若此处发现错误值则可能是check的逻辑有误
+	}
+	return nil
+}

+ 14 - 0
src/consolewatcher/posix.go

@@ -0,0 +1,14 @@
+// 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.
+
+//go:build !windows
+
+package consolewatcher
+
+func NewWin32ConsoleExitChannel() (chan consoleutils.Event, chan any, error) {
+	var exitChannel = make(chan consoleutils.Event)
+	var waitExitChannel = make(chan any)
+
+	return exitChannel, waitExitChannel, nil
+}

+ 72 - 0
src/consolewatcher/win32.go

@@ -0,0 +1,72 @@
+// 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.
+
+//go:build windows
+
+package consolewatcher
+
+import (
+	"github.com/SongZihuan/BackendServerTemplate/src/config"
+	"github.com/SongZihuan/BackendServerTemplate/src/logger"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/consoleutils"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/strconvutils"
+	"time"
+)
+
+// 控制台事件处理函数(回调)
+func consoleHandler(exitChannel chan consoleutils.Event, waitExitChannel chan any) func(event uint) bool {
+	return func(event uint) bool {
+		switch event {
+		case consoleutils.CTRL_CLOSE_EVENT.GetCode(), consoleutils.CTRL_LOGOFF_EVENT.GetCode(), consoleutils.CTRL_SHUTDOWN_EVENT.GetCode():
+			exitChannel <- consoleutils.EventMap[event]
+
+			if config.Data().Win32Console.ConsoleCloseRecovery.IsEnable(false) {
+				err := consoleutils.MakeNewConsole(consoleutils.CodePageUTF8)
+				if err != nil {
+					logger.Errorf("win32 make new console failed: %s", err.Error())
+				}
+			}
+
+			logger.Warnf("终端暂时重启,等待程序清理完毕,请勿关闭当前终端!")
+			logger.Warnf("若不希望重启终端,可在配置文件处关闭。")
+
+			select {
+			case <-waitExitChannel:
+				// pass
+			case <-time.After(4500 * time.Millisecond):
+				logger.Errorf("Windows Console - 退出清理超时... (%s)", strconvutils.TimeDurationToString(4500*time.Millisecond))
+			}
+			return true
+		case consoleutils.CTRL_C_EVENT.GetCode():
+			if config.Data().Win32Console.CtrlCExit.IsEnable(true) {
+				exitChannel <- consoleutils.CTRL_C_EVENT
+			}
+			return true
+		case consoleutils.CTRL_BREAK_EVENT.GetCode():
+			if config.Data().Win32Console.CtrlBreakExit.IsEnable(true) {
+				exitChannel <- consoleutils.CTRL_BREAK_EVENT
+			}
+			return true
+		default:
+			logger.Errorf("未知事件: %d\n", event)
+			return false
+		}
+	}
+}
+
+func NewWin32ConsoleExitChannel() (chan consoleutils.Event, chan any, error) {
+	var exitChannel = make(chan consoleutils.Event)
+	var waitExitChannel = make(chan any)
+
+	if !config.Data().Win32Console.Use {
+		return exitChannel, waitExitChannel, nil
+	}
+
+	err := consoleutils.SetConsoleCtrlHandler(consoleHandler(exitChannel, waitExitChannel), true)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return exitChannel, waitExitChannel, nil
+}

+ 27 - 7
src/mainfunc/lion/v1/main.go

@@ -9,6 +9,7 @@ import (
 	"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/logger/loglevel"
 	"github.com/SongZihuan/BackendServerTemplate/src/server/controller"
@@ -16,12 +17,18 @@ import (
 	"github.com/SongZihuan/BackendServerTemplate/src/server/example2"
 	"github.com/SongZihuan/BackendServerTemplate/src/server/servercontext"
 	"github.com/SongZihuan/BackendServerTemplate/src/signalwatcher"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/consoleutils"
 	"github.com/SongZihuan/BackendServerTemplate/src/utils/exitutils"
 )
 
 func MainV1() (exitCode int) {
 	var err error
 
+	err = consoleutils.SetConsoleCPSafe(consoleutils.CodePageUTF8)
+	if err != nil {
+		return exitutils.InitFailedErrorForWin32ConsoleModule(err.Error())
+	}
+
 	err = logger.InitBaseLogger(loglevel.LevelDebug, true, true, nil, nil)
 	if err != nil {
 		return exitutils.InitFailedErrorForLoggerModule(err.Error())
@@ -47,7 +54,11 @@ func MainV1() (exitCode int) {
 	}
 
 	sigchan := signalwatcher.NewSignalExitChannel()
-	defer close(sigchan)
+
+	consolechan, consolewaitexitchan, err := consolewatcher.NewWin32ConsoleExitChannel()
+	if err != nil {
+		return exitutils.InitFailedError("Win32 console channel", err.Error())
+	}
 
 	ctrl, err := controller.NewController(&controller.ControllerOption{
 		StopWaitTime: config.Data().Server.StopWaitTimeDuration,
@@ -79,24 +90,33 @@ func MainV1() (exitCode int) {
 	logger.Infof("Start to run server controller")
 	go ctrl.Run()
 
+	var stopErr error
 	select {
-	case <-sigchan:
-		logger.Infof("stop by signal")
+	case sig := <-sigchan:
+		logger.Warnf("stop by signal (%s)", sig.String())
+		err = nil
+		stopErr = nil
+	case event := <-consolechan:
+		logger.Infof("stop by console event (%s)", event.String())
 		err = nil
+		stopErr = nil
 	case <-ctrl.GetCtx().Listen():
 		err = ctrl.GetCtx().Error()
 		if err == nil || errors.Is(err, servercontext.StopAllTask) {
-			err = nil
 			logger.Infof("stop by controller")
+			err = nil
+			stopErr = nil
 		} else {
-			logger.Errorf("stop by controller with error")
+			logger.Errorf("stop by controller with error: %s", err.Error())
+			stopErr = err
 		}
 	}
 
 	ctrl.Stop()
+	close(consolewaitexitchan)
 
-	if err != nil {
-		return exitutils.RunError(err.Error())
+	if stopErr != nil {
+		return exitutils.RunError(stopErr.Error())
 	}
 
 	return exitutils.SuccessExit("all tasks are completed and the main go routine exits")

+ 27 - 7
src/mainfunc/tiger/v1/main.go

@@ -9,17 +9,24 @@ import (
 	"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/logger/loglevel"
 	"github.com/SongZihuan/BackendServerTemplate/src/server/example1"
 	"github.com/SongZihuan/BackendServerTemplate/src/server/servercontext"
 	"github.com/SongZihuan/BackendServerTemplate/src/signalwatcher"
+	"github.com/SongZihuan/BackendServerTemplate/src/utils/consoleutils"
 	"github.com/SongZihuan/BackendServerTemplate/src/utils/exitutils"
 )
 
 func MainV1() (exitCode int) {
 	var err error
 
+	err = consoleutils.SetConsoleCPSafe(consoleutils.CodePageUTF8)
+	if err != nil {
+		return exitutils.InitFailedErrorForWin32ConsoleModule(err.Error())
+	}
+
 	err = logger.InitBaseLogger(loglevel.LevelDebug, true, true, nil, nil)
 	if err != nil {
 		return exitutils.InitFailedErrorForLoggerModule(err.Error())
@@ -45,7 +52,11 @@ func MainV1() (exitCode int) {
 	}
 
 	sigchan := signalwatcher.NewSignalExitChannel()
-	defer close(sigchan)
+
+	consolechan, consolewaitexitchan, err := consolewatcher.NewWin32ConsoleExitChannel()
+	if err != nil {
+		return exitutils.InitFailedError("Win32 console channel", err.Error())
+	}
 
 	ser, _, err := example1.NewServerExample1(&example1.ServerExample1Option{
 		StopWaitTime: config.Data().Server.StopWaitTimeDuration,
@@ -57,24 +68,33 @@ func MainV1() (exitCode int) {
 	logger.Infof("Start to run server controller")
 	go ser.Run()
 
+	var stopErr error
 	select {
-	case <-sigchan:
-		logger.Infof("stop by signal")
+	case sig := <-sigchan:
+		logger.Warnf("stop by signal (%s)", sig.String())
 		err = nil
+		stopErr = nil
+	case event := <-consolechan:
+		logger.Infof("stop by console event (%s)", event.String())
+		err = nil
+		stopErr = nil
 	case <-ser.GetCtx().Listen():
 		err = ser.GetCtx().Error()
 		if err == nil || errors.Is(err, servercontext.StopAllTask) {
+			logger.Infof("stop by server")
 			err = nil
-			logger.Infof("stop by controller")
+			stopErr = nil
 		} else {
-			logger.Errorf("stop by controller with error")
+			logger.Errorf("stop by server with error: %s", err.Error())
+			stopErr = err
 		}
 	}
 
 	ser.Stop()
+	close(consolewaitexitchan)
 
-	if err != nil {
-		return exitutils.RunError(err.Error())
+	if stopErr != nil {
+		return exitutils.RunError(stopErr.Error())
 	}
 
 	return exitutils.SuccessExit("all tasks are completed and the main go routine exits")

+ 2 - 1
src/server/controller/controller.go

@@ -232,8 +232,9 @@ func (s *Controller) Stop() {
 
 		select {
 		case <-time.After(s.stopWaitTime):
-			logger.Errorf("stop timeout (%s)", strconvutils.TimeDurationToString(s.stopWaitTime))
+			logger.Errorf("%s - 退出清理超时... (%s)", s.name, strconvutils.TimeDurationToString(s.stopWaitTime))
 		case <-wgchan:
+			// pass
 		}
 	}
 }

+ 4 - 0
src/server/example1/server.go

@@ -7,8 +7,10 @@ package example1
 import (
 	"fmt"
 	"github.com/SongZihuan/BackendServerTemplate/src/global"
+	"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"
 )
@@ -105,7 +107,9 @@ func (s *ServerExample1) Stop() {
 
 		select {
 		case <-time.After(s.stopWaitTime):
+			logger.Errorf("%s - 退出清理超时... (%s)", s.name, strconvutils.TimeDurationToString(s.stopWaitTime))
 		case <-wgchan:
+			// pass
 		}
 	}
 }

+ 4 - 0
src/server/example2/server.go

@@ -6,8 +6,10 @@ package example2
 
 import (
 	"fmt"
+	"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"
 )
@@ -100,7 +102,9 @@ func (s *ServerExample2) Stop() {
 
 		select {
 		case <-time.After(s.stopWaitTime):
+			logger.Errorf("%s - 退出清理超时... (%s)", s.name, strconvutils.TimeDurationToString(s.stopWaitTime))
 		case <-wgchan:
+			// pass
 		}
 	}
 }

+ 33 - 6
src/signalwatcher/signal.go

@@ -13,24 +13,51 @@ import (
 
 func NewSignalExitChannel() chan os.Signal {
 	var exitChannel = make(chan os.Signal)
-	var signalList = make([]os.Signal, 0, 4)
+
+	if !config.Data().Signal.Use {
+		return exitChannel
+	}
+
+	var sigChannel = make(chan os.Signal)
+
+	var signalList = []os.Signal{
+		syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT,
+	}
+	var signalExitMap = make(map[os.Signal]bool, 4)
 
 	if config.Data().Signal.SigIntExit.IsEnable(true) {
-		signalList = append(signalList, syscall.SIGINT)
+		signalExitMap[syscall.SIGINT] = true
+	} else {
+		signalExitMap[syscall.SIGINT] = true
 	}
 
 	if config.Data().Signal.SigTermExit.IsEnable(true) {
-		signalList = append(signalList, syscall.SIGTERM)
+		signalExitMap[syscall.SIGTERM] = true
+	} else {
+		signalExitMap[syscall.SIGTERM] = true
 	}
 
 	if config.Data().Signal.SigHupExit.IsEnable(true) {
-		signalList = append(signalList, syscall.SIGHUP)
+		signalExitMap[syscall.SIGHUP] = true
+	} else {
+		signalExitMap[syscall.SIGHUP] = true
 	}
 
 	if config.Data().Signal.SigQuitExit.IsEnable(false) {
-		signalList = append(signalList, syscall.SIGQUIT)
+		signalExitMap[syscall.SIGQUIT] = true
+	} else {
+		signalExitMap[syscall.SIGQUIT] = true
 	}
 
-	signal.Notify(exitChannel, signalList...)
+	go func() {
+		signal.Notify(sigChannel, signalList...)
+
+		for sig := range sigChannel {
+			if yes, ok := signalExitMap[sig]; ok && yes {
+				exitChannel <- sig
+			}
+		}
+	}()
+
 	return exitChannel
 }

+ 62 - 0
src/utils/consoleutils/global.go

@@ -0,0 +1,62 @@
+// 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 consoleutils
+
+type Event interface {
+	String() string
+	ConsoleEvent()
+	GetCode() uint
+}
+
+type EventData struct {
+	Name string
+	Code uint
+}
+
+func (e *EventData) String() string {
+	return e.Name
+}
+
+func (e *EventData) GetCode() uint {
+	return e.Code
+}
+
+func (*EventData) ConsoleEvent() {}
+
+// 定义控制台事件类型
+var (
+	CTRL_C_EVENT Event = &EventData{
+		Name: "CTRL_C_EVENT",
+		Code: 0,
+	} // ctrl+c
+
+	CTRL_BREAK_EVENT Event = &EventData{
+		Name: "CTRL_BREAK_EVENT",
+		Code: 1,
+	} // ctrl+break
+
+	CTRL_CLOSE_EVENT Event = &EventData{
+		Name: "CTRL_CLOSE_EVENT",
+		Code: 2,
+	} // console关闭
+
+	CTRL_LOGOFF_EVENT Event = &EventData{
+		Name: "CTRL_LOGOFF_EVENT",
+		Code: 5,
+	} // 用户注销
+
+	CTRL_SHUTDOWN_EVENT Event = &EventData{
+		Name: "CTRL_SHUTDOWN_EVENT",
+		Code: 6,
+	} // 系统关机
+)
+
+var EventMap = map[uint]Event{
+	0: CTRL_C_EVENT,
+	1: CTRL_BREAK_EVENT,
+	2: CTRL_CLOSE_EVENT,
+	5: CTRL_LOGOFF_EVENT,
+	6: CTRL_SHUTDOWN_EVENT,
+}

+ 51 - 0
src/utils/consoleutils/posix.go

@@ -0,0 +1,51 @@
+// 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.
+
+//go:build !windows
+
+package consoleutils
+
+func FreeConsole() error {
+	return nil
+}
+
+func AllocConsole() error {
+	return nil
+}
+
+func BindStdToConsole() error {
+	return nil
+}
+
+func SetConsoleCtrlHandler(handler func(event uint) uintptr, add bool) error {
+	return nil
+}
+
+func MakeNewConsole() error {
+	return nil
+}
+
+func GetConsoleWindow() uintptr {
+	return 0
+}
+
+func HasConsoleWindow() bool {
+	return GetConsoleWindow() != 0
+}
+
+func SetConsoleInputCP(codePage int) error {
+	return nil
+}
+
+func SetConsoleOutputCP(codePage int) error {
+	return nil
+}
+
+func SetConsoleCP(codePage int) error {
+	return nil
+}
+
+func SetConsoleCPSafe(codePage int) error {
+	return nil
+}

+ 158 - 0
src/utils/consoleutils/win32.go

@@ -0,0 +1,158 @@
+// 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.
+
+//go:build windows
+
+package consoleutils
+
+import (
+	"fmt"
+	"os"
+	"syscall"
+)
+
+const (
+	CodePageUTF8 uint = 65001
+	CodePageGBK  uint = 936
+)
+
+var (
+	kernel32 = syscall.NewLazyDLL("kernel32.dll")
+
+	// 获取 FreeConsole 和 AllocConsole 函数
+	freeConsole           = kernel32.NewProc("FreeConsole")
+	allocConsole          = kernel32.NewProc("AllocConsole")
+	setConsoleCtrlHandler = kernel32.NewProc("SetConsoleCtrlHandler")
+	getConsoleWindow      = kernel32.NewProc("GetConsoleWindow")
+	setConsoleCP          = kernel32.NewProc("SetConsoleCP")
+	setConsoleOutputCP    = kernel32.NewProc("SetConsoleOutputCP")
+)
+
+func FreeConsole() error {
+	ret, _, _ := freeConsole.Call()
+	if ret == 0 {
+		return fmt.Errorf("FreeConsole error")
+	}
+	return nil
+}
+
+func AllocConsole() error {
+	ret, _, _ := allocConsole.Call()
+	if ret == 0 {
+		return fmt.Errorf("AllocConsole error")
+	}
+	return nil
+}
+
+func BindStdToConsole() error {
+	conin, err := os.OpenFile("CONIN$", os.O_RDWR, 0)
+	if err != nil {
+		return err
+	}
+
+	conout, err := os.OpenFile("CONOUT$", os.O_RDWR, 0)
+	if err != nil {
+		return err
+	}
+
+	// 不用关闭旧的标准输入/出/错误
+	os.Stdin = conin
+	os.Stdout = conout
+	os.Stderr = conout
+
+	return nil
+}
+
+func SetConsoleCtrlHandler(handler func(event uint) bool, add bool) error {
+	var _add uintptr = 0
+	if add {
+		_add = 1
+	}
+
+	ret, _, _ := setConsoleCtrlHandler.Call(
+		syscall.NewCallback(func(event uint) uintptr {
+			if handler(event) {
+				return 1
+			}
+			return 0
+		}),
+		_add,
+	)
+	if ret == 0 {
+		return fmt.Errorf("SetConsoleCtrlHandler error")
+	}
+
+	return nil
+}
+
+func MakeNewConsole(codePage uint) error {
+	err := FreeConsole()
+	if err != nil {
+		return err
+	}
+
+	err = AllocConsole()
+	if err != nil {
+		return err
+	}
+
+	err = BindStdToConsole()
+	if err != nil {
+		return err
+	}
+
+	err = SetConsoleCP(codePage)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func GetConsoleWindow() uint {
+	handle, _, _ := getConsoleWindow.Call()
+	return uint(handle)
+}
+
+func HasConsoleWindow() bool {
+	return GetConsoleWindow() != 0
+}
+
+func SetConsoleInputCP(codePage uint) error {
+	ret, _, _ := setConsoleCP.Call(uintptr(codePage))
+	if ret == 0 {
+		return fmt.Errorf("SetConsoleInputCP error")
+	}
+	return nil
+}
+
+func SetConsoleOutputCP(codePage uint) error {
+	ret, _, _ := setConsoleOutputCP.Call(uintptr(codePage))
+	if ret == 0 {
+		return fmt.Errorf("SetConsoleOutputCP error")
+	}
+	return nil
+}
+
+func SetConsoleCP(codePage uint) error {
+	err := SetConsoleInputCP(codePage)
+	if err != nil {
+		return err
+	}
+
+	err = SetConsoleOutputCP(codePage)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func SetConsoleCPSafe(codePage uint) error {
+	if !HasConsoleWindow() {
+		return nil
+	}
+
+	return SetConsoleCP(codePage)
+}

+ 13 - 0
src/utils/exitutils/exit.go

@@ -33,6 +33,19 @@ func getExitCode(defaultExitCode int, exitCode ...int) (ec int) {
 	return ec
 }
 
+func InitFailedErrorForWin32ConsoleModule(reason string, exitCode ...int) int {
+	if reason == "" {
+		reason = "no reason"
+	}
+
+	ec := getExitCode(1, exitCode...)
+
+	log.Printf("The module `Win32 Console` init failed (reason: `%s`) .", reason)
+	log.Printf("Now we should exit with code %d.", ec)
+
+	return ec
+}
+
 func InitFailedErrorForLoggerModule(reason string, exitCode ...int) int {
 	if reason == "" {
 		reason = "no reason"