// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT //go:build !windows && !plan9 package container import ( "errors" "os" "syscall" ) // processKiller terminates a step process together with its whole process // group, which is the Unix counterpart of the Windows Job Object tree-kill. // // Background: a step often launches a process tree (a shell that starts a child // which in turn spawns further background processes). The default // exec.CommandContext cancellation only kills the direct child, so cancelling a // job left the rest of the tree running. Because those orphans inherited the // step's stdout/stderr pipe, cmd.Wait() also blocked forever and the runner // hung. // // Steps are started with Setpgid (or Setsid for the PTY path, see // getSysProcAttr), which makes the step process the leader of a new process // group whose ID equals its PID. Signalling the negative PID delivers to every // process still in that group, so we can tear down the whole tree atomically on // cancellation, which also closes the inherited pipe handles so cmd.Wait() can // return. type processKiller struct { pgid int } // newProcessKiller captures the process group of p (an already-started // process). Because the step is launched with Setpgid/Setsid, p is a group // leader and its PGID equals its PID; children spawned afterwards stay in the // same group unless they explicitly create their own. func newProcessKiller(p *os.Process) (*processKiller, error) { return &processKiller{pgid: p.Pid}, nil } // Kill sends SIGKILL to the entire process group (the step process and every // descendant that stayed in the group). A missing group (ESRCH) means the // processes already exited and is not treated as an error. func (k *processKiller) Kill() error { if k == nil || k.pgid <= 0 { return nil } if err := syscall.Kill(-k.pgid, syscall.SIGKILL); err != nil && !errors.Is(err, syscall.ESRCH) { return err } return nil } // Close is a no-op on Unix; there is no job handle to release. func (k *processKiller) Close() error { return nil }