telegram-cli-bot/bot/services/command_executor.py

300 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Сервис выполнения CLI команд (локальных и SSH)."""
import asyncio
import logging
from typing import Tuple
import asyncssh
from telegram import Update
from bot.config import config, state_manager, server_manager, ssh_session_manager, local_session_manager
from bot.models.server import Server
from bot.utils.ssh_readers import read_ssh_output, read_pty_output, detect_input_type
from bot.utils.formatters import format_long_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.edit_message_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.edit_message_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.edit_message_text(
f"⏳ *Выполнение...*\n\n"
f"Команда: `{command}`\n\n"
f"```\n{output.strip()[-500:] if output else 'Выполняется...'}\n```",
parse_mode="Markdown"
)
while not is_done:
more_output, is_done = read_pty_output(master_fd, timeout=5.0)
output += more_output
session.output_buffer = output
session.last_activity = datetime.now()
input_type = detect_input_type(output)
if input_type in ("password", "confirm"):
session.waiting_for_input = True
session.input_type = input_type
await query.edit_message_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
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.edit_message_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.edit_message_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.edit_message_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.edit_message_text(
"❌ *Таймаут*\n\nКоманда выполнялась дольше 30 секунд и была прервана.",
parse_mode="Markdown"
)
except Exception as e:
logger.error(f"Ошибка выполнения команды: {e}")
ssh_session_manager.close_session(user_id)
await query.edit_message_text(
f"❌ *Ошибка:*\n```\n{str(e)}\n```",
parse_mode="Markdown"
)
async def _show_result(query, command: str, stdout: bytes, stderr: bytes, returncode: int):
"""Показ результата выполнения команды."""
output = clean_ansi_codes(stdout.decode("utf-8", errors="replace"))
output = normalize_output(output)
error = clean_ansi_codes(stderr.decode("utf-8", errors="replace"))
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}`"
# Экранируем backticks
result = escape_markdown(result)
# Отправляем с разбивкой на части если нужно
await send_long_message(query, result, parse_mode="Markdown")