From f2d78cf1e9a3620d915d6586ed809df5042036a9 Mon Sep 17 00:00:00 2001 From: caixw Date: Thu, 11 Apr 2024 14:56:38 +0800 Subject: [PATCH 1/4] =?UTF-8?q?refactor(server/app):=20=E7=B2=BE=E7=AE=80?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 不再公开原来的 App 对象,直接返回接口。 --- server/app/app.go | 60 +++++++++++++------ server/app/cli.go | 17 +++--- server/app/cli_test.go | 12 ++-- server/app/restart.go | 39 ++++++++++++ .../app/{server_test.go => restart_test.go} | 6 +- server/app/server.go | 51 ---------------- 6 files changed, 96 insertions(+), 89 deletions(-) create mode 100644 server/app/restart.go rename server/app/{server_test.go => restart_test.go} (92%) delete mode 100644 server/app/server.go diff --git a/server/app/app.go b/server/app/app.go index 76feef62..3dd5fd0d 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -12,16 +12,27 @@ import ( "github.com/issue9/web" ) -// App [ServerApp] 的简单实现 -type App struct { +// App [web.Server] 的管理接口 +type App interface { + // Exec 运行当前服务 + Exec() error + + // Restart 重启 + // + // 中止旧的 [web.Server],再启动一个新的 [web.Server] 对象。 + // + // NOTE: 如果执行过程中出错,应该尽量阻止旧对象被中止,保证最大限度地可用状态。 + Restart() +} + +type app struct { // 构建新服务的方法 // // 每次重启服务时,都将由此方法生成一个新的服务。 // 只有在返回成功的新实例时,才会替换旧实例,否则旧实例将一直运行。 - NewServer func() (web.Server, error) + newServer func() (web.Server, error) - // 每次关闭服务操作的等待时间 - ShutdownTimeout time.Duration + shutdownTimeout time.Duration srv web.Server srvLock sync.RWMutex @@ -31,29 +42,40 @@ type App struct { restartServerLock sync.Mutex } -func (app *App) getServer() web.Server { +// New 声明一个简要的 [App] 实现 +// +// shutdown 每次关闭服务操作的等待时间; +// newServer 构建新服务的方法。 +func New(shutdown time.Duration, newServer func() (web.Server, error)) App { + if newServer == nil { + panic("App.NewServer 不能为空") + } + + return &app{ + shutdownTimeout: shutdown, + newServer: newServer, + exit: make(chan struct{}, 1), + } +} + +func (app *app) getServer() web.Server { app.srvLock.RLock() s := app.srv app.srvLock.RUnlock() return s } -func (app *App) setServer(s web.Server) { +func (app *app) setServer(s web.Server) { app.srvLock.Lock() app.srv = s app.srvLock.Unlock() } // Exec 运行服务 -func (app *App) Exec() (err error) { - if app.NewServer == nil { - panic("App.NewServer 不能为空") - } - - if app.srv, err = app.NewServer(); err != nil { - return +func (app *app) Exec() (err error) { + if app.srv, err = app.newServer(); err != nil { + return err } - app.exit = make(chan struct{}, 1) RESTART: app.restart = false @@ -65,10 +87,10 @@ RESTART: return err } -// RestartServer 触发重启服务 +// Restart 触发重启服务 // // 该方法将关闭现有的服务,并发送运行新服务的指令,不会等待新服务启动完成。 -func (app *App) RestartServer() { +func (app *app) Restart() { app.restartServerLock.Lock() defer app.restartServerLock.Unlock() @@ -76,13 +98,13 @@ func (app *App) RestartServer() { old := app.getServer() - srv, err := app.NewServer() + srv, err := app.newServer() if err != nil { old.Logs().ERROR().Error(err) return } app.setServer(srv) - old.Close(app.ShutdownTimeout) // 新服务声明成功,尝试关闭旧服务。 + old.Close(app.shutdownTimeout) // 新服务声明成功,尝试关闭旧服务。 <-app.exit // 等待 server.Serve 退出 } diff --git a/server/app/cli.go b/server/app/cli.go index 2c1a5155..cdf70f87 100644 --- a/server/app/cli.go +++ b/server/app/cli.go @@ -98,16 +98,16 @@ type CLI[T any] struct { // 默认值为 [flag.ContinueOnError] ErrorHandling flag.ErrorHandling - app *App + app App action string } // Exec 执行命令 // -// args 表示命令行参数,一般为 [os.Args]。 -// // 如果是 [CLI] 本身字段设置有问题会直接 panic,其它错误则返回该错误信息。 -func (cmd *CLI[T]) Exec(args []string) (err error) { +func (cmd *CLI[T]) Exec() (err error) { return cmd.exec(os.Args) } + +func (cmd *CLI[T]) exec(args []string) (err error) { if err = cmd.sanitize(); err != nil { // 字段值有问题,直接 panic。 panic(err) } @@ -180,18 +180,15 @@ func (cmd *CLI[T]) sanitize() error { cmd.ErrorHandling = flag.ContinueOnError } - cmd.app = &App{ - ShutdownTimeout: cmd.ShutdownTimeout, - NewServer: cmd.initServer, - } + cmd.app = New(cmd.ShutdownTimeout, cmd.initServer) return nil } -// RestartServer 触发重启服务 +// Restart 触发重启服务 // // 该方法将关闭现有的服务,并发送运行新服务的指令,不会等待新服务启动完成。 -func (cmd *CLI[T]) RestartServer() { cmd.app.RestartServer() } +func (cmd *CLI[T]) Restart() { cmd.app.Restart() } func (cmd *CLI[T]) initServer() (web.Server, error) { opt, user, err := server.LoadOptions[T](cmd.ConfigDir, cmd.ConfigFilename) diff --git a/server/app/cli_test.go b/server/app/cli_test.go index 9d8a90df..d02046c7 100644 --- a/server/app/cli_test.go +++ b/server/app/cli_test.go @@ -36,17 +36,17 @@ func TestCLI(t *testing.T) { return server.New(name, ver, opt) }, } - a.NotError(cmd.Exec([]string{"app", "-v"})).Contains(bs.String(), cmd.Version) + a.NotError(cmd.exec([]string{"app", "-v"})).Contains(bs.String(), cmd.Version) bs.Reset() - a.NotError(cmd.Exec([]string{"app", "-a=install"})).Equal(action, "install") + a.NotError(cmd.exec([]string{"app", "-a=install"})).Equal(action, "install") // RestartServer exit := make(chan struct{}, 10) bs.Reset() go func() { - a.ErrorIs(cmd.Exec([]string{"app", "-a=serve"}), http.ErrServerClosed) + a.ErrorIs(cmd.exec([]string{"app", "-a=serve"}), http.ErrServerClosed) exit <- struct{}{} }() time.Sleep(500 * time.Millisecond) // 等待 go func 启动完成 @@ -55,7 +55,7 @@ func TestCLI(t *testing.T) { s1 := cmd.app.getServer() t1 := s1.Uptime() cmd.Name = "restart1" - cmd.RestartServer() + cmd.Restart() time.Sleep(shutdownTimeout + 500*time.Millisecond) // 此值要大于 CLI.ShutdownTimeout s2 := cmd.app.getServer() t2 := s2.Uptime() @@ -63,7 +63,7 @@ func TestCLI(t *testing.T) { // restart2 cmd.Name = "restart2" - cmd.RestartServer() + cmd.Restart() time.Sleep(shutdownTimeout + 500*time.Millisecond) // 此值要大于 CLI.ShutdownTimeout t3 := cmd.app.getServer().Uptime() a.True(t3.After(t2)) @@ -93,6 +93,6 @@ func TestCLI_sanitize(t *testing.T) { cmd = &CLI[empty]{Name: "abc"} a.PanicString(func() { - _ = cmd.Exec(nil) + _ = cmd.exec(nil) }, "字段 Version 不能为空") } diff --git a/server/app/restart.go b/server/app/restart.go new file mode 100644 index 00000000..a26eca7d --- /dev/null +++ b/server/app/restart.go @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2018-2024 caixw +// +// SPDX-License-Identifier: MIT + +package app + +import ( + "os" + "os/signal" + "syscall" +) + +// SignalHUP 让 a 根据 [HUP] 信号重启服务 +// +// app := &App{...} +// SignalHUP(app) +// +// [HUP]: https://en.wikipedia.org/wiki/SIGHUP +func SignalHUP(a App) { + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGHUP) + Restart(sc, a) +} + +// Restart 根据信号 c 重启 a +// +// 可以结合其它方法一起使用,比如和 fsnotify 一起使用: +// +// watcher := fsnotify.NewWatcher(...) +// Restart(watcher.Event, s) +// +// 也可参考 [SignalHUP]。 +func Restart[T any](c chan T, a App) { + go func() { + for range c { + a.Restart() + } + }() +} diff --git a/server/app/server_test.go b/server/app/restart_test.go similarity index 92% rename from server/app/server_test.go rename to server/app/restart_test.go index d5426f50..c7b3f6cc 100644 --- a/server/app/server_test.go +++ b/server/app/restart_test.go @@ -19,8 +19,8 @@ import ( ) var ( - _ ServerApp = &App{} - _ ServerApp = &CLI[empty]{} + _ App = &app{} + _ App = &CLI[empty]{} ) func TestSignalHUP(t *testing.T) { @@ -44,7 +44,7 @@ func TestSignalHUP(t *testing.T) { SignalHUP(cmd) go func() { - a.ErrorIs(cmd.Exec([]string{"app", "-a=serve"}), http.ErrServerClosed) + a.ErrorIs(cmd.exec([]string{"app", "-a=serve"}), http.ErrServerClosed) exit <- struct{}{} }() time.Sleep(2000 * time.Millisecond) // 等待 go func 启动完成 diff --git a/server/app/server.go b/server/app/server.go deleted file mode 100644 index 8eb85f10..00000000 --- a/server/app/server.go +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-FileCopyrightText: 2018-2024 caixw -// -// SPDX-License-Identifier: MIT - -package app - -import ( - "os" - "os/signal" - "syscall" -) - -// ServerApp 定义了服务类型的 APP 的接口 -// -// [App] 和 [CLI] 均实现了此接口。 -type ServerApp interface { - // RestartServer 重启 APP - // - // 中止旧的 [web.Server],再启动一个新的 [web.Server] 对象。 - // - // NOTE: 如果执行过程中出错,应该尽量阻止旧对象被中止,保证最大限度地可用状态。 - RestartServer() -} - -// SignalHUP 让 s 根据 [HUP] 信号重启服务 -// -// app := &App{...} -// SignalHUP(app) -// -// [HUP]: https://en.wikipedia.org/wiki/SIGHUP -func SignalHUP(s ServerApp) { - sc := make(chan os.Signal, 1) - signal.Notify(sc, syscall.SIGHUP) - Restart(sc, s) -} - -// Restart 根据信号 c 重启 s -// -// 可以结合其它方法一起使用,比如和 fsnotify 一起使用: -// -// watcher := fsnotify.NewWatcher(...) -// Restart(watcher.Event, s) -// -// 也可参考 [SignalHUP]。 -func Restart[T any](c chan T, s ServerApp) { - go func() { - for range c { - s.RestartServer() - } - }() -} From 9fc51c4f467b374895f72abb3b1e576e3efbb3a0 Mon Sep 17 00:00:00 2001 From: caixw Date: Thu, 11 Apr 2024 18:44:27 +0800 Subject: [PATCH 2/4] =?UTF-8?q?refactor(server/app):=20=E9=9A=90=E8=97=8F?= =?UTF-8?q?=20CLI=20=E7=9A=84=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 以函数的形式返回 App 接口 --- server/app/app.go | 4 +- server/app/cli.go | 125 ++++++++++++++++++------------------- server/app/cli_test.go | 49 +++------------ server/app/restart_test.go | 21 ++++--- 4 files changed, 84 insertions(+), 115 deletions(-) diff --git a/server/app/app.go b/server/app/app.go index 3dd5fd0d..2648d514 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -// Package app 提供了简便的方式管理 [web.Server] +// Package app 提供了简便的方式管理 [web.Server] 的运行 package app import ( @@ -14,7 +14,7 @@ import ( // App [web.Server] 的管理接口 type App interface { - // Exec 运行当前服务 + // Exec 运行当前程序 Exec() error // Restart 重启 diff --git a/server/app/cli.go b/server/app/cli.go index cdf70f87..b4f7fac6 100644 --- a/server/app/cli.go +++ b/server/app/cli.go @@ -26,18 +26,7 @@ const ( cmdShowHelp = web.StringPhrase("cmd.show_help") ) -// CLI 提供一种简单的命令行生成方式 -// -// 生成的命令行带以下几个参数: -// - -v 显示版本号; -// - -h 显示帮助信息; -// - -a 执行的指令,该值会传递给 [CLI.NewServer],由用户根据此值决定初始化方式; -// -// T 表示的是配置文件中的用户自定义数据类型,可参考 [server.LoadOptions] 中有关 User 的说明。 -type CLI[T any] struct { - // NOTE: CLI 仅用于初始化 [web.Server]。对于接口的开发应当是透明的, - // 开发者所有的功能都应该是通过 [web.Context] 和 [web.Server] 获得。 - +type CLIOptions[T any] struct { Name string // 程序名称 Version string // 程序版本 @@ -97,59 +86,85 @@ type CLI[T any] struct { // // 默认值为 [flag.ContinueOnError] ErrorHandling flag.ErrorHandling +} - app App - action string +type cli[T any] struct { + App + exec func(args []string) error } -// Exec 执行命令 +// NewCLI 提供一种简单的命令行生成方式 // -// 如果是 [CLI] 本身字段设置有问题会直接 panic,其它错误则返回该错误信息。 -func (cmd *CLI[T]) Exec() (err error) { return cmd.exec(os.Args) } - -func (cmd *CLI[T]) exec(args []string) (err error) { - if err = cmd.sanitize(); err != nil { // 字段值有问题,直接 panic。 +// 生成的命令行带以下几个参数: +// - -v 显示版本号; +// - -h 显示帮助信息; +// - -a 执行的指令,该值会传递给 [CLIOptions.NewServer],由用户根据此值决定初始化方式; +// +// T 表示的是配置文件中的用户自定义数据类型,可参考 [server.LoadOptions] 中有关 User 的说明。 +// +// 如果是 [CLIOptions] 本身字段设置有问题会直接 panic。 +func NewCLI[T any](o *CLIOptions[T]) App { + if err := o.sanitize(); err != nil { // 字段值有问题,直接 panic。 panic(err) } - wrap := func(err error) error { + var action string // -a 参数 + + initServer := func() (web.Server, error) { + opt, user, err := server.LoadOptions[T](o.ConfigDir, o.ConfigFilename) if err != nil { - if le, ok := err.(web.LocaleStringer); ok { // 对错误信息进行本地化转换 - return errors.New(le.LocaleString(cmd.Printer)) - } + return nil, web.NewStackError(err) } - return err + return o.NewServer(o.Name, o.Version, opt, user, action) } - fs := flag.NewFlagSet(cmd.Name, cmd.ErrorHandling) - fs.SetOutput(cmd.Out) + app := New(o.ShutdownTimeout, initServer) + + return &cli[T]{ + App: app, + exec: func(args []string) (err error) { + wrap := func(err error) error { + if err != nil { + if le, ok := err.(web.LocaleStringer); ok { // 对错误信息进行本地化转换 + return errors.New(le.LocaleString(o.Printer)) + } + } + return err + } + + fs := flag.NewFlagSet(o.Name, o.ErrorHandling) + fs.SetOutput(o.Out) - v := fs.Bool("v", false, cmdShowVersion.LocaleString(cmd.Printer)) - h := fs.Bool("h", false, cmdShowHelp.LocaleString(cmd.Printer)) - fs.StringVar(&cmd.action, "a", "", cmdAction.LocaleString(cmd.Printer)) - if err = fs.Parse(args[1:]); err != nil { - return wrap(err) - } + v := fs.Bool("v", false, cmdShowVersion.LocaleString(o.Printer)) + h := fs.Bool("h", false, cmdShowHelp.LocaleString(o.Printer)) + fs.StringVar(&action, "a", "", cmdAction.LocaleString(o.Printer)) + if err = fs.Parse(args[1:]); err != nil { + return wrap(err) + } - if *v { - _, err = fmt.Fprintln(cmd.Out, cmd.Name, cmd.Version) - return wrap(err) - } + if *v { + _, err = fmt.Fprintln(o.Out, o.Name, o.Version) + return wrap(err) + } - if *h { - fs.PrintDefaults() - return nil - } + if *h { + fs.PrintDefaults() + return nil + } - if slices.Index(cmd.ServeActions, cmd.action) < 0 { // 非服务 - _, err = cmd.initServer() - return wrap(err) - } + if slices.Index(o.ServeActions, action) < 0 { // 非服务 + _, err = initServer() + return wrap(err) + } - return wrap(cmd.app.Exec()) + return wrap(app.Exec()) + }, + } } -func (cmd *CLI[T]) sanitize() error { +func (cmd *cli[T]) Exec() error { return cmd.exec(os.Args) } + +func (cmd *CLIOptions[T]) sanitize() error { if cmd.Name == "" { return errors.New("字段 Name 不能为空") } @@ -180,21 +195,5 @@ func (cmd *CLI[T]) sanitize() error { cmd.ErrorHandling = flag.ContinueOnError } - cmd.app = New(cmd.ShutdownTimeout, cmd.initServer) - return nil } - -// Restart 触发重启服务 -// -// 该方法将关闭现有的服务,并发送运行新服务的指令,不会等待新服务启动完成。 -func (cmd *CLI[T]) Restart() { cmd.app.Restart() } - -func (cmd *CLI[T]) initServer() (web.Server, error) { - opt, user, err := server.LoadOptions[T](cmd.ConfigDir, cmd.ConfigFilename) - if err != nil { - return nil, web.NewStackError(err) - } - - return cmd.NewServer(cmd.Name, cmd.Version, opt, user, cmd.action) -} diff --git a/server/app/cli_test.go b/server/app/cli_test.go index d02046c7..edd0e3c5 100644 --- a/server/app/cli_test.go +++ b/server/app/cli_test.go @@ -6,10 +6,8 @@ package app import ( "bytes" - "net/http" "os" "testing" - "time" "github.com/issue9/assert/v4" @@ -23,7 +21,7 @@ func TestCLI(t *testing.T) { bs := new(bytes.Buffer) var action string - cmd := &CLI[empty]{ + o := &CLIOptions[empty]{ Name: "test", Version: "1.0.0", ConfigDir: ".", @@ -36,52 +34,24 @@ func TestCLI(t *testing.T) { return server.New(name, ver, opt) }, } - a.NotError(cmd.exec([]string{"app", "-v"})).Contains(bs.String(), cmd.Version) + cmd := NewCLI(o) + ocli := cmd.(*cli[empty]) + a.NotError(ocli.exec([]string{"app", "-v"})).Contains(bs.String(), o.Version) bs.Reset() - a.NotError(cmd.exec([]string{"app", "-a=install"})).Equal(action, "install") - - // RestartServer - - exit := make(chan struct{}, 10) - bs.Reset() - go func() { - a.ErrorIs(cmd.exec([]string{"app", "-a=serve"}), http.ErrServerClosed) - exit <- struct{}{} - }() - time.Sleep(500 * time.Millisecond) // 等待 go func 启动完成 - - // restart1 - s1 := cmd.app.getServer() - t1 := s1.Uptime() - cmd.Name = "restart1" - cmd.Restart() - time.Sleep(shutdownTimeout + 500*time.Millisecond) // 此值要大于 CLI.ShutdownTimeout - s2 := cmd.app.getServer() - t2 := s2.Uptime() - a.True(t2.After(t1)).NotEqual(s1, s2) - - // restart2 - cmd.Name = "restart2" - cmd.Restart() - time.Sleep(shutdownTimeout + 500*time.Millisecond) // 此值要大于 CLI.ShutdownTimeout - t3 := cmd.app.getServer().Uptime() - a.True(t3.After(t2)) - - cmd.app.getServer().Close(0) - <-exit + a.NotError(ocli.exec([]string{"app", "-a=install"})).Equal(action, "install") } func TestCLI_sanitize(t *testing.T) { a := assert.New(t, false) - cmd := &CLI[empty]{} + cmd := &CLIOptions[empty]{} a.ErrorString(cmd.sanitize(), "Name") - cmd = &CLI[empty]{Name: "app", Version: "1.1.1"} + cmd = &CLIOptions[empty]{Name: "app", Version: "1.1.1"} a.ErrorString(cmd.sanitize(), "NewServer") - cmd = &CLI[empty]{ + cmd = &CLIOptions[empty]{ Name: "app", Version: "1.1.1", NewServer: func(name, ver string, opt *server.Options, _ *empty, _ string) (web.Server, error) { @@ -91,8 +61,7 @@ func TestCLI_sanitize(t *testing.T) { } a.NotError(cmd.sanitize()).Equal(cmd.Out, os.Stdout) - cmd = &CLI[empty]{Name: "abc"} a.PanicString(func() { - _ = cmd.exec(nil) + NewCLI(&CLIOptions[empty]{Name: "abc"}) }, "字段 Version 不能为空") } diff --git a/server/app/restart_test.go b/server/app/restart_test.go index c7b3f6cc..3b8bf464 100644 --- a/server/app/restart_test.go +++ b/server/app/restart_test.go @@ -20,7 +20,7 @@ import ( var ( _ App = &app{} - _ App = &CLI[empty]{} + _ App = &cli[empty]{} ) func TestSignalHUP(t *testing.T) { @@ -31,7 +31,7 @@ func TestSignalHUP(t *testing.T) { a := assert.New(t, false) exit := make(chan struct{}, 10) - cmd := &CLI[empty]{ + cmd := &CLIOptions[empty]{ Name: "test", Version: "1.0.0", ConfigDir: ".", @@ -41,30 +41,31 @@ func TestSignalHUP(t *testing.T) { return server.New(name, ver, opt) }, } - SignalHUP(cmd) + c := NewCLI(cmd).(*cli[empty]) + SignalHUP(c) go func() { - a.ErrorIs(cmd.exec([]string{"app", "-a=serve"}), http.ErrServerClosed) + a.ErrorIs(c.exec([]string{"app", "-a=serve"}), http.ErrServerClosed) exit <- struct{}{} }() time.Sleep(2000 * time.Millisecond) // 等待 go func 启动完成 - a.NotNil(cmd.app). - NotNil(cmd.app.getServer()) + a.NotNil(c.App). + NotNil(c.App.(*app).getServer()) p, err := os.FindProcess(os.Getpid()) a.NotError(err).NotNil(p) // hup1 - t1 := cmd.app.getServer().Uptime() + t1 := c.App.(*app).getServer().Uptime() a.NotError(p.Signal(syscall.SIGHUP)).Wait(500 * time.Millisecond) // 此值要大于 CLI.ShutdownTimeout - t2 := cmd.app.getServer().Uptime() + t2 := c.App.(*app).getServer().Uptime() a.True(t2.After(t1)) // hup2 a.NotError(p.Signal(syscall.SIGHUP)).Wait(500 * time.Millisecond) // 此值要大于 CLI.ShutdownTimeout - t3 := cmd.app.getServer().Uptime() + t3 := c.App.(*app).getServer().Uptime() a.True(t3.After(t2)) - cmd.app.getServer().Close(0) + c.App.(*app).getServer().Close(0) <-exit } From 5e7bff746aa18424c84130864ef2c92497509027 Mon Sep 17 00:00:00 2001 From: caixw Date: Fri, 12 Apr 2024 18:11:32 +0800 Subject: [PATCH 3/4] =?UTF-8?q?refactor(server/app):=20=E4=B8=BA=20CLIOpti?= =?UTF-8?q?ons.sanitize=20=E6=B7=BB=E5=8A=A0=E7=BF=BB=E8=AF=91=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/app/cli.go | 43 +++++++++++++++++++++--------------------- server/app/cli_test.go | 5 +++-- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/server/app/cli.go b/server/app/cli.go index b4f7fac6..716c5d04 100644 --- a/server/app/cli.go +++ b/server/app/cli.go @@ -75,8 +75,9 @@ type CLIOptions[T any] struct { // - cmd.show_version // - cmd.action // - cmd.show_help + // - can not be empty // - // NOTE: 此设置仅影响命令行的本地化(panic 信息不支持本地化),[web.Server] 的本地化由其自身管理。 + // NOTE: 此设置仅影响命令行的本地化,[web.Server] 的本地化由其自身管理。 Printer *message.Printer // 每次关闭服务操作的等待时间 @@ -164,35 +165,35 @@ func NewCLI[T any](o *CLIOptions[T]) App { func (cmd *cli[T]) Exec() error { return cmd.exec(os.Args) } -func (cmd *CLIOptions[T]) sanitize() error { - if cmd.Name == "" { - return errors.New("字段 Name 不能为空") +func (o *CLIOptions[T]) sanitize() error { + if o.Printer == nil { + p, err := server.NewPrinter("*.yaml", locales.Locales...) + if err != nil { + return err + } + o.Printer = p } - if cmd.Version == "" { - return errors.New("字段 Version 不能为空") + + if o.Name == "" { + return web.NewFieldError("Name", locales.ErrCanNotBeEmpty()) } - if cmd.NewServer == nil { - return errors.New("字段 NewServer 不能为空") + if o.Version == "" { + return web.NewFieldError("Version", locales.ErrCanNotBeEmpty()) } - - if cmd.ConfigDir == "" { - cmd.ConfigDir = server.DefaultConfigDir + if o.NewServer == nil { + return web.NewFieldError("NewServer", locales.ErrCanNotBeEmpty()) } - if cmd.Printer == nil { - p, err := server.NewPrinter("*.yaml", locales.Locales...) - if err != nil { - return err - } - cmd.Printer = p + if o.ConfigDir == "" { + o.ConfigDir = server.DefaultConfigDir } - if cmd.Out == nil { - cmd.Out = os.Stdout + if o.Out == nil { + o.Out = os.Stdout } - if cmd.ErrorHandling == 0 { - cmd.ErrorHandling = flag.ContinueOnError + if o.ErrorHandling == 0 { + o.ErrorHandling = flag.ContinueOnError } return nil diff --git a/server/app/cli_test.go b/server/app/cli_test.go index edd0e3c5..73ecaa3a 100644 --- a/server/app/cli_test.go +++ b/server/app/cli_test.go @@ -12,6 +12,7 @@ import ( "github.com/issue9/assert/v4" "github.com/issue9/web" + "github.com/issue9/web/locales" "github.com/issue9/web/server" ) @@ -61,7 +62,7 @@ func TestCLI_sanitize(t *testing.T) { } a.NotError(cmd.sanitize()).Equal(cmd.Out, os.Stdout) - a.PanicString(func() { + a.PanicValue(func() { NewCLI(&CLIOptions[empty]{Name: "abc"}) - }, "字段 Version 不能为空") + }, web.NewFieldError("Version", locales.ErrCanNotBeEmpty())) } From b59524cd4ddce582348d483096bbccac899a23ad Mon Sep 17 00:00:00 2001 From: caixw Date: Fri, 12 Apr 2024 18:34:24 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix(server/app):=20=E4=BF=AE=E6=AD=A3=20pan?= =?UTF-8?q?ic=20=E4=B8=8D=E8=83=BD=E6=AD=A3=E5=A4=A7=E5=9C=A8=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E6=9C=AC=E5=9C=B0=E5=8C=96=E4=BF=A1=E6=81=AF=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/app/cli.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/server/app/cli.go b/server/app/cli.go index 716c5d04..b0b03cb2 100644 --- a/server/app/cli.go +++ b/server/app/cli.go @@ -106,7 +106,7 @@ type cli[T any] struct { // 如果是 [CLIOptions] 本身字段设置有问题会直接 panic。 func NewCLI[T any](o *CLIOptions[T]) App { if err := o.sanitize(); err != nil { // 字段值有问题,直接 panic。 - panic(err) + panic(localeError(err, o.Printer)) } var action string // -a 参数 @@ -124,15 +124,6 @@ func NewCLI[T any](o *CLIOptions[T]) App { return &cli[T]{ App: app, exec: func(args []string) (err error) { - wrap := func(err error) error { - if err != nil { - if le, ok := err.(web.LocaleStringer); ok { // 对错误信息进行本地化转换 - return errors.New(le.LocaleString(o.Printer)) - } - } - return err - } - fs := flag.NewFlagSet(o.Name, o.ErrorHandling) fs.SetOutput(o.Out) @@ -140,12 +131,12 @@ func NewCLI[T any](o *CLIOptions[T]) App { h := fs.Bool("h", false, cmdShowHelp.LocaleString(o.Printer)) fs.StringVar(&action, "a", "", cmdAction.LocaleString(o.Printer)) if err = fs.Parse(args[1:]); err != nil { - return wrap(err) + return web.NewStackError(localeError(err, o.Printer)) } if *v { _, err = fmt.Fprintln(o.Out, o.Name, o.Version) - return wrap(err) + return web.NewStackError(localeError(err, o.Printer)) } if *h { @@ -155,14 +146,23 @@ func NewCLI[T any](o *CLIOptions[T]) App { if slices.Index(o.ServeActions, action) < 0 { // 非服务 _, err = initServer() - return wrap(err) + return localeError(err, o.Printer) } - return wrap(app.Exec()) + return localeError(app.Exec(), o.Printer) }, } } +func localeError(err error, p *message.Printer) error { + if err != nil { + if le, ok := err.(web.LocaleStringer); ok { // 对错误信息进行本地化转换 + return errors.New(le.LocaleString(p)) + } + } + return err +} + func (cmd *cli[T]) Exec() error { return cmd.exec(os.Args) } func (o *CLIOptions[T]) sanitize() error {