Add parallel ADB scanning with QtConcurrent, optimize device sync logic, and replace blocking API calls with fire-and-forget dispatch for improved performance.
This commit is contained in:
parent
29ae105c64
commit
e08514ea2e
@ -58,6 +58,7 @@ find_package(Qt6 REQUIRED COMPONENTS
|
||||
Widgets
|
||||
Sql
|
||||
Xml
|
||||
Concurrent
|
||||
)
|
||||
|
||||
file(GLOB_RECURSE DB_SOURCES CONFIGURE_DEPENDS
|
||||
@ -156,6 +157,7 @@ if (WIN32)
|
||||
Qt6::Xml
|
||||
Qt6::Sql
|
||||
Qt6::Network
|
||||
Qt6::Concurrent
|
||||
Tesseract::libtesseract
|
||||
)
|
||||
|
||||
@ -276,6 +278,7 @@ elseif (APPLE)
|
||||
Qt6::Sql
|
||||
Qt6::Xml
|
||||
Qt6::Network
|
||||
Qt6::Concurrent
|
||||
tesseract
|
||||
leptonica
|
||||
)
|
||||
|
||||
@ -7,6 +7,8 @@
|
||||
#include <QBuffer>
|
||||
#include <qeventloop.h>
|
||||
#include <QThread>
|
||||
#include <QThreadPool>
|
||||
#include <QtConcurrent>
|
||||
#include <QTimer>
|
||||
#include <QImage>
|
||||
#include <QCryptographicHash>
|
||||
@ -152,35 +154,6 @@ QJsonValue DeviceScreener::apiPost(const QString &path, const QJsonObject &body)
|
||||
return result;
|
||||
}
|
||||
|
||||
void DeviceScreener::apiPatch(const QString &path, const QJsonObject &body) {
|
||||
if (!m_manager) m_manager = new QNetworkAccessManager(this);
|
||||
|
||||
QNetworkRequest request(QUrl(m_apiBase + path));
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Authorization",
|
||||
("Bearer " + SettingsDAO::get("access_token")).toUtf8());
|
||||
|
||||
QNetworkReply *reply = m_manager->sendCustomRequest(request, "PATCH", QJsonDocument(body).toJson());
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
timeout.setSingleShot(true);
|
||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
|
||||
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
timeout.start(30000);
|
||||
loop.exec();
|
||||
|
||||
if (!timeout.isActive()) {
|
||||
qCritical().noquote() << "DeviceScreener apiPatch timeout:"
|
||||
<< NetworkErrorUtils::describeClientTimeout(reply, path);
|
||||
reply->abort();
|
||||
} else if (reply->error() != QNetworkReply::NoError) {
|
||||
qCritical().noquote() << "DeviceScreener apiPatch error:"
|
||||
<< NetworkErrorUtils::describeNetError(reply, path);
|
||||
}
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
QString DeviceScreener::createDeviceOnApi(const DeviceInfo &device) {
|
||||
const QString desktopId = SettingsDAO::get("desktop_id");
|
||||
if (desktopId.isEmpty()) {
|
||||
@ -204,51 +177,6 @@ QString DeviceScreener::createDeviceOnApi(const DeviceInfo &device) {
|
||||
return {};
|
||||
}
|
||||
|
||||
void DeviceScreener::postScreenshotToApi(const QString &apiId, const QByteArray &image) {
|
||||
if (!m_manager) m_manager = new QNetworkAccessManager(this);
|
||||
|
||||
const QString path = "/api/v1/device/add_screenshot/" + apiId;
|
||||
QNetworkRequest request(QUrl(m_apiBase + path));
|
||||
request.setRawHeader("Authorization",
|
||||
("Bearer " + SettingsDAO::get("access_token")).toUtf8());
|
||||
|
||||
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
QHttpPart filePart;
|
||||
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
|
||||
filePart.setHeader(QNetworkRequest::ContentDispositionHeader,
|
||||
QStringLiteral("form-data; name=\"file\"; filename=\"screenshot.jpg\""));
|
||||
filePart.setBody(image);
|
||||
multiPart->append(filePart);
|
||||
|
||||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||||
multiPart->setParent(reply);
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
timeout.setSingleShot(true);
|
||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
|
||||
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
timeout.start(30000);
|
||||
loop.exec();
|
||||
|
||||
if (!timeout.isActive()) {
|
||||
qWarning().noquote() << "postScreenshotToApi timeout:" << apiId
|
||||
<< NetworkErrorUtils::describeClientTimeout(reply, path);
|
||||
reply->abort();
|
||||
} else if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning().noquote() << "postScreenshotToApi error:" << apiId
|
||||
<< NetworkErrorUtils::describeNetError(reply, path);
|
||||
} else {
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
||||
if (doc.isObject() && doc.object()["success"].toBool()) {
|
||||
// ok
|
||||
} else {
|
||||
qWarning() << "postScreenshotToApi failed for device" << apiId << ":" << doc.toJson(QJsonDocument::Compact);
|
||||
}
|
||||
}
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
void DeviceScreener::dispatchScreenshotToMonitoring(const QString &desktopId,
|
||||
const QString &deviceId,
|
||||
const QString &name,
|
||||
@ -271,8 +199,76 @@ void DeviceScreener::dispatchScreenshotToMonitoring(const QString &desktopId,
|
||||
thread->start();
|
||||
}
|
||||
|
||||
void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device) {
|
||||
// Находим первый активный bank_profile для этого устройства
|
||||
void DeviceScreener::dispatchDeviceSyncToApi(const QString &apiBase,
|
||||
const QString &accessToken,
|
||||
const QString &apiId,
|
||||
const QJsonObject &patchBody,
|
||||
const QByteArray &image) {
|
||||
if (apiId.isEmpty()) return;
|
||||
|
||||
// Отдельный поток + локальный QNetworkAccessManager, чтобы 30-секундный таймаут
|
||||
// PATCH/POST не блокировал цикл опроса (паттерн dispatchScreenshotToMonitoring).
|
||||
auto *thread = new QThread;
|
||||
QObject::connect(thread, &QThread::started, [=]() {
|
||||
QNetworkAccessManager manager; // живёт в этом потоке
|
||||
const QByteArray auth = ("Bearer " + accessToken).toUtf8();
|
||||
|
||||
// Локальный цикл ожидания одного reply с таймаутом 30с.
|
||||
auto waitReply = [](QNetworkReply *reply) {
|
||||
QEventLoop loop;
|
||||
QTimer timeout;
|
||||
timeout.setSingleShot(true);
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
QObject::connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
|
||||
QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
timeout.start(30000);
|
||||
loop.exec();
|
||||
if (!timeout.isActive()) reply->abort();
|
||||
};
|
||||
|
||||
// PATCH статуса устройства.
|
||||
{
|
||||
QNetworkRequest request(QUrl(apiBase + "/api/v1/device/" + apiId));
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Authorization", auth);
|
||||
QNetworkReply *reply = manager.sendCustomRequest(request, "PATCH", QJsonDocument(patchBody).toJson());
|
||||
waitReply(reply);
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning().noquote() << "dispatchDeviceSyncToApi PATCH error:" << apiId << reply->errorString();
|
||||
}
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
// POST скриншота (если есть).
|
||||
if (!image.isEmpty()) {
|
||||
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
QHttpPart filePart;
|
||||
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
|
||||
filePart.setHeader(QNetworkRequest::ContentDispositionHeader,
|
||||
QStringLiteral("form-data; name=\"file\"; filename=\"screenshot.jpg\""));
|
||||
filePart.setBody(image);
|
||||
multiPart->append(filePart);
|
||||
|
||||
QNetworkRequest request(QUrl(apiBase + "/api/v1/device/add_screenshot/" + apiId));
|
||||
request.setRawHeader("Authorization", auth);
|
||||
QNetworkReply *reply = manager.post(request, multiPart);
|
||||
multiPart->setParent(reply);
|
||||
waitReply(reply);
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning().noquote() << "dispatchDeviceSyncToApi screenshot error:" << apiId << reply->errorString();
|
||||
}
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
thread->quit();
|
||||
});
|
||||
QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater);
|
||||
thread->start();
|
||||
}
|
||||
|
||||
void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device, const QByteArray &image) {
|
||||
// Находим первый активный bank_profile для этого устройства (БД-запрос — должен
|
||||
// выполняться на главном потоке, поэтому строим тело здесь).
|
||||
QJsonValue bankProfileId = QJsonValue::Null;
|
||||
const QList<BankProfileInfo> apps = BankProfileDAO::getAllApplicationsByDeviceId(device.id);
|
||||
for (const BankProfileInfo &app : apps) {
|
||||
@ -287,7 +283,10 @@ void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device) {
|
||||
body["bank_profile_id"] = bankProfileId;
|
||||
body["battery"] = device.battery;
|
||||
body["update_time"] = QDateTime::currentSecsSinceEpoch();
|
||||
apiPatch("/api/v1/device/" + device.apiId, body);
|
||||
|
||||
// Сетевую часть — fire-and-forget, чтобы таймаут не тормозил цикл опроса.
|
||||
dispatchDeviceSyncToApi(m_apiBase, SettingsDAO::get("access_token"),
|
||||
device.apiId, body, image);
|
||||
}
|
||||
|
||||
QJsonValue DeviceScreener::apiGet(const QString &path) {
|
||||
@ -329,16 +328,50 @@ QJsonValue DeviceScreener::apiGet(const QString &path) {
|
||||
|
||||
|
||||
void DeviceScreener::start() {
|
||||
// Пул для параллельного adb-опроса (фаза A). Лимит kMaxParallelScans, чтобы
|
||||
// одновременные screencap'ы не насыщали USB-шину.
|
||||
QThreadPool scanPool;
|
||||
scanPool.setMaxThreadCount(kMaxParallelScans);
|
||||
|
||||
while (m_running) {
|
||||
QList<QPair<QString, QString> > devices = readAdbDevices();
|
||||
QSet<QString> liveSet; // устройства, видимые как "device" в текущем скане
|
||||
const qint64 nowSec = QDateTime::currentSecsSinceEpoch();
|
||||
|
||||
// --- Фаза A: чистый adb-I/O параллельно по всем "device".
|
||||
// Устройства не в статусе "device" (offline/unauthorized) игнорируем — они
|
||||
// и раньше не попадали в liveSet и не меняли состояние, лишь уходили в
|
||||
// офлайн по дебаунсу. Снимок кэша берём на главном потоке (read-only вход
|
||||
// в потоки пула — без гонок).
|
||||
QStringList deviceSerials;
|
||||
for (const auto &[adbSerial, status]: devices) {
|
||||
DeviceInfo device;
|
||||
if (status == "device") deviceSerials << adbSerial;
|
||||
}
|
||||
const QMap<QString, DeviceInfo> cacheSnapshot = m_staticInfoCache;
|
||||
// Какие устройства пора перечитать на список приложений (троттл).
|
||||
QSet<QString> appsDue;
|
||||
for (const QString &serial : deviceSerials) {
|
||||
if (nowSec - m_lastAppsCheck.value(serial, 0) >= kAppsCheckIntervalSecs) {
|
||||
appsDue.insert(serial);
|
||||
}
|
||||
}
|
||||
const QList<ScanResult> scans = QtConcurrent::blockingMapped<QList<ScanResult> >(
|
||||
&scanPool, deviceSerials,
|
||||
[&cacheSnapshot, &appsDue](const QString &serial) {
|
||||
return scanDeviceIo(serial, cacheSnapshot.value(serial), appsDue.contains(serial));
|
||||
});
|
||||
|
||||
if (status == "device") {
|
||||
device = readDeviceInfo(adbSerial);
|
||||
// --- Фаза B: всё, что трогает БД/члены класса — последовательно в этом потоке.
|
||||
for (const ScanResult &scan : scans) {
|
||||
// android_id не прочитался — пропускаем устройство в этом цикле.
|
||||
if (!scan.identified) continue;
|
||||
|
||||
DeviceInfo device = scan.info;
|
||||
device.status = "device";
|
||||
device.updateTime = QDateTime::currentDateTime();
|
||||
|
||||
// Обновляем кэш статики/battery (главный поток — безопасно).
|
||||
m_staticInfoCache[scan.adbSerial] = scan.info;
|
||||
|
||||
// Проверяем: есть ли уже устройство с таким id в БД (с apiId)
|
||||
const DeviceInfo existingById = DeviceDAO::getDeviceById(device.id);
|
||||
@ -365,27 +398,26 @@ void DeviceScreener::start() {
|
||||
|
||||
liveSet.insert(device.id);
|
||||
|
||||
device.image = takeScreenshot(adbSerial);
|
||||
device.image = scan.image;
|
||||
if (!device.image.isEmpty()) {
|
||||
if (!DeviceDAO::updateImage(device.id, device.image)) {
|
||||
qDebug() << "Cant update screenshot";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
device.id = adbSerial;
|
||||
}
|
||||
device.status = status;
|
||||
device.updateTime = QDateTime::currentDateTime();
|
||||
if (status == "device") {
|
||||
|
||||
if (!DeviceDAO::upsertDevice(device)) {
|
||||
qDebug() << "Cant update device";
|
||||
}
|
||||
|
||||
// Проверяем установленные приложения каждый цикл
|
||||
QSet<QString> apps = AdbUtils::getListApps(adbSerial);
|
||||
if (!BankProfileDAO::checkInstalledApps(device.id, apps)) {
|
||||
// Проверяем установленные приложения по троттлу (список прочитан в фазе A).
|
||||
// Пустой список = сбой adb — не двигаем метку, чтобы перечитать на след.
|
||||
// цикле и не пометить все приложения как отсутствующие.
|
||||
if (scan.appsRead && !scan.apps.isEmpty()) {
|
||||
if (!BankProfileDAO::checkInstalledApps(device.id, scan.apps)) {
|
||||
qWarning() << "Cant check installed apps";
|
||||
}
|
||||
m_lastAppsCheck[scan.adbSerial] = nowSec;
|
||||
}
|
||||
|
||||
// Обновляем версии установленных банков (не чаще раза в 6 часов на профиль)
|
||||
constexpr qint64 kAppVersionRefreshSecs = 6 * 60 * 60;
|
||||
@ -396,13 +428,12 @@ void DeviceScreener::start() {
|
||||
|| profile.appVersionCheckedAt.secsTo(nowUtc) >= kAppVersionRefreshSecs;
|
||||
if (!stale) continue;
|
||||
|
||||
const QString ver = AdbUtils::getAppVersion(adbSerial, profile.package);
|
||||
const QString ver = AdbUtils::getAppVersion(scan.adbSerial, profile.package);
|
||||
if (ver.isEmpty()) continue; // не затираем известную версию пустотой
|
||||
if (!BankProfileDAO::updateAppVersion(profile.id, ver)) {
|
||||
qWarning() << "Cant update app_version for" << profile.code << profile.package;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Синхронизация с API
|
||||
DeviceInfo saved = DeviceDAO::getDeviceById(device.id);
|
||||
@ -415,15 +446,13 @@ void DeviceScreener::start() {
|
||||
}
|
||||
qDebug() << "Device registered on API:" << device.id << "->" << apiId;
|
||||
}
|
||||
} else if (status == "device") {
|
||||
} else {
|
||||
const qint64 now = QDateTime::currentSecsSinceEpoch();
|
||||
// Основной API обновляем не чаще раза в 60 секунд.
|
||||
if (now - m_lastApiUpdate.value(device.id, 0) >= 60) {
|
||||
device.apiId = saved.apiId;
|
||||
updateDeviceOnApi(device);
|
||||
if (!device.image.isEmpty()) {
|
||||
postScreenshotToApi(saved.apiId, device.image);
|
||||
}
|
||||
// PATCH статуса + POST скриншота уходят fire-and-forget внутри.
|
||||
updateDeviceOnApi(device, device.image);
|
||||
m_lastApiUpdate[device.id] = now;
|
||||
}
|
||||
|
||||
@ -473,6 +502,15 @@ void DeviceScreener::start() {
|
||||
for (const QString &id : goneOffline) {
|
||||
m_onlineDevices.remove(id);
|
||||
m_offlineSince.remove(id);
|
||||
// Сбрасываем кэш статики для этого устройства: при переподключении по тому
|
||||
// же adbSerial может оказаться другой аппарат — перечитаем заново.
|
||||
for (auto it = m_staticInfoCache.begin(); it != m_staticInfoCache.end();) {
|
||||
if (it.value().id == id) {
|
||||
it = m_staticInfoCache.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
qDebug() << "[DeviceScreener] device went OFFLINE (debounced):" << id;
|
||||
// Сообщаем серверу что устройство офлайн (отдельно от bank_profile-синка)
|
||||
const DeviceInfo offlineDevice = DeviceDAO::getDeviceById(id);
|
||||
@ -584,45 +622,83 @@ QList<QPair<QString, QString> > DeviceScreener::readAdbDevices() {
|
||||
static const QRegularExpression screenRegex(R"(Physical size: (\d+)x(\d+))");
|
||||
static const QRegularExpression batteryRegex(R"(level:\s*(\d+))");
|
||||
|
||||
DeviceInfo DeviceScreener::readDeviceInfo(const QString &adbSerial) {
|
||||
DeviceInfo info;
|
||||
info.screenWidth = 0;
|
||||
info.screenHeight = 0;
|
||||
info.battery = 0;
|
||||
DeviceScreener::ScanResult DeviceScreener::scanDeviceIo(const QString &adbSerial,
|
||||
const DeviceInfo &cachedStatic,
|
||||
bool readApps) {
|
||||
ScanResult result;
|
||||
result.adbSerial = adbSerial;
|
||||
|
||||
auto runAdbCommand = [&](const QStringList &args) -> QString {
|
||||
// ok=false означает, что процесс не завершился за таймаут (зависшая команда):
|
||||
// результат считаем «нет данных», а не валидной пустой строкой, чтобы вызывающий
|
||||
// код не затирал известные значения.
|
||||
auto runAdbCommand = [&](const QStringList &args, bool *ok = nullptr) -> QString {
|
||||
QProcess process;
|
||||
process.setWorkingDirectory(QFileInfo(AdbUtils::adbPath()).absolutePath());
|
||||
process.start(AdbUtils::adbPath(), QStringList{"-s", adbSerial} + args);
|
||||
process.waitForFinished();
|
||||
if (!process.waitForFinished(kAdbCommandTimeoutMs)) {
|
||||
qWarning() << "[DeviceScreener] adb command timeout for" << adbSerial << args;
|
||||
process.kill();
|
||||
process.waitForFinished(1000);
|
||||
if (ok) *ok = false;
|
||||
return {};
|
||||
}
|
||||
if (ok) *ok = true;
|
||||
return QString::fromUtf8(process.readAllStandardOutput()).trimmed();
|
||||
};
|
||||
|
||||
const QString androidId = runAdbCommand({"shell", "settings", "get", "secure", "android_id"});
|
||||
const QString model = runAdbCommand({"shell", "getprop", "ro.product.model"});
|
||||
// Статика (id/name/android/размер экрана) не меняется за сессию. Если передан
|
||||
// непустой кэш — переиспользуем его и не дёргаем adb. Иначе читаем с нуля.
|
||||
DeviceInfo info = cachedStatic;
|
||||
if (info.id.isEmpty()) {
|
||||
bool idOk = false;
|
||||
bool modelOk = false;
|
||||
const QString androidId = runAdbCommand({"shell", "settings", "get", "secure", "android_id"}, &idOk);
|
||||
const QString model = runAdbCommand({"shell", "getprop", "ro.product.model"}, &modelOk);
|
||||
|
||||
info.id = androidId.isEmpty() ? adbSerial : androidId;
|
||||
// Идентичность не прочиталась (таймаут/пусто) — НЕ подменяем id на adbSerial.
|
||||
// Возвращаем identified=false; вызывающий код пропустит устройство в этом
|
||||
// цикле и повторит на следующем.
|
||||
if (!idOk || androidId.isEmpty()) {
|
||||
qWarning() << "[DeviceScreener] device not identified yet (android_id unavailable):" << adbSerial;
|
||||
info.adbSerial = adbSerial;
|
||||
info.name = model + "-" + info.id;
|
||||
info.updateTime = QDateTime::currentDateTime();
|
||||
result.info = info;
|
||||
result.identified = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
info.id = androidId;
|
||||
info.adbSerial = adbSerial;
|
||||
info.name = (modelOk && !model.isEmpty() ? model : QStringLiteral("device")) + "-" + info.id;
|
||||
info.android = runAdbCommand({"shell", "getprop", "ro.build.version.release"});
|
||||
|
||||
// Размер экрана
|
||||
const QString screenOutput = runAdbCommand({"shell", "wm", "size"});
|
||||
QRegularExpressionMatch match = screenRegex.match(screenOutput);
|
||||
if (match.hasMatch()) {
|
||||
if (QRegularExpressionMatch match = screenRegex.match(screenOutput); match.hasMatch()) {
|
||||
info.screenWidth = match.captured(1).toInt();
|
||||
info.screenHeight = match.captured(2).toInt();
|
||||
}
|
||||
}
|
||||
result.identified = true;
|
||||
|
||||
// Уровень батареи
|
||||
const QString batteryOutput = runAdbCommand({"shell", "dumpsys", "battery"});
|
||||
match = batteryRegex.match(batteryOutput);
|
||||
if (match.hasMatch()) {
|
||||
// Battery — динамическое, читаем каждый цикл. На таймаут/непрочитанное значение
|
||||
// оставляем прошлое (info уже содержит закэшированное), а не пишем 0.
|
||||
bool batteryOk = false;
|
||||
const QString batteryOutput = runAdbCommand({"shell", "dumpsys", "battery"}, &batteryOk);
|
||||
if (batteryOk) {
|
||||
if (QRegularExpressionMatch match = batteryRegex.match(batteryOutput); match.hasMatch()) {
|
||||
info.battery = match.captured(1).toInt();
|
||||
}
|
||||
|
||||
}
|
||||
info.updateTime = QDateTime::currentDateTime();
|
||||
return info;
|
||||
result.info = info;
|
||||
|
||||
// Тяжёлый чистый adb-I/O — скриншот всегда, список приложений по троттлу.
|
||||
result.image = takeScreenshot(adbSerial);
|
||||
if (readApps) {
|
||||
result.apps = AdbUtils::getListApps(adbSerial);
|
||||
result.appsRead = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -17,6 +17,16 @@ public:
|
||||
static constexpr qint64 kOfflineDebounceSecs = 30;
|
||||
// Как часто проверять/слать кадр истории экрана на сервер логов (с дедупом).
|
||||
static constexpr qint64 kMonitoringScreenIntervalSecs = 30;
|
||||
// Таймаут одной adb-команды в scanDeviceIo. На порядок выше нормального
|
||||
// времени (<1с), но ограничивает зависшую команду на флапающем устройстве,
|
||||
// чтобы один телефон не держал весь цикл опроса.
|
||||
static constexpr int kAdbCommandTimeoutMs = 10000;
|
||||
// Сколько устройств опрашивать по adb параллельно (фаза A). Ограничено, чтобы
|
||||
// не насыщать USB-шину одновременными screencap'ами.
|
||||
static constexpr int kMaxParallelScans = 6;
|
||||
// Как часто перечитывать список установленных приложений (getListApps). Список
|
||||
// меняется редко, поэтому не дёргаем adb каждый цикл.
|
||||
static constexpr qint64 kAppsCheckIntervalSecs = 60;
|
||||
|
||||
public slots:
|
||||
void start();
|
||||
@ -41,22 +51,43 @@ private:
|
||||
QMap<QString, qint64> m_offlineSince; // deviceId → когда пропал из ADB (для дебаунса)
|
||||
QMap<QString, qint64> m_lastMonitoringScreen; // deviceId → unix ts последней проверки кадра для истории экрана
|
||||
QMap<QString, QByteArray> m_lastSentScreenHash; // deviceId → sha256 последнего кадра, отправленного в историю экрана
|
||||
// Кэш неизменных свойств устройства (id/name/android/размер экрана) по adbSerial.
|
||||
// Эти поля не меняются за сессию, поэтому читаются по adb один раз, а не каждый
|
||||
// проход цикла. Каждый цикл обновляется только battery. Инвалидируется при уходе
|
||||
// устройства в OFFLINE (на случай переподключения другого аппарата по тому же serial).
|
||||
QMap<QString, DeviceInfo> m_staticInfoCache;
|
||||
QMap<QString, qint64> m_lastAppsCheck; // adbSerial → unix ts последнего getListApps
|
||||
|
||||
// Результат чистого adb-опроса одного устройства (фаза A, выполняется в пуле
|
||||
// потоков). Не содержит ничего, что требует БД/членов класса.
|
||||
struct ScanResult {
|
||||
QString adbSerial;
|
||||
DeviceInfo info; // идентичность + battery + размер экрана
|
||||
bool identified = false; // false → android_id не прочитался, пропустить цикл
|
||||
QByteArray image; // JPEG скриншот (может быть пустым)
|
||||
QSet<QString> apps; // установленные пакеты (валидно только при appsRead)
|
||||
bool appsRead = false; // true → getListApps вызывался в этом цикле
|
||||
};
|
||||
|
||||
static QList<QPair<QString, QString> > readAdbDevices();
|
||||
|
||||
static QByteArray takeScreenshot(const QString &deviceId);
|
||||
|
||||
static DeviceInfo readDeviceInfo(const QString &adbSerial);
|
||||
// Чистый adb-I/O одного устройства: потокобезопасно (не трогает БД и члены).
|
||||
// cachedStatic — снимок ранее прочитанной статики (пустой id → читаем с нуля).
|
||||
// readApps — перечитывать ли список установленных приложений в этом цикле.
|
||||
static ScanResult scanDeviceIo(const QString &adbSerial, const DeviceInfo &cachedStatic, bool readApps);
|
||||
|
||||
void postDevicesData(const QList<DeviceInfo> &devices);
|
||||
|
||||
QJsonValue apiGet(const QString &path);
|
||||
QJsonValue apiPost(const QString &path, const QJsonObject &body);
|
||||
void apiPatch(const QString &path, const QJsonObject &body);
|
||||
|
||||
QString createDeviceOnApi(const DeviceInfo &device);
|
||||
void updateDeviceOnApi(const DeviceInfo &device);
|
||||
void postScreenshotToApi(const QString &apiId, const QByteArray &image);
|
||||
// Строит тело PATCH (с поиском bank_profile_id по БД — вызывать на главном
|
||||
// потоке) и отправляет статус + опциональный скриншот fire-and-forget через
|
||||
// dispatchDeviceSyncToApi, чтобы сетевой таймаут не тормозил цикл опроса.
|
||||
void updateDeviceOnApi(const DeviceInfo &device, const QByteArray &image = {});
|
||||
|
||||
// Fire-and-forget отправка кадра истории экрана на сервер логов (monitoringBase).
|
||||
// Создаёт временный NetworkService в отдельном потоке (паттерн LogArchiveSender).
|
||||
@ -64,4 +95,14 @@ private:
|
||||
const QString &deviceId,
|
||||
const QString &name,
|
||||
const QByteArray &jpeg);
|
||||
|
||||
// Fire-and-forget синхронизация устройства с основным API: PATCH статуса и
|
||||
// (если image непустой) POST скриншота. Поднимает свой поток + QNetworkAccessManager,
|
||||
// чтобы 30-секундный таймаут не блокировал цикл опроса. Всё необходимое
|
||||
// (токен, тело запроса) передаётся по значению — БД здесь не трогается.
|
||||
static void dispatchDeviceSyncToApi(const QString &apiBase,
|
||||
const QString &accessToken,
|
||||
const QString &apiId,
|
||||
const QJsonObject &patchBody,
|
||||
const QByteArray &image);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user