diff --git a/process.go b/process.go index 2c153d8..76cfc5d 100644 --- a/process.go +++ b/process.go @@ -166,6 +166,22 @@ func (p *Process) kill() (bool, error) { return true, p.cmd.Process.Kill() } +// killTree forcefully terminates the process group when one exists. +func (p *Process) killTree() (bool, error) { + p.mu.Lock() + defer p.mu.Unlock() + + if p.Status != StatusRunning { + return false, nil + } + + if p.cmd == nil || p.cmd.Process == nil { + return false, nil + } + + return true, syscall.Kill(-p.cmd.Process.Pid, syscall.SIGKILL) +} + // Shutdown gracefully stops the process: SIGTERM, then SIGKILL after grace period. // If GracePeriod was not set (zero), falls back to immediate Kill(). // If KillGroup is set, signals are sent to the entire process group. diff --git a/service.go b/service.go index 13cc019..c0d3c6b 100644 --- a/service.go +++ b/service.go @@ -112,7 +112,7 @@ func (s *Service) OnShutdown(ctx context.Context) error { s.mu.RUnlock() for _, p := range procs { - _ = p.Kill() + _, _ = p.killTree() } return nil @@ -165,10 +165,9 @@ func (s *Service) StartWithOptions(ctx context.Context, opts RunOptions) (*Proce cmd.Env = append(cmd.Environ(), opts.Env...) } - // Detached processes get their own process group - if opts.Detach { - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - } + // Put every subprocess in its own process group so shutdown can terminate + // the full tree without affecting the parent process. + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} // Set up pipes stdout, err := cmd.StdoutPipe()