Skip to content

Commit 49abdbc

Browse files
author
Bryan C. Mills
committed
cmd/go/internal/script: use the Cancel and WaitDelay fields for subprocesses
The Cancel and WaitDelay fields recently added to exec.Cmd are intended to support exactly the sort of cancellation behavior that we need for script tests. Use them, and simplify the cmd/go tests accordingly. The more robust implementation may also help to diagose recurring test hangs (#50187). For #50187. Updates #27494. Change-Id: I7817fca0dd9a18e18984a252d3116f6a5275a401 Reviewed-on: https://go-review.googlesource.com/c/go/+/445357 Run-TryBot: Bryan Mills <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 939f9fd commit 49abdbc

File tree

3 files changed

+30
-84
lines changed

3 files changed

+30
-84
lines changed

src/cmd/go/internal/script/cmds.go

+15-78
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package script
66

77
import (
88
"cmd/go/internal/robustio"
9-
"context"
109
"errors"
1110
"fmt"
1211
"internal/diff"
@@ -36,7 +35,7 @@ func DefaultCmds() map[string]Cmd {
3635
"cp": Cp(),
3736
"echo": Echo(),
3837
"env": Env(),
39-
"exec": Exec(os.Interrupt, 100*time.Millisecond), // arbitrary grace period
38+
"exec": Exec(func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }, 100*time.Millisecond), // arbitrary grace period
4039
"exists": Exists(),
4140
"grep": Grep(),
4241
"help": Help(),
@@ -400,7 +399,7 @@ func Env() Cmd {
400399
// When the Script's context is canceled, Exec sends the interrupt signal, then
401400
// waits for up to the given delay for the subprocess to flush output before
402401
// terminating it with os.Kill.
403-
func Exec(interrupt os.Signal, delay time.Duration) Cmd {
402+
func Exec(cancel func(*exec.Cmd) error, waitDelay time.Duration) Cmd {
404403
return Command(
405404
CmdUsage{
406405
Summary: "run an executable program with arguments",
@@ -428,13 +427,19 @@ func Exec(interrupt os.Signal, delay time.Duration) Cmd {
428427
}
429428
}
430429

431-
return startCommand(s, name, path, args[1:], interrupt, delay)
430+
return startCommand(s, name, path, args[1:], cancel, waitDelay)
432431
})
433432
}
434433

435-
func startCommand(s *State, name, path string, args []string, interrupt os.Signal, gracePeriod time.Duration) (WaitFunc, error) {
434+
func startCommand(s *State, name, path string, args []string, cancel func(*exec.Cmd) error, waitDelay time.Duration) (WaitFunc, error) {
436435
var stdoutBuf, stderrBuf strings.Builder
437-
cmd := exec.Command(path, args...)
436+
cmd := exec.CommandContext(s.Context(), path, args...)
437+
if cancel == nil {
438+
cmd.Cancel = nil
439+
} else {
440+
cmd.Cancel = func() error { return cancel(cmd) }
441+
}
442+
cmd.WaitDelay = waitDelay
438443
cmd.Args[0] = name
439444
cmd.Dir = s.Getwd()
440445
cmd.Env = s.env
@@ -444,16 +449,9 @@ func startCommand(s *State, name, path string, args []string, interrupt os.Signa
444449
return nil, err
445450
}
446451

447-
var waitErr error
448-
done := make(chan struct{})
449-
go func() {
450-
waitErr = waitOrStop(s.Context(), cmd, interrupt, gracePeriod)
451-
close(done)
452-
}()
453-
454452
wait := func(s *State) (stdout, stderr string, err error) {
455-
<-done
456-
return stdoutBuf.String(), stderrBuf.String(), waitErr
453+
err = cmd.Wait()
454+
return stdoutBuf.String(), stderrBuf.String(), err
457455
}
458456
return wait, nil
459457
}
@@ -535,67 +533,6 @@ func pathEnvName() string {
535533
}
536534
}
537535

538-
// waitOrStop waits for the already-started command cmd by calling its Wait method.
539-
//
540-
// If cmd does not return before ctx is done, waitOrStop sends it the given interrupt signal.
541-
// If killDelay is positive, waitOrStop waits that additional period for Wait to return before sending os.Kill.
542-
//
543-
// This function is copied from the one added to x/playground/internal in
544-
// http://golang.org/cl/228438.
545-
func waitOrStop(ctx context.Context, cmd *exec.Cmd, interrupt os.Signal, killDelay time.Duration) error {
546-
if cmd.Process == nil {
547-
panic("waitOrStop called with a nil cmd.Process — missing Start call?")
548-
}
549-
if interrupt == nil {
550-
panic("waitOrStop requires a non-nil interrupt signal")
551-
}
552-
553-
errc := make(chan error)
554-
go func() {
555-
select {
556-
case errc <- nil:
557-
return
558-
case <-ctx.Done():
559-
}
560-
561-
err := cmd.Process.Signal(interrupt)
562-
if err == nil {
563-
err = ctx.Err() // Report ctx.Err() as the reason we interrupted.
564-
} else if err == os.ErrProcessDone {
565-
errc <- nil
566-
return
567-
}
568-
569-
if killDelay > 0 {
570-
timer := time.NewTimer(killDelay)
571-
select {
572-
// Report ctx.Err() as the reason we interrupted the process...
573-
case errc <- ctx.Err():
574-
timer.Stop()
575-
return
576-
// ...but after killDelay has elapsed, fall back to a stronger signal.
577-
case <-timer.C:
578-
}
579-
580-
// Wait still hasn't returned.
581-
// Kill the process harder to make sure that it exits.
582-
//
583-
// Ignore any error: if cmd.Process has already terminated, we still
584-
// want to send ctx.Err() (or the error from the Interrupt call)
585-
// to properly attribute the signal that may have terminated it.
586-
_ = cmd.Process.Kill()
587-
}
588-
589-
errc <- err
590-
}()
591-
592-
waitErr := cmd.Wait()
593-
if interruptErr := <-errc; interruptErr != nil {
594-
return interruptErr
595-
}
596-
return waitErr
597-
}
598-
599536
// Exists checks that the named file(s) exist.
600537
func Exists() Cmd {
601538
return Command(
@@ -834,7 +771,7 @@ func Mv() Cmd {
834771

835772
// Program returns a new command that runs the named program, found from the
836773
// host process's PATH (not looked up in the script's PATH).
837-
func Program(name string, interrupt os.Signal, gracePeriod time.Duration) Cmd {
774+
func Program(name string, cancel func(*exec.Cmd) error, waitDelay time.Duration) Cmd {
838775
var (
839776
shortName string
840777
summary string
@@ -864,7 +801,7 @@ func Program(name string, interrupt os.Signal, gracePeriod time.Duration) Cmd {
864801
if pathErr != nil {
865802
return nil, pathErr
866803
}
867-
return startCommand(s, shortName, path, args, interrupt, gracePeriod)
804+
return startCommand(s, shortName, path, args, cancel, waitDelay)
868805
})
869806
}
870807

src/cmd/go/internal/vcweb/script.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"log"
1717
"net/http"
1818
"os"
19+
"os/exec"
1920
"path/filepath"
2021
"runtime"
2122
"strconv"
@@ -31,7 +32,7 @@ import (
3132
func newScriptEngine() *script.Engine {
3233
conds := script.DefaultConds()
3334

34-
interrupt := os.Interrupt
35+
interrupt := func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
3536
gracePeriod := 1 * time.Second // arbitrary
3637

3738
cmds := script.DefaultCmds()

src/cmd/go/scriptcmds_test.go

+13-5
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,23 @@ import (
1111
"errors"
1212
"fmt"
1313
"os"
14+
"os/exec"
1415
"strings"
1516
"time"
1617
)
1718

18-
func scriptCommands(interrupt os.Signal, gracePeriod time.Duration) map[string]script.Cmd {
19+
func scriptCommands(interrupt os.Signal, waitDelay time.Duration) map[string]script.Cmd {
1920
cmds := scripttest.DefaultCmds()
2021

2122
// Customize the "exec" interrupt signal and grace period.
22-
cmdExec := script.Exec(quitSignal(), gracePeriod)
23+
var cancel func(cmd *exec.Cmd) error
24+
if interrupt != nil {
25+
cancel = func(cmd *exec.Cmd) error {
26+
return cmd.Process.Signal(interrupt)
27+
}
28+
}
29+
30+
cmdExec := script.Exec(cancel, waitDelay)
2331
cmds["exec"] = cmdExec
2432

2533
add := func(name string, cmd script.Cmd) {
@@ -30,7 +38,7 @@ func scriptCommands(interrupt os.Signal, gracePeriod time.Duration) map[string]s
3038
}
3139

3240
add("cc", scriptCC(cmdExec))
33-
cmdGo := scriptGo(interrupt, gracePeriod)
41+
cmdGo := scriptGo(cancel, waitDelay)
3442
add("go", cmdGo)
3543
add("stale", scriptStale(cmdGo))
3644

@@ -62,8 +70,8 @@ func scriptCC(cmdExec script.Cmd) script.Cmd {
6270
}
6371

6472
// scriptGo runs the go command.
65-
func scriptGo(interrupt os.Signal, gracePeriod time.Duration) script.Cmd {
66-
return script.Program(testGo, interrupt, gracePeriod)
73+
func scriptGo(cancel func(*exec.Cmd) error, waitDelay time.Duration) script.Cmd {
74+
return script.Program(testGo, cancel, waitDelay)
6775
}
6876

6977
// scriptStale checks that the named build targets are stale.

0 commit comments

Comments
 (0)