Full implementation of multi-type server management across GUI and CLI: New clients: SQLClient (MariaDB/MSSQL/PostgreSQL), RedisClient, GrafanaClient, PrometheusClient, TelnetSession, WinRMClient, RemoteDesktopLauncher. New GUI tabs: QueryTab (SQL editor + Treeview), RedisTab (console + history), GrafanaTab (dashboards + alerts), PrometheusTab (PromQL + targets), PowershellTab (PS/CMD), LaunchTab (RDP/VNC external client). Infrastructure: TAB_REGISTRY for conditional tabs per server type, adaptive server_dialog fields, colored type badges in sidebar, status checker for all types (SSH/TCP/SQL/Redis/HTTP), 100+ i18n keys. CLI: ssh.py extended with --sql, --redis, --grafana-*, --prom-*, --ps, --cmd. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
181 lines
6.2 KiB
Python
181 lines
6.2 KiB
Python
"""
|
|
Telnet client — interactive telnet session with the same interface as ShellSession.
|
|
"""
|
|
|
|
import asyncio
|
|
import threading
|
|
from core.logger import log
|
|
|
|
|
|
class TelnetSession:
|
|
"""Interactive telnet session — same interface as ShellSession from ssh_client.py."""
|
|
|
|
def __init__(self, server: dict, cols: int = 80, rows: int = 24):
|
|
self.server = server
|
|
self.cols = cols
|
|
self.rows = rows
|
|
self._loop: asyncio.AbstractEventLoop | None = None
|
|
self._thread: threading.Thread | None = None
|
|
self._reader = None
|
|
self._writer = None
|
|
self._running = False
|
|
|
|
# Callbacks — set by the owner
|
|
self.on_data = None # on_data(data: bytes)
|
|
self.on_disconnect = None # on_disconnect()
|
|
|
|
@property
|
|
def connected(self) -> bool:
|
|
return self._running and self._writer is not None
|
|
|
|
def connect(self):
|
|
"""Start telnet connection in a background thread running an asyncio event loop."""
|
|
self._running = True
|
|
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
|
self._thread.start()
|
|
|
|
def _run_loop(self):
|
|
"""Entry point for the background thread — creates event loop and runs connection."""
|
|
self._loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(self._loop)
|
|
try:
|
|
self._loop.run_until_complete(self._async_connect())
|
|
except Exception as e:
|
|
log.debug(f"TelnetSession loop error: {e}")
|
|
finally:
|
|
self._running = False
|
|
try:
|
|
self._loop.close()
|
|
except Exception:
|
|
pass
|
|
self._loop = None
|
|
if self.on_disconnect:
|
|
self.on_disconnect()
|
|
|
|
async def _async_connect(self):
|
|
"""Async telnet connection: open, login, then read loop."""
|
|
try:
|
|
import telnetlib3
|
|
except ImportError:
|
|
log.error("telnetlib3 not installed. Run: pip install telnetlib3")
|
|
raise ImportError("telnetlib3 is required for telnet connections")
|
|
|
|
hostname = self.server["ip"]
|
|
port = self.server.get("port", 23)
|
|
user = self.server.get("user", "")
|
|
password = self.server.get("password", "")
|
|
|
|
log.info(f"TelnetSession connecting to {self.server.get('alias', '?')} port {port}")
|
|
|
|
reader, writer = await telnetlib3.open_connection(
|
|
host=hostname,
|
|
port=port,
|
|
cols=self.cols,
|
|
rows=self.rows,
|
|
connect_minwait=0.5,
|
|
)
|
|
self._reader = reader
|
|
self._writer = writer
|
|
|
|
# Login sequence — wait for prompts and send credentials
|
|
await self._login_sequence(reader, writer, user, password)
|
|
|
|
# Main read loop
|
|
await self._read_loop(reader)
|
|
|
|
async def _login_sequence(self, reader, writer, user: str, password: str):
|
|
"""Wait for login/password prompts and send credentials."""
|
|
buf = ""
|
|
timeout = 10.0 # seconds to wait for login prompt
|
|
|
|
while self._running:
|
|
try:
|
|
data = await asyncio.wait_for(reader.read(4096), timeout=timeout)
|
|
except asyncio.TimeoutError:
|
|
log.debug("TelnetSession login sequence timed out waiting for prompt")
|
|
break
|
|
except Exception:
|
|
break
|
|
|
|
if not data:
|
|
break
|
|
|
|
if self.on_data:
|
|
self.on_data(data.encode("utf-8", errors="replace") if isinstance(data, str) else data)
|
|
|
|
buf += data if isinstance(data, str) else data.decode("utf-8", errors="replace")
|
|
buf_lower = buf.lower()
|
|
|
|
if "login:" in buf_lower or "username:" in buf_lower:
|
|
writer.write(user + "\r\n")
|
|
buf = ""
|
|
continue
|
|
|
|
if "password:" in buf_lower:
|
|
writer.write(password + "\r\n")
|
|
buf = ""
|
|
break # Login done, proceed to read loop
|
|
|
|
# If we see a shell prompt, login may not be required
|
|
if buf_lower.rstrip().endswith(("$", "#", ">")):
|
|
break
|
|
|
|
log.debug("TelnetSession login sequence complete")
|
|
|
|
async def _read_loop(self, reader):
|
|
"""Read data from telnet and forward to on_data callback."""
|
|
try:
|
|
while self._running:
|
|
try:
|
|
data = await asyncio.wait_for(reader.read(65536), timeout=0.5)
|
|
except asyncio.TimeoutError:
|
|
continue
|
|
except Exception:
|
|
break
|
|
|
|
if not data:
|
|
break
|
|
|
|
raw = data.encode("utf-8", errors="replace") if isinstance(data, str) else data
|
|
if self.on_data:
|
|
self.on_data(raw)
|
|
except Exception as e:
|
|
log.debug(f"TelnetSession read loop error: {e}")
|
|
|
|
def send(self, data: bytes):
|
|
"""Send data to the telnet session."""
|
|
if not self._running or self._writer is None or self._loop is None:
|
|
return
|
|
text = data.decode("utf-8", errors="replace")
|
|
try:
|
|
self._loop.call_soon_threadsafe(self._writer.write, text)
|
|
except RuntimeError:
|
|
self._running = False
|
|
if self.on_disconnect:
|
|
self.on_disconnect()
|
|
|
|
def resize(self, cols: int, rows: int):
|
|
"""Resize terminal — NAWS negotiation if supported, otherwise no-op."""
|
|
self.cols = cols
|
|
self.rows = rows
|
|
# telnetlib3 handles NAWS during initial negotiation;
|
|
# runtime resize requires protocol-level support which
|
|
# is not reliably available, so this is a best-effort no-op.
|
|
log.debug(f"TelnetSession resize requested: {cols}x{rows} (no-op)")
|
|
|
|
def disconnect(self):
|
|
"""Close telnet session and stop background thread."""
|
|
self._running = False
|
|
if self._writer is not None:
|
|
try:
|
|
self._writer.close()
|
|
except Exception as e:
|
|
log.debug(f"TelnetSession writer close: {e}")
|
|
self._writer = None
|
|
self._reader = None
|
|
if self._loop is not None:
|
|
try:
|
|
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
except RuntimeError:
|
|
pass
|