github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/agent/transport.go (about)

     1  package agent
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"os/exec"
     7  	"strings"
     8  	"unicode/utf8"
     9  )
    10  
    11  // Transport is the standard agent transport interface, allowing the agent
    12  // dialing structure to invoke commands on the remote, copy files to the remote,
    13  // and classify errors from the remote. Transport interfaces do not need to be
    14  // safe for concurrent invocation unless being used for concurrent Dial
    15  // operations.
    16  type Transport interface {
    17  	// Copy copies the specified local file (which is guaranteed to exist and be
    18  	// a file) to the remote. The provided local path will be absolute. The
    19  	// remote path will be a filename (i.e. without path separators) that should
    20  	// be treated as being relative to the user's home directory.
    21  	Copy(localPath, remoteName string) error
    22  	// Command creates (but does not start) a process that will invoke the
    23  	// specified command on the specified remote. It should not re-direct any of
    24  	// the output streams of the process. The command on the remote must be
    25  	// invoked with the user's home directory as the working directory. Any
    26  	// command provided to this interface is guaranteed to be lexable by simply
    27  	// splitting on spaces.
    28  	Command(command string) (*exec.Cmd, error)
    29  	// ClassifyError is used to determine how the agent dialing infrastructure
    30  	// should attempt to handle failure when launching agents. It is provided
    31  	// with the process exit state as well as a string containing the standard
    32  	// error output from the command. It should return a bool representing
    33  	// whether or not the error condition represents a failure due to an agent
    34  	// either not being installed or being installed improperly and a bool
    35  	// representing whether or not the remote system should be treated as a
    36  	// cmd.exe-like environment on Windows. If neither of these can be
    37  	// determined reliably, this method should return an error to abort dialing.
    38  	// If the second bool changes the dialer's platform hypothesis, it will
    39  	// attempt to reconnect using the correct command syntax for that platform.
    40  	// Otherwise, if the first bool indicates that the agent binary simply needs
    41  	// to be (re-)installed, it will attempt to do so and then reconnect.
    42  	ClassifyError(processState *os.ProcessState, errorOutput string) (bool, bool, error)
    43  }
    44  
    45  // run is a utility method that invokes a command via a transport, waits for it
    46  // to complete, and returns its exit error. If there is an error creating the
    47  // command, it will be returned wrapped, but otherwise the result of the run
    48  // method will be returned un-wrapped, so it can be treated as an
    49  // os/exec.ExitError.
    50  func run(transport Transport, command string) error {
    51  	// Create the process.
    52  	process, err := transport.Command(command)
    53  	if err != nil {
    54  		return fmt.Errorf("unable to create command: %w", err)
    55  	}
    56  
    57  	// Run the command. We use the Output method as opposed to the Run method
    58  	// because the former will collect standard error output that can be useful
    59  	// in formulating an error message for the purposes of debugging.
    60  	_, err = process.Output()
    61  
    62  	// If there was an error, then attempt to convert it to a more useful error
    63  	// that includes standard error output from the remote.
    64  	if err != nil {
    65  		exitErr, ok := err.(*exec.ExitError)
    66  		if ok && utf8.Valid(exitErr.Stderr) {
    67  			remoteError := strings.TrimSuffix(string(exitErr.Stderr), "\n")
    68  			if len(remoteError) > 0 {
    69  				return fmt.Errorf("remote error: %s", remoteError)
    70  			}
    71  		}
    72  		return err
    73  	}
    74  
    75  	// Success.
    76  	return nil
    77  }
    78  
    79  // output is a utility method that invokes a command via a transport, waits for
    80  // it to complete, and returns its standard output and exit error. If there is
    81  // an error creating the command, it will be returned wrapped, but otherwise the
    82  // result of the run method will be returned un-wrapped, so it can be treated as
    83  // an os/exec.ExitError.
    84  func output(transport Transport, command string) ([]byte, error) {
    85  	// Create the process.
    86  	process, err := transport.Command(command)
    87  	if err != nil {
    88  		return nil, fmt.Errorf("unable to create command: %w", err)
    89  	}
    90  
    91  	// Run the process.
    92  	return process.Output()
    93  }