1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

Only set the terminal in raw mode for commands which need it

The raw mode is actually only needed when you attach to a container.
Having it enabled all the time can be a pain, e.g: if docker crashes
your terminal will end up in a broken state.

Since we are currently missing a real API for the docker daemon to
negotiate this kind of options, this changeset actually enable the raw
mode on the login (because it outputs a password), run and attach
commands.

This "optional raw mode" is implemented by passing a more complicated
interface than io.Writer as the stdout argument of each command. This
interface (DockerConn) exposes a method which allows the command to set
the terminal in raw mode or not.

Finally, the code added by this changeset will be deprecated by a real
API for the docker daemon.
This commit is contained in:
Louis Opter 2013-04-03 12:25:19 -07:00
parent 4e5001b46a
commit 7d0ab3858e
4 changed files with 238 additions and 56 deletions

View file

@ -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

View file

@ -2,6 +2,7 @@ package main
import (
"flag"
"fmt"
"github.com/dotcloud/docker"
"github.com/dotcloud/docker/rcli"
"github.com/dotcloud/docker/term"
@ -56,15 +57,11 @@ 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()))
func setRawTerminal() (*term.State, error) {
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
return err
return nil, err
}
defer term.Restore(int(os.Stdin.Fd()), oldState)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
@ -74,12 +71,68 @@ func runCommand(args []string) error {
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
}

View file

@ -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 {

View file

@ -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"}
}