Class TerminalProcess

java.lang.Object
org.apache.juneau.terminal.region.TerminalProcess
All Implemented Interfaces:
AutoCloseable, TerminalSink

public final class TerminalProcess extends Object implements AutoCloseable, TerminalSink
Runs one tool under a pseudo-terminal through juneau_terminal.py --pty and exposes its output as a FileTerminalSource and its steps as a FileTerminalViewSource.

The command line is python3 <script> --console none --pty --size <cols>x<rows> --full-log <log> --events <events> <tool> -- <cmd...>, with the log ("terminal.log"), its .size sidecar and the events file ("terminal.events.jsonl") in the given run directory. The script is found through the system property "juneau.terminal.script", then the environment variable "JUNEAU_TERMINAL_SCRIPT".

The run directory is single-use. It is also the tool's working directory, and the runner's own stdout and stderr go to runner.out there, in append mode and uncapped; that file is diagnostic only.

Both sources share one done flag, set once the process has exited. If the runner exits without writing a done event, a warning note and done {status:"fail"} are appended first. Only the last 65536 bytes of the events file are scanned for that done event, because the protocol puts it last.

The exit hook runs on one daemon thread per process, named juneau-terminal-exit-<pid>. Its executor is shut down once the hook has run, and close() waits for that.

Started with input on, the runner also gets --input stdin (right after the events pair) and the process is a TerminalSink: write(byte[]) queues the bytes in a 65536-byte pending buffer and never blocks, and one daemon thread per process, named juneau-terminal-input-<pid>, copies them to the runner's stdin, flushing after each chunk. A closed or broken pipe turns acceptsInput() false and discards the buffer. With input off the command line and the streams are exactly as above, no writer thread starts, and acceptsInput() is always false.

Started with a TerminalPrompts, the runner gets --prompts stdio and its stdout and stdin become the prompt channel: one daemon thread per process, named terminal-prompts-<dir name>, reads the stdout lines (at most 1048576 bytes each; a longer one is dropped) and hands them to TerminalPrompts.onLine(String), then calls TerminalPrompts.onEnd() once. The runner's stderr goes to runner.out. answer(String) writes one line back. Neither a prompt line nor an answer is ever logged. Input and prompts are mutually exclusive.

POSIX only: there is no Windows pseudo-terminal support.

Example:

