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