Skip to content

Commit 7a015ab

Browse files
findleyrgopherbot
authored andcommitted
internal/gocommand: send SIGQUIT to hanging go commands on posix
Existing debug information has not proven useful for understanding hanging go commands. On posix systems we can send SIGQUIT, and peek at stderr, to try to see what the go command is doing. Tested locally by setting the timeout to 10ms. Updates golang/go#54461 Change-Id: I1bc38d6da82b0dffb55081364b0af8f20a3afcfc Reviewed-on: https://go-review.googlesource.com/c/tools/+/643915 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Auto-Submit: Robert Findley <[email protected]>
1 parent fcc9d81 commit 7a015ab

File tree

3 files changed

+53
-11
lines changed

3 files changed

+53
-11
lines changed

internal/gocommand/invoke.go

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ type Invocation struct {
179179
CleanEnv bool
180180
Env []string
181181
WorkingDir string
182-
Logf func(format string, args ...interface{})
182+
Logf func(format string, args ...any)
183183
}
184184

185185
// Postcondition: both error results have same nilness.
@@ -388,7 +388,9 @@ func runCmdContext(ctx context.Context, cmd *exec.Cmd) (err error) {
388388
case err := <-resChan:
389389
return err
390390
case <-timer.C:
391-
HandleHangingGoCommand(startTime, cmd)
391+
// HandleHangingGoCommand terminates this process.
392+
// Pass off resChan in case we can collect the command error.
393+
handleHangingGoCommand(startTime, cmd, resChan)
392394
case <-ctx.Done():
393395
}
394396
} else {
@@ -413,32 +415,32 @@ func runCmdContext(ctx context.Context, cmd *exec.Cmd) (err error) {
413415
}
414416

415417
// Didn't shut down in response to interrupt. Kill it hard.
416-
// TODO(rfindley): per advice from bcmills@, it may be better to send SIGQUIT
417-
// on certain platforms, such as unix.
418418
if err := cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) && debug {
419419
log.Printf("error killing the Go command: %v", err)
420420
}
421421

422422
return <-resChan
423423
}
424424

425-
func HandleHangingGoCommand(start time.Time, cmd *exec.Cmd) {
425+
// handleHangingGoCommand outputs debugging information to help diagnose the
426+
// cause of a hanging Go command, and then exits with log.Fatalf.
427+
func handleHangingGoCommand(start time.Time, cmd *exec.Cmd, resChan chan error) {
426428
switch runtime.GOOS {
427429
case "linux", "darwin", "freebsd", "netbsd":
428430
fmt.Fprintln(os.Stderr, `DETECTED A HANGING GO COMMAND
429431
430-
The gopls test runner has detected a hanging go command. In order to debug
431-
this, the output of ps and lsof/fstat is printed below.
432+
The gopls test runner has detected a hanging go command. In order to debug
433+
this, the output of ps and lsof/fstat is printed below.
432434
433-
See golang/go#54461 for more details.`)
435+
See golang/go#54461 for more details.`)
434436

435437
fmt.Fprintln(os.Stderr, "\nps axo ppid,pid,command:")
436438
fmt.Fprintln(os.Stderr, "-------------------------")
437439
psCmd := exec.Command("ps", "axo", "ppid,pid,command")
438440
psCmd.Stdout = os.Stderr
439441
psCmd.Stderr = os.Stderr
440442
if err := psCmd.Run(); err != nil {
441-
panic(fmt.Sprintf("running ps: %v", err))
443+
log.Printf("Handling hanging Go command: running ps: %v", err)
442444
}
443445

444446
listFiles := "lsof"
@@ -452,10 +454,24 @@ See golang/go#54461 for more details.`)
452454
listFilesCmd.Stdout = os.Stderr
453455
listFilesCmd.Stderr = os.Stderr
454456
if err := listFilesCmd.Run(); err != nil {
455-
panic(fmt.Sprintf("running %s: %v", listFiles, err))
457+
log.Printf("Handling hanging Go command: running %s: %v", listFiles, err)
458+
}
459+
// Try to extract information about the slow go process by issuing a SIGQUIT.
460+
if err := cmd.Process.Signal(sigStuckProcess); err == nil {
461+
select {
462+
case err := <-resChan:
463+
stderr := "not a bytes.Buffer"
464+
if buf, _ := cmd.Stderr.(*bytes.Buffer); buf != nil {
465+
stderr = buf.String()
466+
}
467+
log.Printf("Quit hanging go command:\n\terr:%v\n\tstderr:\n%v\n\n", err, stderr)
468+
case <-time.After(5 * time.Second):
469+
}
470+
} else {
471+
log.Printf("Sending signal %d to hanging go command: %v", sigStuckProcess, err)
456472
}
457473
}
458-
panic(fmt.Sprintf("detected hanging go command (golang/go#54461); waited %s\n\tcommand:%s\n\tpid:%d", time.Since(start), cmd, cmd.Process.Pid))
474+
log.Fatalf("detected hanging go command (golang/go#54461); waited %s\n\tcommand:%s\n\tpid:%d", time.Since(start), cmd, cmd.Process.Pid)
459475
}
460476

461477
func cmdDebugStr(cmd *exec.Cmd) string {

internal/gocommand/invoke_notunix.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build !unix
6+
7+
package gocommand
8+
9+
import "os"
10+
11+
// sigStuckProcess is the signal to send to kill a hanging subprocess.
12+
// On Unix we send SIGQUIT, but on non-Unix we only have os.Kill.
13+
var sigStuckProcess = os.Kill

internal/gocommand/invoke_unix.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build unix
6+
7+
package gocommand
8+
9+
import "syscall"
10+
11+
// Sigstuckprocess is the signal to send to kill a hanging subprocess.
12+
// Send SIGQUIT to get a stack trace.
13+
var sigStuckProcess = syscall.SIGQUIT

0 commit comments

Comments
 (0)