Browse code

Windows: VirtualTerminalInput native console

Signed-off-by: John Howard <jhoward@microsoft.com>

John Howard authored on 2015/11/12 08:04:40
Showing 1 changed files
... ...
@@ -15,7 +15,8 @@ import (
15 15
 
16 16
 // State holds the console mode for the terminal.
17 17
 type State struct {
18
-	mode uint32
18
+	inMode, outMode     uint32
19
+	inHandle, outHandle syscall.Handle
19 20
 }
20 21
 
21 22
 // Winsize is used for window size.
... ...
@@ -26,6 +27,15 @@ type Winsize struct {
26 26
 	y      uint16
27 27
 }
28 28
 
29
+const (
30
+	// https://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx
31
+	enableVirtualTerminalInput      = 0x0200
32
+	enableVirtualTerminalProcessing = 0x0004
33
+)
34
+
35
+// usingNativeConsole is true if we are using the Windows native console
36
+var usingNativeConsole bool
37
+
29 38
 // StdStreams returns the standard streams (stdin, stdout, stedrr).
30 39
 func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
31 40
 	switch {
... ...
@@ -37,6 +47,7 @@ func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
37 37
 		return windows.ConsoleStreams()
38 38
 	default:
39 39
 		if useNativeConsole() {
40
+			usingNativeConsole = true
40 41
 			return os.Stdin, os.Stdout, os.Stderr
41 42
 		}
42 43
 		return windows.ConsoleStreams()
... ...
@@ -52,7 +63,7 @@ func useNativeConsole() bool {
52 52
 		return false
53 53
 	}
54 54
 
55
-	// Native console is not available major version 10
55
+	// Native console is not available before major version 10
56 56
 	if osv.MajorVersion < 10 {
57 57
 		return false
58 58
 	}
... ...
@@ -62,6 +73,17 @@ func useNativeConsole() bool {
62 62
 		return false
63 63
 	}
64 64
 
65
+	// Get the console modes. If this fails, we can't use the native console
66
+	state, err := getNativeConsole()
67
+	if err != nil {
68
+		return false
69
+	}
70
+
71
+	// Probe the console to see if it can be enabled.
72
+	if nil != probeNativeConsole(state) {
73
+		return false
74
+	}
75
+
65 76
 	// Environment variable override
66 77
 	if e := os.Getenv("USE_NATIVE_CONSOLE"); e != "" {
67 78
 		if e == "1" {
... ...
@@ -70,30 +92,84 @@ func useNativeConsole() bool {
70 70
 		return false
71 71
 	}
72 72
 
73
+	// TODO Windows. The native emulator still has issues which
74
+	// mean it shouldn't be enabled for everyone. Change this next line to true
75
+	// to change the default to "enable if available". In the meantime, users
76
+	// can still try it out by using USE_NATIVE_CONSOLE env variable.
77
+	return false
78
+}
79
+
80
+// getNativeConsole returns the console modes ('state') for the native Windows console
81
+func getNativeConsole() (State, error) {
82
+	var (
83
+		err   error
84
+		state State
85
+	)
86
+
73 87
 	// Get the handle to stdout
74
-	stdOutHandle, err := syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE)
75
-	if err != nil {
76
-		return false
88
+	if state.outHandle, err = syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE); err != nil {
89
+		return state, err
77 90
 	}
78 91
 
79 92
 	// Get the console mode from the consoles stdout handle
80
-	var mode uint32
81
-	if err := syscall.GetConsoleMode(stdOutHandle, &mode); err != nil {
82
-		return false
93
+	if err = syscall.GetConsoleMode(state.outHandle, &state.outMode); err != nil {
94
+		return state, err
83 95
 	}
84 96
 
85
-	// Legacy mode does not have native ANSI emulation.
86
-	// https://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx
87
-	const enableVirtualTerminalProcessing = 0x0004
88
-	if mode&enableVirtualTerminalProcessing == 0 {
89
-		return false
97
+	// Get the handle to stdin
98
+	if state.inHandle, err = syscall.GetStdHandle(syscall.STD_INPUT_HANDLE); err != nil {
99
+		return state, err
90 100
 	}
91 101
 
92
-	// TODO Windows (Post TP4). The native emulator still has issues which
93
-	// mean it shouldn't be enabled for everyone. Change this next line to true
94
-	// to change the default to "enable if available". In the meantime, users
95
-	// can still try it out by using USE_NATIVE_CONSOLE env variable.
96
-	return false
102
+	// Get the console mode from the consoles stdin handle
103
+	if err = syscall.GetConsoleMode(state.inHandle, &state.inMode); err != nil {
104
+		return state, err
105
+	}
106
+
107
+	return state, nil
108
+}
109
+
110
+// probeNativeConsole probes the console to determine if native can be supported,
111
+func probeNativeConsole(state State) error {
112
+	if err := winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode|enableVirtualTerminalProcessing); err != nil {
113
+		return err
114
+	}
115
+	defer winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode)
116
+
117
+	if err := winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode|enableVirtualTerminalInput); err != nil {
118
+		return err
119
+	}
120
+	defer winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode)
121
+
122
+	return nil
123
+}
124
+
125
+// enableNativeConsole turns on native console mode
126
+func enableNativeConsole(state State) error {
127
+	if err := winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode|enableVirtualTerminalProcessing); err != nil {
128
+		return err
129
+	}
130
+
131
+	if err := winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode|enableVirtualTerminalInput); err != nil {
132
+		winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode) // restore out if we can
133
+		return err
134
+	}
135
+
136
+	return nil
137
+}
138
+
139
+// disableNativeConsole turns off native console mode
140
+func disableNativeConsole(state *State) error {
141
+	// Try and restore both in an out before error checking.
142
+	errout := winterm.SetConsoleMode(uintptr(state.outHandle), state.outMode)
143
+	errin := winterm.SetConsoleMode(uintptr(state.inHandle), state.inMode)
144
+	if errout != nil {
145
+		return errout
146
+	}
147
+	if errin != nil {
148
+		return errin
149
+	}
150
+	return nil
97 151
 }
