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
+26 -5
View File
@@ -15,11 +15,12 @@ import (
agentv1 "github.com/wangjia/pangolin/server/internal/pb/agentv1"
)
// Restarter applies a freshly-rendered sing-box config. sing-box has no hot
// reload, so the production implementation restarts the systemd unit (a ~1-2s
// data-plane blip). Tests inject a fake.
// Restarter applies a freshly-rendered sing-box config. sing-box 支持 SIGHUP
// 热重载(进程内 validate→close 旧→start 新,坏配置保留旧实例不断网),故配置
// 变更走 Reload(不整进程重启、不全断流),冷启动走 Restart。Tests inject a fake.
type Restarter interface {
Restart(ctx context.Context) error
Restart(ctx context.Context) error // 冷启动:systemctl restart(~1-2s blip)
Reload(ctx context.Context) error // 热重载:SIGHUP 给 sing-box MainPID
}
// Cred is the agent's in-memory view of one data-plane credential. It is the
@@ -60,6 +61,11 @@ type SingBox struct {
hy2 *agentv1.Hy2Inbound
configVersion int64
// started 标记 sing-box 是否已被本 agent 冷启动过:首次走 Restart(冷启动),
// 之后配置变更走 Reload(SIGHUP 热重载)。agent 进程重启后复位为 false,
// 下次渲染做一次冷启动以确保与渲染配置一致。
started bool
// debounce machinery
dirty chan struct{}
done chan struct{}
@@ -328,7 +334,21 @@ func (s *SingBox) writeAndRestart(ctx context.Context) error {
if err := atomicWrite(s.cfg.SingboxConfigPath, data, 0o644); err != nil {
return err
}
return s.restarter.Restart(ctx)
// 首次/进程未被本 agent 启动过 → 冷启动;已在跑 + 配置变更 → SIGHUP 热重载
// (Reload 内部在取不到 PID/发信号失败时已自动回退 Restart)。
s.mu.Lock()
started := s.started
s.mu.Unlock()
if !started {
if err := s.restarter.Restart(ctx); err != nil {
return err
}
s.mu.Lock()
s.started = true
s.mu.Unlock()
return nil
}
return s.restarter.Reload(ctx)
}
// ensureHy2Cert 为启用 hy2 的节点准备自签证书(幂等),并把 CertPath/KeyPath 写回当前
@@ -396,3 +416,4 @@ func (s *SingBox) Run(ctx context.Context) {
type noopRestarter struct{}
func (noopRestarter) Restart(context.Context) error { return nil }
func (noopRestarter) Reload(context.Context) error { return nil }