terraform/internal/terminal/impl_windows.go

163 lines
5.7 KiB
Go

//go:build windows
// +build windows
package terminal
import (
"fmt"
"os"
"syscall"
"golang.org/x/sys/windows"
// We're continuing to use this third-party library on Windows because it
// has the additional IsCygwinTerminal function, which includes some useful
// heuristics for recognizing when a pipe seems to be connected to a
// legacy terminal emulator on Windows versions that lack true pty support.
// We now use golang.org/x/term's functionality on other platforms.
isatty "github.com/mattn/go-isatty"
)
func configureOutputHandle(f *os.File) (*OutputStream, error) {
ret := &OutputStream{
File: f,
}
if fd := f.Fd(); isatty.IsTerminal(fd) {
// We have a few things to deal with here:
// - Activating UTF-8 output support (mandatory)
// - Activating virtual terminal support (optional)
// These will not succeed on Windows 8 or early versions of Windows 10.
// UTF-8 support means switching the console "code page" to CP_UTF8.
// Notice that this doesn't take the specific file descriptor, because
// the console is just ambiently associated with our process.
err := SetConsoleOutputCP(CP_UTF8)
if err != nil {
return nil, fmt.Errorf("failed to set the console to UTF-8 mode; you may need to use a newer version of Windows: %s", err)
}
// If the console also allows us to turn on
// ENABLE_VIRTUAL_TERMINAL_PROCESSING then we can potentially use VT
// output, although the methods of Settings will make the final
// determination on that because we might have some handles pointing at
// terminals and other handles pointing at files/pipes.
ret.getColumns = getColumnsWindowsConsole
var mode uint32
err = windows.GetConsoleMode(windows.Handle(fd), &mode)
if err != nil {
return ret, nil // We'll treat this as success but without VT support
}
mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
err = windows.SetConsoleMode(windows.Handle(fd), mode)
if err != nil {
return ret, nil // We'll treat this as success but without VT support
}
// If we get here then we've successfully turned on VT processing, so
// we can return an OutputStream that answers true when asked if it
// is a Terminal.
ret.isTerminal = staticTrue
return ret, nil
} else if isatty.IsCygwinTerminal(fd) {
// Cygwin terminals -- and other VT100 "fakers" for older versions of
// Windows -- are not really terminals in the usual sense, but rather
// are pipes between the child process (Terraform) and the terminal
// emulator. isatty.IsCygwinTerminal uses some heuristics to
// distinguish those pipes from other pipes we might see if the user
// were, for example, using the | operator on the command line.
// If we get in here then we'll assume that we can send VT100 sequences
// to this stream, even though it isn't a terminal in the usual sense.
ret.isTerminal = staticTrue
// TODO: Is it possible to detect the width of these fake terminals?
return ret, nil
}
// If we fall out here then we have a non-terminal filehandle, so we'll
// just accept all of the default OutputStream behaviors
return ret, nil
}
func configureInputHandle(f *os.File) (*InputStream, error) {
ret := &InputStream{
File: f,
}
if fd := f.Fd(); isatty.IsTerminal(fd) {
// We have to activate UTF-8 input, or else we fail. This will not
// succeed on Windows 8 or early versions of Windows 10.
// Notice that this doesn't take the specific file descriptor, because
// the console is just ambiently associated with our process.
err := SetConsoleCP(CP_UTF8)
if err != nil {
return nil, fmt.Errorf("failed to set the console to UTF-8 mode; you may need to use a newer version of Windows: %s", err)
}
ret.isTerminal = staticTrue
return ret, nil
} else if isatty.IsCygwinTerminal(fd) {
// As with the output handles above, we'll use isatty's heuristic to
// pretend that a pipe from mintty or a similar userspace terminal
// emulator is actually a terminal.
ret.isTerminal = staticTrue
return ret, nil
}
// If we fall out here then we have a non-terminal filehandle, so we'll
// just accept all of the default InputStream behaviors
return ret, nil
}
func getColumnsWindowsConsole(f *os.File) int {
// We'll just unconditionally ask the given file for its console buffer
// info here, and let it fail if the file isn't actually a console.
// (In practice, the init functions above only hook up this function
// if the handle looks like a console, so this should succeed.)
var info windows.ConsoleScreenBufferInfo
err := windows.GetConsoleScreenBufferInfo(windows.Handle(f.Fd()), &info)
if err != nil {
return defaultColumns
}
return int(info.Size.X)
}
// Unfortunately not all of the Windows kernel functions we need are in
// x/sys/windows at the time of writing, so we need to call some of them
// directly. (If you're maintaining this in future and have the capacity to
// test it well, consider checking if these functions have been added upstream
// yet and switch to their wrapper stubs if so.
var modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
var procSetConsoleCP = modkernel32.NewProc("SetConsoleCP")
var procSetConsoleOutputCP = modkernel32.NewProc("SetConsoleOutputCP")
const CP_UTF8 = 65001
// (These are written in the style of the stubs in x/sys/windows, which is
// a little non-idiomatic just due to the awkwardness of the low-level syscall
// interface.)
func SetConsoleCP(codepageID uint32) (err error) {
r1, _, e1 := syscall.Syscall(procSetConsoleCP.Addr(), 1, uintptr(codepageID), 0, 0)
if r1 == 0 {
err = e1
}
return
}
func SetConsoleOutputCP(codepageID uint32) (err error) {
r1, _, e1 := syscall.Syscall(procSetConsoleOutputCP.Addr(), 1, uintptr(codepageID), 0, 0)
if r1 == 0 {
err = e1
}
return
}
func staticTrue(f *os.File) bool {
return true
}
func staticFalse(f *os.File) bool {
return false
}