1
0
forked from BRT/arc

Handle edge cases in log processing, enhance JSON rendering in monitoring logs, and improve logging in AdbUtils.

This commit is contained in:
slava 2026-06-04 22:07:36 +05:00
parent a0583c4966
commit 521101463b
3 changed files with 72 additions and 9 deletions

View File

@ -243,13 +243,20 @@ bool AdbUtils::tryToStartApplication(const QString &deviceId, const QString &pac
logMultiline("[AdbUtils::tryToStartApplication] monkey stdout:", stdOut); logMultiline("[AdbUtils::tryToStartApplication] monkey stdout:", stdOut);
logMultiline("[AdbUtils::tryToStartApplication] monkey stderr:", stdErr); 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; return true;
} }
// Фолбэк: am start с конкретной activity. У него осмысленный stderr и exit code, // Фолбэк: am start с конкретной activity. У него осмысленный stderr и exit code,
// в отличие от monkey, который при пустом resolve просто молча печатает "No activities found". // в отличие от monkey, который при пустом resolve просто молча печатает "No activities found".
// Также используется когда monkey дал "Events injected: 1" но приложение упало (NativeCrash).
if (!launcherActivity.isEmpty()) { if (!launcherActivity.isEmpty()) {
if (hadNativeCrash)
qWarning().noquote() << "[AdbUtils::tryToStartApplication] NativeCrash detected, retrying via am start -n" << launcherActivity;
else
qDebug().noquote() << "[AdbUtils::tryToStartApplication] monkey не дал Events injected;" qDebug().noquote() << "[AdbUtils::tryToStartApplication] monkey не дал Events injected;"
<< "пробую am start -n" << launcherActivity; << "пробую am start -n" << launcherActivity;
QProcess am; QProcess am;

View File

@ -19,6 +19,7 @@ import asyncio
import hashlib import hashlib
import hmac import hmac
import html as html_lib import html as html_lib
import json as json_lib
import logging import logging
import os import os
import re 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) _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: def _render_msg(message: str) -> str:
"""Клиент шлёт логи ошибок уже обёрнутыми в структурные теги <b>…</b>, """Клиент шлёт логи ошибок уже обёрнутыми в структурные теги <b>…</b>,
динамику внутри экранирует на стороне C++ (toHtmlEscaped). Прочие источники динамику внутри экранирует на стороне C++ (toHtmlEscaped). Прочие источники
(AppLogger, task_success) шлют голый текст. Экранируем всё, затем возвращаем (AppLogger, task_success) шлют голый текст. Экранируем всё, затем возвращаем
из белого списка только <b>/</b> bold рендерится, инъекция HTML невозможна. из белого списка только <b>/</b> bold рендерится, инъекция HTML невозможна.
Тело JSON (всё после маркера «JSON: » до конца сообщения) выносим в Тело JSON (всё после маркера «JSON: » до конца сообщения, либо bare-JSON на
моноширинный <pre>-блок. Саму подпись «JSON:» не показываем и так понятно. последней строке) выносим в моноширинный <pre>-блок с pretty-print.
Содержимое уже экранировано, инъекции нет.""" Содержимое уже экранировано, инъекций нет."""
s = html_lib.escape(message or "") s = html_lib.escape(message or "")
s = s.replace("&lt;b&gt;", "<b>").replace("&lt;/b&gt;", "</b>") s = s.replace("&lt;b&gt;", "<b>").replace("&lt;/b&gt;", "</b>")
json_body = None
head = s
marker = "\nJSON: " marker = "\nJSON: "
idx = s.find(marker) idx = s.find(marker)
if idx != -1: if idx != -1:
head = s[:idx] head = s[:idx]
body = s[idx + len(marker):] json_body = s[idx + len(marker):]
s = f'{head}\n<pre class=json>{body}</pre>' 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<pre class=json>{_pretty_json(json_body)}</pre>'
return s return s

View File

@ -84,8 +84,40 @@ bool LogArchiveSender::collectInto(const QString &destDir) {
QDir().mkpath(destDir); QDir().mkpath(destDir);
bool any = false; bool any = false;
if (hasLog) { if (hasLog) {
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 (QFile::copy(logPath, destDir + "/application.log")) any = true;
} }
}
if (hasDb) { if (hasDb) {
const QString dbBaseName = QFileInfo(dbPath).fileName(); const QString dbBaseName = QFileInfo(dbPath).fileName();
if (QFile::copy(dbPath, destDir + "/" + dbBaseName)) any = true; if (QFile::copy(dbPath, destDir + "/" + dbBaseName)) any = true;