2025, Sep 24 03:16
Как управлять дочерним PTY в Python и отслеживать экран терминала
Запуск программ в PTY на Python, интерпретация управляющих кодов и ведение виртуального экрана. Как читать состояние терминала и тестировать TUI на практике.
Управлять интерактивной программой через дочерний PTY и запрашивать состояние экрана в произвольных позициях кажется простой задачей — пока не попробуешь сделать это на Python. Цель понятна: запустить подпроцесс в псевдотерминале, считывать его вывод так, как это делает терминал, отправлять нажатия клавиш в ответ и в любой момент иметь возможность спросить: «какой символ находится в строке r, столбце c?» Сложность в том, что терминалы — это не простые текстовые потоки. Это устройства со внутренним состоянием, управляемые управляющими последовательностями, и это меняет картину целиком.
Постановка задачи
Ниже — упрощённый сценарий в виде короткого фрагмента. Мы запускаем программу в PTY, смотрим на конкретную ячейку экрана, подаём ввод и проверяем, что ячейка изменилась как ожидается.
# концептуальное использование
session = create_pty()
session.launch(app_to_run)
session.peek_cell(2, 4) # ожидается "@"
session.feed_keys("A")
session.peek_cell(2, 4) # ожидается "!" после того, как ввод изменит экран
Что на самом деле мешает
В стандартной библиотеке нет инструмента, который сделает это за вас. PTY операционной системы предоставляет поток байтов. Он не знает, что такое тип терминала; это знание находится на другой стороне соединения — в физическом терминале или эмуляторе терминала вроде xterm, putty, MacTerm, cmd или PowerShell. Чтобы отслеживать, что сейчас «на экране», недостающий компонент нужно реализовать самостоятельно.
Для этого подпроцесс должен стартовать с корректным типом терминала в окружении. Даже после этого его вывод нужно интерпретировать согласно этому типу терминала. То есть преобразовывать управляющие коды терминала в перемещения курсора, вывод символов, стирание и прокрутку, а затем поддерживать в памяти модель экрана, которая отражает то, что показал бы настоящий терминал. Задача нетривиальная.
Подход к решению
На практике подход состоит из двух шагов. Во‑первых, запустите дочерний процесс внутри PTY и убедитесь, что он видит нужный тип терминала. Во‑вторых, перехватывайте его вывод и интерпретируйте его с учётом этого типа, ведя собственный виртуальный экранный буфер, к которому можно обращаться по координатам. Запись в PTY отправляет ввод дочернему процессу — так же, как печать в оконном терминале. При проектировании взаимодействия с терминалом можно присмотреться к библиотеке curses, но ключевое требование не меняется: нужно интерпретировать вывод подпроцесса, чтобы отслеживать состояние экрана.
import os
import pty
def run_in_pty(cmd, argv):
master_fd, slave_fd = pty.openpty()
pid = os.fork()
if pid == 0:
# дочерний процесс
env = os.environ.copy()
env["TERM"] = "set-proper-term" # укажите подходящий тип терминала
os.setsid()
os.dup2(slave_fd, 0)
os.dup2(slave_fd, 1)
os.dup2(slave_fd, 2)
os.close(master_fd)
os.close(slave_fd)
os.execvpe(cmd, argv, env)
else:
# родительский процесс
os.close(slave_fd)
return pid, master_fd
class DisplayModel:
def __init__(self, rows=24, cols=80):
self.rows = rows
self.cols = cols
self.grid = [[" "] * cols for _ in range(rows)]
# в реальной реализации также отслеживались бы состояние курсора и прокрутки
def ingest(self, data_bytes):
# Интерпретируйте data_bytes согласно типу терминала.
# Преобразуйте управляющие последовательности в перемещения курсора, вывод и прокрутку.
# Намеренно не реализовано; именно это и является сложной частью.
pass
def glyph_at(self, r, c):
return self.grid[r][c]
def send_keys(fd, text):
os.write(fd, text.encode())
# пример связки (неполный; цикл чтения опущен):
# pid, pty_fd = run_in_pty("my_program", ["my_program"])
# view = DisplayModel()
# ... read() from pty_fd and call view.ingest(...) repeatedly ...
# send_keys(pty_fd, "A")
# cell = view.glyph_at(2, 4)
Почему это важно
Автоматизация, скрейпинг и тестирование терминальных приложений часто требуют проверки состояния экрана после каждого нажатия клавиши. Без интерпретации вывода в рамках выбранного типа терминала любая попытка «прочитать конкретный символ в строке и столбце» будет ненадёжной. Сам PTY экрана не хранит — он лишь передаёт байты. Если вам нужен экран, его придётся построить из этих байтов, как это делает эмулятор терминала.
Итоги
Если вам нужно обращаться к дочернему PTY как к двумерной сетке со случайным доступом, закладывайте две обязанности: обеспечить дочернему процессу правильный тип терминала и реализовать слой, который преобразует его вывод в виртуальный экран. Для этого нет готовой функции в стандартной библиотеке, а основная сложность — именно логика интерпретации. Если вы работаете с терминальными интерфейсами, стоит посмотреть на библиотеку curses для паттернов взаимодействия, но ключевое требование остаётся прежним: необходимо переводить управляющие коды терминала в достоверную модель экрана, чтобы можно было надёжно отвечать на вопросы вроде «какой символ в строке r, столбце c?»