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
+37 -5
View File
@@ -7,6 +7,9 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
)
// logf is the agent's structured-ish logger. It deliberately never logs
@@ -48,15 +51,44 @@ type SystemdRestarter struct {
Unit string // e.g. "sing-box"
}
// Restart runs `systemctl restart <unit>`.
// Restart runs `systemctl restart <unit>` (cold start: ~1-2s data-plane blip).
func (r SystemdRestarter) Restart(ctx context.Context) error {
unit := r.Unit
if unit == "" {
unit = "sing-box"
}
unit := r.unit()
cmd := exec.CommandContext(ctx, "systemctl", "restart", unit)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("agentd: restart %s: %w: %s", unit, err, out)
}
return nil
}
// Reload hot-reloads sing-box via SIGHUP to its MainPID. sing-box validates the
// new config first and keeps the OLD instance running on error (no full
// process restart). agent 与 sing-box 同 pangolin 用户,直接发信号即可(免
// polkit/不改 unit)。PID 取不到或发信号失败时回退到 Restart(冷启动)。
func (r SystemdRestarter) Reload(ctx context.Context) error {
unit := r.unit()
pid, err := singboxMainPID(ctx, unit)
if err != nil || pid <= 0 {
return r.Restart(ctx)
}
if err := syscall.Kill(pid, syscall.SIGHUP); err != nil {
return r.Restart(ctx)
}
return nil
}
func (r SystemdRestarter) unit() string {
if r.Unit == "" {
return "sing-box"
}
return r.Unit
}
// singboxMainPID 读 systemd 记录的主进程 PID(0 表示未在跑)。
func singboxMainPID(ctx context.Context, unit string) (int, error) {
out, err := exec.CommandContext(ctx, "systemctl", "show", "-p", "MainPID", "--value", unit).Output()
if err != nil {
return 0, err
}
return strconv.Atoi(strings.TrimSpace(string(out)))
}