Ctrl K
ComunidadGuías…

agent/reaper/reaper_unix.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//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
}