cmd/gitmirror: kill child processes upon timeout

We've seen evidence that we're not terminating Git subprocesses on
timeout. This is probably an instance of https://golang.org/issue/23019,
which we can deal with by killing all the children rather than just the
one we started.

Updates golang/go#38887.

Change-Id: Ie7999122f9c063f04de1a107a57fd307e7a5c1d2
Reviewed-on: https://go-review.googlesource.com/c/build/+/347294
Trust: Heschi Kreinick <heschi@google.com>
Run-TryBot: Heschi Kreinick <heschi@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/cmd/gitmirror/gitmirror.go b/cmd/gitmirror/gitmirror.go
index f68e8a6..f664aab 100644
--- a/cmd/gitmirror/gitmirror.go
+++ b/cmd/gitmirror/gitmirror.go
@@ -723,8 +723,12 @@
 	}
 }
 
-// runCommandContext runs cmd controlled by ctx.
-func runCmdContext(ctx context.Context, cmd *exec.Cmd) error {
+// runCmdContext allows OS-specific overrides of process execution behavior.
+// See runCmdContextLinux.
+var runCmdContext = runCmdContextDefault
+
+// runCommandContextDefault runs cmd controlled by ctx.
+func runCmdContextDefault(ctx context.Context, cmd *exec.Cmd) error {
 	if err := cmd.Start(); err != nil {
 		return err
 	}
diff --git a/cmd/gitmirror/gitmirror_linux.go b/cmd/gitmirror/gitmirror_linux.go
new file mode 100644
index 0000000..960443b
--- /dev/null
+++ b/cmd/gitmirror/gitmirror_linux.go
@@ -0,0 +1,48 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"os/exec"
+	"syscall"
+	"time"
+)
+
+func init() {
+	runCmdContext = runCmdContextLinux
+}
+
+// runCommandContext runs cmd controlled by ctx, killing it and all its
+// children if necessary. cmd.SysProcAttr must be unset.
+func runCmdContextLinux(ctx context.Context, cmd *exec.Cmd) error {
+	if cmd.SysProcAttr != nil {
+		return fmt.Errorf("cmd.SysProcAttr must be nil")
+	}
+	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
+	if err := cmd.Start(); err != nil {
+		return err
+	}
+	resChan := make(chan error, 1)
+	go func() {
+		resChan <- cmd.Wait()
+	}()
+
+	select {
+	case err := <-resChan:
+		return err
+	case <-ctx.Done():
+	}
+	// Canceled. Interrupt and see if it ends voluntarily.
+	cmd.Process.Signal(os.Interrupt)
+	select {
+	case <-resChan:
+		return ctx.Err()
+	case <-time.After(time.Second):
+	}
+	// Didn't shut down in response to interrupt. It may have child processes
+	// holding stdout/sterr open. Kill its process group hard.
+	syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
+	<-resChan
+	return ctx.Err()
+}