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 4c2de73
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 5 deletions.
63 changes: 58 additions & 5 deletions log/term/terminal_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,77 @@
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 {
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.
if err := syscall.GetConsoleMode(handle, &st); err == nil && st != 0 {
return true
}

// 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
}
56 changes: 56 additions & 0 deletions log/term/terminal_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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")
}
}

0 comments on commit 4c2de73

Please sign in to comment.