1
0
forked from BRT/arc

Add appVersion to TaskDumpMeta structure and update associated scripts for enhanced application version tracking in tasks and monitoring outputs.

This commit is contained in:
slava 2026-06-01 14:29:05 +05:00
parent f6d895ab34
commit 29ae105c64
23 changed files with 152 additions and 34 deletions

View File

@ -37,7 +37,7 @@ void GetCardInfoScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("black", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("black", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error);
// Читаем текущий экран // Читаем текущий экран

View File

@ -35,7 +35,7 @@ void GetProfileInfoScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("black", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("black", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error);
// Читаем текущий экран // Читаем текущий экран

View File

@ -36,7 +36,7 @@ void LoginAndCheckAccountsScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("black", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("black", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -188,6 +188,8 @@ QString TaskDumpRecorder::buildCaption(const QString &errorMessage) const {
body += QStringLiteral("\nMaterial: %1").arg(m_meta.materialId.toHtmlEscaped()); body += QStringLiteral("\nMaterial: %1").arg(m_meta.materialId.toHtmlEscaped());
if (!m_meta.profilePhone.isEmpty()) if (!m_meta.profilePhone.isEmpty())
body += QStringLiteral("\nPhone: %1").arg(m_meta.profilePhone.toHtmlEscaped()); body += QStringLiteral("\nPhone: %1").arg(m_meta.profilePhone.toHtmlEscaped());
if (!m_meta.appVersion.isEmpty())
body += QStringLiteral("\nAppVersion: %1").arg(m_meta.appVersion.toHtmlEscaped());
if (m_meta.amount != 0) if (m_meta.amount != 0)
body += QStringLiteral("\nAmount: %1").arg(static_cast<qint64>(m_meta.amount)); body += QStringLiteral("\nAmount: %1").arg(static_cast<qint64>(m_meta.amount));
if (!m_meta.phone.isEmpty()) if (!m_meta.phone.isEmpty())
@ -256,6 +258,7 @@ void TaskDumpRecorder::finishInternal(const QString &errorMessage) {
if (!m_meta.androidId.isEmpty()) ts << "android_id: " << m_meta.androidId << "\n"; if (!m_meta.androidId.isEmpty()) ts << "android_id: " << m_meta.androidId << "\n";
if (!m_meta.bankProfileId.isEmpty()) ts << "profile_id: " << m_meta.bankProfileId << "\n"; if (!m_meta.bankProfileId.isEmpty()) ts << "profile_id: " << m_meta.bankProfileId << "\n";
if (!m_meta.profilePhone.isEmpty()) ts << "profile_phone: " << m_meta.profilePhone << "\n"; if (!m_meta.profilePhone.isEmpty()) ts << "profile_phone: " << m_meta.profilePhone << "\n";
if (!m_meta.appVersion.isEmpty()) ts << "app_version: " << m_meta.appVersion << "\n";
if (!m_meta.materialId.isEmpty()) ts << "material_id: " << m_meta.materialId << "\n"; if (!m_meta.materialId.isEmpty()) ts << "material_id: " << m_meta.materialId << "\n";
if (m_meta.amount != 0) ts << "amount: " << static_cast<qint64>(m_meta.amount) << "\n"; if (m_meta.amount != 0) ts << "amount: " << static_cast<qint64>(m_meta.amount) << "\n";
if (!m_meta.phone.isEmpty()) ts << "phone: " << m_meta.phone << "\n"; if (!m_meta.phone.isEmpty()) ts << "phone: " << m_meta.phone << "\n";

View File

@ -14,6 +14,7 @@ struct TaskDumpMeta {
QString bankProfileId; // серверный UUID банковского профиля (детект в общих логах) QString bankProfileId; // серверный UUID банковского профиля (детект в общих логах)
QString profilePhone; // телефон банковского профиля (не получателя) QString profilePhone; // телефон банковского профиля (не получателя)
QString androidId; // стабильный android_id устройства (в отличие от adbSerial не меняется) QString androidId; // стабильный android_id устройства (в отличие от adbSerial не меняется)
QString appVersion; // версия банковского приложения (из pm dump versionName)
}; };
class TaskDumpRecorder { class TaskDumpRecorder {

View File

@ -36,7 +36,7 @@ void GetCardInfoScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -278,7 +278,7 @@ void GetLastTransactionsScript::doStart() {
} }
const double firstAmount = m_targets.isEmpty() ? 0.0 : m_targets.first().first; const double firstAmount = m_targets.isEmpty() ? 0.0 : m_targets.first().first;
const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}, app.bankProfileId, app.phone, device.id}; const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
@ -407,12 +407,12 @@ void GetLastTransactionsScript::doStart() {
// Проход идёт вниз от текущей позиции. Если цель не нашлась — возможно, // Проход идёт вниз от текущей позиции. Если цель не нашлась — возможно,
// предыдущая (более новая) цель отсутствовала и увела список вниз, а эта // предыдущая (более новая) цель отсутствовала и увела список вниз, а эта
// лежит выше. Тогда надёжно возвращаемся к началу Выписки и пробуем ещё раз. // лежит выше. Тогда надёжно возвращаемся к началу Выписки и пробуем ещё раз.
bool found = searchOne(device, accountId, amount, time); SearchResult res = searchOne(device, accountId, amount, time);
if (!found && m_error != "Interrupted") { if (res == SearchResult::NotFound && m_error != "Interrupted") {
scrollHistoryToTop(device); scrollHistoryToTop(device);
found = searchOne(device, accountId, amount, time); res = searchOne(device, accountId, amount, time);
} }
if (!found) { if (res != SearchResult::Found) {
if (m_error == "Interrupted") break; if (m_error == "Interrupted") break;
notFound << QString("amount=%1 time=%2") notFound << QString("amount=%1 time=%2")
.arg(QString::number(amount, 'f', 2), time.toString(Qt::ISODate)); .arg(QString::number(amount, 'f', 2), time.toString(Qt::ISODate));
@ -484,8 +484,9 @@ void GetLastTransactionsScript::scrollHistoryToTop(const DeviceInfo &device) {
QThread::msleep(500); QThread::msleep(500);
} }
bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountId, GetLastTransactionsScript::SearchResult GetLastTransactionsScript::searchOne(
double searchAmount, const QDateTime &searchTime) { const DeviceInfo &device, int accountId,
double searchAmount, const QDateTime &searchTime) {
// Скриншот списка транзакций — отправим при неудаче // Скриншот списка транзакций — отправим при неудаче
const QByteArray listScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId); const QByteArray listScreenshot = AdbUtils::captureScreenshotBytes(m_deviceId);
@ -511,7 +512,7 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI
for (int scroll = 0; scroll <= maxScrolls; ++scroll) { for (int scroll = 0; scroll <= maxScrolls; ++scroll) {
if (interrupted()) { if (interrupted()) {
m_error = "Interrupted"; m_error = "Interrupted";
return false; return SearchResult::NotFound;
} }
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
@ -763,13 +764,13 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI
const int tapX = txNode.x1() + 80; const int tapX = txNode.x1() + 80;
AdbUtils::makeTap(m_deviceId, tapX, txNode.y()); AdbUtils::makeTap(m_deviceId, tapX, txNode.y());
QThread::msleep(3000); QThread::msleep(3000);
if (interrupted()) { m_error = "Interrupted"; return false; } if (interrupted()) { m_error = "Interrupted"; return SearchResult::NotFound; }
// Ждём экран деталей Выписки — ImageView с content-desc начинающимся с "Успешный платеж" // Ждём экран деталей Выписки — ImageView с content-desc начинающимся с "Успешный платеж"
OcrTransactionDetail detail; OcrTransactionDetail detail;
bool detailLoaded = false; bool detailLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) { for (int attempt = 0; attempt < 10; ++attempt) {
if (interrupted()) { m_error = "Interrupted"; return false; } if (interrupted()) { m_error = "Interrupted"; return SearchResult::NotFound; }
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
// 1) Bottom-sheet формат (3.2.33, см. assets/dushanbe/3.2.33/tx_detail.xml): // 1) Bottom-sheet формат (3.2.33, см. assets/dushanbe/3.2.33/tx_detail.xml):
@ -975,7 +976,7 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI
} }
QThread::msleep(1000); QThread::msleep(1000);
return true; // успех — транзакция найдена и отдана через transactionParsed return SearchResult::Found;
} }
// Окно поиска — только "сегодня + вчера". Список Выписки идёт сверху-вниз // Окно поиска — только "сегодня + вчера". Список Выписки идёт сверху-вниз
@ -997,13 +998,13 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI
" (newest visible=" << newestVisible.toString("dd.MM.yyyy") " (newest visible=" << newestVisible.toString("dd.MM.yyyy")
<< "< cutoff=" << cutoff.toString("dd.MM.yyyy") << "< cutoff=" << cutoff.toString("dd.MM.yyyy")
<< ") — stopping scroll"; << ") — stopping scroll";
break; return SearchResult::PastWindow;
} }
} }
// Скроллим вниз — свайп внутри области транзакций // Скроллим вниз — свайп внутри области транзакций
if (scroll < maxScrolls) { if (scroll < maxScrolls) {
if (interrupted()) { m_error = "Interrupted"; return false; } if (interrupted()) { m_error = "Interrupted"; return SearchResult::NotFound; }
const int x = device.screenWidth / 2; const int x = device.screenWidth / 2;
// Определяем область контента из реальных Y позиций items // Определяем область контента из реальных Y позиций items
int contentBottom = device.screenHeight * 2 / 3; int contentBottom = device.screenHeight * 2 / 3;
@ -1031,7 +1032,7 @@ bool GetLastTransactionsScript::searchOne(const DeviceInfo &device, int accountI
? AdbUtils::captureScreenshotBytes(m_deviceId) : listScreenshot; ? AdbUtils::captureScreenshotBytes(m_deviceId) : listScreenshot;
if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot); if (!m_resultScreenshot.isEmpty()) emit screenshotReady(m_resultScreenshot);
return false; return SearchResult::NotFound;
} }
} // namespace Dushanbe } // namespace Dushanbe

