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