diff --git a/commands.go b/commands.go index 0bc5583738..8bc451b6bf 100644 --- a/commands.go +++ b/commands.go @@ -62,7 +62,7 @@ func (srv *Server) Help() string { } // 'docker login': login / register a user to registry service. -func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { // Read a line on raw terminal with support for simple backspace // sequences and echo. // @@ -71,7 +71,7 @@ func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout io.Writer, args ...strin // - we have to read a password (without echoing it); // - the rcli "protocol" only supports cannonical and raw modes and you // can't tune it once the command as been started. - var readStringOnRawTerminal = func(stdin io.Reader, stdout io.Writer, echo bool) string { + var readStringOnRawTerminal = func(stdin io.Reader, stdout rcli.DockerConn, echo bool) string { char := make([]byte, 1) buffer := make([]byte, 64) var i = 0 @@ -106,13 +106,15 @@ func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout io.Writer, args ...strin } return string(buffer[:i]) } - var readAndEchoString = func(stdin io.Reader, stdout io.Writer) string { + var readAndEchoString = func(stdin io.Reader, stdout rcli.DockerConn) string { return readStringOnRawTerminal(stdin, stdout, true) } - var readString = func(stdin io.Reader, stdout io.Writer) string { + var readString = func(stdin io.Reader, stdout rcli.DockerConn) string { return readStringOnRawTerminal(stdin, stdout, false) } + stdout.SetOptionRawTerminal() + cmd := rcli.Subcmd(stdout, "login", "", "Register or Login to the docker registry server") if err := cmd.Parse(args); err != nil { return nil @@ -158,7 +160,7 @@ func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout io.Writer, args ...strin } // 'docker wait': block until a container stops -func (srv *Server) CmdWait(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdWait(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "wait", "[OPTIONS] NAME", "Block until a container stops, then print its exit code.") if err := cmd.Parse(args); err != nil { return nil @@ -178,14 +180,14 @@ func (srv *Server) CmdWait(stdin io.ReadCloser, stdout io.Writer, args ...string } // 'docker version': show version information -func (srv *Server) CmdVersion(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdVersion(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { fmt.Fprintf(stdout, "Version:%s\n", VERSION) fmt.Fprintf(stdout, "Git Commit:%s\n", GIT_COMMIT) return nil } // 'docker info': display system-wide information. -func (srv *Server) CmdInfo(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdInfo(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { images, _ := srv.runtime.graph.All() var imgcount int if images == nil { @@ -214,7 +216,7 @@ func (srv *Server) CmdInfo(stdin io.ReadCloser, stdout io.Writer, args ...string return nil } -func (srv *Server) CmdStop(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdStop(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "stop", "[OPTIONS] NAME", "Stop a running container") if err := cmd.Parse(args); err != nil { return nil @@ -236,7 +238,7 @@ func (srv *Server) CmdStop(stdin io.ReadCloser, stdout io.Writer, args ...string return nil } -func (srv *Server) CmdRestart(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdRestart(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "restart", "[OPTIONS] NAME", "Restart a running container") if err := cmd.Parse(args); err != nil { return nil @@ -258,7 +260,7 @@ func (srv *Server) CmdRestart(stdin io.ReadCloser, stdout io.Writer, args ...str return nil } -func (srv *Server) CmdStart(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdStart(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "start", "[OPTIONS] NAME", "Start a stopped container") if err := cmd.Parse(args); err != nil { return nil @@ -280,7 +282,7 @@ func (srv *Server) CmdStart(stdin io.ReadCloser, stdout io.Writer, args ...strin return nil } -func (srv *Server) CmdInspect(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdInspect(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "inspect", "[OPTIONS] CONTAINER", "Return low-level information on a container") if err := cmd.Parse(args); err != nil { return nil @@ -315,7 +317,7 @@ func (srv *Server) CmdInspect(stdin io.ReadCloser, stdout io.Writer, args ...str return nil } -func (srv *Server) CmdPort(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdPort(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "port", "[OPTIONS] CONTAINER PRIVATE_PORT", "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT") if err := cmd.Parse(args); err != nil { return nil @@ -339,7 +341,7 @@ func (srv *Server) CmdPort(stdin io.ReadCloser, stdout io.Writer, args ...string } // 'docker rmi NAME' removes all images with the name NAME -func (srv *Server) CmdRmi(stdin io.ReadCloser, stdout io.Writer, args ...string) (err error) { +func (srv *Server) CmdRmi(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) (err error) { cmd := rcli.Subcmd(stdout, "rmimage", "[OPTIONS] IMAGE", "Remove an image") if err := cmd.Parse(args); err != nil { return nil @@ -356,7 +358,7 @@ func (srv *Server) CmdRmi(stdin io.ReadCloser, stdout io.Writer, args ...string) return nil } -func (srv *Server) CmdHistory(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdHistory(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "history", "[OPTIONS] IMAGE", "Show the history of an image") if err := cmd.Parse(args); err != nil { return nil @@ -382,7 +384,7 @@ func (srv *Server) CmdHistory(stdin io.ReadCloser, stdout io.Writer, args ...str }) } -func (srv *Server) CmdRm(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdRm(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "rm", "[OPTIONS] CONTAINER", "Remove a container") if err := cmd.Parse(args); err != nil { return nil @@ -400,7 +402,7 @@ func (srv *Server) CmdRm(stdin io.ReadCloser, stdout io.Writer, args ...string) } // 'docker kill NAME' kills a running container -func (srv *Server) CmdKill(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdKill(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "kill", "[OPTIONS] CONTAINER [CONTAINER...]", "Kill a running container") if err := cmd.Parse(args); err != nil { return nil @@ -417,7 +419,7 @@ func (srv *Server) CmdKill(stdin io.ReadCloser, stdout io.Writer, args ...string return nil } -func (srv *Server) CmdImport(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdImport(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "import", "[OPTIONS] URL|- [REPOSITORY [TAG]]", "Create a new filesystem image from the contents of a tarball") var archive io.Reader var resp *http.Response @@ -464,7 +466,7 @@ func (srv *Server) CmdImport(stdin io.ReadCloser, stdout io.Writer, args ...stri return nil } -func (srv *Server) CmdPush(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdPush(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "push", "NAME", "Push an image or a repository to the registry") if err := cmd.Parse(args); err != nil { return nil @@ -523,7 +525,7 @@ func (srv *Server) CmdPush(stdin io.ReadCloser, stdout io.Writer, args ...string return nil } -func (srv *Server) CmdPull(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdPull(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "pull", "NAME", "Pull an image or a repository from the registry") if err := cmd.Parse(args); err != nil { return nil @@ -548,7 +550,7 @@ func (srv *Server) CmdPull(stdin io.ReadCloser, stdout io.Writer, args ...string return nil } -func (srv *Server) CmdImages(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdImages(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "images", "[OPTIONS] [NAME]", "List images") //limit := cmd.Int("l", 0, "Only show the N most recent versions of each image") quiet := cmd.Bool("q", false, "only show numeric IDs") @@ -638,7 +640,7 @@ func (srv *Server) CmdImages(stdin io.ReadCloser, stdout io.Writer, args ...stri return nil } -func (srv *Server) CmdPs(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdPs(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "ps", "[OPTIONS]", "List containers") quiet := cmd.Bool("q", false, "Only display numeric IDs") @@ -685,7 +687,7 @@ func (srv *Server) CmdPs(stdin io.ReadCloser, stdout io.Writer, args ...string) return nil } -func (srv *Server) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdCommit(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "commit", "[OPTIONS] CONTAINER [REPOSITORY [TAG]]", "Create a new image from a container's changes") @@ -706,7 +708,7 @@ func (srv *Server) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...stri return nil } -func (srv *Server) CmdExport(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdExport(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "export", "CONTAINER", "Export the contents of a filesystem as a tar archive") @@ -728,7 +730,7 @@ func (srv *Server) CmdExport(stdin io.ReadCloser, stdout io.Writer, args ...stri return fmt.Errorf("No such container: %s", name) } -func (srv *Server) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdDiff(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "diff", "CONTAINER [OPTIONS]", "Inspect changes on a container's filesystem") @@ -752,7 +754,7 @@ func (srv *Server) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...string return nil } -func (srv *Server) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdLogs(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "logs", "[OPTIONS] CONTAINER", "Fetch the logs of a container") if err := cmd.Parse(args); err != nil { return nil @@ -784,7 +786,8 @@ func (srv *Server) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...string return fmt.Errorf("No such container: %s", cmd.Arg(0)) } -func (srv *Server) CmdAttach(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdAttach(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { + stdout.SetOptionRawTerminal() cmd := rcli.Subcmd(stdout, "attach", "CONTAINER", "Attach to a running container") if err := cmd.Parse(args); err != nil { return nil @@ -857,7 +860,7 @@ func (opts AttachOpts) Get(val string) bool { return false } -func (srv *Server) CmdTag(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdTag(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "tag", "[OPTIONS] IMAGE REPOSITORY [TAG]", "Tag an image into a repository") force := cmd.Bool("f", false, "Force") if err := cmd.Parse(args); err != nil { @@ -870,7 +873,8 @@ func (srv *Server) CmdTag(stdin io.ReadCloser, stdout io.Writer, args ...string) return srv.runtime.repositories.Set(cmd.Arg(1), cmd.Arg(2), cmd.Arg(0), *force) } -func (srv *Server) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdRun(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { + stdout.SetOptionRawTerminal() config, err := ParseRun(args, stdout) if err != nil { return err diff --git a/docker/docker.go b/docker/docker.go index c9c599954b..fa5379384c 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -2,6 +2,7 @@ package main import ( "flag" + "fmt" "github.com/dotcloud/docker" "github.com/dotcloud/docker/rcli" "github.com/dotcloud/docker/term" @@ -56,30 +57,82 @@ func daemon() error { return rcli.ListenAndServe("tcp", "127.0.0.1:4242", service) } -func runCommand(args []string) error { - var oldState *term.State - var err error - if term.IsTerminal(int(os.Stdin.Fd())) && os.Getenv("NORAW") == "" { - oldState, err = term.MakeRaw(int(os.Stdin.Fd())) - if err != nil { - return err - } - defer term.Restore(int(os.Stdin.Fd()), oldState) - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - go func() { - for _ = range c { - term.Restore(int(os.Stdin.Fd()), oldState) - log.Printf("\nSIGINT received\n") - os.Exit(0) - } - }() +func setRawTerminal() (*term.State, error) { + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + return nil, err } + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + for _ = range c { + term.Restore(int(os.Stdin.Fd()), oldState) + log.Printf("\nSIGINT received\n") + os.Exit(0) + } + }() + return oldState, err +} + +func restoreTerminal(state *term.State) { + term.Restore(int(os.Stdin.Fd()), state) +} + +type DockerLocalConn struct { + file *os.File + savedState *term.State +} + +func newDockerLocalConn(output *os.File) *DockerLocalConn { + return &DockerLocalConn{file: output} +} + +func (c *DockerLocalConn) Read(b []byte) (int, error) { return c.file.Read(b) } + +func (c *DockerLocalConn) Write(b []byte) (int, error) { return c.file.Write(b) } + +func (c *DockerLocalConn) Close() error { + if c.savedState != nil { + restoreTerminal(c.savedState) + c.savedState = nil + } + return c.file.Close() +} + +func (c *DockerLocalConn) CloseWrite() error { return nil } + +func (c *DockerLocalConn) CloseRead() error { return nil } + +func (c *DockerLocalConn) GetOptions() *rcli.DockerConnOptions { return nil } + +func (c *DockerLocalConn) SetOptionRawTerminal() { + if state, err := setRawTerminal(); err != nil { + fmt.Fprintf( + os.Stderr, + "Can't set the terminal in raw mode: %v", + err.Error(), + ) + } else { + c.savedState = state + } +} + +func runCommand(args []string) error { // FIXME: we want to use unix sockets here, but net.UnixConn doesn't expose // CloseWrite(), which we need to cleanly signal that stdin is closed without // closing the connection. // See http://code.google.com/p/go/issues/detail?id=3345 if conn, err := rcli.Call("tcp", "127.0.0.1:4242", args...); err == nil { + options := conn.GetOptions() + if options.RawTerminal && + term.IsTerminal(int(os.Stdin.Fd())) && + os.Getenv("NORAW") == "" { + if oldState, err := setRawTerminal(); err != nil { + return err + } else { + defer restoreTerminal(oldState) + } + } receiveStdout := docker.Go(func() error { _, err := io.Copy(os.Stdout, conn) return err @@ -104,12 +157,11 @@ func runCommand(args []string) error { if err != nil { return err } - if err := rcli.LocalCall(service, os.Stdin, os.Stdout, args...); err != nil { + dockerConn := newDockerLocalConn(os.Stdout) + defer dockerConn.Close() + if err := rcli.LocalCall(service, os.Stdin, dockerConn, args...); err != nil { return err } } - if oldState != nil { - term.Restore(int(os.Stdin.Fd()), oldState) - } return nil } diff --git a/rcli/tcp.go b/rcli/tcp.go index ff7e191f42..6fbf2abd09 100644 --- a/rcli/tcp.go +++ b/rcli/tcp.go @@ -2,6 +2,7 @@ package rcli import ( "bufio" + "bytes" "encoding/json" "fmt" "io" @@ -15,22 +16,104 @@ import ( var DEBUG_FLAG bool = false var CLIENT_SOCKET io.Writer = nil +type DockerTCPConn struct { + conn *net.TCPConn + options *DockerConnOptions + optionsBuf *[]byte + handshaked bool + client bool +} + +func NewDockerTCPConn(conn *net.TCPConn, client bool) *DockerTCPConn { + return &DockerTCPConn{ + conn: conn, + options: &DockerConnOptions{}, + client: client, + } +} + +func (c *DockerTCPConn) SetOptionRawTerminal() { + c.options.RawTerminal = true +} + +func (c *DockerTCPConn) GetOptions() *DockerConnOptions { + if c.client && !c.handshaked { + // Attempt to parse options encoded as a JSON dict and store + // the reminder of what we read from the socket in a buffer. + // + // bufio (and its ReadBytes method) would have been nice here, + // but if json.Unmarshal() fails (which will happen if we speak + // to a version of docker that doesn't send any option), then + // we can't put the data back in it for the next Read(). + c.handshaked = true + buf := make([]byte, 4096) + if n, _ := c.conn.Read(buf); n > 0 { + buf = buf[:n] + if nl := bytes.IndexByte(buf, '\n'); nl != -1 { + if err := json.Unmarshal(buf[:nl], c.options); err == nil { + buf = buf[nl+1:] + } + } + c.optionsBuf = &buf + } + } + + return c.options +} + +func (c *DockerTCPConn) Read(b []byte) (int, error) { + if c.optionsBuf != nil { + // Consume what we buffered in GetOptions() first: + optionsBuf := *c.optionsBuf + optionsBuflen := len(optionsBuf) + copied := copy(b, optionsBuf) + if copied < optionsBuflen { + optionsBuf = optionsBuf[copied:] + c.optionsBuf = &optionsBuf + return copied, nil + } + c.optionsBuf = nil + return copied, nil + } + return c.conn.Read(b) +} + +func (c *DockerTCPConn) Write(b []byte) (int, error) { + optionsLen := 0 + if !c.client && !c.handshaked { + c.handshaked = true + options, _ := json.Marshal(c.options) + options = append(options, '\n') + if optionsLen, err := c.conn.Write(options); err != nil { + return optionsLen, err + } + } + n, err := c.conn.Write(b) + return n + optionsLen, err +} + +func (c *DockerTCPConn) Close() error { return c.conn.Close() } + +func (c *DockerTCPConn) CloseWrite() error { return c.conn.CloseWrite() } + +func (c *DockerTCPConn) CloseRead() error { return c.conn.CloseRead() } + // Connect to a remote endpoint using protocol `proto` and address `addr`, // issue a single call, and return the result. // `proto` may be "tcp", "unix", etc. See the `net` package for available protocols. -func Call(proto, addr string, args ...string) (*net.TCPConn, error) { +func Call(proto, addr string, args ...string) (DockerConn, error) { cmd, err := json.Marshal(args) if err != nil { return nil, err } - conn, err := net.Dial(proto, addr) + conn, err := dialDocker(proto, addr) if err != nil { return nil, err } if _, err := fmt.Fprintln(conn, string(cmd)); err != nil { return nil, err } - return conn.(*net.TCPConn), nil + return conn, nil } // Listen on `addr`, using protocol `proto`, for incoming rcli calls, @@ -46,6 +129,10 @@ func ListenAndServe(proto, addr string, service Service) error { if conn, err := listener.Accept(); err != nil { return err } else { + conn, err := newDockerServerConn(conn) + if err != nil { + return err + } go func() { if DEBUG_FLAG { CLIENT_SOCKET = conn @@ -63,7 +150,7 @@ func ListenAndServe(proto, addr string, service Service) error { // Parse an rcli call on a new connection, and pass it to `service` if it // is valid. -func Serve(conn io.ReadWriter, service Service) error { +func Serve(conn DockerConn, service Service) error { r := bufio.NewReader(conn) var args []string if line, err := r.ReadString('\n'); err != nil { diff --git a/rcli/types.go b/rcli/types.go index 2600fe240d..8bfadb5420 100644 --- a/rcli/types.go +++ b/rcli/types.go @@ -13,10 +13,49 @@ import ( "fmt" "io" "log" + "net" "reflect" "strings" ) +type DockerConnOptions struct { + RawTerminal bool +} + +type DockerConn interface { + io.ReadWriteCloser + CloseWrite() error + CloseRead() error + GetOptions() *DockerConnOptions + SetOptionRawTerminal() +} + +var UnknownDockerProto = errors.New("Only TCP is actually supported by Docker at the moment") + +func dialDocker(proto string, addr string) (DockerConn, error) { + conn, err := net.Dial(proto, addr) + if err != nil { + return nil, err + } + switch i := conn.(type) { + case *net.TCPConn: + return NewDockerTCPConn(i, true), nil + } + return nil, UnknownDockerProto +} + +func newDockerFromConn(conn net.Conn, client bool) (DockerConn, error) { + switch i := conn.(type) { + case *net.TCPConn: + return NewDockerTCPConn(i, client), nil + } + return nil, UnknownDockerProto +} + +func newDockerServerConn(conn net.Conn) (DockerConn, error) { + return newDockerFromConn(conn, false) +} + type Service interface { Name() string Help() string @@ -26,11 +65,11 @@ type Cmd func(io.ReadCloser, io.Writer, ...string) error type CmdMethod func(Service, io.ReadCloser, io.Writer, ...string) error // FIXME: For reverse compatibility -func call(service Service, stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func call(service Service, stdin io.ReadCloser, stdout DockerConn, args ...string) error { return LocalCall(service, stdin, stdout, args...) } -func LocalCall(service Service, stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func LocalCall(service Service, stdin io.ReadCloser, stdout DockerConn, args ...string) error { if len(args) == 0 { args = []string{"help"} }