new windows support

This commit is contained in:
photostorm
2022-07-25 17:24:45 -04:00
parent 0d412c9fbe
commit 580600d366
17 changed files with 1035 additions and 137 deletions

View File

@@ -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.
## Install
@@ -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

391
cmd_windows.go Normal file
View File

@@ -0,0 +1,391 @@
//go:build windows
// +build windows
package pty
import (
"bytes"
"errors"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"unicode/utf16"
"unsafe"
"golang.org/x/sys/windows"
)
// 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.
// WindowExecCmd represents an external command being prepared or run.
//
// A cmd cannot be reused after calling its Run, Output or CombinedOutput
// methods.
type WindowExecCmd struct {
cmd *exec.Cmd
waitCalled bool
consoleHandle syscall.Handle
tty *WindowsTty
conPty *WindowsPty
attrList *windows.ProcThreadAttributeListContainer
}
var errProcessNotStarted = errors.New("exec: process has not started yet")
func (c *WindowExecCmd) close() error {
c.attrList.Delete()
_ = c.conPty.Close()
_ = c.tty.Close()
return nil
}
func (c *WindowExecCmd) Run() error {
err := c.Start()
if err != nil {
return err
}
return c.Wait()
}
func (c *WindowExecCmd) Wait() error {
if c.cmd.Process == nil {
return errProcessNotStarted
}
var err error
if c.waitCalled {
return errors.New("exec: wait was already called")
}
c.waitCalled = true
c.cmd.ProcessState, err = c.cmd.Process.Wait()
if err != nil {
return err
}
err = c.close()
if err != nil {
return err
}
if !c.cmd.ProcessState.Success() {
return &exec.ExitError{ProcessState: c.cmd.ProcessState}
}
return nil
}
func (c *WindowExecCmd) StdinPipe() (io.WriteCloser, error) {
if c.cmd.Stdin != nil {
return nil, errors.New("exec: Stdin already set")
}
if c.cmd.Process != nil {
return nil, errors.New("exec: StdinPipe after process started")
}
if c.conPty != nil {
return c.conPty.InputPipe(), nil
}
return nil, ErrUnsupported
}
func (c *WindowExecCmd) StdoutPipe() (io.ReadCloser, error) {
if c.cmd.Stdout != nil {
return nil, errors.New("exec: Stdout already set")
}
if c.cmd.Process != nil {
return nil, errors.New("exec: StdoutPipe after process started")
}
if c.conPty != nil {
return c.conPty.OutputPipe(), nil
}
return nil, ErrUnsupported
}
func (c *WindowExecCmd) StderrPipe() (io.ReadCloser, error) {
if c.cmd.Stderr != nil {
return nil, errors.New("exec: Stderr already set")
}
if c.cmd.Process != nil {
return nil, errors.New("exec: StderrPipe after process started")
}
if c.conPty != nil {
return c.conPty.OutputPipe(), nil
}
return nil, ErrUnsupported
}
func (c *WindowExecCmd) Output() ([]byte, error) {
if c.cmd.Stdout != nil {
return nil, errors.New("exec: Stdout already set")
}
var stdout bytes.Buffer
c.cmd.Stdout = &stdout
err := c.Run()
return stdout.Bytes(), err
}
func (c *WindowExecCmd) CombinedOutput() ([]byte, error) {
if c.cmd.Stdout != nil {
return nil, errors.New("exec: Stdout already set")
}
if c.cmd.Stderr != nil {
return nil, errors.New("exec: Stderr already set")
}
var b bytes.Buffer
c.cmd.Stdout = &b
c.cmd.Stderr = &b
err := c.Run()
return b.Bytes(), err
}
func (c *WindowExecCmd) argv() []string {
if len(c.cmd.Args) > 0 {
return c.cmd.Args
}
return []string{c.cmd.Path}
}
//
// Helpers for working with Windows. These are exact copies of the same utilities found in the go stdlib.
//
func lookExtensions(path, dir string) (string, error) {
if filepath.Base(path) == path {
path = filepath.Join(".", path)
}
if dir == "" {
return exec.LookPath(path)
}
if filepath.VolumeName(path) != "" {
return exec.LookPath(path)
}
if len(path) > 1 && os.IsPathSeparator(path[0]) {
return exec.LookPath(path)
}
dirandpath := filepath.Join(dir, path)
// We assume that LookPath will only add file extension.
lp, err := exec.LookPath(dirandpath)
if err != nil {
return "", err
}
ext := strings.TrimPrefix(lp, dirandpath)
return path + ext, nil
}
func dedupEnvCase(caseInsensitive bool, env []string) []string {
// Construct the output in reverse order, to preserve the
// last occurrence of each key.
out := make([]string, 0, len(env))
saw := make(map[string]bool, len(env))
for n := len(env); n > 0; n-- {
kv := env[n-1]
i := strings.Index(kv, "=")
if i == 0 {
// We observe in practice keys with a single leading "=" on Windows.
// TODO(#49886): Should we consume only the first leading "=" as part
// of the key, or parse through arbitrarily many of them until a non-"="?
i = strings.Index(kv[1:], "=") + 1
}
if i < 0 {
if kv != "" {
// The entry is not of the form "key=value" (as it is required to be).
// Leave it as-is for now.
// TODO(#52436): should we strip or reject these bogus entries?
out = append(out, kv)
}
continue
}
k := kv[:i]
if caseInsensitive {
k = strings.ToLower(k)
}
if saw[k] {
continue
}
saw[k] = true
out = append(out, kv)
}
// Now reverse the slice to restore the original order.
for i := 0; i < len(out)/2; i++ {
j := len(out) - i - 1
out[i], out[j] = out[j], out[i]
}
return out
}
func addCriticalEnv(env []string) []string {
for _, kv := range env {
eq := strings.Index(kv, "=")
if eq < 0 {
continue
}
k := kv[:eq]
if strings.EqualFold(k, "SYSTEMROOT") {
// We already have it.
return env
}
}
return append(env, "SYSTEMROOT="+os.Getenv("SYSTEMROOT"))
}
func execEnvDefault(sys *syscall.SysProcAttr) (env []string, err error) {
if sys == nil || sys.Token == 0 {
return syscall.Environ(), nil
}
var block *uint16
err = windows.CreateEnvironmentBlock(&block, windows.Token(sys.Token), false)
if err != nil {
return nil, err
}
defer windows.DestroyEnvironmentBlock(block)
blockp := uintptr(unsafe.Pointer(block))
for {
// find NUL terminator
end := unsafe.Pointer(blockp)
for *(*uint16)(end) != 0 {
end = unsafe.Pointer(uintptr(end) + 2)
}
n := (uintptr(end) - uintptr(unsafe.Pointer(blockp))) / 2
if n == 0 {
// environment block ends with empty string
break
}
entry := (*[(1 << 30) - 1]uint16)(unsafe.Pointer(blockp))[:n:n]
env = append(env, string(utf16.Decode(entry)))
blockp += 2 * (uintptr(len(entry)) + 1)
}
return
}
func createEnvBlock(envv []string) *uint16 {
if len(envv) == 0 {
return &utf16.Encode([]rune("\x00\x00"))[0]
}
length := 0
for _, s := range envv {
length += len(s) + 1
}
length += 1
b := make([]byte, length)
i := 0
for _, s := range envv {
l := len(s)
copy(b[i:i+l], []byte(s))
copy(b[i+l:i+l+1], []byte{0})
i = i + l + 1
}
copy(b[i:i+1], []byte{0})
return &utf16.Encode([]rune(string(b)))[0]
}
func makeCmdLine(args []string) string {
var s string
for _, v := range args {
if s != "" {
s += " "
}
s += windows.EscapeArg(v)
}
return s
}
func isSlash(c uint8) bool {
return c == '\\' || c == '/'
}
func normalizeDir(dir string) (name string, err error) {
ndir, err := syscall.FullPath(dir)
if err != nil {
return "", err
}
if len(ndir) > 2 && isSlash(ndir[0]) && isSlash(ndir[1]) {
// dir cannot have \\server\share\path form
return "", syscall.EINVAL
}
return ndir, nil
}
func volToUpper(ch int) int {
if 'a' <= ch && ch <= 'z' {
ch += 'A' - 'a'
}
return ch
}
func joinExeDirAndFName(dir, p string) (name string, err error) {
if len(p) == 0 {
return "", syscall.EINVAL
}
if len(p) > 2 && isSlash(p[0]) && isSlash(p[1]) {
// \\server\share\path form
return p, nil
}
if len(p) > 1 && p[1] == ':' {
// has drive letter
if len(p) == 2 {
return "", syscall.EINVAL
}
if isSlash(p[2]) {
return p, nil
} else {
d, err := normalizeDir(dir)
if err != nil {
return "", err
}
if volToUpper(int(p[0])) == volToUpper(int(d[0])) {
return syscall.FullPath(d + "\\" + p[2:])
} else {
return syscall.FullPath(p)
}
}
} else {
// no drive letter
d, err := normalizeDir(dir)
if err != nil {
return "", err
}
if isSlash(p[0]) {
return windows.FullPath(d[:2] + p)
} else {
return windows.FullPath(d + "\\" + p)
}
}
}

