github.com/walkingsparrow/docker@v1.4.2-0.20151218153551-b708a2249bfa/pkg/term/term_windows.go (about)

     1  // +build windows
     2  
     3  package term
     4  
     5  import (
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"os/signal"
    10  	"syscall"
    11  
    12  	"github.com/Azure/go-ansiterm/winterm"
    13  	"github.com/Sirupsen/logrus"
    14  	"github.com/docker/docker/pkg/system"
    15  	"github.com/docker/docker/pkg/term/windows"
    16  )
    17  
    18  // State holds the console mode for the terminal.
    19  type State struct {
    20  	mode uint32
    21  }
    22  
    23  // Winsize is used for window size.
    24  type Winsize struct {
    25  	Height uint16
    26  	Width  uint16
    27  	x      uint16
    28  	y      uint16
    29  }
    30  
    31  // StdStreams returns the standard streams (stdin, stdout, stedrr).
    32  func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
    33  	switch {
    34  	case os.Getenv("ConEmuANSI") == "ON":
    35  		// The ConEmu shell emulates ANSI well by default.
    36  		return os.Stdin, os.Stdout, os.Stderr
    37  	case os.Getenv("MSYSTEM") != "":
    38  		// MSYS (mingw) does not emulate ANSI well.
    39  		return windows.ConsoleStreams()
    40  	default:
    41  		if useNativeConsole() {
    42  			return os.Stdin, os.Stdout, os.Stderr
    43  		}
    44  		return windows.ConsoleStreams()
    45  	}
    46  }
    47  
    48  // useNativeConsole determines if the docker client should use the built-in
    49  // console which supports ANSI emulation, or fall-back to the golang emulator
    50  // (github.com/azure/go-ansiterm).
    51  func useNativeConsole() bool {
    52  	osv, err := system.GetOSVersion()
    53  	if err != nil {
    54  		return false
    55  	}
    56  
    57  	// Native console is not available major version 10
    58  	if osv.MajorVersion < 10 {
    59  		return false
    60  	}
    61  
    62  	// Must have a late pre-release TP4 build of Windows Server 2016/Windows 10 TH2 or later
    63  	if osv.Build < 10578 {
    64  		return false
    65  	}
    66  
    67  	// Environment variable override
    68  	if e := os.Getenv("USE_NATIVE_CONSOLE"); e != "" {
    69  		if e == "1" {
    70  			return true
    71  		}
    72  		return false
    73  	}
    74  
    75  	// Get the handle to stdout
    76  	stdOutHandle, err := syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE)
    77  	if err != nil {
    78  		return false
    79  	}
    80  
    81  	// Get the console mode from the consoles stdout handle
    82  	var mode uint32
    83  	if err := syscall.GetConsoleMode(stdOutHandle, &mode); err != nil {
    84  		return false
    85  	}
    86  
    87  	// Legacy mode does not have native ANSI emulation.
    88  	// https://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx
    89  	const enableVirtualTerminalProcessing = 0x0004
    90  	if mode&enableVirtualTerminalProcessing == 0 {
    91  		return false
    92  	}
    93  
    94  	// TODO Windows (Post TP4). The native emulator still has issues which
    95  	// mean it shouldn't be enabled for everyone. Change this next line to true
    96  	// to change the default to "enable if available". In the meantime, users
    97  	// can still try it out by using USE_NATIVE_CONSOLE env variable.
    98  	return false
    99  }
   100  
   101  // GetFdInfo returns the file descriptor for an os.File and indicates whether the file represents a terminal.
   102  func GetFdInfo(in interface{}) (uintptr, bool) {
   103  	return windows.GetHandleInfo(in)
   104  }
   105  
   106  // GetWinsize returns the window size based on the specified file descriptor.
   107  func GetWinsize(fd uintptr) (*Winsize, error) {
   108  
   109  	info, err := winterm.GetConsoleScreenBufferInfo(fd)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	winsize := &Winsize{
   115  		Width:  uint16(info.Window.Right - info.Window.Left + 1),
   116  		Height: uint16(info.Window.Bottom - info.Window.Top + 1),
   117  		x:      0,
   118  		y:      0}
   119  
   120  	// Note: GetWinsize is called frequently -- uncomment only for excessive details
   121  	// logrus.Debugf("[windows] GetWinsize: Console(%v)", info.String())
   122  	// logrus.Debugf("[windows] GetWinsize: Width(%v), Height(%v), x(%v), y(%v)", winsize.Width, winsize.Height, winsize.x, winsize.y)
   123  	return winsize, nil
   124  }
   125  
   126  // SetWinsize tries to set the specified window size for the specified file descriptor.
   127  func SetWinsize(fd uintptr, ws *Winsize) error {
   128  
   129  	// Ensure the requested dimensions are no larger than the maximum window size
   130  	info, err := winterm.GetConsoleScreenBufferInfo(fd)
   131  	if err != nil {
   132  		return err
   133  	}
   134  
   135  	if ws.Width == 0 || ws.Height == 0 || ws.Width > uint16(info.MaximumWindowSize.X) || ws.Height > uint16(info.MaximumWindowSize.Y) {
   136  		return fmt.Errorf("Illegal window size: (%v,%v) -- Maximum allow: (%v,%v)",
   137  			ws.Width, ws.Height, info.MaximumWindowSize.X, info.MaximumWindowSize.Y)
   138  	}
   139  
   140  	// Narrow the sizes to that used by Windows
   141  	width := winterm.SHORT(ws.Width)
   142  	height := winterm.SHORT(ws.Height)
   143  
   144  	// Set the dimensions while ensuring they remain within the bounds of the backing console buffer
   145  	// -- Shrinking will always succeed. Growing may push the edges past the buffer boundary. When that occurs,
   146  	//    shift the upper left just enough to keep the new window within the buffer.
   147  	rect := info.Window
   148  	if width < rect.Right-rect.Left+1 {
   149  		rect.Right = rect.Left + width - 1
   150  	} else if width > rect.Right-rect.Left+1 {
   151  		rect.Right = rect.Left + width - 1
   152  		if rect.Right >= info.Size.X {
   153  			rect.Left = info.Size.X - width
   154  			rect.Right = info.Size.X - 1
   155  		}
   156  	}
   157  
   158  	if height < rect.Bottom-rect.Top+1 {
   159  		rect.Bottom = rect.Top + height - 1
   160  	} else if height > rect.Bottom-rect.Top+1 {
   161  		rect.Bottom = rect.Top + height - 1
   162  		if rect.Bottom >= info.Size.Y {
   163  			rect.Top = info.Size.Y - height
   164  			rect.Bottom = info.Size.Y - 1
   165  		}
   166  	}
   167  	logrus.Debugf("[windows] SetWinsize: Requested((%v,%v)) Actual(%v)", ws.Width, ws.Height, rect)
   168  
   169  	return winterm.SetConsoleWindowInfo(fd, true, rect)
   170  }
   171  
   172  // IsTerminal returns true if the given file descriptor is a terminal.
   173  func IsTerminal(fd uintptr) bool {
   174  	return windows.IsConsole(fd)
   175  }
   176  
   177  // RestoreTerminal restores the terminal connected to the given file descriptor
   178  // to a previous state.
   179  func RestoreTerminal(fd uintptr, state *State) error {
   180  	return winterm.SetConsoleMode(fd, state.mode)
   181  }
   182  
   183  // SaveState saves the state of the terminal connected to the given file descriptor.
   184  func SaveState(fd uintptr) (*State, error) {
   185  	mode, e := winterm.GetConsoleMode(fd)
   186  	if e != nil {
   187  		return nil, e
   188  	}
   189  	return &State{mode}, nil
   190  }
   191  
   192  // DisableEcho disables echo for the terminal connected to the given file descriptor.
   193  // -- See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx
   194  func DisableEcho(fd uintptr, state *State) error {
   195  	mode := state.mode
   196  	mode &^= winterm.ENABLE_ECHO_INPUT
   197  	mode |= winterm.ENABLE_PROCESSED_INPUT | winterm.ENABLE_LINE_INPUT
   198  
   199  	err := winterm.SetConsoleMode(fd, mode)
   200  	if err != nil {
   201  		return err
   202  	}
   203  
   204  	// Register an interrupt handler to catch and restore prior state
   205  	restoreAtInterrupt(fd, state)
   206  	return nil
   207  }
   208  
   209  // SetRawTerminal puts the terminal connected to the given file descriptor into raw
   210  // mode and returns the previous state.
   211  func SetRawTerminal(fd uintptr) (*State, error) {
   212  	state, err := MakeRaw(fd)
   213  	if err != nil {
   214  		return nil, err
   215  	}
   216  
   217  	// Register an interrupt handler to catch and restore prior state
   218  	restoreAtInterrupt(fd, state)
   219  	return state, err
   220  }
   221  
   222  // MakeRaw puts the terminal (Windows Console) connected to the given file descriptor into raw
   223  // mode and returns the previous state of the terminal so that it can be restored.
   224  func MakeRaw(fd uintptr) (*State, error) {
   225  	state, err := SaveState(fd)
   226  	if err != nil {
   227  		return nil, err
   228  	}
   229  
   230  	// See
   231  	// -- https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx
   232  	// -- https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx
   233  	mode := state.mode
   234  
   235  	// Disable these modes
   236  	mode &^= winterm.ENABLE_ECHO_INPUT
   237  	mode &^= winterm.ENABLE_LINE_INPUT
   238  	mode &^= winterm.ENABLE_MOUSE_INPUT
   239  	mode &^= winterm.ENABLE_WINDOW_INPUT
   240  	mode &^= winterm.ENABLE_PROCESSED_INPUT
   241  
   242  	// Enable these modes
   243  	mode |= winterm.ENABLE_EXTENDED_FLAGS
   244  	mode |= winterm.ENABLE_INSERT_MODE
   245  	mode |= winterm.ENABLE_QUICK_EDIT_MODE
   246  
   247  	err = winterm.SetConsoleMode(fd, mode)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  	return state, nil
   252  }
   253  
   254  func restoreAtInterrupt(fd uintptr, state *State) {
   255  	sigchan := make(chan os.Signal, 1)
   256  	signal.Notify(sigchan, os.Interrupt)
   257  
   258  	go func() {
   259  		_ = <-sigchan
   260  		RestoreTerminal(fd, state)
   261  		os.Exit(0)
   262  	}()
   263  }