瀏覽代碼

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 <jostarks@microsoft.com>
(cherry picked from commit 4acc2c7499425b9c2521977019d9fe4da73e6af8)
Signed-off-by: Tibor Vass <tibor@docker.com>
John Starks 9 年之前
父節點
當前提交
4d0f6041ad
共有 6 個文件被更改,包括 111 次插入218 次删除
  1. 23 9
      api/client/cli.go
  2. 9 1
      pkg/term/term.go
  3. 71 144
      pkg/term/term_windows.go
  4. 4 1
      pkg/term/windows/ansi_reader.go
  5. 4 1
      pkg/term/windows/ansi_writer.go
  6. 0 62
      pkg/term/windows/console.go

+ 23 - 9
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..

+ 9 - 1
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)

+ 71 - 144
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":
-		// 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
+	// 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
 		}
-		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
-	}
-
-	// Get the console modes. If this fails, we can't use the native console
-	state, err := getNativeConsole()
-	if err != nil {
-		return false
 	}
 
-	// Probe the console to see if it can be enabled.
-	if nil != probeNativeConsole(state) {
-		return false
-	}
-
-	// Environment variable override
-	if e := os.Getenv("USE_NATIVE_CONSOLE"); e != "" {
-		if e == "1" {
-			return 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)
 		}
-		return false
-	}
-
-	// 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
+	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)
+		}
 	}
 
-	// Get the handle to stdin
-	if state.inHandle, err = syscall.GetStdHandle(syscall.STD_INPUT_HANDLE); err != nil {
-		return state, err
+	if os.Getenv("ConEmuANSI") == "ON" {
+		// The ConEmu terminal emulates ANSI on output streams well.
+		emulateStdout = false
+		emulateStderr = false
 	}
 
-	// Get the console mode from the consoles stdin handle
-	if err = syscall.GetConsoleMode(state.inHandle, &state.inMode); err != nil {
-		return state, err
+	if emulateStdin {
+		stdIn = windows.NewAnsiReader(syscall.STD_INPUT_HANDLE)
+	} else {
+		stdIn = os.Stdin
 	}
 
-	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
+	if emulateStdout {
+		stdOut = windows.NewAnsiWriter(syscall.STD_OUTPUT_HANDLE)
+	} else {
+		stdOut = os.Stdout
 	}
-	defer winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode)
 
-	if err := winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode|enableVirtualTerminalInput); err != nil {
-		return err
+	if emulateStderr {
+		stdErr = windows.NewAnsiWriter(syscall.STD_ERROR_HANDLE)
+	} else {
+		stdErr = os.Stderr
 	}
-	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 {

+ 4 - 1
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{

+ 4 - 1
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)

+ 0 - 62
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) {