mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 13:11:31 -10:00
* feat(execution): add a stdin-only write for interactive processes * feat(tui): attach overlay for interactive terminal sessions * feat(tui): add /attach for interactive terminal sessions * feat(tools): point the model at tty sessions for interactive prompts * feat(tui): open the terminal automatically for tty exec sessions * feat(tools): tell the model to keep polling while the user types * feat(tui): give attached terminal sessions the full viewport * feat(tools): route sudo through an escalated tty session * fix(tui): route the auto-attach tick to its run * fix(tui): defer auto-attach while another view owns the screen * fix(tui): keep bracketed-paste framing for attached sessions * fix(tools): let tty sessions run interactive commands * fix(tui): capture the auto-attach baseline before the exec starts
99 lines
2.7 KiB
Go
99 lines
2.7 KiB
Go
//go:build linux
|
|
|
|
package execution
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"syscall"
|
|
"time"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
func startPTYProcess(command *exec.Cmd, output io.Writer) (io.WriteCloser, func(), error) {
|
|
master, slave, err := openPTY()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
cleanup := func() { _ = master.Close(); _ = slave.Close() }
|
|
command.Stdin, command.Stdout, command.Stderr = slave, slave, slave
|
|
hardenPTYProcess(command)
|
|
if err := command.Start(); err != nil {
|
|
cleanup()
|
|
return nil, nil, err
|
|
}
|
|
_ = slave.Close()
|
|
copied := make(chan struct{})
|
|
go func() { defer close(copied); _, _ = io.Copy(output, master) }()
|
|
return master, func() {
|
|
select {
|
|
case <-copied:
|
|
case <-time.After(processWaitDelay):
|
|
}
|
|
_ = master.Close()
|
|
}, nil
|
|
}
|
|
|
|
func openPTY() (*os.File, *os.File, error) {
|
|
masterFD, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY|unix.O_CLOEXEC, 0)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
master := os.NewFile(uintptr(masterFD), "/dev/ptmx")
|
|
if err := unix.IoctlSetPointerInt(masterFD, unix.TIOCSPTLCK, 0); err != nil {
|
|
_ = master.Close()
|
|
return nil, nil, err
|
|
}
|
|
// A fresh PTY defaults to a 0x0 window; seed a sane size so the process
|
|
// never sees zero dimensions before an attached client reports its own.
|
|
_ = unix.IoctlSetWinsize(masterFD, unix.TIOCSWINSZ, &unix.Winsize{Row: 24, Col: 80})
|
|
pts, err := unix.IoctlGetInt(masterFD, unix.TIOCGPTN)
|
|
if err != nil {
|
|
_ = master.Close()
|
|
return nil, nil, err
|
|
}
|
|
slaveName := fmt.Sprintf("/dev/pts/%d", pts)
|
|
slaveFD, err := unix.Open(slaveName, unix.O_RDWR|unix.O_NOCTTY|unix.O_CLOEXEC, 0)
|
|
if err != nil {
|
|
_ = master.Close()
|
|
return nil, nil, err
|
|
}
|
|
return master, os.NewFile(uintptr(slaveFD), slaveName), nil
|
|
}
|
|
|
|
func hardenPTYProcess(command *exec.Cmd) {
|
|
if command.SysProcAttr == nil {
|
|
command.SysProcAttr = &syscall.SysProcAttr{}
|
|
}
|
|
command.SysProcAttr.Setsid = true
|
|
command.SysProcAttr.Setctty = true
|
|
command.SysProcAttr.Ctty = 0
|
|
command.WaitDelay = processWaitDelay
|
|
command.Cancel = func() error {
|
|
if command.Process == nil {
|
|
return nil
|
|
}
|
|
if err := syscall.Kill(-command.Process.Pid, syscall.SIGKILL); err != nil && !errors.Is(err, syscall.ESRCH) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// resizePTY reports a new window size to the PTY behind an interactive
|
|
// process's stdin writer, so full-screen attached terminals drive the
|
|
// session's own line wrapping.
|
|
func resizePTY(w io.Writer, cols, rows int) error {
|
|
file, ok := w.(interface{ Fd() uintptr })
|
|
if !ok {
|
|
return errors.New("pty transport is unavailable on this platform")
|
|
}
|
|
return unix.IoctlSetWinsize(int(file.Fd()), unix.TIOCSWINSZ, &unix.Winsize{
|
|
Row: uint16(rows), Col: uint16(cols),
|
|
})
|
|
}
|