70 lines
2.3 KiB
Python
70 lines
2.3 KiB
Python
|
|
"""Интерактивный терминал: 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
|