Skip to content

Commit

Permalink
feat: use interfaces and switch to windows package
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed Oct 25, 2023
1 parent e1b4b5d commit 3c49ba9
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 145 deletions.
2 changes: 1 addition & 1 deletion cmd_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func (c *Cmd) start() error {
return errors.New("exec: already started")
}

pty, ok := c.pty.(*UnixPty)
pty, ok := c.pty.(*unixPty)
if !ok {
return ErrInvalidCommand
}
Expand Down
2 changes: 1 addition & 1 deletion cmd_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type conPtySys struct {
}

func (c *Cmd) start() error {
pty, ok := c.pty.(*ConPty)
pty, ok := c.pty.(*conPty)
if !ok {
return ErrInvalidCommand
}
Expand Down
8 changes: 4 additions & 4 deletions examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ replace github.com/creack/pty => github.com/aymanbagabas/pty v1.1.19-0.202309220
require (
github.com/aymanbagabas/go-pty v0.0.0-00010101000000-000000000000
github.com/charmbracelet/ssh v0.0.0-20230822194956-1a051f898e09
github.com/u-root/u-root v0.11.0
golang.org/x/crypto v0.13.0
golang.org/x/term v0.12.0
golang.org/x/term v0.13.0
)

require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/creack/pty v1.1.15 // indirect
golang.org/x/sys v0.12.0 // indirect
github.com/u-root/u-root v0.11.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/sys v0.13.0 // indirect
)
12 changes: 6 additions & 6 deletions examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90 h1:zTk5683I9K
github.com/u-root/u-root v0.11.0 h1:6gCZLOeRyevw7gbTwMj3fKxnr9+yHFlgF3N7udUVNO8=
github.com/u-root/u-root v0.11.0/go.mod h1:DBkDtiZyONk9hzVEdB/PWI9B4TxDkElWlVTHseglrZY=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
29 changes: 29 additions & 0 deletions pty.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"io"
"os"
)

