2026-01-09 22:03:42 +05:30
|
|
|
package pty
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
2026-05-13 19:14:00 +05:30
|
|
|
"context"
|
2026-01-09 22:03:42 +05:30
|
|
|
"io"
|
|
|
|
|
"sync"
|
|
|
|
|
"sync/atomic"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// OutputRouter manages buffered vs live output.
|
|
|
|
|
type OutputRouter struct {
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
stdout io.Writer
|
|
|
|
|
buffer bytes.Buffer
|
|
|
|
|
buffering bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewOutputRouter(out io.Writer) (*OutputRouter, error) {
|
|
|
|
|
return &OutputRouter{
|
|
|
|
|
stdout: out,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *OutputRouter) Write(p []byte) (n int, err error) {
|
|
|
|
|
r.mu.Lock()
|
|
|
|
|
defer r.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
if r.buffering {
|
|
|
|
|
// We are in "Prompt Mode", so save this output for later.
|
|
|
|
|
// If we printed it now, it would mess up the confirmation prompt.
|
|
|
|
|
return r.buffer.Write(p)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Normal mode: just print it to stdout.
|
|
|
|
|
return r.stdout.Write(p)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pause starts buffering output. Call this before showing a confirmation prompt.
|
|
|
|
|
func (r *OutputRouter) Pause() {
|
|
|
|
|
r.mu.Lock()
|
|
|
|
|
defer r.mu.Unlock()
|
|
|
|
|
r.buffering = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Resume stops buffering, flushes any buffered output, and resumes live output.
|
|
|
|
|
// Call this after the confirmation prompt is complete.
|
|
|
|
|
func (r *OutputRouter) Resume() {
|
|
|
|
|
r.mu.Lock()
|
|
|
|
|
defer r.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
// Flush any buffered output
|
|
|
|
|
if r.buffer.Len() > 0 {
|
|
|
|
|
_, _ = io.Copy(r.stdout, &r.buffer)
|
|
|
|
|
r.buffer.Reset()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r.buffering = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writerDest wraps io.Writer for use with atomic.Pointer
|
|
|
|
|
// (atomic.Value panics on nil interface stores)
|
|
|
|
|
type writerDest struct {
|
|
|
|
|
w io.Writer
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// InputRouter manages routing stdin to either PTY or a prompt pipe.
|
|
|
|
|
// Only ONE goroutine should call ReadLoop().
|
|
|
|
|
type InputRouter struct {
|
|
|
|
|
dest atomic.Pointer[writerDest]
|
|
|
|
|
defaultDst io.Writer // PTY writer
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewInputRouter(ptyWriter io.Writer) (*InputRouter, error) {
|
|
|
|
|
return &InputRouter{
|
|
|
|
|
defaultDst: ptyWriter,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ReadLoop continuously reads from src and routes data to the current destination.
|
|
|
|
|
//
|
|
|
|
|
// IMPORTANT: Only ONE goroutine should call ReadLoop() because:
|
|
|
|
|
// 1. Multiple readers on the same source (e.g., stdin) cause data splitting -
|
|
|
|
|
// one goroutine might read "hel" while another reads "lo\n"
|
|
|
|
|
// 2. Concurrent routing decisions create race conditions on the destination
|
|
|
|
|
// 3. User input becomes unpredictably interleaved between readers
|
|
|
|
|
//
|
|
|
|
|
// This function blocks until src returns an error (e.g., EOF).
|
|
|
|
|
func (r *InputRouter) ReadLoop(src io.Reader) {
|
2026-05-13 19:14:00 +05:30
|
|
|
r.ReadLoopContext(context.Background(), src)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ReadLoopContext continuously reads from src and routes data to the current
|
|
|
|
|
// destination until src returns an error or ctx is cancelled.
|
|
|
|
|
func (r *InputRouter) ReadLoopContext(ctx context.Context, src io.Reader) {
|
2026-01-09 22:03:42 +05:30
|
|
|
buf := make([]byte, 1024)
|
|
|
|
|
for {
|
2026-05-13 19:14:00 +05:30
|
|
|
nr, err := readInput(ctx, src, buf)
|
2026-01-09 22:03:42 +05:30
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 19:14:00 +05:30
|
|
|
if nr == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 22:03:42 +05:30
|
|
|
if dest := r.dest.Load(); dest != nil {
|
2026-05-13 19:14:00 +05:30
|
|
|
if _, err := dest.w.Write(buf[:nr]); err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-01-09 22:03:42 +05:30
|
|
|
} else {
|
2026-05-13 19:14:00 +05:30
|
|
|
if _, err := r.defaultDst.Write(buf[:nr]); err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-01-09 22:03:42 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RouteToPrompt switches input to go to the given writer (prompt pipe)
|
|
|
|
|
func (r *InputRouter) RouteToPrompt(w io.Writer) {
|
|
|
|
|
r.dest.Store(&writerDest{w: w})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RouteToPTY switches input back to the PTY (default)
|
|
|
|
|
func (r *InputRouter) RouteToPTY() {
|
|
|
|
|
r.dest.Store(nil)
|
|
|
|
|
}
|