接入本机重置通道生命周期并验证失败关闭
This commit is contained in:
parent
651d7ba917
commit
c7cfe98e1c
89
backend/internal/cli/application_services.go
Normal file
89
backend/internal/cli/application_services.go
Normal file
@ -0,0 +1,89 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/localreset"
|
||||
)
|
||||
|
||||
type applicationHTTP interface {
|
||||
ListenAndServe() error
|
||||
Listening() <-chan struct{}
|
||||
}
|
||||
|
||||
type applicationControl interface {
|
||||
Serve(context.Context, localreset.Factory) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// runApplicationServices 将本机通道绑定到正式主服务生命周期,并在数据库关闭前等待重置处理退出。
|
||||
func runApplicationServices(
|
||||
ctx context.Context,
|
||||
web applicationHTTP,
|
||||
startRenewal func(context.Context),
|
||||
factory localreset.Factory,
|
||||
openControl func() (applicationControl, error),
|
||||
) (_ bool, resultErr error) {
|
||||
if ctx.Err() != nil {
|
||||
return false, nil
|
||||
}
|
||||
control, err := openControl()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("启动本机密码重置通道失败:%w", err)
|
||||
}
|
||||
controlCtx, cancel := context.WithCancel(ctx)
|
||||
controlDone := make(chan error, 1)
|
||||
go func() { controlDone <- control.Serve(controlCtx, factory) }()
|
||||
controlFinished := false
|
||||
defer func() {
|
||||
cancel()
|
||||
resultErr = errors.Join(resultErr, control.Close())
|
||||
if !controlFinished {
|
||||
resultErr = errors.Join(resultErr, <-controlDone)
|
||||
}
|
||||
}()
|
||||
httpDone := make(chan error, 1)
|
||||
go func() { httpDone <- web.ListenAndServe() }()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false, nil
|
||||
case err := <-controlDone:
|
||||
controlFinished = true
|
||||
return false, applicationControlExit(ctx, err)
|
||||
case err := <-httpDone:
|
||||
return false, applicationHTTPExit(err)
|
||||
case <-web.Listening():
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return false, nil
|
||||
}
|
||||
if startRenewal != nil {
|
||||
startRenewal(ctx)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false, nil
|
||||
case err := <-controlDone:
|
||||
controlFinished = true
|
||||
return false, applicationControlExit(ctx, err)
|
||||
case err := <-httpDone:
|
||||
return false, applicationHTTPExit(err)
|
||||
}
|
||||
}
|
||||
|
||||
func applicationControlExit(ctx context.Context, err error) error {
|
||||
if ctx.Err() != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("本机密码重置通道意外退出:%w", errors.Join(localreset.ErrUnavailable, err))
|
||||
}
|
||||
|
||||
func applicationHTTPExit(err error) error {
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
187
backend/internal/cli/application_services_test.go
Normal file
187
backend/internal/cli/application_services_test.go
Normal file
@ -0,0 +1,187 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/localreset"
|
||||
)
|
||||
|
||||
type applicationTestHTTP struct {
|
||||
ready chan struct{}
|
||||
exit chan error
|
||||
started atomic.Int32
|
||||
}
|
||||
|
||||
func (h *applicationTestHTTP) Listening() <-chan struct{} { return h.ready }
|
||||
func (h *applicationTestHTTP) ListenAndServe() error {
|
||||
h.started.Add(1)
|
||||
return <-h.exit
|
||||
}
|
||||
|
||||
type applicationTestControl struct {
|
||||
serve func(context.Context, localreset.Factory) error
|
||||
closed atomic.Int32
|
||||
}
|
||||
|
||||
func (c *applicationTestControl) Serve(ctx context.Context, factory localreset.Factory) error {
|
||||
return c.serve(ctx, factory)
|
||||
}
|
||||
func (c *applicationTestControl) Close() error {
|
||||
c.closed.Add(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func applicationTestFactory(context.Context, localreset.Peer) (localreset.Session, error) {
|
||||
return nil, localreset.ErrUnavailable
|
||||
}
|
||||
|
||||
func TestApplicationControlFailurePreventsHTTPStartup(t *testing.T) {
|
||||
web := &applicationTestHTTP{ready: make(chan struct{}), exit: make(chan error, 1)}
|
||||
expected := errors.New("test-control-bind-failure")
|
||||
_, err := runApplicationServices(t.Context(), web, nil, applicationTestFactory, func() (applicationControl, error) {
|
||||
return nil, expected
|
||||
})
|
||||
if !errors.Is(err, expected) || web.started.Load() != 0 {
|
||||
t.Fatal("本机通道创建失败后仍启动了网络服务")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationCancellationBeforeStartupDoesNotOpenControl(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
opened := false
|
||||
_, err := runApplicationServices(ctx, nil, nil, nil, func() (applicationControl, error) {
|
||||
opened = true
|
||||
return nil, nil
|
||||
})
|
||||
if err != nil || opened {
|
||||
t.Fatal("取消后仍然启动本机通道")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationBootstrapCannotOpenControl(t *testing.T) {
|
||||
cfg := config.Default(t.TempDir())
|
||||
cfg.SetupReady = false
|
||||
if _, err := runApplication(t.Context(), cfg, ""); !errors.Is(err, localreset.ErrUnavailable) {
|
||||
t.Fatal("初始化阶段没有拒绝正式本机通道")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationWaitsForControlWorkBeforeReturning(t *testing.T) {
|
||||
for _, scenario := range []string{"cancel-starting", "cancel-running", "http-startup-error", "http-runtime-error", "http-closed"} {
|
||||
t.Run(scenario, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
web := &applicationTestHTTP{ready: make(chan struct{}), exit: make(chan error, 1)}
|
||||
t.Cleanup(func() { web.exit <- http.ErrServerClosed })
|
||||
entered := make(chan struct{})
|
||||
cancelled := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
finished := make(chan struct{})
|
||||
control := &applicationTestControl{serve: func(ctx context.Context, factory localreset.Factory) error {
|
||||
if factory == nil {
|
||||
t.Error("主服务未提供重置工厂")
|
||||
}
|
||||
close(entered)
|
||||
<-ctx.Done()
|
||||
close(cancelled)
|
||||
<-release
|
||||
close(finished)
|
||||
return nil
|
||||
}}
|
||||
renewal := make(chan struct{})
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := runApplicationServices(ctx, web, func(context.Context) { close(renewal) }, applicationTestFactory, func() (applicationControl, error) { return control, nil })
|
||||
done <- err
|
||||
}()
|
||||
<-entered
|
||||
if scenario == "cancel-running" || scenario == "http-runtime-error" || scenario == "http-closed" {
|
||||
close(web.ready)
|
||||
<-renewal
|
||||
}
|
||||
expected := errors.New("test-http-failure")
|
||||
switch scenario {
|
||||
case "cancel-starting", "cancel-running":
|
||||
cancel()
|
||||
case "http-closed":
|
||||
web.exit <- http.ErrServerClosed
|
||||
default:
|
||||
web.exit <- expected
|
||||
}
|
||||
select {
|
||||
case <-cancelled:
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("没有取消本机重置处理")
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
t.Fatal("业务处理尚未结束就返回,可能提前关闭数据库")
|
||||
default:
|
||||
}
|
||||
close(release)
|
||||
select {
|
||||
case err := <-done:
|
||||
if scenario == "http-startup-error" || scenario == "http-runtime-error" {
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("丢失 HTTP 启动或运行错误")
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("本机通道退出后主服务仍未返回")
|
||||
}
|
||||
<-finished
|
||||
if control.closed.Load() != 1 {
|
||||
t.Fatal("没有关闭本机通道")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationControlUnexpectedExitFailsClosed(t *testing.T) {
|
||||
for _, ready := range []bool{false, true} {
|
||||
name := "starting"
|
||||
if ready {
|
||||
name = "running"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
web := &applicationTestHTTP{ready: make(chan struct{}), exit: make(chan error, 1)}
|
||||
t.Cleanup(func() { web.exit <- http.ErrServerClosed })
|
||||
stop := make(chan struct{})
|
||||
control := &applicationTestControl{serve: func(context.Context, localreset.Factory) error {
|
||||
<-stop
|
||||
return nil
|
||||
}}
|
||||
renewal := make(chan struct{})
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := runApplicationServices(t.Context(), web, func(context.Context) { close(renewal) }, applicationTestFactory, func() (applicationControl, error) { return control, nil })
|
||||
done <- err
|
||||
}()
|
||||
if ready {
|
||||
close(web.ready)
|
||||
<-renewal
|
||||
}
|
||||
close(stop)
|
||||
select {
|
||||
case err := <-done:
|
||||
if !errors.Is(err, localreset.ErrUnavailable) {
|
||||
t.Fatal("本机安全通道异常退出被当作正常状态")
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("本机通道退出后 HTTP 生命周期未结束")
|
||||
}
|
||||
if control.closed.Load() != 1 {
|
||||
t.Fatal("本机通道没有关闭")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -4,13 +4,13 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/app"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/localreset"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -77,35 +77,21 @@ func bootstrapFirewallMode(command *cobra.Command, value string) string {
|
||||
}
|
||||
|
||||
func runApplication(ctx context.Context, cfg config.Config, path string) (bool, error) {
|
||||
if !cfg.SetupReady {
|
||||
return false, localreset.ErrUnavailable
|
||||
}
|
||||
service, err := app.NewWithConfigPath(ctx, cfg, path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer service.Close()
|
||||
errorsChannel := make(chan error, 1)
|
||||
go func() { errorsChannel <- service.HTTP.ListenAndServe() }()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false, nil
|
||||
case err := <-errorsChannel:
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
case <-service.HTTP.Listening():
|
||||
}
|
||||
var startRenewal func(context.Context)
|
||||
if service.Renewal != nil {
|
||||
service.Renewal.Start(ctx)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false, nil
|
||||
case err := <-errorsChannel:
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
startRenewal = service.Renewal.Start
|
||||
}
|
||||
return runApplicationServices(ctx, service.HTTP, startRenewal, service.HTTP.LocalResetFactory(), func() (applicationControl, error) {
|
||||
return localreset.Listen("")
|
||||
})
|
||||
}
|
||||
|
||||
func bootstrapURLPath(cfg config.Config) string {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user