feat(server): agent 热重载 sing-box(SIGHUP)替代全量重启(#3)

sing-box 支持 SIGHUP 热重载(cmd_run.go:进程内 check()验证→close 旧实例→
start 新实例;坏配置保留旧实例继续跑、不断网)。原 agent 用 systemctl restart
(全量重启:断所有连接 + 坏配置让 sing-box 起不来全断)。

- Restarter 接口加 Reload;SystemdRestarter.Reload 取 MainPID(systemctl show
  -p MainPID)发 SIGHUP——agent 与 sing-box 同 pangolin 用户,免 polkit/不改 unit;
  PID 取不到或发信号失败回退 Restart
- SingBox.started:首次/进程未起走 Restart(冷启动),之后配置变更走 Reload
- noopRestarter/fakeRestarter 补 Reload;加 TestWriteAndRestartFirstColdThenReload

价值:切节点/续期/加删用户不整进程重启、不全断流;坏配置不至于打死节点。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-19 19:36:46 +08:00
parent c1a52d4fc2
commit cf2bd93c93
3 changed files with 102 additions and 12 deletions
+39 -2
View File
@@ -15,8 +15,9 @@ import (
// fakeRestarter counts Restart calls.
type fakeRestarter struct {
mu sync.Mutex
n int
mu sync.Mutex
n int // Restart 次数(冷启动)
reloads int // Reload 次数(热重载)
}
func (f *fakeRestarter) Restart(context.Context) error {
@@ -26,12 +27,25 @@ func (f *fakeRestarter) Restart(context.Context) error {
return nil
}
func (f *fakeRestarter) Reload(context.Context) error {
f.mu.Lock()
f.reloads++
f.mu.Unlock()
return nil
}
func (f *fakeRestarter) count() int {
f.mu.Lock()
defer f.mu.Unlock()
return f.n
}
func (f *fakeRestarter) reloadCount() int {
f.mu.Lock()
defer f.mu.Unlock()
return f.reloads
}
func testConfig(t *testing.T) Config {
t.Helper()
dir := t.TempDir()
@@ -227,3 +241,26 @@ func assertNoIdentityFields(t *testing.T, blob []byte) {
}
}
}
// 首次渲染走冷启动(Restart),之后的配置变更走 SIGHUP 热重载(Reload)。
func TestWriteAndRestartFirstColdThenReload(t *testing.T) {
fr := &fakeRestarter{}
sb := NewSingBox(testConfig(t), fr)
sb.ApplyConfig(sampleSnapshot(), true) // 设置 inbounds 使渲染成功
ctx := context.Background()
if err := sb.writeAndRestart(ctx); err != nil {
t.Fatalf("first writeAndRestart: %v", err)
}
if r, rl := fr.count(), fr.reloadCount(); r != 1 || rl != 0 {
t.Fatalf("首次后 restarts=%d reloads=%d, 期望 1/0", r, rl)
}
sb.Upsert(&Cred{DpUUID: "z", Protocol: agentv1.ProtocolBoth})
if err := sb.writeAndRestart(ctx); err != nil {
t.Fatalf("second writeAndRestart: %v", err)
}
if r, rl := fr.count(), fr.reloadCount(); r != 1 || rl != 1 {
t.Fatalf("变更后 restarts=%d reloads=%d, 期望 1/1(不再 Restart,走 Reload)", r, rl)
}
}