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>
This commit is contained in:
John Starks 2016-06-22 16:34:01 -07:00
parent 138f9538f3
commit 4acc2c7499
6 changed files with 118 additions and 225 deletions

View file

@ -47,8 +47,10 @@ type DockerCli struct {
isTerminalOut bool isTerminalOut bool
// client is the http client that performs all API operations // client is the http client that performs all API operations
client client.APIClient client client.APIClient
// state holds the terminal state // state holds the terminal input state
state *term.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 // 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 { func (cli *DockerCli) setRawTerminal() error {
if cli.isTerminalIn && os.Getenv("NORAW") == "" { if os.Getenv("NORAW") == "" {
state, err := term.SetRawTerminal(cli.inFd) if cli.isTerminalIn {
if err != nil { state, err := term.SetRawTerminal(cli.inFd)
return err 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 return nil
} }
func (cli *DockerCli) restoreTerminal(in io.Closer) error { func (cli *DockerCli) restoreTerminal(in io.Closer) error {
if cli.state != nil { if cli.inState != nil {
term.RestoreTerminal(cli.inFd, cli.state) term.RestoreTerminal(cli.inFd, cli.inState)
}
if cli.outState != nil {
term.RestoreTerminal(cli.outFd, cli.outState)
} }
// WARNING: DO NOT REMOVE THE OS CHECK !!! // WARNING: DO NOT REMOVE THE OS CHECK !!!
// For some reason this Close call blocks on darwin.. // For some reason this Close call blocks on darwin..

View file

@ -88,7 +88,8 @@ func DisableEcho(fd uintptr, state *State) error {
} }
// SetRawTerminal puts the terminal connected to the given file descriptor into // 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) { func SetRawTerminal(fd uintptr) (*State, error) {
oldState, err := MakeRaw(fd) oldState, err := MakeRaw(fd)
if err != nil { if err != nil {
@ -98,6 +99,13 @@ func SetRawTerminal(fd uintptr) (*State, error) {
return oldState, err 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) { func handleInterrupt(fd uintptr, state *State) {
sigchan := make(chan os.Signal, 1) sigchan := make(chan os.Signal, 1)
signal.Notify(sigchan, os.Interrupt) signal.Notify(sigchan, os.Interrupt)

View file

@ -9,14 +9,12 @@ import (
"syscall" "syscall"
"github.com/Azure/go-ansiterm/winterm" "github.com/Azure/go-ansiterm/winterm"
"github.com/docker/docker/pkg/system"
"github.com/docker/docker/pkg/term/windows" "github.com/docker/docker/pkg/term/windows"
) )
// State holds the console mode for the terminal. // State holds the console mode for the terminal.
type State struct { type State struct {
inMode, outMode uint32 mode uint32
inHandle, outHandle syscall.Handle
} }
// Winsize is used for window size. // Winsize is used for window size.
@ -32,143 +30,70 @@ const (
disableNewlineAutoReturn = 0x0008 disableNewlineAutoReturn = 0x0008
) )
// usingNativeConsole is true if we are using the Windows native console // vtInputSupported is true if enableVirtualTerminalInput is supported by the console
var usingNativeConsole bool var vtInputSupported bool
// StdStreams returns the standard streams (stdin, stdout, stedrr). // StdStreams returns the standard streams (stdin, stdout, stedrr).
func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) { func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
switch { // Turn on VT handling on all std handles, if possible. This might
case os.Getenv("ConEmuANSI") == "ON": // 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. // The ConEmu terminal emulates ANSI on output streams well.
return windows.ConEmuStreams() emulateStdout = false
case os.Getenv("MSYSTEM") != "": emulateStderr = false
// 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
} }
// Get the console modes. If this fails, we can't use the native console if emulateStdin {
state, err := getNativeConsole() stdIn = windows.NewAnsiReader(syscall.STD_INPUT_HANDLE)
if err != nil { } else {
return false stdIn = os.Stdin
} }
// Probe the console to see if it can be enabled. if emulateStdout {
if nil != probeNativeConsole(state) { stdOut = windows.NewAnsiWriter(syscall.STD_OUTPUT_HANDLE)
return false } else {
stdOut = os.Stdout
} }
// Environment variable override if emulateStderr {
if e := os.Getenv("USE_NATIVE_CONSOLE"); e != "" { stdErr = windows.NewAnsiWriter(syscall.STD_ERROR_HANDLE)
if e == "1" { } else {
return true stdErr = os.Stderr
}
return false
} }
// Must have a post-TP5 RS1 build of Windows Server 2016/Windows 10 for return
// 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
} }
// GetFdInfo returns the file descriptor for an os.File and indicates whether the file represents a terminal. // 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 // RestoreTerminal restores the terminal connected to the given file descriptor
// to a previous state. // to a previous state.
func RestoreTerminal(fd uintptr, state *State) error { func RestoreTerminal(fd uintptr, state *State) error {
if usingNativeConsole { return winterm.SetConsoleMode(fd, state.mode)
return disableNativeConsole(state)
}
return winterm.SetConsoleMode(fd, state.outMode)
} }
// SaveState saves the state of the terminal connected to the given file descriptor. // SaveState saves the state of the terminal connected to the given file descriptor.
func SaveState(fd uintptr) (*State, error) { 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) mode, e := winterm.GetConsoleMode(fd)
if e != nil { if e != nil {
return nil, e 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. // 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 // -- See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx
func DisableEcho(fd uintptr, state *State) error { func DisableEcho(fd uintptr, state *State) error {
mode := state.inMode mode := state.mode
mode &^= winterm.ENABLE_ECHO_INPUT mode &^= winterm.ENABLE_ECHO_INPUT
mode |= winterm.ENABLE_PROCESSED_INPUT | winterm.ENABLE_LINE_INPUT mode |= winterm.ENABLE_PROCESSED_INPUT | winterm.ENABLE_LINE_INPUT
err := winterm.SetConsoleMode(fd, mode) err := winterm.SetConsoleMode(fd, mode)
@ -239,8 +153,9 @@ func DisableEcho(fd uintptr, state *State) error {
return nil return nil
} }
// SetRawTerminal puts the terminal connected to the given file descriptor into raw // SetRawTerminal puts the terminal connected to the given file descriptor into
// 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) { func SetRawTerminal(fd uintptr) (*State, error) {
state, err := MakeRaw(fd) state, err := MakeRaw(fd)
if err != nil { if err != nil {
@ -252,6 +167,21 @@ func SetRawTerminal(fd uintptr) (*State, error) {
return state, err 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 // 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. // mode and returns the previous state of the terminal so that it can be restored.
func MakeRaw(fd uintptr) (*State, error) { func MakeRaw(fd uintptr) (*State, error) {
@ -260,13 +190,7 @@ func MakeRaw(fd uintptr) (*State, error) {
return nil, err return nil, err
} }
mode := state.inMode mode := state.mode
if usingNativeConsole {
if err := enableNativeConsole(*state); err != nil {
return nil, err
}
mode |= enableVirtualTerminalInput
}
// See // See
// -- https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx // -- 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_EXTENDED_FLAGS
mode |= winterm.ENABLE_INSERT_MODE mode |= winterm.ENABLE_INSERT_MODE
mode |= winterm.ENABLE_QUICK_EDIT_MODE mode |= winterm.ENABLE_QUICK_EDIT_MODE
if vtInputSupported {
mode |= enableVirtualTerminalInput
}
err = winterm.SetConsoleMode(fd, mode) err = winterm.SetConsoleMode(fd, mode)
if err != nil { if err != nil {

View file

@ -6,6 +6,7 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"strings" "strings"
"unsafe" "unsafe"
@ -27,7 +28,9 @@ type ansiReader struct {
command []byte 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() initLogger()
file, fd := winterm.GetStdFile(nFile) file, fd := winterm.GetStdFile(nFile)
return &ansiReader{ return &ansiReader{

View file

@ -3,6 +3,7 @@
package windows package windows
import ( import (
"io"
"os" "os"
ansiterm "github.com/Azure/go-ansiterm" ansiterm "github.com/Azure/go-ansiterm"
@ -20,7 +21,9 @@ type ansiWriter struct {
parser *ansiterm.AnsiParser 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() initLogger()
file, fd := winterm.GetStdFile(nFile) file, fd := winterm.GetStdFile(nFile)
info, err := winterm.GetConsoleScreenBufferInfo(fd) info, err := winterm.GetConsoleScreenBufferInfo(fd)

View file

@ -3,73 +3,11 @@
package windows package windows
import ( import (
"io"
"os" "os"
"syscall"
"github.com/Azure/go-ansiterm/winterm" "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. // GetHandleInfo returns file descriptor and bool indicating whether the file is a console.
func GetHandleInfo(in interface{}) (uintptr, bool) { func GetHandleInfo(in interface{}) (uintptr, bool) {
switch t := in.(type) { switch t := in.(type) {