Source code for ralph.process._pty_process

"""PTY process handle for the parent side of a pseudo-terminal."""

from __future__ import annotations

import os
import signal
import time
from dataclasses import dataclass

from ralph.process._suppress_close_error import _SuppressCloseError
from ralph.process._suppress_missing_process import _SuppressMissingProcess

_READ_CHUNK_SIZE = 4096


[docs] @dataclass class PtyProcess: """Tracked PTY child process owned by the parent master file descriptor.""" pid: int master_fd: int slave_fd: int _returncode: int | None = None _closed: bool = False @property def returncode(self) -> int | None: self.poll() return self._returncode def poll(self) -> int | None: if self._returncode is not None: return self._returncode pid, status = os.waitpid(self.pid, os.WNOHANG) if pid == 0: return None self._returncode = _status_to_returncode(status) return self._returncode def wait(self, timeout: float | None = None) -> int: if self._returncode is not None: return self._returncode if timeout is None: _pid, status = os.waitpid(self.pid, 0) self._returncode = _status_to_returncode(status) return self._returncode deadline = time.monotonic() + timeout while True: rc = self.poll() if rc is not None: return rc if time.monotonic() >= deadline: raise TimeoutError from None time.sleep(0.01) def terminate(self) -> None: with _SuppressMissingProcess(): os.kill(self.pid, signal.SIGTERM) def kill(self) -> None: with _SuppressMissingProcess(): os.kill(self.pid, signal.SIGKILL) def read(self, max_bytes: int = _READ_CHUNK_SIZE) -> bytes: return os.read(self.master_fd, max_bytes) def fileno(self) -> int: return self.master_fd def isatty(self) -> bool: return os.isatty(self.master_fd) def close(self) -> None: if self._closed: return self._closed = True for fd in (self.master_fd, self.slave_fd): with _SuppressCloseError(): os.close(fd)
def _status_to_returncode(status: int) -> int: if os.WIFEXITED(status): return os.WEXITSTATUS(status) if os.WIFSIGNALED(status): return -os.WTERMSIG(status) return status