From fb9fa279a6c821f9d7335da4e827e5f0e9b490e8 Mon Sep 17 00:00:00 2001 From: slava Date: Tue, 2 Jun 2026 18:25:25 +0500 Subject: [PATCH] Refine error handling for "Interrupted" events, optimize screenshot throttling, and reduce redundant logs and heavy operations in ADB workflows. --- android/dump/TaskDumpRecorder.cpp | 10 +++++-- services/DeviceScreener.cpp | 29 +++++++++++++++++---- services/DeviceScreener.h | 12 ++++++++- services/EventHandler.cpp | 43 ++++++++++++++++++++----------- 4 files changed, 71 insertions(+), 23 deletions(-) diff --git a/android/dump/TaskDumpRecorder.cpp b/android/dump/TaskDumpRecorder.cpp index 248798b..1546a47 100644 --- a/android/dump/TaskDumpRecorder.cpp +++ b/android/dump/TaskDumpRecorder.cpp @@ -223,9 +223,15 @@ void TaskDumpRecorder::finishInternal(const QString &errorMessage) { m_finished = true; if (m_folder.isEmpty()) return; - if (errorMessage.isEmpty()) { + // "Interrupted" трактуем как НЕ-ошибку для дампа: это следствие внешнего + // requestInterruption() (watchdog-таймаут или clearAll при остановке/выходе), + // а не самостоятельный сбой. Причину уже репортит инициатор прерывания + // (watchdog шлёт "Script timeout (N min)"), поэтому тяжёлый ZIP-дамп по + // Interrupted не шлём — иначе на один таймаут копится дубль log+dump. + if (errorMessage.isEmpty() || errorMessage == "Interrupted") { QDir(m_folder).removeRecursively(); - qDebug() << "[TaskDumpRecorder] success — removed" << m_folder; + qDebug() << "[TaskDumpRecorder]" << (errorMessage.isEmpty() ? "success" : "interrupted") + << "— removed" << m_folder; return; } diff --git a/services/DeviceScreener.cpp b/services/DeviceScreener.cpp index b6a12df..0d874d0 100644 --- a/services/DeviceScreener.cpp +++ b/services/DeviceScreener.cpp @@ -355,10 +355,19 @@ void DeviceScreener::start() { appsDue.insert(serial); } } + // С каких устройств пора снять превью-кадр (троттл screencap, см. + // kScreencapIntervalSecs) — чтобы не грузить adb каждый цикл. + QSet shotDue; + for (const QString &serial : deviceSerials) { + if (nowSec - m_lastScreencap.value(serial, 0) >= kScreencapIntervalSecs) { + shotDue.insert(serial); + } + } const QList scans = QtConcurrent::blockingMapped >( &scanPool, deviceSerials, - [&cacheSnapshot, &appsDue](const QString &serial) { - return scanDeviceIo(serial, cacheSnapshot.value(serial), appsDue.contains(serial)); + [&cacheSnapshot, &appsDue, &shotDue](const QString &serial) { + return scanDeviceIo(serial, cacheSnapshot.value(serial), + appsDue.contains(serial), shotDue.contains(serial)); }); // --- Фаза B: всё, что трогает БД/члены класса — последовательно в этом потоке. @@ -398,6 +407,12 @@ void DeviceScreener::start() { liveSet.insert(device.id); + // Отмечаем попытку захвата превью (даже если кадр пустой) — чтобы не + // дёргать screencap каждый цикл на устройстве, где он подвисает. + if (shotDue.contains(scan.adbSerial)) { + m_lastScreencap[scan.adbSerial] = nowSec; + } + device.image = scan.image; if (!device.image.isEmpty()) { if (!DeviceDAO::updateImage(device.id, device.image)) { @@ -624,7 +639,7 @@ static const QRegularExpression batteryRegex(R"(level:\s*(\d+))"); DeviceScreener::ScanResult DeviceScreener::scanDeviceIo(const QString &adbSerial, const DeviceInfo &cachedStatic, - bool readApps) { + bool readApps, bool takeShot) { ScanResult result; result.adbSerial = adbSerial; @@ -692,8 +707,12 @@ DeviceScreener::ScanResult DeviceScreener::scanDeviceIo(const QString &adbSerial info.updateTime = QDateTime::currentDateTime(); result.info = info; - // Тяжёлый чистый adb-I/O — скриншот всегда, список приложений по троттлу. - result.image = takeScreenshot(adbSerial); + // Тяжёлый чистый adb-I/O — превью-кадр и список приложений по троттлу. + // takeShot=false → кадр не снимаем (result.image пустой), фаза B просто не + // обновит превью в этом цикле; следующий захват — через kScreencapIntervalSecs. + if (takeShot) { + result.image = takeScreenshot(adbSerial); + } if (readApps) { result.apps = AdbUtils::getListApps(adbSerial); result.appsRead = true; diff --git a/services/DeviceScreener.h b/services/DeviceScreener.h index 783f719..aa22977 100644 --- a/services/DeviceScreener.h +++ b/services/DeviceScreener.h @@ -24,6 +24,13 @@ public: // Сколько устройств опрашивать по adb параллельно (фаза A). Ограничено, чтобы // не насыщать USB-шину одновременными screencap'ами. static constexpr int kMaxParallelScans = 6; + // Как часто снимать превью-кадр устройства (screencap). Раньше кадр снимался + // КАЖДЫЙ цикл (~1-2с на устройство) и при ~10 девайсах насыщал общий adb-сервер, + // конкурируя с uiautomator-дампами рабочих скриптов (FETCH_TRANSACTIONS) и + // приближая их к 10-минутному watchdog'у. Превью в UI и истории экрана так часто + // не нужно (отправка в мониторинг и так троттлится kMonitoringScreenIntervalSecs), + // поэтому захват ограничиваем. Online/offline-детект от кадра не зависит. + static constexpr qint64 kScreencapIntervalSecs = 4; // Как часто перечитывать список установленных приложений (getListApps). Список // меняется редко, поэтому не дёргаем adb каждый цикл. static constexpr qint64 kAppsCheckIntervalSecs = 60; @@ -57,6 +64,7 @@ private: // устройства в OFFLINE (на случай переподключения другого аппарата по тому же serial). QMap m_staticInfoCache; QMap m_lastAppsCheck; // adbSerial → unix ts последнего getListApps + QMap m_lastScreencap; // adbSerial → unix ts последнего screencap (троттл превью) // Результат чистого adb-опроса одного устройства (фаза A, выполняется в пуле // потоков). Не содержит ничего, что требует БД/членов класса. @@ -76,7 +84,9 @@ private: // Чистый adb-I/O одного устройства: потокобезопасно (не трогает БД и члены). // cachedStatic — снимок ранее прочитанной статики (пустой id → читаем с нуля). // readApps — перечитывать ли список установленных приложений в этом цикле. - static ScanResult scanDeviceIo(const QString &adbSerial, const DeviceInfo &cachedStatic, bool readApps); + // takeShot — снимать ли превью-кадр (screencap) в этом цикле (троттл, см. + // kScreencapIntervalSecs). false → result.image пустой, превью не обновляется. + static ScanResult scanDeviceIo(const QString &adbSerial, const DeviceInfo &cachedStatic, bool readApps, bool takeShot); void postDevicesData(const QList &devices); diff --git a/services/EventHandler.cpp b/services/EventHandler.cpp index 6d01e4a..08e6229 100644 --- a/services/EventHandler.cpp +++ b/services/EventHandler.cpp @@ -363,22 +363,35 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event, if (newStatus == EventStatus::Error) { // Используем скриншот из скрипта если есть, иначе снимаем новый QByteArray screenshot = m_screenshots.take(key); - if (screenshot.isEmpty() && !event.deviceId.isEmpty()) { - const DeviceInfo device = DeviceDAO::getDeviceById(event.deviceId); - const QString adbSerial = device.adbSerial.isEmpty() ? event.deviceId : device.adbSerial; - screenshot = AdbUtils::captureScreenshotBytes(adbSerial); - } - if (!screenshot.isEmpty()) { - EventDAO::updateScreenshot(event.id, screenshot); - } - // Дублируем ошибку в general_logs со скриншотом - const DeviceInfo logDevice = DeviceDAO::getDeviceById(event.deviceId); - const QString deviceLabel = logDevice.name.isEmpty() ? event.deviceId : logDevice.name; - const QString logMsg = QString("[%1] device=%2 bank=%3: %4") - .arg(eventTypeToString(event.type), deviceLabel, event.bankName, result); - GeneralLogDAO::insert("CRITICAL", "EventHandler", logMsg, screenshot); - sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, result, screenshot, event.id, event.transactionId, event.accountId); + // "Interrupted" — не самостоятельная ошибка, а следствие внешнего + // requestInterruption(): его выставляет либо watchdog (он уже шлёт + // "Script timeout (N min)" в startThreadForTask), либо clearAll() при + // остановке/выходе (намеренная остановка). Реальную причину репортит + // инициатор прерывания, поэтому здесь Interrupted на сервер НЕ + // дублируем — иначе на один таймаут выходит три записи в мониторинге + // (timeout-log + interrupted-log + interrupted-dump). Логику declined + // для CREATE_TRANSACTION ниже это не затрагивает. + if (result != "Interrupted") { + if (screenshot.isEmpty() && !event.deviceId.isEmpty()) { + const DeviceInfo device = DeviceDAO::getDeviceById(event.deviceId); + const QString adbSerial = device.adbSerial.isEmpty() ? event.deviceId : device.adbSerial; + screenshot = AdbUtils::captureScreenshotBytes(adbSerial); + } + if (!screenshot.isEmpty()) { + EventDAO::updateScreenshot(event.id, screenshot); + } + // Дублируем ошибку в general_logs со скриншотом + const DeviceInfo logDevice = DeviceDAO::getDeviceById(event.deviceId); + const QString deviceLabel = logDevice.name.isEmpty() ? event.deviceId : logDevice.name; + const QString logMsg = QString("[%1] device=%2 bank=%3: %4") + .arg(eventTypeToString(event.type), deviceLabel, event.bankName, result); + GeneralLogDAO::insert("CRITICAL", "EventHandler", logMsg, screenshot); + + sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, result, screenshot, event.id, event.transactionId, event.accountId); + } else { + qDebug() << "[EventHandler] Interrupted — server error report skipped (cause already reported by interrupter), event id:" << event.id; + } if (event.type == EventType::CreateTransaction) { const MaterialInfo declAcc = MaterialDAO::getAccountById(event.accountId);