| // Copyright 2021 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 testexec is a wrapper of the standard os/exec package optimized for |
| // use cases of Tast. Tast tests should always use this package instead of |
| // os/exec. |
| // |
| // This package is designed to be a drop-in replacement of os/exec. Just |
| // rewriting imports should work. In addition, several methods are available, |
| // such as Kill and DumpLog. |
| // |
| // Features |
| // |
| // Automatic log collection. os/exec sends stdout/stderr to /dev/null unless |
| // explicitly specified to collect them. This default behavior makes it very |
| // difficult to debug external command failures. This wrapper automatically |
| // collects those uncaptured logs and allows to log them later. |
| // |
| // Process group handling. On timeout, os/exec kills the direct child process |
| // only. This can often leave orphaned subprocesses in DUT and interfere with |
| // later tests. To avoid this issue, this wrapper will kill the whole tree |
| // of subprocesses on timeout by setting process group ID appropriately. |
| // |
| // Usage |
| // |
| // cmd := testexec.CommandContext(ctx, "some", "external", "command") |
| // if err := cmd.Run(testexec.DumpLogOnError); err != nil { |
| // return err |
| // } |
| package testexec |
| |
| import ( |
| "bytes" |
| "context" |
| "os/exec" |
| "strings" |
| "sync" |
| "sync/atomic" |
| "syscall" |
| "unsafe" |
| |
| "chromiumos/tast/errors" |
| tastexec "chromiumos/tast/exec" |
| "chromiumos/tast/shutil" |
| "chromiumos/tast/testing" |
| ) |
| |
| // Cmd represents an external command being prepared or run. |
| // |
| // This struct embeds Cmd in os/exec. |
| // |
| // Callers may wish to use shutil.EscapeSlice when logging Args. |
| type Cmd struct { |
| // Cmd is the underlying exec.Cmd object. |
| *exec.Cmd |
| // log is the buffer uncaptured stdout/stderr is sent to by default. |
| log bytes.Buffer |
| // ctx is the context given to Command that specifies the timeout of the external command. |
| ctx context.Context |
| // timedOut indicates if the process hit timeout. This is set in Wait(). |
| timedOut bool |
| // watchdogStop is notified in Wait to ask the watchdog goroutine to stop. |
| watchdogStop chan bool |
| |
| // doneFlg is set to 1, when the process is terminated but before collecting |
| // the process. This can be accessed from various goroutines concurrently, |
| // so it should be read/written through done() and setDone(). |
| doneFlg uint32 |
| |
| // sigMu is the mutex lock to guard from sending signals to processes |
| // which is already collected. |
| sigMu sync.RWMutex |
| } |
| |
| // RunOption is enum of options which can be passed to Run, Output, |
| // CombinedOutput and Wait to control precise behavior of them. |
| type RunOption = tastexec.RunOption |
| |
| // DumpLogOnError is an option to dump logs if the executed command fails |
| // (i.e., exited with non-zero status code). |
| const DumpLogOnError = tastexec.DumpLogOnError |
| |
| var ( |
| errStdoutSet = errors.New("Stdout was already set") |
| errStderrSet = errors.New("Stderr was already set") |
| errAlreadyStarted = errors.New("Start was already called") |
| errNotStarted = errors.New("Start was not yet called") |
| errAlreadyWaited = errors.New("Wait was already called") |
| errNotWaited = errors.New("Wait was not yet called") |
| ) |
| |
| // CommandContext prepares to run an external command. |
| // |
| // Timeout set in ctx is honored on running the command. |
| // |
| // See os/exec package for details. |
| func CommandContext(ctx context.Context, name string, arg ...string) *Cmd { |
| cmd := exec.Command(name, arg...) |
| |
| // Enable Setpgid so we can terminate the whole subprocesses. |
| cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} |
| |
| return &Cmd{ |
| Cmd: cmd, |
| ctx: ctx, |
| watchdogStop: make(chan bool, 1), |
| } |
| } |
| |
| // Run runs an external command and waits for its completion. |
| // |
| // See os/exec package for details. |
| func (c *Cmd) Run(opts ...RunOption) error { |
| if err := c.Start(); err != nil { |
| return err |
| } |
| |
| err := c.Wait(opts...) |
| return err |
| } |
| |
| // Output runs an external command, waits for its completion and returns |
| // stdout output of the command. |
| // |
| // See os/exec package for details. |
| func (c *Cmd) Output(opts ...RunOption) ([]byte, error) { |
| if c.Stdout != nil { |
| return nil, errStdoutSet |
| } |
| |
| var buf bytes.Buffer |
| c.Stdout = &buf |
| |
| if err := c.Start(); err != nil { |
| return nil, err |
| } |
| |
| err := c.Wait(opts...) |
| return buf.Bytes(), err |
| } |
| |
| // CombinedOutput runs an external command, waits for its completion and |
| // returns stdout/stderr output of the command. |
| // |
| // See os/exec package for details. |
| func (c *Cmd) CombinedOutput(opts ...RunOption) ([]byte, error) { |
| if c.Stdout != nil { |
| return nil, errStdoutSet |
| } |
| if c.Stderr != nil { |
| return nil, errStderrSet |
| } |
| |
| var buf bytes.Buffer |
| c.Stdout = &buf |
| c.Stderr = &buf |
| |
| if err := c.Start(); err != nil { |
| return nil, err |
| } |
| |
| err := c.Wait(opts...) |
| return buf.Bytes(), err |
| } |
| |
| // SeparatedOutput runs an external command, waits for its completion and |
| // returns stdout/stderr output of the command separately. |
| func (c *Cmd) SeparatedOutput(opts ...RunOption) (stdout, stderr []byte, err error) { |
| if c.Stdout != nil { |
| return nil, nil, errStdoutSet |
| } |
| if c.Stderr != nil { |
| return nil, nil, errStderrSet |
| } |
| |
| var outbuf, errbuf bytes.Buffer |
| c.Stdout = &outbuf |
| c.Stderr = &errbuf |
| |
| if err := c.Start(); err != nil { |
| return nil, nil, err |
| } |
| |
| err = c.Wait(opts...) |
| if err != nil { |
| err = errors.Wrapf(err, "command %q returned non-zero error code", strings.Join(c.Args, " ")) |
| } |
| |
| return outbuf.Bytes(), errbuf.Bytes(), err |
| } |
| |
| // Start starts an external command. |
| // |
| // See os/exec package for details. |
| func (c *Cmd) Start() error { |
| if c.Process != nil { |
| return errAlreadyStarted |
| } |
| |
| // Return early if deadline is already expired. |
| select { |
| case <-c.ctx.Done(): |
| return c.ctx.Err() |
| default: |
| } |
| |
| // Collect stdout/stderr to log by default. |
| if c.Stdout == nil { |
| c.Stdout = &c.log |
| } |
| if c.Stderr == nil { |
| c.Stderr = &c.log |
| } |
| |
| if err := c.Cmd.Start(); err != nil { |
| return err |
| } |
| |
| // Watchdog goroutine to terminate the process on timeout. |
| go func() { |
| select { |
| case <-c.ctx.Done(): |
| c.Kill() |
| case <-c.watchdogStop: |
| } |
| }() |
| |
| return nil |
| } |
| |
| // Wait waits for the process to finish and releases all associated resources. |
| // |
| // See os/exec package for details. |
| func (c *Cmd) Wait(opts ...RunOption) error { |
| if c.Process == nil { |
| return errNotStarted |
| } |
| if c.ProcessState != nil { |
| return errAlreadyWaited |
| } |
| |
| // Wait for the process to be terminated, without collecting the |
| // process itself. |
| if err := c.blockUntilWaitable(); err != nil { |
| return err |
| } |
| |
| // Instead of simple mutex and bool, here atomic variable and |
| // R/W Lock is used for better performance and consistency with |
| // standard library implementation. |
| |
| // Marking done atomically. At anytime after this point, sending |
| // a signal will be guarded. |
| c.setDone() |
| // Stop the timeout watchdog here, because the process is terminated. |
| c.watchdogStop <- true |
| |
| // Take and release the sigMu in order to wait for the completion |
| // of signal sending which is already in process. |
| c.sigMu.Lock() |
| c.sigMu.Unlock() |
| |
| // Actual wait to collect the subprocess. |
| werr := c.Cmd.Wait() |
| cerr := c.ctx.Err() |
| |
| if (werr != nil || cerr != nil) && hasOpt(DumpLogOnError, opts) { |
| // Ignore the DumpLog intentionally, because the primary error |
| // here is either werr or cerr. Note that, practically, the |
| // error from DumpLog is returned when ProcessState is nil, |
| // so it shouldn't happen here, because it should be assigned |
| // in Wait() above. |
| c.DumpLog(c.ctx) |
| } |
| |
| if cerr != nil { |
| c.timedOut = true |
| return cerr |
| } |
| return werr |
| } |
| |
| // blockUntilWaitable waits for the process to be ready to be collected. |
| func (c *Cmd) blockUntilWaitable() error { |
| for { |
| // Use the same strategy with os/wait_waitid.go. |
| var siginfo [16]uint64 |
| const P_PID = 1 // Taken from syscall. NOLINT |
| _, _, err := syscall.Syscall6(syscall.SYS_WAITID, P_PID, uintptr(c.Process.Pid), uintptr(unsafe.Pointer(&siginfo)), syscall.WEXITED|syscall.WNOWAIT, 0, 0) |
| if err == 0 { |
| return nil |
| } |
| if err != syscall.EINTR { |
| return err |
| } |
| } |
| } |
| |
| // setDone marks this command is already terminated. |
| // This and done below can be called from various goroutines concurrently. |
| func (c *Cmd) setDone() { |
| atomic.StoreUint32(&c.doneFlg, 1) |
| } |
| |
| // done returns true if this command is marked as terminated already. |
| // It is done in Wait(). This can be called from various goroutines |
| // concurrently. |
| func (c *Cmd) done() bool { |
| return atomic.LoadUint32(&c.doneFlg) > 0 |
| } |
| |
| // Signal sends the input signal to the process tree. |
| // |
| // This is a new method that does not exist in os/exec. |
| // |
| // Even after successful completion of this function, you still need to call |
| // Wait to release all associated resources. |
| func (c *Cmd) Signal(signal syscall.Signal) error { |
| if c.Process == nil { |
| return errNotStarted |
| } |
| |
| // ProcessState may be set in another go-routine calling Wait(), |
| // so there's a room of timing issue. |
| // However, because we do not check the contents, also, "done()" |
| // is checked below with sync mechanism, there should be not |
| // a problem. |
| if c.ProcessState != nil { |
| return errAlreadyWaited |
| } |
| |
| // Guard by a lock so that the signal won't be sent to the process |
| // which is already terminated. |
| c.sigMu.RLock() |
| defer c.sigMu.RUnlock() |
| if c.done() { |
| return errAlreadyWaited |
| } |
| |
| // Negative PID means the process group led by the process. |
| return syscall.Kill(-c.Process.Pid, signal) |
| } |
| |
| // Kill sends SIGKILL to the process tree. |
| // |
| // This is a new method that does not exist in os/exec. |
| // |
| // Even after successful completion of this function, you still need to call |
| // Wait to release all associated resources. |
| func (c *Cmd) Kill() error { |
| return c.Signal(syscall.SIGKILL) |
| } |
| |
| // Cred is a helper function that sets SysProcAttr.Credential to control |
| // the credentials (e.g. UID, GID, etc.) used to run the command. |
| func (c *Cmd) Cred(cred syscall.Credential) { |
| if c.SysProcAttr == nil { |
| c.SysProcAttr = &syscall.SysProcAttr{} |
| } |
| c.SysProcAttr.Credential = &cred |
| } |
| |
| // DumpLog logs details of the executed external command, including uncaptured output. |
| // |
| // This is a new method that does not exist in os/exec. |
| // |
| // Call this function when the test is failing due to unexpected external command result. |
| // You should not call this function for every external command invocation to avoid |
| // spamming logs. |
| // |
| // This function must be called after Wait. |
| func (c *Cmd) DumpLog(ctx context.Context) error { |
| if c.ProcessState == nil { |
| return errNotWaited |
| } |
| |
| if c.ProcessState.Success() { |
| testing.ContextLog(ctx, "External command succeeded") |
| } else if c.timedOut { |
| testing.ContextLog(ctx, "External command timed out") |
| } else { |
| testing.ContextLog(ctx, "External command failed: ", c.ProcessState) |
| } |
| testing.ContextLog(ctx, "Command: ", shutil.EscapeSlice(c.Args)) |
| testing.ContextLog(ctx, "Uncaptured output:\n", c.log.String()) // NOLINT |
| return nil |
| } |
| |
| // GetWaitStatus extracts WaitStatus from error. |
| // WaitStatus is typically returned from Run, Output, CombinedOutput and Wait to |
| // indicate a child process's exit status. |
| // If err is nil, it returns WaitStatus representing successful exit. |
| func GetWaitStatus(err error) (status syscall.WaitStatus, ok bool) { |
| if err == nil { |
| return 0, true |
| } |
| errExit, ok := err.(*exec.ExitError) |
| if !ok { |
| return 0, false |
| } |
| status, ok = errExit.Sys().(syscall.WaitStatus) |
| return status, ok |
| } |
| |
| // ExitCode extracts exit code from error returned by exec.Command.Run(). |
| // Returns exit code and true if exit code is extracted. (0, false) otherwise. |
| // Note that "true" does not mean that the process itself exited correctly, only |
| // that the exit code was extracted successfully. |
| func ExitCode(cmdErr error) (int, bool) { |
| s, ok := GetWaitStatus(cmdErr) |
| if !ok { |
| return 0, false |
| } |
| if s.Exited() { |
| return s.ExitStatus(), true |
| } |
| if s.Signaled() { |
| return int(s.Signal()) + 128, true |
| } |
| return 0, false |
| } |
| |
| // hasOpt returns whether the given opts contain the opt. |
| func hasOpt(opt RunOption, opts []RunOption) bool { |
| for _, o := range opts { |
| if o == opt { |
| return true |
| } |
| } |
| return false |
| } |