Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion log/term/colorwriter_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type colorWriter struct {
// platform support for ANSI color codes. If w is not a terminal it is
// returned unmodified.
func NewColorWriter(w io.Writer) io.Writer {
if !IsTerminal(w) {
if !IsConsole(w) {
return w
}

Expand Down
81 changes: 76 additions & 5 deletions log/term/terminal_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,95 @@
package term

import (
"encoding/binary"
"io"
"regexp"
"syscall"
"unsafe"
)

var kernel32 = syscall.NewLazyDLL("kernel32.dll")

var (
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
procGetFileInformationByHandleEx = kernel32.NewProc("GetFileInformationByHandleEx")
msysPipeNameRegex = regexp.MustCompile(`\\(cygwin|msys)-\w+-pty\d?-(to|from)-master`)
)

const (
fileNameInfo = 0x02
)

// IsTerminal returns true if w writes to a terminal.
func IsTerminal(w io.Writer) bool {
fw, ok := w.(fder)
if !ok {
return IsConsole(w) || IsMSYSTerminal(w)
}

// IsConsole returns true if w writes to a Windows console.
func IsConsole(w io.Writer) bool {
var handle syscall.Handle

if fw, ok := w.(fder); ok {
handle = syscall.Handle(fw.Fd())
} else {
// The writer has no file-descriptor and so can't be a terminal.
return false
}

var st uint32
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fw.Fd(), uintptr(unsafe.Pointer(&st)), 0)
return r != 0 && e == 0
err := syscall.GetConsoleMode(handle, &st)

// If the handle is attached to a terminal, GetConsoleMode returns a
// non-zero value containing the console mode flags. We don't care about
// the specifics of flags, just that it is not zero.
return (err == nil && st != 0)
}

// IsMSYSTerminal returns true if w writes to a MSYS/MSYS2 terminal.
func IsMSYSTerminal(w io.Writer) bool {
var handle syscall.Handle

if fw, ok := w.(fder); ok {
handle = syscall.Handle(fw.Fd())
} else {
// The writer has no file-descriptor and so can't be a terminal.
return false
}

// MSYS(2) terminal reports as a pipe for STDIN/STDOUT/STDERR. If it isn't
// a pipe, it can't be a MSYS(2) terminal.
filetype, err := syscall.GetFileType(handle)

if filetype != syscall.FILE_TYPE_PIPE || err != nil {
return false
}

// MSYS2/Cygwin terminal's name looks like: \msys-dd50a72ab4668b33-pty2-to-master
data := make([]byte, 256, 256)

r, _, e := syscall.Syscall6(
procGetFileInformationByHandleEx.Addr(),
4,
uintptr(handle),
uintptr(fileNameInfo),
uintptr(unsafe.Pointer(&data[0])),
uintptr(len(data)),
0,
0,
)

if r != 0 && e == 0 {
// The first 4 bytes of the buffer are the size of the UTF16 name, in bytes.
unameLen := binary.LittleEndian.Uint32(data[:4]) / 2
uname := make([]uint16, unameLen, unameLen)

for i := uint32(0); i < unameLen; i++ {
uname[i] = binary.LittleEndian.Uint16(data[i*2+4 : i*2+2+4])
}

name := syscall.UTF16ToString(uname)

return msysPipeNameRegex.MatchString(name)
}

return false
}
71 changes: 71 additions & 0 deletions log/term/terminal_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package term

import (
"fmt"
"syscall"
"testing"
)

// +build windows

type myWriter struct {
fd uintptr
}

func (w *myWriter) Write(p []byte) (int, error) {
return 0, fmt.Errorf("not implemented")
}

func (w *myWriter) Fd() uintptr {
return w.fd
}

var procGetStdHandle = kernel32.NewProc("GetStdHandle")

const stdOutputHandle = ^uintptr(0) - 11 + 1

func getConsoleHandle() syscall.Handle {
ptr, err := syscall.UTF16PtrFromString("CONOUT$")

if err != nil {
panic(err)
}

handle, err := syscall.CreateFile(ptr, syscall.GENERIC_READ|syscall.GENERIC_WRITE, syscall.FILE_SHARE_READ, nil, syscall.OPEN_EXISTING, 0, 0)

if err != nil {
panic(err)
}

return handle
}

func TestIsTerminal(t *testing.T) {
// This is necessary because depending on whether `go test` is called with
// the `-v` option, stdout will or will not be bound, changing the behavior
// of the test. So we refer to it directly to avoid flakyness.
handle := getConsoleHandle()

writer := &myWriter{
fd: uintptr(handle),
}

if !IsTerminal(writer) {
t.Errorf("output is supposed to be a terminal")
}
}

func TestIsConsole(t *testing.T) {
// This is necessary because depending on whether `go test` is called with
// the `-v` option, stdout will or will not be bound, changing the behavior
// of the test. So we refer to it directly to avoid flakyness.
handle := getConsoleHandle()

writer := &myWriter{
fd: uintptr(handle),
}

if !IsConsole(writer) {
t.Errorf("output is supposed to be a console")
}
}