mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
135 lines
3.8 KiB
Go
135 lines
3.8 KiB
Go
6 months ago
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
|
||
|
// Package conpty implements support for Windows pseudo-consoles.
|
||
|
package conpty
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"os"
|
||
|
|
||
|
"github.com/dblohm7/wingoes"
|
||
|
"golang.org/x/sys/windows"
|
||
|
"tailscale.com/util/winutil"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
// ErrUnsupported is returned by NewPseudoConsole if the current Windows
|
||
|
// build does not support this package's API.
|
||
|
ErrUnsupported = errors.New("conpty unsupported on this version of Windows")
|
||
|
)
|
||
|
|
||
|
// PseudoConsole encapsulates a Windows pseudo-console. Use NewPseudoConsole
|
||
|
// to create a new instance.
|
||
|
type PseudoConsole struct {
|
||
|
outputRead io.ReadCloser
|
||
|
inputWrite io.WriteCloser
|
||
|
console windows.Handle
|
||
|
}
|
||
|
|
||
|
// NewPseudoConsole creates a new PseudoConsole using size for its initial
|
||
|
// width and height. It requires Windows 10 1809 or newer, and will return
|
||
|
// ErrUnsupported if that requirement is not met.
|
||
|
func NewPseudoConsole(size windows.Coord) (pty *PseudoConsole, err error) {
|
||
|
if !wingoes.IsWin10BuildOrGreater(wingoes.Win10Build1809) {
|
||
|
return nil, ErrUnsupported
|
||
|
}
|
||
|
if size.X <= 0 || size.Y <= 0 {
|
||
|
return nil, fmt.Errorf("%w: size must contain positive values", os.ErrInvalid)
|
||
|
}
|
||
|
|
||
|
var inputRead, inputWrite windows.Handle
|
||
|
if err := windows.CreatePipe(&inputRead, &inputWrite, nil, 0); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer func() {
|
||
|
windows.CloseHandle(inputRead)
|
||
|
if err != nil {
|
||
|
windows.CloseHandle(inputWrite)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
var outputRead, outputWrite windows.Handle
|
||
|
if err := windows.CreatePipe(&outputRead, &outputWrite, nil, 0); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer func() {
|
||
|
windows.CloseHandle(outputWrite)
|
||
|
if err != nil {
|
||
|
windows.CloseHandle(outputRead)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
var console windows.Handle
|
||
|
if err := windows.CreatePseudoConsole(size, inputRead, outputWrite, 0, &console); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
pty = &PseudoConsole{
|
||
|
outputRead: os.NewFile(uintptr(outputRead), "ptyOutputRead"),
|
||
|
inputWrite: os.NewFile(uintptr(inputWrite), "ptyInputWrite"),
|
||
|
console: console,
|
||
|
}
|
||
|
return pty, nil
|
||
|
}
|
||
|
|
||
|
// Resize sets the width and height of pty to size.
|
||
|
func (pty *PseudoConsole) Resize(size windows.Coord) error {
|
||
|
if pty.console == 0 {
|
||
|
return fmt.Errorf("PseudoConsole is closed")
|
||
|
}
|
||
|
if size.X <= 0 || size.Y <= 0 {
|
||
|
return fmt.Errorf("%w: size must contain positive values", os.ErrInvalid)
|
||
|
}
|
||
|
|
||
|
return windows.ResizePseudoConsole(pty.console, size)
|
||
|
}
|
||
|
|
||
|
// Close shuts down the pty. The caller must continue reading from the
|
||
|
// ReadCloser returned by Output until either EOF is reached or Close returns;
|
||
|
// failure to adequately drain the ReadCloser may result in Close deadlocking.
|
||
|
func (pty *PseudoConsole) Close() error {
|
||
|
if pty.console != 0 {
|
||
|
windows.ClosePseudoConsole(pty.console)
|
||
|
pty.console = 0
|
||
|
}
|
||
|
|
||
|
// now we can stop these
|
||
|
if pty.outputRead != nil {
|
||
|
pty.outputRead.Close()
|
||
|
pty.outputRead = nil
|
||
|
}
|
||
|
if pty.inputWrite != nil {
|
||
|
pty.inputWrite.Close()
|
||
|
pty.inputWrite = nil
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// ConfigureStartupInfo associates pty with the process to be started using sib.
|
||
|
func (pty *PseudoConsole) ConfigureStartupInfo(sib *winutil.StartupInfoBuilder) error {
|
||
|
if sib == nil {
|
||
|
return os.ErrInvalid
|
||
|
}
|
||
|
// We need to explicitly set null std handles.
|
||
|
// Failure to do so causes interference between the pty and the console
|
||
|
// handles that are implicitly inherited from the parent.
|
||
|
// This isn't explicitly documented anywhere. Windows Terminal does this too.
|
||
|
if err := sib.SetStdHandles(0, 0, 0); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return sib.SetPseudoConsole(pty.console)
|
||
|
}
|
||
|
|
||
|
// OutputPipe returns the ReadCloser for reading pty's output.
|
||
|
func (pty *PseudoConsole) OutputPipe() io.ReadCloser {
|
||
|
return pty.outputRead
|
||
|
}
|
||
|
|
||
|
// InputPipe returns the WriteCloser for writing pty's output.
|
||
|
func (pty *PseudoConsole) InputPipe() io.WriteCloser {
|
||
|
return pty.inputWrite
|
||
|
}
|