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  }