34
doc.go
View File

@@ -3,7 +3,7 @@ package pty
import (
"errors"
"os"
"io"
)
// ErrUnsupported is returned if a function is not
@@ -11,6 +11,36 @@ import (
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) {
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
type Pty interface {
// FdHolder Fd intended to resize Tty of child process in current process
FdHolder
Name() string
// 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
type Tty interface {
// FdHolder Fd only intended for manual InheritSize from Pty
FdHolder
Name() string
io.ReadWriteCloser
}

2
go.mod
View File

@@ -1,3 +1,5 @@
module github.com/creack/pty
go 1.13
require golang.org/x/sys v0.0.0-20220721230656-c6bc011c0c49

2
go.sum Normal file
View File

@@ -0,0 +1,2 @@
golang.org/x/sys v0.0.0-20220721230656-c6bc011c0c49 h1:TMjZDarEwf621XDryfitp/8awEhiZNiwgphKlTMGRIg=
golang.org/x/sys v0.0.0-20220721230656-c6bc011c0c49/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@@ -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

195
pty_windows.go Normal file
View File

@@ -0,0 +1,195 @@
//go:build windows
// +build windows
package pty
import (
"fmt"
"os"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
const (
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x20016
)
type WindowsPty struct {
handle windows.Handle
r, w *os.File
}
type WindowsTty struct {
handle windows.Handle
r, w *os.File
}
var (
// 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 = windows.NewLazyDLL("kernel32.dll")
// https://docs.microsoft.com/en-us/windows/console/createpseudoconsole
createPseudoConsole = kernel32DLL.NewProc("CreatePseudoConsole")
closePseudoConsole = kernel32DLL.NewProc("ClosePseudoConsole")
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
}
var consoleHandle windows.Handle
err = procCreatePseudoConsole(windows.Handle(consoleR.Fd()), windows.Handle(consoleW.Fd()),
0, &consoleHandle)
if err != nil {
_ = consoleW.Close()
_ = pr.Close()
_ = pw.Close()
_ = consoleR.Close()
return nil, nil, err
}
// These pipes can be closed here without any worry
err = consoleW.Close()
if err != nil {
return nil, nil, fmt.Errorf("failed to close pseudo console handle: %w", err)
}
err = consoleR.Close()
if err != nil {
return nil, nil, fmt.Errorf("failed to close pseudo console handle: %w", err)
}
return &WindowsPty{
handle: consoleHandle,
r: pr,
w: pw,
}, &WindowsTty{
handle: consoleHandle,
r: consoleR,
w: consoleW,
}, nil
}
func (p *WindowsPty) Name() string {
return p.r.Name()
}
func (p *WindowsPty) Fd() uintptr {
return uintptr(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) UpdateProcThreadAttribute(attrList *windows.ProcThreadAttributeListContainer) error {
var err error
if err = attrList.Update(
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
unsafe.Pointer(p.handle),
unsafe.Sizeof(p.handle),
); err != nil {
return fmt.Errorf("failed to update proc thread attributes for pseudo console: %w", err)
}
return nil
}
func (p *WindowsPty) Close() error {
_ = p.r.Close()
_ = p.w.Close()
err := closePseudoConsole.Find()
if err != nil {
return err
}
_, _, err = closePseudoConsole.Call(uintptr(p.handle))
return err
}
func (t *WindowsTty) Name() string {
return t.r.Name()
}
func (t *WindowsTty) Fd() uintptr {
return uintptr(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()
}
func procCreatePseudoConsole(hInput windows.Handle, hOutput windows.Handle, dwFlags uint32, consoleHandle *windows.Handle) error {
var r0 uintptr
var err error
err = createPseudoConsole.Find()
if err != nil {
return err
}
r0, _, err = createPseudoConsole.Call(
(windowsCoord{X: 80, Y: 30}).Pack(), // size: default 80x30 window
uintptr(hInput), // console input
uintptr(hOutput), // console output
uintptr(dwFlags), // console flags, currently only PSEUDOCONSOLE_INHERIT_CURSOR supported
uintptr(unsafe.Pointer(consoleHandle)), // console handler value return
)
if int32(r0) < 0 {
if r0&0x1fff0000 == 0x00070000 {
r0 &= 0xffff
}
// S_OK: 0
return syscall.Errno(r0)
}
return nil
}

45
run.go
View File

@@ -1,9 +1,7 @@
package pty
import (
"os"
"os/exec"
"syscall"
)
// Start assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout,
@@ -11,47 +9,6 @@ import (
// corresponding pty.
//
// Starts the process in a new session and sets the controlling terminal.
func Start(cmd *exec.Cmd) (*os.File, error) {
func Start(cmd *exec.Cmd) (Pty, error) {
return StartWithSize(cmd, nil)
}
// StartWithAttrs assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout,
// and c.Stderr, calls c.Start, and returns the File of the tty's
// corresponding pty.
//
// This will resize the pty to the specified size before starting the command if a size is provided.
// The `attrs` parameter overrides the one set in c.SysProcAttr.
//
// This should generally not be needed. Used in some edge cases where it is needed to create a pty
// without a controlling terminal.
func StartWithAttrs(c *exec.Cmd, sz *Winsize, attrs *syscall.SysProcAttr) (*os.File, error) {
pty, tty, err := Open()
if err != nil {
return nil, err
}
defer func() { _ = tty.Close() }() // Best effort.
if sz != nil {
if err := Setsize(pty, sz); err != nil {
_ = pty.Close() // Best effort.
return nil, err
}
}
if c.Stdout == nil {
c.Stdout = tty
}
if c.Stderr == nil {
c.Stderr = tty
}
if c.Stdin == nil {
c.Stdin = tty
}
c.SysProcAttr = attrs
if err := c.Start(); err != nil {
_ = pty.Close() // Best effort.
return nil, err
}
return pty, err
}

69
run_unix.go Normal file
View File

@@ -0,0 +1,69 @@
//go:build !windows
// +build !windows
package pty
import (
"os/exec"
"syscall"
)
// StartWithSize assigns a pseudo-terminal Tty to c.Stdin, c.Stdout,
// and c.Stderr, calls c.Start, and returns the File of the tty's
// corresponding Pty.
//
// This will resize the Pty to the specified size before starting the command.
// Starts the process in a new session and sets the controlling terminal.
func StartWithSize(c *exec.Cmd, sz *Winsize) (Pty, error) {
if c.SysProcAttr == nil {
c.SysProcAttr = &syscall.SysProcAttr{}
}
c.SysProcAttr.Setsid = true
c.SysProcAttr.Setctty = true
return StartWithAttrs(c, sz, c.SysProcAttr)
}
// StartWithAttrs assigns a pseudo-terminal Tty to c.Stdin, c.Stdout,
// and c.Stderr, calls c.Start, and returns the File of the tty's
// corresponding Pty.
//
// This will resize the Pty to the specified size before starting the command if a size is provided.
// The `attrs` parameter overrides the one set in c.SysProcAttr.
//
// This should generally not be needed. Used in some edge cases where it is needed to create a pty
// without a controlling terminal.
func StartWithAttrs(c *exec.Cmd, sz *Winsize, attrs *syscall.SysProcAttr) (Pty, error) {
pty, tty, err := open()
if err != nil {
return nil, err
}
defer func() {
// always close tty fds since it's being used in another process
// but pty is kept to resize tty
_ = tty.Close()
}()
if sz != nil {
if err := Setsize(pty, sz); err != nil {
_ = pty.Close()
return nil, err
}
}
if c.Stdout == nil {
c.Stdout = tty
}
if c.Stderr == nil {
c.Stderr = tty
}
if c.Stdin == nil {
c.Stdin = tty
}
c.SysProcAttr = attrs
if err := c.Start(); err != nil {
_ = pty.Close()
return nil, err
}
return pty, err
}

235
run_windows.go Normal file
View File

@@ -0,0 +1,235 @@
//go:build windows
// +build windows
package pty
import (
"errors"
"fmt"
"os"
"os/exec"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
// StartWithSize assigns a pseudo-terminal Tty to c.Stdin, c.Stdout,
// and c.Stderr, calls c.Start, and returns the File of the tty's
// corresponding Pty.
//
// This will resize the Pty to the specified size before starting the command.
// Starts the process in a new session and sets the controlling terminal.
func StartWithSize(c *exec.Cmd, sz *Winsize) (Pty, error) {
return StartWithAttrs(c, sz, c.SysProcAttr)
}
// StartWithAttrs assigns a pseudo-terminal Tty to c.Stdin, c.Stdout,
// and c.Stderr, calls c.Start, and returns the File of the tty's
// corresponding Pty.
//
// This will resize the Pty to the specified size before starting the command if a size is provided.
// The `attrs` parameter overrides the one set in c.SysProcAttr.
//
// This should generally not be needed. Used in some edge cases where it is needed to create a pty
// without a controlling terminal.
func StartWithAttrs(c *exec.Cmd, sz *Winsize, attrs *syscall.SysProcAttr) (_ Pty, err error) {
pty, tty, err := open()
if err != nil {
return nil, err
}
defer func() {
// unlike unix command exec, do not close tty unless error happened
if err != nil {
_ = tty.Close()
_ = pty.Close()
}
}()
if sz != nil {
if err = Setsize(pty, sz); err != nil {
return nil, err
}
}
// unlike unix command exec, do not set stdin/stdout/stderr
c.SysProcAttr = attrs
// do not use os/exec.Start since we need to append console handler to startup info
w := WindowExecCmd{
cmd: c,
waitCalled: false,
consoleHandle: syscall.Handle(tty.Fd()),
tty: tty.(*WindowsTty),
conPty: pty.(*WindowsPty),
}
err = w.Start()
if err != nil {
_ = tty.Close()
_ = pty.Close()
return nil, err
}
return pty, err
}
// Start the specified command but does not wait for it to complete.
//
// If Start returns successfully, the c.Process field will be set.
//
// The Wait method will return the exit code and release associated resources
// once the command exits.
func (c *WindowExecCmd) Start() error {
if c.cmd.Process != nil {
return errors.New("exec: already started")
}
var argv0 = c.cmd.Path
var argv0p *uint16
var argvp *uint16
var dirp *uint16
var err error
sys := c.cmd.SysProcAttr
if sys == nil {
sys = &syscall.SysProcAttr{}
}
if c.cmd.Env == nil {
c.cmd.Env, err = execEnvDefault(sys)
if err != nil {
return err
}
}
var lp string
lp, err = lookExtensions(c.cmd.Path, c.cmd.Dir)
if err != nil {
return err
}
c.cmd.Path = lp
if len(c.cmd.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.
argv0, err = joinExeDirAndFName(c.cmd.Dir, c.cmd.Path)
if err != nil {
return err
}
}
argv0p, err = syscall.UTF16PtrFromString(argv0)
if err != nil {
return err
}
var cmdline string
// Windows CreateProcess takes the command line as a single string:
// use attr.CmdLine if set, else build the command line by escaping
// and joining each argument with spaces
if sys.CmdLine != "" {
cmdline = sys.CmdLine
} else {
cmdline = makeCmdLine(c.argv())
}
if len(cmdline) != 0 {
argvp, err = windows.UTF16PtrFromString(cmdline)
if err != nil {
return err
}
}
if len(c.cmd.Dir) != 0 {
dirp, err = windows.UTF16PtrFromString(c.cmd.Dir)
if err != nil {
return err
}
}
// Acquire the fork lock so that no other threads
// create new fds that are not yet close-on-exec
// before we fork.
syscall.ForkLock.Lock()
defer syscall.ForkLock.Unlock()
siEx := new(windows.StartupInfoEx)
siEx.Flags = windows.STARTF_USESTDHANDLES
if sys.HideWindow {
siEx.Flags |= syscall.STARTF_USESHOWWINDOW
siEx.ShowWindow = syscall.SW_HIDE
}
pi := new(windows.ProcessInformation)
// Need EXTENDED_STARTUPINFO_PRESENT as we're making use of the attribute list field.
flags := sys.CreationFlags | uint32(windows.CREATE_UNICODE_ENVIRONMENT) | windows.EXTENDED_STARTUPINFO_PRESENT
c.attrList, err = windows.NewProcThreadAttributeList(3)
if err != nil {
return fmt.Errorf("failed to initialize process thread attribute list: %w", err)
}
if c.conPty != nil {
if err = c.conPty.UpdateProcThreadAttribute(c.attrList); err != nil {
return err
}
}
siEx.ProcThreadAttributeList = c.attrList.List()
siEx.Cb = uint32(unsafe.Sizeof(*siEx))
if sys.Token != 0 {
err = windows.CreateProcessAsUser(
windows.Token(sys.Token),
argv0p,
argvp,
nil,
nil,
false,
flags,
createEnvBlock(addCriticalEnv(dedupEnvCase(true, c.cmd.Env))),
dirp,
&siEx.StartupInfo,
pi,
)
} else {
err = windows.CreateProcess(
argv0p,
argvp,
nil,
nil,
false,
flags,
createEnvBlock(addCriticalEnv(dedupEnvCase(true, c.cmd.Env))),
dirp,
&siEx.StartupInfo,
pi,
)
}
if err != nil {
return err
}
defer func() {
_ = windows.CloseHandle(pi.Thread)
}()
c.cmd.Process, err = os.FindProcess(int(pi.ProcessId))
if err != nil {
return err
}
return nil
}

View File

@@ -1,25 +0,0 @@
//go:build !windows
// +build !windows
package pty
import (
"os"
"os/exec"
"syscall"
)
// StartWithSize assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout,
// and c.Stderr, calls c.Start, and returns the File of the tty's
// corresponding pty.
//
// This will resize the pty to the specified size before starting the command.
// Starts the process in a new session and sets the controlling terminal.
func StartWithSize(cmd *exec.Cmd, ws *Winsize) (*os.File, error) {
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
cmd.SysProcAttr.Setsid = true
cmd.SysProcAttr.Setctty = true
return StartWithAttrs(cmd, ws, cmd.SysProcAttr)
}

View File

@@ -1,19 +0,0 @@
//go:build windows
// +build windows
package pty
import (
"os"
"os/exec"
)
// StartWithSize assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout,
// and c.Stderr, calls c.Start, and returns the File of the tty's
// corresponding pty.
//
// This will resize the pty to the specified size before starting the command.
// Starts the process in a new session and sets the controlling terminal.
func StartWithSize(cmd *exec.Cmd, ws *Winsize) (*os.File, error) {
return nil, ErrUnsupported
}

View File

@@ -32,9 +32,7 @@ cross netbsd amd64 386 arm arm64
cross openbsd amd64 386 arm arm64
cross dragonfly amd64
cross solaris amd64
# Not expected to work but should still compile.
cross windows amd64 386 arm
cross windows amd64 386 arm
# TODO: Fix compilation error on openbsd/arm.
# TODO: Merge the solaris PR.

View File

@@ -1,15 +1,22 @@
package pty
import "os"
// Winsize describes the terminal size.
type Winsize struct {
Rows uint16 // ws_row: Number of rows (in cells)
Cols uint16 // ws_col: Number of columns (in cells)
X uint16 // ws_xpixel: Width in pixels
Y uint16 // ws_ypixel: Height in pixels
}
// InheritSize applies the terminal size of pty to tty. This should be run
// in a signal handler for syscall.SIGWINCH to automatically resize the tty when
// the pty receives a window size change notification.
func InheritSize(pty, tty *os.File) error {
func InheritSize(pty Pty, tty Tty) error {
size, err := GetsizeFull(pty)
if err != nil {
return err
}
if err := Setsize(tty, size); err != nil {
return err
}
@@ -18,10 +25,7 @@ func InheritSize(pty, tty *os.File) error {
// Getsize returns the number of rows (lines) and cols (positions
// in each line) in terminal t.
func Getsize(t *os.File) (rows, cols int, err error) {
func Getsize(t FdHolder) (rows, cols int, err error) {
ws, err := GetsizeFull(t)
if err != nil {
return 0, 0, err
}
return int(ws.Rows), int(ws.Cols), nil
return int(ws.Rows), int(ws.Cols), err
}

View File

@@ -4,27 +4,18 @@
package pty
import (
"os"
"syscall"
"unsafe"
)
// Winsize describes the terminal size.
type Winsize struct {
Rows uint16 // ws_row: Number of rows (in cells)
Cols uint16 // ws_col: Number of columns (in cells)
X uint16 // ws_xpixel: Width in pixels
Y uint16 // ws_ypixel: Height in pixels
}
// Setsize resizes t to s.
func Setsize(t *os.File, ws *Winsize) error {
func Setsize(t FdHolder, ws *Winsize) error {
//nolint:gosec // Expected unsafe pointer for Syscall call.
return ioctl(t.Fd(), syscall.TIOCSWINSZ, uintptr(unsafe.Pointer(ws)))
}
// GetsizeFull returns the full terminal size description.
func GetsizeFull(t *os.File) (size *Winsize, err error) {
func GetsizeFull(t FdHolder) (size *Winsize, err error) {
var ws Winsize
//nolint:gosec // Expected unsafe pointer for Syscall call.

View File

@@ -1,23 +0,0 @@
//go:build windows
// +build windows
package pty
import (
"os"
)
// Winsize is a dummy struct to enable compilation on unsupported platforms.
type Winsize struct {
Rows, Cols, X, Y uint16
}
// Setsize resizes t to s.
func Setsize(*os.File, *Winsize) error {
return ErrUnsupported
}
// GetsizeFull returns the full terminal size description.
func GetsizeFull(*os.File) (*Winsize, error) {
return nil, ErrUnsupported
}

89
winsize_windows.go Normal file
View File

@@ -0,0 +1,89 @@
//go:build windows
// +build windows
package pty
import (
"syscall"
"unsafe"
)
// types from golang.org/x/sys/windows
// copy of https://pkg.go.dev/golang.org/x/sys/windows#Coord
type windowsCoord struct {
X int16
Y int16
}
// copy of https://pkg.go.dev/golang.org/x/sys/windows#SmallRect
type windowsSmallRect struct {
Left int16
Top int16
Right int16
Bottom int16
}
// copy of https://pkg.go.dev/golang.org/x/sys/windows#ConsoleScreenBufferInfo
type windowsConsoleScreenBufferInfo struct {
Size windowsCoord
CursorPosition windowsCoord
Attributes uint16
Window windowsSmallRect
MaximumWindowSize windowsCoord
}
func (c windowsCoord) Pack() uintptr {
return uintptr((int32(c.Y) << 16) | int32(c.X))
}
// Setsize resizes t to ws.
func Setsize(t FdHolder, ws *Winsize) error {
var r0 uintptr
var err error
err = resizePseudoConsole.Find()
if err != nil {
return err
}
r0, _, err = resizePseudoConsole.Call(
t.Fd(),
(windowsCoord{X: int16(ws.Cols), Y: int16(ws.Rows)}).Pack(),
)
if int32(r0) < 0 {
if r0&0x1fff0000 == 0x00070000 {
r0 &= 0xffff
}
// S_OK: 0
return syscall.Errno(r0)
}
return nil
}
// GetsizeFull returns the full terminal size description.
func GetsizeFull(t FdHolder) (size *Winsize, err error) {
err = getConsoleScreenBufferInfo.Find()
if err != nil {
return nil, err
}
var info windowsConsoleScreenBufferInfo
var r0 uintptr
r0, _, err = getConsoleScreenBufferInfo.Call(t.Fd(), uintptr(unsafe.Pointer(&info)))
if int32(r0) < 0 {
if r0&0x1fff0000 == 0x00070000 {
r0 &= 0xffff
}
// S_OK: 0
return nil, syscall.Errno(r0)
}
return &Winsize{
Rows: uint16(info.Window.Bottom - info.Window.Top + 1),
Cols: uint16(info.Window.Right - info.Window.Left + 1),
}, nil
}