dashboard/builder: modernize ancient exec wrappers

They were from a time before we had the os/exec package, if
memory serves.

Also, make verbose also mean that the main build's stdout
and stderr go to the real stdout and stderr as well.
I'll want that for the Docker-based builder.

LGTM=adg
R=adg
CC=golang-codereviews
https://golang.org/cl/135000043
This commit is contained in:
Brad Fitzpatrick 2014-08-28 14:58:15 -07:00
parent 776a9335ce
commit cd91e8d096
5 changed files with 101 additions and 106 deletions

View File

@ -87,23 +87,24 @@ func (b *Builder) buildBenchmark(workpath string, update bool) (benchBin, log st
"GOPATH=" + gopath}, "GOPATH=" + gopath},
b.envv()...) b.envv()...)
// First, download without installing. // First, download without installing.
cmd := []string{gobin, "get", "-d"} args := []string{"get", "-d"}
if update { if update {
cmd = append(cmd, "-u") args = append(args, "-u")
} }
cmd = append(cmd, *benchPath) args = append(args, *benchPath)
var buildlog bytes.Buffer var buildlog bytes.Buffer
ok, err := runOutput(*buildTimeout, env, &buildlog, workpath, cmd...) runOpts := []runOpt{runTimeout(*buildTimeout), runEnv(env), allOutput(&buildlog), runDir(workpath)}
if !ok || err != nil { err = run(exec.Command(gobin, args...), runOpts...)
if err != nil {
fmt.Fprintf(&buildlog, "go get -d %s failed: %s", *benchPath, err) fmt.Fprintf(&buildlog, "go get -d %s failed: %s", *benchPath, err)
return "", buildlog.String(), err return "", buildlog.String(), err
} }
// Then, build into workpath. // Then, build into workpath.
benchBin = filepath.Join(workpath, "benchbin") + exeExt benchBin = filepath.Join(workpath, "benchbin") + exeExt
cmd = []string{gobin, "build", "-o", benchBin, *benchPath} args = []string{"build", "-o", benchBin, *benchPath}
buildlog.Reset() buildlog.Reset()
ok, err = runOutput(*buildTimeout, env, &buildlog, workpath, cmd...) err = run(exec.Command(gobin, args...), runOpts...)
if !ok || err != nil { if err != nil {
fmt.Fprintf(&buildlog, "go build %s failed: %s", *benchPath, err) fmt.Fprintf(&buildlog, "go build %s failed: %s", *benchPath, err)
return "", buildlog.String(), err return "", buildlog.String(), err
} }
@ -172,23 +173,23 @@ func (b *Builder) executeBenchmark(workpath, hash, benchBin, bench string, procs
"GOGCTRACE=1", // before Go1.2 "GOGCTRACE=1", // before Go1.2
fmt.Sprintf("GOMAXPROCS=%v", procs)}, fmt.Sprintf("GOMAXPROCS=%v", procs)},
b.envv()...) b.envv()...)
cmd := []string{benchBin, args := []string{
"-bench", bench, "-bench", bench,
"-benchmem", strconv.Itoa(*benchMem), "-benchmem", strconv.Itoa(*benchMem),
"-benchtime", benchTime.String(), "-benchtime", benchTime.String(),
"-benchnum", strconv.Itoa(*benchNum), "-benchnum", strconv.Itoa(*benchNum),
"-tmpdir", workpath} "-tmpdir", workpath}
if affinity != 0 { if affinity != 0 {
cmd = append(cmd, "-affinity", strconv.Itoa(affinity)) args = append(args, "-affinity", strconv.Itoa(affinity))
} }
benchlog := new(bytes.Buffer) benchlog := new(bytes.Buffer)
ok, err := runOutput(*buildTimeout, env, benchlog, workpath, cmd...) err := run(exec.Command(benchBin, args...), runEnv(env), allOutput(benchlog), runDir(workpath))
if strip := benchlog.Len() - 512<<10; strip > 0 { if strip := benchlog.Len() - 512<<10; strip > 0 {
// Leave the last 512K, that part contains metrics. // Leave the last 512K, that part contains metrics.
benchlog = bytes.NewBuffer(benchlog.Bytes()[strip:]) benchlog = bytes.NewBuffer(benchlog.Bytes()[strip:])
} }
artifacts = []PerfArtifact{{Type: "log", Body: benchlog.String()}} artifacts = []PerfArtifact{{Type: "log", Body: benchlog.String()}}
if !ok || err != nil { if err != nil {
if err != nil { if err != nil {
log.Printf("Failed to execute benchmark '%v': %v", bench, err) log.Printf("Failed to execute benchmark '%v': %v", bench, err)
ok = false ok = false

View File

@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime" "runtime"
@ -151,16 +152,10 @@ func (env *gccgoEnv) setup(repo *Repo, workpath, hash string, envv []string) (st
} }
// get the modified files for this commit. // get the modified files for this commit.
statusCmd := []string{
"hg",
"status",
"--no-status",
"--change",
hash,
}
var buf bytes.Buffer var buf bytes.Buffer
if _, err := runOutput(*cmdTimeout, envv, &buf, repo.Path, statusCmd...); err != nil { if err := run(exec.Command("hg", "status", "--no-status", "--change", hash),
allOutput(&buf), runDir(repo.Path), runEnv(envv)); err != nil {
return "", fmt.Errorf("Failed to find the modified files for %s: %s", hash, err) return "", fmt.Errorf("Failed to find the modified files for %s: %s", hash, err)
} }
modifiedFiles := strings.Split(buf.String(), "\n") modifiedFiles := strings.Split(buf.String(), "\n")
@ -204,7 +199,7 @@ func (env *gccgoEnv) setup(repo *Repo, workpath, hash string, envv []string) (st
} }
buf.Reset() buf.Reset()
if _, err := runOutput(*cmdTimeout, envv, &buf, gccpath, logCmd...); err != nil { if err := run(exec.Command(gccpath, logCmd...), runEnv(envv), allOutput(&buf), runDir(gccpath)); err != nil {
return "", fmt.Errorf("%s: %s", errMsg, err) return "", fmt.Errorf("%s: %s", errMsg, err)
} }
gccRev := buf.String() gccRev := buf.String()
@ -214,13 +209,7 @@ func (env *gccgoEnv) setup(repo *Repo, workpath, hash string, envv []string) (st
// checkout gccRev // checkout gccRev
// TODO(cmang): Fix this to work in parallel mode. // TODO(cmang): Fix this to work in parallel mode.
checkoutCmd := []string{ if err := run(exec.Command("git", "reset", "--hard", strings.TrimSpace(gccRev)), runEnv(envv), runDir(gccpath)); err != nil {
"git",
"reset",
"--hard",
strings.TrimSpace(gccRev),
}
if _, err := runOutput(*cmdTimeout, envv, ioutil.Discard, gccpath, checkoutCmd...); err != nil {
return "", fmt.Errorf("Failed to checkout commit at revision %s: %s", gccRev, err) return "", fmt.Errorf("Failed to checkout commit at revision %s: %s", gccRev, err)
} }
@ -231,18 +220,16 @@ func (env *gccgoEnv) setup(repo *Repo, workpath, hash string, envv []string) (st
} }
// configure GCC with substituted gofrontend and libgo // configure GCC with substituted gofrontend and libgo
gccConfigCmd := []string{ if err := run(exec.Command(filepath.Join(gccpath, "configure"),
filepath.Join(gccpath, "configure"),
"--enable-languages=c,c++,go", "--enable-languages=c,c++,go",
"--disable-bootstrap", "--disable-bootstrap",
"--disable-multilib", "--disable-multilib",
} ), runEnv(envv), runDir(gccobjdir)); err != nil {
if _, err := runOutput(*cmdTimeout, envv, ioutil.Discard, gccobjdir, gccConfigCmd...); err != nil { return "", fmt.Errorf("Failed to configure GCC: %v", err)
return "", fmt.Errorf("Failed to configure GCC: %s", err)
} }
// build gcc // build gcc
if _, err := runOutput(*buildTimeout, envv, ioutil.Discard, gccobjdir, "make"); err != nil { if err := run(exec.Command("make"), runTimeout(*buildTimeout), runEnv(envv), runDir(gccobjdir)); err != nil {
return "", fmt.Errorf("Failed to build GCC: %s", err) return "", fmt.Errorf("Failed to build GCC: %s", err)
} }

View File

@ -5,70 +5,62 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"log" "log"
"os"
"os/exec" "os/exec"
"time" "time"
) )
// run is a simple wrapper for exec.Run/Close // run runs a command with optional arguments.
func run(d time.Duration, envv []string, dir string, argv ...string) error { func run(cmd *exec.Cmd, opts ...runOpt) error {
if *verbose { a := runArgs{cmd, *cmdTimeout}
log.Println("run", argv) for _, opt := range opts {
opt.modArgs(&a)
}
if *verbose {
log.Printf("running %v", a.cmd.Args)
} }
cmd := exec.Command(argv[0], argv[1:]...)
cmd.Dir = dir
cmd.Env = envv
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return err return err
} }
err := timeout(d, cmd.Wait) err := timeout(a.timeout, cmd.Wait)
if _, ok := err.(timeoutError); ok { if _, ok := err.(timeoutError); ok {
cmd.Process.Kill() cmd.Process.Kill()
} }
return err return err
} }
// runLog runs a process and returns the combined stdout/stderr. It returns // Zero or more runOpts can be passed to run to modify the command
// process combined stdout and stderr output, exit status and error. The // before it's run.
// error returned is nil, if process is started successfully, even if exit type runOpt interface {
// status is not successful. modArgs(*runArgs)
func runLog(timeout time.Duration, envv []string, dir string, argv ...string) (string, bool, error) {
var b bytes.Buffer
ok, err := runOutput(timeout, envv, &b, dir, argv...)
return b.String(), ok, err
} }
// runOutput runs a process and directs any output to the supplied writer. // allOutput sends both stdout and stderr to w.
// It returns exit status and error. The error returned is nil, if process func allOutput(w io.Writer) optFunc {
// is started successfully, even if exit status is not successful. return func(a *runArgs) {
func runOutput(d time.Duration, envv []string, out io.Writer, dir string, argv ...string) (bool, error) { a.cmd.Stdout = w
if *verbose { a.cmd.Stderr = w
log.Println("runOutput", argv)
} }
}
cmd := exec.Command(argv[0], argv[1:]...) func runTimeout(timeout time.Duration) optFunc {
cmd.Dir = dir return func(a *runArgs) {
cmd.Env = envv a.timeout = timeout
cmd.Stdout = out
cmd.Stderr = out
startErr := cmd.Start()
if startErr != nil {
return false, startErr
} }
}
if err := timeout(d, cmd.Wait); err != nil { func runDir(dir string) optFunc {
if _, ok := err.(timeoutError); ok { return func(a *runArgs) {
cmd.Process.Kill() a.cmd.Dir = dir
} }
return false, err }
func runEnv(env []string) optFunc {
return func(a *runArgs) {
a.cmd.Env = env
} }
return true, nil
} }
// timeout runs f and returns its error value, or if the function does not // timeout runs f and returns its error value, or if the function does not
@ -93,3 +85,14 @@ type timeoutError time.Duration
func (e timeoutError) Error() string { func (e timeoutError) Error() string {
return fmt.Sprintf("timed out after %v", time.Duration(e)) return fmt.Sprintf("timed out after %v", time.Duration(e))
} }
// optFunc implements runOpt with a function, like http.HandlerFunc.
type optFunc func(*runArgs)
func (f optFunc) modArgs(a *runArgs) { f(a) }
// internal detail to exec.go:
type runArgs struct {
cmd *exec.Cmd
timeout time.Duration
}

View File

@ -12,6 +12,7 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime" "runtime"
@ -409,15 +410,14 @@ func (b *Builder) buildRepoOnHash(workpath, hash, cmd string) (buildLog string,
} }
// build // build
var buildlog bytes.Buffer var buildbuf bytes.Buffer
logfile := filepath.Join(workpath, "build.log") logfile := filepath.Join(workpath, "build.log")
f, err := os.Create(logfile) f, err := os.Create(logfile)
if err != nil { if err != nil {
buildLog = err.Error() return err.Error(), 0, err
return
} }
defer f.Close() defer f.Close()
w := io.MultiWriter(f, &buildlog) w := io.MultiWriter(f, &buildbuf)
// go's build command is a script relative to the srcDir, whereas // go's build command is a script relative to the srcDir, whereas
// gccgo's build command is usually "make check-go" in the srcDir. // gccgo's build command is usually "make check-go" in the srcDir.
@ -427,23 +427,28 @@ func (b *Builder) buildRepoOnHash(workpath, hash, cmd string) (buildLog string,
} }
} }
// make sure commands with extra arguments are handled properly // naive splitting of command from its arguments:
splitCmd := strings.Split(cmd, " ") args := strings.Split(cmd, " ")
c := exec.Command(args[0], args[1:]...)
c.Dir = srcDir
c.Env = b.envv()
if *verbose {
c.Stdout = io.MultiWriter(os.Stdout, w)
c.Stderr = io.MultiWriter(os.Stderr, w)
} else {
c.Stdout = w
c.Stderr = w
}
startTime := time.Now() startTime := time.Now()
ok, err := runOutput(*buildTimeout, b.envv(), w, srcDir, splitCmd...) err = run(c, runTimeout(*buildTimeout))
runTime = time.Now().Sub(startTime) runTime = time.Since(startTime)
if !ok && err == nil { if err != nil {
err = fmt.Errorf("build failed") fmt.Fprintf(w, "Build complete, duration %v. Result: error: %v\n", runTime, err)
} else {
fmt.Fprintf(w, "Build complete, duration %v. Result: success\n", runTime)
} }
errf := func() string { return buildbuf.String(), runTime, err
if err != nil {
return fmt.Sprintf("error: %v", err)
}
return "success"
}
fmt.Fprintf(w, "Build complete, duration %v. Result: %v\n", runTime, errf())
buildLog = buildlog.String()
return
} }
// failBuild checks for a new commit for this builder // failBuild checks for a new commit for this builder
@ -516,13 +521,12 @@ func (b *Builder) buildSubrepo(goRoot, goPath, pkg, hash string) (string, error)
} }
// fetch package and dependencies // fetch package and dependencies
log, ok, err := runLog(*cmdTimeout, env, goPath, goTool, "get", "-d", pkg+"/...") var outbuf bytes.Buffer
if err == nil && !ok { err := run(exec.Command(goTool, "get", "-d", pkg+"/..."), runEnv(env), allOutput(&outbuf), runDir(goPath))
err = fmt.Errorf("go exited with status 1")
}
if err != nil { if err != nil {
return log, err return outbuf.String(), err
} }
outbuf.Reset()
// hg update to the specified hash // hg update to the specified hash
pkgmaster, err := vcs.RepoRootForImportPath(pkg, *verbose) pkgmaster, err := vcs.RepoRootForImportPath(pkg, *verbose)
@ -538,11 +542,9 @@ func (b *Builder) buildSubrepo(goRoot, goPath, pkg, hash string) (string, error)
} }
// test the package // test the package
log, ok, err = runLog(*buildTimeout, env, goPath, goTool, "test", "-short", pkg+"/...") err = run(exec.Command(goTool, "test", "-short", pkg+"/..."),
if err == nil && !ok { runTimeout(*buildTimeout), runEnv(env), allOutput(&outbuf), runDir(goPath))
err = fmt.Errorf("go exited with status 1") return outbuf.String(), err
}
return log, err
} }
// repoForTool returns the correct RepoRoot for the buildTool, or an error if // repoForTool returns the correct RepoRoot for the buildTool, or an error if

View File

@ -9,6 +9,7 @@ import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
@ -73,9 +74,10 @@ func (r *Repo) Export(path, rev string) error {
return err return err
} }
args := []string{r.Master.VCS.Cmd, "archive", "-t", "files", "-r", rev, path} cmd := exec.Command(r.Master.VCS.Cmd, "archive", "-t", "files", "-r", rev, path)
if err := run(*cmdTimeout, nil, downloadPath, args...); err != nil { cmd.Dir = downloadPath
return fmt.Errorf("executing %s: %v", strings.Join(args, " "), err) if err := run(cmd); err != nil {
return fmt.Errorf("executing %v: %v", cmd.Args, err)
} }
return nil return nil
} }