buildlet: add context argument to Client.Exec

(And update all the callers in the tree.)

Updates golang/go#35707

Change-Id: I504ef73ea4ba7f8028f47a658c1cd519c7b5d986
Reviewed-on: https://go-review.googlesource.com/c/build/+/207908
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/buildlet/buildletclient.go b/buildlet/buildletclient.go
index 3c51e08..dc2cf1a 100644
--- a/buildlet/buildletclient.go
+++ b/buildlet/buildletclient.go
@@ -507,9 +507,6 @@
 	// response from the buildlet, but before the output begins
 	// writing to Output.
 	OnStartExec func()
-
-	// Timeout is an optional duration before ErrTimeout is returned.
-	Timeout time.Duration
 }
 
 var ErrTimeout = errors.New("buildlet: timeout waiting for command to complete")
@@ -521,7 +518,10 @@
 // were system errors preventing the command from being started or
 // seen to completition. If execErr is non-nil, the remoteErr is
 // meaningless.
-func (c *Client) Exec(cmd string, opts ExecOpts) (remoteErr, execErr error) {
+//
+// If the context's deadline is exceeded, the returned execErr is
+// ErrTimeout.
+func (c *Client) Exec(ctx context.Context, cmd string, opts ExecOpts) (remoteErr, execErr error) {
 	var mode string
 	if opts.SystemLevel {
 		mode = "sys"
@@ -545,6 +545,7 @@
 	if err != nil {
 		return nil, err
 	}
+	req = req.WithContext(ctx)
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 
 	// The first thing the buildlet's exec handler does is flush the headers, so
@@ -596,17 +597,16 @@
 			resc <- errs{} // success
 		}
 	}()
-	var timer <-chan time.Time
-	if opts.Timeout > 0 {
-		t := time.NewTimer(opts.Timeout)
-		defer t.Stop()
-		timer = t.C
-	}
 	select {
-	case <-timer:
-		c.MarkBroken()
-		return nil, ErrTimeout
 	case res := <-resc:
+		if res.execErr != nil {
+			c.MarkBroken()
+			if res.execErr == context.DeadlineExceeded {
+				// Historical pre-context value.
+				// TODO: update docs & callers to just use the context value.
+				res.execErr = ErrTimeout
+			}
+		}
 		return res.remoteErr, res.execErr
 	case <-c.peerDead:
 		return nil, c.deadErr
diff --git a/cmd/coordinator/coordinator.go b/cmd/coordinator/coordinator.go
index 13598de..1f9400a 100644
--- a/cmd/coordinator/coordinator.go
+++ b/cmd/coordinator/coordinator.go
@@ -2061,7 +2061,7 @@
 	st.getHelpersReadySoon()
 
 	if !st.useSnapshot() {
-		remoteErr, err = st.goBuilder().RunMake(st.bc, st)
+		remoteErr, err = st.goBuilder().RunMake(st.ctx, st.bc, st)
 		if err != nil {
 			return nil, err
 		}
@@ -2128,7 +2128,7 @@
 
 	goos, goarch := st.conf.GOOS(), st.conf.GOARCH()
 
-	remoteErr, err := kubeBC.Exec("/bin/bash", buildlet.ExecOpts{
+	remoteErr, err := kubeBC.Exec(st.ctx, "/bin/bash", buildlet.ExecOpts{
 		SystemLevel: true,
 		Args: []string{
 			"-c",
@@ -2175,7 +2175,7 @@
 func (st *buildStatus) runAllLegacy() (remoteErr, err error) {
 	allScript := st.conf.AllScript()
 	sp := st.CreateSpan("legacy_all_path", allScript)
-	remoteErr, err = st.bc.Exec(path.Join("go", allScript), buildlet.ExecOpts{
+	remoteErr, err = st.bc.Exec(st.ctx, path.Join("go", allScript), buildlet.ExecOpts{
 		Output:   st,
 		ExtraEnv: st.conf.Env(),
 		Debug:    true,
@@ -2310,7 +2310,7 @@
 		args = append(args, "--compile-only")
 	}
 	var buf bytes.Buffer
-	remoteErr, err = st.bc.Exec("go/bin/go", buildlet.ExecOpts{
+	remoteErr, err = st.bc.Exec(st.ctx, "go/bin/go", buildlet.ExecOpts{
 		Output:      &buf,
 		ExtraEnv:    append(st.conf.Env(), "GOROOT="+goroot),
 		OnStartExec: func() { st.LogEventTime("discovering_tests") },
@@ -2533,7 +2533,7 @@
 		repoPath := importPathOfRepo(st.SubName)
 		var buf bytes.Buffer
 		sp := st.CreateSpan("listing_subrepo_packages", st.SubName)
-		rErr, err := st.bc.Exec("go/bin/go", buildlet.ExecOpts{
+		rErr, err := st.bc.Exec(st.ctx, "go/bin/go", buildlet.ExecOpts{
 			Output:   &buf,
 			Dir:      "gopath/src/" + repoPath,
 			ExtraEnv: append(st.conf.Env(), "GOROOT="+goroot, "GOPATH="+gopath),
@@ -2580,7 +2580,7 @@
 
 	var remoteErrors []error
 	for _, tr := range testRuns {
-		rErr, err := st.bc.Exec("go/bin/go", buildlet.ExecOpts{
+		rErr, err := st.bc.Exec(st.ctx, "go/bin/go", buildlet.ExecOpts{
 			Debug:    true, // make buildlet print extra debug in output for failures
 			Output:   st,
 			Dir:      tr.Dir,
@@ -2610,7 +2610,7 @@
 // It uses module-specific environment variables from st.conf.ModulesEnv.
 func (st *buildStatus) goMod(importPath, goroot, gopath string) (string, error) {
 	var buf bytes.Buffer
-	rErr, err := st.bc.Exec("go/bin/go", buildlet.ExecOpts{
+	rErr, err := st.bc.Exec(st.ctx, "go/bin/go", buildlet.ExecOpts{
 		Output:   &buf,
 		Dir:      "gopath/src/" + importPath,
 		ExtraEnv: append(append(st.conf.Env(), "GOROOT="+goroot, "GOPATH="+gopath), st.conf.ModulesEnv(st.SubName)...),
@@ -2653,7 +2653,7 @@
 	findDeps := func(repo string) (rErr, err error) {
 		repoPath := importPathOfRepo(repo)
 		var buf bytes.Buffer
-		rErr, err = st.bc.Exec("go/bin/go", buildlet.ExecOpts{
+		rErr, err = st.bc.Exec(st.ctx, "go/bin/go", buildlet.ExecOpts{
 			Output:   &buf,
 			Dir:      "gopath/src/" + repoPath,
 			ExtraEnv: append(st.conf.Env(), "GOROOT="+goroot, "GOPATH="+gopath),
@@ -3138,12 +3138,16 @@
 	var buf bytes.Buffer
 	t0 := time.Now()
 	timeout := st.conf.DistTestsExecTimeout(names)
+
+	ctx, cancel := context.WithTimeout(st.ctx, timeout)
+	defer cancel()
+
 	var remoteErr, err error
 	if ti := tis[0]; ti.bench != nil {
 		pbr, perr := st.parentRev()
 		// TODO(quentin): Error if parent commit could not be determined?
 		if perr == nil {
-			remoteErr, err = ti.bench.Run(buildEnv, st, st.conf, bc, &buf, []buildgo.BuilderRev{st.BuilderRev, pbr})
+			remoteErr, err = ti.bench.Run(st.ctx, buildEnv, st, st.conf, bc, &buf, []buildgo.BuilderRev{st.BuilderRev, pbr})
 		}
 	} else {
 		env := append(st.conf.Env(),
@@ -3153,7 +3157,7 @@
 		)
 		env = append(env, st.conf.ModulesEnv("go")...)
 
-		remoteErr, err = bc.Exec("go/bin/go", buildlet.ExecOpts{
+		remoteErr, err = bc.Exec(ctx, "go/bin/go", buildlet.ExecOpts{
 			// We set Dir to "." instead of the default ("go/bin") so when the dist tests
 			// try to run os/exec.Command("go", "test", ...), the LookPath of "go" doesn't
 			// return "./go.exe" (which exists in the current directory: "go/bin") and then
@@ -3163,7 +3167,6 @@
 			Dir:      ".",
 			Output:   &buf, // see "maybe stream lines" TODO below
 			ExtraEnv: env,
-			Timeout:  timeout,
 			Path:     []string{"$WORKDIR/go/bin", "$PATH"},
 			Args:     args,
 		})
diff --git a/cmd/debugnewvm/debugnewvm.go b/cmd/debugnewvm/debugnewvm.go
index 864c28e..c82ba8b 100644
--- a/cmd/debugnewvm/debugnewvm.go
+++ b/cmd/debugnewvm/debugnewvm.go
@@ -128,12 +128,12 @@
 	log.Printf("WorkDir: %v, %v", dir, err)
 
 	if *sleepSec > 0 {
-		bc.Exec("sysctl", buildlet.ExecOpts{
+		bc.Exec(ctx, "sysctl", buildlet.ExecOpts{
 			Output:      os.Stdout,
 			SystemLevel: true,
 			Args:        []string{"kern.timecounter.hardware"},
 		})
-		bc.Exec("bash", buildlet.ExecOpts{
+		bc.Exec(ctx, "bash", buildlet.ExecOpts{
 			Output:      os.Stdout,
 			SystemLevel: true,
 			Args:        []string{"-c", "rdate -p -v time.nist.gov; sleep " + fmt.Sprint(*sleepSec) + "; rdate -p -v time.nist.gov"},
@@ -172,7 +172,7 @@
 		}
 		t0 := time.Now()
 		log.Printf("Running %s ...", script)
-		remoteErr, err := bc.Exec(path.Join("go", script), buildlet.ExecOpts{
+		remoteErr, err := bc.Exec(ctx, path.Join("go", script), buildlet.ExecOpts{
 			Output:   os.Stdout,
 			ExtraEnv: bconf.Env(),
 			Debug:    true,
diff --git a/cmd/gomote/run.go b/cmd/gomote/run.go
index f89b255..d05409b 100644
--- a/cmd/gomote/run.go
+++ b/cmd/gomote/run.go
@@ -5,6 +5,7 @@
 package main
 
 import (
+	"context"
 	"flag"
 	"fmt"
 	"os"
@@ -71,7 +72,7 @@
 	}
 	env = append(env, "GO_DISABLE_OUTBOUND_NETWORK="+fmt.Sprint(firewall))
 
-	remoteErr, execErr := bc.Exec(cmd, buildlet.ExecOpts{
+	remoteErr, execErr := bc.Exec(context.Background(), cmd, buildlet.ExecOpts{
 		Dir:         dir,
 		SystemLevel: sys || strings.HasPrefix(cmd, "/"),
 		Output:      os.Stdout,
diff --git a/cmd/perfrun/perfrun.go b/cmd/perfrun/perfrun.go
index 4c74615..6b86865 100644
--- a/cmd/perfrun/perfrun.go
+++ b/cmd/perfrun/perfrun.go
@@ -9,6 +9,7 @@
 
 import (
 	"bytes"
+	"context"
 	"encoding/json"
 	"flag"
 	"fmt"
@@ -58,7 +59,7 @@
 		// Build binaries
 		log.Printf("Building bench binary for rev %s", rev)
 		var buf bytes.Buffer
-		remoteErr, err := bc.Exec(path.Join(dir, "bin", "go"), buildlet.ExecOpts{
+		remoteErr, err := bc.Exec(context.Background(), path.Join(dir, "bin", "go"), buildlet.ExecOpts{
 			Output:   &buf,
 			ExtraEnv: []string{"GOROOT=" + path.Join(workDir, dir)},
 			Args:     []string{"test", "-c"},
@@ -81,7 +82,7 @@
 		log.Printf("Starting bench run %d", i)
 		for _, rev := range commits {
 			var buf bytes.Buffer
-			remoteErr, err := bc.Exec(path.Join("go-"+rev, "test/bench/go1/go1.test"), buildlet.ExecOpts{
+			remoteErr, err := bc.Exec(context.Background(), path.Join("go-"+rev, "test/bench/go1/go1.test"), buildlet.ExecOpts{
 				Output: &buf,
 				Args:   []string{"-test.bench", ".", "-test.benchmem"},
 			})
diff --git a/cmd/release/release.go b/cmd/release/release.go
index d674bbd..fe71eab 100644
--- a/cmd/release/release.go
+++ b/cmd/release/release.go
@@ -366,7 +366,7 @@
 	if *watch && *target != "" {
 		execOut = io.MultiWriter(out, os.Stdout)
 	}
-	remoteErr, err := client.Exec(filepath.Join(goDir, bc.MakeScript()), buildlet.ExecOpts{
+	remoteErr, err := client.Exec(context.Background(), filepath.Join(goDir, bc.MakeScript()), buildlet.ExecOpts{
 		Output:   execOut,
 		ExtraEnv: env,
 		Args:     bc.MakeScriptArgs(),
@@ -396,7 +396,7 @@
 		if len(args) > 0 && args[0] == "run" && hostArch != "" {
 			cmdEnv = setGOARCH(cmdEnv, hostArch)
 		}
-		remoteErr, err := client.Exec(goCmd, buildlet.ExecOpts{
+		remoteErr, err := client.Exec(context.Background(), goCmd, buildlet.ExecOpts{
 			Output:   execOut,
 			Dir:      ".", // root of buildlet work directory
 			Args:     args,
@@ -595,7 +595,7 @@
 		if *watch && *target != "" {
 			execOut = io.MultiWriter(out, os.Stdout)
 		}
-		remoteErr, err := client.Exec(filepath.Join(goDir, bc.AllScript()), buildlet.ExecOpts{
+		remoteErr, err := client.Exec(context.Background(), filepath.Join(goDir, bc.AllScript()), buildlet.ExecOpts{
 			Output:   execOut,
 			ExtraEnv: env,
 			Args:     bc.AllScriptArgs(),
@@ -814,7 +814,7 @@
 	}
 	var out bytes.Buffer
 	file := fmt.Sprintf("go/pkg/linux_%s/runtime/cgo.a", b.Arch)
-	remoteErr, err := client.Exec("readelf", buildlet.ExecOpts{
+	remoteErr, err := client.Exec(context.Background(), "readelf", buildlet.ExecOpts{
 		Output:      &out,
 		Args:        []string{"-r", "--wide", file},
 		SystemLevel: true, // look for readelf in system's PATH
diff --git a/internal/buildgo/benchmarks.go b/internal/buildgo/benchmarks.go
index 3c78197..c540519 100644
--- a/internal/buildgo/benchmarks.go
+++ b/internal/buildgo/benchmarks.go
@@ -58,7 +58,7 @@
 	if found {
 		return nil, nil
 	}
-	return bc.Exec(path.Join(goroot, "bin", "go"), buildlet.ExecOpts{
+	return bc.Exec(context.TODO(), path.Join(goroot, "bin", "go"), buildlet.ExecOpts{
 		Output:   w,
 		ExtraEnv: []string{"GOROOT=" + conf.FilePathJoin(workDir, goroot)},
 		Args:     []string{"test", "-c"},
@@ -72,7 +72,7 @@
 	if err != nil {
 		return nil, err
 	}
-	return bc.Exec(path.Join(goroot, "bin", "go"), buildlet.ExecOpts{
+	return bc.Exec(context.TODO(), path.Join(goroot, "bin", "go"), buildlet.ExecOpts{
 		Output:   w,
 		ExtraEnv: []string{"GOROOT=" + conf.FilePathJoin(workDir, goroot)},
 		Args:     []string{"test", "-c", "-o", conf.FilePathJoin(workDir, goroot, name), pkg},
@@ -90,7 +90,7 @@
 			return nil, err
 		}
 	}
-	return bc.Exec(path.Join(goroot, "bin/go"), buildlet.ExecOpts{
+	return bc.Exec(context.TODO(), path.Join(goroot, "bin/go"), buildlet.ExecOpts{
 		Output: w,
 		ExtraEnv: []string{
 			"GOROOT=" + conf.FilePathJoin(workDir, goroot),
@@ -134,7 +134,7 @@
 	// Enumerate x/benchmarks
 	if benchmarksRev != "" {
 		var buf bytes.Buffer
-		remoteErr, err := bc.Exec(path.Join(gb.Goroot, "bin/go"), buildlet.ExecOpts{
+		remoteErr, err := bc.Exec(context.TODO(), path.Join(gb.Goroot, "bin/go"), buildlet.ExecOpts{
 			Output: &buf,
 			ExtraEnv: []string{
 				"GOROOT=" + gb.Conf.FilePathJoin(workDir, gb.Goroot),
@@ -161,7 +161,7 @@
 	if len(pkgs) > 0 {
 		// Find packages that actually have benchmarks or tests.
 		var buf bytes.Buffer
-		remoteErr, err := bc.Exec(path.Join(gb.Goroot, "bin/go"), buildlet.ExecOpts{
+		remoteErr, err := bc.Exec(context.TODO(), path.Join(gb.Goroot, "bin/go"), buildlet.ExecOpts{
 			Output: &buf,
 			ExtraEnv: []string{
 				"GOROOT=" + gb.Conf.FilePathJoin(workDir, gb.Goroot),
@@ -205,7 +205,7 @@
 		return nil, fmt.Errorf("runOneBenchBinary, WorkDir: %v", err)
 	}
 	// Some benchmarks need GOROOT so they can invoke cmd/go.
-	return bc.Exec(binaryPath, buildlet.ExecOpts{
+	return bc.Exec(context.TODO(), binaryPath, buildlet.ExecOpts{
 		Output: w,
 		Dir:    dir,
 		Args:   args,
@@ -226,7 +226,7 @@
 	})
 }
 
-func buildRev(buildEnv *buildenv.Environment, sl spanlog.Logger, conf *dashboard.BuildConfig, bc *buildlet.Client, w io.Writer, goroot string, br BuilderRev) error {
+func buildRev(ctx context.Context, buildEnv *buildenv.Environment, sl spanlog.Logger, conf *dashboard.BuildConfig, bc *buildlet.Client, w io.Writer, goroot string, br BuilderRev) error {
 	if br.SnapshotExists(context.TODO(), buildEnv) {
 		return bc.PutTarFromURL(br.SnapshotURL(buildEnv), goroot)
 	}
@@ -246,7 +246,7 @@
 		Conf:       conf,
 		Goroot:     goroot,
 	}
-	remoteErr, err := builder.RunMake(bc, w)
+	remoteErr, err := builder.RunMake(ctx, bc, w)
 	if err != nil {
 		return err
 	}
@@ -257,12 +257,12 @@
 // Build output is sent to w. Benchmark output is stored in b.output.
 // revs must contain exactly two revs. The first rev is assumed to be present in "go", and the second will be placed into "go-parent".
 // TODO(quentin): Support len(revs) != 2.
-func (b *BenchmarkItem) Run(buildEnv *buildenv.Environment, sl spanlog.Logger, conf *dashboard.BuildConfig, bc *buildlet.Client, w io.Writer, revs []BuilderRev) (remoteErr, err error) {
+func (b *BenchmarkItem) Run(ctx context.Context, buildEnv *buildenv.Environment, sl spanlog.Logger, conf *dashboard.BuildConfig, bc *buildlet.Client, w io.Writer, revs []BuilderRev) (remoteErr, err error) {
 	// Ensure we have a built parent repo.
 	if err := bc.ListDir("go-parent", buildlet.ListDirOpts{}, func(buildlet.DirEntry) {}); err != nil {
 		pbr := revs[1]
 		sp := sl.CreateSpan("bench_build_parent", bc.Name())
-		err = buildRev(buildEnv, sl, conf, bc, w, "go-parent", pbr)
+		err = buildRev(ctx, buildEnv, sl, conf, bc, w, "go-parent", pbr)
 		sp.Done(err)
 		if err != nil {
 			return nil, err
diff --git a/internal/buildgo/buildgo.go b/internal/buildgo/buildgo.go
index 723474b..780f6a7 100644
--- a/internal/buildgo/buildgo.go
+++ b/internal/buildgo/buildgo.go
@@ -103,10 +103,10 @@
 // goroot is relative to the workdir with forward slashes.
 // w is the Writer to send build output to.
 // remoteErr and err are as described at the top of this file.
-func (gb GoBuilder) RunMake(bc *buildlet.Client, w io.Writer) (remoteErr, err error) {
+func (gb GoBuilder) RunMake(ctx context.Context, bc *buildlet.Client, w io.Writer) (remoteErr, err error) {
 	// Build the source code.
 	makeSpan := gb.CreateSpan("make", gb.Conf.MakeScript())
-	remoteErr, err = bc.Exec(path.Join(gb.Goroot, gb.Conf.MakeScript()), buildlet.ExecOpts{
+	remoteErr, err = bc.Exec(ctx, path.Join(gb.Goroot, gb.Conf.MakeScript()), buildlet.ExecOpts{
 		Output:   w,
 		ExtraEnv: append(gb.Conf.Env(), "GOBIN="),
 		Debug:    true,
@@ -125,7 +125,7 @@
 	// Need to run "go install -race std" before the snapshot + tests.
 	if pkgs := gb.Conf.GoInstallRacePackages(); len(pkgs) > 0 {
 		sp := gb.CreateSpan("install_race_std")
-		remoteErr, err = bc.Exec(path.Join(gb.Goroot, "bin/go"), buildlet.ExecOpts{
+		remoteErr, err = bc.Exec(ctx, path.Join(gb.Goroot, "bin/go"), buildlet.ExecOpts{
 			Output:   w,
 			ExtraEnv: append(gb.Conf.Env(), "GOBIN="),
 			Debug:    true,
@@ -143,7 +143,7 @@
 	}
 
 	if gb.Name == "linux-amd64-racecompile" {
-		return gb.runConcurrentGoBuildStdCmd(bc, w)
+		return gb.runConcurrentGoBuildStdCmd(ctx, bc, w)
 	}
 
 	return nil, nil
@@ -155,9 +155,9 @@
 // with -gcflags=-c=8 using a race-enabled cmd/compile (built by
 // caller, runMake, per builder config).
 // The idea is that this might find data races in cmd/compile.
-func (gb GoBuilder) runConcurrentGoBuildStdCmd(bc *buildlet.Client, w io.Writer) (remoteErr, err error) {
+func (gb GoBuilder) runConcurrentGoBuildStdCmd(ctx context.Context, bc *buildlet.Client, w io.Writer) (remoteErr, err error) {
 	span := gb.CreateSpan("go_build_c128_std_cmd")
-	remoteErr, err = bc.Exec(path.Join(gb.Goroot, "bin/go"), buildlet.ExecOpts{
+	remoteErr, err = bc.Exec(ctx, path.Join(gb.Goroot, "bin/go"), buildlet.ExecOpts{
 		Output:   w,
 		ExtraEnv: append(gb.Conf.Env(), "GOBIN="),
 		Debug:    true,