mirror of
https://github.com/Cheviiot/Vintner.git
synced 2026-08-03 15:57:24 +00:00
Stop orphaning wine subprocesses when vintner is killed by PID
Every wrapped tool invocation (cl/link/msbuild/etc via wine, plus the native cmd/findstr shims) now starts its child in its own process group and forwards SIGINT/SIGTERM to that group, escalating to SIGKILL after a 5s grace period if it doesn't exit. Previously, interactive Ctrl-C happened to work by accident (the child inherited the terminal's foreground process group and got the signal directly), but anything that signals vintner by PID alone - a CI job's timeout, a supervisor's `kill <pid>` - never reached the wine/wineserver tree underneath it, which got reparented to init and kept running: wasted CPU, held file locks, stray FIFOs/temp files. Verified two ways: a unit test (signals_test.go) that starts a detached `sleep 30`, signals the test process itself, and checks the child actually dies; and a real end-to-end run - killed an in-flight `msbuild` driver build by PID mid-compile and confirmed no orphaned msbuild/cl/link/vintner process was left behind (wineserver and its persistent service processes are expected to survive, by design - see pipeDrainGrace's doc comment).
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// killGrace bounds how long a forwarded SIGINT/SIGTERM gets to make a
|
||||
// subprocess tree exit on its own before escalating to SIGKILL - long
|
||||
// enough for wineserver to tear down a Windows process tree cleanly, short
|
||||
// enough that an unresponsive one doesn't hang vintner's own shutdown.
|
||||
const killGrace = 5 * time.Second
|
||||
|
||||
// setNewProcessGroup puts cmd's eventual child in its own process group
|
||||
// (pgid = its own pid) instead of inheriting vintner's. Without this, a
|
||||
// caller that signals vintner by PID alone (a CI runner enforcing a
|
||||
// timeout, a supervisor's `kill <pid>`) never reaches the wine/wineserver
|
||||
// tree underneath it, which is then reparented to init and keeps running -
|
||||
// wasting CPU, holding file locks, leaving stray FIFOs/temp files behind.
|
||||
// (Interactive Ctrl-C already reaches every process in the terminal's
|
||||
// foreground group regardless of this, but forwardSignals below handles
|
||||
// that case too now that the child has moved to its own group.)
|
||||
func setNewProcessGroup(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
}
|
||||
|
||||
// forwardSignals relays SIGINT/SIGTERM received by vintner itself to
|
||||
// proc's entire process group (proc must have been started via a cmd that
|
||||
// called setNewProcessGroup, making proc.Pid also the group id), escalating
|
||||
// to SIGKILL after killGrace if the group hasn't exited by then. Callers
|
||||
// must call the returned stop func once the process has actually exited
|
||||
// (e.g. right after cmd.Wait() returns), both to stop listening for
|
||||
// signals and to cancel a pending escalation.
|
||||
func forwardSignals(proc *os.Process) (stop func()) {
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
pgid := -proc.Pid
|
||||
for {
|
||||
select {
|
||||
case sig := <-sigCh:
|
||||
s, ok := sig.(syscall.Signal)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
_ = syscall.Kill(pgid, s)
|
||||
select {
|
||||
case <-time.After(killGrace):
|
||||
_ = syscall.Kill(pgid, syscall.SIGKILL)
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return func() {
|
||||
signal.Stop(sigCh)
|
||||
close(done)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user