Skip to content

Commit

Permalink
Add conpty (pseudo console) package (microsoft#1228)
Browse files Browse the repository at this point in the history
* Add conpty (pseudo console) package

This change adds a conpty package that houses go friendly wrappers around
the Pseudo Console API in Windows. This will be used to support tty scenarios
for Host Process containers.

There's not many tests I can add here as you need to hook this up to a running
process, where that work is coming.

Signed-off-by: Daniel Canter <[email protected]>
  • Loading branch information
dcantah committed Dec 15, 2021
1 parent 9238796 commit f8cbd0b
Show file tree
Hide file tree
Showing 9 changed files with 542 additions and 24 deletions.
153 changes: 153 additions & 0 deletions internal/conpty/conpty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package conpty

import (
"errors"
"fmt"
"os"
"sync"
"unsafe"

"github.com/Microsoft/hcsshim/internal/winapi"
"golang.org/x/sys/windows"
)

var (
errClosedConPty = errors.New("pseudo console is closed")
errNotInitialized = errors.New("pseudo console hasn't been initialized")
)

// ConPTY is a wrapper around a Windows PseudoConsole handle. Create a new instance by calling `New()`.
type ConPTY struct {
// handleLock guards hpc
handleLock sync.RWMutex
// hpc is the pseudo console handle
hpc windows.Handle
// inPipe and outPipe are our end of the pipes to read/write to the pseudo console.
inPipe *os.File
outPipe *os.File
}

// New returns a new `ConPTY` object. This object is not ready for IO until `UpdateProcThreadAttribute` is called and a process has been started.
func New(width, height int16, flags uint32) (*ConPTY, error) {
// First we need to make both ends of the conpty's pipes, two to get passed into a process to use as input/output, and two for us to keep to
// make use of this data.
ptyIn, inPipeOurs, err := os.Pipe()
if err != nil {
return nil, fmt.Errorf("failed to create pipes for pseudo console: %w", err)
}

outPipeOurs, ptyOut, err := os.Pipe()
if err != nil {
return nil, fmt.Errorf("failed to create pipes for pseudo console: %w", err)
}

var hpc windows.Handle
coord := windows.Coord{X: width, Y: height}
err = winapi.CreatePseudoConsole(coord, windows.Handle(ptyIn.Fd()), windows.Handle(ptyOut.Fd()), 0, &hpc)
if err != nil {
return nil, fmt.Errorf("failed to create pseudo console: %w", err)
}

// The pty's end of its pipes can be closed here without worry. They're duped into the conhost
// that will be launched and will be released on a call to ClosePseudoConsole() (Close() on the ConPTY object).
if err := ptyOut.Close(); err != nil {
return nil, fmt.Errorf("failed to close pseudo console handle: %w", err)
}
if err := ptyIn.Close(); err != nil {
return nil, fmt.Errorf("failed to close pseudo console handle: %w", err)
}

return &ConPTY{
hpc: hpc,
inPipe: inPipeOurs,
outPipe: outPipeOurs,
}, nil
}

// UpdateProcThreadAttribute updates the passed in attribute list to contain the entry necessary for use with
// CreateProcess.
func (c *ConPTY) UpdateProcThreadAttribute(attributeList *winapi.ProcThreadAttributeList) error {
c.handleLock.RLock()
defer c.handleLock.RUnlock()

if c.hpc == 0 {
return errClosedConPty
}

err := winapi.UpdateProcThreadAttribute(
attributeList,
0,
winapi.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
unsafe.Pointer(c.hpc),
unsafe.Sizeof(c.hpc),
nil,
nil,
)
if err != nil {
return fmt.Errorf("failed to update proc thread attributes for pseudo console: %w", err)
}
return nil
}

// Resize resizes the internal buffers of the pseudo console to the passed in size
func (c *ConPTY) Resize(width, height int16) error {
c.handleLock.RLock()
defer c.handleLock.RUnlock()

if c.hpc == 0 {
return errClosedConPty
}

coord := windows.Coord{X: width, Y: height}
if err := winapi.ResizePseudoConsole(c.hpc, coord); err != nil {
return fmt.Errorf("failed to resize pseudo console: %w", err)
}
return nil
}

// Close closes the pseudo-terminal and cleans up all attached resources
func (c *ConPTY) Close() error {
c.handleLock.Lock()
defer c.handleLock.Unlock()

if c.hpc == 0 {
return errClosedConPty
}

// Close the pseudo console, set the handle to 0 to invalidate this object and then close the side of the pipes that we own.
winapi.ClosePseudoConsole(c.hpc)
c.hpc = 0
if err := c.inPipe.Close(); err != nil {
return fmt.Errorf("failed to close pseudo console input pipe: %w", err)
}
if err := c.outPipe.Close(); err != nil {
return fmt.Errorf("failed to close pseudo console output pipe: %w", err)
}
return nil
}

// OutPipe returns the output pipe of the pseudo console.
func (c *ConPTY) OutPipe() *os.File {
return c.outPipe
}

// InPipe returns the input pipe of the pseudo console.
func (c *ConPTY) InPipe() *os.File {
return c.inPipe
}

// Write writes the contents of `buf` to the pseudo console. Returns the number of bytes written and an error if there is one.
func (c *ConPTY) Write(buf []byte) (int, error) {
if c.inPipe == nil {
return 0, errNotInitialized
}
return c.inPipe.Write(buf)
}

// Read reads from the pseudo console into `buf`. Returns the number of bytes read and an error if there is one.
func (c *ConPTY) Read(buf []byte) (int, error) {
if c.outPipe == nil {
return 0, errNotInitialized
}
return c.outPipe.Read(buf)
}
44 changes: 44 additions & 0 deletions internal/winapi/console.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package winapi

import (
"unsafe"

"golang.org/x/sys/windows"
)

const PSEUDOCONSOLE_INHERIT_CURSOR = 0x1

// CreatePseudoConsole creates a windows pseudo console.
func CreatePseudoConsole(size windows.Coord, hInput windows.Handle, hOutput windows.Handle, dwFlags uint32, hpcon *windows.Handle) error {
// We need this wrapper as the function takes a COORD struct and not a pointer to one, so we need to cast to something beforehand.
return createPseudoConsole(*((*uint32)(unsafe.Pointer(&size))), hInput, hOutput, 0, hpcon)
}

// ResizePseudoConsole resizes the internal buffers of the pseudo console to the width and height specified in `size`.
func ResizePseudoConsole(hpcon windows.Handle, size windows.Coord) error {
// We need this wrapper as the function takes a COORD struct and not a pointer to one, so we need to cast to something beforehand.
return resizePseudoConsole(hpcon, *((*uint32)(unsafe.Pointer(&size))))
}

// HRESULT WINAPI CreatePseudoConsole(
// _In_ COORD size,
// _In_ HANDLE hInput,
// _In_ HANDLE hOutput,
// _In_ DWORD dwFlags,
// _Out_ HPCON* phPC
// );
//
//sys createPseudoConsole(size uint32, hInput windows.Handle, hOutput windows.Handle, dwFlags uint32, hpcon *windows.Handle) (hr error) = kernel32.CreatePseudoConsole

// void WINAPI ClosePseudoConsole(
// _In_ HPCON hPC
// );
//
//sys ClosePseudoConsole(hpc windows.Handle) = kernel32.ClosePseudoConsole

// HRESULT WINAPI ResizePseudoConsole(
// _In_ HPCON hPC ,
// _In_ COORD size
// );
//
//sys resizePseudoConsole(hPc windows.Handle, size uint32) (hr error) = kernel32.ResizePseudoConsole
83 changes: 78 additions & 5 deletions internal/winapi/process.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,83 @@
package winapi

import (
"errors"
"unsafe"

"golang.org/x/sys/windows"
)

const PROCESS_ALL_ACCESS uint32 = 2097151

// DWORD GetProcessImageFileNameW(
// HANDLE hProcess,
// LPWSTR lpImageFileName,
// DWORD nSize
const (
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x20016
PROC_THREAD_ATTRIBUTE_JOB_LIST = 0x2000D
)

type ProcThreadAttributeList struct {
_ [1]byte
}

// typedef struct _STARTUPINFOEXW {
// STARTUPINFOW StartupInfo;
// LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList;
// } STARTUPINFOEXW, *LPSTARTUPINFOEXW;
type StartupInfoEx struct {
// This is a recreation of the same binding from the stdlib. The x/sys/windows variant for whatever reason
// doesn't work when updating the list for the pseudo console attribute. It has the process immediately exit
// with exit code 0xc0000142 shortly after start.
//
// TODO (dcantah): Swap to the x/sys/windows definitions after https://go-review.googlesource.com/c/sys/+/371276/1/windows/exec_windows.go#153
// gets in.
windows.StartupInfo
ProcThreadAttributeList *ProcThreadAttributeList
}

// NewProcThreadAttributeList allocates a new ProcThreadAttributeList, with
// the requested maximum number of attributes. This must be cleaned up by calling
// DeleteProcThreadAttributeList.
func NewProcThreadAttributeList(maxAttrCount uint32) (*ProcThreadAttributeList, error) {
var size uintptr
err := initializeProcThreadAttributeList(nil, maxAttrCount, 0, &size)
if err != windows.ERROR_INSUFFICIENT_BUFFER {
if err == nil {
return nil, errors.New("unable to query buffer size from InitializeProcThreadAttributeList")
}
return nil, err
}
al := (*ProcThreadAttributeList)(unsafe.Pointer(&make([]byte, size)[0]))
err = initializeProcThreadAttributeList(al, maxAttrCount, 0, &size)
if err != nil {
return nil, err
}
return al, nil
}

// BOOL InitializeProcThreadAttributeList(
// [out, optional] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
// [in] DWORD dwAttributeCount,
// DWORD dwFlags,
// [in, out] PSIZE_T lpSize
// );
//sys GetProcessImageFileName(hProcess windows.Handle, imageFileName *uint16, nSize uint32) (size uint32, err error) = kernel32.GetProcessImageFileNameW
//
//sys initializeProcThreadAttributeList(lpAttributeList *ProcThreadAttributeList, dwAttributeCount uint32, dwFlags uint32, lpSize *uintptr) (err error) = kernel32.InitializeProcThreadAttributeList

// void DeleteProcThreadAttributeList(
// [in, out] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList
// );
//
//sys DeleteProcThreadAttributeList(lpAttributeList *ProcThreadAttributeList) = kernel32.DeleteProcThreadAttributeList

// BOOL UpdateProcThreadAttribute(
// [in, out] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
// [in] DWORD dwFlags,
// [in] DWORD_PTR Attribute,
// [in] PVOID lpValue,
// [in] SIZE_T cbSize,
// [out, optional] PVOID lpPreviousValue,
// [in, optional] PSIZE_T lpReturnSize
// );
//
//sys UpdateProcThreadAttribute(lpAttributeList *ProcThreadAttributeList, dwFlags uint32, attribute uintptr, lpValue unsafe.Pointer, cbSize uintptr, lpPreviousValue unsafe.Pointer, lpReturnSize *uintptr) (err error) = kernel32.UpdateProcThreadAttribute

//sys CreateProcessAsUser(token windows.Token, appName *uint16, commandLine *uint16, procSecurity *windows.SecurityAttributes, threadSecurity *windows.SecurityAttributes, inheritHandles bool, creationFlags uint32, env *uint16, currentDir *uint16, startupInfo *windows.StartupInfo, outProcInfo *windows.ProcessInformation) (err error) = advapi32.CreateProcessAsUserW
2 changes: 1 addition & 1 deletion internal/winapi/winapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
// be thought of as an extension to golang.org/x/sys/windows.
package winapi

//go:generate go run ..\..\mksyscall_windows.go -output zsyscall_windows.go system.go net.go path.go thread.go iocp.go jobobject.go logon.go memory.go process.go processor.go devices.go filesystem.go errors.go
//go:generate go run ..\..\mksyscall_windows.go -output zsyscall_windows.go console.go system.go net.go path.go thread.go iocp.go jobobject.go logon.go memory.go process.go processor.go devices.go filesystem.go errors.go
Loading

0 comments on commit f8cbd0b

Please sign in to comment.