Optimize database logging, improve thread performance, and implement periodic maintenance to avoid GUI blocking. Replace infinite waits in AdbUtils with safe timeouts to prevent thread starvation.
This commit is contained in:
parent
cfc562eae6
commit
e760f2bb5f
@ -70,6 +70,23 @@ bool AdbUtils::startProcess(const QString &program, const QStringList &args,
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ждёт завершения процесса с таймаутом. Раньше здесь стоял бесконечный
|
||||
// waitForFinished() — если adb-клиент зависал на неотвечающем устройстве/USB,
|
||||
// рабочий поток блокировался навсегда (накопление зависших потоков → фриз
|
||||
// приложения через несколько часов). При таймауте убиваем зависший процесс и
|
||||
// реапим его, чтобы и поток освободился, и деструктор QProcess не блокировался.
|
||||
// Возвращает true, если процесс завершился сам в отведённое время.
|
||||
static bool waitOrKill(QProcess &process, int timeoutMs, const char *tag) {
|
||||
if (process.waitForFinished(timeoutMs)) {
|
||||
return true;
|
||||
}
|
||||
qWarning() << "[AdbUtils]" << tag << "timed out after" << timeoutMs
|
||||
<< "ms — killing wedged adb process";
|
||||
process.kill();
|
||||
process.waitForFinished(2000);
|
||||
return false;
|
||||
}
|
||||
|
||||
// adb shell dumpsys package ru.rshb.dbo | grep permission
|
||||
// TODO доделать обработку пермишинов
|
||||
// adb shell pm revoke ru.rshb.dbo android.permission.WRITE_CONTACTS; adb shell pm revoke ru.rshb.dbo android.permission.READ_CONTACTS; adb shell pm revoke ru.rshb.dbo android.permission.ACCESS_COARSE_LOCATION; adb shell pm revoke ru.rshb.dbo android.permission.ACCESS_FINE_LOCATION
|
||||
@ -147,7 +164,7 @@ QString AdbUtils::tryToGetRunningAppPid(const QString &deviceId, const QString &
|
||||
qWarning() << "[AdbUtils] Failed to start adb pidof:" << process.errorString();
|
||||
return {};
|
||||
}
|
||||
process.waitForFinished();
|
||||
waitOrKill(process, 10000, "tryToGetRunningAppPid");
|
||||
QString pid = process.readAllStandardOutput();
|
||||
return pid.replace("\n", "");
|
||||
}
|
||||
@ -230,7 +247,7 @@ bool AdbUtils::tryToStartApplication(const QString &deviceId, const QString &pac
|
||||
qWarning() << "[AdbUtils] Failed to start adb monkey:" << process.errorString();
|
||||
return false;
|
||||
}
|
||||
process.waitForFinished();
|
||||
waitOrKill(process, 15000, "tryToStartApplication(monkey)");
|
||||
const QString stdOut = QString::fromUtf8(process.readAllStandardOutput());
|
||||
const QString stdErr = QString::fromUtf8(process.readAllStandardError());
|
||||
const int exitCode = process.exitCode();
|
||||
@ -294,7 +311,7 @@ bool AdbUtils::tryToKillApplication(const QString &deviceId, const QString &pack
|
||||
qWarning() << "[AdbUtils] Failed to start adb force-stop:" << process.errorString();
|
||||
return false;
|
||||
}
|
||||
process.waitForFinished();
|
||||
waitOrKill(process, 15000, "tryToKillApplication");
|
||||
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
|
||||
}
|
||||
|
||||
@ -480,7 +497,7 @@ bool AdbUtils::makeTap(const QString &deviceId, const int x, const int y) {
|
||||
qWarning() << "[AdbUtils] Failed to start adb tap:" << process.errorString();
|
||||
return false;
|
||||
}
|
||||
process.waitForFinished();
|
||||
waitOrKill(process, 10000, "makeTap");
|
||||
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
|
||||
}
|
||||
|
||||
@ -595,7 +612,7 @@ bool AdbUtils::inputAdbIMEText(const QString &deviceId, const QString &text) {
|
||||
qWarning() << "[AdbUtils] Failed to start adb broadcast:" << process.errorString();
|
||||
return false;
|
||||
}
|
||||
process.waitForFinished();
|
||||
waitOrKill(process, 10000, "inputAdbIMEText");
|
||||
qDebug() << "[AdbUtils] inputAdbIMEText (b64) result:" << process.exitCode()
|
||||
<< "text:" << text
|
||||
<< "output:" << process.readAllStandardOutput().trimmed()
|
||||
@ -612,7 +629,7 @@ bool AdbUtils::disableAdbIMEKeyboard(const QString &deviceId) {
|
||||
qWarning() << "[AdbUtils] Failed to start adb ime disable:" << process.errorString();
|
||||
return false;
|
||||
}
|
||||
process.waitForFinished();
|
||||
waitOrKill(process, 10000, "disableAdbIMEKeyboard");
|
||||
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
|
||||
}
|
||||
|
||||
@ -637,7 +654,7 @@ bool AdbUtils::makeSwipe(const QString &deviceId, const int x1, const int y1, co
|
||||
qWarning() << "[AdbUtils] Failed to start adb swipe:" << process.errorString();
|
||||
return false;
|
||||
}
|
||||
process.waitForFinished();
|
||||
waitOrKill(process, 10000, "makeSwipe");
|
||||
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
|
||||
}
|
||||
|
||||
@ -653,7 +670,7 @@ bool AdbUtils::inputText(const QString &deviceId, const QString &text) {
|
||||
qWarning() << "[AdbUtils] Failed to start adb input text:" << process.errorString();
|
||||
return false;
|
||||
}
|
||||
process.waitForFinished();
|
||||
waitOrKill(process, 10000, "inputText");
|
||||
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
|
||||
}
|
||||
|
||||
@ -706,7 +723,7 @@ bool AdbUtils::goBack(const QString &deviceId) {
|
||||
qWarning() << "[AdbUtils] Failed to start adb keyevent:" << process.errorString();
|
||||
return false;
|
||||
}
|
||||
process.waitForFinished();
|
||||
waitOrKill(process, 10000, "goBack");
|
||||
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
|
||||
}
|
||||
|
||||
@ -722,7 +739,7 @@ QSet<QString> AdbUtils::getListApps(const QString &deviceId) {
|
||||
qWarning() << "[AdbUtils] Failed to start adb pm list:" << process.errorString();
|
||||
return {};
|
||||
}
|
||||
process.waitForFinished();
|
||||
waitOrKill(process, 15000, "getListApps");
|
||||
|
||||
const QString output = process.readAllStandardOutput();
|
||||
QSet<QString> apps;
|
||||
|
||||
@ -80,6 +80,17 @@ QSqlDatabase DatabaseManager::database() {
|
||||
QSqlQuery pragmaQuery(db);
|
||||
pragmaQuery.exec("PRAGMA foreign_keys = ON");
|
||||
|
||||
// WAL: читатели (например, посекундный refresh GUI через getAllDevices)
|
||||
// больше не блокируются писателями (screencap-BLOB каждые 4с/устройство,
|
||||
// события, sync). В прежнем режиме (rollback journal = delete) писатель
|
||||
// брал эксклюзивную блокировку всего файла и стопорил читателей — под
|
||||
// нагрузкой 5-10 устройств это давало нарастающие подвисания GUI.
|
||||
// journal_mode персистентен в файле; busy_timeout/synchronous — пер-конекшн,
|
||||
// поэтому ставим их на каждом новом соединении.
|
||||
pragmaQuery.exec("PRAGMA journal_mode = WAL");
|
||||
pragmaQuery.exec("PRAGMA busy_timeout = 5000");
|
||||
pragmaQuery.exec("PRAGMA synchronous = NORMAL");
|
||||
|
||||
connectionPool[name] = db;
|
||||
|
||||
// Автоматическое удаление соединения при завершении потока
|
||||
@ -287,6 +298,43 @@ void DatabaseManager::cleanupOldData(int days) {
|
||||
qDebug() << "[cleanup] VACUUM done";
|
||||
}
|
||||
|
||||
void DatabaseManager::performMaintenance() {
|
||||
// Оставляем последние N строк general_logs — защита от роста при всплесках
|
||||
// логирования. Кап по id (а не по timestamp) формат-независим и надёжен.
|
||||
constexpr int kMaxGeneralLogRows = 5000;
|
||||
// Старые завершённые события/транзакции держим максимум столько дней.
|
||||
constexpr int kKeepDays = 2;
|
||||
|
||||
QSqlDatabase db = database();
|
||||
QSqlQuery q(db);
|
||||
|
||||
const QString cutoff = QDateTime::currentDateTimeUtc().addDays(-kKeepDays).toString(Qt::ISODate);
|
||||
|
||||
db.transaction();
|
||||
|
||||
q.prepare("DELETE FROM general_logs WHERE id < "
|
||||
"(SELECT MAX(id) FROM general_logs) - :keep");
|
||||
q.bindValue(":keep", kMaxGeneralLogRows);
|
||||
if (q.exec() && q.numRowsAffected() > 0)
|
||||
qDebug() << "[maintenance] general_logs: deleted" << q.numRowsAffected() << "rows (cap" << kMaxGeneralLogRows << ")";
|
||||
|
||||
q.prepare("DELETE FROM events WHERE timestamp < :cutoff AND status IN ('complete', 'error')");
|
||||
q.bindValue(":cutoff", cutoff);
|
||||
if (q.exec() && q.numRowsAffected() > 0)
|
||||
qDebug() << "[maintenance] events: deleted" << q.numRowsAffected() << "rows older than" << kKeepDays << "days";
|
||||
|
||||
q.prepare("DELETE FROM transactions WHERE timestamp < :cutoff");
|
||||
q.bindValue(":cutoff", cutoff);
|
||||
if (q.exec() && q.numRowsAffected() > 0)
|
||||
qDebug() << "[maintenance] transactions: deleted" << q.numRowsAffected() << "rows older than" << kKeepDays << "days";
|
||||
|
||||
db.commit();
|
||||
|
||||
// Подрезаем WAL, чтобы он не рос неограниченно за сессию (TRUNCATE сбрасывает
|
||||
// файл к нулю после переноса кадров в основную БД). Вне транзакции.
|
||||
q.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
||||
}
|
||||
|
||||
void DatabaseManager::closeConnection() {
|
||||
QMutexLocker<QRecursiveMutex> locker(&mutex);
|
||||
if (const QString name = connectionName(); connectionPool.contains(name)) {
|
||||
|
||||
@ -14,6 +14,11 @@ public:
|
||||
QSqlDatabase database();
|
||||
void initSchema();
|
||||
void cleanupOldData(int days = 5);
|
||||
// Лёгкое периодическое обслуживание в течение сессии (вызывать с фонового
|
||||
// потока по таймеру): кап general_logs по количеству, удаление старых
|
||||
// завершённых событий/транзакций и подрезка WAL-файла. Без VACUUM —
|
||||
// он берёт эксклюзивную блокировку и слишком тяжёл для частого вызова.
|
||||
void performMaintenance();
|
||||
void closeConnection();
|
||||
|
||||
// Путь к файлу БД из config.ini (с раскрытием ~). Единая точка вычисления.
|
||||
|
||||
76
main.cpp
76
main.cpp
@ -1,6 +1,7 @@
|
||||
#include <QApplication>
|
||||
#include <QSslSocket>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
|
||||
#include "dao/MaterialDAO.h"
|
||||
#include "views/MainWindow.h"
|
||||
@ -253,7 +254,8 @@ void messageHandler(
|
||||
break;
|
||||
}
|
||||
|
||||
const QString source = context.function ? QString("%1:%2").arg(context.function).arg(context.line) : QString();
|
||||
// source использовался только для записи в general_logs (отключено ниже).
|
||||
// const QString source = context.function ? QString("%1:%2").arg(context.function).arg(context.line) : QString();
|
||||
|
||||
const char *func = context.function ? context.function : "unknown";
|
||||
const QString line = QString("%1 [%2] [%3:%4] %5\n")
|
||||
@ -267,28 +269,35 @@ void messageHandler(
|
||||
}
|
||||
}
|
||||
|
||||
// Дублируем в БД (с защитой от рекурсии), пропуская внутренние Qt-сообщения
|
||||
static const QStringList qtNoisePatterns = {
|
||||
"killTimer",
|
||||
"setGeometry",
|
||||
"QSqlDatabase requires",
|
||||
"Cannot be used without QCoreApplication",
|
||||
"QWindowsWindow",
|
||||
"Timers cannot be stopped from another thread",
|
||||
};
|
||||
bool isQtNoise = false;
|
||||
for (const QString &pattern : qtNoisePatterns) {
|
||||
if (msg.contains(pattern)) { isQtNoise = true; break; }
|
||||
}
|
||||
|
||||
if (dbReady.load() && !isQtNoise) {
|
||||
static thread_local bool writing = false;
|
||||
if (!writing) {
|
||||
writing = true;
|
||||
GeneralLogDAO::insert(level, source, msg);
|
||||
writing = false;
|
||||
}
|
||||
}
|
||||
// ОТКЛЮЧЕНО: каждое сообщение лога (DEBUG/INFO/WARNING со всех потоков) писалось
|
||||
// синхронно в general_logs. Таблица чистится только при старте (cleanupOldData),
|
||||
// поэтому за сессию росла без ограничений; все вставки/чтения сериализуются на
|
||||
// одном мьютексе DatabaseManager (общий с EventHandler/DeviceScreener/NetworkService),
|
||||
// и GUI каждую секунду читает getLogs(). Рост таблицы → растёт время удержания
|
||||
// мьютекса → GUI-поток голодает → зависание через 5–6 часов работы.
|
||||
// Логи по-прежнему пишутся в application.log и stderr; явные ошибки идут в БД
|
||||
// через AppLogger::log (WARNING, низкий объём) для панели ошибок и мониторинга.
|
||||
// static const QStringList qtNoisePatterns = {
|
||||
// "killTimer",
|
||||
// "setGeometry",
|
||||
// "QSqlDatabase requires",
|
||||
// "Cannot be used without QCoreApplication",
|
||||
// "QWindowsWindow",
|
||||
// "Timers cannot be stopped from another thread",
|
||||
// };
|
||||
// bool isQtNoise = false;
|
||||
// for (const QString &pattern : qtNoisePatterns) {
|
||||
// if (msg.contains(pattern)) { isQtNoise = true; break; }
|
||||
// }
|
||||
//
|
||||
// if (dbReady.load() && !isQtNoise) {
|
||||
// static thread_local bool writing = false;
|
||||
// if (!writing) {
|
||||
// writing = true;
|
||||
// GeneralLogDAO::insert(level, source, msg);
|
||||
// writing = false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// По желанию — дублировать в консоль
|
||||
const QByteArray localMsg = line.toLocal8Bit();
|
||||
@ -385,6 +394,27 @@ int main(int argc, char *argv[]) {
|
||||
DatabaseManager::instance().cleanupOldData(30);
|
||||
dbReady.store(true);
|
||||
|
||||
// Периодическое обслуживание БД на фоновом потоке (раз в 10 минут): кап
|
||||
// general_logs по количеству, удаление старых событий/транзакций и подрезка
|
||||
// WAL-файла. Держит размер БД/WAL ограниченным в течение долгой сессии и не
|
||||
// трогает GUI-поток. cleanupOldData() выше — разовая чистка по возрасту при старте.
|
||||
{
|
||||
auto *maintenanceThread = new QThread;
|
||||
auto *maintenanceTimer = new QTimer;
|
||||
maintenanceTimer->setInterval(10 * 60 * 1000);
|
||||
maintenanceTimer->moveToThread(maintenanceThread);
|
||||
QObject::connect(maintenanceThread, &QThread::started,
|
||||
maintenanceTimer, qOverload<>(&QTimer::start));
|
||||
QObject::connect(maintenanceTimer, &QTimer::timeout, maintenanceTimer, []() {
|
||||
DatabaseManager::instance().performMaintenance();
|
||||
});
|
||||
QObject::connect(&app, &QCoreApplication::aboutToQuit, maintenanceThread, [maintenanceThread]() {
|
||||
maintenanceThread->quit();
|
||||
maintenanceThread->wait(3000);
|
||||
});
|
||||
maintenanceThread->start();
|
||||
}
|
||||
|
||||
// 1. Если есть api_key — логинимся и проверяем/создаём desktop
|
||||
// - ошибка сети или сервера [code -1]
|
||||
// - неверный api_key [code 0] — показываем окно ввода
|
||||
|
||||
@ -298,17 +298,21 @@ void MainWindow::createDevicePage() {
|
||||
m_noDevicesLabel->setVisible(devices.isEmpty());
|
||||
m_scrollArea->setVisible(!devices.isEmpty());
|
||||
|
||||
// Панель логов внизу
|
||||
m_logList = new QListWidget(container);
|
||||
m_logList->setMaximumHeight(120);
|
||||
m_logList->setStyleSheet(
|
||||
"QListWidget { background-color: #1e1e1e; color: #cccccc; font-family: monospace; font-size: 11px; border-top: 1px solid #444; }"
|
||||
"QListWidget::item { padding: 1px 4px; }"
|
||||
);
|
||||
m_logList->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
m_logList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
containerLayout->addWidget(m_logList);
|
||||
updateLogPanel();
|
||||
// ОТКЛЮЧЕНО: панель вывода ошибок на главном экране. Она заполнялась через
|
||||
// updateLogPanel() каждую секунду (timer 1000ms) запросом getLogs() к general_logs
|
||||
// + множеством вложенных DAO-запросов на GUI-потоке. m_logList остаётся nullptr,
|
||||
// поэтому updateLogPanel() сразу выходит (см. `if (!m_logList) return;`) — панель
|
||||
// не показывается и посекундный запрос к БД больше не выполняется.
|
||||
// m_logList = new QListWidget(container);
|
||||
// m_logList->setMaximumHeight(120);
|
||||
// m_logList->setStyleSheet(
|
||||
// "QListWidget { background-color: #1e1e1e; color: #cccccc; font-family: monospace; font-size: 11px; border-top: 1px solid #444; }"
|
||||
// "QListWidget::item { padding: 1px 4px; }"
|
||||
// );
|
||||
// m_logList->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
// m_logList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
// containerLayout->addWidget(m_logList);
|
||||
// updateLogPanel();
|
||||
|
||||
m_devicesWindow = container;
|
||||
stackedWidget->addWidget(m_devicesWindow);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user