From 4c2de732e12912dd1997268b95a90b6fa23807d4 Mon Sep 17 00:00:00 2001 From: Julien Kauffmann Date: Mon, 6 Mar 2017 15:59:36 -0500 Subject: [PATCH] Added msys2/cygwin terminal detection support --- log/term/terminal_windows.go | 63 ++++++++++++++++++++++++++++--- log/term/terminal_windows_test.go | 56 +++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 log/term/terminal_windows_test.go diff --git a/log/term/terminal_windows.go b/log/term/terminal_windows.go index 5e797f4ee..82353ce52 100644 --- a/log/term/terminal_windows.go +++ b/log/term/terminal_windows.go @@ -8,7 +8,9 @@ package term import ( + "encoding/binary" "io" + "regexp" "syscall" "unsafe" ) @@ -16,16 +18,67 @@ import ( 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 } diff --git a/log/term/terminal_windows_test.go b/log/term/terminal_windows_test.go new file mode 100644 index 000000000..1640ff452 --- /dev/null +++ b/log/term/terminal_windows_test.go @@ -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") + } +}