Source code for ralph.process.pty

"""POSIX PTY process primitives for unattended interactive runtimes.

This module owns the low-level pseudo-terminal spawn path used by transports that
must behave like a real interactive terminal session. The parent process keeps the
master file descriptor; the child gets the slave side as its controlling terminal.
"""

from __future__ import annotations

import errno
import fcntl
import os
import select
import struct
import termios
from typing import TYPE_CHECKING

from ralph.process._pty_process import PtyProcess

if TYPE_CHECKING:
    from collections.abc import Sequence

_DEFAULT_COLUMNS = 80
_DEFAULT_ROWS = 24
_READ_CHUNK_SIZE = 4096
_STDERR_FD = 2


[docs] def spawn_pty_process( command: Sequence[str], *, cwd: str | None, env: dict[str, str] | None, cols: int = _DEFAULT_COLUMNS, rows: int = _DEFAULT_ROWS, ) -> PtyProcess: """Spawn a child under a real PTY and return the parent-side handle.""" if os.name == "nt": raise OSError("PTY-backed interactive Claude is supported only on POSIX platforms") master_fd, slave_fd = os.openpty() _set_winsize(slave_fd, rows=rows, cols=cols) pid = os.fork() if pid == 0: try: os.close(master_fd) os.setsid() fcntl.ioctl(slave_fd, termios.TIOCSCTTY, 0) os.dup2(slave_fd, 0) os.dup2(slave_fd, 1) os.dup2(slave_fd, _STDERR_FD) if slave_fd > _STDERR_FD: os.close(slave_fd) if cwd is not None: os.chdir(cwd) child_env = dict(os.environ) if env is not None: child_env.update(env) child_env.setdefault("TERM", "xterm-256color") os.execvpe(command[0], list(command), child_env) except BaseException: os._exit(127) _set_nonblocking(master_fd) return PtyProcess(pid=pid, master_fd=master_fd, slave_fd=slave_fd)
[docs] def wait_for_master_readable(master_fd: int, timeout_seconds: float) -> bool: """Return True when the PTY master has readable data within the timeout.""" readable, _writable, _errors = select.select([master_fd], [], [], timeout_seconds) return bool(readable)
[docs] def read_master_chunk(master_fd: int, max_bytes: int = _READ_CHUNK_SIZE) -> bytes: """Read one chunk from the PTY master, tolerating EIO-on-EOF semantics.""" try: return os.read(master_fd, max_bytes) except OSError as exc: if exc.errno == errno.EIO: return b"" raise
def _set_nonblocking(fd: int) -> None: flags = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) def _set_winsize(fd: int, *, rows: int, cols: int) -> None: packed = struct.pack("HHHH", rows, cols, 0, 0) fcntl.ioctl(fd, termios.TIOCSWINSZ, packed) __all__ = [ "PtyProcess", "read_master_chunk", "spawn_pty_process", "wait_for_master_readable", ]