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

Support pty on windows with ConPty api #109

Closed
wants to merge 13 commits into from
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pty

Pty is a Go package for using unix pseudo-terminals.
Pty is a Go package for using unix pseudo-terminals and windows ConPty.
creack marked this conversation as resolved.
Show resolved Hide resolved

## Install

Expand All @@ -12,6 +12,8 @@ go get github.com/creack/pty

Note that those examples are for demonstration purpose only, to showcase how to use the library. They are not meant to be used in any kind of production environment.

__NOTE:__ This package requires `ConPty` support on windows platform, please make sure your windows system meet [these requirements](https://docs.microsoft.com/en-us/windows/console/createpseudoconsole#requirements)

### Command

```go
Expand Down
146 changes: 146 additions & 0 deletions cmd_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//go:build windows
// +build windows

package pty

import (
"context"
"io"
"os"
"syscall"

_ "unsafe" // for go:linkname
)

// copied from os/exec.Cmd for platform compatibility
// we need to use startupInfoEx for pty support, but os/exec.Cmd only have
// support for startupInfo on windows, so we have to rewrite some internal
// logic for windows while keep its behavior compatible with other platforms.

// cmd represents an external command being prepared or run.
//
// A cmd cannot be reused after calling its Run, Output or CombinedOutput
// methods.
//go:linkname cmd os/exec.Cmd
type cmd struct {
// Path is the path of the command to run.
//
// This is the only field that must be set to a non-zero
// value. If Path is relative, it is evaluated relative
// to Dir.
Path string

// Args holds command line arguments, including the command as Args[0].
// If the Args field is empty or nil, Run uses {Path}.
//
// In typical use, both Path and Args are set by calling Command.
Args []string

// Env specifies the environment of the process.
// Each entry is of the form "key=value".
// If Env is nil, the new process uses the current process's
// environment.
// If Env contains duplicate environment keys, only the last
// value in the slice for each duplicate key is used.
// As a special case on Windows, SYSTEMROOT is always added if
// missing and not explicitly set to the empty string.
Env []string

// Dir specifies the working directory of the command.
// If Dir is the empty string, Run runs the command in the
// calling process's current directory.
Dir string

// Stdin specifies the process's standard input.
//
// If Stdin is nil, the process reads from the null device (os.DevNull).
//
// If Stdin is an *os.File, the process's standard input is connected
// directly to that file.
//
// Otherwise, during the execution of the command a separate
// goroutine reads from Stdin and delivers that data to the command
// over a pipe. In this case, Wait does not complete until the goroutine
// stops copying, either because it has reached the end of Stdin
// (EOF or a read error) or because writing to the pipe returned an error.
Stdin io.Reader

// Stdout and Stderr specify the process's standard output and error.
//
// If either is nil, Run connects the corresponding file descriptor
// to the null device (os.DevNull).
//
// If either is an *os.File, the corresponding output from the process
// is connected directly to that file.
//
// Otherwise, during the execution of the command a separate goroutine
// reads from the process over a pipe and delivers that data to the
// corresponding Writer. In this case, Wait does not complete until the
// goroutine reaches EOF or encounters an error.
//
// If Stdout and Stderr are the same writer, and have a type that can
// be compared with ==, at most one goroutine at a time will call Write.
Stdout io.Writer
Stderr io.Writer

// ExtraFiles specifies additional open files to be inherited by the
// new process. It does not include standard input, standard output, or
// standard error. If non-nil, entry i becomes file descriptor 3+i.
//
// ExtraFiles is not supported on Windows.
ExtraFiles []*os.File

// SysProcAttr holds optional, operating system-specific attributes.
// Run passes it to os.StartProcess as the os.ProcAttr's Sys field.
SysProcAttr *syscall.SysProcAttr

// Process is the underlying process, once started.
Process *os.Process

// ProcessState contains information about an exited process,
// available after a call to Wait or Run.
ProcessState *os.ProcessState

ctx context.Context // nil means none
lookPathErr error // LookPath error, if any.
finished bool // when Wait was called
childFiles []*os.File
closeAfterStart []io.Closer
closeAfterWait []io.Closer
goroutine []func() error
errch chan error // one send per goroutine
waitDone chan struct{}
}

//go:linkname _cmd_closeDescriptors os/exec.(*Cmd).closeDescriptors
func _cmd_closeDescriptors(c *cmd, closers []io.Closer)

//go:linkname _cmd_envv os/exec.(*Cmd).envv
func _cmd_envv(c *cmd) ([]string, error)

//go:linkname _cmd_argv os/exec.(*Cmd).argv
func _cmd_argv(c *cmd) []string

//go:linkname lookExtensions os/exec.lookExtensions
func lookExtensions(path, dir string) (string, error)

//go:linkname dedupEnv os/exec.dedupEnv
func dedupEnv(env []string) []string

//go:linkname addCriticalEnv os/exec.addCriticalEnv
func addCriticalEnv(env []string) []string

//go:linkname newProcess os.newProcess
func newProcess(pid int, handle uintptr) *os.Process

//go:linkname execEnvDefault internal/syscall/execenv.Default
func execEnvDefault(sys *syscall.SysProcAttr) (env []string, err error)

//go:linkname createEnvBlock syscall.createEnvBlock
func createEnvBlock(envv []string) *uint16

//go:linkname makeCmdLine syscall.makeCmdLine
func makeCmdLine(args []string) string

//go:linkname joinExeDirAndFName syscall.joinExeDirAndFName
func joinExeDirAndFName(dir, p string) (name string, err error)
16 changes: 16 additions & 0 deletions cmd_windows.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// disable default -complete option to `go tool compile` on windows

// We are using `go:linkname` to support golang os/exec.Cmd for extended
// windows process creation (startupInfoEx), and that is not supported by its
// standard library implementation.

// By default, the go compiler will require all functions in *.go files
// with body defined, if that's not the case, we have to enable CGO to
// enable symbol lookup. One solution to disable that compile time check
// is to add some go assembly file to your project.

// For this project, we don't use CGO at all, and should not require users
// to set `CGO_ENABLED=1` when compiling their projects using this package.

// By adding this empty assembly file, the go compiler will enable symbol
// lookup, so that we can have functions with no body defined in *.go files.
32 changes: 30 additions & 2 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,42 @@ package pty

import (
"errors"
"os"
"io"
)

// ErrUnsupported is returned if a function is not
// available on the current platform.
var ErrUnsupported = errors.New("unsupported")

// Open a pty and its corresponding tty.
func Open() (pty, tty *os.File, err error) {
func Open() (Pty, Tty, error) {
This conversation was marked as resolved.
Show resolved Hide resolved
return open()
}

type (
FdHolder interface {
Fd() uintptr
}

// Pty for terminal control in current process
// for unix systems, the real type is *os.File
// for windows, the real type is a *WindowsPty for ConPTY handle
Pty interface {
// Fd intended to resize Tty of child process in current process
FdHolder

// WriteString is only used to identify Pty and Tty
WriteString(s string) (n int, err error)
io.ReadWriteCloser
}

// Tty for data i/o in child process
// for unix systems, the real type is *os.File
// for windows, the real type is a *WindowsTty, which is a combination of two pipe file
Tty interface {
// Fd only intended for manual InheritSize from Pty
FdHolder

io.ReadWriteCloser
}
)
Empty file added go.sum
Empty file.
4 changes: 2 additions & 2 deletions pty_unsupported.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris
//+build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris
//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris && !windows
//+build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris,!windows

package pty

Expand Down
159 changes: 159 additions & 0 deletions pty_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//go:build windows
// +build windows

package pty

import (
"os"
"syscall"
"unsafe"
)

var (
This conversation was marked as resolved.
Show resolved Hide resolved
// NOTE(security): as noted by the comment of syscall.NewLazyDLL and syscall.LoadDLL
// user need to call internal/syscall/windows/sysdll.Add("kernel32.dll") to make sure
// the kernel32.dll is loaded from windows system path
//
// ref: https://pkg.go.dev/syscall@go1.13?GOOS=windows#LoadDLL
kernel32DLL = syscall.NewLazyDLL("kernel32.dll")

// https://docs.microsoft.com/en-us/windows/console/createpseudoconsole
createPseudoConsole = kernel32DLL.NewProc("CreatePseudoConsole")
closePseudoConsole = kernel32DLL.NewProc("ClosePseudoConsole")

deleteProcThreadAttributeList = kernel32DLL.NewProc("DeleteProcThreadAttributeList")
initializeProcThreadAttributeList = kernel32DLL.NewProc("InitializeProcThreadAttributeList")

// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-updateprocthreadattribute
updateProcThreadAttribute = kernel32DLL.NewProc("UpdateProcThreadAttribute")

resizePseudoConsole = kernel32DLL.NewProc("ResizePseudoConsole")
getConsoleScreenBufferInfo = kernel32DLL.NewProc("GetConsoleScreenBufferInfo")
)

func open() (_ Pty, _ Tty, err error) {
pr, consoleW, err := os.Pipe()
if err != nil {
return nil, nil, err
}

consoleR, pw, err := os.Pipe()
if err != nil {
_ = consoleW.Close()
_ = pr.Close()
return nil, nil, err
}

defer func() {
if err != nil {
_ = consoleW.Close()
_ = pr.Close()

_ = pw.Close()
_ = consoleR.Close()
}
}()

err = createPseudoConsole.Find()
if err != nil {
return nil, nil, err
}

var consoleHandle syscall.Handle
r1, _, err := createPseudoConsole.Call(
(windowsCoord{X: 80, Y: 30}).Pack(), // size: default 80x30 window
consoleR.Fd(), // console input
consoleW.Fd(), // console output
0, // console flags, currently only PSEUDOCONSOLE_INHERIT_CURSOR supported
uintptr(unsafe.Pointer(&consoleHandle)), // console handler value return
)
if r1 != 0 {
// S_OK: 0
return nil, nil, os.NewSyscallError("CreatePseudoConsole", err)
}

return &WindowsPty{
handle: uintptr(consoleHandle),
r: pr,
w: pw,
consoleR: consoleR,
consoleW: consoleW,
}, &WindowsTty{
handle: uintptr(consoleHandle),
r: consoleR,
w: consoleW,
}, nil
}

var _ Pty = (*WindowsPty)(nil)

type WindowsPty struct {
handle uintptr
r, w *os.File

consoleR, consoleW *os.File
}

func (p *WindowsPty) Fd() uintptr {
return p.handle
}

func (p *WindowsPty) Read(data []byte) (int, error) {
return p.r.Read(data)
}

func (p *WindowsPty) Write(data []byte) (int, error) {
return p.w.Write(data)
}

func (p *WindowsPty) WriteString(s string) (int, error) {
return p.w.WriteString(s)
}

func (p *WindowsPty) InputPipe() *os.File {
return p.w
}

func (p *WindowsPty) OutputPipe() *os.File {
return p.r
}

func (p *WindowsPty) Close() error {
_ = p.r.Close()
_ = p.w.Close()

_ = p.consoleR.Close()
_ = p.consoleW.Close()

err := closePseudoConsole.Find()
if err != nil {
return err
}

_, _, err = closePseudoConsole.Call(p.handle)
return err
}

var _ Tty = (*WindowsTty)(nil)

type WindowsTty struct {
handle uintptr
r, w *os.File
}

func (t *WindowsTty) Fd() uintptr {
return t.handle
}

func (t *WindowsTty) Read(p []byte) (int, error) {
return t.r.Read(p)
}

func (t *WindowsTty) Write(p []byte) (int, error) {
return t.w.Write(p)
}

func (t *WindowsTty) Close() error {
_ = t.r.Close()
return t.w.Close()
}
Loading