feat: Add support for executing processes with Windows ConPty (#311)

* Initial agent

* fix: Use buffered reader in peer to fix ShortBuffer

This prevents a io.ErrShortBuffer from occurring when the byte
slice being read is smaller than the chunks sent from the opposite
pipe.

This makes sense for unordered connections, where transmission is
not guarunteed, but does not make sense for TCP-like connections.

We use a bufio.Reader when ordered to ensure data isn't lost.

* SSH server works!

* Start Windows support

* Something works

* Refactor pty package to support Windows spawn

* SSH server now works on Windows

* Fix non-Windows

* Fix Linux PTY render

* FIx linux build tests

* Remove agent and wintest

* Add test for Windows resize

* Fix linting errors

* Add Windows environment variables

* Add strings import

* Add comment for attrs

* Add goleak

* Add require import
This commit is contained in:
Kyle Carberry
2022-02-17 10:44:49 -06:00
committed by GitHub
parent c2ad91bb74
commit 503d09c149
32 changed files with 582 additions and 1140 deletions

107
pty/pty_windows.go Normal file
View File

@@ -0,0 +1,107 @@
//go:build windows
// +build windows
package pty
import (
"io"
"os"
"sync"
"unsafe"
"golang.org/x/sys/windows"
"golang.org/x/xerrors"
)
var (
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
procResizePseudoConsole = kernel32.NewProc("ResizePseudoConsole")
procCreatePseudoConsole = kernel32.NewProc("CreatePseudoConsole")
procClosePseudoConsole = kernel32.NewProc("ClosePseudoConsole")
)
// See: https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session
func newPty() (PTY, error) {
// We use the CreatePseudoConsole API which was introduced in build 17763
vsn := windows.RtlGetVersion()
if vsn.MajorVersion < 10 ||
vsn.BuildNumber < 17763 {
// If the CreatePseudoConsole API is not available, we fall back to a simpler
// implementation that doesn't create an actual PTY - just uses os.Pipe
return nil, xerrors.Errorf("pty not supported")
}
ptyWindows := &ptyWindows{}
var err error
ptyWindows.inputRead, ptyWindows.inputWrite, err = os.Pipe()
if err != nil {
return nil, err
}
ptyWindows.outputRead, ptyWindows.outputWrite, err = os.Pipe()
consoleSize := uintptr(80) + (uintptr(80) << 16)
ret, _, err := procCreatePseudoConsole.Call(
consoleSize,
uintptr(ptyWindows.inputRead.Fd()),
uintptr(ptyWindows.outputWrite.Fd()),
0,
uintptr(unsafe.Pointer(&ptyWindows.console)),
)
if int32(ret) < 0 {
return nil, xerrors.Errorf("create pseudo console (%d): %w", int32(ret), err)
}
return ptyWindows, nil
}
type ptyWindows struct {
console windows.Handle
outputWrite *os.File
outputRead *os.File
inputWrite *os.File
inputRead *os.File
closeMutex sync.Mutex
closed bool
}
func (p *ptyWindows) Output() io.ReadWriter {
return readWriter{
Reader: p.outputRead,
Writer: p.outputWrite,
}
}
func (p *ptyWindows) Input() io.ReadWriter {
return readWriter{
Reader: p.inputRead,
Writer: p.inputWrite,
}
}
func (p *ptyWindows) Resize(cols uint16, rows uint16) error {
ret, _, err := procResizePseudoConsole.Call(uintptr(p.console), uintptr(cols)+(uintptr(rows)<<16))
if ret != 0 {
return err
}
return nil
}
func (p *ptyWindows) Close() error {
p.closeMutex.Lock()
defer p.closeMutex.Unlock()
if p.closed {
return nil
}
p.closed = true
ret, _, err := procClosePseudoConsole.Call(uintptr(p.console))
if ret != 0 {
return xerrors.Errorf("close pseudo console: %w", err)
}
_ = p.outputRead.Close()
_ = p.inputWrite.Close()
return nil
}