gopty

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 15, 2026 License: MIT Imports: 9 Imported by: 0

README

pty

平台无关的 pty(伪终端)后端 + 交互式测试夹具。

从 gofer/internal/pty 抽出,供工作区内多个项目复用(gofer serve 的交互会话、各 CLI 的 交互 e2e 测试)。

两个包

包 作用
github.com/gookit/gopty 产品用:Spec + Start(unix=creack/pty,windows=内置 vendored ConPTY)+ Pty(Read/Write/Resize/Wait/Close) + IsAvailable() / IsHostBlocked(err)
github.com/gookit/gopty/ptytest 测试用:在 pty 里跑一个命令并提供 expect/发送/断言/收尾(Expect/Count/Text(剥 ANSI)/Send/SendLine/SendCtrlC/SendKeys/WaitExit/Cleanup),不可用时自动 t.Skip

依赖

creack/pty(MIT,unix)+ golang.org/x/sys(windows)。不引入 hcsshim:Windows 侧用 vendored 的 ./conpty(UserExistsError/conpty 单文件版,MIT,见 conpty/LICENSE),这也是当初 排除 aymanbagabas/go-pty(会拖进 hcsshim 一串间接依赖)的原因。

用法

产品侧:

import "github.com/gookit/gopty"

p, err := pty.Start(pty.Spec{Command: "bash", Cols: 100, Rows: 30})
defer p.Close()
p.Write([]byte("ls\n"))
code, err := p.Wait(ctx)

测试侧:

import "github.com/gookit/gopty/ptytest"

func TestAskPrompt(t *testing.T) {
	s := ptytest.Start(t, "sh", "-c", `printf "name? "; read n; echo got=$n`)
	s.SendLine("tom")
	if !s.Expect(`got=tom`) {
		t.Fatalf("no answer, output:\n%s", s.Text())
	}
}

Windows 注意事项(实测)

  • ConPTY 需要 Win10 1809+;IsAvailable() 会探测。
  • 部分 Windows 进程上下文会拒绝 ConPTY 子进程(job 对象不允许 breakaway 时,CreateProcess 带 PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE 报 0xC0000142/STATUS_DLL_INIT_FAILED)。 实测差异:suga 的 scripts/wincons 在 gofer job 里报过这个错,而本库在 gofer job 里 cmd.exe 子进程正常(exit 0)——所以它取决于环境,不要假设哪种。IsHostBlocked(err) / IsBlockedExitCode(code) 用于识别;ptytest.Start 会据此 t.Skip 并说明原因。 真被拒时想测交互提示,用真实控制台驱动(见 gookit/gcli 的 scripts/wincons)。
  • Pty.Close() 在 Windows 上幂等:conpty.Close() 会无条件关闭 4 个管道句柄 + 伪控制台, 二次调用可能关到 Windows 已复用的句柄值(实测会让测试进程静默挂掉),适配层用 sync.Once 护住了。
  • Ctrl-C 等合成按键在 ConPTY 下不等于 CTRL_C_EVENT(往输入写 0x03 即可,但控制台模式决定)。

测试

go test ./...                      # unix 本机 (5 个 pty 用例 + 5 个 ptytest 用例)
GOOS=windows go build ./...        # 交叉编译检查
# windows 本机: 2 个 ptytest 用例 + 1 个 pty 用例(ConPTY);
# host 被 job 包裹且拒绝 ConPTY 时, 会跳过并说明原因

实测(2026-10-15): Windows 本机 go1.25.10 全绿; Linux 容器 golang:1.26 全绿。

Documentation

Overview

Package gopty is a small, platform-neutral pty (pseudo terminal) backend: it starts a child process attached to a pty master and hands the caller a raw bidirectional byte stream, plus resize and exit-code wait.

Backends (see pty_unix.go / pty_windows.go):

  • unix: github.com/creack/pty, whose master file is the stream.
  • windows: a vendored copy of UserExistsError/conpty (./conpty, MIT), Win10 1809+. ConPTY takes ONE command-line string, so Start joins Command+Args into a safely quoted line (windows.EscapeArg); a caller-supplied Env is de-duplicated with the os/exec rule ("last occurrence wins") because CreateProcess keeps the first.

Both backends satisfy the same Pty contract, so a program that runs pty children compiles and behaves the same on a unix worker and on Windows.

A ConPTY child must be created through the ConPTY API rather than os/exec (golang/go#62708), and some Windows process contexts refuse it: suga's console harness (scripts/wincons) reported CreateProcess failing with STATUS_DLL_INIT_FAILED (0xC0000142) inside a gofer job, while this repo's own probe in a gofer job starts a ConPTY child fine (cmd.exe, exit 0) — so the outcome is environment-dependent. Treat it as possible: check IsHostBlocked / IsBlockedExitCode instead of assuming either way. When it is blocked, drive a real console for interactive work instead.

Interactive (test) children are best driven through the sibling package github.com/gookit/gopty/ptytest, which adds expect/send helpers on this contract.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsAvailable

func IsAvailable() bool

IsAvailable is always true on unix: creack/pty opens /dev/ptmx at Start time and a failure surfaces there (probing here would just duplicate that).

func IsBlockedExitCode

func IsBlockedExitCode(int) bool

IsBlockedExitCode is always false on unix: a unix pty child always runs.

func IsHostBlocked

func IsHostBlocked(error) bool

IsHostBlocked is always false on unix: no platform mechanism blocks a pty child the way a Windows job object does (see the windows file).

Types

type Pty

type Pty interface {
	io.ReadWriteCloser
	// Resize sets the pty window size in character cells.
	Resize(cols, rows int) error
	// Wait blocks until the child exits (or ctx is cancelled) and returns the
	// process exit code. A ctx cancellation returns a non-nil error without
	// reaping; the caller kills via Close and calls Wait once.
	Wait(ctx context.Context) (exitCode int, err error)
}

Pty is a running child attached to a pty master. Read/Write carry the raw byte stream to/from the child (stdout+stderr multiplexed on Read, stdin on Write); Resize sets the terminal window size; Wait blocks for the child to exit and returns its exit code. Close releases the master fd (and, on unix, kills the child) — teardown ORDER is owned by the caller's state machine, this type only provides the primitives.

func Start

func Start(spec Spec) (Pty, error)

Start runs spec under a freshly allocated pty (unix: creack/pty).

Unlike creack.Start, the parent keeps its own slave fd until the child has been reaped. On darwin the child's exit would otherwise be the LAST close of the slave, and the BSD tty layer flushes output the master has not read yet on that close — a child that printed and exited before the reader caught up lost its output, all of it for a short-lived one. Holding the slave makes the reaper's close the last one; it runs only after the exit is known, and the reader drains the buffer to EOF before Close tears the master down (see Close).

type Spec

type Spec struct {
	Command string   // executable to run
	Args    []string // args (not including argv[0])
	Env     []string // full process env (KEY=VALUE); nil = inherit os.Environ
	Dir     string   // working directory; "" = inherit
	Cols    int      // initial terminal width (columns); 0 = platform default
	Rows    int      // initial terminal height (rows); 0 = platform default
}

Spec is a platform-neutral request to start a child under a pty.

Directories

Path Synopsis
Package conpty is the vendored Windows ConPTY backend (see conpty.go, which carries the //go:build windows constraint).
Package conpty is the vendored Windows ConPTY backend (see conpty.go, which carries the //go:build windows constraint).
Package ptytest drives a program inside a pty from ordinary Go tests: start it, wait for the prompt, type, assert on the output, then shut it down.
Package ptytest drives a program inside a pty from ordinary Go tests: start it, wait for the prompt, type, assert on the output, then shut it down.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL