builder/docker: Communicator.Start doesn't block

This commit is contained in:
Mitchell Hashimoto 2013-11-09 10:13:27 -08:00
parent f2475baa7a
commit 5f752269df
1 changed files with 115 additions and 89 deletions

View File

@ -11,6 +11,7 @@ import (
"os/exec"
"path/filepath"
"strconv"
"sync"
"syscall"
"time"
)
@ -19,6 +20,8 @@ type Communicator struct {
ContainerId string
HostDir string
ContainerDir string
lock sync.Mutex
}
func (c *Communicator) Start(remote *packer.RemoteCmd) error {
@ -38,100 +41,14 @@ func (c *Communicator) Start(remote *packer.RemoteCmd) error {
exitCodePath := outputFile.Name() + "-exit"
defer os.Remove(exitCodePath)
// Modify the remote command so that all the output of the commands
// go to a single file and so that the exit code is redirected to
// a single file. This lets us determine both when the command
// is truly complete (because the file will have data), what the
// exit status is (because Docker loses it because of the pty, not
// Docker's fault), and get the output (Docker bug).
remoteCmd := fmt.Sprintf("(%s) >%s 2>&1; echo $? >%s",
remote.Command,
filepath.Join(c.ContainerDir, filepath.Base(outputFile.Name())),
filepath.Join(c.ContainerDir, filepath.Base(exitCodePath)))
cmd := exec.Command("docker", "attach", c.ContainerId)
stdin_w, err := cmd.StdinPipe()
if err != nil {
return err
}
log.Printf("Executing in container %s: %#v", c.ContainerId, remoteCmd)
if err := cmd.Start(); err != nil {
return err
}
go func() {
defer stdin_w.Close()
// This sleep needs to be here because of the issue linked to below.
// Basically, without it, Docker will hang on reading stdin forever,
// and won't see what we write, for some reason.
//
// https://github.com/dotcloud/docker/issues/2628
time.Sleep(2 * time.Second)
stdin_w.Write([]byte(remoteCmd + "\n"))
}()
err = cmd.Wait()
if exitErr, ok := err.(*exec.ExitError); ok {
exitStatus := 1
// There is no process-independent way to get the REAL
// exit status so we just try to go deeper.
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
exitStatus = status.ExitStatus()
}
// Say that we ended, since if Docker itself failed, then
// the command must've not run, or so we assume
remote.SetExited(exitStatus)
return nil
}
// Wait for the exit code to appear in our file...
log.Println("Waiting for exit code to appear for remote command...")
for {
fi, err := os.Stat(exitCodePath)
if err == nil && fi.Size() > 0 {
break
}
time.Sleep(1 * time.Second)
}
// Read the exit code
exitRaw, err := ioutil.ReadFile(exitCodePath)
if err != nil {
return err
}
exitStatus, err := strconv.ParseInt(string(bytes.TrimSpace(exitRaw)), 10, 0)
if err != nil {
return err
}
log.Printf("Executed command exit status: %d", exitStatus)
// Read the output
f, err := os.Open(outputFile.Name())
if err != nil {
return err
}
defer f.Close()
if remote.Stdout != nil {
io.Copy(remote.Stdout, f)
} else {
output, err := ioutil.ReadAll(f)
if err != nil {
return err
}
log.Printf("Command output: %s", string(output))
}
// Finally, we're done
remote.SetExited(int(exitStatus))
// Run the actual command in a goroutine so that Start doesn't block
go c.run(cmd, remote, stdin_w, outputFile, exitCodePath)
return nil
}
@ -151,7 +68,8 @@ func (c *Communicator) Upload(dst string, src io.Reader) error {
return err
}
// TODO(mitchellh): Copy the file into place
// Copy the file into place by copying the temporary file we put
// into the shared folder into the proper location in the container
cmd := &packer.RemoteCmd{
Command: fmt.Sprintf("cp %s/%s %s", c.ContainerDir,
filepath.Base(tempfile.Name()), dst),
@ -177,3 +95,111 @@ func (c *Communicator) UploadDir(dst string, src string, exclude []string) error
func (c *Communicator) Download(src string, dst io.Writer) error {
return nil
}
// Runs the given command and blocks until completion
func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.WriteCloser, outputFile *os.File, exitCodePath string) {
// For Docker, remote communication must be serialized since it
// only supports single execution.
c.lock.Lock()
defer c.lock.Unlock()
// Modify the remote command so that all the output of the commands
// go to a single file and so that the exit code is redirected to
// a single file. This lets us determine both when the command
// is truly complete (because the file will have data), what the
// exit status is (because Docker loses it because of the pty, not
// Docker's fault), and get the output (Docker bug).
remoteCmd := fmt.Sprintf("(%s) >%s 2>&1; echo $? >%s",
remote.Command,
filepath.Join(c.ContainerDir, filepath.Base(outputFile.Name())),
filepath.Join(c.ContainerDir, filepath.Base(exitCodePath)))
// Start the command
log.Printf("Executing in container %s: %#v", c.ContainerId, remoteCmd)
if err := cmd.Start(); err != nil {
log.Printf("Error executing: %s", err)
remote.SetExited(254)
return
}
go func() {
defer stdin_w.Close()
// This sleep needs to be here because of the issue linked to below.
// Basically, without it, Docker will hang on reading stdin forever,
// and won't see what we write, for some reason.
//
// https://github.com/dotcloud/docker/issues/2628
time.Sleep(2 * time.Second)
stdin_w.Write([]byte(remoteCmd + "\n"))
}()
err := cmd.Wait()
if exitErr, ok := err.(*exec.ExitError); ok {
exitStatus := 1
// There is no process-independent way to get the REAL
// exit status so we just try to go deeper.
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
exitStatus = status.ExitStatus()
}
// Say that we ended, since if Docker itself failed, then
// the command must've not run, or so we assume
remote.SetExited(exitStatus)
return
}
// Wait for the exit code to appear in our file...
log.Println("Waiting for exit code to appear for remote command...")
for {
fi, err := os.Stat(exitCodePath)
if err == nil && fi.Size() > 0 {
break
}
time.Sleep(1 * time.Second)
}
// Read the exit code
exitRaw, err := ioutil.ReadFile(exitCodePath)
if err != nil {
log.Printf("Error executing: %s", err)
remote.SetExited(254)
return
}
exitStatus, err := strconv.ParseInt(string(bytes.TrimSpace(exitRaw)), 10, 0)
if err != nil {
log.Printf("Error executing: %s", err)
remote.SetExited(254)
return
}
log.Printf("Executed command exit status: %d", exitStatus)
// Read the output
f, err := os.Open(outputFile.Name())
if err != nil {
log.Printf("Error executing: %s", err)
remote.SetExited(254)
return
}
defer f.Close()
if remote.Stdout != nil {
io.Copy(remote.Stdout, f)
} else {
output, err := ioutil.ReadAll(f)
if err != nil {
log.Printf("Error executing: %s", err)
remote.SetExited(254)
return
}
log.Printf("Command output: %s", string(output))
}
// Finally, we're done
remote.SetExited(int(exitStatus))
}