mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Tests that call ForkReap or send signals to their own process now re-exec as isolated subprocesses. This prevents ForkReap's syscall.ForkExec and process-directed signals from interfering with the parent test binary or other tests running in parallel. Also: - Wait for the reaper goroutine to fully exit between subtests to prevent overlapping reapers from competing on Wait4(-1). - Register signal handlers synchronously before spawning the forwarding goroutine so no signal is lost between ForkExec and the handler being ready.
125 lines
2.9 KiB
Go
125 lines
2.9 KiB
Go
//go:build linux
|
|
|
|
package reaper
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"github.com/hashicorp/go-reap"
|
|
"golang.org/x/xerrors"
|
|
|
|
"cdr.dev/slog/v3"
|
|
)
|
|
|
|
// IsInitProcess returns true if the current process's PID is 1.
|
|
func IsInitProcess() bool {
|
|
return os.Getpid() == 1
|
|
}
|
|
|
|
// startSignalForwarding registers signal handlers synchronously
|
|
// then forwards caught signals to the child in a background
|
|
// goroutine. Registering before the goroutine starts ensures no
|
|
// signal is lost between ForkExec and the handler being ready.
|
|
func startSignalForwarding(logger slog.Logger, pid int, sigs []os.Signal) {
|
|
if len(sigs) == 0 {
|
|
return
|
|
}
|
|
|
|
sc := make(chan os.Signal, 1)
|
|
signal.Notify(sc, sigs...)
|
|
|
|
logger.Info(context.Background(), "reaper catching signals",
|
|
slog.F("signals", sigs),
|
|
slog.F("child_pid", pid),
|
|
)
|
|
|
|
go func() {
|
|
defer signal.Stop(sc)
|
|
for s := range sc {
|
|
sig, ok := s.(syscall.Signal)
|
|
if ok {
|
|
logger.Info(context.Background(), "reaper caught signal, killing child process",
|
|
slog.F("signal", sig.String()),
|
|
slog.F("child_pid", pid),
|
|
)
|
|
_ = syscall.Kill(pid, sig)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// ForkReap spawns a goroutine that reaps children. In order to avoid
|
|
// complications with spawning `exec.Commands` in the same process that
|
|
// is reaping, we forkexec a child process. This prevents a race between
|
|
// the reaper and an exec.Command waiting for its process to complete.
|
|
// The provided 'pids' channel may be nil if the caller does not care about the
|
|
// reaped children PIDs.
|
|
//
|
|
// Returns the child's exit code (using 128+signal for signal termination)
|
|
// and any error from Wait4.
|
|
func ForkReap(opt ...Option) (int, error) {
|
|
opts := &options{
|
|
ExecArgs: os.Args,
|
|
}
|
|
|
|
for _, o := range opt {
|
|
o(opts)
|
|
}
|
|
|
|
go func() {
|
|
reap.ReapChildren(opts.PIDs, nil, opts.ReaperStop, opts.ReapLock)
|
|
if opts.ReaperStopped != nil {
|
|
close(opts.ReaperStopped)
|
|
}
|
|
}()
|
|
|
|
pwd, err := os.Getwd()
|
|
if err != nil {
|
|
return 1, xerrors.Errorf("get wd: %w", err)
|
|
}
|
|
|
|
pattrs := &syscall.ProcAttr{
|
|
Dir: pwd,
|
|
Env: os.Environ(),
|
|
Sys: &syscall.SysProcAttr{
|
|
Setsid: true,
|
|
},
|
|
Files: []uintptr{
|
|
uintptr(syscall.Stdin),
|
|
uintptr(syscall.Stdout),
|
|
uintptr(syscall.Stderr),
|
|
},
|
|
}
|
|
|
|
//#nosec G204
|
|
pid, err := syscall.ForkExec(opts.ExecArgs[0], opts.ExecArgs, pattrs)
|
|
if err != nil {
|
|
return 1, xerrors.Errorf("fork exec: %w", err)
|
|
}
|
|
|
|
startSignalForwarding(opts.Logger, pid, opts.CatchSignals)
|
|
|
|
var wstatus syscall.WaitStatus
|
|
_, err = syscall.Wait4(pid, &wstatus, 0, nil)
|
|
for xerrors.Is(err, syscall.EINTR) {
|
|
_, err = syscall.Wait4(pid, &wstatus, 0, nil)
|
|
}
|
|
|
|
// Convert wait status to exit code using standard Unix conventions:
|
|
// - Normal exit: use the exit code
|
|
// - Signal termination: use 128 + signal number
|
|
var exitCode int
|
|
switch {
|
|
case wstatus.Exited():
|
|
exitCode = wstatus.ExitStatus()
|
|
case wstatus.Signaled():
|
|
exitCode = 128 + int(wstatus.Signal())
|
|
default:
|
|
exitCode = 1
|
|
}
|
|
return exitCode, err
|
|
}
|