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 }