1/*
2   Copyright The containerd Authors.
3
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7
8       http://www.apache.org/licenses/LICENSE-2.0
9
10   Unless required by applicable law or agreed to in writing, software
11   distributed under the License is distributed on an "AS IS" BASIS,
12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   See the License for the specific language governing permissions and
14   limitations under the License.
15*/
16
17package console
18
19import (
20	"fmt"
21	"os"
22
23	"github.com/pkg/errors"
24	"golang.org/x/sys/windows"
25)
26
27var (
28	vtInputSupported  bool
29	ErrNotImplemented = errors.New("not implemented")
30)
31
32func (m *master) initStdios() {
33	m.in = windows.Handle(os.Stdin.Fd())
34	if err := windows.GetConsoleMode(m.in, &m.inMode); err == nil {
35		// Validate that windows.ENABLE_VIRTUAL_TERMINAL_INPUT is supported, but do not set it.
36		if err = windows.SetConsoleMode(m.in, m.inMode|windows.ENABLE_VIRTUAL_TERMINAL_INPUT); err == nil {
37			vtInputSupported = true
38		}
39		// Unconditionally set the console mode back even on failure because SetConsoleMode
40		// remembers invalid bits on input handles.
41		windows.SetConsoleMode(m.in, m.inMode)
42	} else {
43		fmt.Printf("failed to get console mode for stdin: %v\n", err)
44	}
45
46	m.out = windows.Handle(os.Stdout.Fd())
47	if err := windows.GetConsoleMode(m.out, &m.outMode); err == nil {
48		if err := windows.SetConsoleMode(m.out, m.outMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err == nil {
49			m.outMode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
50		} else {
51			windows.SetConsoleMode(m.out, m.outMode)
52		}
53	} else {
54		fmt.Printf("failed to get console mode for stdout: %v\n", err)
55	}
56
57	m.err = windows.Handle(os.Stderr.Fd())
58	if err := windows.GetConsoleMode(m.err, &m.errMode); err == nil {
59		if err := windows.SetConsoleMode(m.err, m.errMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err == nil {
60			m.errMode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
61		} else {
62			windows.SetConsoleMode(m.err, m.errMode)
63		}
64	} else {
65		fmt.Printf("failed to get console mode for stderr: %v\n", err)
66	}
67}
68
69type master struct {
70	in     windows.Handle
71	inMode uint32
72
73	out     windows.Handle
74	outMode uint32
75
76	err     windows.Handle
77	errMode uint32
78}
79
80func (m *master) SetRaw() error {
81	if err := makeInputRaw(m.in, m.inMode); err != nil {
82		return err
83	}
84
85	// Set StdOut and StdErr to raw mode, we ignore failures since
86	// windows.DISABLE_NEWLINE_AUTO_RETURN might not be supported on this version of
87	// Windows.
88
89	windows.SetConsoleMode(m.out, m.outMode|windows.DISABLE_NEWLINE_AUTO_RETURN)
90
91	windows.SetConsoleMode(m.err, m.errMode|windows.DISABLE_NEWLINE_AUTO_RETURN)
92
93	return nil
94}
95
96func (m *master) Reset() error {
97	for _, s := range []struct {
98		fd   windows.Handle
99		mode uint32
100	}{
101		{m.in, m.inMode},
102		{m.out, m.outMode},
103		{m.err, m.errMode},
104	} {
105		if err := windows.SetConsoleMode(s.fd, s.mode); err != nil {
106			return errors.Wrap(err, "unable to restore console mode")
107		}
108	}
109
110	return nil
111}
112
113func (m *master) Size() (WinSize, error) {
114	var info windows.ConsoleScreenBufferInfo
115	err := windows.GetConsoleScreenBufferInfo(m.out, &info)
116	if err != nil {
117		return WinSize{}, errors.Wrap(err, "unable to get console info")
118	}
119
120	winsize := WinSize{
121		Width:  uint16(info.Window.Right - info.Window.Left + 1),
122		Height: uint16(info.Window.Bottom - info.Window.Top + 1),
123	}
124
125	return winsize, nil
126}
127
128func (m *master) Resize(ws WinSize) error {
129	return ErrNotImplemented
130}
131
132func (m *master) ResizeFrom(c Console) error {
133	return ErrNotImplemented
134}
135
136func (m *master) DisableEcho() error {
137	mode := m.inMode &^ windows.ENABLE_ECHO_INPUT
138	mode |= windows.ENABLE_PROCESSED_INPUT
139	mode |= windows.ENABLE_LINE_INPUT
140
141	if err := windows.SetConsoleMode(m.in, mode); err != nil {
142		return errors.Wrap(err, "unable to set console to disable echo")
143	}
144
145	return nil
146}
147
148func (m *master) Close() error {
149	return nil
150}
151
152func (m *master) Read(b []byte) (int, error) {
153	return os.Stdin.Read(b)
154}
155
156func (m *master) Write(b []byte) (int, error) {
157	return os.Stdout.Write(b)
158}
159
160func (m *master) Fd() uintptr {
161	return uintptr(m.in)
162}
163
164// on windows, console can only be made from os.Std{in,out,err}, hence there
165// isnt a single name here we can use. Return a dummy "console" value in this
166// case should be sufficient.
167func (m *master) Name() string {
168	return "console"
169}
170
171// makeInputRaw puts the terminal (Windows Console) connected to the given
172// file descriptor into raw mode
173func makeInputRaw(fd windows.Handle, mode uint32) error {
174	// See
175	// -- https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx
176	// -- https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx
177
178	// Disable these modes
179	mode &^= windows.ENABLE_ECHO_INPUT
180	mode &^= windows.ENABLE_LINE_INPUT
181	mode &^= windows.ENABLE_MOUSE_INPUT
182	mode &^= windows.ENABLE_WINDOW_INPUT
183	mode &^= windows.ENABLE_PROCESSED_INPUT
184
185	// Enable these modes
186	mode |= windows.ENABLE_EXTENDED_FLAGS
187	mode |= windows.ENABLE_INSERT_MODE
188	mode |= windows.ENABLE_QUICK_EDIT_MODE
189
190	if vtInputSupported {
191		mode |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT
192	}
193
194	if err := windows.SetConsoleMode(fd, mode); err != nil {
195		return errors.Wrap(err, "unable to set console to raw mode")
196	}
197
198	return nil
199}
200
201func checkConsole(f File) error {
202	var mode uint32
203	if err := windows.GetConsoleMode(windows.Handle(f.Fd()), &mode); err != nil {
204		return err
205	}
206	return nil
207}
208
209func newMaster(f File) (Console, error) {
210	if f != os.Stdin && f != os.Stdout && f != os.Stderr {
211		return nil, errors.New("creating a console from a file is not supported on windows")
212	}
213	m := &master{}
214	m.initStdios()
215	return m, nil
216}
217