mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-12 12:16:37 +08:00
fix: protect shared rules across delivery and recovery (#560)
This commit is contained in:
+5
-3
@@ -125,11 +125,13 @@ func main() {
|
||||
|
||||
distro := socket.DetectDistro()
|
||||
fullVersion := fmt.Sprintf("%s (%s/%s)", version, distro, runtime.GOARCH)
|
||||
wsReporter := socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, config.Http, config.Tls, config.Socks, fullVersion)
|
||||
defer wsReporter.Stop()
|
||||
service.SetHTTPReportURL(config.Addr, config.Secret)
|
||||
|
||||
p := &program{}
|
||||
p := &program{
|
||||
startReporter: func() reporter {
|
||||
return socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, config.Http, config.Tls, config.Socks, fullVersion)
|
||||
},
|
||||
}
|
||||
if err := svc.Run(p); err != nil {
|
||||
logger.Default().Fatal(err)
|
||||
}
|
||||
|
||||
+88
-35
@@ -3,6 +3,14 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/auth"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/service"
|
||||
@@ -18,20 +26,22 @@ import (
|
||||
xservice "github.com/go-gost/x/service"
|
||||
"github.com/go-gost/x/socket"
|
||||
"github.com/judwhite/go-svc"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type program struct {
|
||||
srvApi service.Service
|
||||
srvMetrics service.Service
|
||||
srvProfiling *http.Server
|
||||
type reporter interface {
|
||||
Stop()
|
||||
}
|
||||
|
||||
cancel context.CancelFunc
|
||||
type program struct {
|
||||
startReporter func() reporter
|
||||
reporter reporter
|
||||
srvApi service.Service
|
||||
srvMetrics service.Service
|
||||
srvProfiling *http.Server
|
||||
profilingListener net.Listener
|
||||
|
||||
cancel context.CancelFunc
|
||||
stopped bool
|
||||
}
|
||||
|
||||
func (p *program) Init(env svc.Environment) error {
|
||||
@@ -48,7 +58,15 @@ func (p *program) Init(env svc.Environment) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *program) Start() error {
|
||||
func (p *program) Start() (err error) {
|
||||
unlock := config.LockMutation()
|
||||
defer unlock()
|
||||
p.stopped = false
|
||||
defer func() {
|
||||
if err != nil {
|
||||
p.stopRuntime()
|
||||
}
|
||||
}()
|
||||
cfg, err := parser.Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -61,23 +79,28 @@ func (p *program) Start() error {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
config.Set(cfg)
|
||||
|
||||
if err := loader.Load(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Enable config persistence after initial load so runtime mutations
|
||||
// (AddService, UpdateService, DeleteService, etc.) are saved to disk.
|
||||
socket.EnableConfigPersist()
|
||||
|
||||
if err := p.run(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config.Set(cfg)
|
||||
socket.EnableConfigPersist()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
p.cancel = cancel
|
||||
go p.reload(ctx)
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGHUP)
|
||||
go p.reload(ctx, c)
|
||||
|
||||
// A connected panel may immediately send commands. Only expose the agent
|
||||
// after initial config loading, runtime startup and persistence are ready.
|
||||
if p.startReporter != nil {
|
||||
p.reporter = p.startReporter()
|
||||
}
|
||||
|
||||
go func() {
|
||||
select {
|
||||
@@ -91,7 +114,14 @@ func (p *program) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *program) run(cfg *config.Config) error {
|
||||
func (p *program) run(cfg *config.Config) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// Auxiliary listeners may occupy ports required by the rollback config.
|
||||
// Release all resources opened by this attempt before rebuilding it.
|
||||
p.stopRuntime()
|
||||
}
|
||||
}()
|
||||
for _, svc := range registry.ServiceRegistry().GetAll() {
|
||||
svc := svc
|
||||
go func() {
|
||||
@@ -152,6 +182,10 @@ func (p *program) run(cfg *config.Config) error {
|
||||
|
||||
if p.srvProfiling != nil {
|
||||
p.srvProfiling.Close()
|
||||
if p.profilingListener != nil {
|
||||
p.profilingListener.Close()
|
||||
p.profilingListener = nil
|
||||
}
|
||||
p.srvProfiling = nil
|
||||
}
|
||||
if cfg.Profiling != nil {
|
||||
@@ -162,7 +196,12 @@ func (p *program) run(cfg *config.Config) error {
|
||||
s := &http.Server{
|
||||
Addr: addr,
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.srvProfiling = s
|
||||
p.profilingListener = ln
|
||||
|
||||
go func() {
|
||||
defer s.Close()
|
||||
@@ -170,7 +209,7 @@ func (p *program) run(cfg *config.Config) error {
|
||||
log := logger.Default().WithFields(map[string]any{"kind": "service", "service": "@profiling"})
|
||||
|
||||
log.Info("listening on ", addr)
|
||||
if err := s.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||
if err := s.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Error(err)
|
||||
}
|
||||
}()
|
||||
@@ -184,30 +223,45 @@ func (p *program) Stop() error {
|
||||
p.cancel()
|
||||
}
|
||||
|
||||
for name, srv := range registry.ServiceRegistry().GetAll() {
|
||||
srv.Close()
|
||||
if p.reporter != nil {
|
||||
p.reporter.Stop()
|
||||
}
|
||||
unlock := config.LockMutation()
|
||||
defer unlock()
|
||||
p.stopped = true
|
||||
p.stopRuntime()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *program) stopRuntime() {
|
||||
for name := range registry.ServiceRegistry().GetAll() {
|
||||
registry.ServiceRegistry().Unregister(name)
|
||||
logger.Default().Debugf("service %s shutdown", name)
|
||||
}
|
||||
|
||||
if p.srvApi != nil {
|
||||
p.srvApi.Close()
|
||||
p.srvApi = nil
|
||||
logger.Default().Debug("service @api shutdown")
|
||||
}
|
||||
if p.srvMetrics != nil {
|
||||
p.srvMetrics.Close()
|
||||
p.srvMetrics = nil
|
||||
logger.Default().Debug("service @metrics shutdown")
|
||||
}
|
||||
if p.srvProfiling != nil {
|
||||
p.srvProfiling.Close()
|
||||
if p.profilingListener != nil {
|
||||
p.profilingListener.Close()
|
||||
p.profilingListener = nil
|
||||
}
|
||||
p.srvProfiling = nil
|
||||
logger.Default().Debug("service @profiling shutdown")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *program) reload(ctx context.Context) {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGHUP)
|
||||
func (p *program) reload(ctx context.Context, c chan os.Signal) {
|
||||
defer signal.Stop(c)
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -225,13 +279,16 @@ func (p *program) reload(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (p *program) reloadConfig() error {
|
||||
unlock := config.LockMutation()
|
||||
defer unlock()
|
||||
if p.stopped {
|
||||
return errors.New("agent is shutting down")
|
||||
}
|
||||
cfg, err := parser.Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.Set(cfg)
|
||||
|
||||
if err := loader.Load(cfg); err != nil {
|
||||
if err := loader.Reload(cfg, p.run); err != nil {
|
||||
return err
|
||||
}
|
||||
activeServices := make(map[string]struct{}, len(cfg.Services))
|
||||
@@ -242,10 +299,6 @@ func (p *program) reloadConfig() error {
|
||||
}
|
||||
xservice.GetGlobalTrafficManager().RetainServices(activeServices)
|
||||
|
||||
if err := p.run(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
//go:build linux || darwin
|
||||
|
||||
package lifecycle_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// Exercise the real binary: initial parsing is held at a FIFO while the panel
|
||||
// tries to send a rule as soon as the WebSocket connects. This reproduced the
|
||||
// old startup overwrite reliably without timing a large config load.
|
||||
func TestAgentStartupAndFailedReloadPreservePanelRules(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("builds and runs the real agent")
|
||||
}
|
||||
binary := filepath.Join(t.TempDir(), "gost")
|
||||
build := exec.Command("go", "build", "-o", binary, "../..")
|
||||
if output, err := build.CombinedOutput(); err != nil {
|
||||
t.Fatalf("build: %v\n%s", err, output)
|
||||
}
|
||||
dir := t.TempDir()
|
||||
early, baseline := address(t), address(t)
|
||||
apiAddr := address(t)
|
||||
panelConnections := make(chan *websocket.Conn, 2)
|
||||
wsReady := make(chan struct{})
|
||||
responses := make(chan map[string]any, 8)
|
||||
var ready sync.Once
|
||||
upgrader := websocket.Upgrader{}
|
||||
panel := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/system-info" {
|
||||
w.Write([]byte("ok"))
|
||||
return
|
||||
}
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
ready.Do(func() { close(wsReady) })
|
||||
panelConnections <- c
|
||||
if err := c.WriteJSON(map[string]any{"type": "AddService", "requestId": "early-rule", "data": []any{service("70_1_0", early)}}); err != nil {
|
||||
return
|
||||
}
|
||||
for {
|
||||
_, payload, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
env := decodeResponse(t, payload)
|
||||
if env["requestId"] == "early-rule" {
|
||||
responses <- env
|
||||
}
|
||||
}
|
||||
}))
|
||||
defer panel.Close()
|
||||
writeJSON(t, filepath.Join(dir, "config.json"), map[string]any{"addr": panel.URL, "secret": "audit-secret", "http": 1, "tls": 1, "socks": 1})
|
||||
fifo := filepath.Join(dir, "delayed.json")
|
||||
if err := syscall.Mkfifo(fifo, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
logPath := filepath.Join(dir, "agent.log")
|
||||
logfile, err := os.Create(logPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer logfile.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
agent := exec.CommandContext(ctx, binary, "-C", fifo)
|
||||
agent.Dir, agent.Stdout, agent.Stderr = dir, logfile, logfile
|
||||
if err := agent.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exited := make(chan error, 1)
|
||||
go func() { exited <- agent.Wait() }()
|
||||
defer func() {
|
||||
agent.Process.Kill()
|
||||
if t.Failed() {
|
||||
b, _ := os.ReadFile(logPath)
|
||||
t.Logf("agent log:\n%s", b)
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case <-wsReady:
|
||||
t.Fatal("panel connected before initial config loaded")
|
||||
case err := <-exited:
|
||||
t.Fatalf("agent exited during startup: %v", err)
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
}
|
||||
boot := map[string]any{"services": []any{service("71_1_0", baseline)}, "api": map[string]any{"addr": apiAddr}}
|
||||
writeJSON(t, filepath.Join(dir, "gost.json"), boot)
|
||||
writeFIFO(t, fifo, boot)
|
||||
select {
|
||||
case <-wsReady:
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("panel did not connect after startup")
|
||||
}
|
||||
select {
|
||||
case response := <-responses:
|
||||
if response["success"] != true {
|
||||
t.Fatalf("AddService failed: %v", response)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("missing AddService response")
|
||||
}
|
||||
await(t, "both startup and panel listeners", func() bool { return listening(early) && listening(baseline) })
|
||||
saved, err := os.ReadFile(fifo)
|
||||
if err != nil || !strings.Contains(string(saved), "70_1_0") {
|
||||
t.Fatalf("acknowledged rule not persisted: %v", err)
|
||||
}
|
||||
|
||||
// A valid listener plus an invalid handler also exercises rollback of a
|
||||
// partially initialized service which already bound the original port.
|
||||
invalid := service("71_1_0", baseline)
|
||||
invalid["handler"] = map[string]any{"type": "handler-does-not-exist"}
|
||||
// The first persisted mutation atomically replaced the FIFO with a regular
|
||||
// config file, so subsequent reloads use the same real persistence path.
|
||||
writeJSON(t, fifo, map[string]any{"services": []any{invalid}})
|
||||
if err := agent.Process.Signal(syscall.SIGHUP); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
await(t, "failed reload rollback", func() bool {
|
||||
b, _ := os.ReadFile(logPath)
|
||||
return strings.Contains(string(b), "previous config restored")
|
||||
})
|
||||
if !listening(early) || !listening(baseline) {
|
||||
t.Fatal("failed reload lost a previously acknowledged listener")
|
||||
}
|
||||
|
||||
// Starting the candidate API on the old service port must not prevent
|
||||
// rollback when a later auxiliary listener fails to initialize.
|
||||
logBefore, _ := os.ReadFile(logPath)
|
||||
writeJSON(t, fifo, map[string]any{
|
||||
"services": []any{service("candidate", address(t))},
|
||||
"api": map[string]any{"addr": baseline},
|
||||
"metrics": map[string]any{"addr": "127.0.0.1:not-a-port"},
|
||||
})
|
||||
if err := agent.Process.Signal(syscall.SIGHUP); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
await(t, "auxiliary listener rollback", func() bool {
|
||||
b, _ := os.ReadFile(logPath)
|
||||
return strings.Count(string(b), "previous config restored") > strings.Count(string(logBefore), "previous config restored")
|
||||
})
|
||||
if !listening(early) || !listening(baseline) || !listening(apiAddr) {
|
||||
t.Fatal("candidate auxiliary listener prevented rollback")
|
||||
}
|
||||
|
||||
// An authenticated API request that never finishes its body must not hold
|
||||
// the runtime transaction lock against WS mutations or process shutdown.
|
||||
slow, err := net.Dial("tcp", apiAddr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer slow.Close()
|
||||
if _, err = fmt.Fprintf(slow, "POST /config/services HTTP/1.1\r\nHost: localhost\r\nAuthorization: Basic dGVzdDp0ZXN0\r\nContent-Type: application/json\r\nContent-Length: 100000\r\n\r\n{"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
panelConn := <-panelConnections
|
||||
if err := panelConn.WriteJSON(map[string]any{"type": "DeleteService", "requestId": "slow-body-check", "data": map[string]any{"services": []string{"70_1_0"}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
await(t, "WS mutation despite slow API upload", func() bool { return !listening(early) })
|
||||
if err := agent.Process.Signal(syscall.SIGTERM); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case err := <-exited:
|
||||
if err != nil {
|
||||
t.Fatalf("shutdown: %v", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("agent did not stop")
|
||||
}
|
||||
if listening(early) || listening(baseline) {
|
||||
t.Fatal("shutdown left listeners open")
|
||||
}
|
||||
|
||||
// Startup errors must exit without advertising a node ready to accept rules.
|
||||
bad := filepath.Join(dir, "invalid.json")
|
||||
writeJSON(t, bad, map[string]any{"services": []any{invalid}})
|
||||
failed := exec.CommandContext(ctx, binary, "-C", bad)
|
||||
failed.Dir = dir
|
||||
if output, err := failed.CombinedOutput(); err == nil {
|
||||
t.Fatalf("invalid startup succeeded: %s", output)
|
||||
}
|
||||
select {
|
||||
case response := <-responses:
|
||||
t.Fatalf("failed startup accepted panel command: %v", response)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func service(name, addr string) map[string]any {
|
||||
return map[string]any{"name": name, "addr": addr, "listener": map[string]any{"type": "tcp"}, "handler": map[string]any{"type": "auto"}}
|
||||
}
|
||||
func address(t *testing.T) string {
|
||||
t.Helper()
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
return l.Addr().String()
|
||||
}
|
||||
func listening(addr string) bool {
|
||||
c, err := net.DialTimeout("tcp", addr, 50*time.Millisecond)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
c.Close()
|
||||
return true
|
||||
}
|
||||
func await(t *testing.T, label string, pred func() bool) {
|
||||
t.Helper()
|
||||
for until := time.Now().Add(5 * time.Second); time.Now().Before(until); time.Sleep(10 * time.Millisecond) {
|
||||
if pred() {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("timeout: " + label)
|
||||
}
|
||||
func writeJSON(t *testing.T, path string, value any) {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, b, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
func writeFIFO(t *testing.T, path string, value any) {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
f, err := os.OpenFile(path, os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
_, err = f.Write(b)
|
||||
f.Close()
|
||||
done <- err
|
||||
}()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("agent did not read config FIFO")
|
||||
}
|
||||
}
|
||||
func decodeResponse(t *testing.T, payload []byte) map[string]any {
|
||||
t.Helper()
|
||||
env := map[string]any{}
|
||||
if err := json.Unmarshal(payload, &env); err != nil {
|
||||
t.Error(err)
|
||||
return nil
|
||||
}
|
||||
if encrypted, _ := env["encrypted"].(bool); !encrypted {
|
||||
return env
|
||||
}
|
||||
encoded, _ := env["data"].(string)
|
||||
raw, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return nil
|
||||
}
|
||||
hash := sha256.Sum256([]byte("audit-secret"))
|
||||
block, _ := aes.NewCipher(hash[:])
|
||||
gcm, _ := cipher.NewGCM(block)
|
||||
if len(raw) < gcm.NonceSize() {
|
||||
t.Error("short encrypted response")
|
||||
return nil
|
||||
}
|
||||
payload, err = gcm.Open(nil, raw[:gcm.NonceSize()], raw[gcm.NonceSize():], nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return nil
|
||||
}
|
||||
env = map[string]any{}
|
||||
if err := json.Unmarshal(payload, &env); err != nil {
|
||||
t.Error(err)
|
||||
return nil
|
||||
}
|
||||
return env
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func Register(r *gin.Engine, opts *Options) {
|
||||
router.StaticFS("/docs", http.FS(swaggerDoc))
|
||||
|
||||
config := router.Group("/config")
|
||||
config.Use(mwBasicAuth(opts.Auther))
|
||||
config.Use(mwBasicAuth(opts.Auther), configTransaction())
|
||||
|
||||
config.GET("", getConfig)
|
||||
config.POST("", saveConfig)
|
||||
|
||||
@@ -37,9 +37,12 @@ func reloadConfig(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
config.Set(cfg)
|
||||
|
||||
if err := loader.Load(cfg); err != nil {
|
||||
if err := loader.Reload(cfg, func(*config.Config) error {
|
||||
for _, svc := range registry.ServiceRegistry().GetAll() {
|
||||
go svc.Serve()
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
writeError(ctx, NewError(http.StatusBadRequest, ErrCodeInvalid, err.Error()))
|
||||
return
|
||||
}
|
||||
@@ -51,13 +54,6 @@ func reloadConfig(ctx *gin.Context) {
|
||||
}
|
||||
xservice.GetGlobalTrafficManager().RetainServices(activeServices)
|
||||
|
||||
for _, svc := range registry.ServiceRegistry().GetAll() {
|
||||
svc := svc
|
||||
go func() {
|
||||
svc.Serve()
|
||||
}()
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, Response{
|
||||
Msg: "OK",
|
||||
})
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-gost/x/config"
|
||||
)
|
||||
|
||||
const maxConfigRequestBody = 16 << 20
|
||||
|
||||
// Read the complete bounded request before acquiring the runtime transaction
|
||||
// lock. Buffer the response until after it is released: neither a slow upload
|
||||
// nor a client that stops reading may block panel commands, reload or shutdown.
|
||||
func configTransaction() gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
if ctx.Request.Body != nil {
|
||||
body := http.MaxBytesReader(ctx.Writer, ctx.Request.Body, maxConfigRequestBody)
|
||||
data, err := io.ReadAll(body)
|
||||
body.Close()
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
var tooLarge *http.MaxBytesError
|
||||
if errors.As(err, &tooLarge) {
|
||||
status = http.StatusRequestEntityTooLarge
|
||||
}
|
||||
ctx.AbortWithStatusJSON(status, Response{Code: status, Msg: "Unable to read configuration request"})
|
||||
return
|
||||
}
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewReader(data))
|
||||
}
|
||||
|
||||
writer := ctx.Writer
|
||||
buffered := &configResponseWriter{ResponseWriter: writer, header: writer.Header().Clone(), status: http.StatusOK, size: -1}
|
||||
ctx.Writer = buffered
|
||||
defer func() { ctx.Writer = writer }()
|
||||
func() {
|
||||
unlock := config.LockMutation()
|
||||
defer unlock()
|
||||
// A request waiting behind reload may have been closed during shutdown.
|
||||
if ctx.Request.Context().Err() != nil {
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
ctx.Next()
|
||||
}()
|
||||
ctx.Writer = writer
|
||||
for key, values := range buffered.header {
|
||||
writer.Header()[key] = values
|
||||
}
|
||||
writer.WriteHeader(buffered.status)
|
||||
writer.Write(buffered.body.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
// Config endpoints return JSON rather than streaming. Preserve Gin's response
|
||||
// bookkeeping while delaying all network writes until the transaction ends.
|
||||
type configResponseWriter struct {
|
||||
gin.ResponseWriter
|
||||
header http.Header
|
||||
body bytes.Buffer
|
||||
status int
|
||||
size int
|
||||
}
|
||||
|
||||
func (w *configResponseWriter) Header() http.Header { return w.header }
|
||||
func (w *configResponseWriter) WriteHeader(status int) {
|
||||
if !w.Written() && status > 0 {
|
||||
w.status = status
|
||||
}
|
||||
}
|
||||
func (w *configResponseWriter) WriteHeaderNow() {
|
||||
if !w.Written() {
|
||||
w.size = 0
|
||||
}
|
||||
}
|
||||
func (w *configResponseWriter) Write(p []byte) (int, error) {
|
||||
w.WriteHeaderNow()
|
||||
n, err := w.body.Write(p)
|
||||
w.size += n
|
||||
return n, err
|
||||
}
|
||||
func (w *configResponseWriter) WriteString(s string) (int, error) { return w.Write([]byte(s)) }
|
||||
func (w *configResponseWriter) Status() int { return w.status }
|
||||
func (w *configResponseWriter) Size() int { return w.size }
|
||||
func (w *configResponseWriter) Written() bool { return w.size >= 0 }
|
||||
func (w *configResponseWriter) Flush() { w.WriteHeaderNow() }
|
||||
@@ -0,0 +1,76 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-gost/x/config"
|
||||
)
|
||||
|
||||
func assertMutationAvailable(t *testing.T) {
|
||||
t.Helper()
|
||||
done := make(chan struct{})
|
||||
go func() { unlock := config.LockMutation(); unlock(); close(done) }()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("network I/O holds runtime mutation lock")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigTransactionDoesNotLockWhileReadingBody(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.Use(configTransaction())
|
||||
router.POST("/config", func(c *gin.Context) { c.JSON(http.StatusOK, Response{Msg: "OK"}) })
|
||||
reader, writer := io.Pipe()
|
||||
defer reader.Close()
|
||||
defer writer.Close()
|
||||
request := httptest.NewRequest(http.MethodPost, "/config", reader)
|
||||
done := make(chan struct{})
|
||||
go func() { defer close(done); router.ServeHTTP(httptest.NewRecorder(), request) }()
|
||||
if _, err := writer.Write([]byte("{")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertMutationAvailable(t)
|
||||
writer.Close()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("handler did not finish")
|
||||
}
|
||||
}
|
||||
|
||||
type blockedResponse struct {
|
||||
header http.Header
|
||||
started chan struct{}
|
||||
release chan struct{}
|
||||
}
|
||||
|
||||
func (w *blockedResponse) Header() http.Header { return w.header }
|
||||
func (w *blockedResponse) WriteHeader(int) {}
|
||||
func (w *blockedResponse) Write(p []byte) (int, error) {
|
||||
close(w.started)
|
||||
<-w.release
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func TestConfigTransactionReleasesLockBeforeSendingResponse(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.Use(configTransaction())
|
||||
router.POST("/config", func(c *gin.Context) { c.JSON(http.StatusOK, Response{Msg: "OK"}) })
|
||||
writer := &blockedResponse{header: make(http.Header), started: make(chan struct{}), release: make(chan struct{})}
|
||||
defer close(writer.release)
|
||||
request := httptest.NewRequest(http.MethodPost, "/config", strings.NewReader("{}"))
|
||||
go router.ServeHTTP(writer, request)
|
||||
select {
|
||||
case <-writer.started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("response did not start")
|
||||
}
|
||||
assertMutationAvailable(t)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-gost/core/auth"
|
||||
@@ -67,7 +68,11 @@ func NewService(network, addr string, opts ...Option) (service.Service, error) {
|
||||
|
||||
return &server{
|
||||
s: &http.Server{
|
||||
Handler: r,
|
||||
Handler: r,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
},
|
||||
ln: ln,
|
||||
cclose: make(chan struct{}),
|
||||
@@ -83,7 +88,11 @@ func (s *server) Addr() net.Addr {
|
||||
}
|
||||
|
||||
func (s *server) Close() error {
|
||||
return s.s.Close()
|
||||
// Close can race the goroutine entering Serve during a failed startup.
|
||||
// http.Server.Close alone does not own the listener until Serve starts.
|
||||
err := s.s.Close()
|
||||
s.ln.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *server) IsClosed() bool {
|
||||
|
||||
@@ -23,10 +23,20 @@ func init() {
|
||||
}
|
||||
|
||||
var (
|
||||
global = &Config{}
|
||||
globalMux sync.RWMutex
|
||||
global = &Config{}
|
||||
globalMux sync.RWMutex
|
||||
mutationMux sync.Mutex
|
||||
)
|
||||
|
||||
// LockMutation serializes complete runtime/config transactions across panel
|
||||
// commands, management API requests, startup and reload. It is separate from
|
||||
// globalMux so callers may safely use Global, Set and OnUpdate while holding it.
|
||||
// Lock before reading/parsing the config and hold through persistence/rollback.
|
||||
func LockMutation() func() {
|
||||
mutationMux.Lock()
|
||||
return mutationMux.Unlock
|
||||
}
|
||||
|
||||
func Global() *Config {
|
||||
globalMux.RLock()
|
||||
defer globalMux.RUnlock()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package loader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/config/parsing"
|
||||
@@ -30,6 +32,34 @@ func Load(cfg *config.Config) error {
|
||||
return defaultLoader.Load(cfg)
|
||||
}
|
||||
|
||||
// Reload replaces the runtime and commits the config only after it starts.
|
||||
// The caller must hold config.LockMutation, including while parsing cfg, so the
|
||||
// rollback snapshot includes every previously acknowledged runtime command.
|
||||
// Failed loads can partially replace registries and bind listeners; always
|
||||
// rebuild the last successful snapshot before returning an error.
|
||||
func Reload(cfg *config.Config, run func(*config.Config) error) error {
|
||||
previous := config.Global()
|
||||
if err := apply(cfg, run); err != nil {
|
||||
// A failed partial load may have left both old and new listeners behind.
|
||||
for name := range registry.ServiceRegistry().GetAll() {
|
||||
registry.ServiceRegistry().Unregister(name)
|
||||
}
|
||||
if rollbackErr := apply(previous, run); rollbackErr != nil {
|
||||
return fmt.Errorf("reload failed: %w; restore previous config failed: %v", err, rollbackErr)
|
||||
}
|
||||
return fmt.Errorf("reload failed (previous config restored): %w", err)
|
||||
}
|
||||
config.Set(cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func apply(cfg *config.Config, run func(*config.Config) error) error {
|
||||
if err := Load(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
return run(cfg)
|
||||
}
|
||||
|
||||
type loader struct{}
|
||||
|
||||
func (l *loader) Load(cfg *config.Config) error {
|
||||
@@ -217,12 +247,19 @@ func register(cfg *config.Config) error {
|
||||
registry.ServiceRegistry().Unregister(name)
|
||||
}
|
||||
for _, svcCfg := range cfg.Services {
|
||||
if svcCfg == nil {
|
||||
return fmt.Errorf("service config is nil")
|
||||
}
|
||||
if paused, _ := svcCfg.Metadata["paused"].(bool); paused {
|
||||
continue
|
||||
}
|
||||
svc, err := service_parser.ParseService(svcCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if svc != nil {
|
||||
if err := registry.ServiceRegistry().Register(svcCfg.Name, svc); err != nil {
|
||||
svc.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package loader_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/config/loader"
|
||||
_ "github.com/go-gost/x/handler/auto"
|
||||
_ "github.com/go-gost/x/listener/tcp"
|
||||
"github.com/go-gost/x/registry"
|
||||
)
|
||||
|
||||
func TestReloadRestoresPreviousRuntime(t *testing.T) {
|
||||
for _, failure := range []string{"listener", "handler", "partial", "run"} {
|
||||
t.Run(failure, func(t *testing.T) {
|
||||
unlock := config.LockMutation()
|
||||
defer unlock()
|
||||
original := config.Global()
|
||||
defer config.Set(original)
|
||||
defer func() {
|
||||
for name := range registry.ServiceRegistry().GetAll() {
|
||||
registry.ServiceRegistry().Unregister(name)
|
||||
}
|
||||
}()
|
||||
old := &config.Config{Services: []*config.ServiceConfig{
|
||||
{Name: "shared-rule", Addr: "127.0.0.1:0", Handler: &config.HandlerConfig{Type: "auto"}, Listener: &config.ListenerConfig{Type: "tcp"}},
|
||||
{Name: "paused-rule", Addr: "127.0.0.1:0", Metadata: map[string]any{"paused": true}},
|
||||
}}
|
||||
if err := loader.Load(old); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
addr := registry.ServiceRegistry().Get("shared-rule").Addr().String()
|
||||
old.Services[0].Addr = addr
|
||||
config.Set(old)
|
||||
serve := func(*config.Config) error {
|
||||
for _, svc := range registry.ServiceRegistry().GetAll() {
|
||||
go svc.Serve()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
serve(old)
|
||||
replacement := config.Global()
|
||||
replacement.Services = replacement.Services[:1]
|
||||
switch failure {
|
||||
case "listener":
|
||||
replacement.Services[0].Listener.Type = "invalid-listener"
|
||||
case "handler":
|
||||
replacement.Services[0].Handler.Type = "invalid-handler"
|
||||
case "partial":
|
||||
replacement.Services = append(replacement.Services, &config.ServiceConfig{Name: "broken", Listener: &config.ListenerConfig{Type: "invalid-listener"}})
|
||||
}
|
||||
run := serve
|
||||
if failure == "run" {
|
||||
run = func(cfg *config.Config) error {
|
||||
if cfg == replacement {
|
||||
return &net.AddrError{Err: "auxiliary listener failed", Addr: "test"}
|
||||
}
|
||||
return serve(cfg)
|
||||
}
|
||||
}
|
||||
if err := loader.Reload(replacement, run); err == nil {
|
||||
t.Fatal("expected reload failure")
|
||||
}
|
||||
restored := registry.ServiceRegistry().Get("shared-rule")
|
||||
if restored == nil || restored.Addr().String() != addr {
|
||||
t.Fatal("previous service was not restored on its original port")
|
||||
}
|
||||
if registry.ServiceRegistry().Get("paused-rule") != nil {
|
||||
t.Fatal("rollback resumed a paused service")
|
||||
}
|
||||
conn, err := net.DialTimeout("tcp", addr, time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("restored listener unavailable: %v", err)
|
||||
}
|
||||
conn.Close()
|
||||
got := config.Global()
|
||||
if len(got.Services) != 2 || got.Services[0].Listener.Type != "tcp" || got.Services[0].Handler.Type != "auto" {
|
||||
t.Fatalf("failed config was committed: %+v", got.Services)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -255,6 +255,15 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Listener initialization binds the port. If handler/TLS/forwarder parsing
|
||||
// fails, release it so a reload rollback can restore the previous listener.
|
||||
configured := false
|
||||
defer func() {
|
||||
if !configured {
|
||||
ln.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
handlerLogger := serviceLogger.WithFields(map[string]any{
|
||||
"kind": "handler",
|
||||
})
|
||||
@@ -379,6 +388,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
||||
)
|
||||
|
||||
serviceLogger.Infof("listening on %s/%s", s.Addr().String(), s.Addr().Network())
|
||||
configured = true
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,9 @@ func (s *metricService) Addr() net.Addr {
|
||||
}
|
||||
|
||||
func (s *metricService) Close() error {
|
||||
return s.s.Close()
|
||||
err := s.s.Close()
|
||||
s.ln.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *metricService) IsClosed() bool {
|
||||
|
||||
@@ -370,43 +370,43 @@ type getConfigResponse struct {
|
||||
|
||||
// getConfigData 获取配置数据(避免循环依赖)
|
||||
func getConfigData() ([]byte, error) {
|
||||
config.OnUpdate(func(c *config.Config) error {
|
||||
for _, svc := range c.Services {
|
||||
if svc == nil {
|
||||
continue
|
||||
// Reporting is read-only: enriching a detached snapshot must not persist
|
||||
// stale state over a config currently being reloaded or acknowledged.
|
||||
cfg := config.Global()
|
||||
for _, svc := range cfg.Services {
|
||||
if svc == nil {
|
||||
continue
|
||||
}
|
||||
s := registry.ServiceRegistry().Get(svc.Name)
|
||||
ss, ok := s.(serviceStatus)
|
||||
if ok && ss != nil {
|
||||
status := ss.Status()
|
||||
svc.Status = &config.ServiceStatus{
|
||||
CreateTime: status.CreateTime().Unix(),
|
||||
State: string(status.State()),
|
||||
}
|
||||
s := registry.ServiceRegistry().Get(svc.Name)
|
||||
ss, ok := s.(serviceStatus)
|
||||
if ok && ss != nil {
|
||||
status := ss.Status()
|
||||
svc.Status = &config.ServiceStatus{
|
||||
CreateTime: status.CreateTime().Unix(),
|
||||
State: string(status.State()),
|
||||
if st := status.Stats(); st != nil {
|
||||
svc.Status.Stats = &config.ServiceStats{
|
||||
TotalConns: st.Get(stats.KindTotalConns),
|
||||
CurrentConns: st.Get(stats.KindCurrentConns),
|
||||
TotalErrs: st.Get(stats.KindTotalErrs),
|
||||
InputBytes: st.Get(stats.KindInputBytes),
|
||||
OutputBytes: st.Get(stats.KindOutputBytes),
|
||||
}
|
||||
if st := status.Stats(); st != nil {
|
||||
svc.Status.Stats = &config.ServiceStats{
|
||||
TotalConns: st.Get(stats.KindTotalConns),
|
||||
CurrentConns: st.Get(stats.KindCurrentConns),
|
||||
TotalErrs: st.Get(stats.KindTotalErrs),
|
||||
InputBytes: st.Get(stats.KindInputBytes),
|
||||
OutputBytes: st.Get(stats.KindOutputBytes),
|
||||
}
|
||||
}
|
||||
for _, ev := range status.Events() {
|
||||
if !ev.Time.IsZero() {
|
||||
svc.Status.Events = append(svc.Status.Events, config.ServiceEvent{
|
||||
Time: ev.Time.Unix(),
|
||||
Msg: ev.Message,
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, ev := range status.Events() {
|
||||
if !ev.Time.IsZero() {
|
||||
svc.Status.Events = append(svc.Status.Events, config.ServiceEvent{
|
||||
Time: ev.Time.Unix(),
|
||||
Msg: ev.Message,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var resp getConfigResponse
|
||||
resp.Config = config.Global()
|
||||
resp.Config = cfg
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
resp.Config.Write(buf, "json")
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/go-gost/x/config"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -148,3 +152,31 @@ func TestPostJSONWithFallbackRemembersDetectedURL(t *testing.T) {
|
||||
t.Fatalf("expected remembered http url first, got %s", calls[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigReportDoesNotPersistStaleRuntime(t *testing.T) {
|
||||
previous, path := config.Global(), config.PersistPath()
|
||||
defer config.Set(previous)
|
||||
defer config.SetPersistPath(path)
|
||||
config.Set(&config.Config{Services: []*config.ServiceConfig{{Name: "old-runtime"}}})
|
||||
filename := filepath.Join(t.TempDir(), "gost.json")
|
||||
candidate := []byte(`{"services":[{"name":"new-on-disk"}]}`)
|
||||
if err := os.WriteFile(filename, candidate, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config.SetPersistPath(filename)
|
||||
config.EnablePersist()
|
||||
report, err := getConfigData()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Contains(report, []byte("old-runtime")) {
|
||||
t.Fatalf("unexpected report: %s", report)
|
||||
}
|
||||
saved, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(saved, candidate) {
|
||||
t.Fatalf("report overwrote candidate config: %s", saved)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,3 +115,39 @@ func TestMutationQueueExecutesCommandsInArrivalOrder(t *testing.T) {
|
||||
}
|
||||
close(second.release)
|
||||
}
|
||||
|
||||
func TestMutationCommandWaitsForRuntimeTransaction(t *testing.T) {
|
||||
original := config.Global()
|
||||
defer config.Set(original)
|
||||
name := "reload_transaction_service"
|
||||
svc := &blockingCommandService{started: make(chan struct{}), release: make(chan struct{})}
|
||||
close(svc.release)
|
||||
if err := registry.ServiceRegistry().Register(name, svc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer registry.ServiceRegistry().Unregister(name)
|
||||
config.Set(&config.Config{Services: []*config.ServiceConfig{{Name: name}}})
|
||||
reporter := NewWebSocketReporter("", "transaction-test-secret")
|
||||
defer reporter.Stop()
|
||||
unlock := config.LockMutation()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
reporter.routeCommand(CommandMessage{Type: "DeleteService", Data: map[string]any{"services": []string{name}}})
|
||||
}()
|
||||
select {
|
||||
case <-svc.started:
|
||||
unlock()
|
||||
t.Fatal("command interleaved with runtime transaction")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
unlock()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("command did not resume after transaction")
|
||||
}
|
||||
if registry.ServiceRegistry().Get(name) != nil {
|
||||
t.Fatal("service was not removed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +179,7 @@ type WebSocketReporter struct {
|
||||
tcpPingSem chan struct{} // 限制诊断探测并发,避免离线目标耗尽连接
|
||||
readCommandSem chan struct{} // 限制只读命令并发,避免诊断请求耗尽资源
|
||||
mutationQueue chan CommandMessage
|
||||
workers sync.WaitGroup
|
||||
}
|
||||
|
||||
var wsDial = func(dialer *websocket.Dialer, rawURL string) (*websocket.Conn, *http.Response, error) {
|
||||
@@ -238,8 +239,15 @@ func (w *WebSocketReporter) releaseTCPPingSlot() {
|
||||
|
||||
// Start 启动WebSocket报告器
|
||||
func (w *WebSocketReporter) Start() {
|
||||
go w.runMutationCommands()
|
||||
go w.run()
|
||||
w.workers.Add(2)
|
||||
go func() {
|
||||
defer w.workers.Done()
|
||||
w.runMutationCommands()
|
||||
}()
|
||||
go func() {
|
||||
defer w.workers.Done()
|
||||
w.run()
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop 停止WebSocket报告器
|
||||
@@ -250,6 +258,7 @@ func (w *WebSocketReporter) Stop() {
|
||||
w.conn.Close()
|
||||
}
|
||||
w.connMutex.Unlock()
|
||||
w.workers.Wait()
|
||||
}
|
||||
|
||||
// backoffWithJitter 返回带随机抖动的退避时间(±25%)
|
||||
@@ -339,10 +348,10 @@ func (w *WebSocketReporter) connect() error {
|
||||
|
||||
candidates := buildWebSocketCandidates(w.addr, w.secret, w.version, cfg.Http, cfg.Tls, cfg.Socks, w.preferredWSScheme)
|
||||
|
||||
dialer := websocket.DefaultDialer
|
||||
dialer := *websocket.DefaultDialer
|
||||
dialer.HandshakeTimeout = 10 * time.Second
|
||||
|
||||
conn, usedURL, err := dialWebSocketWithFallback(dialer, candidates)
|
||||
conn, usedURL, err := dialWebSocketWithFallback(&dialer, candidates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -528,7 +537,11 @@ func (w *WebSocketReporter) handleConnection() {
|
||||
}()
|
||||
|
||||
// 启动消息接收goroutine
|
||||
go w.receiveMessages()
|
||||
w.workers.Add(1)
|
||||
go func() {
|
||||
defer w.workers.Done()
|
||||
w.receiveMessages()
|
||||
}()
|
||||
|
||||
// 指标上报 ticker
|
||||
metricTicker := time.NewTicker(w.pingInterval)
|
||||
@@ -887,6 +900,14 @@ func isMutationCommand(commandType string) bool {
|
||||
|
||||
// routeCommand 路由命令到对应的处理函数
|
||||
func (w *WebSocketReporter) routeCommand(cmd CommandMessage) {
|
||||
if isMutationCommand(cmd.Type) {
|
||||
unlock := config.LockMutation()
|
||||
defer unlock()
|
||||
if w.ctx.Err() != nil {
|
||||
w.sendCommandFailure(cmd, "Agent is shutting down")
|
||||
return
|
||||
}
|
||||
}
|
||||
jsonBytes, errs := json.Marshal(cmd)
|
||||
if errs != nil {
|
||||
fmt.Println("Error marshaling JSON:", errs)
|
||||
|
||||
Reference in New Issue
Block a user