Browse code

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>

John Starks authored on 2016/06/23 08:34:01
Showing 6 changed files
... ...
@@ -47,8 +47,10 @@ type DockerCli struct {
47 47
 	isTerminalOut bool
48 48
 	// client is the http client that performs all API operations
49 49
 	client client.APIClient
50
-	// state holds the terminal state
51
-	state *term.State
50
+	// state holds the terminal input state
51
+	inState *term.State
52
+	// outState holds the terminal output state
53
+	outState *term.State
52 54
 }
53 55
 
54 56
 // Initialize calls the init function that will setup the configuration for the client
... ...
@@ -124,19 +126,31 @@ func (cli *DockerCli) ImagesFormat() string {
124 124
 }
125 125
 
126 126
 func (cli *DockerCli) setRawTerminal() error {
127
-	if cli.isTerminalIn && os.Getenv("NORAW") == "" {
128
-		state, err := term.SetRawTerminal(cli.inFd)
129
-		if err != nil {
130
-			return err
127
+	if os.Getenv("NORAW") == "" {
128
+		if cli.isTerminalIn {
129
+			state, err := term.SetRawTerminal(cli.inFd)
130
+			if err != nil {
131
+				return err
132
+			}
133
+			cli.inState = state
134
+		}
135
+		if cli.isTerminalOut {
136
+			state, err := term.SetRawTerminalOutput(cli.outFd)
137
+			if err != nil {
138
+				return err
139
+			}
140
+			cli.outState = state
131 141
 		}
132
-		cli.state = state
133 142
 	}
134 143
 	return nil
135 144
 }
136 145
 
137 146
 func (cli *DockerCli) restoreTerminal(in io.Closer) error {
138
-	if cli.state != nil {
139
-		term.RestoreTerminal(cli.inFd, cli.state)
147
+	if cli.inState != nil {
148
+		term.RestoreTerminal(cli.inFd, cli.inState)
149
+	}
150
+	if cli.outState != nil {
151
+		term.RestoreTerminal(cli.outFd, cli.outState)
140 152
 	}
141 153
 	// WARNING: DO NOT REMOVE THE OS CHECK !!!
142 154
 	// For some reason this Close call blocks on darwin..
... ...
@@ -88,7 +88,8 @@ func DisableEcho(fd uintptr, state *State) error {
88 88
 }
89 89
 
90 90
 // SetRawTerminal puts the terminal connected to the given file descriptor into
91
-// raw mode and returns the previous state.
91
+// raw mode and returns the previous state. On UNIX, this puts both the input
92
+// and output into raw mode. On Windows, it only puts the input into raw mode.
92 93
 func SetRawTerminal(fd uintptr) (*State, error) {
93 94
 	oldState, err := MakeRaw(fd)
94 95
 	if err != nil {
... ...
@@ -98,6 +99,13 @@ func SetRawTerminal(fd uintptr) (*State, error) {
98 98
 	return oldState, err
99 99
 }
100 100
 
101
+// SetRawTerminalOutput puts the output of terminal connected to the given file
102
+// descriptor into raw mode. On UNIX, this does nothing and returns nil for the
103
+// state. On Windows, it disables LF -> CRLF translation.
104
+func SetRawTerminalOutput(fd uintptr) (*State, error) {
105
+	return nil, nil
106
+}
107
+
101 108
 func handleInterrupt(fd uintptr, state *State) {
102 109
 	sigchan := make(chan os.Signal, 1)
103 110
 	signal.Notify(sigchan, os.Interrupt)
... ...
@@ -9,14 +9,12 @@ import (
9 9
 	"syscall"
10 10
 
11 11
 	"github.com/Azure/go-ansiterm/winterm"
12
-	"github.com/docker/docker/pkg/system"
13 12
 	"github.com/docker/docker/pkg/term/windows"
14 13
 )
15 14
 
16 15
 // State holds the console mode for the terminal.
17 16
 type State struct {
18
-	inMode, outMode     uint32
19
-	inHandle, outHandle syscall.Handle
17
+	mode uint32
20 18
 }
21 19
 
22 20
 // Winsize is used for window size.
... ...
@@ -32,143 +30,70 @@ const (
32 32
 	disableNewlineAutoReturn        = 0x0008
33 33
 )
34 34
 
35
-// usingNativeConsole is true if we are using the Windows native console
36
-var usingNativeConsole bool
35
+// vtInputSupported is true if enableVirtualTerminalInput is supported by the console
36
+var vtInputSupported bool
37 37
 
38 38
 // StdStreams returns the standard streams (stdin, stdout, stedrr).
39 39
 func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
40
-	switch {
41
-	case os.Getenv("ConEmuANSI") == "ON":
42
-		// The ConEmu terminal emulates ANSI on output streams well.
43
-		return windows.ConEmuStreams()
44
-	case os.Getenv("MSYSTEM") != "":
45
-		// MSYS (mingw) does not emulate ANSI well.
46
-		return windows.ConsoleStreams()
47
-	default:
48
-		if useNativeConsole() {
49
-			usingNativeConsole = true
50
-			return os.Stdin, os.Stdout, os.Stderr
40
+	// Turn on VT handling on all std handles, if possible. This might
41
+	// fail, in which case we will fall back to terminal emulation.
42
+	var emulateStdin, emulateStdout, emulateStderr bool
43
+	fd := os.Stdin.Fd()
44
+	if mode, err := winterm.GetConsoleMode(fd); err == nil {
45
+		// Validate that enableVirtualTerminalInput is supported, but do not set it.
46
+		if err = winterm.SetConsoleMode(fd, mode|enableVirtualTerminalInput); err != nil {
47
+			emulateStdin = true
48
+		} else {
49
+			winterm.SetConsoleMode(fd, mode)
50
+			vtInputSupported = true
51 51
 		}
52
-		return windows.ConsoleStreams()
53
-	}
54
-}
55
-
56
-// useNativeConsole determines if the docker client should use the built-in
57
-// console which supports ANSI emulation, or fall-back to the golang emulator
58
-// (github.com/azure/go-ansiterm).
59
-func useNativeConsole() bool {
60
-	osv := system.GetOSVersion()
61
-
62
-	// Native console is not available before major version 10
63
-	if osv.MajorVersion < 10 {
64
-		return false
65
-	}
66
-
67
-	// Get the console modes. If this fails, we can't use the native console
68
-	state, err := getNativeConsole()
69
-	if err != nil {
70
-		return false
71 52
 	}
72 53
 
73
-	// Probe the console to see if it can be enabled.
74
-	if nil != probeNativeConsole(state) {
75
-		return false
76
-	}
77
-
78
-	// Environment variable override
79
-	if e := os.Getenv("USE_NATIVE_CONSOLE"); e != "" {
80
-		if e == "1" {
81
-			return true
54
+	fd = os.Stdout.Fd()
55
+	if mode, err := winterm.GetConsoleMode(fd); err == nil {
56
+		// Validate disableNewlineAutoReturn is supported, but do not set it.
57
+		if err = winterm.SetConsoleMode(fd, mode|enableVirtualTerminalProcessing|disableNewlineAutoReturn); err != nil {
58
+			emulateStdout = true
59
+		} else {
60
+			winterm.SetConsoleMode(fd, mode|enableVirtualTerminalProcessing)
82 61
 		}
83
-		return false
84
-	}
85
-
86
-	// Must have a post-TP5 RS1 build of Windows Server 2016/Windows 10 for
87
-	// the native console to be usable.
88
-	if osv.Build < 14350 {
89
-		return false
90 62
 	}
91 63
 
92
-	return true
93
-}
94
-
95
-// getNativeConsole returns the console modes ('state') for the native Windows console
96
-func getNativeConsole() (State, error) {
97
-	var (
98
-		err   error
99
-		state State
100
-	)
101
-
102
-	// Get the handle to stdout
103
-	if state.outHandle, err = syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE); err != nil {
104
-		return state, err
105
-	}
106
-
107
-	// Get the console mode from the consoles stdout handle
108
-	if err = syscall.GetConsoleMode(state.outHandle, &state.outMode); err != nil {
109
-		return state, err
64
+	fd = os.Stderr.Fd()
65
+	if mode, err := winterm.GetConsoleMode(fd); err == nil {
66
+		// Validate disableNewlineAutoReturn is supported, but do not set it.
67
+		if err = winterm.SetConsoleMode(fd, mode|enableVirtualTerminalProcessing|disableNewlineAutoReturn); err != nil {
68
+			emulateStderr = true
69
+		} else {
70
+			winterm.SetConsoleMode(fd, mode|enableVirtualTerminalProcessing)
71
+		}
110 72
 	}
111 73
 
112
-	// Get the handle to stdin
113
-	if state.inHandle, err = syscall.GetStdHandle(syscall.STD_INPUT_HANDLE); err != nil {
114
-		return state, err
74
+	if os.Getenv("ConEmuANSI") == "ON" {
75
+		// The ConEmu terminal emulates ANSI on output streams well.
76
+		emulateStdout = false
77
+		emulateStderr = false
115 78
 	}
116 79
 
117
-	// Get the console mode from the consoles stdin handle
118
-	if err = syscall.GetConsoleMode(state.inHandle, &state.inMode); err != nil {
119
-		return state, err
80
+	if emulateStdin {
81
+		stdIn = windows.NewAnsiReader(syscall.STD_INPUT_HANDLE)
82
+	} else {
83
+		stdIn = os.Stdin
120 84
 	}
121 85
 
122
-	return state, nil
123
-}
124
-
125
-// probeNativeConsole probes the console to determine if native can be supported,
126
-func probeNativeConsole(state State) error {
127
-	if err := winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode|enableVirtualTerminalProcessing); err != nil {
128
-		return err
86
+	if emulateStdout {
87
+		stdOut = windows.NewAnsiWriter(syscall.STD_OUTPUT_HANDLE)
88
+	} else {
89
+		stdOut = os.Stdout
129 90
 	}
130
-	defer winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode)
131 91
 
132
-	if err := winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode|enableVirtualTerminalInput); err != nil {
133
-		return err
92
+	if emulateStderr {
93
+		stdErr = windows.NewAnsiWriter(syscall.STD_ERROR_HANDLE)
94
+	} else {
95
+		stdErr = os.Stderr
134 96
 	}
135
-	defer winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode)
136 97
 
137
-	return nil
138
-}
139
-
140
-// enableNativeConsole turns on native console mode
141
-func enableNativeConsole(state State) error {
142
-	// First attempt both enableVirtualTerminalProcessing and disableNewlineAutoReturn
143
-	if err := winterm.SetConsoleMode(uintptr(state.outHandle),
144
-		state.outMode|(enableVirtualTerminalProcessing|disableNewlineAutoReturn)); err != nil {
145
-
146
-		// That may fail, so fallback to trying just enableVirtualTerminalProcessing
147
-		if err := winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode|enableVirtualTerminalProcessing); err != nil {
148
-			return err
149
-		}
150
-	}
151
-
152
-	if err := winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode|enableVirtualTerminalInput); err != nil {
153
-		winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode) // restore out if we can
154
-		return err
155
-	}
156
-
157
-	return nil
158
-}
159
-
160
-// disableNativeConsole turns off native console mode
161
-func disableNativeConsole(state *State) error {
162
-	// Try and restore both in an out before error checking.
163
-	errout := winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode)
164
-	errin := winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode)
165
-	if errout != nil {
166
-		return errout
167
-	}
168
-	if errin != nil {
169
-		return errin
170
-	}
171
-	return nil
98
+	return
172 99
 }
173 100
 
174 101
 // 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 {
199 199
 // RestoreTerminal restores the terminal connected to the given file descriptor
200 200
 // to a previous state.
201 201
 func RestoreTerminal(fd uintptr, state *State) error {
202
-	if usingNativeConsole {
203
-		return disableNativeConsole(state)
204
-	}
205
-	return winterm.SetConsoleMode(fd, state.outMode)
202
+	return winterm.SetConsoleMode(fd, state.mode)
206 203
 }
207 204
 
208 205
 // SaveState saves the state of the terminal connected to the given file descriptor.
209 206
 func SaveState(fd uintptr) (*State, error) {
210
-	if usingNativeConsole {
211
-		state, err := getNativeConsole()
212
-		if err != nil {
213
-			return nil, err
214
-		}
215
-		return &state, nil
216
-	}
217
-
218 207
 	mode, e := winterm.GetConsoleMode(fd)
219 208
 	if e != nil {
220 209
 		return nil, e
221 210
 	}
222 211
 
223
-	return &State{outMode: mode}, nil
212
+	return &State{mode: mode}, nil
224 213
 }
225 214
 
226 215
 // DisableEcho disables echo for the terminal connected to the given file descriptor.
227 216
 // -- See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx
228 217
 func DisableEcho(fd uintptr, state *State) error {
229
-	mode := state.inMode
218
+	mode := state.mode
230 219
 	mode &^= winterm.ENABLE_ECHO_INPUT
231 220
 	mode |= winterm.ENABLE_PROCESSED_INPUT | winterm.ENABLE_LINE_INPUT
232 221
 	err := winterm.SetConsoleMode(fd, mode)
... ...
@@ -239,8 +153,9 @@ func DisableEcho(fd uintptr, state *State) error {
239 239
 	return nil
240 240
 }
241 241
 
242
-// SetRawTerminal puts the terminal connected to the given file descriptor into raw
243
-// mode and returns the previous state.
242
+// SetRawTerminal puts the terminal connected to the given file descriptor into
243
+// raw mode and returns the previous state. On UNIX, this puts both the input
244
+// and output into raw mode. On Windows, it only puts the input into raw mode.
244 245
 func SetRawTerminal(fd uintptr) (*State, error) {
245 246
 	state, err := MakeRaw(fd)
246 247
 	if err != nil {
... ...
@@ -252,6 +167,21 @@ func SetRawTerminal(fd uintptr) (*State, error) {
252 252
 	return state, err
253 253
 }
254 254
 
255
+// SetRawTerminalOutput puts the output of terminal connected to the given file
256
+// descriptor into raw mode. On UNIX, this does nothing and returns nil for the
257
+// state. On Windows, it disables LF -> CRLF translation.
258
+func SetRawTerminalOutput(fd uintptr) (*State, error) {
259
+	state, err := SaveState(fd)
260
+	if err != nil {
261
+		return nil, err
262
+	}
263
+
264
+	// Ignore failures, since disableNewlineAutoReturn might not be supported on this
265
+	// version of Windows.
266
+	winterm.SetConsoleMode(fd, state.mode|disableNewlineAutoReturn)
267
+	return state, err
268
+}
269
+
255 270
 // MakeRaw puts the terminal (Windows Console) connected to the given file descriptor into raw
256 271
 // mode and returns the previous state of the terminal so that it can be restored.
257 272
 func MakeRaw(fd uintptr) (*State, error) {
... ...
@@ -260,13 +190,7 @@ func MakeRaw(fd uintptr) (*State, error) {
260 260
 		return nil, err
261 261
 	}
262 262
 
263
-	mode := state.inMode
264
-	if usingNativeConsole {
265
-		if err := enableNativeConsole(*state); err != nil {
266
-			return nil, err
267
-		}
268
-		mode |= enableVirtualTerminalInput
269
-	}
263
+	mode := state.mode
270 264
 
271 265
 	// See
272 266
 	// -- https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx
... ...
@@ -283,6 +207,9 @@ func MakeRaw(fd uintptr) (*State, error) {
283 283
 	mode |= winterm.ENABLE_EXTENDED_FLAGS
284 284
 	mode |= winterm.ENABLE_INSERT_MODE
285 285
 	mode |= winterm.ENABLE_QUICK_EDIT_MODE
286
+	if vtInputSupported {
287
+		mode |= enableVirtualTerminalInput
288
+	}
286 289
 
287 290
 	err = winterm.SetConsoleMode(fd, mode)
288 291
 	if err != nil {
... ...
@@ -6,6 +6,7 @@ import (
6 6
 	"bytes"
7 7
 	"errors"
8 8
 	"fmt"
9
+	"io"
9 10
 	"os"
10 11
 	"strings"
11 12
 	"unsafe"
... ...
@@ -27,7 +28,9 @@ type ansiReader struct {
27 27
 	command  []byte
28 28
 }
29 29
 
30
-func newAnsiReader(nFile int) *ansiReader {
30
+// NewAnsiReader returns an io.ReadCloser that provides VT100 terminal emulation on top of a
31
+// Windows console input handle.
32
+func NewAnsiReader(nFile int) io.ReadCloser {
31 33
 	initLogger()
32 34
 	file, fd := winterm.GetStdFile(nFile)
33 35
 	return &ansiReader{
... ...
@@ -3,6 +3,7 @@
3 3
 package windows
4 4
 
5 5
 import (
6
+	"io"
6 7
 	"os"
7 8
 
8 9
 	ansiterm "github.com/Azure/go-ansiterm"
... ...
@@ -20,7 +21,9 @@ type ansiWriter struct {
20 20
 	parser         *ansiterm.AnsiParser
21 21
 }
22 22
 
23
-func newAnsiWriter(nFile int) *ansiWriter {
23
+// NewAnsiWriter returns an io.Writer that provides VT100 terminal emulation on top of a
24
+// Windows console output handle.
25
+func NewAnsiWriter(nFile int) io.Writer {
24 26
 	initLogger()
25 27
 	file, fd := winterm.GetStdFile(nFile)
26 28
 	info, err := winterm.GetConsoleScreenBufferInfo(fd)
... ...
@@ -3,73 +3,11 @@
3 3
 package windows
4 4
 
5 5
 import (
6
-	"io"
7 6
 	"os"
8
-	"syscall"
9 7
 
10 8
 	"github.com/Azure/go-ansiterm/winterm"
11
-
12
-	ansiterm "github.com/Azure/go-ansiterm"
13
-	"github.com/Sirupsen/logrus"
14
-	"io/ioutil"
15 9
 )
16 10
 
17
-// ConEmuStreams returns prepared versions of console streams,
18
-// for proper use in ConEmu terminal.
19
-// The ConEmu terminal emulates ANSI on output streams well by default.
20
-func ConEmuStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
21
-	if IsConsole(os.Stdin.Fd()) {
22
-		stdIn = newAnsiReader(syscall.STD_INPUT_HANDLE)
23
-	} else {
24
-		stdIn = os.Stdin
25
-	}
26
-
27
-	stdOut = os.Stdout
28
-	stdErr = os.Stderr
29
-
30
-	// WARNING (BEGIN): sourced from newAnsiWriter
31
-
32
-	logFile := ioutil.Discard
33
-
34
-	if isDebugEnv := os.Getenv(ansiterm.LogEnv); isDebugEnv == "1" {
35
-		logFile, _ = os.Create("ansiReaderWriter.log")
36
-	}
37
-
38
-	logger = &logrus.Logger{
39
-		Out:       logFile,
40
-		Formatter: new(logrus.TextFormatter),
41
-		Level:     logrus.DebugLevel,
42
-	}
43
-
44
-	// WARNING (END): sourced from newAnsiWriter
45
-
46
-	return stdIn, stdOut, stdErr
47
-}
48
-
49
-// ConsoleStreams returns a wrapped version for each standard stream referencing a console,
50
-// that handles ANSI character sequences.
51
-func ConsoleStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
52
-	if IsConsole(os.Stdin.Fd()) {
53
-		stdIn = newAnsiReader(syscall.STD_INPUT_HANDLE)
54
-	} else {
55
-		stdIn = os.Stdin
56
-	}
57
-
58
-	if IsConsole(os.Stdout.Fd()) {
59
-		stdOut = newAnsiWriter(syscall.STD_OUTPUT_HANDLE)
60
-	} else {
61
-		stdOut = os.Stdout
62
-	}
63
-
64
-	if IsConsole(os.Stderr.Fd()) {
65
-		stdErr = newAnsiWriter(syscall.STD_ERROR_HANDLE)
66
-	} else {
67
-		stdErr = os.Stderr
68
-	}
69
-
70
-	return stdIn, stdOut, stdErr
71
-}
72
-
73 11
 // GetHandleInfo returns file descriptor and bool indicating whether the file is a console.
74 12
 func GetHandleInfo(in interface{}) (uintptr, bool) {
75 13
 	switch t := in.(type) {