mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
Add support for package executors and support for PTY handling (#100)
* define contract for package executors * introduce npx executor * add npx and pnpx cmd support * fix typo * rm PackageExecutor and depend on PackageManager interface * add support for PTY to handle parent-child process interaction * refactor PTY handling in proxy flow * enforce interactiveSession interface check * close reader explicitly and clean npm version for pkg executors * rm interaction from interceptors * add docs and wait for outputRouter before exit * add support for non interactive TTY for proxy mode * add support for CI env var check for non interactive tty proxy mode * update readme to include npx, pnpx support * Update internal/flows/proxy_flow.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com> * update ptyx lib * fix docs typo --------- Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
package pty
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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) {
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
nr, err := src.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check where to route the data
|
||||
if dest := r.dest.Load(); dest != nil {
|
||||
// Send confirmation prompt response to the pipe. (PMG)
|
||||
_, _ = dest.w.Write(buf[:nr])
|
||||
} else {
|
||||
// Send response to the child PTY.
|
||||
_, _ = r.defaultDst.Write(buf[:nr])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user