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