isatty_windows.go 3.3 KB

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