#!/usr/bin/env python3 """Сервис выполнения CLI команд (локальных и SSH).""" import asyncio import logging import os import pty from datetime import datetime from typing import Tuple import asyncssh from telegram import Update from bot.config import config, state_manager, server_manager from bot.models.server import Server from bot.models.session import ssh_session_manager, local_session_manager from bot.utils.ssh_readers import read_ssh_output, read_pty_output, detect_input_type from bot.utils.formatters import format_long_output, escape_markdown, send_long_message from bot.utils.cleaners import clean_ansi_codes, normalize_output logger = logging.getLogger(__name__) async def execute_cli_command(query, command: str): """Выполнение CLI команды из кнопки меню.""" user_id = query.from_user.id state = state_manager.get(user_id) server_name = state.current_server server = server_manager.get(server_name) # Определяем рабочую директорию working_dir = state.working_directory or config.working_directory logger.info(f"Выполнение команды: {command} на сервере: {server_name}, в директории: {working_dir}") # Если локальный сервер — выполняем локально if server_name == "local" or server is None: await _execute_local_command(query, command, working_dir) else: # Выполняем через SSH await _execute_ssh_command(query, command, server, working_dir) async def _execute_local_command(query, command: str, working_dir: str): """Выполнение локальной команды через PTY.""" user_id = query.from_user.id try: logger.info(f"Создание PTY для команды: {command}") # Создаём PTY master_fd, slave_fd = pty.openpty() logger.info(f"PTY создан: master_fd={master_fd}") # Запускаем процесс в PTY pid = os.fork() if pid == 0: # Дочерний процесс os.close(master_fd) os.setsid() os.dup2(slave_fd, 0) # stdin os.dup2(slave_fd, 1) # stdout os.dup2(slave_fd, 2) # stderr os.close(slave_fd) os.chdir(working_dir) os.execvp("/bin/bash", ["/bin/bash", "-c", command]) else: # Родительский процесс os.close(slave_fd) logger.info(f"Процесс запущен: pid={pid}") # Создаём сессию session = local_session_manager.create_session( user_id=user_id, command=command, master_fd=master_fd, pid=pid ) # Читаем начальный вывод logger.info("Чтение вывода из PTY...") output, is_done = read_pty_output(master_fd, timeout=3.0) logger.info(f"Прочитано: {len(output)} байт, is_done={is_done}") logger.debug(f"Вывод: {output[:500] if output else '(пусто)'}") session.output_buffer = output session.last_activity = datetime.now() # Проверяем тип ввода input_type = detect_input_type(output) logger.info(f"Тип ввода: {input_type}") if input_type == "password": session.waiting_for_input = True session.input_type = "password" await query.answer() await query.message.reply_text( f"⏳ *Требуется ввод*\n\n" f"Команда: `{command}`\n\n" f"🔐 *Запрошен пароль*\n\n" f"```\n{output.strip()[-200:]}\n```\n\n" f"Отправьте пароль в чат:", parse_mode="Markdown" ) return elif input_type == "confirm": session.waiting_for_input = True session.input_type = "confirm" await query.answer() await query.message.reply_text( f"⏳ *Требуется ввод*\n\n" f"Команда: `{command}`\n\n" f"❓ *Требуется подтверждение*\n\n" f"```\n{output.strip()[-200:]}\n```\n\n" f"Отправьте `y` (да) или `n` (нет):", parse_mode="Markdown" ) return elif is_done: local_session_manager.close_session(user_id) await _show_result(query, command, output.encode(), b"", 0) return else: # Команда ещё выполняется await query.answer() await query.message.reply_text( f"⏳ *Выполнение...*\n\n" f"Команда: `{command}`\n\n" f"```\n{output.strip()[-500:] if output else 'Выполняется...'}\n```", parse_mode="Markdown" ) max_iterations = 60 # Максимум 60 итераций (5 минут при timeout=5.0) iteration_count = 0 while not is_done and iteration_count < max_iterations: more_output, is_done = read_pty_output(master_fd, timeout=5.0) output += more_output session.output_buffer = output session.last_activity = datetime.now() iteration_count += 1 input_type = detect_input_type(output) if input_type in ("password", "confirm"): session.waiting_for_input = True session.input_type = input_type await query.answer() await query.message.reply_text( f"⏳ *Требуется ввод*\n\n" f"Команда: `{command}`\n\n" f"{'🔐 *Запрошен пароль*' if input_type == 'password' else '❓ *Требуется подтверждение'}\n\n" f"```\n{output.strip()[-200:]}\n```\n\n" f"{'Отправьте пароль в чат:' if input_type == 'password' else 'Отправьте `y` (да) или `n` (нет):'}", parse_mode="Markdown" ) return if iteration_count >= max_iterations: logger.warning(f"Превышено максимальное количество итераций ({max_iterations}) для команды {command}") local_session_manager.close_session(user_id) await _show_result(query, command, output.encode(), "Превышено время выполнения команды".encode(), 1) return local_session_manager.close_session(user_id) await _show_result(query, command, output.encode(), b"", 0) except Exception as e: logger.error(f"Ошибка выполнения команды: {e}") local_session_manager.close_session(user_id) await query.edit_message_text( f"❌ *Ошибка:*\n```\n{str(e)}\n```", parse_mode="Markdown" ) async def _execute_ssh_command(query, command: str, server: Server, working_dir: str): """Выполнение команды через SSH с интерактивной сессией.""" user_id = query.from_user.id try: # Подготовка SSH ключа client_keys = [server_manager.ssh_key_path] if server_manager.ssh_key_path else None # Подготовка параметров подключения connect_kwargs = { "host": server.host, "port": server.port, "username": server.user, "client_host_keys": None, "known_hosts": None } # Добавляем ключ или пароль if client_keys: connect_kwargs["client_keys"] = client_keys if server.password: connect_kwargs["password"] = server.password logger.info(f"SSH подключение к {server.host}:{server.port} как {server.user}") # Подключение к серверу conn = await asyncssh.connect(**connect_kwargs) # Выполнение команды с cd в рабочую директорию full_command = f"cd {working_dir} && {command}" if working_dir else command # Создаем интерактивный процесс с PTY для поддержки ввода # TERM环境变量设置 для корректной кодировки process = await conn.create_process( full_command, term_type='xterm-256color', env={'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8'} ) # Создаём сессию session = ssh_session_manager.create_session( user_id=user_id, server=server, working_dir=working_dir, conn=conn, process=process, command=command ) # Читаем начальный вывод output, is_done = await read_ssh_output(process, timeout=3.0) session.output_buffer = output session.last_activity = datetime.now() # Читаем пока процесс не завершится while not is_done: more_output, is_done = await read_ssh_output(process, timeout=2.0) output += more_output session.output_buffer = output session.last_activity = datetime.now() # Проверяем тип ввода input_type = detect_input_type(output) if input_type == "password": # Запрос пароля session.waiting_for_input = True session.input_type = "password" await query.answer() await query.message.reply_text( f"⏳ *Требуется ввод*\n\n" f"Команда: `{command}`\n\n" f"🔐 *Запрошен пароль*\n\n" f"```\n{output.strip()[-200:]}\n```\n\n" f"Отправьте пароль в чат:", parse_mode="Markdown" ) return elif input_type == "confirm": # Запрос подтверждения session.waiting_for_input = True session.input_type = "confirm" await query.answer() await query.message.reply_text( f"⏳ *Требуется ввод*\n\n" f"Команда: `{command}`\n\n" f"❓ *Требуется подтверждение*\n\n" f"```\n{output.strip()[-200:]}\n```\n\n" f"Отправьте `y` (да) или `n` (нет):", parse_mode="Markdown" ) return else: # Команда завершена, показываем результат ssh_session_manager.close_session(user_id) await _show_result(query, command, output.encode(), "", 0) return except asyncssh.Error as e: logger.error(f"SSH ошибка: {e}") ssh_session_manager.close_session(user_id) await query.answer() await query.message.reply_text( f"❌ *SSH ошибка:*\n```\n{str(e)}\n```", parse_mode="Markdown" ) except asyncio.TimeoutError: logger.error("Таймаут SSH подключения") ssh_session_manager.close_session(user_id) await query.answer() await query.message.reply_text( "❌ *Таймаут*\n\nКоманда выполнялась дольше 30 секунд и была прервана.", parse_mode="Markdown" ) except Exception as e: logger.error(f"Ошибка выполнения команды: {e}") ssh_session_manager.close_session(user_id) await query.answer() await query.message.reply_text( f"❌ *Ошибка:*\n```\n{str(e)}\n```", parse_mode="Markdown" ) async def _show_result(query, command: str, stdout: bytes | str, stderr: bytes | str, returncode: int): """Показ результата выполнения команды.""" # Обрабатываем как bytes так и str if isinstance(stdout, bytes): output = clean_ansi_codes(stdout.decode("utf-8", errors="replace")) else: output = clean_ansi_codes(str(stdout)) output = normalize_output(output) if isinstance(stderr, bytes): error = clean_ansi_codes(stderr.decode("utf-8", errors="replace")) else: error = clean_ansi_codes(str(stderr)) result = f"✅ *Результат:*\n\n" if output: # Форматируем длинный вывод output = format_long_output(output) result += f"```\n{output}\n```\n" if error: result += f"*Ошибки:*\n```\n{error}\n```\n" result += f"\n*Код возврата:* `{returncode}`" # НЕ используем escape_markdown — вывод внутри ``` не требует экранирования # Экранируем только backticks если они есть вне блоков кода result = escape_markdown(result) # Отправляем с разбивкой на части если нужно await send_long_message(query, result, parse_mode="Markdown")