mirror of
https://github.com/creack/pty.git
synced 2026-04-01 19:09:48 +09:00
new windows support
This commit is contained in:
@@ -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
391
cmd_windows.go
Normal 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
34
doc.go
@@ -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
2
go.mod
@@ -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
2
go.sum
Normal 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=
|
||||
@@ -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
195
pty_windows.go
Normal 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
45
run.go
@@ -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
69
run_unix.go
Normal 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
235
run_windows.go
Normal 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
|
||||
}
|
||||
25
start.go
25
start.go
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
18
winsize.go
18
winsize.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
89
winsize_windows.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user