var (
Expand Down Expand Up @@ -45,3 +46,31 @@ type Pty interface {
// On Windows, this will return the handle of the console.
Fd() uintptr
}

// UnixPty is a Unix pseudo-terminal interface.
type UnixPty interface {
Pty

// Master returns the pseudo-terminal master end (pty).
Master() *os.File

// Slave returns the pseudo-terminal slave end (tty).
Slave() *os.File

// Control calls f on the pseudo-terminal master end (pty).
Control(f func(fd uintptr)) error

// SetWinsize sets the pseudo-terminal window size.
SetWinsize(ws *Winsize) error
}

// ConPty is a Windows ConPTY interface.
type ConPty interface {
Pty

// InputPipe returns the ConPty input pipe.
InputPipe() *os.File

// OutputPipe returns the ConPty output pipe.
OutputPipe() *os.File
}
47 changes: 26 additions & 21 deletions pty_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ import (
"golang.org/x/sys/unix"
)

// UnixPty is a POSIX compliant Unix pseudo-terminal.
// unixPty is a POSIX compliant Unix pseudo-terminal.
// See: https://pubs.opengroup.org/onlinepubs/9699919799/
type UnixPty struct {
type unixPty struct {
master, slave *os.File
closed bool
}

var _ Pty = &UnixPty{}
var _ Pty = &unixPty{}

// Close implements Pty.
func (p *UnixPty) Close() error {
func (p *unixPty) Close() error {
if p.closed {
return nil
}
Expand All @@ -33,7 +33,7 @@ func (p *UnixPty) Close() error {
}

// Command implements Pty.
func (p *UnixPty) Command(name string, args ...string) *Cmd {
func (p *unixPty) Command(name string, args ...string) *Cmd {
c := &Cmd{
pty: p,
Path: name,
Expand All @@ -43,47 +43,52 @@ func (p *UnixPty) Command(name string, args ...string) *Cmd {
}

// CommandContext implements Pty.
func (p *UnixPty) CommandContext(ctx context.Context, name string, args ...string) *Cmd {
func (p *unixPty) CommandContext(ctx context.Context, name string, args ...string) *Cmd {
c := p.Command(name, args...)
c.ctx = ctx
return c
}

// Name implements Pty.
func (p *UnixPty) Name() string {
func (p *unixPty) Name() string {
return p.slave.Name()
}

// Read implements Pty.
func (p *UnixPty) Read(b []byte) (n int, err error) {
func (p *unixPty) Read(b []byte) (n int, err error) {
return p.master.Read(b)
}

func (p *UnixPty) Control(f func(fd uintptr)) error {
// Control implements UnixPty.
func (p *unixPty) Control(f func(fd uintptr)) error {
return p.control(f)
}

func (p *unixPty) control(f func(fd uintptr)) error {
conn, err := p.master.SyscallConn()
if err != nil {
return err
}
return conn.Control(f)
}

// Master returns the pseudo-terminal master end (pty).
func (p *UnixPty) Master() *os.File {
// Master implements UnixPty.
func (p *unixPty) Master() *os.File {
return p.master
}

// Slave returns the pseudo-terminal slave end (tty).
func (p *UnixPty) Slave() *os.File {
// Slave implements UnixPty.
func (p *unixPty) Slave() *os.File {
return p.slave
}

// Winsize represents the terminal window size.
type Winsize = unix.Winsize

// SetWinsize sets the pseudo-terminal window size.
func (p *UnixPty) SetWinsize(ws *Winsize) error {
// SetWinsize implements UnixPty.
func (p *unixPty) SetWinsize(ws *Winsize) error {
var ctrlErr error
if err := p.Control(func(fd uintptr) {
if err := p.control(func(fd uintptr) {
ctrlErr = unix.IoctlSetWinsize(int(fd), unix.TIOCSWINSZ, ws)
}); err != nil {
return err
Expand All @@ -93,30 +98,30 @@ func (p *UnixPty) SetWinsize(ws *Winsize) error {
}

// Resize implements Pty.
func (p *UnixPty) Resize(width int, height int) error {
func (p *unixPty) Resize(width int, height int) error {
return p.SetWinsize(&Winsize{
Row: uint16(height),
Col: uint16(width),
})
}

// Write implements Pty.
func (p *UnixPty) Write(b []byte) (n int, err error) {
func (p *unixPty) Write(b []byte) (n int, err error) {
return p.master.Write(b)
}

// Fd implements Pty.
func (p *UnixPty) Fd() uintptr {
func (p *unixPty) Fd() uintptr {
return p.master.Fd()
}

func newPty() (Pty, error) {
func newPty() (UnixPty, error) {
master, slave, err := pty.Open()
if err != nil {
return nil, err
}

return &UnixPty{
return &unixPty{
master: master,
slave: slave,
}, nil
Expand Down
64 changes: 27 additions & 37 deletions pty_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,20 @@ var (
errNotStarted = errors.New("process not started")
)

// Install this from github.com/Microsoft/go-winio
// go install github.com/Microsoft/go-winio/tools/mkwinsyscall@latest
//go:generate mkwinsyscall -output zsyscall_windows.go ./*.go

// ConPty is a Windows console pseudo-terminal.
// conPty is a Windows console pseudo-terminal.
// It uses Windows pseudo console API to create a console that can be used to
// start processes attached to it.
//
// See: https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session
type ConPty struct {
type conPty struct {
handle windows.Handle
inPipe, outPipe *os.File
mtx sync.RWMutex
}

var _ Pty = &ConPty{}
var _ Pty = &conPty{}

func newPty() (Pty, error) {
func newPty() (ConPty, error) {
ptyIn, inPipeOurs, err := os.Pipe()
if err != nil {
return nil, fmt.Errorf("failed to create pipes for pseudo console: %w", err)
Expand All @@ -53,7 +49,7 @@ func newPty() (Pty, error) {

var hpc windows.Handle
coord := windows.Coord{X: 80, Y: 25}
err = createPseudoConsole(coord, windows.Handle(ptyIn.Fd()), windows.Handle(ptyOut.Fd()), 0, &hpc)
err = windows.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)
}
Expand All @@ -65,24 +61,24 @@ func newPty() (Pty, error) {
return nil, fmt.Errorf("failed to close pseudo console handle: %w", err)
}

return &ConPty{
return &conPty{
handle: hpc,
inPipe: inPipeOurs,
outPipe: outPipeOurs,
}, nil
}

// Close implements Pty.
func (p *ConPty) Close() error {
func (p *conPty) Close() error {
p.mtx.Lock()
defer p.mtx.Unlock()

closePseudoConsole(p.handle)
windows.ClosePseudoConsole(p.handle)
return errors.Join(p.inPipe.Close(), p.outPipe.Close())
}

// Command implements Pty.
func (p *ConPty) Command(name string, args ...string) *Cmd {
func (p *conPty) Command(name string, args ...string) *Cmd {
c := &Cmd{
pty: p,
Path: name,
Expand All @@ -92,7 +88,7 @@ func (p *ConPty) Command(name string, args ...string) *Cmd {
}

// CommandContext implements Pty.
func (p *ConPty) CommandContext(ctx context.Context, name string, args ...string) *Cmd {
func (p *conPty) CommandContext(ctx context.Context, name string, args ...string) *Cmd {
if ctx == nil {
panic("nil context")
}
Expand All @@ -105,40 +101,50 @@ func (p *ConPty) CommandContext(ctx context.Context, name string, args ...string
}

// Name implements Pty.
func (*ConPty) Name() string {
func (*conPty) Name() string {
return "windows-pty"
}

// Read implements Pty.
func (p *ConPty) Read(b []byte) (n int, err error) {
func (p *conPty) Read(b []byte) (n int, err error) {
return p.outPipe.Read(b)
}

// Resize implements Pty.
func (p *ConPty) Resize(width int, height int) error {
func (p *conPty) Resize(width int, height int) error {
p.mtx.RLock()
defer p.mtx.RUnlock()
if err := resizePseudoConsole(p.handle, windows.Coord{X: int16(height), Y: int16(width)}); err != nil {
if err := windows.ResizePseudoConsole(p.handle, windows.Coord{X: int16(height), Y: int16(width)}); err != nil {
return fmt.Errorf("failed to resize pseudo console: %w", err)
}
return nil
}

// Write implements Pty.
func (p *ConPty) Write(b []byte) (n int, err error) {
func (p *conPty) Write(b []byte) (n int, err error) {
return p.inPipe.Write(b)
}

// Fd implements Pty.
func (p *ConPty) Fd() uintptr {
func (p *conPty) Fd() uintptr {
p.mtx.RLock()
defer p.mtx.RUnlock()
return uintptr(p.handle)
}

// InputPipe implements ConPty.
func (p *conPty) InputPipe() *os.File {
return p.inPipe
}

// OutputPipe implements ConPty.
func (p *conPty) OutputPipe() *os.File {
return p.outPipe
}

// updateProcThreadAttribute updates the passed in attribute list to contain the entry necessary for use with
// CreateProcess.
func (p *ConPty) updateProcThreadAttribute(attrList *windows.ProcThreadAttributeListContainer) error {
func (p *conPty) updateProcThreadAttribute(attrList *windows.ProcThreadAttributeListContainer) error {
p.mtx.RLock()
defer p.mtx.RUnlock()

Expand All @@ -156,19 +162,3 @@ func (p *ConPty) updateProcThreadAttribute(attrList *windows.ProcThreadAttribute

return nil
}

// 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, dwFlags, 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))))
}

//sys _createPseudoConsole(size uint32, hInput windows.Handle, hOutput windows.Handle, dwFlags uint32, hpcon *windows.Handle) (hr error) = kernel32.CreatePseudoConsole
//sys _resizePseudoConsole(hPc windows.Handle, size uint32) (hr error) = kernel32.ResizePseudoConsole
//sys closePseudoConsole(hpc windows.Handle) = kernel32.ClosePseudoConsole
12 changes: 12 additions & 0 deletions winsize_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris
// +build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris

package pty

// Winsize is used to set the size of a terminal.
type Winsize struct {
Row uint16
Col uint16
Xpixel uint16
Ypixel uint16
}

0 comments on commit 3c49ba9

Please sign in to comment.