View File

@ -33,11 +33,15 @@ protected:
void doStart() override; void doStart() override;
private: private:
// Поиск одной цели по сумме и времени на экране "Выписка". Возвращает true enum class SearchResult { Found, NotFound, PastWindow };
// и эмитит transactionParsed, если нашли; false — если нет. Прерывание
// выставляет m_error="Interrupted". // Поиск одной цели по сумме и времени на экране "Выписка".
bool searchOne(const DeviceInfo &device, int accountId, // Found — нашли, transactionParsed эмитирован.
double searchAmount, const QDateTime &searchTime); // NotFound — не нашли, список прокручен до maxScrolls.
// PastWindow — список старше окна today+yesterday, retry бессмысленен.
// Прерывание выставляет m_error="Interrupted" и возвращает NotFound.
SearchResult searchOne(const DeviceInfo &device, int accountId,
double searchAmount, const QDateTime &searchTime);
// Возврат к началу списка "Выписка" между поисками целей. // Возврат к началу списка "Выписка" между поисками целей.
void scrollHistoryToTop(const DeviceInfo &device); void scrollHistoryToTop(const DeviceInfo &device);

View File

@ -39,7 +39,7 @@ void GetProfileInfoScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -41,7 +41,7 @@ void LoginAndCheckAccountsScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("dushanbe", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -123,7 +123,7 @@ void PayByCardScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone, device.id}; const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_CARD", device.adbSerial, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_CARD", device.adbSerial, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));

View File

@ -120,7 +120,7 @@ void PayByPhoneScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone, device.id}; const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_PHONE", device.adbSerial, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_PHONE", device.adbSerial, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));

View File

@ -42,7 +42,7 @@ void GetCardInfoScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_CARDS", m_deviceId, dumpMeta, &m_error);
// Читаем текущий экран // Читаем текущий экран

View File

@ -109,7 +109,7 @@ void GetLastTransactionsScript::doStart() {
} }
const double firstAmount = m_targets.isEmpty() ? 0.0 : m_targets.first().first; const double firstAmount = m_targets.isEmpty() ? 0.0 : m_targets.first().first;
const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}, app.bankProfileId, app.phone, device.id}; const TaskDumpMeta dumpMeta{m_materialId, firstAmount, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_TRANSACTIONS", m_deviceId, dumpMeta, &m_error);
if (!navigateToTransactionList(device, app)) { if (!navigateToTransactionList(device, app)) {

View File

@ -37,7 +37,7 @@ void GetProfileInfoScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_PROFILE_INFO", m_deviceId, dumpMeta, &m_error);
// Читаем текущий экран // Читаем текущий экран

View File

@ -40,7 +40,7 @@ void LoginAndCheckAccountsScript::doStart() {
return; return;
} }
const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id}; const TaskDumpMeta dumpMeta{{}, 0, {}, {}, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("ozon", "FETCH_PROFILE", m_deviceId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));

View File

@ -91,7 +91,7 @@ void PayByCardScript::doStart() {
} }
const QString &adbId = device.adbSerial; const QString &adbId = device.adbSerial;
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone, device.id}; const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, {}, m_cardNumber, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_CARD", adbId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_CARD", adbId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));

