Skip to content

Commit

Permalink
Merge branch 'dev.app'
Browse files Browse the repository at this point in the history
  • Loading branch information
caixw committed Apr 12, 2024
2 parents 709efda + b59524c commit a221b3f
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 205 deletions.
62 changes: 42 additions & 20 deletions server/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MIT

// Package app 提供了简便的方式管理 [web.Server]
// Package app 提供了简便的方式管理 [web.Server] 的运行
package app

import (
Expand All @@ -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
Expand All @@ -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
Expand All @@ -65,24 +87,24 @@ RESTART:
return err
}

// RestartServer 触发重启服务
// Restart 触发重启服务
//
// 该方法将关闭现有的服务,并发送运行新服务的指令,不会等待新服务启动完成。
func (app *App) RestartServer() {
func (app *app) Restart() {
app.restartServerLock.Lock()
defer app.restartServerLock.Unlock()

app.restart = true

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 退出
}
159 changes: 78 additions & 81 deletions server/app/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 // 程序版本

Expand Down Expand Up @@ -86,8 +75,9 @@ type CLI[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

// 每次关闭服务操作的等待时间
Expand All @@ -97,107 +87,114 @@ 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 提供一种简单的命令行生成方式
//
// 生成的命令行带以下几个参数:
// - -v 显示版本号;
// - -h 显示帮助信息;
// - -a 执行的指令,该值会传递给 [CLIOptions.NewServer],由用户根据此值决定初始化方式;
//
// args 表示命令行参数,一般为 [os.Args]
// T 表示的是配置文件中的用户自定义数据类型,可参考 [server.LoadOptions] 中有关 User 的说明
//
// 如果是 [CLI] 本身字段设置有问题会直接 panic,其它错误则返回该错误信息
func (cmd *CLI[T]) Exec(args []string) (err error) {
if err = cmd.sanitize(); err != nil { // 字段值有问题,直接 panic。
panic(err)
// 如果是 [CLIOptions] 本身字段设置有问题会直接 panic。
func NewCLI[T any](o *CLIOptions[T]) App {
if err := o.sanitize(); err != nil { // 字段值有问题,直接 panic。
panic(localeError(err, o.Printer))
}

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)

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)
}
return &cli[T]{
App: app,
exec: func(args []string) (err error) {
fs := flag.NewFlagSet(o.Name, o.ErrorHandling)
fs.SetOutput(o.Out)

if *v {
_, err = fmt.Fprintln(cmd.Out, cmd.Name, cmd.Version)
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 web.NewStackError(localeError(err, o.Printer))
}

if *h {
fs.PrintDefaults()
return nil
}
if *v {
_, err = fmt.Fprintln(o.Out, o.Name, o.Version)
return web.NewStackError(localeError(err, o.Printer))
}

if slices.Index(cmd.ServeActions, cmd.action) < 0 { // 非服务
_, err = cmd.initServer()
return wrap(err)
}
if *h {
fs.PrintDefaults()
return nil
}

return wrap(cmd.app.Exec())
}
if slices.Index(o.ServeActions, action) < 0 { // 非服务
_, err = initServer()
return localeError(err, o.Printer)
}

func (cmd *CLI[T]) sanitize() error {
if cmd.Name == "" {
return errors.New("字段 Name 不能为空")
}
if cmd.Version == "" {
return errors.New("字段 Version 不能为空")
}
if cmd.NewServer == nil {
return errors.New("字段 NewServer 不能为空")
return localeError(app.Exec(), o.Printer)
},
}
}

if cmd.ConfigDir == "" {
cmd.ConfigDir = server.DefaultConfigDir
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
}

if cmd.Printer == nil {
func (cmd *cli[T]) Exec() error { return cmd.exec(os.Args) }

func (o *CLIOptions[T]) sanitize() error {
if o.Printer == nil {
p, err := server.NewPrinter("*.yaml", locales.Locales...)
if err != nil {
return err
}
cmd.Printer = p
o.Printer = p
}

if cmd.Out == nil {
cmd.Out = os.Stdout
if o.Name == "" {
return web.NewFieldError("Name", locales.ErrCanNotBeEmpty())
}

if cmd.ErrorHandling == 0 {
cmd.ErrorHandling = flag.ContinueOnError
if o.Version == "" {
return web.NewFieldError("Version", locales.ErrCanNotBeEmpty())
}

cmd.app = &App{
ShutdownTimeout: cmd.ShutdownTimeout,
NewServer: cmd.initServer,
if o.NewServer == nil {
return web.NewFieldError("NewServer", locales.ErrCanNotBeEmpty())
}

return nil
}
if o.ConfigDir == "" {
o.ConfigDir = server.DefaultConfigDir
}

// RestartServer 触发重启服务
//
// 该方法将关闭现有的服务,并发送运行新服务的指令,不会等待新服务启动完成。
func (cmd *CLI[T]) RestartServer() { cmd.app.RestartServer() }
if o.Out == nil {
o.Out = os.Stdout
}

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)
if o.ErrorHandling == 0 {
o.ErrorHandling = flag.ContinueOnError
}

return cmd.NewServer(cmd.Name, cmd.Version, opt, user, cmd.action)
return nil
}
Loading

0 comments on commit a221b3f

Please sign in to comment.