From 521101463b36970fe328ef1ca7f10700658cafa1 Mon Sep 17 00:00:00 2001 From: slava Date: Thu, 4 Jun 2026 22:07:36 +0500 Subject: [PATCH] Handle edge cases in log processing, enhance JSON rendering in monitoring logs, and improve logging in AdbUtils. --- android/adb/AdbUtils.cpp | 13 ++++++++++--- monitoring/app.py | 34 +++++++++++++++++++++++++++++----- services/LogArchiveSender.cpp | 34 +++++++++++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 9 deletions(-) diff --git a/android/adb/AdbUtils.cpp b/android/adb/AdbUtils.cpp index b22554e..fe69cc0 100644 --- a/android/adb/AdbUtils.cpp +++ b/android/adb/AdbUtils.cpp @@ -243,15 +243,22 @@ bool AdbUtils::tryToStartApplication(const QString &deviceId, const QString &pac logMultiline("[AdbUtils::tryToStartApplication] monkey stdout:", stdOut); logMultiline("[AdbUtils::tryToStartApplication] monkey stderr:", stdErr); - if (stdOut.contains("Events injected: 1")) { + const bool eventsInjected = stdOut.contains("Events injected: 1"); + const bool hadNativeCrash = stdOut.contains("NativeCrash"); + + if (eventsInjected && !hadNativeCrash) { return true; } // Фолбэк: am start с конкретной activity. У него осмысленный stderr и exit code, // в отличие от monkey, который при пустом resolve просто молча печатает "No activities found". + // Также используется когда monkey дал "Events injected: 1" но приложение упало (NativeCrash). if (!launcherActivity.isEmpty()) { - qDebug().noquote() << "[AdbUtils::tryToStartApplication] monkey не дал Events injected;" - << "пробую am start -n" << launcherActivity; + if (hadNativeCrash) + qWarning().noquote() << "[AdbUtils::tryToStartApplication] NativeCrash detected, retrying via am start -n" << launcherActivity; + else + qDebug().noquote() << "[AdbUtils::tryToStartApplication] monkey не дал Events injected;" + << "пробую am start -n" << launcherActivity; QProcess am; am.setWorkingDirectory(adbDir()); am.start(adbPath(), diff --git a/monitoring/app.py b/monitoring/app.py index a529924..d6e4295 100644 --- a/monitoring/app.py +++ b/monitoring/app.py @@ -19,6 +19,7 @@ import asyncio import hashlib import hmac import html as html_lib +import json as json_lib import logging import os import re @@ -666,22 +667,45 @@ _ISO_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T") _META_PREFIX_RE = re.compile(r"^(Device|Material|Amount|Phone|Card):", re.IGNORECASE) +def _pretty_json(raw: str) -> str: + """Pretty-print JSON string; return original if not valid JSON.""" + try: + obj = json_lib.loads(html_lib.unescape(raw.strip())) + return html_lib.escape(json_lib.dumps(obj, ensure_ascii=False, indent=2)) + except (json_lib.JSONDecodeError, ValueError): + return raw + + def _render_msg(message: str) -> str: """Клиент шлёт логи ошибок уже обёрнутыми в структурные теги , динамику внутри экранирует на стороне C++ (toHtmlEscaped). Прочие источники (AppLogger, task_success) шлют голый текст. Экранируем всё, затем возвращаем из белого списка только / — bold рендерится, инъекция HTML невозможна. - Тело JSON (всё после маркера «JSON: » до конца сообщения) выносим в - моноширинный
-блок. Саму подпись «JSON:» не показываем — и так понятно.
-    Содержимое уже экранировано, инъекции нет."""
+    Тело JSON (всё после маркера «JSON: » до конца сообщения, либо bare-JSON на
+    последней строке) выносим в моноширинный 
-блок с pretty-print.
+    Содержимое уже экранировано, инъекций нет."""
     s = html_lib.escape(message or "")
     s = s.replace("<b>", "").replace("</b>", "")
+
+    json_body = None
+    head = s
+
     marker = "\nJSON: "
     idx = s.find(marker)
     if idx != -1:
         head = s[:idx]
-        body = s[idx + len(marker):]
-        s = f'{head}\n
{body}
' + json_body = s[idx + len(marker):] + else: + # Bare JSON block at end of message (no "JSON: " prefix) + nl = s.rfind("\n") + if nl != -1: + last = s[nl + 1:].lstrip() + if last.startswith("{") or last.startswith("["): + head = s[:nl] + json_body = s[nl + 1:] + + if json_body is not None: + s = f'{head}\n
{_pretty_json(json_body)}
' return s diff --git a/services/LogArchiveSender.cpp b/services/LogArchiveSender.cpp index e7eea16..8e4a883 100644 --- a/services/LogArchiveSender.cpp +++ b/services/LogArchiveSender.cpp @@ -84,7 +84,39 @@ bool LogArchiveSender::collectInto(const QString &destDir) { QDir().mkpath(destDir); bool any = false; if (hasLog) { - if (QFile::copy(logPath, destDir + "/application.log")) any = true; + QFile srcLog(logPath); + if (srcLog.open(QIODevice::ReadOnly)) { + const QDateTime cutoff = QDateTime::currentDateTime().addSecs(-3600); + + // Binary search for the first line with ts >= cutoff. + // Relies on monotonically non-decreasing timestamps in the log. + qint64 lo = 0, hi = srcLog.size(); + while (lo < hi) { + const qint64 mid = lo + (hi - lo) / 2; + srcLog.seek(mid); + if (mid > 0) srcLog.readLine(); // align to next line boundary + const qint64 linePos = srcLog.pos(); + if (linePos >= hi) { hi = mid; continue; } + const QByteArray probe = srcLog.readLine(); + if (probe.isEmpty()) { hi = mid; continue; } + const QDateTime ts = QDateTime::fromString( + QString::fromLatin1(probe.left(23)), "yyyy-MM-dd hh:mm:ss.zzz"); + if (!ts.isValid() || ts < cutoff) + lo = srcLog.pos(); // this line is old, search upper half + else + hi = linePos; // this line is new, search lower half + } + srcLog.seek(lo); + + QFile dstLog(destDir + "/application.log"); + if (dstLog.open(QIODevice::WriteOnly)) { + while (!srcLog.atEnd()) + dstLog.write(srcLog.readLine()); + any = true; + } + } else { + if (QFile::copy(logPath, destDir + "/application.log")) any = true; + } } if (hasDb) { const QString dbBaseName = QFileInfo(dbPath).fileName();