6ea551362e
Note: The original proxy/process_unix.go had a noop for setProcAttributes so it also did not stop grandchildren processes. This patch adds that capability and improves reliability. -- Stop() no longer hangs on a shell wrapper that forks the real binary. The upstream is built with exec.CommandContext + cmd.Cancel + cmd.WaitDelay, so cmd.Wait() returns even when a forked grandchild inherits the stdout/stderr pipes. killProcess sends the stop signal directly (not by cancelling the context) so cmd.WaitDelay measures from process exit and never silently caps the caller's graceful timeout. The upstream is also started in its own process group (Setpgid) on Unix, so the graceful SIGTERM — and the SIGKILL escalation after the timeout — are delivered to the whole group via the negative PID. A forked grandchild is reaped with its parent instead of leaking as an orphan. The loading-spinner SSE goroutine can no longer panic when it outlives the request. net/http recycles the response writer via Reset(nil) once ServeHTTP returns; the orphaned goroutine then flushed against a nil-backed writer and crashed with a SIGSEGV. A release() fence on loadingWriter lets any in-flight write finish then short-circuits later writes/flushes, and all three ServeHTTP select branches run a finishLoading helper (cancelLoad, waitForCompletion, release) before the writer is reclaimed. - internal/process: exec.CommandContext + WaitDelay, Setpgid process groups, group-wide SIGTERM/SIGKILL teardown - internal/router: release() fence + finishLoading on loadingWriter fixes #804
45 lines
1.6 KiB
Go
45 lines
1.6 KiB
Go
//go:build !windows
|
|
|
|
package process
|
|
|
|
import (
|
|
"os/exec"
|
|
"syscall"
|
|
)
|
|
|
|
// setProcAttributes starts the upstream in its own process group (Setpgid) so
|
|
// the entire process tree can be signalled at once via its negative PID. This
|
|
// is what lets us reap a forked grandchild — e.g. a shell wrapper that
|
|
// backgrounds the real binary and exits — instead of leaking it as an orphan
|
|
// that holds the inherited stdout/stderr pipes open.
|
|
func setProcAttributes(cmd *exec.Cmd) {
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
|
}
|
|
|
|
// terminateProcessTree sends SIGTERM to the whole process group led by the
|
|
// command, giving every process in the tree a chance to shut down gracefully.
|
|
func terminateProcessTree(cmd *exec.Cmd) error {
|
|
return signalProcessTree(cmd, syscall.SIGTERM)
|
|
}
|
|
|
|
// killProcessTree sends SIGKILL to the whole process group, force-terminating
|
|
// every process in the tree.
|
|
func killProcessTree(cmd *exec.Cmd) error {
|
|
return signalProcessTree(cmd, syscall.SIGKILL)
|
|
}
|
|
|
|
// signalProcessTree signals the process group led by cmd.Process. Because the
|
|
// child was started with Setpgid it is its own group leader (pgid == pid), so
|
|
// targeting -pid reaches the child and every descendant still in the group.
|
|
// Falls back to signalling just the child if the group send fails (e.g. the
|
|
// group has already drained), so we never silently skip the signal.
|
|
func signalProcessTree(cmd *exec.Cmd, sig syscall.Signal) error {
|
|
if cmd == nil || cmd.Process == nil {
|
|
return nil
|
|
}
|
|
if err := syscall.Kill(-cmd.Process.Pid, sig); err != nil {
|
|
return cmd.Process.Signal(sig)
|
|
}
|
|
return nil
|
|
}
|