Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(term): add term/conpty #28

Merged
merged 4 commits into from
Jan 10, 2024
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
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ use (
./exp/slice
./exp/strings
./exp/teatest
./term
)
8 changes: 2 additions & 6 deletions go.work.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,2 @@
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
284 changes: 284 additions & 0 deletions term/conpty/conpty_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
//go:build windows
// +build windows

package conpty

import (
"errors"
"fmt"
"io"
"os"
"syscall"
"unsafe"

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

// Default size.
const (
DefaultWidth = 80
DefaultHeight = 25
)

// ConPty represents a Windows Console Pseudo-terminal.
// https://learn.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session#preparing-the-communication-channels
type ConPty struct {
hpc *windows.Handle
inPipeFd, outPipeFd windows.Handle
inPipe, outPipe *os.File
attrList *windows.ProcThreadAttributeListContainer
size windows.Coord
}

var (
_ io.Writer = &ConPty{}
_ io.Reader = &ConPty{}
)

// New creates a new ConPty device.
// Accepts a custom width, height, and flags that will get passed to
// windows.CreatePseudoConsole.
func New(w int, h int, flags int) (c *ConPty, err error) {
if w <= 0 {
w = DefaultWidth
}
if h <= 0 {
h = DefaultHeight
}

c = &ConPty{
hpc: new(windows.Handle),
size: windows.Coord{
X: int16(w), Y: int16(h),
},
}

var ptyIn, ptyOut windows.Handle
if err := windows.CreatePipe(&ptyIn, &c.inPipeFd, nil, 0); err != nil {
return nil, fmt.Errorf("failed to create pipes for pseudo console: %w", err)
}

if err := windows.CreatePipe(&c.outPipeFd, &ptyOut, nil, 0); err != nil {
return nil, fmt.Errorf("failed to create pipes for pseudo console: %w", err)
}

if err := windows.CreatePseudoConsole(c.size, ptyIn, ptyOut, uint32(flags), c.hpc); err != nil {
return nil, fmt.Errorf("failed to create pseudo console: %w", err)
}

// We don't need the pty pipes anymore, these will get dup'd when the
// new process starts.
if err := windows.CloseHandle(ptyOut); err != nil {
return nil, fmt.Errorf("failed to close pseudo console handle: %w", err)
}
if err := windows.CloseHandle(ptyIn); err != nil {
return nil, fmt.Errorf("failed to close pseudo console handle: %w", err)
}

c.inPipe = os.NewFile(uintptr(c.inPipeFd), "|0")
c.outPipe = os.NewFile(uintptr(c.outPipeFd), "|1")

// Allocate an attribute list that's large enough to do the operations we care about
// 1. Pseudo console setup
c.attrList, err = windows.NewProcThreadAttributeList(1)
if err != nil {
return nil, err
}

if err := c.attrList.Update(
windows.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
unsafe.Pointer(*c.hpc),
unsafe.Sizeof(*c.hpc),
); err != nil {
return nil, fmt.Errorf("failed to update proc thread attributes for pseudo console: %w", err)
}

return
}

// Fd returns the ConPty handle.
func (p *ConPty) Fd() uintptr {
return uintptr(*p.hpc)
}

// Close closes the ConPty device.
func (p *ConPty) Close() error {
if p.attrList != nil {
p.attrList.Delete()
}

windows.ClosePseudoConsole(*p.hpc)
return errors.Join(p.inPipe.Close(), p.outPipe.Close())
}

// InPipe returns the ConPty input pipe.
func (p *ConPty) InPipe() *os.File {
return p.inPipe
}

// InPipeFd returns the ConPty input pipe file descriptor handle.
func (p *ConPty) InPipeFd() uintptr {
return uintptr(p.inPipeFd)
}

// OutPipe returns the ConPty output pipe.
func (p *ConPty) OutPipe() *os.File {
return p.outPipe
}

// OutPipeFd returns the ConPty output pipe file descriptor handle.
func (p *ConPty) OutPipeFd() uintptr {
return uintptr(p.outPipeFd)
}

// Write safely writes bytes to the ConPty.
func (c *ConPty) Write(p []byte) (n int, err error) {
var l uint32
err = windows.WriteFile(c.inPipeFd, p, &l, nil)
return int(l), err
}

// Read safely reads bytes from the ConPty.
func (c *ConPty) Read(p []byte) (n int, err error) {
var l uint32
err = windows.ReadFile(c.outPipeFd, p, &l, nil)
return int(l), err
}

// Resize resizes the pseudo-console.
func (c *ConPty) Resize(w int, h int) error {
size := windows.Coord{X: int16(w), Y: int16(h)}
if err := windows.ResizePseudoConsole(*c.hpc, size); err != nil {
return fmt.Errorf("failed to resize pseudo console: %w", err)
}
c.size = size
return nil
}

// Size returns the current pseudo-console size.
func (c *ConPty) Size() (w int, h int) {
w = int(c.size.X)
h = int(c.size.Y)
return
}

var zeroAttr syscall.ProcAttr

// Spawn spawns a new process attached to the pseudo-console.
func (c *ConPty) Spawn(name string, args []string, attr *syscall.ProcAttr) (pid int, handle uintptr, err error) {
if attr == nil {
attr = &zeroAttr
}

argv0, err := lookExtensions(name, attr.Dir)
if err != nil {
return 0, 0, err
}
if len(attr.Dir) != 0 {
// Windows CreateProcess looks for argv0 relative to the current
// directory, and, only once the new process is started, it does
// Chdir(attr.Dir). We are adjusting for that difference here by
// making argv0 absolute.
var err error
argv0, err = joinExeDirAndFName(attr.Dir, argv0)
if err != nil {
return 0, 0, err
}
}

argv0p, err := windows.UTF16PtrFromString(argv0)
if err != nil {
return 0, 0, err
}

var cmdline string
if attr.Sys != nil && attr.Sys.CmdLine != "" {
cmdline = attr.Sys.CmdLine
} else {
cmdline = windows.ComposeCommandLine(args)
}
argvp, err := windows.UTF16PtrFromString(cmdline)
if err != nil {
return 0, 0, err
}

var dirp *uint16
if len(attr.Dir) != 0 {
dirp, err = windows.UTF16PtrFromString(attr.Dir)
if err != nil {
return 0, 0, err
}
}

if attr.Env == nil {
attr.Env, err = execEnvDefault(attr.Sys)
if err != nil {
return 0, 0, err
}
}

siEx := new(windows.StartupInfoEx)
siEx.Flags = windows.STARTF_USESTDHANDLES

pi := new(windows.ProcessInformation)

// Need EXTENDED_STARTUPINFO_PRESENT as we're making use of the attribute list field.
flags := uint32(windows.CREATE_UNICODE_ENVIRONMENT) | windows.EXTENDED_STARTUPINFO_PRESENT
if attr.Sys != nil && attr.Sys.CreationFlags != 0 {
flags |= attr.Sys.CreationFlags
}

var zeroSec windows.SecurityAttributes
pSec := &windows.SecurityAttributes{Length: uint32(unsafe.Sizeof(zeroSec)), InheritHandle: 1}
if attr.Sys != nil && attr.Sys.ProcessAttributes != nil {
pSec = &windows.SecurityAttributes{
Length: attr.Sys.ProcessAttributes.Length,
InheritHandle: attr.Sys.ProcessAttributes.InheritHandle,
}
}
tSec := &windows.SecurityAttributes{Length: uint32(unsafe.Sizeof(zeroSec)), InheritHandle: 1}
if attr.Sys != nil && attr.Sys.ThreadAttributes != nil {
tSec = &windows.SecurityAttributes{
Length: attr.Sys.ThreadAttributes.Length,
InheritHandle: attr.Sys.ThreadAttributes.InheritHandle,
}
}

siEx.ProcThreadAttributeList = c.attrList.List() //nolint:govet // unusedwrite: ProcThreadAttributeList will be read in syscall
siEx.Cb = uint32(unsafe.Sizeof(*siEx))
if attr.Sys != nil && attr.Sys.Token != 0 {
err = windows.CreateProcessAsUser(
windows.Token(attr.Sys.Token),
argv0p,
argvp,
pSec,
tSec,
false,
flags,
createEnvBlock(addCriticalEnv(dedupEnvCase(true, attr.Env))),
dirp,
&siEx.StartupInfo,
pi,
)
} else {
err = windows.CreateProcess(
argv0p,
argvp,
pSec,
tSec,
false,
flags,
createEnvBlock(addCriticalEnv(dedupEnvCase(true, attr.Env))),
dirp,
&siEx.StartupInfo,
pi,
)
}
if err != nil {
return 0, 0, fmt.Errorf("failed to create process: %w", err)
}

defer windows.CloseHandle(pi.Thread)

return int(pi.ProcessId), uintptr(pi.Process), nil
}
5 changes: 5 additions & 0 deletions term/conpty/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Package conpty implements Windows Console Pseudo-terminal support.
//
// https://learn.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session

package conpty
Loading
Loading