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 ` (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))) }