Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Comment thread
katiewasnothere marked this conversation as resolved.
// 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)
Comment thread
dcantah marked this conversation as resolved.
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