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  }