github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/communicator/remote/command.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package remote
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"sync"
    10  )
    11  
    12  // Cmd represents a remote command being prepared or run.
    13  type Cmd struct {
    14  	// Command is the command to run remotely. This is executed as if
    15  	// it were a shell command, so you are expected to do any shell escaping
    16  	// necessary.
    17  	Command string
    18  
    19  	// Stdin specifies the process's standard input. If Stdin is
    20  	// nil, the process reads from an empty bytes.Buffer.
    21  	Stdin io.Reader
    22  
    23  	// Stdout and Stderr represent the process's standard output and
    24  	// error.
    25  	//
    26  	// If either is nil, it will be set to ioutil.Discard.
    27  	Stdout io.Writer
    28  	Stderr io.Writer
    29  
    30  	// Once Wait returns, his will contain the exit code of the process.
    31  	exitStatus int
    32  
    33  	// Internal fields
    34  	exitCh chan struct{}
    35  
    36  	// err is used to store any error reported by the Communicator during
    37  	// execution.
    38  	err error
    39  
    40  	// This thing is a mutex, lock when making modifications concurrently
    41  	sync.Mutex
    42  }
    43  
    44  // Init must be called by the Communicator before executing the command.
    45  func (c *Cmd) Init() {
    46  	c.Lock()
    47  	defer c.Unlock()
    48  
    49  	c.exitCh = make(chan struct{})
    50  }
    51  
    52  // SetExitStatus stores the exit status of the remote command as well as any
    53  // communicator related error. SetExitStatus then unblocks any pending calls
    54  // to Wait.
    55  // This should only be called by communicators executing the remote.Cmd.
    56  func (c *Cmd) SetExitStatus(status int, err error) {
    57  	c.Lock()
    58  	defer c.Unlock()
    59  
    60  	c.exitStatus = status
    61  	c.err = err
    62  
    63  	close(c.exitCh)
    64  }
    65  
    66  // Wait waits for the remote command to complete.
    67  // Wait may return an error from the communicator, or an ExitError if the
    68  // process exits with a non-zero exit status.
    69  func (c *Cmd) Wait() error {
    70  	<-c.exitCh
    71  
    72  	c.Lock()
    73  	defer c.Unlock()
    74  
    75  	if c.err != nil || c.exitStatus != 0 {
    76  		return &ExitError{
    77  			Command:    c.Command,
    78  			ExitStatus: c.exitStatus,
    79  			Err:        c.err,
    80  		}
    81  	}
    82  
    83  	return nil
    84  }
    85  
    86  // ExitError is returned by Wait to indicate and error executing the remote
    87  // command, or a non-zero exit status.
    88  type ExitError struct {
    89  	Command    string
    90  	ExitStatus int
    91  	Err        error
    92  }
    93  
    94  func (e *ExitError) Error() string {
    95  	if e.Err != nil {
    96  		return fmt.Sprintf("error executing %q: %v", e.Command, e.Err)
    97  	}
    98  	return fmt.Sprintf("%q exit status: %d", e.Command, e.ExitStatus)
    99  }