fix: cleanup reaper implementation (#2563)

- Clean up the agent/reaper API to be a more isolated and reusable package.
This commit is contained in:
Jon Ayers
2022-06-21 18:01:34 -05:00
committed by GitHub
parent 0585372170
commit ee5918217b
5 changed files with 50 additions and 41 deletions

28
agent/reaper/reaper.go Normal file
View File

@ -0,0 +1,28 @@
package reaper
import "github.com/hashicorp/go-reap"
type Option func(o *options)
// WithExecArgs specifies the exec arguments for the fork exec call.
// By default the same arguments as the parent are used as dictated by
// os.Args. Since ForkReap calls a fork-exec it is the responsibility of
// the caller to avoid fork-bombing oneself.
func WithExecArgs(args ...string) Option {
return func(o *options) {
o.ExecArgs = args
}
}
// WithPIDCallback sets the channel that reaped child process PIDs are pushed
// onto.
func WithPIDCallback(ch reap.PidCh) Option {
return func(o *options) {
o.PIDs = ch
}
}
type options struct {
ExecArgs []string
PIDs reap.PidCh
}

View File

@ -2,18 +2,11 @@
package reaper package reaper
import "github.com/hashicorp/go-reap"
// IsChild returns true if we're the forked process.
func IsChild() bool {
return false
}
// IsInitProcess returns true if the current process's PID is 1. // IsInitProcess returns true if the current process's PID is 1.
func IsInitProcess() bool { func IsInitProcess() bool {
return false return false
} }
func ForkReap(_ reap.PidCh) error { func ForkReap(opt ...Option) error {
return nil return nil
} }

View File

@ -24,17 +24,16 @@ func TestReap(t *testing.T) {
t.Skip("Detected CI, skipping reaper tests") t.Skip("Detected CI, skipping reaper tests")
} }
// Because we're forkexecing these tests will try to run twice...
if reaper.IsChild() {
t.Skip("I'm a child!")
}
// OK checks that's the reaper is successfully reaping // OK checks that's the reaper is successfully reaping
// exited processes and passing the PIDs through the shared // exited processes and passing the PIDs through the shared
// channel. // channel.
t.Run("OK", func(t *testing.T) { t.Run("OK", func(t *testing.T) {
pids := make(reap.PidCh, 1) pids := make(reap.PidCh, 1)
err := reaper.ForkReap(pids) err := reaper.ForkReap(
reaper.WithPIDCallback(pids),
// Provide some argument that immediately exits.
reaper.WithExecArgs("/bin/sh", "-c", "exit 0"),
)
require.NoError(t, err) require.NoError(t, err)
cmd := exec.Command("tail", "-f", "/dev/null") cmd := exec.Command("tail", "-f", "/dev/null")

View File

@ -3,7 +3,6 @@
package reaper package reaper
import ( import (
"fmt"
"os" "os"
"syscall" "syscall"
@ -11,17 +10,6 @@ import (
"golang.org/x/xerrors" "golang.org/x/xerrors"
) )
// agentEnvMark is a simple environment variable that we use as a marker
// to indicated that the process is a child as opposed to the reaper.
// Since we are forkexec'ing we need to be able to differentiate between
// the two to avoid fork bombing ourselves.
const agentEnvMark = "CODER_DO_NOT_REAP"
// IsChild returns true if we're the forked process.
func IsChild() bool {
return os.Getenv(agentEnvMark) != ""
}
// IsInitProcess returns true if the current process's PID is 1. // IsInitProcess returns true if the current process's PID is 1.
func IsInitProcess() bool { func IsInitProcess() bool {
return os.Getpid() == 1 return os.Getpid() == 1
@ -33,19 +21,16 @@ func IsInitProcess() bool {
// the reaper and an exec.Command waiting for its process to complete. // 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 // The provided 'pids' channel may be nil if the caller does not care about the
// reaped children PIDs. // reaped children PIDs.
func ForkReap(pids reap.PidCh) error { func ForkReap(opt ...Option) error {
// Check if the process is the parent or the child. opts := &options{
// If it's the child we want to skip attempting to reap. ExecArgs: os.Args,
if IsChild() {
return nil
} }
go reap.ReapChildren(pids, nil, nil, nil) for _, o := range opt {
o(opts)
}
args := os.Args go reap.ReapChildren(opts.PIDs, nil, nil, nil)
// This is simply done to help identify the real agent process
// when viewing in something like 'ps'.
args = append(args, "#Agent")
pwd, err := os.Getwd() pwd, err := os.Getwd()
if err != nil { if err != nil {
@ -54,8 +39,7 @@ func ForkReap(pids reap.PidCh) error {
pattrs := &syscall.ProcAttr{ pattrs := &syscall.ProcAttr{
Dir: pwd, Dir: pwd,
// Add our marker for identifying the child process. Env: os.Environ(),
Env: append(os.Environ(), fmt.Sprintf("%s=true", agentEnvMark)),
Sys: &syscall.SysProcAttr{ Sys: &syscall.SysProcAttr{
Setsid: true, Setsid: true,
}, },
@ -67,7 +51,7 @@ func ForkReap(pids reap.PidCh) error {
} }
//#nosec G204 //#nosec G204
pid, _ := syscall.ForkExec(args[0], args, pattrs) pid, _ := syscall.ForkExec(opts.ExecArgs[0], opts.ExecArgs, pattrs)
var wstatus syscall.WaitStatus var wstatus syscall.WaitStatus
_, err = syscall.Wait4(pid, &wstatus, 0, nil) _, err = syscall.Wait4(pid, &wstatus, 0, nil)

View File

@ -32,6 +32,7 @@ func workspaceAgent() *cobra.Command {
auth string auth string
pprofEnabled bool pprofEnabled bool
pprofAddress string pprofAddress string
noReap bool
) )
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "agent", Use: "agent",
@ -58,9 +59,12 @@ func workspaceAgent() *cobra.Command {
// Spawn a reaper so that we don't accumulate a ton // Spawn a reaper so that we don't accumulate a ton
// of zombie processes. // of zombie processes.
if reaper.IsInitProcess() && !reaper.IsChild() && isLinux { if reaper.IsInitProcess() && !noReap && isLinux {
logger.Info(cmd.Context(), "spawning reaper process") logger.Info(cmd.Context(), "spawning reaper process")
err := reaper.ForkReap(nil) // Do not start a reaper on the child process. It's important
// to do this else we fork bomb ourselves.
args := append(os.Args, "--no-reap")
err := reaper.ForkReap(reaper.WithExecArgs(args...))
if err != nil { if err != nil {
logger.Error(cmd.Context(), "failed to reap", slog.Error(err)) logger.Error(cmd.Context(), "failed to reap", slog.Error(err))
return xerrors.Errorf("fork reap: %w", err) return xerrors.Errorf("fork reap: %w", err)
@ -182,6 +186,7 @@ func workspaceAgent() *cobra.Command {
cliflag.StringVarP(cmd.Flags(), &auth, "auth", "", "CODER_AGENT_AUTH", "token", "Specify the authentication type to use for the agent") cliflag.StringVarP(cmd.Flags(), &auth, "auth", "", "CODER_AGENT_AUTH", "token", "Specify the authentication type to use for the agent")
cliflag.BoolVarP(cmd.Flags(), &pprofEnabled, "pprof-enable", "", "CODER_AGENT_PPROF_ENABLE", false, "Enable serving pprof metrics on the address defined by --pprof-address.") cliflag.BoolVarP(cmd.Flags(), &pprofEnabled, "pprof-enable", "", "CODER_AGENT_PPROF_ENABLE", false, "Enable serving pprof metrics on the address defined by --pprof-address.")
cliflag.BoolVarP(cmd.Flags(), &noReap, "no-reap", "", "", false, "Do not start a process reaper.")
cliflag.StringVarP(cmd.Flags(), &pprofAddress, "pprof-address", "", "CODER_AGENT_PPROF_ADDRESS", "127.0.0.1:6060", "The address to serve pprof.") cliflag.StringVarP(cmd.Flags(), &pprofAddress, "pprof-address", "", "CODER_AGENT_PPROF_ADDRESS", "127.0.0.1:6060", "The address to serve pprof.")
return cmd return cmd
} }