From a0ab33124a52853af611254cd73838e3d4407f51 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 29 Aug 2016 11:11:29 -0400 Subject: [PATCH] Extract stream output handling to a new type. Signed-off-by: Daniel Nephin --- api/client/cli.go | 53 +++++++-------------------- api/client/container/attach.go | 4 +- api/client/container/create.go | 4 +- api/client/container/export.go | 2 +- api/client/container/run.go | 4 +- api/client/container/start.go | 2 +- api/client/image/build.go | 4 +- api/client/image/import.go | 2 +- api/client/image/load.go | 4 +- api/client/image/push.go | 3 +- api/client/image/save.go | 2 +- api/client/out.go | 67 ++++++++++++++++++++++++++++++++++ api/client/trust.go | 6 +-- api/client/utils.go | 23 ++---------- pkg/jsonmessage/jsonmessage.go | 11 ++++++ 15 files changed, 113 insertions(+), 78 deletions(-) create mode 100644 api/client/out.go diff --git a/api/client/cli.go b/api/client/cli.go index 077d1650b9..cfaba28c9a 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -31,27 +31,24 @@ type DockerCli struct { // configFile has the client configuration file configFile *configfile.ConfigFile // in holds the input stream and closer (io.ReadCloser) for the client. + // TODO: remove in io.ReadCloser - // out holds the output stream (io.Writer) for the client. - out io.Writer // err holds the error stream (io.Writer) for the client. err io.Writer // keyFile holds the key file as a string. keyFile string // inFd holds the file descriptor of the client's STDIN (if valid). + // TODO: remove inFd uintptr - // outFd holds file descriptor of the client's STDOUT (if valid). - outFd uintptr // isTerminalIn indicates whether the client's STDIN is a TTY + // TODO: remove isTerminalIn bool - // isTerminalOut indicates whether the client's STDOUT is a TTY - isTerminalOut bool // client is the http client that performs all API operations client client.APIClient // inState holds the terminal input state + // TODO: remove inState *term.State - // outState holds the terminal output state - outState *term.State + out *OutStream } // Client returns the APIClient @@ -60,7 +57,7 @@ func (cli *DockerCli) Client() client.APIClient { } // Out returns the writer used for stdout -func (cli *DockerCli) Out() io.Writer { +func (cli *DockerCli) Out() *OutStream { return cli.out } @@ -80,20 +77,11 @@ func (cli *DockerCli) ConfigFile() *configfile.ConfigFile { } // IsTerminalIn returns true if the clients stdin is a TTY +// TODO: remove func (cli *DockerCli) IsTerminalIn() bool { return cli.isTerminalIn } -// IsTerminalOut returns true if the clients stdout is a TTY -func (cli *DockerCli) IsTerminalOut() bool { - return cli.isTerminalOut -} - -// OutFd returns the fd for the stdout stream -func (cli *DockerCli) OutFd() uintptr { - return cli.outFd -} - // CheckTtyInput checks if we are trying to attach to a container tty // from a non-tty client input stream, and if so, returns an error. func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error { @@ -119,12 +107,8 @@ func (cli *DockerCli) setRawTerminal() error { } cli.inState = state } - if cli.isTerminalOut { - state, err := term.SetRawTerminalOutput(cli.outFd) - if err != nil { - return err - } - cli.outState = state + if err := cli.out.setRawTerminal(); err != nil { + return err } } return nil @@ -134,9 +118,7 @@ func (cli *DockerCli) restoreTerminal(in io.Closer) error { if cli.inState != nil { term.RestoreTerminal(cli.inFd, cli.inState) } - if cli.outState != nil { - term.RestoreTerminal(cli.outFd, cli.outState) - } + cli.out.restoreTerminal() // WARNING: DO NOT REMOVE THE OS CHECK !!! // For some reason this Close call blocks on darwin.. // As the client exists right after, simply discard the close @@ -149,22 +131,17 @@ func (cli *DockerCli) restoreTerminal(in io.Closer) error { // Initialize the dockerCli runs initialization that must happen after command // line flags are parsed. -func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { +func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) (err error) { cli.configFile = LoadDefaultConfigFile(cli.err) - client, err := NewAPIClientFromFlags(opts.Common, cli.configFile) + cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile) if err != nil { return err } - cli.client = client - if cli.in != nil { cli.inFd, cli.isTerminalIn = term.GetFdInfo(cli.in) } - if cli.out != nil { - cli.outFd, cli.isTerminalOut = term.GetFdInfo(cli.out) - } if opts.Common.TrustKey == "" { cli.keyFile = filepath.Join(cliconfig.ConfigDir(), cliflags.DefaultTrustKeyFile) @@ -177,11 +154,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { // NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err. func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli { - return &DockerCli{ - in: in, - out: out, - err: err, - } + return &DockerCli{in: in, out: NewOutStream(out), err: err} } // LoadDefaultConfigFile attempts to load the default config file and returns diff --git a/api/client/container/attach.go b/api/client/container/attach.go index fb3a715506..65e182c722 100644 --- a/api/client/container/attach.go +++ b/api/client/container/attach.go @@ -95,8 +95,8 @@ func runAttach(dockerCli *client.DockerCli, opts *attachOptions) error { } defer resp.Close() - if c.Config.Tty && dockerCli.IsTerminalOut() { - height, width := dockerCli.GetTtySize() + if c.Config.Tty && dockerCli.Out().IsTerminal() { + height, width := dockerCli.Out().GetTtySize() // To handle the case where a user repeatedly attaches/detaches without resizing their // terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially // resize it, then go back to normal. Without this, every attach after the first will diff --git a/api/client/container/create.go b/api/client/container/create.go index 82c14ada67..d9eca183d1 100644 --- a/api/client/container/create.go +++ b/api/client/container/create.go @@ -103,8 +103,8 @@ func pullImage(ctx context.Context, dockerCli *client.DockerCli, image string, o return jsonmessage.DisplayJSONMessagesStream( responseBody, out, - dockerCli.OutFd(), - dockerCli.IsTerminalOut(), + dockerCli.Out().FD(), + dockerCli.Out().IsTerminal(), nil) } diff --git a/api/client/container/export.go b/api/client/container/export.go index 8dbea9f7b0..c2fb176789 100644 --- a/api/client/container/export.go +++ b/api/client/container/export.go @@ -38,7 +38,7 @@ func NewExportCommand(dockerCli *client.DockerCli) *cobra.Command { } func runExport(dockerCli *client.DockerCli, opts exportOptions) error { - if opts.output == "" && dockerCli.IsTerminalOut() { + if opts.output == "" && dockerCli.Out().IsTerminal() { return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.") } diff --git a/api/client/container/run.go b/api/client/container/run.go index 154683819d..af9abdae22 100644 --- a/api/client/container/run.go +++ b/api/client/container/run.go @@ -135,7 +135,7 @@ func runRun(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts *runOptions, // a far better user experience rather than relying on subsequent resizes // to cause things to catch up. if runtime.GOOS == "windows" { - hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.GetTtySize() + hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize() } ctx, cancelFun := context.WithCancel(context.Background()) @@ -234,7 +234,7 @@ func runRun(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts *runOptions, return runStartContainerErr(err) } - if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && dockerCli.IsTerminalOut() { + if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && dockerCli.Out().IsTerminal() { if err := dockerCli.MonitorTtySize(ctx, createResponse.ID, false); err != nil { fmt.Fprintf(stderr, "Error monitoring TTY size: %s\n", err) } diff --git a/api/client/container/start.go b/api/client/container/start.go index db0e948128..50aaa47896 100644 --- a/api/client/container/start.go +++ b/api/client/container/start.go @@ -118,7 +118,7 @@ func runStart(dockerCli *client.DockerCli, opts *startOptions) error { } // 5. Wait for attachment to break. - if c.Config.Tty && dockerCli.IsTerminalOut() { + if c.Config.Tty && dockerCli.Out().IsTerminal() { if err := dockerCli.MonitorTtySize(ctx, c.ID, false); err != nil { fmt.Fprintf(dockerCli.Err(), "Error monitoring TTY size: %s\n", err) } diff --git a/api/client/image/build.go b/api/client/image/build.go index df89e7df1c..226c43dc06 100644 --- a/api/client/image/build.go +++ b/api/client/image/build.go @@ -227,7 +227,7 @@ func runBuild(dockerCli *client.DockerCli, options buildOptions) error { // Setup an upload progress bar progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true) - if !dockerCli.IsTerminalOut() { + if !dockerCli.Out().IsTerminal() { progressOutput = &lastProgressOutput{output: progressOutput} } @@ -293,7 +293,7 @@ func runBuild(dockerCli *client.DockerCli, options buildOptions) error { } defer response.Body.Close() - err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.OutFd(), dockerCli.IsTerminalOut(), nil) + err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), nil) if err != nil { if jerr, ok := err.(*jsonmessage.JSONError); ok { // If no error code is set, default to 1 diff --git a/api/client/image/import.go b/api/client/image/import.go index 2ca775a4ff..d56a5bb668 100644 --- a/api/client/image/import.go +++ b/api/client/image/import.go @@ -84,5 +84,5 @@ func runImport(dockerCli *client.DockerCli, opts importOptions) error { } defer responseBody.Close() - return jsonmessage.DisplayJSONMessagesStream(responseBody, dockerCli.Out(), dockerCli.OutFd(), dockerCli.IsTerminalOut(), nil) + return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil) } diff --git a/api/client/image/load.go b/api/client/image/load.go index 240362d1e7..8905fa2819 100644 --- a/api/client/image/load.go +++ b/api/client/image/load.go @@ -49,7 +49,7 @@ func runLoad(dockerCli *client.DockerCli, opts loadOptions) error { defer file.Close() input = file } - if !dockerCli.IsTerminalOut() { + if !dockerCli.Out().IsTerminal() { opts.quiet = true } response, err := dockerCli.Client().ImageLoad(context.Background(), input, opts.quiet) @@ -59,7 +59,7 @@ func runLoad(dockerCli *client.DockerCli, opts loadOptions) error { defer response.Body.Close() if response.Body != nil && response.JSON { - return jsonmessage.DisplayJSONMessagesStream(response.Body, dockerCli.Out(), dockerCli.OutFd(), dockerCli.IsTerminalOut(), nil) + return jsonmessage.DisplayJSONMessagesToStream(response.Body, dockerCli.Out(), nil) } _, err = io.Copy(dockerCli.Out(), response.Body) diff --git a/api/client/image/push.go b/api/client/image/push.go index 1526b2a26b..3eb41cf08d 100644 --- a/api/client/image/push.go +++ b/api/client/image/push.go @@ -57,6 +57,5 @@ func runPush(dockerCli *client.DockerCli, remote string) error { } defer responseBody.Close() - - return jsonmessage.DisplayJSONMessagesStream(responseBody, dockerCli.Out(), dockerCli.OutFd(), dockerCli.IsTerminalOut(), nil) + return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil) } diff --git a/api/client/image/save.go b/api/client/image/save.go index c5ea849a38..f7563880b7 100644 --- a/api/client/image/save.go +++ b/api/client/image/save.go @@ -38,7 +38,7 @@ func NewSaveCommand(dockerCli *client.DockerCli) *cobra.Command { } func runSave(dockerCli *client.DockerCli, opts saveOptions) error { - if opts.output == "" && dockerCli.IsTerminalOut() { + if opts.output == "" && dockerCli.Out().IsTerminal() { return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.") } diff --git a/api/client/out.go b/api/client/out.go new file mode 100644 index 0000000000..ac33d005f3 --- /dev/null +++ b/api/client/out.go @@ -0,0 +1,67 @@ +package client + +import ( + "io" + "os" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/term" +) + +// OutStream is an output stream used by the DockerCli to write normal program +// output. +type OutStream struct { + out io.Writer + fd uintptr + isTerminal bool + state *term.State +} + +func (o *OutStream) Write(p []byte) (int, error) { + return o.out.Write(p) +} + +// FD returns the file descriptor number for this stream +func (o *OutStream) FD() uintptr { + return o.fd +} + +// IsTerminal returns true if this stream is connected to a terminal +func (o *OutStream) IsTerminal() bool { + return o.isTerminal +} + +func (o *OutStream) setRawTerminal() (err error) { + if os.Getenv("NORAW") != "" || !o.isTerminal { + return nil + } + o.state, err = term.SetRawTerminalOutput(o.fd) + return err +} + +func (o *OutStream) restoreTerminal() { + if o.state != nil { + term.RestoreTerminal(o.fd, o.state) + } +} + +// GetTtySize returns the height and width in characters of the tty +func (o *OutStream) GetTtySize() (int, int) { + if !o.isTerminal { + return 0, 0 + } + ws, err := term.GetWinsize(o.fd) + if err != nil { + logrus.Debugf("Error getting size: %s", err) + if ws == nil { + return 0, 0 + } + } + return int(ws.Height), int(ws.Width) +} + +// NewOutStream returns a new OutStream object from a Writer +func NewOutStream(out io.Writer) *OutStream { + fd, isTerminal := term.GetFdInfo(out) + return &OutStream{out: out, fd: fd, isTerminal: isTerminal} +} diff --git a/api/client/trust.go b/api/client/trust.go index ae33528c2b..e86a141ece 100644 --- a/api/client/trust.go +++ b/api/client/trust.go @@ -440,14 +440,14 @@ func (cli *DockerCli) TrustedPush(ctx context.Context, repoInfo *registry.Reposi // We want trust signatures to always take an explicit tag, // otherwise it will act as an untrusted push. if tag == "" { - if err = jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, nil); err != nil { + if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil); err != nil { return err } fmt.Fprintln(cli.out, "No tag specified, skipping trust metadata push") return nil } - if err = jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, handleTarget); err != nil { + if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), handleTarget); err != nil { return err } @@ -580,7 +580,7 @@ func (cli *DockerCli) ImagePullPrivileged(ctx context.Context, authConfig types. } defer responseBody.Close() - return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, nil) + return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil) } // ImagePushPrivileged push the image diff --git a/api/client/utils.go b/api/client/utils.go index 28fbe85156..c20230dd64 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -17,11 +17,11 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/docker/docker/pkg/signal" - "github.com/docker/docker/pkg/term" + "github.com/docker/engine-api/types" ) func (cli *DockerCli) resizeTty(ctx context.Context, id string, isExec bool) { - height, width := cli.GetTtySize() + height, width := cli.Out().GetTtySize() cli.ResizeTtyTo(ctx, id, height, width, isExec) } @@ -70,10 +70,10 @@ func (cli *DockerCli) MonitorTtySize(ctx context.Context, id string, isExec bool if runtime.GOOS == "windows" { go func() { - prevH, prevW := cli.GetTtySize() + prevH, prevW := cli.Out().GetTtySize() for { time.Sleep(time.Millisecond * 250) - h, w := cli.GetTtySize() + h, w := cli.Out().GetTtySize() if prevW != w || prevH != h { cli.resizeTty(ctx, id, isExec) @@ -94,21 +94,6 @@ func (cli *DockerCli) MonitorTtySize(ctx context.Context, id string, isExec bool return nil } -// GetTtySize returns the height and width in characters of the tty -func (cli *DockerCli) GetTtySize() (int, int) { - if !cli.isTerminalOut { - return 0, 0 - } - ws, err := term.GetWinsize(cli.outFd) - if err != nil { - logrus.Debugf("Error getting size: %s", err) - if ws == nil { - return 0, 0 - } - } - return int(ws.Height), int(ws.Width) -} - // CopyToFile writes the content of the reader to the specified file func CopyToFile(outfile string, r io.Reader) error { tmpFile, err := ioutil.TempFile(filepath.Dir(outfile), ".docker_temp_") diff --git a/pkg/jsonmessage/jsonmessage.go b/pkg/jsonmessage/jsonmessage.go index 91b073b731..3c4a346e46 100644 --- a/pkg/jsonmessage/jsonmessage.go +++ b/pkg/jsonmessage/jsonmessage.go @@ -219,3 +219,14 @@ func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, } return nil } + +type stream interface { + io.Writer + FD() uintptr + IsTerminal() bool +} + +// DisplayJSONMessagesToStream prints json messages to the output stream +func DisplayJSONMessagesToStream(in io.Reader, stream stream, auxCallback func(*json.RawMessage)) error { + return DisplayJSONMessagesStream(in, stream, stream.FD(), stream.IsTerminal(), auxCallback) +}