View File

@ -100,7 +100,7 @@ void PayByPhoneScript::doStart() {
} }
const QString &adbId = device.adbSerial; const QString &adbId = device.adbSerial;
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone, device.id}; const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_PHONE", adbId, dumpMeta, &m_error); TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_PHONE", adbId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));

View File

@ -23,6 +23,7 @@ pathMaterial=/api/v1/profile/material
pathProfilePing=/api/v1/profile/ping pathProfilePing=/api/v1/profile/ping
pathMonitoringLog=/monitoring/log pathMonitoringLog=/monitoring/log
pathMonitoringDump=/monitoring/dump pathMonitoringDump=/monitoring/dump
pathMonitoringScreenshot=/monitoring/screenshot
monitoringBase=http://142.93.102.40:8080 monitoringBase=http://142.93.102.40:8080
monitoringToken=509b91592749959b9ff56f9ed9189e5b3e91e1260003f63c477255e1c187298a monitoringToken=509b91592749959b9ff56f9ed9189e5b3e91e1260003f63c477255e1c187298a

View File

@ -9,6 +9,7 @@
#include <QThread> #include <QThread>
#include <QTimer> #include <QTimer>
#include <QImage> #include <QImage>
#include <QCryptographicHash>
#include <QHttpMultiPart> #include <QHttpMultiPart>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonArray> #include <QJsonArray>
@ -23,6 +24,7 @@
#include "dao/MaterialDAO.h" #include "dao/MaterialDAO.h"
#include "dao/SettingsDAO.h" #include "dao/SettingsDAO.h"
#include "net/NetworkErrorUtils.h" #include "net/NetworkErrorUtils.h"
#include "net/NetworkService.h"
DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) { DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) {
const QSettings settings("config.ini", QSettings::IniFormat); const QSettings settings("config.ini", QSettings::IniFormat);
@ -247,6 +249,28 @@ void DeviceScreener::postScreenshotToApi(const QString &apiId, const QByteArray
reply->deleteLater(); reply->deleteLater();
} }
void DeviceScreener::dispatchScreenshotToMonitoring(const QString &desktopId,
const QString &deviceId,
const QString &name,
const QByteArray &jpeg) {
if (jpeg.isEmpty()) return;
// Отдельный поток + свой NetworkService, чтобы таймаут отправки не тормозил
// цикл сканера (паттерн LogArchiveSender::dispatch).
auto *thread = new QThread;
auto *svc = new NetworkService;
svc->moveToThread(thread);
QObject::connect(thread, &QThread::started, svc,
[svc, desktopId, deviceId, name, jpeg]() {
svc->postMonitoringScreenshot(desktopId, deviceId, name, jpeg);
emit svc->finished();
});
QObject::connect(svc, &NetworkService::finished, thread, &QThread::quit);
QObject::connect(svc, &NetworkService::finished, svc, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater);
thread->start();
}
void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device) { void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device) {
// Находим первый активный bank_profile для этого устройства // Находим первый активный bank_profile для этого устройства
QJsonValue bankProfileId = QJsonValue::Null; QJsonValue bankProfileId = QJsonValue::Null;
@ -392,8 +416,8 @@ void DeviceScreener::start() {
qDebug() << "Device registered on API:" << device.id << "->" << apiId; qDebug() << "Device registered on API:" << device.id << "->" << apiId;
} }
} else if (status == "device") { } else if (status == "device") {
// Обновляем не чаще раза в 60 секунд
const qint64 now = QDateTime::currentSecsSinceEpoch(); const qint64 now = QDateTime::currentSecsSinceEpoch();
// Основной API обновляем не чаще раза в 60 секунд.
if (now - m_lastApiUpdate.value(device.id, 0) >= 60) { if (now - m_lastApiUpdate.value(device.id, 0) >= 60) {
device.apiId = saved.apiId; device.apiId = saved.apiId;
updateDeviceOnApi(device); updateDeviceOnApi(device);
@ -402,6 +426,21 @@ void DeviceScreener::start() {
} }
m_lastApiUpdate[device.id] = now; m_lastApiUpdate[device.id] = now;
} }
// История экрана на сервере логов: проверяем не чаще раза в
// kMonitoringScreenIntervalSecs и шлём кадр только если экран
// изменился относительно последнего отправленного (дедуп по sha256).
if (!device.image.isEmpty()
&& now - m_lastMonitoringScreen.value(device.id, 0) >= kMonitoringScreenIntervalSecs) {
m_lastMonitoringScreen[device.id] = now;
const QByteArray hash =
QCryptographicHash::hash(device.image, QCryptographicHash::Sha256);
if (m_lastSentScreenHash.value(device.id) != hash) {
m_lastSentScreenHash[device.id] = hash;
dispatchScreenshotToMonitoring(SettingsDAO::get("desktop_id"),
device.id, device.name, device.image);
}
}
} }
} }

