Skip to content

Commit ee59182

Browse files
authored
fix: cleanup reaper implementation (#2563)
- Clean up the agent/reaper API to be a more isolated and reusable package.
1 parent 0585372 commit ee59182

File tree

5 files changed

+50
-41
lines changed

5 files changed

+50
-41
lines changed

agent/reaper/reaper.go

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package reaper
2+
3+
import "github.com/hashicorp/go-reap"
4+
5+
type Option func(o *options)
6+
7+
// WithExecArgs specifies the exec arguments for the fork exec call.
8+
// By default the same arguments as the parent are used as dictated by
9+
// os.Args. Since ForkReap calls a fork-exec it is the responsibility of
10+
// the caller to avoid fork-bombing oneself.
11+
func WithExecArgs(args ...string) Option {
12+
return func(o *options) {
13+
o.ExecArgs = args
14+
}
15+
}
16+
17+
// WithPIDCallback sets the channel that reaped child process PIDs are pushed
18+
// onto.
19+
func WithPIDCallback(ch reap.PidCh) Option {
20+
return func(o *options) {
21+
o.PIDs = ch
22+
}
23+
}
24+
25+
type options struct {
26+
ExecArgs []string
27+
PIDs reap.PidCh
28+
}

agent/reaper/reaper_stub.go

+1-8
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,11 @@
22

33
package reaper
44

5-
import "github.com/hashicorp/go-reap"
6-
7-
// IsChild returns true if we're the forked process.
8-
func IsChild() bool {
9-
return false
10-
}
11-
125
// IsInitProcess returns true if the current process's PID is 1.
136
func IsInitProcess() bool {
147
return false
158
}
169

17-
func ForkReap(_ reap.PidCh) error {
10+
func ForkReap(opt ...Option) error {
1811
return nil
1912
}

agent/reaper/reaper_test.go

+5-6
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,16 @@ func TestReap(t *testing.T) {
2424
t.Skip("Detected CI, skipping reaper tests")
2525
}
2626

27-
// Because we're forkexecing these tests will try to run twice...
28-
if reaper.IsChild() {
29-
t.Skip("I'm a child!")
30-
}
31-
3227
// OK checks that's the reaper is successfully reaping
3328
// exited processes and passing the PIDs through the shared
3429
// channel.
3530
t.Run("OK", func(t *testing.T) {
3631
pids := make(reap.PidCh, 1)
37-
err := reaper.ForkReap(pids)
32+
err := reaper.ForkReap(
33+
reaper.WithPIDCallback(pids),
34+
// Provide some argument that immediately exits.
35+
reaper.WithExecArgs("/bin/sh", "-c", "exit 0"),
36+
)
3837
require.NoError(t, err)
3938

4039
cmd := exec.Command("tail", "-f", "/dev/null")

agent/reaper/reaper_unix.go

+9-25
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,13 @@
33
package reaper
44

55
import (
6-
"fmt"
76
"os"
87
"syscall"
98

109
"github.com/hashicorp/go-reap"
1110
"golang.org/x/xerrors"
1211
)
1312

14-
// agentEnvMark is a simple environment variable that we use as a marker
15-
// to indicated that the process is a child as opposed to the reaper.
16-
// Since we are forkexec'ing we need to be able to differentiate between
17-
// the two to avoid fork bombing ourselves.
18-
const agentEnvMark = "CODER_DO_NOT_REAP"
19-
20-
// IsChild returns true if we're the forked process.
21-
func IsChild() bool {
22-
return os.Getenv(agentEnvMark) != ""
23-
}
24-
2513
// IsInitProcess returns true if the current process's PID is 1.
2614
func IsInitProcess() bool {
2715
return os.Getpid() == 1
@@ -33,19 +21,16 @@ func IsInitProcess() bool {
3321
// the reaper and an exec.Command waiting for its process to complete.
3422
// The provided 'pids' channel may be nil if the caller does not care about the
3523
// reaped children PIDs.
36-
func ForkReap(pids reap.PidCh) error {
37-
// Check if the process is the parent or the child.
38-
// If it's the child we want to skip attempting to reap.
39-
if IsChild() {
40-
return nil
24+
func ForkReap(opt ...Option) error {
25+
opts := &options{
26+
ExecArgs: os.Args,
4127
}
4228

43-
go reap.ReapChildren(pids, nil, nil, nil)
29+
for _, o := range opt {
30+
o(opts)
31+
}
4432

45-
args := os.Args
46-
// This is simply done to help identify the real agent process
47-
// when viewing in something like 'ps'.
48-
args = append(args, "#Agent")
33+
go reap.ReapChildren(opts.PIDs, nil, nil, nil)
4934

5035
pwd, err := os.Getwd()
5136
if err != nil {
@@ -54,8 +39,7 @@ func ForkReap(pids reap.PidCh) error {
5439

5540
pattrs := &syscall.ProcAttr{
5641
Dir: pwd,
57-
// Add our marker for identifying the child process.
58-
Env: append(os.Environ(), fmt.Sprintf("%s=true", agentEnvMark)),
42+
Env: os.Environ(),
5943
Sys: &syscall.SysProcAttr{
6044
Setsid: true,
6145
},
@@ -67,7 +51,7 @@ func ForkReap(pids reap.PidCh) error {
6751
}
6852

6953
//#nosec G204
70-
pid, _ := syscall.ForkExec(args[0], args, pattrs)
54+
pid, _ := syscall.ForkExec(opts.ExecArgs[0], opts.ExecArgs, pattrs)
7155

7256
var wstatus syscall.WaitStatus
7357
_, err = syscall.Wait4(pid, &wstatus, 0, nil)

cli/agent.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func workspaceAgent() *cobra.Command {
3232
auth string
3333
pprofEnabled bool
3434
pprofAddress string
35+
noReap bool
3536
)
3637
cmd := &cobra.Command{
3738
Use: "agent",
@@ -58,9 +59,12 @@ func workspaceAgent() *cobra.Command {
5859

5960
// Spawn a reaper so that we don't accumulate a ton
6061
// of zombie processes.
61-
if reaper.IsInitProcess() && !reaper.IsChild() && isLinux {
62+
if reaper.IsInitProcess() && !noReap && isLinux {
6263
logger.Info(cmd.Context(), "spawning reaper process")
63-
err := reaper.ForkReap(nil)
64+
// Do not start a reaper on the child process. It's important
65+
// to do this else we fork bomb ourselves.
66+
args := append(os.Args, "--no-reap")
67+
err := reaper.ForkReap(reaper.WithExecArgs(args...))
6468
if err != nil {
6569
logger.Error(cmd.Context(), "failed to reap", slog.Error(err))
6670
return xerrors.Errorf("fork reap: %w", err)
@@ -182,6 +186,7 @@ func workspaceAgent() *cobra.Command {
182186

183187
cliflag.StringVarP(cmd.Flags(), &auth, "auth", "", "CODER_AGENT_AUTH", "token", "Specify the authentication type to use for the agent")
184188
cliflag.BoolVarP(cmd.Flags(), &pprofEnabled, "pprof-enable", "", "CODER_AGENT_PPROF_ENABLE", false, "Enable serving pprof metrics on the address defined by --pprof-address.")
189+
cliflag.BoolVarP(cmd.Flags(), &noReap, "no-reap", "", "", false, "Do not start a process reaper.")
185190
cliflag.StringVarP(cmd.Flags(), &pprofAddress, "pprof-address", "", "CODER_AGENT_PPROF_ADDRESS", "127.0.0.1:6060", "The address to serve pprof.")
186191
return cmd
187192
}

0 commit comments

Comments
 (0)