cf2bd93c93
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>
95 lines
2.8 KiB
Go
95 lines
2.8 KiB
Go
package agentd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
// logf is the agent's structured-ish logger. It deliberately never logs
|
|
// dp_uuids, peers' addresses or any payload — only operational events — to honour
|
|
// the no-log policy.
|
|
func logf(format string, args ...any) {
|
|
log.Printf("[pangolin-agent] "+format, args...)
|
|
}
|
|
|
|
// atomicWrite writes data to a temp file in the same directory and renames it into
|
|
// place, so a crash mid-write never leaves a truncated config/state file.
|
|
func atomicWrite(path string, data []byte, perm os.FileMode) error {
|
|
dir := filepath.Dir(path)
|
|
tmp, err := os.CreateTemp(dir, ".tmp-*")
|
|
if err != nil {
|
|
return fmt.Errorf("agentd: temp file: %w", err)
|
|
}
|
|
tmpName := tmp.Name()
|
|
defer os.Remove(tmpName) // no-op if the rename succeeded
|
|
if _, err := tmp.Write(data); err != nil {
|
|
tmp.Close()
|
|
return fmt.Errorf("agentd: write temp: %w", err)
|
|
}
|
|
if err := tmp.Chmod(perm); err != nil {
|
|
tmp.Close()
|
|
return fmt.Errorf("agentd: chmod temp: %w", err)
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return fmt.Errorf("agentd: close temp: %w", err)
|
|
}
|
|
if err := os.Rename(tmpName, path); err != nil {
|
|
return fmt.Errorf("agentd: rename temp: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SystemdRestarter restarts the sing-box unit via `systemctl restart`.
|
|
type SystemdRestarter struct {
|
|
Unit string // e.g. "sing-box"
|
|
}
|
|
|
|
// Restart runs `systemctl restart <unit>` (cold start: ~1-2s data-plane blip).
|
|
func (r SystemdRestarter) Restart(ctx context.Context) error {
|
|
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)))
|
|
}
|