github.com/ttysteale/packer@v0.8.2-0.20150708160520-e5f8ea386ed8/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 80 // StartWithUi runs the remote command and streams the output to any 81 // configured Writers for stdout/stderr, while also writing each line 82 // as it comes to a Ui. 83 func (r *RemoteCmd) StartWithUi(c Communicator, ui Ui) error { 84 stdout_r, stdout_w := io.Pipe() 85 stderr_r, stderr_w := io.Pipe() 86 defer stdout_w.Close() 87 defer stderr_w.Close() 88 89 // Retain the original stdout/stderr that we can replace back in. 90 originalStdout := r.Stdout 91 originalStderr := r.Stderr 92 defer func() { 93 r.Lock() 94 defer r.Unlock() 95 96 r.Stdout = originalStdout 97 r.Stderr = originalStderr 98 }() 99 100 // Set the writers for the output so that we get it streamed to us 101 if r.Stdout == nil { 102 r.Stdout = stdout_w 103 } else { 104 r.Stdout = io.MultiWriter(r.Stdout, stdout_w) 105 } 106 107 if r.Stderr == nil { 108 r.Stderr = stderr_w 109 } else { 110 r.Stderr = io.MultiWriter(r.Stderr, stderr_w) 111 } 112 113 // Start the command 114 if err := c.Start(r); err != nil { 115 return err 116 } 117 118 // Create the channels we'll use for data 119 exitCh := make(chan struct{}) 120 stdoutCh := iochan.DelimReader(stdout_r, '\n') 121 stderrCh := iochan.DelimReader(stderr_r, '\n') 122 123 // Start the goroutine to watch for the exit 124 go func() { 125 defer close(exitCh) 126 defer stdout_w.Close() 127 defer stderr_w.Close() 128 r.Wait() 129 }() 130 131 // Loop and get all our output 132 OutputLoop: 133 for { 134 select { 135 case output := <-stderrCh: 136 if output != "" { 137 ui.Message(r.cleanOutputLine(output)) 138 } 139 case output := <-stdoutCh: 140 if output != "" { 141 ui.Message(r.cleanOutputLine(output)) 142 } 143 case <-exitCh: 144 break OutputLoop 145 } 146 } 147 148 // Make sure we finish off stdout/stderr because we may have gotten 149 // a message from the exit channel before finishing these first. 150 for output := range stdoutCh { 151 ui.Message(strings.TrimSpace(output)) 152 } 153 154 for output := range stderrCh { 155 ui.Message(strings.TrimSpace(output)) 156 } 157 158 return nil 159 } 160 161 // SetExited is a helper for setting that this process is exited. This 162 // should be called by communicators who are running a remote command in 163 // order to set that the command is done. 164 func (r *RemoteCmd) SetExited(status int) { 165 r.Lock() 166 defer r.Unlock() 167 168 if r.exitCh == nil { 169 r.exitCh = make(chan struct{}) 170 } 171 172 r.Exited = true 173 r.ExitStatus = status 174 close(r.exitCh) 175 } 176 177 // Wait waits for the remote command to complete. 178 func (r *RemoteCmd) Wait() { 179 // Make sure our condition variable is initialized. 180 r.Lock() 181 if r.exitCh == nil { 182 r.exitCh = make(chan struct{}) 183 } 184 r.Unlock() 185 186 <-r.exitCh 187 } 188 189 // cleanOutputLine cleans up a line so that '\r' don't muck up the 190 // UI output when we're reading from a remote command. 191 func (r *RemoteCmd) cleanOutputLine(line string) string { 192 // Trim surrounding whitespace 193 line = strings.TrimSpace(line) 194 195 // Trim up to the first carriage return, since that text would be 196 // lost anyways. 197 idx := strings.LastIndex(line, "\r") 198 if idx > -1 { 199 line = line[idx+1:] 200 } 201 202 return line 203 }