All checks were successful
PR Check / check (pull_request) Successful in 1m0s
Three issues reported on Windows + one user-requested limit bump:
1. Dashboard CPU/RAM/Network all at 0
handleSystemMetrics read /proc/* exclusively. Replaced with a
platform-split:
- metrics_unix.go (!windows): existing /proc reading code.
- metrics_windows.go: kernel32!GetSystemTimes for CPU
(delta of idle vs kernel+user FILETIMEs) and
kernel32!GlobalMemoryStatusEx for memory. Network left at zero
for now — MIB_IF_ROW2 is too version-sensitive to parse by hand.
handlers_info.go::handleSystemMetrics reduced to one delegating
call.
2. Terminal black screen on Windows
creack/pty/v2 returns "unsupported" on Windows; the v0.7.1 pipe
fallback works but pipes don't carry TTY signals, so cmd/pwsh/wsl
go silent. Implemented native ConPTY:
- terminal_conpty_windows.go: CreatePseudoConsole + STARTUPINFOEX
+ PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE wiring via
windows.NewProcThreadAttributeList. CreateProcessW launches
child with the PC attached, full ANSI / line discipline /
resize.
- canUseConPTY() probes once at startup (Win10 1809+ check).
- Restructure: terminal_session.go now holds just the interface
+ ptySession + pipeSession structs. terminal_session_unix.go
wires creack/pty. terminal_session_windows.go tries ConPTY
first, falls back to pipeSession.
3. Agent stops after 15 tool calls
MaxToolIterations bumped 15 → 500. Doc comment explains why the
cap exists at all (infinite-loop safety) and that 500 is well
above realistic usage.
- internal/version/version.go: 0.7.5 → 0.7.6
- CHANGELOG.md: v0.7.6 entry covers the three fixes
189 lines
3.9 KiB
Go
189 lines
3.9 KiB
Go
package api
|
|
|
|
// Cross-platform terminal session abstraction.
|
|
//
|
|
// On Linux / macOS the unix-tagged file (terminal_session_unix.go) wires
|
|
// startTermSession to creack/pty for a real PTY: full TTY semantics,
|
|
// resize support, interactive apps (vim, top…) work.
|
|
//
|
|
// On Windows the windows-tagged file (terminal_session_windows.go) tries
|
|
// the kernel32 ConPTY API first, with a pipe-based fallback for older
|
|
// hosts. pipeSession does NOT carry TTY signals, so most shells go silent
|
|
// — it's only kept as a last resort.
|
|
//
|
|
// Both platforms share the termSession interface, the ptySession type
|
|
// (used by unix), and the pipeSession type (used by the Windows fallback).
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"sync"
|
|
|
|
"github.com/creack/pty/v2"
|
|
)
|
|
|
|
// termSession is the read/write/resize/close surface used by handleTerminalWS.
|
|
type termSession interface {
|
|
Read([]byte) (int, error)
|
|
Write([]byte) (int, error)
|
|
Resize(rows, cols uint16) error
|
|
Close() error
|
|
Wait() error
|
|
Pid() int
|
|
}
|
|
|
|
// ptySession wraps creack/pty's *os.File-backed PTY (unix path).
|
|
type ptySession struct {
|
|
ptmx *os.File
|
|
cmd *exec.Cmd
|
|
}
|
|
|
|
func (s *ptySession) Read(p []byte) (int, error) { return s.ptmx.Read(p) }
|
|
func (s *ptySession) Write(p []byte) (int, error) { return s.ptmx.Write(p) }
|
|
func (s *ptySession) Resize(rows, cols uint16) error {
|
|
return pty.Setsize(s.ptmx, &pty.Winsize{Rows: rows, Cols: cols})
|
|
}
|
|
func (s *ptySession) Close() error {
|
|
err := s.ptmx.Close()
|
|
if s.cmd.Process != nil {
|
|
s.cmd.Process.Kill()
|
|
}
|
|
return err
|
|
}
|
|
func (s *ptySession) Wait() error {
|
|
if s.cmd.Process == nil {
|
|
return nil
|
|
}
|
|
return s.cmd.Wait()
|
|
}
|
|
func (s *ptySession) Pid() int {
|
|
if s.cmd.Process == nil {
|
|
return 0
|
|
}
|
|
return s.cmd.Process.Pid
|
|
}
|
|
|
|
// pipeSession is the Windows last-resort fallback when ConPTY is not
|
|
// available: stdin pipe + merged stdout/stderr, no TTY signals. Most
|
|
// interactive shells go silent in this mode, so it should rarely be hit on
|
|
// modern Windows (10 1809+).
|
|
type pipeSession struct {
|
|
cmd *exec.Cmd
|
|
stdin io.WriteCloser
|
|
stdout io.ReadCloser
|
|
stderr io.ReadCloser
|
|
mu sync.Mutex
|
|
merged chan []byte
|
|
closed bool
|
|
closeCh chan struct{}
|
|
}
|
|
|
|
func startPipeSession(cmd *exec.Cmd) (termSession, error) {
|
|
stdin, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
stdin.Close()
|
|
return nil, err
|
|
}
|
|
stderr, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
stdin.Close()
|
|
stdout.Close()
|
|
return nil, err
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
stdin.Close()
|
|
stdout.Close()
|
|
stderr.Close()
|
|
return nil, err
|
|
}
|
|
s := &pipeSession{
|
|
cmd: cmd,
|
|
stdin: stdin,
|
|
stdout: stdout,
|
|
stderr: stderr,
|
|
merged: make(chan []byte, 32),
|
|
closeCh: make(chan struct{}),
|
|
}
|
|
go s.pump(stdout)
|
|
go s.pump(stderr)
|
|
return s, nil
|
|
}
|
|
|
|
func (s *pipeSession) pump(r io.ReadCloser) {
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, err := r.Read(buf)
|
|
if n > 0 {
|
|
chunk := make([]byte, n)
|
|
copy(chunk, buf[:n])
|
|
select {
|
|
case s.merged <- chunk:
|
|
case <-s.closeCh:
|
|
return
|
|
}
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *pipeSession) Read(p []byte) (int, error) {
|
|
select {
|
|
case chunk, ok := <-s.merged:
|
|
if !ok {
|
|
return 0, io.EOF
|
|
}
|
|
n := copy(p, chunk)
|
|
return n, nil
|
|
case <-s.closeCh:
|
|
return 0, io.EOF
|
|
}
|
|
}
|
|
|
|
func (s *pipeSession) Write(p []byte) (int, error) {
|
|
return s.stdin.Write(p)
|
|
}
|
|
|
|
func (s *pipeSession) Resize(rows, cols uint16) error {
|
|
// No real TTY → resize is a no-op; the child won't get SIGWINCH.
|
|
return nil
|
|
}
|
|
|
|
func (s *pipeSession) Close() error {
|
|
s.mu.Lock()
|
|
if s.closed {
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
s.closed = true
|
|
close(s.closeCh)
|
|
s.mu.Unlock()
|
|
s.stdin.Close()
|
|
s.stdout.Close()
|
|
s.stderr.Close()
|
|
if s.cmd.Process != nil {
|
|
s.cmd.Process.Kill()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *pipeSession) Wait() error {
|
|
if s.cmd.Process == nil {
|
|
return nil
|
|
}
|
|
return s.cmd.Wait()
|
|
}
|
|
|
|
func (s *pipeSession) Pid() int {
|
|
if s.cmd.Process == nil {
|
|
return 0
|
|
}
|
|
return s.cmd.Process.Pid
|
|
}
|