tailscale/util/winutil/conpty/conpty_windows.go
Aaron Klotz 34e8820301 util/winutil: add conpty package and helper for building windows.StartupInfoEx
StartupInfoBuilder is a helper for constructing StartupInfoEx structures
featuring proc/thread attribute lists. Calling its setters triggers the
appropriate setting of fields, adjusting flags as necessary, and populating
the proc/thread attribute list as necessary. Currently it supports four
features: setting std handles, setting pseudo-consoles, specifying handles
for inheritance, and specifying jobs.

The conpty package simplifies creation of pseudo-consoles, their associated
pipes, and assignment of the pty to StartupInfoEx proc/thread attributes.

Updates #12383

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2024-06-06 14:18:36 -06:00

135 lines
3.8 KiB
Go

// 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
}