"""Интерактивный терминал: PTY-сессия по SSH с потоковым вводом/выводом.""" from __future__ import annotations import asyncio from typing import Callable, Optional import asyncssh from app.db.models import Server class TerminalSession: """PTY-терминал к серверу. output_cb(str) вызывается для каждого куска вывода.""" def __init__(self, server: Server, output_cb: Callable[[str], None], cols: int = 120, rows: int = 30): self.server = server self.output_cb = output_cb self.cols = cols self.rows = rows self._conn: Optional[asyncssh.SSHClientConnection] = None self._proc: Optional[asyncssh.SSHClientProcess] = None self._closed = False async def open(self) -> None: kwargs: dict = dict( host=self.server.host, port=self.server.port, username=self.server.username, known_hosts=None, connect_timeout=15, ) if self.server.auth_type == "key" and self.server.key_path: kwargs["client_keys"] = [self.server.key_path] if self.server.password: kwargs["passphrase"] = self.server.password else: kwargs["password"] = self.server.password self._conn = await asyncssh.connect(**kwargs) self._proc = await self._conn.create_process( term_type="xterm-256color", term_size=(self.cols, self.rows), ) asyncio.ensure_future(self._reader()) async def _reader(self) -> None: try: while not self._closed and self._proc: data = await self._proc.stdout.read(4096) if not data: break self.output_cb(data) except Exception: pass def write(self, data: str) -> None: if self._proc: self._proc.stdin.write(data) def resize(self, cols: int, rows: int) -> None: if self._proc: self._proc.change_terminal_size(cols, rows) def close(self) -> None: self._closed = True try: if self._proc: self._proc.terminate() if self._conn: self._conn.close() except Exception: pass