TerminalProcess p = TerminalProcess.start("maven", List.of("mvn", "test"), 120, 40, runDir); terminals.put(id, p.terminal()); terminalViews.put(id, p.events());

  • Field Details

  • Method Details

    • start

      public static TerminalProcess start(String tool, List<String> cmd, int cols, int rows, Path dir) throws IOException
      Starts a tool under a pseudo-terminal.
      Parameters:
      tool - The juneau_terminal tool parser: maven, pytest, playwright or generic.
      cmd - The command and its arguments; at least one element. It is a token list: each element reaches the tool as one argument, through ProcessBuilder. It is never joined into a string or given to a shell, so shell metacharacters in an element are literal.
      cols - The terminal width, 1-9999.
      rows - The terminal height, 1-9999.
      dir - The run directory; created if missing. Each run needs its own: the log and events files must be missing or empty. It is also the tool's working directory, and the runner's own stdout and stderr go to runner.out in it, in append mode and uncapped; that file is diagnostic only.
      Returns:
      The running process.
      Throws:
      IOException - If the directory could not be prepared or the process could not start.
      IllegalArgumentException - If an argument is out of range.
      IllegalStateException - If the script is not configured, python3 is not on the PATH, or the run directory already holds a run.
      UnsupportedOperationException - On Windows.
    • start

      public static TerminalProcess start(String tool, List<String> cmd, int cols, int rows, Path dir, boolean input) throws IOException
      Starts a tool under a pseudo-terminal, optionally with input.

      Same as start(String, List, int, int, Path), except that with input on the runner gets --input stdin and the returned process accepts write(byte[]).

      Parameters:
      tool - The juneau_terminal tool parser.
      cmd - The command and its arguments; at least one element.
      cols - The terminal width, 1-9999.
      rows - The terminal height, 1-9999.
      dir - The run directory; see start(String, List, int, int, Path).
      input - Whether the job takes input through write(byte[]).
      Returns:
      The running process.
      Throws:
      IOException - If the directory could not be prepared or the process could not start.
      IllegalArgumentException - If an argument is out of range.
      IllegalStateException - If the script is not configured, python3 is not on the PATH, or the run directory already holds a run.
      UnsupportedOperationException - On Windows.
    • start

      public static TerminalProcess start(String tool, List<String> cmd, int cols, int rows, Path dir, TerminalPrompts prompts) throws IOException
      Starts a tool under a pseudo-terminal with a prompt channel.

      Same as start(String, List, int, int, Path), except that the runner gets --prompts stdio, the script's prompt lines go to prompts and answer(String) sends the replies.

      Parameters:
      tool - The juneau_terminal tool parser.
      cmd - The command and its arguments; at least one element.
      cols - The terminal width, 1-9999.
      rows - The terminal height, 1-9999.
      dir - The run directory; see start(String, List, int, int, Path).
      prompts - Receives the prompt lines; not null.
      Returns:
      The running process.
      Throws:
      IOException - If the directory could not be prepared or the process could not start.
      IllegalArgumentException - If an argument is out of range.
      IllegalStateException - If the script is not configured, python3 is not on the PATH, or the run directory already holds a run.
      UnsupportedOperationException - On Windows.
    • terminal

      The raw byte source over the log.
      Returns:
      The source.
    • events

      The step events source over the events file.
      Returns:
      The source.
    • log

      public Path log()
      The byte log.
      Returns:
      The log path.
    • eventsFile

      public Path eventsFile()
      The events file.
      Returns:
      The events path.
    • isDone

      public boolean isDone()
      Whether the process has exited and the events file ends with a done event.
      Returns:
      true once finished.
    • waitFor

      public int waitFor(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
      Waits for the process to exit and for the done event to be in place.
      Parameters:
      timeout - The longest wait.
      unit - The unit of timeout.
      Returns:
      The runner's exit code.
      Throws:
      InterruptedException - If interrupted.
      TimeoutException - If the process is still running.
    • acceptsInput

      public boolean acceptsInput()
      Whether input is accepted right now.
      Specified by:
      acceptsInput in interface TerminalSink
      Returns:
      true only when input is on, the process is alive, no write has failed and the process has not been closed.
    • write

      public boolean write(byte[] bytes)
      Queues the bytes for the runner's stdin; never blocks.
      Specified by:
      write in interface TerminalSink
      Parameters:
      bytes - The bytes to send; copied.
      Returns:
      false when input is not accepted, or when the bytes do not fit in the 65536-byte pending buffer. An empty array returns acceptsInput().
    • acceptsAnswers

      public boolean acceptsAnswers()
      Whether an answer can be sent right now.
      Returns:
      true only when a prompt channel is on, the process is alive and the channel has not ended or failed.
    • answer

      public boolean answer(String jsonLine)
      Writes one answer line to the script's prompt channel; never blocks behind another answer and never logs the line.
      Parameters:
      jsonLine - The answer as one JSON line, without a newline.
      Returns:
      false when there is no channel, it has ended, the line holds a newline, another answer is being written, or the write failed.
    • nudges

      public TerminalProcess nudges(String terminalId, TerminalNudgePublisher publisher)
      Polls the log's size every 100 ms and reports growth, then a final done, to a publisher.

      The runner writes the log in another process, so there is no in-process append hook to use. The publisher runs on a daemon thread named juneau-terminal-nudge-<pid>; a RuntimeException it throws is logged at FINE and otherwise ignored. The thread ends after the done report, or when close() is called.

      Parameters:
      terminalId - The terminal id passed to the publisher; must be a valid run id.
      publisher - The publisher.
      Returns:
      This object.
      Throws:
      NullPointerException - If an argument is null.
      IllegalArgumentException - If the id is not a valid run id.
      IllegalStateException - If a publisher is already set, or this process has been closed.
    • close

      public void close()
      Kills the runner and its tool if they are still running, then waits up to 5 seconds for the exit hook.

      The hook writes the done event and shuts its executor down, so once this returns normally both have happened. The executor is not shut down here: a hook submitted after that would be refused, and the done flag would never be set.

      With input on, it first stops input: pending bytes are dropped, stdin is closed and the writer thread is told to stop. If the writer thread is blocked writing to a full pipe, it closes stdin itself once the kill breaks the pipe. Afterwards this waits up to 1000 ms for the writer thread. The nudge thread, if any, is stopped first of all and joined for up to 1000 ms. With input off, it only kills.

      Specified by:
      close in interface AutoCloseable