process,router: make model shutdown and load-streaming robust

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
This commit is contained in:
Benson Wong
2026-05-31 10:11:12 -07:00
committed by GitHub
parent 03d58e53fa
commit 6ea551362e
6 changed files with 475 additions and 49 deletions
+20
View File
@@ -14,3 +14,23 @@ func setProcAttributes(cmd *exec.Cmd) {
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
}
}
// terminateProcessTree asks the upstream process to stop. Windows has no
// process-group signalling here — process-tree teardown is handled by the
// configured CmdStop, which defaults to `taskkill /f /t` — so this preserves
// the previous single-process SIGTERM behaviour.
func terminateProcessTree(cmd *exec.Cmd) error {
if cmd == nil || cmd.Process == nil {
return nil
}
return cmd.Process.Signal(syscall.SIGTERM)
}
// killProcessTree force-terminates the upstream process. Tree teardown on
// Windows relies on CmdStop (taskkill /t); this kills the launched process.
func killProcessTree(cmd *exec.Cmd) error {
if cmd == nil || cmd.Process == nil {
return nil
}
return cmd.Process.Kill()
}