ProcessService: allocate a terminal (PTY) for interactive processes

Open
#123 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
42/100
Issue type
Feature
Clarity
Clearly specified
Activity status
Active
Tech stack
go, grpc, linux
Domain
api, backend

Research direction

Start from the ProcessService definitions and the StartProcess handler, and read how stdin, stdout and stderr are wired to plain pipes and how output is spooled for StreamProcessOutput. Add the optional TerminalSize field 6 and the ResizeProcessTerminal RPC, then check that a process started with a terminal gets a controlling PTY, that SIGWINCH is delivered on resize, and that the no-terminal path is unchanged.

Written by the indexing model from the issue text.

Description

StartProcess always runs the command with plain pipes for stdin, stdout and stderr. There's no
way to ask for a terminal, so clients can't offer an interactive session. Without a terminal,
programs in the guest have:

  • no prompt, job control or line editing; arrow keys arrive as ^[[A
  • no full-screen programs (vim, top, less), because isatty() is false and there is no
    window size
  • Ctrl-C handled by the client, which has to translate it to SignalProcess(SIGINT), instead
    of by the terminal inside the guest

Clients that need a terminal include:

  • Interactive shells: for example, AX's ax ssh <task>
    opens a shell in a running task through this API.
  • Web and desktop terminals: dashboards or IDEs that embed a terminal (xterm.js) for a
    sandbox.
  • Agents driving interactive CLIs: REPLs, debuggers and tools that change their behavior or
    refuse to run without a TTY.

Today, clients work around this by wrapping the command in script(1) to create a PTY inside the
guest and passing the window size with stty rows R cols C. AX's desktop app does this:

stty raw -echo
ax ssh task -- sh -c 'exec env TERM=xterm-256color script -qefc \
  "stty rows $0 cols $1; exec bash -i" /dev/null' "$(tput lines)" "$(tput cols)"
stty sane

This depends on script being in the image, ignores window resizes after startup, and every
client has to reinvent it.

Proposal

Add an optional terminal to StartProcessRequest, and an RPC to resize it.

message TerminalSize {
  // Rows and columns of the terminal window. Both must be > 0.
  uint32 rows = 1;
  uint32 cols = 2;
}

message StartProcessRequest {
  // ... existing fields 1-5 ...

  // If set, the process runs with a pseudo-terminal of this size as its
  // controlling terminal and its stdin, stdout and stderr. Requires stdin.
  // All output arrives as ProcessOutput.stdout; stderr is unused.
  TerminalSize size = 6;
}

service ProcessService {
  // ... existing RPCs ...

  // ResizeProcessTerminal changes the window size of a process started with
  // a terminal, and delivers SIGWINCH to its foreground process group.
  // Fails with FAILED_PRECONDITION if the process has no terminal.
  rpc ResizeProcessTerminal(ResizeProcessTerminalRequest) returns (Process);
}

message ResizeProcessTerminalRequest {
  string process_id = 1;
  TerminalSize size = 2;
}
Semantics
  • Startup: the guest opens a PTY, sets its size, makes it the process's controlling terminal
    (setsid plus TIOCSCTTY), and connects the PTY to stdin, stdout and stderr.
  • Input: WriteProcessInput writes to the PTY. Control characters such as Ctrl-C (0x03) and
    Ctrl-Z go through the terminal, which sends the signal itself, so clients just forward raw
    keystrokes. close closes the input side, and the process sees EOF or a hangup.
  • Output: everything the PTY emits is spooled as stdout, so StreamProcessOutput offsets and
    replay keep working. stderr stays empty.
  • Environment: TERM is unchanged. Clients set it through env, e.g.
    TERM=xterm-256color.
  • No terminal requested: behavior is the same as today.
Example client: ax ssh

When ax ssh runs in a terminal, it would:

  1. read the window size, and put the local terminal in raw mode;
  2. call StartProcess with stdin: true, terminal: {rows, cols} and
    env: {TERM: $TERM};
  3. forward keystrokes with WriteProcessInput, and call ResizeProcessTerminal on SIGWINCH;
  4. restore the local terminal when the process exits.

A web terminal does the same thing, using xterm.js's onData and onResize events.

Alternatives considered
  • script(1) wrapper (current workaround): depends on the image, sizes the terminal only
    once, and has to be duplicated in every client.
  • Resize sent in-band through stdin: mixes control data with user input and can't carry the
    size reliably.

Related to #107.

Dominant language
Go
Stars
70
Forks
17
Avg merge
21h 36m
Merged PRs (30d)
19

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from agent-substrate/env

All issues in agent-substrate/env

Similar issues

More Go issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.