blob: 9a485623d18c71887f828cb1cd98f7214b4756f7 [file] [log] [blame]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package genericexec
import (
"context"
"io"
"os"
"os/exec"
"go.chromium.org/tast/core/internal/debugger"
)
// ExecCmd represents a local command to execute.
type ExecCmd struct {
name string
baseArgs []string
debugPort int
}
var _ Cmd = &ExecCmd{}
// CommandExec constructs a new ExecCmd representing a local command to execute.
func CommandExec(name string, baseArgs ...string) *ExecCmd {
return &ExecCmd{
name: name,
baseArgs: baseArgs,
}
}
// DebugCommand returns a version of this command that will run under the debugger.
func (c *ExecCmd) DebugCommand(ctx context.Context, debugPort int) (Cmd, error) {
if debugPort == 0 {
return c, nil
}
if err := debugger.FindPreemptiveDebuggerErrors(debugPort, false); err != nil {
return nil, err
}
debugEnv := debugger.DlvHostEnv
if debugger.IsRunningOnDUT() {
debugEnv = debugger.DlvDUTEnv
}
name, baseArgs := debugger.RewriteDebugCommand(debugPort, debugEnv, c.name, c.baseArgs...)
return &ExecCmd{name: name, baseArgs: baseArgs, debugPort: debugPort}, nil
}
// Run runs a local command synchronously. See Cmd.Run for details.
func (c *ExecCmd) Run(ctx context.Context, extraArgs []string, stdin io.Reader, stdout, stderr io.Writer) error {
cmd := exec.CommandContext(ctx, c.name, append(c.baseArgs, extraArgs...)...)
cmd.Stdin = stdin
cmd.Stdout = stdout
cmd.Stderr = stderr
// Set FD 3 to the real stderr so that the subprocess can write stack
// traces.
// TODO(b/189332919): Remove this hack and write stack traces to stderr
// once we finish migrating to gRPC-based protocol. This hack is needed
// because JSON-based protocol is designed to write messages to stderr
// in case of errors and thus Tast CLI consumes stderr.
cmd.ExtraFiles = []*os.File{os.Stderr}
cmd.Env = append(os.Environ(), "TAST_B189332919_STACK_TRACE_FD=3")
debugger.PrintWaitingMessage(ctx, c.debugPort)
return cmd.Run()
}
// Interact runs a local command asynchronously. See Cmd.Interact for details.
func (c *ExecCmd) Interact(ctx context.Context, extraArgs []string) (p Process, retErr error) {
ctx, cancel := context.WithCancel(ctx)
defer func() {
if retErr != nil {
cancel()
}
}()
cmd := exec.CommandContext(ctx, c.name, append(c.baseArgs, extraArgs...)...)
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, err
}
if err := cmd.Start(); err != nil {
return nil, err
}
debugger.PrintWaitingMessage(ctx, c.debugPort)
return &ExecProcess{
cmd: cmd,
cancel: cancel,
stdin: stdin,
stdout: stdout,
stderr: stderr,
}, nil
}
// ExecProcess represents a locally running process.
type ExecProcess struct {
cmd *exec.Cmd
cancel context.CancelFunc
stdin io.WriteCloser
stdout io.ReadCloser
stderr io.ReadCloser
}
var _ Process = &ExecProcess{}
// Stdin returns stdin of the process.
func (p *ExecProcess) Stdin() io.WriteCloser { return p.stdin }
// Stdout returns stdout of the process.
func (p *ExecProcess) Stdout() io.ReadCloser { return p.stdout }
// Stderr returns stderr of the process.
func (p *ExecProcess) Stderr() io.ReadCloser { return p.stderr }
// Wait waits for the process to exit. See Process.Wait for details.
func (p *ExecProcess) Wait(ctx context.Context) error {
exited := make(chan struct{})
defer close(exited)
// Cancel the context passed to exec.CommandContext to kill the
// process.
go func() {
select {
case <-ctx.Done():
case <-exited:
}
p.cancel()
}()
return p.cmd.Wait()
}
// ProcessState returns the os.ProcessState object for the process..
func (p *ExecProcess) ProcessState() *os.ProcessState {
return p.cmd.ProcessState
}