isatty_windows.go raw

   1  //go:build windows && !appengine
   2  // +build windows,!appengine
   3  
   4  package isatty
   5  
   6  import (
   7  	"errors"
   8  	"strings"
   9  	"syscall"
  10  	"unicode/utf16"
  11  	"unsafe"
  12  )
  13  
  14  const (
  15  	objectNameInfo uintptr = 1
  16  	fileNameInfo           = 2
  17  	fileTypePipe           = 3
  18  )
  19  
  20  var (
  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  
  29  func 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.
  37  func 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
  46  func 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
  83  func 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.
 100  func 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  }
 126