98 152
 
99 153
 // GetFdInfo returns the file descriptor for an os.File and indicates whether the file represents a terminal.
... ...
@@ -103,7 +179,6 @@ func GetFdInfo(in interface{}) (uintptr, bool) {
103 103
 
104 104
 // GetWinsize returns the window size based on the specified file descriptor.
105 105
 func GetWinsize(fd uintptr) (*Winsize, error) {
106
-
107 106
 	info, err := winterm.GetConsoleScreenBufferInfo(fd)
108 107
 	if err != nil {
109 108
 		return nil, err
... ...
@@ -115,9 +190,6 @@ func GetWinsize(fd uintptr) (*Winsize, error) {
115 115
 		x:      0,
116 116
 		y:      0}
117 117
 
118
-	// Note: GetWinsize is called frequently -- uncomment only for excessive details
119
-	// logrus.Debugf("[windows] GetWinsize: Console(%v)", info.String())
120
-	// logrus.Debugf("[windows] GetWinsize: Width(%v), Height(%v), x(%v), y(%v)", winsize.Width, winsize.Height, winsize.x, winsize.y)
121 118
 	return winsize, nil
122 119
 }
123 120
 
... ...
@@ -129,25 +201,36 @@ func IsTerminal(fd uintptr) bool {
129 129
 // RestoreTerminal restores the terminal connected to the given file descriptor
130 130
 // to a previous state.
131 131
 func RestoreTerminal(fd uintptr, state *State) error {
132
-	return winterm.SetConsoleMode(fd, state.mode)
132
+	if usingNativeConsole {
133
+		return disableNativeConsole(state)
134
+	}
135
+	return winterm.SetConsoleMode(fd, state.outMode)
133 136
 }
134 137
 
135 138
 // SaveState saves the state of the terminal connected to the given file descriptor.
136 139
 func SaveState(fd uintptr) (*State, error) {
140
+	if usingNativeConsole {
141
+		state, err := getNativeConsole()
142
+		if err != nil {
143
+			return nil, err
144
+		}
145
+		return &state, nil
146
+	}
147
+
137 148
 	mode, e := winterm.GetConsoleMode(fd)
138 149
 	if e != nil {
139 150
 		return nil, e
140 151
 	}
141
-	return &State{mode}, nil
152
+
153
+	return &State{outMode: mode}, nil
142 154
 }
143 155
 
144 156
 // DisableEcho disables echo for the terminal connected to the given file descriptor.
145 157
 // -- See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx
146 158
 func DisableEcho(fd uintptr, state *State) error {
147
-	mode := state.mode
159
+	mode := state.inMode
148 160
 	mode &^= winterm.ENABLE_ECHO_INPUT
149 161
 	mode |= winterm.ENABLE_PROCESSED_INPUT | winterm.ENABLE_LINE_INPUT
150
-
151 162
 	err := winterm.SetConsoleMode(fd, mode)
152 163
 	if err != nil {
153 164
 		return err
... ...
@@ -179,10 +262,17 @@ func MakeRaw(fd uintptr) (*State, error) {
179 179
 		return nil, err
180 180
 	}
181 181
 
182
+	mode := state.inMode
183
+	if usingNativeConsole {
184
+		if err := enableNativeConsole(*state); err != nil {
185
+			return nil, err
186
+		}
187
+		mode |= enableVirtualTerminalInput
188
+	}
189
+
182 190
 	// See
183 191
 	// -- https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx
184 192
 	// -- https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx
185
-	mode := state.mode
186 193
 
187 194
 	// Disable these modes
188 195
 	mode &^= winterm.ENABLE_ECHO_INPUT