View File

@ -15,6 +15,8 @@ public:
~DeviceScreener() override; ~DeviceScreener() override;
static constexpr qint64 kOfflineDebounceSecs = 30; static constexpr qint64 kOfflineDebounceSecs = 30;
// Как часто проверять/слать кадр истории экрана на сервер логов (с дедупом).
static constexpr qint64 kMonitoringScreenIntervalSecs = 30;
public slots: public slots:
void start(); void start();
@ -37,6 +39,8 @@ private:
QMap<QString, qint64> m_lastApiUpdate; // deviceId → unix timestamp последнего PATCH QMap<QString, qint64> m_lastApiUpdate; // deviceId → unix timestamp последнего PATCH
QSet<QString> m_onlineDevices; // дебаунс-онлайн (true online после dedup) QSet<QString> m_onlineDevices; // дебаунс-онлайн (true online после dedup)
QMap<QString, qint64> m_offlineSince; // deviceId → когда пропал из ADB (для дебаунса) QMap<QString, qint64> m_offlineSince; // deviceId → когда пропал из ADB (для дебаунса)
QMap<QString, qint64> m_lastMonitoringScreen; // deviceId → unix ts последней проверки кадра для истории экрана
QMap<QString, QByteArray> m_lastSentScreenHash; // deviceId → sha256 последнего кадра, отправленного в историю экрана
static QList<QPair<QString, QString> > readAdbDevices(); static QList<QPair<QString, QString> > readAdbDevices();
@ -53,4 +57,11 @@ private:
QString createDeviceOnApi(const DeviceInfo &device); QString createDeviceOnApi(const DeviceInfo &device);
void updateDeviceOnApi(const DeviceInfo &device); void updateDeviceOnApi(const DeviceInfo &device);
void postScreenshotToApi(const QString &apiId, const QByteArray &image); void postScreenshotToApi(const QString &apiId, const QByteArray &image);
// Fire-and-forget отправка кадра истории экрана на сервер логов (monitoringBase).
// Создаёт временный NetworkService в отдельном потоке (паттерн LogArchiveSender).
static void dispatchScreenshotToMonitoring(const QString &desktopId,
const QString &deviceId,
const QString &name,
const QByteArray &jpeg);
}; };

View File

