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  }