Files
weichen 86e9d3706b feat(sysadmin): admin env settings + maintenance FD handoff
Persist agent/secrets settings with masked secrets and runner hot-reload;
add maintenance windows, maintpage binary, and zero-downtime listener handoff.
2026-09-24 23:20:20 +00:00

128 lines
3.0 KiB
Go

package handoff
import (
"fmt"
"net"
"os"
"os/exec"
"strconv"
"syscall"
"time"
)
const (
EnvInheritFD = "AGENT_INHERIT_FD"
EnvReadyPipeFD = "AGENT_READY_PIPE_FD"
)
// ListenOrAdopt 若 AGENT_INHERIT_FD 已设置则继承 listener,否则新建。
func ListenOrAdopt(addr string) (net.Listener, error) {
if ln, ok, err := AdoptFromEnv(); ok {
return ln, err
}
return net.Listen("tcp", addr)
}
// AdoptFromEnv 从 AGENT_INHERIT_FD 继承 TCP listener。
func AdoptFromEnv() (net.Listener, bool, error) {
raw := os.Getenv(EnvInheritFD)
if raw == "" {
return nil, false, nil
}
fd, err := strconv.Atoi(raw)
if err != nil {
return nil, true, fmt.Errorf("handoff: invalid %s: %w", EnvInheritFD, err)
}
f := os.NewFile(uintptr(fd), "inherit-listener")
if f == nil {
return nil, true, fmt.Errorf("handoff: NewFile(%d) failed", fd)
}
ln, err := net.FileListener(f)
_ = f.Close() // FileListener dup'd; close our copy
if err != nil {
return nil, true, err
}
_ = os.Unsetenv(EnvInheritFD)
return ln, true, nil
}
// PassToChild 把 listener FD 传给子进程,并等待 ready 信号(可选超时)。
// extraEnv 追加到子进程环境;child 应调用 SignalReady 后父进程再返回。
func PassToChild(ln net.Listener, bin string, args []string, extraEnv []string, readyTimeout time.Duration) (*exec.Cmd, error) {
tcp, ok := ln.(*net.TCPListener)
if !ok {
return nil, fmt.Errorf("handoff: listener is not *net.TCPListener")
}
file, err := tcp.File()
if err != nil {
return nil, err
}
defer file.Close()
readyR, readyW, err := os.Pipe()
if err != nil {
return nil, err
}
cmd := exec.Command(bin, args...)
cmd.Env = append(os.Environ(), extraEnv...)
cmd.Env = append(cmd.Env,
fmt.Sprintf("%s=3", EnvInheritFD),
fmt.Sprintf("%s=4", EnvReadyPipeFD),
)
cmd.ExtraFiles = []*os.File{file, readyW}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
if err := cmd.Start(); err != nil {
_ = readyR.Close()
_ = readyW.Close()
return nil, err
}
_ = readyW.Close() // child holds the write end via ExtraFiles
if readyTimeout <= 0 {
readyTimeout = 15 * time.Second
}
done := make(chan error, 1)
go func() {
buf := make([]byte, 1)
_, err := readyR.Read(buf)
done <- err
_ = readyR.Close()
}()
select {
case err := <-done:
if err != nil {
_ = cmd.Process.Kill()
return cmd, fmt.Errorf("handoff: ready signal: %w", err)
}
case <-time.After(readyTimeout):
_ = cmd.Process.Kill()
_ = readyR.Close()
return cmd, fmt.Errorf("handoff: ready timeout")
}
return cmd, nil
}
// SignalReady 子进程通知父进程已接管 listener。
func SignalReady() error {
raw := os.Getenv(EnvReadyPipeFD)
if raw == "" {
return nil
}
fd, err := strconv.Atoi(raw)
if err != nil {
return err
}
f := os.NewFile(uintptr(fd), "ready-pipe")
if f == nil {
return fmt.Errorf("handoff: ready pipe fd invalid")
}
defer f.Close()
_, err = f.Write([]byte{1})
_ = os.Unsetenv(EnvReadyPipeFD)
return err
}