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