feat(agent): SIGHUP 重读节点本地配置并热重渲染,不踢在线用户

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-23 02:15:27 +08:00
parent d216e157c3
commit 6f183ff0df
3 changed files with 50 additions and 0 deletions
+17
View File
@@ -44,6 +44,23 @@ func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// SIGHUP 重读节点本地配置(acl.json / warp.json)并重渲染。走 sing-box 的 SIGHUP
// 热重载路径,在线用户不掉线;重启 agent 则会冷启 sing-box,把所有人踢下线。
hup := make(chan os.Signal, 1)
signal.Notify(hup, syscall.SIGHUP)
defer signal.Stop(hup)
go func() {
for {
select {
case <-ctx.Done():
return
case <-hup:
log.Printf("[pangolin-agent] SIGHUP: re-reading node-local config (acl.json/warp.json)")
agent.SingBox().Refresh()
}
}
}()
log.Printf("[pangolin-agent] starting (control-plane=%s, version=%s)", cfg.ControlPlaneAddr, cfg.AgentVersion)
start := time.Now()
if err := agent.Run(ctx); err != nil {
+28
View File
@@ -1,12 +1,14 @@
package agentd
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
agentv1 "github.com/wangjia/pangolin/server/internal/pb/agentv1"
)
@@ -439,6 +441,32 @@ func TestACL_DeletedFileKeepsInMemoryLastGood(t *testing.T) {
}
}
// Refresh() 必须能触发一次重渲染(经 debounce 循环),用于「编辑 acl.json 后
// systemctl reload pangolin-agent」而不必重启 agent(重启会冷启 sing-box 踢人)。
func TestSingBoxRefresh_TriggersRender(t *testing.T) {
cfg := testConfig(t)
writeACL(t, cfg.ACLConfigPath, validACL)
fr := &fakeRestarter{}
sb := NewSingBox(cfg, fr)
sb.ApplyConfig(sampleSnapshot(&agentv1.Credential{DpUUID: "aaaa", Protocol: agentv1.ProtocolBoth}), true)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go sb.Run(ctx)
// 等首次渲染落地(ApplyConfig 已 markDirty)
eventually(t, 2*time.Second, func() bool {
_, err := os.Stat(cfg.SingboxConfigPath)
return err == nil
}, "首次渲染写出配置")
// 断言 reloadCount 而非 count:首次渲染已冷启动过(started=true),此后的重渲染
// 一律走 Reload(SIGHUP 热重载),Restart 计数不会再增加。断言错计数器会假失败。
before := fr.reloadCount()
sb.Refresh()
eventually(t, 2*time.Second, func() bool { return fr.reloadCount() > before }, "Refresh 触发了热重载")
}
// 新 agent 实例(模拟进程重启) + 被删的 acl.json → 磁盘 last-good 兜底,规则仍在。
func TestACL_ColdStartAfterDeletedFileFallsBackToDisk(t *testing.T) {
cfg := testConfig(t)
+5
View File
@@ -446,6 +446,11 @@ func (s *SingBox) markDirty() {
}
}
// Refresh 请求一次重渲染。供 agent 收到 SIGHUP 时调用,使编辑节点本地配置文件
// (acl.json / warp.json)后无需重启进程即可生效 —— 重启 agent 会让 sing-box 走
// 冷启动(Restart),把全部在线用户踢下线。
func (s *SingBox) Refresh() { s.markDirty() }
// Run drives the debounce loop: it coalesces bursts of changes within
// DebounceWindow into a single write+restart. It blocks until ctx is cancelled.
func (s *SingBox) Run(ctx context.Context) {