150 lines
3.6 KiB
Go
150 lines
3.6 KiB
Go
// Package pty wraps creack/pty with the small surface the spike needs.
|
|
package pty
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"syscall"
|
|
|
|
cpty "github.com/creack/pty"
|
|
)
|
|
|
|
// PTY holds a child process attached to a pseudo-terminal master fd.
|
|
type PTY struct {
|
|
master *os.File
|
|
cmd *exec.Cmd
|
|
}
|
|
|
|
// Start spawns argv with stdin/stdout/stderr attached to a new PTY sized
|
|
// (cols, rows). The returned PTY exposes the master fd for the parent to
|
|
// read from and write to.
|
|
func Start(argv []string, env []string, workDir string, cols, rows uint16) (*PTY, error) {
|
|
if len(argv) == 0 {
|
|
return nil, fmt.Errorf("pty: empty argv")
|
|
}
|
|
cmd := exec.Command(argv[0], argv[1:]...)
|
|
cmd.Dir = workDir
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true}
|
|
if env != nil {
|
|
cmd.Env = ensureTerm(env)
|
|
} else {
|
|
// Default to the parent environment but force TERM to xterm-256color
|
|
// so child programs assume something modern and we observe SGR + alt
|
|
// screen sequences.
|
|
cmd.Env = ensureTerm(os.Environ())
|
|
}
|
|
|
|
ws := &cpty.Winsize{Cols: cols, Rows: rows}
|
|
master, err := cpty.StartWithSize(cmd, ws)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("pty: start %v: %w", argv, err)
|
|
}
|
|
return &PTY{master: master, cmd: cmd}, nil
|
|
}
|
|
|
|
func (p *PTY) Read(b []byte) (int, error) {
|
|
if p.master == nil {
|
|
return 0, io.ErrClosedPipe
|
|
}
|
|
return p.master.Read(b)
|
|
}
|
|
|
|
func (p *PTY) Write(b []byte) (int, error) {
|
|
if p.master == nil {
|
|
return 0, io.ErrClosedPipe
|
|
}
|
|
return p.master.Write(b)
|
|
}
|
|
|
|
func (p *PTY) Resize(cols, rows uint16) error {
|
|
if p.master == nil {
|
|
return io.ErrClosedPipe
|
|
}
|
|
return cpty.Setsize(p.master, &cpty.Winsize{Cols: cols, Rows: rows})
|
|
}
|
|
|
|
// Wait blocks until the child exits and returns its exit error if any.
|
|
func (p *PTY) Wait() error {
|
|
if p.cmd == nil {
|
|
return nil
|
|
}
|
|
return p.cmd.Wait()
|
|
}
|
|
|
|
// Pid returns the child's PID, or -1 if the process is not running.
|
|
func (p *PTY) Pid() int {
|
|
if p.cmd == nil || p.cmd.Process == nil {
|
|
return -1
|
|
}
|
|
return p.cmd.Process.Pid
|
|
}
|
|
|
|
// Close terminates the child (best effort) and releases the master fd.
|
|
func (p *PTY) Close() error {
|
|
var firstErr error
|
|
if p.master != nil {
|
|
if err := p.master.Close(); err != nil && firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
p.master = nil
|
|
}
|
|
if p.cmd != nil && p.cmd.Process != nil {
|
|
pid := p.cmd.Process.Pid
|
|
if pid > 0 {
|
|
_ = syscall.Kill(-pid, syscall.SIGKILL)
|
|
}
|
|
_ = p.cmd.Process.Kill()
|
|
}
|
|
return firstErr
|
|
}
|
|
|
|
// envDefaults are added to the child's environment unless the parent already
|
|
// set them. Modern agent TUIs check these and silently downgrade rendering
|
|
// when they're missing (no truecolor, ASCII-only banners, etc.).
|
|
var envDefaults = map[string]string{
|
|
"TERM": "xterm-256color",
|
|
"COLORTERM": "truecolor",
|
|
}
|
|
|
|
// envStrip names variables we DROP before launching a child. COLUMNS /
|
|
// LINES inherited from the parent shell describe the *host* terminal,
|
|
// not the PTY we created — when they leak through, TUIs that prefer
|
|
// env over TIOCGWINSZ render past the PTY's actual cell grid and
|
|
// overwrite our chrome.
|
|
var envStrip = map[string]bool{
|
|
"COLUMNS": true,
|
|
"LINES": true,
|
|
}
|
|
|
|
func ensureTerm(env []string) []string {
|
|
have := make(map[string]bool, len(envDefaults))
|
|
out := make([]string, 0, len(env)+len(envDefaults))
|
|
for _, kv := range env {
|
|
key := envKey(kv)
|
|
if envStrip[key] {
|
|
continue
|
|
}
|
|
if _, isDefault := envDefaults[key]; isDefault {
|
|
have[key] = true
|
|
}
|
|
out = append(out, kv)
|
|
}
|
|
for k, v := range envDefaults {
|
|
if !have[k] {
|
|
out = append(out, k+"="+v)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func envKey(kv string) string {
|
|
for i := 0; i < len(kv); i++ {
|
|
if kv[i] == '=' {
|
|
return kv[:i]
|
|
}
|
|
}
|
|
return kv
|
|
}
|