blob: eb9bbf0e6671e4bfda02743b236920199d3bebe0 [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package runner
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"os/signal"
"path/filepath"
"sort"
"strings"
"sync"
"syscall"
"time"
"github.com/shirou/gopsutil/process"
"golang.org/x/sys/unix"
"chromiumos/tast/errors"
"chromiumos/tast/internal/bundle"
"chromiumos/tast/internal/command"
"chromiumos/tast/internal/devserver"
"chromiumos/tast/internal/testcontext"
"chromiumos/tast/internal/testing"
)
// getBundlesAndTests returns matched tests and paths to the bundles containing them.
func getBundlesAndTests(args *Args) (bundles []string, tests []*testing.EntityWithRunnabilityInfo, err *command.StatusError) {
var glob string
switch args.Mode {
case RunTestsMode:
glob = args.RunTests.BundleGlob
case ListTestsMode:
glob = args.ListTests.BundleGlob
default:
return nil, nil, command.NewStatusErrorf(statusBadArgs, "bundles unneeded for mode %v", args.Mode)
}
if bundles, err = getBundles(glob); err != nil {
return nil, nil, err
}
tests, bundles, err = getTests(args, bundles)
return bundles, tests, err
}
// getBundles returns the full paths of all test bundles matched by glob.
func getBundles(glob string) ([]string, *command.StatusError) {
ps, err := filepath.Glob(glob)
if err != nil {
return nil, command.NewStatusErrorf(statusNoBundles, "failed to get bundle(s) %q: %v", glob, err)
}
bundles := make([]string, 0)
for _, p := range ps {
fi, err := os.Stat(p)
// Only match executable regular files.
if err == nil && fi.Mode().IsRegular() && (fi.Mode().Perm()&0111) != 0 {
bundles = append(bundles, p)
}
}
if len(bundles) == 0 {
return nil, command.NewStatusErrorf(statusNoBundles, "no bundles matched by %q", glob)
}
sort.Strings(bundles)
return bundles, nil
}
type testsOrError struct {
bundle string
tests []*testing.EntityWithRunnabilityInfo
err *command.StatusError
}
// getTests returns tests in bundles matched by args.Patterns. It does this by executing
// each bundle to ask it to marshal and print its tests. A slice of paths to bundles
// with matched tests is also returned.
func getTests(args *Args, bundles []string) (tests []*testing.EntityWithRunnabilityInfo,
bundlesWithTests []string, statusErr *command.StatusError) {
bundleArgs, err := args.bundleArgs(bundle.ListTestsMode)
if err != nil {
return nil, nil, command.NewStatusErrorf(statusBadArgs, "%v", err)
}
// Run all bundles in parallel.
ch := make(chan testsOrError, len(bundles))
for _, b := range bundles {
bundle := b
go func() {
out := bytes.Buffer{}
if err := runBundle(bundle, bundleArgs, &out); err != nil {
ch <- testsOrError{bundle, nil, err}
return
}
ts := make([]*testing.EntityWithRunnabilityInfo, 0)
if err := json.Unmarshal(out.Bytes(), &ts); err != nil {
ch <- testsOrError{bundle, nil,
command.NewStatusErrorf(statusBundleFailed, "bundle %v gave bad output: %v", bundle, err)}
return
}
ch <- testsOrError{bundle, ts, nil}
}()
}
// Read results into a map from bundle to that bundle's tests.
bundleTests := make(map[string][]*testing.EntityWithRunnabilityInfo)
for i := 0; i < len(bundles); i++ {
toe := <-ch
if toe.err != nil {
return nil, nil, toe.err
}
if len(toe.tests) > 0 {
bundleTests[toe.bundle] = toe.tests
}
}
// Sort both the tests and the bundles by bundle path.
for b := range bundleTests {
bundlesWithTests = append(bundlesWithTests, b)
}
sort.Strings(bundlesWithTests)
for _, b := range bundlesWithTests {
tests = append(tests, bundleTests[b]...)
}
return tests, bundlesWithTests, nil
}
// startBundleCmd creates and returns a new command running the test bundle at path using args.
// cmd's Start method has already been called, and the caller is responsible for calling Wait.
// A new session is created for the bundle process.
func startBundleCmd(path string, bundleArgs *bundle.Args, stdout, stderr io.Writer) (*exec.Cmd, error) {
var stdin bytes.Buffer
if err := json.NewEncoder(&stdin).Encode(bundleArgs); err != nil {
return nil, err
}
cmd := exec.Command(path)
cmd.Stdin = &stdin
cmd.Stdout = stdout
cmd.Stderr = stderr
// Run the bundle in a new session so we can identify test processes later.
// We can't just use a process group here, as the testexec package places each command
// run by a test into its own process group.
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
cmd.SysProcAttr.Setsid = true
if err := cmd.Start(); err != nil {
return nil, err
}
return cmd, nil
}
// runBundle runs the bundle at path to completion, passing bundleArgs.
// The bundle's stdout is copied to the stdout arg.
func runBundle(path string, bundleArgs *bundle.Args, stdout io.Writer) *command.StatusError {
// Watch for stdout being closed so we can abort the bundle and clean up: https://crbug.com/945626
// Otherwise, the runner, bundle, and processes started by tests may run indefinitely.
// When stdout is closed, it's important that we clean up before writing anything to it, as Go will
// terminate the process if SIGPIPE is generated by a write to a closed stdout/stderr.
// See https://golang.org/pkg/os/signal/#hdr-SIGPIPE for more details.
stdoutWatcher, err := newPipeWatcher(int(os.Stdout.Fd()))
if err != nil {
return command.NewStatusErrorf(statusError, "failed watching stdout: %v", err)
}
defer stdoutWatcher.close()
// Also catch SIGINT so we can clean up if the runner was executed manually and
// later interrupted with Ctrl-C, and SIGTERM in case we're killed by another runner process.
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(sigCh)
stderr := bytes.Buffer{}
cmd, err := startBundleCmd(path, bundleArgs, stdout, &stderr)
if err != nil {
return command.NewStatusErrorf(statusBundleFailed, "%v", err)
}
// When we return, kill the bundle process and any other processes in its session.
// Per setsid(2), "[the calling process's] session ID is made the same as its process ID".
defer killSession(cmd.Process.Pid, syscall.SIGKILL)
waitCh := make(chan error, 1)
go func() {
waitCh <- cmd.Wait()
}()
select {
case err := <-waitCh:
// The bundle process exited on its own.
if err == nil {
return nil
}
var detail string
if msg := strings.TrimSpace(stderr.String()); len(msg) > 0 {
detail = fmt.Sprintf(" (%v)", msg)
}
return command.NewStatusErrorf(statusBundleFailed, "%v%s", err, detail)
case <-stdoutWatcher.readClosed:
// The read end of stdout was closed (i.e. the shell or SSH connection used to run us died).
return command.NewStatusErrorf(statusInterrupted, "stdout closed")
case sig := <-sigCh:
// We caught SIGINT (i.e. we were run manually and the user hit Ctrl-C) or
// SIGTERM (we were likely killed by another test runner process).
status := statusError
switch sig {
case syscall.SIGINT:
status = statusInterrupted
case syscall.SIGTERM:
status = statusTerminated
}
return command.NewStatusErrorf(status, "caught signal %d (%s)", sig, sig)
}
}
// killSession makes a best-effort attempt to kill all processes in session sid.
// It makes several passes over the list of running processes, sending sig to any
// that are part of the session. After it doesn't find any new processes, it returns.
// Note that this is racy: it's possible (but hopefully unlikely) that continually-forking
// processes could spawn children that don't get killed.
func killSession(sid int, sig syscall.Signal) {
const maxPasses = 3
for i := 0; i < maxPasses; i++ {
procs, err := process.Processes()
if err != nil {
return
}
n := 0
for _, proc := range procs {
pid := int(proc.Pid)
if s, err := unix.Getsid(pid); err == nil && s == sid {
syscall.Kill(pid, sig)
n++
}
}
// If we didn't find any processes in the session, we're done.
if n == 0 {
return
}
}
}
// handleDownloadPrivateBundles handles a DownloadPrivateBundlesMode request from args
// and JSON-marshals a DownloadPrivateBundlesResult struct to w.
func handleDownloadPrivateBundles(ctx context.Context, args *Args, cfg *Config, stdout io.Writer) error {
if cfg.PrivateBundlesStampPath == "" {
return errors.New("this test runner is not configured for private bundles")
}
if args.DownloadPrivateBundles.BuildArtifactsURL == "" {
return errors.New("failed to determine the build artifacts URL (non-official image?)")
}
var logs []string
var mu sync.Mutex
ctx = testcontext.WithLogger(ctx, func(msg string) {
mu.Lock()
defer mu.Unlock()
logs = append(logs, fmt.Sprintf("[%s] %s", time.Now().Format("15:04:05.000"), msg))
})
defer func() {
res := &DownloadPrivateBundlesResult{Messages: logs}
json.NewEncoder(stdout).Encode(res)
}()
// If the stamp file exists, private bundles have been already downloaded.
if _, err := os.Stat(cfg.PrivateBundlesStampPath); err == nil {
return nil
}
// Download the archive via devserver.
archiveURL := args.DownloadPrivateBundles.BuildArtifactsURL + "tast_bundles.tar.bz2"
testcontext.Logf(ctx, "Downloading private bundles from %s", archiveURL)
cl, err := devserver.NewClient(ctx, args.DownloadPrivateBundles.Devservers,
args.DownloadPrivateBundles.TLWServer,
args.DownloadPrivateBundles.DUTName)
if err != nil {
return errors.Wrapf(err, "failed to create new client [devservers=%v, TLWServer=%s]",
args.DownloadPrivateBundles.Devservers, args.DownloadPrivateBundles.TLWServer)
}
defer cl.TearDown()
r, err := cl.Open(ctx, archiveURL)
if err != nil {
return err
}
defer r.Close()
tf, err := ioutil.TempFile("", "tast_bundles.")
if err != nil {
return err
}
defer os.Remove(tf.Name())
_, err = io.Copy(tf, r)
if cerr := tf.Close(); err == nil {
err = cerr
}
if err == nil {
// Extract the archive, and touch the stamp file.
cmd := exec.Command("tar", "xf", tf.Name())
cmd.Dir = "/usr/local"
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to extract %s: %v", strings.Join(cmd.Args, " "), err)
}
testcontext.Log(ctx, "Download finished successfully")
} else if os.IsNotExist(err) {
testcontext.Log(ctx, "Private bundles not found")
} else {
return fmt.Errorf("failed to download %s: %v", archiveURL, err)
}
return ioutil.WriteFile(cfg.PrivateBundlesStampPath, nil, 0644)
}