Persist agent/secrets settings with masked secrets and runner hot-reload; add maintenance windows, maintpage binary, and zero-downtime listener handoff.
128 lines
3.0 KiB
Go
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
|
|
}
|