터미널 세션 core와 CLI mode executor 경계를 분리해 후속 remote terminal bridge가 재사용할 내부 실행 기반을 만든다. adapter 기본값과 vLLM experimental surface도 명시해 production 실행 경로가 mock fallback이나 미구현 실행으로 숨지 않게 한다.
174 lines
3.4 KiB
Go
174 lines
3.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/creack/pty"
|
|
"iop/apps/node/internal/adapters/cli/status"
|
|
)
|
|
|
|
type terminalSessionOptions struct {
|
|
Command string
|
|
Args []string
|
|
Env []string
|
|
}
|
|
|
|
type terminalSessionCore struct {
|
|
cmd *exec.Cmd
|
|
input io.WriteCloser
|
|
output io.ReadCloser
|
|
tail *status.TailBuffer
|
|
outputCh chan cliOutput
|
|
doneCh chan error
|
|
closed bool
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func startTerminalSessionCore(ctx context.Context, opts terminalSessionOptions) (*terminalSessionCore, error) {
|
|
cmd := exec.Command(opts.Command, opts.Args...)
|
|
if len(opts.Env) > 0 {
|
|
cmd.Env = append(cmd.Environ(), opts.Env...)
|
|
}
|
|
ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{
|
|
Rows: terminalRows,
|
|
Cols: terminalCols,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("pty start: %w", err)
|
|
}
|
|
|
|
core := &terminalSessionCore{
|
|
cmd: cmd,
|
|
input: ptmx,
|
|
output: ptmx,
|
|
tail: status.NewTailBuffer(2048),
|
|
outputCh: make(chan cliOutput, 1024),
|
|
doneCh: make(chan error, 1),
|
|
}
|
|
|
|
go core.readLoop()
|
|
|
|
return core, nil
|
|
}
|
|
|
|
func (s *terminalSessionCore) readLoop() {
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, err := s.output.Read(buf)
|
|
if n > 0 {
|
|
text := string(buf[:n])
|
|
s.tail.Append(text)
|
|
s.outputCh <- cliOutput{text: text}
|
|
}
|
|
if err != nil {
|
|
s.doneCh <- s.cmd.Wait()
|
|
close(s.outputCh)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *terminalSessionCore) WritePrompt(ctx context.Context, prompt string) error {
|
|
s.mu.Lock()
|
|
if s.closed {
|
|
s.mu.Unlock()
|
|
return errors.New("terminal session core closed")
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
for _, r := range prompt {
|
|
s.mu.Lock()
|
|
if s.closed {
|
|
s.mu.Unlock()
|
|
return errors.New("terminal session core closed")
|
|
}
|
|
_, err := io.WriteString(s.input, string(r))
|
|
s.mu.Unlock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
timer := time.NewTimer(terminalInputDelay)
|
|
select {
|
|
case <-ctx.Done():
|
|
timer.Stop()
|
|
return ctx.Err()
|
|
case <-timer.C:
|
|
}
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.closed {
|
|
return errors.New("terminal session core closed")
|
|
}
|
|
_, err := io.WriteString(s.input, "\r")
|
|
return err
|
|
}
|
|
|
|
func (s *terminalSessionCore) Snapshot() status.Snapshot {
|
|
return status.Snapshot{
|
|
Tail: s.tail.String(),
|
|
}
|
|
}
|
|
|
|
func (s *terminalSessionCore) Resize(rows, cols uint16) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.closed {
|
|
return errors.New("terminal session core closed")
|
|
}
|
|
if rows == 0 || cols == 0 {
|
|
return errors.New("invalid terminal size: rows and cols must be greater than 0")
|
|
}
|
|
f, ok := s.input.(*os.File)
|
|
if !ok {
|
|
return errors.New("terminal input is not a file")
|
|
}
|
|
return pty.Setsize(f, &pty.Winsize{Rows: rows, Cols: cols})
|
|
}
|
|
|
|
func (s *terminalSessionCore) WriteInput(ctx context.Context, data []byte) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.closed {
|
|
return errors.New("terminal session core closed")
|
|
}
|
|
_, err := s.input.Write(data)
|
|
return err
|
|
}
|
|
|
|
func (s *terminalSessionCore) Signal(sig os.Signal) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.closed {
|
|
return errors.New("terminal session core closed")
|
|
}
|
|
if s.cmd == nil || s.cmd.Process == nil {
|
|
return errors.New("no process to signal")
|
|
}
|
|
return s.cmd.Process.Signal(sig)
|
|
}
|
|
|
|
func (s *terminalSessionCore) Close() error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.closed {
|
|
return nil
|
|
}
|
|
s.closed = true
|
|
err := s.input.Close()
|
|
if s.cmd != nil && s.cmd.Process != nil {
|
|
_ = s.cmd.Process.Kill()
|
|
}
|
|
if isAlreadyClosedError(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|