305 lines
13 KiB
Python
305 lines
13 KiB
Python
#!/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.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")
|
||
|