github.com/amanya/packer@v0.12.1-0.20161117214323-902ac5ab2eb6/packer/communicator.go (about) 1 package packer 2 3 import ( 4 "io" 5 "os" 6 "strings" 7 "sync" 8 9 "github.com/mitchellh/iochan" 10 ) 11 12 // CmdDisconnect is a sentry value to indicate a RemoteCmd 13 // exited because the remote side disconnected us. 14 const CmdDisconnect int = 2300218 15 16 // RemoteCmd represents a remote command being prepared or run. 17 type RemoteCmd struct { 18 // Command is the command to run remotely. This is executed as if 19 // it were a shell command, so you are expected to do any shell escaping 20 // necessary. 21 Command string 22 23 // Stdin specifies the process's standard input. If Stdin is 24 // nil, the process reads from an empty bytes.Buffer. 25 Stdin io.Reader 26 27 // Stdout and Stderr represent the process's standard output and 28 // error. 29 // 30 // If either is nil, it will be set to ioutil.Discard. 31 Stdout io.Writer 32 Stderr io.Writer 33 34 // This will be set to true when the remote command has exited. It 35 // shouldn't be set manually by the user, but there is no harm in 36 // doing so. 37 Exited bool 38 39 // Once Exited is true, this will contain the exit code of the process. 40 ExitStatus int 41 42 // Internal fields 43 exitCh chan struct{} 44 45 // This thing is a mutex, lock when making modifications concurrently 46 sync.Mutex 47 } 48 49 // A Communicator is the interface used to communicate with the machine 50 // that exists that will eventually be packaged into an image. Communicators 51 // allow you to execute remote commands, upload files, etc. 52 // 53 // Communicators must be safe for concurrency, meaning multiple calls to 54 // Start or any other method may be called at the same time. 55 type Communicator interface { 56 // Start takes a RemoteCmd and starts it. The RemoteCmd must not be 57 // modified after being used with Start, and it must not be used with 58 // Start again. The Start method returns immediately once the command 59 // is started. It does not wait for the command to complete. The 60 // RemoteCmd.Exited field should be used for this. 61 Start(*RemoteCmd) error 62 63 // Upload uploads a file to the machine to the given path with the 64 // contents coming from the given reader. This method will block until 65 // it completes. 66 Upload(string, io.Reader, *os.FileInfo) error 67 68 // UploadDir uploads the contents of a directory recursively to 69 // the remote path. It also takes an optional slice of paths to 70 // ignore when uploading. 71 // 72 // The folder name of the source folder should be created unless there 73 // is a trailing slash on the source "/". For example: "/tmp/src" as 74 // the source will create a "src" directory in the destination unless 75 // a trailing slash is added. This is identical behavior to rsync(1). 76 UploadDir(dst string, src string, exclude []string) error 77 78 // Download downloads a file from the machine from the given remote path 79 // with the contents writing to the given writer. This method will 80 // block until it completes. 81 Download(string, io.Writer) error 82 83 DownloadDir(src string, dst string, exclude []string) error 84 } 85 86 // StartWithUi runs the remote command and streams the output to any 87 // configured Writers for stdout/stderr, while also writing each line 88 // as it comes to a Ui. 89 func (r *RemoteCmd) StartWithUi(c Communicator, ui Ui) error { 90 stdout_r, stdout_w := io.Pipe() 91 stderr_r, stderr_w := io.Pipe() 92 defer stdout_w.Close() 93 defer stderr_w.Close() 94 95 // Retain the original stdout/stderr that we can replace back in. 96 originalStdout := r.Stdout 97 originalStderr := r.Stderr 98 defer func() { 99 r.Lock() 100 defer r.Unlock() 101 102 r.Stdout = originalStdout 103 r.Stderr = originalStderr 104 }() 105 106 // Set the writers for the output so that we get it streamed to us 107 if r.Stdout == nil { 108 r.Stdout = stdout_w 109 } else { 110 r.Stdout = io.MultiWriter(r.Stdout, stdout_w) 111 } 112 113 if r.Stderr == nil { 114 r.Stderr = stderr_w 115 } else { 116 r.Stderr = io.MultiWriter(r.Stderr, stderr_w) 117 } 118 119 // Start the command 120 if err := c.Start(r); err != nil { 121 return err 122 } 123 124 // Create the channels we'll use for data 125 exitCh := make(chan struct{}) 126 stdoutCh := iochan.DelimReader(stdout_r, '\n') 127 stderrCh := iochan.DelimReader(stderr_r, '\n') 128 129 // Start the goroutine to watch for the exit 130 go func() { 131 defer close(exitCh) 132 defer stdout_w.Close() 133 defer stderr_w.Close() 134 r.Wait() 135 }() 136 137 // Loop and get all our output 138 OutputLoop: 139 for { 140 select { 141 case output := <-stderrCh: 142 if output != "" { 143 ui.Message(r.cleanOutputLine(output)) 144 } 145 case output := <-stdoutCh: 146 if output != "" { 147 ui.Message(r.cleanOutputLine(output)) 148 } 149 case <-exitCh: 150 break OutputLoop 151 } 152 } 153 154 // Make sure we finish off stdout/stderr because we may have gotten 155 // a message from the exit channel before finishing these first. 156 for output := range stdoutCh { 157 ui.Message(strings.TrimSpace(output)) 158 } 159 160 for output := range stderrCh { 161 ui.Message(strings.TrimSpace(output)) 162 } 163 164 return nil 165 } 166 167 // SetExited is a helper for setting that this process is exited. This 168 // should be called by communicators who are running a remote command in 169 // order to set that the command is done. 170 func (r *RemoteCmd) SetExited(status int) { 171 r.Lock() 172 defer r.Unlock() 173 174 if r.exitCh == nil { 175 r.exitCh = make(chan struct{}) 176 } 177 178 r.Exited = true 179 r.ExitStatus = status 180 close(r.exitCh) 181 } 182 183 // Wait waits for the remote command to complete. 184 func (r *RemoteCmd) Wait() { 185 // Make sure our condition variable is initialized. 186 r.Lock() 187 if r.exitCh == nil { 188 r.exitCh = make(chan struct{}) 189 } 190 r.Unlock() 191 192 <-r.exitCh 193 } 194 195 // cleanOutputLine cleans up a line so that '\r' don't muck up the 196 // UI output when we're reading from a remote command. 197 func (r *RemoteCmd) cleanOutputLine(line string) string { 198 // Trim surrounding whitespace 199 line = strings.TrimSpace(line) 200 201 // Trim up to the first carriage return, since that text would be 202 // lost anyways. 203 idx := strings.LastIndex(line, "\r") 204 if idx > -1 { 205 line = line[idx+1:] 206 } 207 208 return line 209 }