Skip to content

Commit

Permalink
Added msys2/cygwin terminal detection support
Browse files Browse the repository at this point in the history
  • Loading branch information
jkauffmann-ubi committed Mar 23, 2017
1 parent cdd47ca commit ef721fa
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 6 deletions.
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
80 changes: 75 additions & 5 deletions log/term/terminal_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,94 @@
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

// If the terminal is a Windows console, succeed right away.
err := syscall.GetConsoleMode(handle, &st)

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
}

// The terminal is not a cmd.exe terminal, let's try to detect MSYS2 terminals.
filetype, err := syscall.GetFileType(handle)

// MSYS2 terminal reports as a pipe.
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")
}
}

0 comments on commit ef721fa

Please sign in to comment.