Skip to content
Closed
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.
Comment thread
creack marked this conversation as 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) {
Comment thread
This conversation was marked as 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 (
Comment thread
This conversation was marked as 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