isatty_windows.go

  1//go:build windows && !appengine
  2// +build windows,!appengine
  3
  4package isatty
  5
  6import (
  7	"errors"
  8	"strings"
  9	"syscall"
 10	"unicode/utf16"
 11	"unsafe"
 12)
 13
 14const (
 15	objectNameInfo uintptr = 1
 16	fileNameInfo           = 2
 17	fileTypePipe           = 3
 18)
 19
 20var (
 21	kernel32                         = syscall.NewLazyDLL("kernel32.dll")
 22	ntdll                            = syscall.NewLazyDLL("ntdll.dll")
 23	procGetConsoleMode               = kernel32.NewProc("GetConsoleMode")
 24	procGetFileInformationByHandleEx = kernel32.NewProc("GetFileInformationByHandleEx")
 25	procGetFileType                  = kernel32.NewProc("GetFileType")
 26	procNtQueryObject                = ntdll.NewProc("NtQueryObject")
 27)
 28
 29func init() {
 30	// Check if GetFileInformationByHandleEx is available.
 31	if procGetFileInformationByHandleEx.Find() != nil {
 32		procGetFileInformationByHandleEx = nil
 33	}
 34}
 35
 36// IsTerminal return true if the file descriptor is terminal.
 37func IsTerminal(fd uintptr) bool {
 38	var st uint32
 39	r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0)
 40	return r != 0 && e == 0
 41}
 42
 43// Check pipe name is used for cygwin/msys2 pty.
 44// Cygwin/MSYS2 PTY has a name like:
 45//   \{cygwin,msys}-XXXXXXXXXXXXXXXX-ptyN-{from,to}-master
 46func isCygwinPipeName(name string) bool {
 47	token := strings.Split(name, "-")
 48	if len(token) < 5 {
 49		return false
 50	}
 51
 52	if token[0] != `\msys` &&
 53		token[0] != `\cygwin` &&
 54		token[0] != `\Device\NamedPipe\msys` &&
 55		token[0] != `\Device\NamedPipe\cygwin` {
 56		return false
 57	}
 58
 59	if token[1] == "" {
 60		return false
 61	}
 62
 63	if !strings.HasPrefix(token[2], "pty") {
 64		return false
 65	}
 66
 67	if token[3] != `from` && token[3] != `to` {
 68		return false
 69	}
 70
 71	if token[4] != "master" {
 72		return false
 73	}
 74
 75	return true
 76}
 77
 78// getFileNameByHandle use the undocomented ntdll NtQueryObject to get file full name from file handler
 79// since GetFileInformationByHandleEx is not available under windows Vista and still some old fashion
 80// guys are using Windows XP, this is a workaround for those guys, it will also work on system from
 81// Windows vista to 10
 82// see https://stackoverflow.com/a/18792477 for details
 83func getFileNameByHandle(fd uintptr) (string, error) {
 84	if procNtQueryObject == nil {
 85		return "", errors.New("ntdll.dll: NtQueryObject not supported")
 86	}
 87
 88	var buf [4 + syscall.MAX_PATH]uint16
 89	var result int
 90	r, _, e := syscall.Syscall6(procNtQueryObject.Addr(), 5,
 91		fd, objectNameInfo, uintptr(unsafe.Pointer(&buf)), uintptr(2*len(buf)), uintptr(unsafe.Pointer(&result)), 0)
 92	if r != 0 {
 93		return "", e
 94	}
 95	return string(utf16.Decode(buf[4 : 4+buf[0]/2])), nil
 96}
 97
 98// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2
 99// terminal.
100func IsCygwinTerminal(fd uintptr) bool {
101	if procGetFileInformationByHandleEx == nil {
102		name, err := getFileNameByHandle(fd)
103		if err != nil {
104			return false
105		}
106		return isCygwinPipeName(name)
107	}
108
109	// Cygwin/msys's pty is a pipe.
110	ft, _, e := syscall.Syscall(procGetFileType.Addr(), 1, fd, 0, 0)
111	if ft != fileTypePipe || e != 0 {
112		return false
113	}
114
115	var buf [2 + syscall.MAX_PATH]uint16
116	r, _, e := syscall.Syscall6(procGetFileInformationByHandleEx.Addr(),
117		4, fd, fileNameInfo, uintptr(unsafe.Pointer(&buf)),
118		uintptr(len(buf)*2), 0, 0)
119	if r == 0 || e != 0 {
120		return false
121	}
122
123	l := *(*uint32)(unsafe.Pointer(&buf))
124	return isCygwinPipeName(string(utf16.Decode(buf[2 : 2+l/2])))
125}