diff --git a/config.go b/config.go index 2595431..5ad7f23 100644 --- a/config.go +++ b/config.go @@ -1,23 +1,30 @@ package process -import "os" +import ( + "os" + "time" +) type Option func(cfg *Config) error type Config struct { - Name string - Args []string - Combined bool - StateDir string - KillSignal *int - Environment []string - Stdin *os.File - WorkDir string + Name string + Args []string + Combined bool + StateDir string + KillSignal *int + Environment []string + Stdin *os.File + WorkDir string + GracefulTimeout time.Duration // Time to wait after SIGTERM before SIGKILL + KillProcessGroup bool // Whether to kill entire process group (default true) } func DefaultConfig() *Config { return &Config{ - Environment: os.Environ(), + Environment: os.Environ(), + KillProcessGroup: true, // Default to killing process group + GracefulTimeout: 15 * time.Second, // Default 15 second grace period } } diff --git a/go.mod b/go.mod index 3b43e1c..8db6790 100644 --- a/go.mod +++ b/go.mod @@ -1,25 +1,19 @@ module github.com/mudler/go-processmanager -go 1.18 +go 1.24.0 + +toolchain go1.24.11 require ( github.com/onsi/ginkgo v1.16.4 github.com/onsi/gomega v1.16.0 - github.com/shirou/gopsutil/v4 v4.24.7 + golang.org/x/sys v0.40.0 ) require ( github.com/fsnotify/fsnotify v1.4.9 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/nxadm/tail v1.4.8 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 // indirect - golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.3.6 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 9663c11..807d9c5 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -21,11 +18,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -37,25 +30,10 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/shirou/gopsutil/v4 v4.24.7 h1:V9UGTK4gQ8HvcnPKf6Zt3XHyQq/peaekfxpJ2HSocJk= -github.com/shirou/gopsutil/v4 v4.24.7/go.mod h1:0uW/073rP7FYLOkvxolUQM5rMOLTNmRXnFKafpb71rw= -github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= -github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -74,19 +52,15 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -118,4 +92,3 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/options.go b/options.go index 9e6a1ff..c52372a 100644 --- a/options.go +++ b/options.go @@ -2,6 +2,7 @@ package process import ( "os" + "time" ) // WithKillSignal sets the given signal while attemping to stop. Defaults to 9 @@ -61,3 +62,19 @@ func WithWorkDir(s string) func(cfg *Config) error { return nil } } + +// WithGracefulTimeout sets the duration to wait after SIGTERM before SIGKILL +func WithGracefulTimeout(d time.Duration) func(cfg *Config) error { + return func(cfg *Config) error { + cfg.GracefulTimeout = d + return nil + } +} + +// WithKillProcessGroup enables or disables killing the entire process group +func WithKillProcessGroup(b bool) func(cfg *Config) error { + return func(cfg *Config) error { + cfg.KillProcessGroup = b + return nil + } +} diff --git a/process.go b/process.go index a463716..79d8609 100644 --- a/process.go +++ b/process.go @@ -7,8 +7,7 @@ import ( "path/filepath" "strconv" "syscall" - - "github.com/shirou/gopsutil/v4/process" + "time" ) type Process struct { @@ -91,6 +90,14 @@ func (p *Process) Run() error { } } + // Set the current process as a subreaper to manage orphaned child processes + // This ensures that when child processes terminate, their zombie processes + // are reparented to us instead of init, allowing proper cleanup + if err := SetSubreaper(); err != nil { + // Non-fatal error - log and continue + // This will fail on non-Linux systems but that's expected + } + wd := p.config.WorkDir if wd == "" { var err error @@ -108,6 +115,7 @@ func (p *Process) Run() error { NewLog(p.StdoutPath()), NewLog(p.StderrPath()), }, + Sys: getSysProcAttr(), } args := append([]string{p.config.Name}, p.config.Args...) process, err := os.StartProcess(p.config.Name, args, proc) @@ -152,30 +160,51 @@ func (p *Process) IsAlive() bool { return false } -// Stop stops the running process by senging KillSignal to the PID annotated in the pidfile +// Stop stops the running process by sending KillSignal to the PID annotated in the pidfile func (p *Process) Stop() error { pid, err := p.readPID() - if err != nil || pid == "" { - return errors.New("no pid") + if err != nil { + return fmt.Errorf("failed to read PID: %w", err) } - // convert pid string to int32 + if pid == "" { + return errors.New("stop failed: PID is empty") + } + + // convert pid string to int pidInt, err := strconv.ParseInt(pid, 10, 64) if err != nil { - return err + return fmt.Errorf("failed to parse PID %q: %w", pid, err) } - backendProcess, err := process.NewProcess(int32(pidInt)) - if err != nil { - return err + // Determine which signal to send + signal := syscall.SIGTERM + if p.config.KillSignal != nil { + signal = syscall.Signal(*p.config.KillSignal) } - if p.config.KillSignal != nil { - backendProcess.SendSignal(syscall.Signal(*p.config.KillSignal)) - } else { - backendProcess.SendSignal(syscall.SIGTERM) - err = backendProcess.Kill() - if err != nil { - return err + // Send the initial signal (SIGTERM or custom KillSignal) + if err := killProcess(int(pidInt), signal, p.config.KillProcessGroup); err != nil { + return fmt.Errorf("failed to send signal %v to process %d: %w", signal, pidInt, err) + } + + // Wait for graceful timeout then send SIGKILL if the process is still alive + if p.config.GracefulTimeout > 0 { + // Wait for the graceful timeout period + deadline := time.Now().Add(p.config.GracefulTimeout) + for time.Now().Before(deadline) { + if !p.IsAlive() { + // Process has terminated, no need to force kill + p.release() + return nil + } + time.Sleep(100 * time.Millisecond) + } + + // If still alive after grace period, send SIGKILL + if p.IsAlive() { + if err := killProcess(int(pidInt), syscall.SIGKILL, p.config.KillProcessGroup); err != nil { + return fmt.Errorf("failed to send SIGKILL to process %d: %w", pidInt, err) + } } } @@ -208,6 +237,11 @@ func (p *Process) monitor() { if p.proc == nil { return } + + // Start a goroutine to reap orphaned child processes + // This is needed when we're acting as a subreaper + go p.reapChildren() + status := make(chan *os.ProcessState) died := make(chan error) go func() { diff --git a/process_test.go b/process_test.go index 1d060e3..298e31a 100644 --- a/process_test.go +++ b/process_test.go @@ -138,4 +138,51 @@ exit 2 Expect(e).To(Equal("2")) }) }) + + Context("process group termination", func() { + It("kills child processes when parent is stopped", func() { + dir, err := os.MkdirTemp(os.TempDir(), "") + Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(dir) + + // Start a parent process that spawns a child process + p := New( + WithStateDir(dir), + WithName("/bin/bash"), + WithArgs("-c", ` +echo "starting" + +# Start a child process that runs indefinitely +( + while true; do + sleep 1 + done +) & + +# Parent also runs indefinitely +while true; do + sleep 1 +done +`), + ) + Expect(p.Run()).ToNot(HaveOccurred()) + + // Wait for the process to start + Eventually(func() string { + c, _ := os.ReadFile(p.StdoutPath()) + return string(c) + }, "5s").Should(ContainSubstring("starting")) + + // Verify process is alive + Expect(p.IsAlive()).To(BeTrue()) + + // Stop the parent process (should kill entire process group) + Expect(p.Stop()).ToNot(HaveOccurred()) + + // Verify the process is no longer alive + Eventually(func() bool { + return p.IsAlive() + }, "10s").Should(BeFalse()) + }) + }) }) diff --git a/process_unix.go b/process_unix.go new file mode 100644 index 0000000..3cc3b47 --- /dev/null +++ b/process_unix.go @@ -0,0 +1,58 @@ +//go:build unix + +package process + +import ( + "syscall" + "time" +) + +// getSysProcAttr returns the platform-specific syscall attributes for process creation +func getSysProcAttr() *syscall.SysProcAttr { + return &syscall.SysProcAttr{ + Setpgid: true, // Create new process group on Unix + } +} + +// killProcess sends a signal to a process or process group +func killProcess(pid int, signal syscall.Signal, killProcessGroup bool) error { + target := pid + if killProcessGroup { + // Use negative PID to target the entire process group on Unix + target = -pid + } + return syscall.Kill(target, signal) +} + +// reapChildren waits for and reaps orphaned child processes +// This is called when the process manager is acting as a subreaper +func (p *Process) reapChildren() { + for { + // Wait for any child process with WNOHANG (non-blocking) + var status syscall.WaitStatus + pid, err := syscall.Wait4(-1, &status, syscall.WNOHANG, nil) + + // If we successfully reaped a child, continue the loop immediately + // to check for more children + if err == nil && pid > 0 { + continue + } + + // No children to reap right now (ECHILD means no children, pid == 0 means no children ready) + if err == syscall.ECHILD || pid == 0 { + // Check if the main process is still alive + // If not, we can exit the reaper + if p.proc == nil || !p.IsAlive() { + return + } + // Sleep briefly before checking again to prevent busy-waiting + time.Sleep(100 * time.Millisecond) + continue + } + + // For other errors, exit the reaper + if err != nil { + return + } + } +} diff --git a/process_windows.go b/process_windows.go new file mode 100644 index 0000000..bac29e7 --- /dev/null +++ b/process_windows.go @@ -0,0 +1,31 @@ +//go:build windows + +package process + +import ( + "os" + "syscall" +) + +// getSysProcAttr returns the platform-specific syscall attributes for process creation +func getSysProcAttr() *syscall.SysProcAttr { + // Windows doesn't support process groups in the same way as Unix + return &syscall.SysProcAttr{} +} + +// killProcess sends a signal to a process on Windows +func killProcess(pid int, signal syscall.Signal, killProcessGroup bool) error { + // On Windows, we need to use os.FindProcess and Kill + // Process groups are not supported in the same way + proc, err := os.FindProcess(pid) + if err != nil { + return err + } + return proc.Kill() +} + +// reapChildren is a no-op on Windows +// Windows doesn't have the same zombie process concept as Unix +func (p *Process) reapChildren() { + // No-op on Windows +} diff --git a/subreaper_linux.go b/subreaper_linux.go new file mode 100644 index 0000000..a50c42f --- /dev/null +++ b/subreaper_linux.go @@ -0,0 +1,15 @@ +//go:build linux + +package process + +import "golang.org/x/sys/unix" + +// SetSubreaper configures the calling process to be a subreaper. +// A subreaper fulfills the role of init(1) for its descendant processes. +// When a process becomes orphaned (its immediate parent terminates), it will be +// reparented to the nearest still living ancestor subreaper. +// This is useful in containerized environments to ensure proper cleanup of +// orphaned child processes. +func SetSubreaper() error { + return unix.Prctl(unix.PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) +} diff --git a/subreaper_other.go b/subreaper_other.go new file mode 100644 index 0000000..a02f8cf --- /dev/null +++ b/subreaper_other.go @@ -0,0 +1,9 @@ +//go:build !linux + +package process + +// SetSubreaper is a no-op on non-Linux platforms. +// On Linux, it configures the calling process to be a subreaper. +func SetSubreaper() error { + return nil +}