diff --git a/api/client/commands.go b/api/client/commands.go index c47f0bfd16..01e7e6d743 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -625,7 +625,7 @@ func (cli *DockerCli) CmdStart(args ...string) error { v.Set("stderr", "1") cErr = utils.Go(func() error { - return cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), tty, in, cli.out, cli.err, nil) + return cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), tty, in, cli.out, cli.err, nil, nil) }) } @@ -1827,7 +1827,7 @@ func (cli *DockerCli) CmdAttach(args ...string) error { defer signal.StopCatch(sigc) } - if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), tty, in, cli.out, cli.err, nil); err != nil { + if err := cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), tty, in, cli.out, cli.err, nil, nil); err != nil { return err } @@ -2109,7 +2109,7 @@ func (cli *DockerCli) CmdRun(args ...string) error { } errCh = utils.Go(func() error { - return cli.hijack("POST", "/containers/"+runResult.Get("Id")+"/attach?"+v.Encode(), config.Tty, in, out, stderr, hijacked) + return cli.hijack("POST", "/containers/"+runResult.Get("Id")+"/attach?"+v.Encode(), config.Tty, in, out, stderr, hijacked, nil) }) } else { close(hijacked) @@ -2299,3 +2299,77 @@ func (cli *DockerCli) CmdLoad(args ...string) error { } return nil } + +func (cli *DockerCli) CmdExec(args ...string) error { + cmd := cli.Subcmd("exec", "[OPTIONS] CONTAINER COMMAND [ARG...]", "Run a command in an existing container") + + execConfig, err := runconfig.ParseExec(cmd, args) + if err != nil { + return err + } + if execConfig.Container == "" { + cmd.Usage() + return nil + } + + if execConfig.Detach { + _, _, err := cli.call("POST", "/containers/"+execConfig.Container+"/exec", execConfig, false) + return err + } + var ( + out, stderr io.Writer + in io.ReadCloser + // We need to instanciate the chan because the select needs it. It can + // be closed but can't be uninitialized. + hijacked = make(chan io.Closer) + errCh chan error + ) + // Block the return until the chan gets closed + defer func() { + log.Debugf("End of CmdExec(), Waiting for hijack to finish.") + if _, ok := <-hijacked; ok { + log.Errorf("Hijack did not finish (chan still open)") + } + }() + + if execConfig.AttachStdin { + in = cli.in + } + if execConfig.AttachStdout { + out = cli.out + } + if execConfig.AttachStderr { + if execConfig.Tty { + stderr = cli.out + } else { + stderr = cli.err + } + } + errCh = utils.Go(func() error { + return cli.hijack("POST", "/containers/"+execConfig.Container+"/exec?", execConfig.Tty, in, out, stderr, hijacked, execConfig) + }) + + // Acknowledge the hijack before starting + select { + case closer := <-hijacked: + // Make sure that hijack gets closed when returning. (result + // in closing hijack chan and freeing server's goroutines. + if closer != nil { + defer closer.Close() + } + case err := <-errCh: + if err != nil { + log.Debugf("Error hijack: %s", err) + return err + } + } + // TODO(vishh): Enable tty size monitoring once the daemon can support that. + if errCh != nil { + if err := <-errCh; err != nil { + log.Debugf("Error hijack: %s", err) + return err + } + } + + return nil +} diff --git a/api/client/hijack.go b/api/client/hijack.go index ba6ebfb0d8..63fd8edb75 100644 --- a/api/client/hijack.go +++ b/api/client/hijack.go @@ -25,14 +25,18 @@ func (cli *DockerCli) dial() (net.Conn, error) { return net.Dial(cli.proto, cli.addr) } -func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer, started chan io.Closer) error { +func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer, started chan io.Closer, body interface{}) error { defer func() { if started != nil { close(started) } }() - req, err := http.NewRequest(method, fmt.Sprintf("/v%s%s", api.APIVERSION, path), nil) + params, err := cli.getUrlBody(body) + if err != nil { + return err + } + req, err := http.NewRequest(method, fmt.Sprintf("/v%s%s", api.APIVERSION, path), params) if err != nil { return err } diff --git a/api/client/utils.go b/api/client/utils.go index e4ef8d3875..e1d829e6da 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -40,24 +40,31 @@ func (cli *DockerCli) HTTPClient() *http.Client { return &http.Client{Transport: tr} } -func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo bool) (io.ReadCloser, int, error) { +func (cli *DockerCli) getUrlBody(data interface{}) (*bytes.Buffer, error) { params := bytes.NewBuffer(nil) if data != nil { if env, ok := data.(engine.Env); ok { if err := env.Encode(params); err != nil { - return nil, -1, err + return nil, err } } else { buf, err := json.Marshal(data) if err != nil { - return nil, -1, err + return nil, err } if _, err := params.Write(buf); err != nil { - return nil, -1, err + return nil, err } } } + return params, nil +} +func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo bool) (io.ReadCloser, int, error) { + params, err := cli.getUrlBody(data) + if err != nil { + return nil, -1, err + } req, err := http.NewRequest(method, fmt.Sprintf("/v%s%s", api.APIVERSION, path), params) if err != nil { return nil, -1, err diff --git a/api/server/server.go b/api/server/server.go index ea708bc6e9..6201b35d5e 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -1025,6 +1025,65 @@ func postContainersCopy(eng *engine.Engine, version version.Version, w http.Resp return nil } +func postContainersExec(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := parseForm(r); err != nil { + return nil + } + var ( + name = vars["name"] + job = eng.Job("exec", name) + ) + if err := job.DecodeEnv(r.Body); err != nil { + return err + } + var errOut io.Writer = os.Stderr + + if !job.GetenvBool("Detach") { + // Setting up the streaming http interface. + inStream, outStream, err := hijackServer(w) + if err != nil { + return err + } + + defer func() { + if tcpc, ok := inStream.(*net.TCPConn); ok { + tcpc.CloseWrite() + } else { + inStream.Close() + } + }() + defer func() { + if tcpc, ok := outStream.(*net.TCPConn); ok { + tcpc.CloseWrite() + } else if closer, ok := outStream.(io.Closer); ok { + closer.Close() + } + }() + + var errStream io.Writer + + fmt.Fprintf(outStream, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n") + if !job.GetenvBool("Tty") && version.GreaterThanOrEqualTo("1.6") { + errStream = utils.NewStdWriter(outStream, utils.Stderr) + outStream = utils.NewStdWriter(outStream, utils.Stdout) + } else { + errStream = outStream + } + job.Stdin.Add(inStream) + job.Stdout.Add(outStream) + job.Stderr.Set(errStream) + errOut = outStream + } + // Now run the user process in container. + if err := job.Run(); err != nil { + fmt.Fprintf(errOut, "Error running in container %s: %s\n", name, err) + return err + } + w.WriteHeader(http.StatusNoContent) + + return nil +} + func optionsHandler(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { w.WriteHeader(http.StatusOK) return nil @@ -1147,6 +1206,7 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st "/containers/{name:.*}/resize": postContainersResize, "/containers/{name:.*}/attach": postContainersAttach, "/containers/{name:.*}/copy": postContainersCopy, + "/containers/{name:.*}/exec": postContainersExec, }, "DELETE": { "/containers/{name:.*}": deleteContainers, diff --git a/daemon/daemon.go b/daemon/daemon.go index 973efd6ed1..36e6bac58b 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -122,6 +122,7 @@ func (daemon *Daemon) Install(eng *engine.Engine) error { "unpause": daemon.ContainerUnpause, "wait": daemon.ContainerWait, "image_delete": daemon.ImageDelete, // FIXME: see above + "exec": daemon.ContainerExec, } { if err := eng.Register(name, method); err != nil { return err diff --git a/daemon/exec.go b/daemon/exec.go index 2bbc1965d7..263d1d0fd9 100644 --- a/daemon/exec.go +++ b/daemon/exec.go @@ -92,6 +92,7 @@ func (d *Daemon) ContainerExec(job *engine.Job) engine.Status { attachErr = d.Attach(&execConfig.StreamConfig, config.AttachStdin, false, config.Tty, cStdin, cStdinCloser, cStdout, cStderr) }() + log.Debugf("Exec Config is %+v\n", execConfig) go func() { err := container.Exec(execConfig) if err != nil {