@ -78,6 +78,7 @@ NetworkService::NetworkService(QObject *parent) : QObject(parent) {
m_pathTaskResult = settings.value("net/pathTaskResult", "/api/v1/task/task_result").toString(); m_pathTaskResult = settings.value("net/pathTaskResult", "/api/v1/task/task_result").toString();
m_pathMonitoringLog = settings.value("net/pathMonitoringLog", "/monitoring/log").toString(); m_pathMonitoringLog = settings.value("net/pathMonitoringLog", "/monitoring/log").toString();
m_pathMonitoringDump = settings.value("net/pathMonitoringDump", "/monitoring/dump").toString(); m_pathMonitoringDump = settings.value("net/pathMonitoringDump", "/monitoring/dump").toString();
m_pathMonitoringScreenshot = settings.value("net/pathMonitoringScreenshot", "/monitoring/screenshot").toString();
m_monitoringBase = settings.value("net/monitoringBase", m_apiBase).toString(); m_monitoringBase = settings.value("net/monitoringBase", m_apiBase).toString();
m_monitoringToken = settings.value("net/monitoringToken").toString(); m_monitoringToken = settings.value("net/monitoringToken").toString();
m_pathCurrentProfile = settings.value("net/pathCurrentProfile", "/api/v1/profile/current_profile").toString(); m_pathCurrentProfile = settings.value("net/pathCurrentProfile", "/api/v1/profile/current_profile").toString();
@ -1766,6 +1767,54 @@ void NetworkService::postMonitoringDump(const QString &text, const QByteArray &a
reply->deleteLater(); reply->deleteLater();
} }
void NetworkService::postMonitoringScreenshot(const QString &desktopId,
const QString &deviceId,
const QString &name,
const QByteArray &jpeg) {
if (jpeg.isEmpty()) return;
QNetworkRequest request(QUrl(m_monitoringBase + m_pathMonitoringScreenshot));
if (!m_monitoringToken.isEmpty()) {
request.setRawHeader("X-Monitoring-Token", m_monitoringToken.toUtf8());
}
QNetworkReply *reply = nullptr;
bool timedOut = true;
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
if (reply) { reply->deleteLater(); reply = nullptr; }
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
addFormField(multiPart, "desktop_id", desktopId);
addFormField(multiPart, "device_id", deviceId);
addFormField(multiPart, "name", name);
QHttpPart imagePart;
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
imagePart.setHeader(
QNetworkRequest::ContentDispositionHeader,
QStringLiteral("form-data; name=\"image\"; filename=\"screen.jpg\"")
);
imagePart.setBody(jpeg);
multiPart->append(imagePart);
reply = m_manager->post(request, multiPart);
multiPart->setParent(reply);
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
qWarning() << "[NetworkService] postMonitoringScreenshot timeout attempt"
<< attempt << "/" << kMaxRequestAttempts;
reply->abort();
}
if (timedOut) {
qWarning() << "[NetworkService] postMonitoringScreenshot timeout after"
<< kMaxRequestAttempts << "attempts";
} else if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[NetworkService] postMonitoringScreenshot failed:" << reply->errorString()
<< "\n response body:" << reply->readAll();
}
reply->deleteLater();
}
void NetworkService::start() { void NetworkService::start() {
m_running = true; m_running = true;

View File

@ -117,6 +117,14 @@ public slots:
const QByteArray &image, const QByteArray &image,
const QString &archiveFilename = QStringLiteral("task_dump.zip")); const QString &archiveFilename = QStringLiteral("task_dump.zip"));
// Отправляет JPEG-скриншот девайса на monitoringBase (/monitoring/screenshot,
// X-Monitoring-Token) для истории экрана. Q_INVOKABLE — вызывается через
// QueuedConnection из fire-and-forget потока DeviceScreener.
Q_INVOKABLE void postMonitoringScreenshot(const QString &desktopId,
const QString &deviceId,
const QString &name,
const QByteArray &jpeg);
void start(); void start();
void stop(); void stop();
void disableAllProfilesOnServer(); void disableAllProfilesOnServer();
@ -165,6 +173,7 @@ private:
QString m_pathTaskResult; QString m_pathTaskResult;
QString m_pathMonitoringLog; QString m_pathMonitoringLog;
QString m_pathMonitoringDump; QString m_pathMonitoringDump;
QString m_pathMonitoringScreenshot;
QString m_monitoringBase; QString m_monitoringBase;
QString m_monitoringToken; QString m_monitoringToken;
QString m_pathCurrentProfile; QString m_pathCurrentProfile;