Files
healthbot/src/healthbot/runs.py
T

148 lines
4.5 KiB
Python

import contextlib
from collections.abc import Awaitable, Callable
from aiogram.exceptions import TelegramBadRequest
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from healthbot.komodo import KomodoError, Update, clean_output, komodo
from healthbot.ui import edit, esc, pre, show
from utils.format import clock
RUN_TIMEOUT = 30 * 60
OUTPUT_LIMIT = 3000
QUICK_SECONDS = 10
NOISE = ("Action completed successfully",)
ACTIVE: set[str] = set()
Back = list[tuple[str, str]]
def _stage_text(output: str) -> str:
lines = [
line
for line in clean_output(output).splitlines()
if not any(mark in line for mark in NOISE)
]
return "\n".join(lines).strip()
def output_of(update: Update, *, only_failed: bool) -> str:
parts = []
for stage in update.stages:
if "config" in stage.name.lower():
if not stage.success:
parts.append(f"{stage.name}: вывод скрыт, в нём бывают секреты")
continue
if only_failed and stage.success:
continue
text = _stage_text(stage.output)
if not text:
continue
parts.append(f"{stage.name}:\n{text}" if len(update.stages) > 1 else text)
return "\n\n".join(parts)
def result_text(title: str, update: Update, *, verbose: bool, extra: str = "") -> str:
icon = "✅" if update.success else "🔴"
state = "" if update.success else " не удалось"
lines = [f"{icon} <b>{esc(title)}</b>{state} · {clock(update.seconds)}"]
if extra:
lines.append(extra)
if verbose or not update.success:
output = output_of(update, only_failed=not verbose)
if output:
lines.append(pre(output, OUTPUT_LIMIT))
return "\n".join(lines)
def result_kb(update: Update | None, back: Back) -> InlineKeyboardMarkup:
kb = InlineKeyboardBuilder()
for text, data in back:
kb.button(text=text, callback_data=data)
rows = [len(back)] if back else []
if update is not None and len(output_of(update, only_failed=False)) > OUTPUT_LIMIT:
kb.button(text="Весь вывод файлом", callback_data=f"run:file:{update.id}")
rows.append(1)
kb.adjust(*rows or [1])
return kb.as_markup()
async def track( # noqa: PLR0913
origin: Message | CallbackQuery,
title: str,
starter: Callable[[], Awaitable[Update]],
*,
back: Back,
verbose: bool,
extra: str = "",
new: bool = False,
) -> None:
source = origin.message if isinstance(origin, CallbackQuery) else origin
key = f"{source.message_id if source else 0}:{title}"
if key in ACTIVE:
return
ACTIVE.add(key)
try:
await _track(
origin, title, starter, back, verbose=verbose, extra=extra, new=new
)
finally:
ACTIVE.discard(key)
async def _track( # noqa: PLR0913
origin: Message | CallbackQuery,
title: str,
starter: Callable[[], Awaitable[Update]],
back: Back,
*,
verbose: bool,
extra: str,
new: bool,
) -> None:
progress = await show(origin, f"⏳ <b>{esc(title)}</b> выполняется", new=new)
if progress is None:
return
try:
started = await starter()
except KomodoError as exc:
await edit(
progress,
f"🔴 <b>{esc(title)}</b> не запустилось\n{esc(exc)}",
result_kb(None, back),
)
return
if not started.id:
await edit(progress, f"✅ <b>{esc(title)}</b> запущено", result_kb(None, back))
return
check = InlineKeyboardBuilder()
check.button(text="Проверить", callback_data=f"run:st:{started.id}")
try:
update = await komodo.wait(started.id, RUN_TIMEOUT)
except KomodoError as exc:
await edit(
progress,
f"⏳ <b>{esc(title)}</b> запущено, результат узнать не удалось\n{esc(exc)}",
check.as_markup(),
)
return
if update is None:
await edit(
progress,
f"⏳ <b>{esc(title)}</b> идёт дольше {RUN_TIMEOUT // 60} минут",
check.as_markup(),
)
return
text = result_text(title, update, verbose=verbose, extra=extra)
markup = result_kb(update, back)
if update.seconds < QUICK_SECONDS:
await edit(progress, text, markup)
return
await show(progress, text, markup)
with contextlib.suppress(TelegramBadRequest):
await progress.delete()