github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/logger/output_windows.go (about)

     1  // Copyright 2015 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  //go:build windows
     5  // +build windows
     6  
     7  // Windows 10 has a new terminal that can do ANSI codes by itself, so all this
     8  // other stuff is legacy - EXCEPT that the colors are not right! If they ever
     9  // get it right, a capable terminal can be identified by
    10  // calling GetConsoleMode and checking for
    11  // ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
    12  
    13  package logger
    14  
    15  import (
    16  	"bytes"
    17  	"fmt"
    18  	"io"
    19  	"os"
    20  	"sync"
    21  	"syscall"
    22  
    23  	"unsafe"
    24  
    25  	"golang.org/x/sys/windows"
    26  )
    27  
    28  var (
    29  	kernel32DLL                    = windows.NewLazySystemDLL("kernel32.dll")
    30  	setConsoleTextAttributeProc    = kernel32DLL.NewProc("SetConsoleTextAttribute")
    31  	getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo")
    32  	stdOutMutex                    sync.Mutex
    33  	stdErrMutex                    sync.Mutex
    34  	consoleMode                    WORD
    35  )
    36  
    37  type (
    38  	SHORT int16
    39  	WORD  uint16
    40  
    41  	SMALL_RECT struct {
    42  		Left   SHORT
    43  		Top    SHORT
    44  		Right  SHORT
    45  		Bottom SHORT
    46  	}
    47  
    48  	COORD struct {
    49  		X SHORT
    50  		Y SHORT
    51  	}
    52  
    53  	CONSOLE_SCREEN_BUFFER_INFO struct {
    54  		Size              COORD
    55  		CursorPosition    COORD
    56  		Attributes        WORD
    57  		Window            SMALL_RECT
    58  		MaximumWindowSize COORD
    59  	}
    60  )
    61  
    62  const (
    63  	// Character attributes
    64  	// Note:
    65  	// -- The attributes are combined to produce various colors (e.g., Blue + Green will create Cyan).
    66  	//    Clearing all foreground or background colors results in black; setting all creates white.
    67  	// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682088(v=vs.85).aspx#_win32_character_attributes.
    68  	fgBlack     WORD = 0x0000
    69  	fgBlue      WORD = 0x0001
    70  	fgGreen     WORD = 0x0002
    71  	fgCyan      WORD = 0x0003
    72  	fgRed       WORD = 0x0004
    73  	fgMagenta   WORD = 0x0005
    74  	fgYellow    WORD = 0x0006
    75  	fgWhite     WORD = 0x0007
    76  	fgIntensity WORD = 0x0008
    77  
    78  	bgBlack     WORD = 0x0000
    79  	bgBlue      WORD = 0x0010
    80  	bgGreen     WORD = 0x0020
    81  	bgCyan      WORD = 0x0030
    82  	bgRed       WORD = 0x0040
    83  	bgMagenta   WORD = 0x0050
    84  	bgYellow    WORD = 0x0060
    85  	bgWhite     WORD = 0x0070
    86  	bgIntensity WORD = 0x0080
    87  )
    88  
    89  var codesWin = map[byte]WORD{
    90  	0:   fgWhite | bgBlack,       //	"reset":
    91  	1:   fgIntensity,             //CpBold          = CodePair{1, 22}
    92  	22:  fgWhite,                 //	UnBold:        // Just assume this means reset to white fg
    93  	39:  fgWhite,                 //	"resetfg":
    94  	49:  fgWhite,                 //	"resetbg":        // Just assume this means reset to white fg
    95  	30:  fgBlack,                 //	"black":
    96  	31:  fgRed,                   //	"red":
    97  	32:  fgGreen,                 //	"green":
    98  	33:  fgYellow,                //	"yellow":
    99  	34:  fgBlue,                  //	"blue":
   100  	35:  fgMagenta,               //	"magenta":
   101  	36:  fgCyan,                  //	"cyan":
   102  	37:  fgWhite,                 //	"white":
   103  	90:  fgBlack | fgIntensity,   //	"grey":
   104  	91:  fgRed | fgIntensity,     //	"red":
   105  	92:  fgGreen | fgIntensity,   //	"green":
   106  	93:  fgYellow | fgIntensity,  //	"yellow":
   107  	94:  fgBlue | fgIntensity,    //	"blue":
   108  	95:  fgMagenta | fgIntensity, //	"magenta":
   109  	96:  fgCyan | fgIntensity,    //	"cyan":
   110  	97:  fgWhite | fgIntensity,   //	"white":
   111  	40:  bgBlack,                 //	"bgBlack":
   112  	41:  bgRed,                   //	"bgRed":
   113  	42:  bgGreen,                 //	"bgGreen":
   114  	43:  bgYellow,                //	"bgYellow":
   115  	44:  bgBlue,                  //	"bgBlue":
   116  	45:  bgMagenta,               //	"bgMagenta":
   117  	46:  bgCyan,                  //	"bgCyan":
   118  	47:  bgWhite,                 //	"bgWhite":
   119  	100: bgBlack | bgIntensity,   //	"bgBlack":
   120  	101: bgRed | bgIntensity,     //	"bgRed":
   121  	102: bgGreen | bgIntensity,   //	"bgGreen":
   122  	103: bgYellow | bgIntensity,  //	"bgYellow":
   123  	104: bgBlue | bgIntensity,    //	"bgBlue":
   124  	105: bgMagenta | bgIntensity, //	"bgMagenta":
   125  	106: bgCyan | bgIntensity,    //	"bgCyan":
   126  	107: bgWhite | bgIntensity,   //	"bgWhite":
   127  }
   128  
   129  // Return our writer so we can override Write()
   130  func OutputWriterFromFile(out *os.File) io.Writer {
   131  	return &ColorWriter{w: out, fd: out.Fd(), mutex: &stdOutMutex, lastFgColor: fgWhite}
   132  }
   133  
   134  // Return our writer so we can override Write()
   135  func OutputWriter() io.Writer {
   136  	return OutputWriterFromFile(os.Stdout)
   137  }
   138  
   139  // Return our writer so we can override Write()
   140  func ErrorWriter() io.Writer {
   141  	return &ColorWriter{w: os.Stderr, fd: os.Stderr.Fd(), mutex: &stdErrMutex, lastFgColor: fgWhite}
   142  }
   143  
   144  type ColorWriter struct {
   145  	w           io.Writer
   146  	fd          uintptr
   147  	mutex       *sync.Mutex
   148  	lastFgColor WORD
   149  	bold        bool
   150  }
   151  
   152  // Fd returns the underlying file descriptor. This is mainly to
   153  // support checking whether it's a terminal or not.
   154  func (cw *ColorWriter) Fd() uintptr {
   155  	return cw.fd
   156  }
   157  
   158  // Rough emulation of Ansi terminal codes.
   159  // Parse the string, pick out the codes, and convert to
   160  // calls to SetConsoleTextAttribute.
   161  //
   162  // This function must mix calls to SetConsoleTextAttribute
   163  // with separate Write() calls, so to prevent pieces of colored
   164  // strings from being interleaved by unsynchronized goroutines,
   165  // we unfortunately must lock a mutex.
   166  //
   167  // Notice this is only necessary for what is now called
   168  // "legacy" terminal mode:
   169  // https://technet.microsoft.com/en-us/library/mt427362.aspx?f=255&MSPPError=-2147217396
   170  func (cw *ColorWriter) Write(p []byte) (n int, err error) {
   171  	cw.mutex.Lock()
   172  	defer cw.mutex.Unlock()
   173  
   174  	var totalWritten = len(p)
   175  	ctlStart := []byte{0x1b, '['}
   176  
   177  	for nextIndex := 0; len(p) > 0; {
   178  
   179  		// search for the next control code
   180  		nextIndex = bytes.Index(p, ctlStart)
   181  		if nextIndex == -1 {
   182  			nextIndex = len(p)
   183  		}
   184  
   185  		cw.w.Write(p[0:nextIndex])
   186  		if nextIndex+2 < len(p) {
   187  			// Skip past the control code
   188  			nextIndex += 2
   189  		}
   190  
   191  		p = p[nextIndex:]
   192  
   193  		if len(p) > 0 {
   194  			// The control code is written as separate ascii digits. usually 2,
   195  			// ending with 'm'.
   196  			// Stop at 4 as a sanity check.
   197  			if '0' <= p[0] && p[0] <= '9' {
   198  				p = cw.parseColorControl(p)
   199  			} else {
   200  				p = cw.parseControlCode(p)
   201  			}
   202  		}
   203  
   204  	}
   205  	return totalWritten, nil
   206  }
   207  
   208  func (cw *ColorWriter) parseColorControl(p []byte) []byte {
   209  
   210  	var controlIndex int
   211  	controlCode := p[controlIndex] - '0'
   212  	controlIndex++
   213  	for i := 0; controlIndex < len(p) && p[controlIndex] != 'm' && i < 4; i++ {
   214  		if '0' <= p[controlIndex] && p[controlIndex] <= '9' {
   215  			controlCode *= 10
   216  			controlCode += p[controlIndex] - '0'
   217  		}
   218  		controlIndex++
   219  	}
   220  
   221  	if code, ok := codesWin[controlCode]; ok {
   222  		if controlCode == 1 {
   223  			// intensity
   224  			cw.bold = true
   225  		} else if controlCode == 0 || controlCode == 22 || controlCode == 39 {
   226  			// reset
   227  			code = fgWhite
   228  			cw.bold = false
   229  		}
   230  		if code >= fgBlue && code <= fgWhite {
   231  			cw.lastFgColor = code
   232  		} else {
   233  			code = cw.lastFgColor
   234  		}
   235  		if cw.bold {
   236  			code = code | fgIntensity
   237  		}
   238  		setConsoleTextAttribute(cw.fd, code)
   239  	}
   240  	if controlIndex+1 <= len(p) {
   241  		controlIndex++
   242  	}
   243  
   244  	return p[controlIndex:]
   245  }
   246  
   247  // parseControlCode is for absorbing backspaces, which
   248  // caused junk to come out on the console, and whichever
   249  // other control code we're probably unprepared for
   250  func (cw *ColorWriter) parseControlCode(p []byte) []byte {
   251  	if p[0] == 'D' {
   252  		cw.w.Write([]byte(fmt.Sprintf("\b")))
   253  	}
   254  	return p[1:]
   255  }
   256  
   257  // setConsoleTextAttribute sets the attributes of characters written to the
   258  // console screen buffer by the WriteFile or WriteConsole function.
   259  // See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686047(v=vs.85).aspx.
   260  func setConsoleTextAttribute(handle uintptr, attribute WORD) error {
   261  	r1, r2, err := setConsoleTextAttributeProc.Call(handle, uintptr(attribute), 0)
   262  	use(attribute)
   263  	return checkError(r1, r2, err)
   264  }
   265  
   266  // GetConsoleScreenBufferInfo retrieves information about the specified console screen buffer.
   267  // http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx
   268  func getConsoleTextAttribute(handle uintptr) (WORD, error) {
   269  	var info CONSOLE_SCREEN_BUFFER_INFO
   270  	if err := checkError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0)); err != nil {
   271  		return 0, err
   272  	}
   273  	return info.Attributes, nil
   274  }
   275  
   276  // SaveConsoleMode records the current text attributes in a global, so
   277  // it can be restored later, in case nonstandard colors are expected.
   278  func SaveConsoleMode() error {
   279  	var err error
   280  	consoleMode, err = getConsoleTextAttribute(os.Stdout.Fd())
   281  	return err
   282  }
   283  
   284  // RestoreConsoleMode restores the current text attributes from a global,
   285  // in case nonstandard colors are expected.
   286  func RestoreConsoleMode() {
   287  	if consoleMode != 0 {
   288  		setConsoleTextAttribute(os.Stdout.Fd(), consoleMode)
   289  	}
   290  }
   291  
   292  // checkError evaluates the results of a Windows API call and returns the error if it failed.
   293  func checkError(r1, r2 uintptr, err error) error {
   294  	// Windows APIs return non-zero to indicate success
   295  	if r1 != 0 {
   296  		return nil
   297  	}
   298  
   299  	// Return the error if provided, otherwise default to EINVAL
   300  	if err != nil {
   301  		return err
   302  	}
   303  	return syscall.EINVAL
   304  }
   305  
   306  // use is a no-op, but the compiler cannot see that it is.
   307  // Calling use(p) ensures that p is kept live until that point.
   308  func use(p interface{}) {}