From 4acc2c7499425b9c2521977019d9fe4da73e6af8 Mon Sep 17 00:00:00 2001 From: John Starks Date: Wed, 22 Jun 2016 16:34:01 -0700 Subject: [PATCH] Windows: Always enable VT emulation Always enable VT output emulation when starting the process so that non-attaching commands can still output VT codes. Also remove the version block for using the native console and just rely on supported flags being present. Signed-off-by: John Starks --- api/client/cli.go | 32 +++-- pkg/term/term.go | 10 +- pkg/term/term_windows.go | 229 +++++++++++--------------------- pkg/term/windows/ansi_reader.go | 5 +- pkg/term/windows/ansi_writer.go | 5 +- pkg/term/windows/console.go | 62 --------- 6 files changed, 118 insertions(+), 225 deletions(-) diff --git a/api/client/cli.go b/api/client/cli.go index b2c06d0671..1732c518c0 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -47,8 +47,10 @@ type DockerCli struct { isTerminalOut bool // client is the http client that performs all API operations client client.APIClient - // state holds the terminal state - state *term.State + // state holds the terminal input state + inState *term.State + // outState holds the terminal output state + outState *term.State } // Initialize calls the init function that will setup the configuration for the client @@ -124,19 +126,31 @@ func (cli *DockerCli) ImagesFormat() string { } func (cli *DockerCli) setRawTerminal() error { - if cli.isTerminalIn && os.Getenv("NORAW") == "" { - state, err := term.SetRawTerminal(cli.inFd) - if err != nil { - return err + if os.Getenv("NORAW") == "" { + if cli.isTerminalIn { + state, err := term.SetRawTerminal(cli.inFd) + if err != nil { + return err + } + cli.inState = state + } + if cli.isTerminalOut { + state, err := term.SetRawTerminalOutput(cli.outFd) + if err != nil { + return err + } + cli.outState = state } - cli.state = state } return nil } func (cli *DockerCli) restoreTerminal(in io.Closer) error { - if cli.state != nil { - term.RestoreTerminal(cli.inFd, cli.state) + if cli.inState != nil { + term.RestoreTerminal(cli.inFd, cli.inState) + } + if cli.outState != nil { + term.RestoreTerminal(cli.outFd, cli.outState) } // WARNING: DO NOT REMOVE THE OS CHECK !!! // For some reason this Close call blocks on darwin.. diff --git a/pkg/term/term.go b/pkg/term/term.go index 8f554847f0..1609a900a9 100644 --- a/pkg/term/term.go +++ b/pkg/term/term.go @@ -88,7 +88,8 @@ func DisableEcho(fd uintptr, state *State) error { } // SetRawTerminal puts the terminal connected to the given file descriptor into -// raw mode and returns the previous state. +// raw mode and returns the previous state. On UNIX, this puts both the input +// and output into raw mode. On Windows, it only puts the input into raw mode. func SetRawTerminal(fd uintptr) (*State, error) { oldState, err := MakeRaw(fd) if err != nil { @@ -98,6 +99,13 @@ func SetRawTerminal(fd uintptr) (*State, error) { return oldState, err } +// SetRawTerminalOutput puts the output of terminal connected to the given file +// descriptor into raw mode. On UNIX, this does nothing and returns nil for the +// state. On Windows, it disables LF -> CRLF translation. +func SetRawTerminalOutput(fd uintptr) (*State, error) { + return nil, nil +} + func handleInterrupt(fd uintptr, state *State) { sigchan := make(chan os.Signal, 1) signal.Notify(sigchan, os.Interrupt) diff --git a/pkg/term/term_windows.go b/pkg/term/term_windows.go index 9bc52a8c65..dbe4678840 100644 --- a/pkg/term/term_windows.go +++ b/pkg/term/term_windows.go @@ -9,14 +9,12 @@ import ( "syscall" "github.com/Azure/go-ansiterm/winterm" - "github.com/docker/docker/pkg/system" "github.com/docker/docker/pkg/term/windows" ) // State holds the console mode for the terminal. type State struct { - inMode, outMode uint32 - inHandle, outHandle syscall.Handle + mode uint32 } // Winsize is used for window size. @@ -32,143 +30,70 @@ const ( disableNewlineAutoReturn = 0x0008 ) -// usingNativeConsole is true if we are using the Windows native console -var usingNativeConsole bool +// vtInputSupported is true if enableVirtualTerminalInput is supported by the console +var vtInputSupported bool // StdStreams returns the standard streams (stdin, stdout, stedrr). func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { - switch { - case os.Getenv("ConEmuANSI") == "ON": + // Turn on VT handling on all std handles, if possible. This might + // fail, in which case we will fall back to terminal emulation. + var emulateStdin, emulateStdout, emulateStderr bool + fd := os.Stdin.Fd() + if mode, err := winterm.GetConsoleMode(fd); err == nil { + // Validate that enableVirtualTerminalInput is supported, but do not set it. + if err = winterm.SetConsoleMode(fd, mode|enableVirtualTerminalInput); err != nil { + emulateStdin = true + } else { + winterm.SetConsoleMode(fd, mode) + vtInputSupported = true + } + } + + fd = os.Stdout.Fd() + if mode, err := winterm.GetConsoleMode(fd); err == nil { + // Validate disableNewlineAutoReturn is supported, but do not set it. + if err = winterm.SetConsoleMode(fd, mode|enableVirtualTerminalProcessing|disableNewlineAutoReturn); err != nil { + emulateStdout = true + } else { + winterm.SetConsoleMode(fd, mode|enableVirtualTerminalProcessing) + } + } + + fd = os.Stderr.Fd() + if mode, err := winterm.GetConsoleMode(fd); err == nil { + // Validate disableNewlineAutoReturn is supported, but do not set it. + if err = winterm.SetConsoleMode(fd, mode|enableVirtualTerminalProcessing|disableNewlineAutoReturn); err != nil { + emulateStderr = true + } else { + winterm.SetConsoleMode(fd, mode|enableVirtualTerminalProcessing) + } + } + + if os.Getenv("ConEmuANSI") == "ON" { // The ConEmu terminal emulates ANSI on output streams well. - return windows.ConEmuStreams() - case os.Getenv("MSYSTEM") != "": - // MSYS (mingw) does not emulate ANSI well. - return windows.ConsoleStreams() - default: - if useNativeConsole() { - usingNativeConsole = true - return os.Stdin, os.Stdout, os.Stderr - } - return windows.ConsoleStreams() - } -} - -// useNativeConsole determines if the docker client should use the built-in -// console which supports ANSI emulation, or fall-back to the golang emulator -// (github.com/azure/go-ansiterm). -func useNativeConsole() bool { - osv := system.GetOSVersion() - - // Native console is not available before major version 10 - if osv.MajorVersion < 10 { - return false + emulateStdout = false + emulateStderr = false } - // Get the console modes. If this fails, we can't use the native console - state, err := getNativeConsole() - if err != nil { - return false + if emulateStdin { + stdIn = windows.NewAnsiReader(syscall.STD_INPUT_HANDLE) + } else { + stdIn = os.Stdin } - // Probe the console to see if it can be enabled. - if nil != probeNativeConsole(state) { - return false + if emulateStdout { + stdOut = windows.NewAnsiWriter(syscall.STD_OUTPUT_HANDLE) + } else { + stdOut = os.Stdout } - // Environment variable override - if e := os.Getenv("USE_NATIVE_CONSOLE"); e != "" { - if e == "1" { - return true - } - return false + if emulateStderr { + stdErr = windows.NewAnsiWriter(syscall.STD_ERROR_HANDLE) + } else { + stdErr = os.Stderr } - // Must have a post-TP5 RS1 build of Windows Server 2016/Windows 10 for - // the native console to be usable. - if osv.Build < 14350 { - return false - } - - return true -} - -// getNativeConsole returns the console modes ('state') for the native Windows console -func getNativeConsole() (State, error) { - var ( - err error - state State - ) - - // Get the handle to stdout - if state.outHandle, err = syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE); err != nil { - return state, err - } - - // Get the console mode from the consoles stdout handle - if err = syscall.GetConsoleMode(state.outHandle, &state.outMode); err != nil { - return state, err - } - - // Get the handle to stdin - if state.inHandle, err = syscall.GetStdHandle(syscall.STD_INPUT_HANDLE); err != nil { - return state, err - } - - // Get the console mode from the consoles stdin handle - if err = syscall.GetConsoleMode(state.inHandle, &state.inMode); err != nil { - return state, err - } - - return state, nil -} - -// probeNativeConsole probes the console to determine if native can be supported, -func probeNativeConsole(state State) error { - if err := winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode|enableVirtualTerminalProcessing); err != nil { - return err - } - defer winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode) - - if err := winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode|enableVirtualTerminalInput); err != nil { - return err - } - defer winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode) - - return nil -} - -// enableNativeConsole turns on native console mode -func enableNativeConsole(state State) error { - // First attempt both enableVirtualTerminalProcessing and disableNewlineAutoReturn - if err := winterm.SetConsoleMode(uintptr(state.outHandle), - state.outMode|(enableVirtualTerminalProcessing|disableNewlineAutoReturn)); err != nil { - - // That may fail, so fallback to trying just enableVirtualTerminalProcessing - if err := winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode|enableVirtualTerminalProcessing); err != nil { - return err - } - } - - if err := winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode|enableVirtualTerminalInput); err != nil { - winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode) // restore out if we can - return err - } - - return nil -} - -// disableNativeConsole turns off native console mode -func disableNativeConsole(state *State) error { - // Try and restore both in an out before error checking. - errout := winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode) - errin := winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode) - if errout != nil { - return errout - } - if errin != nil { - return errin - } - return nil + return } // GetFdInfo returns the file descriptor for an os.File and indicates whether the file represents a terminal. @@ -199,34 +124,23 @@ func IsTerminal(fd uintptr) bool { // RestoreTerminal restores the terminal connected to the given file descriptor // to a previous state. func RestoreTerminal(fd uintptr, state *State) error { - if usingNativeConsole { - return disableNativeConsole(state) - } - return winterm.SetConsoleMode(fd, state.outMode) + return winterm.SetConsoleMode(fd, state.mode) } // SaveState saves the state of the terminal connected to the given file descriptor. func SaveState(fd uintptr) (*State, error) { - if usingNativeConsole { - state, err := getNativeConsole() - if err != nil { - return nil, err - } - return &state, nil - } - mode, e := winterm.GetConsoleMode(fd) if e != nil { return nil, e } - return &State{outMode: mode}, nil + return &State{mode: mode}, nil } // DisableEcho disables echo for the terminal connected to the given file descriptor. // -- See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx func DisableEcho(fd uintptr, state *State) error { - mode := state.inMode + mode := state.mode mode &^= winterm.ENABLE_ECHO_INPUT mode |= winterm.ENABLE_PROCESSED_INPUT | winterm.ENABLE_LINE_INPUT err := winterm.SetConsoleMode(fd, mode) @@ -239,8 +153,9 @@ func DisableEcho(fd uintptr, state *State) error { return nil } -// SetRawTerminal puts the terminal connected to the given file descriptor into raw -// mode and returns the previous state. +// SetRawTerminal puts the terminal connected to the given file descriptor into +// raw mode and returns the previous state. On UNIX, this puts both the input +// and output into raw mode. On Windows, it only puts the input into raw mode. func SetRawTerminal(fd uintptr) (*State, error) { state, err := MakeRaw(fd) if err != nil { @@ -252,6 +167,21 @@ func SetRawTerminal(fd uintptr) (*State, error) { return state, err } +// SetRawTerminalOutput puts the output of terminal connected to the given file +// descriptor into raw mode. On UNIX, this does nothing and returns nil for the +// state. On Windows, it disables LF -> CRLF translation. +func SetRawTerminalOutput(fd uintptr) (*State, error) { + state, err := SaveState(fd) + if err != nil { + return nil, err + } + + // Ignore failures, since disableNewlineAutoReturn might not be supported on this + // version of Windows. + winterm.SetConsoleMode(fd, state.mode|disableNewlineAutoReturn) + return state, err +} + // MakeRaw puts the terminal (Windows Console) connected to the given file descriptor into raw // mode and returns the previous state of the terminal so that it can be restored. func MakeRaw(fd uintptr) (*State, error) { @@ -260,13 +190,7 @@ func MakeRaw(fd uintptr) (*State, error) { return nil, err } - mode := state.inMode - if usingNativeConsole { - if err := enableNativeConsole(*state); err != nil { - return nil, err - } - mode |= enableVirtualTerminalInput - } + mode := state.mode // See // -- https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx @@ -283,6 +207,9 @@ func MakeRaw(fd uintptr) (*State, error) { mode |= winterm.ENABLE_EXTENDED_FLAGS mode |= winterm.ENABLE_INSERT_MODE mode |= winterm.ENABLE_QUICK_EDIT_MODE + if vtInputSupported { + mode |= enableVirtualTerminalInput + } err = winterm.SetConsoleMode(fd, mode) if err != nil { diff --git a/pkg/term/windows/ansi_reader.go b/pkg/term/windows/ansi_reader.go index 1db920db12..58452ad786 100644 --- a/pkg/term/windows/ansi_reader.go +++ b/pkg/term/windows/ansi_reader.go @@ -6,6 +6,7 @@ import ( "bytes" "errors" "fmt" + "io" "os" "strings" "unsafe" @@ -27,7 +28,9 @@ type ansiReader struct { command []byte } -func newAnsiReader(nFile int) *ansiReader { +// NewAnsiReader returns an io.ReadCloser that provides VT100 terminal emulation on top of a +// Windows console input handle. +func NewAnsiReader(nFile int) io.ReadCloser { initLogger() file, fd := winterm.GetStdFile(nFile) return &ansiReader{ diff --git a/pkg/term/windows/ansi_writer.go b/pkg/term/windows/ansi_writer.go index 0299a8842e..a3ce5697d9 100644 --- a/pkg/term/windows/ansi_writer.go +++ b/pkg/term/windows/ansi_writer.go @@ -3,6 +3,7 @@ package windows import ( + "io" "os" ansiterm "github.com/Azure/go-ansiterm" @@ -20,7 +21,9 @@ type ansiWriter struct { parser *ansiterm.AnsiParser } -func newAnsiWriter(nFile int) *ansiWriter { +// NewAnsiWriter returns an io.Writer that provides VT100 terminal emulation on top of a +// Windows console output handle. +func NewAnsiWriter(nFile int) io.Writer { initLogger() file, fd := winterm.GetStdFile(nFile) info, err := winterm.GetConsoleScreenBufferInfo(fd) diff --git a/pkg/term/windows/console.go b/pkg/term/windows/console.go index 3036a04605..ca5c3b2e53 100644 --- a/pkg/term/windows/console.go +++ b/pkg/term/windows/console.go @@ -3,73 +3,11 @@ package windows import ( - "io" "os" - "syscall" "github.com/Azure/go-ansiterm/winterm" - - ansiterm "github.com/Azure/go-ansiterm" - "github.com/Sirupsen/logrus" - "io/ioutil" ) -// ConEmuStreams returns prepared versions of console streams, -// for proper use in ConEmu terminal. -// The ConEmu terminal emulates ANSI on output streams well by default. -func ConEmuStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { - if IsConsole(os.Stdin.Fd()) { - stdIn = newAnsiReader(syscall.STD_INPUT_HANDLE) - } else { - stdIn = os.Stdin - } - - stdOut = os.Stdout - stdErr = os.Stderr - - // WARNING (BEGIN): sourced from newAnsiWriter - - logFile := ioutil.Discard - - if isDebugEnv := os.Getenv(ansiterm.LogEnv); isDebugEnv == "1" { - logFile, _ = os.Create("ansiReaderWriter.log") - } - - logger = &logrus.Logger{ - Out: logFile, - Formatter: new(logrus.TextFormatter), - Level: logrus.DebugLevel, - } - - // WARNING (END): sourced from newAnsiWriter - - return stdIn, stdOut, stdErr -} - -// ConsoleStreams returns a wrapped version for each standard stream referencing a console, -// that handles ANSI character sequences. -func ConsoleStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { - if IsConsole(os.Stdin.Fd()) { - stdIn = newAnsiReader(syscall.STD_INPUT_HANDLE) - } else { - stdIn = os.Stdin - } - - if IsConsole(os.Stdout.Fd()) { - stdOut = newAnsiWriter(syscall.STD_OUTPUT_HANDLE) - } else { - stdOut = os.Stdout - } - - if IsConsole(os.Stderr.Fd()) { - stdErr = newAnsiWriter(syscall.STD_ERROR_HANDLE) - } else { - stdErr = os.Stderr - } - - return stdIn, stdOut, stdErr -} - // GetHandleInfo returns file descriptor and bool indicating whether the file is a console. func GetHandleInfo(in interface{}) (uintptr, bool) { switch t := in.(type) {