Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 70 additions & 9 deletions ignite/pkg/cmdrunner/cmdrunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"os"
"os/exec"
"strings"
"syscall"

"github.com/creack/pty"
"golang.org/x/sync/errgroup"

"github.com/ignite/cli/v29/ignite/pkg/cmdrunner/step"
Expand All @@ -24,6 +26,7 @@ type Runner struct {
workdir string
runParallel bool
debug bool
tty bool
}

// Option defines option to run commands.
Expand Down Expand Up @@ -77,6 +80,13 @@ func EnableDebug() Option {
}
}

// TTY simulates a TTY device.
func TTY() Option {
return func(r *Runner) {
r.tty = true
}
}

// New returns a new command runner.
func New(options ...Option) *Runner {
runner := &Runner{
Expand Down Expand Up @@ -133,7 +143,15 @@ func (r *Runner) Run(ctx context.Context, steps ...*step.Step) error {
}
return err
}
command := r.newCommand(step)

command, err := r.newCommand(step)
if err != nil {
if runErr := runPostExecs(err); runErr != nil {
return runErr
}
continue
}

startErr := command.Start()
if startErr != nil {
if err := runPostExecs(startErr); err != nil {
Expand Down Expand Up @@ -205,11 +223,31 @@ func (e *cmdSignalWithWriter) Write(data []byte) (n int, err error) {
return e.w.Write(data)
}

type ptyExecutor struct {
*exec.Cmd
ptmx *os.File
tty *os.File
}

func (e *ptyExecutor) Signal(s os.Signal) {
_ = e.Cmd.Process.Signal(s)
}

func (e *ptyExecutor) Write(data []byte) (n int, err error) {
return e.ptmx.Write(data)
}

func (e *ptyExecutor) Wait() error {
defer e.ptmx.Close()
defer e.tty.Close()
return e.Cmd.Wait()
}

// newCommand returns a new command to execute.
func (r *Runner) newCommand(step *step.Step) Executor {
func (r *Runner) newCommand(step *step.Step) (Executor, error) {
// Return a dummy executor in case of an empty command
if step.Exec.Command == "" {
return &dummyExecutor{}
return &dummyExecutor{}, nil
}
var (
stdout = step.Stdout
Expand Down Expand Up @@ -240,22 +278,45 @@ func (r *Runner) newCommand(step *step.Step) Executor {
command.Env = append(os.Environ(), step.Env...)
command.Env = append(command.Env, Env("PATH", goenv.Path()))

// If a custom stdin is provided it will be as the stdin for the command
// If TTY is requested, create a pseudo-terminal
if r.tty {
ptmx, tty, err := pty.Open()
if err != nil {
return nil, err
}

// Set up the command to use the PTY
command.Stdout = ptmx
command.Stderr = ptmx
command.Stdin = ptmx
command.SysProcAttr = &syscall.SysProcAttr{
Setctty: true,
Setsid: true,
}

// Return a special executor that handles the PTY
return &ptyExecutor{
Cmd: command,
ptmx: ptmx,
tty: tty,
}, nil
}

// If custom stdin is provided, it will be as the stdin for the command
if stdin != nil {
command.Stdin = stdin
return &cmdSignal{command}
return &cmdSignal{command}, nil
}

// If no custom stdin, the executor can write into the stdin of the program
writer, err := command.StdinPipe()
if err != nil {
// TODO do not panic
panic(err)
return nil, err
}
return &cmdSignalWithWriter{command, writer}
return &cmdSignalWithWriter{command, writer}, nil
}

// Env returns a new env var value from key and val.
// Env returns a new env var value from a key and val.
func Env(key, val string) string {
return fmt.Sprintf("%s=%s", key, val)
}
29 changes: 25 additions & 4 deletions integration/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type execOptions struct {
ctx context.Context
shouldErr, shouldRetry bool
stdout, stderr io.Writer
stdin io.Reader
tty bool
}

type ExecOption func(*execOptions)
Expand Down Expand Up @@ -51,13 +53,27 @@ func ExecStderr(w io.Writer) ExecOption {
}
}

// ExecStdin captures stdin of an execution.
func ExecStdin(r io.Reader) ExecOption {
return func(o *execOptions) {
o.stdin = r
}
}

// ExecRetry retries command until it is successful before context is canceled.
func ExecRetry() ExecOption {
return func(o *execOptions) {
o.shouldRetry = true
}
}

// TTY simulates a TTY device.
func TTY() ExecOption {
return func(o *execOptions) {
o.tty = true
}
}

// Exec executes a command step with options where msg describes the expectation from the test.
// unless calling with Must(), Exec() will not exit test runtime on failure.
func (e Env) Exec(msg string, steps step.Steps, options ...ExecOption) (ok bool) {
Expand All @@ -77,6 +93,13 @@ func (e Env) Exec(msg string, steps step.Steps, options ...ExecOption) (ok bool)
cmdrunner.DefaultStdout(io.MultiWriter(stdout, opts.stdout)),
cmdrunner.DefaultStderr(io.MultiWriter(stderr, opts.stderr)),
}
if opts.stdin != nil {
copts = append(copts, cmdrunner.DefaultStdin(opts.stdin))
}
if opts.tty {
copts = append(copts, cmdrunner.TTY())
}

if HasTestVerboseFlag() {
fmt.Printf("Executing %d step(s) for %q\n", len(steps), msg)
copts = append(copts, cmdrunner.EnableDebug())
Expand All @@ -89,21 +112,19 @@ func (e Env) Exec(msg string, steps step.Steps, options ...ExecOption) (ok bool)
Run(opts.ctx, steps...)
if errors.Is(err, context.Canceled) {
err = nil
}
if err != nil {
} else if err != nil {
fmt.Fprintln(os.Stderr, err)
if opts.shouldRetry && opts.ctx.Err() == nil {
time.Sleep(time.Second)
return e.Exec(msg, steps, options...)
}
}

if err != nil {
msg = fmt.Sprintf("%s\n\nLogs:\n\n%s\n\nError Logs:\n\n%s\n",
msg,
stdout.String(),
stderr.String())
}

if opts.shouldErr {
return assert.Error(e.t, err, msg)
}
Expand Down
Loading