1
0
forked from BRT/arc
arc/main.cpp
2026-06-17 19:30:24 +07:00

552 lines
26 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <QApplication>
#include <QSslSocket>
#include <QThread>
#include <QTimer>
#include "dao/MaterialDAO.h"
#include "views/MainWindow.h"
#include "database/DatabaseManager.h"
#include "database/dao/CommonDataDAO.h"
#include "database/dao/DeviceDAO.h"
#include "services/DeviceScreener.h"
#include "services/LogArchiveSender.h"
#include <atomic>
#include <iostream>
#ifdef Q_OS_WIN
#include <windows.h>
#include <dbghelp.h>
#pragma comment(lib, "dbghelp.lib")
static LONG WINAPI crashHandler(EXCEPTION_POINTERS *ep) {
fprintf(stderr, "\n=== CRASH ===\n");
fprintf(stderr, "Exception code: 0x%08lX\n", ep->ExceptionRecord->ExceptionCode);
fprintf(stderr, "Exception addr: 0x%p\n", ep->ExceptionRecord->ExceptionAddress);
fprintf(stderr, "Thread ID: %lu\n", GetCurrentThreadId());
HANDLE process = GetCurrentProcess();
SymInitialize(process, NULL, TRUE);
void *stack[64];
USHORT frames = CaptureStackBackTrace(0, 64, stack, NULL);
SYMBOL_INFO *symbol = (SYMBOL_INFO *)calloc(sizeof(SYMBOL_INFO) + 256, 1);
symbol->MaxNameLen = 255;
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
for (USHORT i = 0; i < frames; i++) {
SymFromAddr(process, (DWORD64)stack[i], 0, symbol);
fprintf(stderr, " [%2u] %s (0x%0llX)\n", i, symbol->Name, symbol->Address);
}
free(symbol);
SymCleanup(process);
fflush(stderr);
return EXCEPTION_EXECUTE_HANDLER;
}
#endif
#include <QPalette>
#include <QStyleFactory>
#include <thread>
#include <chrono>
#include <QFile>
#include <QIcon>
#include <QSettings>
#include "AppLogger.h"
#include "dump/TaskDumpRecorder.h"
#include "dao/GeneralLogDAO.h"
#include "dao/SettingsDAO.h"
#include "EventHandler.h"
#include "wifi/WifiAdbConnector.h"
#include "net/NetworkService.h"
#include "update/ApplyUpdate.h"
#include "black/PayByCardScript.h"
#include "ozon/PayByPhoneScript.h"
#include "widget/loader/ErrorWindow.h"
#include "widget/loader/LoaderDialog.h"
#include "widget/loader/LoaderWindow.h"
#include "widget/loader/RegistrationWindow.h"
#include <QMessageBox>
#include <QPushButton>
#include <QFileDialog>
void startDeviceScreener(const QCoreApplication &app, NetworkService *networkService = nullptr) {
auto *thread = new QThread();
auto *deviceScreenerWorker = new DeviceScreener;
// Перемещаем worker в новый поток thread.
// Это значит, что все слоты worker, вызываемые через connect(), теперь будут исполняться в этом фоне-потоке.
deviceScreenerWorker->moveToThread(thread);
// Когда поток запустится, автоматически вызовется worker->start() внутри фонового потока.
QObject::connect(thread, &QThread::started, deviceScreenerWorker, &DeviceScreener::start);
// Связь с NetworkService: первый проход → старт периодической синхронизации;
// переходы online/offline → реакция в NetworkService (правило #1, #6).
if (networkService) {
QObject::connect(deviceScreenerWorker, &DeviceScreener::firstPassDone,
networkService, &NetworkService::startPeriodicSync,
Qt::QueuedConnection);
QObject::connect(deviceScreenerWorker, &DeviceScreener::deviceWentOnline,
networkService, &NetworkService::onDeviceWentOnline,
Qt::QueuedConnection);
QObject::connect(deviceScreenerWorker, &DeviceScreener::deviceWentOffline,
networkService, &NetworkService::onDeviceWentOffline,
Qt::QueuedConnection);
}
// 🔁 Эти 3 строки делают автоматическую зачистку:
// Когда worker закончит работу (emit finished()), мы:
// - останавливаем поток (thread->quit())
// - удаляем worker (deleteLater)
// - удаляем thread (deleteLater)
// Это защищает от утечек памяти и оставшихся потоков после завершения.
QObject::connect(deviceScreenerWorker, &DeviceScreener::finished, thread, &QThread::quit);
QObject::connect(deviceScreenerWorker, &DeviceScreener::finished, deviceScreenerWorker, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
// Когда пользователь закрывает приложение (Ctrl+C, exit() и т.п.), мы вызываем worker->stop() — ты сам пишешь, что в этом методе.
//Обычно он просто ставит флаг m_running = false, чтобы остановить бесконечный цикл в start()
QObject::connect(&app, &QCoreApplication::aboutToQuit, [=]() {
deviceScreenerWorker->stop();
});
thread->start();
}
void startEventHandler(const QCoreApplication &app) {
auto *thread = new QThread;
auto *eventHandler = new EventHandler;
eventHandler->moveToThread(thread);
QObject::connect(thread, &QThread::started, eventHandler, &EventHandler::start);
QObject::connect(eventHandler, &EventHandler::finished, thread, &QThread::quit);
QObject::connect(eventHandler, &EventHandler::finished, eventHandler, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
QObject::connect(&app, &QCoreApplication::aboutToQuit, [=]() {
QMetaObject::invokeMethod(eventHandler, "stop", Qt::BlockingQueuedConnection);
});
thread->start();
}
void startWifiAdbConnector(const QCoreApplication &app) {
auto *thread = new QThread;
auto *connector = new WifiAdbConnector;
connector->moveToThread(thread);
QObject::connect(thread, &QThread::started, connector, &WifiAdbConnector::start);
QObject::connect(connector, &WifiAdbConnector::finished, thread, &QThread::quit);
QObject::connect(connector, &WifiAdbConnector::finished, connector, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
QObject::connect(&app, &QCoreApplication::aboutToQuit, [=]() {
connector->stop();
});
thread->start();
}
void startPayByPhoneTest() {
// Берём первый девайс с ozon аккаунтом
QList<MaterialInfo> accounts = MaterialDAO::getAccountsByAppCode("ozon");
if (accounts.isEmpty()) {
qWarning() << "[PayByPhoneTest] No ozon accounts found";
return;
}
MaterialInfo account = accounts.first();
qDebug() << "[PayByPhoneTest] Using account:" << account.id << account.deviceId << account.lastNumbers;
auto *thread = new QThread(nullptr);
auto *worker = new Ozon::PayByPhoneScript(
account,
"+79855532896",
"Альфа-Банк",
150,
nullptr
);
worker->moveToThread(thread);
QObject::connect(thread, &QThread::started, worker, &Ozon::PayByPhoneScript::start);
QObject::connect(worker, &Ozon::PayByPhoneScript::finished, thread, &QThread::quit);
QObject::connect(worker, &Ozon::PayByPhoneScript::finished, worker, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater);
QObject::connect(worker, &Ozon::PayByPhoneScript::finishedWithResult, [](const QString &result) {
qDebug() << "[PayByPhoneTest] Result:" << result;
});
thread->start();
}
void startBlackPayByCardTest() {
QList<MaterialInfo> accounts = MaterialDAO::getAccountsByAppCode("black");
if (accounts.isEmpty()) {
qWarning() << "[BlackPayByCardTest] No black accounts found";
return;
}
MaterialInfo account = accounts.first();
qDebug() << "[BlackPayByCardTest] Using account:" << account.id << account.deviceId << account.lastNumbers;
auto *thread = new QThread(nullptr);
auto *worker = new Black::PayByCardScript(
account,
"0070359630004039066690",
500,
nullptr
);
worker->moveToThread(thread);
QObject::connect(thread, &QThread::started, worker, &Black::PayByCardScript::start);
QObject::connect(worker, &Black::PayByCardScript::finished, thread, &QThread::quit);
QObject::connect(worker, &Black::PayByCardScript::finished, worker, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater);
QObject::connect(worker, &Black::PayByCardScript::finishedWithResult, [](const QString &result) {
qDebug() << "[BlackPayByCardTest] Result:" << result;
});
thread->start();
}
NetworkService* startNetworkService(const QCoreApplication &app) {
auto *paymentThread = new QThread;
auto *networkService = new NetworkService;
AppLogger::setNetworkService(networkService);
TaskDumpRecorder::setNetworkService(networkService);
networkService->moveToThread(paymentThread);
QObject::connect(paymentThread, &QThread::started, networkService, &NetworkService::start);
QObject::connect(networkService, &NetworkService::finished, paymentThread, &QThread::quit);
QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater);
QObject::connect(paymentThread, &QThread::finished, paymentThread, &QThread::deleteLater);
QObject::connect(&app, &QCoreApplication::aboutToQuit, [=]() {
auto *loader = new LoaderDialog();
loader->setWindowTitle("Завершение...");
loader->show();
QCoreApplication::processEvents();
QEventLoop loop;
QObject::connect(networkService, &NetworkService::finished, &loop, &QEventLoop::quit);
// Таймаут на случай если сервер недоступен
QTimer::singleShot(10000, &loop, &QEventLoop::quit);
networkService->stop();
loop.exec();
loader->close();
delete loader;
});
// Сигнал эмитируется из фонового потока → Qt::QueuedConnection доставит его в главный поток
QObject::connect(networkService, &NetworkService::tokenRefreshFailed, qApp, []() {
QMessageBox *box = new QMessageBox(
QMessageBox::Critical,
"Ошибка авторизации",
"Не удалось обновить токен авторизации.\n\n"
"Проверьте подключение к интернету.\n"
"Приложение продолжит попытки автоматически.",
QMessageBox::Ok
);
box->setAttribute(Qt::WA_DeleteOnClose);
box->show();
}, Qt::QueuedConnection);
paymentThread->start();
return networkService;
}
static QFile logFile;
static QMutex logMutex;
static std::atomic<bool> dbReady{false};
void messageHandler(
const QtMsgType type,
const QMessageLogContext &context,
const QString &msg
) {
Q_UNUSED(context)
const QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz");
QString level;
switch (type) {
case QtDebugMsg: level = "DEBUG";
break;
case QtInfoMsg: level = "INFO";
break;
case QtWarningMsg: level = "WARNING";
break;
case QtCriticalMsg: level = "CRITICAL";
break;
case QtFatalMsg: level = "FATAL";
break;
}
// 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")
.arg(timestamp, level, func, QString::number(context.line), msg);
// Записываем в файл (потокобезопасно)
{
QMutexLocker locker(&logMutex);
if (logFile.isOpen()) {
QTextStream(&logFile) << line;
logFile.flush();
}
}
// ОТКЛЮЧЕНО: каждое сообщение лога (DEBUG/INFO/WARNING со всех потоков) писалось
// синхронно в general_logs. Таблица чистится только при старте (cleanupOldData),
// поэтому за сессию росла без ограничений; все вставки/чтения сериализуются на
// одном мьютексе DatabaseManager (общий с EventHandler/DeviceScreener/NetworkService),
// и GUI каждую секунду читает getLogs(). Рост таблицы → растёт время удержания
// мьютекса → GUI-поток голодает → зависание через 56 часов работы.
// Логи по-прежнему пишутся в 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();
fprintf(stderr, "%s", localMsg.constData());
if (type == QtFatalMsg) {
abort();
}
}
int main(int argc, char *argv[]) {
#ifdef Q_OS_WIN
SetUnhandledExceptionFilter(crashHandler);
#endif
// Режим установки обновления: запущены как временная копия ARC.exe с
// --apply-update. Перехватываем ДО QApplication/БД/логгера — это headless
// файловый воркер, а не основное приложение.
if (isApplyUpdateMode(argc, argv)) {
return runApplyUpdate(argc, argv);
}
QApplication app(argc, argv);
app.setApplicationName("ARC");
app.setWindowIcon(QIcon("arc.png"));
// Используем Schannel (нативный Windows TLS) — не требует OpenSSL DLL
if (QSslSocket::availableBackends().contains("schannel")) {
QSslSocket::setActiveBackend("schannel");
qDebug() << "[main] TLS backend set to schannel";
} else {
qDebug() << "[main] Available TLS backends:" << QSslSocket::availableBackends();
}
// Выбрать стиль Fusion + принудительная светлая тема
QApplication::setStyle(QStyleFactory::create("Fusion"));
QPalette lightPalette;
lightPalette.setColor(QPalette::Window, QColor(240, 240, 240));
lightPalette.setColor(QPalette::WindowText, Qt::black);
lightPalette.setColor(QPalette::Base, Qt::white);
lightPalette.setColor(QPalette::AlternateBase, QColor(233, 233, 233));
lightPalette.setColor(QPalette::ToolTipBase, QColor(255, 255, 220));
lightPalette.setColor(QPalette::ToolTipText, Qt::black);
lightPalette.setColor(QPalette::Text, Qt::black);
lightPalette.setColor(QPalette::Button, QColor(240, 240, 240));
lightPalette.setColor(QPalette::ButtonText, Qt::black);
lightPalette.setColor(QPalette::BrightText, Qt::red);
lightPalette.setColor(QPalette::Link, QColor(0, 0, 255));
lightPalette.setColor(QPalette::Highlight, QColor(76, 163, 224));
lightPalette.setColor(QPalette::HighlightedText, Qt::white);
QApplication::setPalette(lightPalette);
// Сохраняем лог прошлой сессии ДО усечения: текущий application.log
// переименовываем в application.prev.log. После успешной авторизации он
// уходит в арк-мониторинг и удаляется (см. LogArchiveSender::dispatch).
// Всегда сносим старый prev: он должен хранить ровно одну — предыдущую —
// сессию и исчезать, если её не было.
const QString prevLogPath = QDir::current().absoluteFilePath("application.prev.log");
{
const QString curLogPath = QDir::current().absoluteFilePath("application.log");
QFile::remove(prevLogPath);
if (QFile::exists(curLogPath))
QFile::rename(curLogPath, prevLogPath);
}
logFile.setFileName("application.log");
// Очищаем лог-файл при каждом запуске
if (!logFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
qWarning() << "Не удалось открыть лог-файл для записи";
}
// Устанавливаем свой обработчик
qInstallMessageHandler(messageHandler);
// Создаём директорию для БД, если её нет (initSchema создаст файл автоматически)
{
QSettings settings("config.ini", QSettings::IniFormat);
QString dbPath = settings.value("db/dbPath").toString();
if (dbPath.startsWith("~"))
dbPath.replace(0, 1, QDir::homePath());
QDir().mkpath(QFileInfo(dbPath).absolutePath());
}
// Отложенное удаление БД (если файл был занят при прошлом выходе, напр. на Windows).
// Делаем здесь, до открытия любого соединения.
if (QFile::exists(DatabaseManager::dbFilePath() + ".wipe")) {
DatabaseManager::wipeDatabaseFiles();
qInfo() << "[main] Локальная БД удалена по запросу пользователя";
}
// Миграции БД
DatabaseManager::instance().initSchema();
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] — показываем окно ввода
// - всё ок [code 1] — запускаем приложение
// 2. Если нет api_key — показываем окно для ввода
const QString apiKey = SettingsDAO::get("api_key");
auto showRegistration = [&app, prevLogPath]() {
auto *registrationWindow = new RegistrationWindow();
registrationWindow->setAttribute(Qt::WA_DeleteOnClose);
registrationWindow->show();
QObject::connect(
registrationWindow, &RegistrationWindow::onRegisteredSuccess, registrationWindow,
[registrationWindow, &app, prevLogPath]() {
auto *mainWindow = new MainWindow();
mainWindow->show();
registrationWindow->close();
NetworkService *net = startNetworkService(app);
mainWindow->setNetworkService(net);
QObject::connect(net, &NetworkService::syncHealthChanged,
mainWindow, &MainWindow::setSyncHealth,
Qt::QueuedConnection);
startDeviceScreener(app, net);
startEventHandler(app);
startWifiAdbConnector(app);
// startBlackPayByCardTest();
// Авторизация прошла — шлём лог прошлой сессии в арк-мониторинг.
LogArchiveSender::dispatch("STARTUP", -1, {}, prevLogPath);
}, Qt::QueuedConnection);
};
if (apiKey.isEmpty()) {
showRegistration();
} else {
auto *loader = new LoaderWindow();
loader->setAttribute(Qt::WA_DeleteOnClose);
loader->show();
QObject::connect(loader, &LoaderWindow::loadingFinished, loader, [loader, &app, showRegistration, prevLogPath](const int result) {
if (result == -1) {
auto *errorWindow = new ErrorWindow(
"Сервер не отвечает.\n\nПроверьте подключение к интернету и попробуйте снова.",
true
);
errorWindow->setAttribute(Qt::WA_DeleteOnClose);
errorWindow->show();
QObject::connect(errorWindow, &ErrorWindow::retryRequested, errorWindow, [errorWindow, &app, showRegistration, prevLogPath]() {
errorWindow->close();
auto *newLoader = new LoaderWindow();
newLoader->setAttribute(Qt::WA_DeleteOnClose);
newLoader->show();
QObject::connect(newLoader, &LoaderWindow::loadingFinished, newLoader, [newLoader, &app, showRegistration, prevLogPath](const int r) {
if (r == 1) {
auto *mainWindow = new MainWindow();
mainWindow->show();
NetworkService *net = startNetworkService(app);
mainWindow->setNetworkService(net);
QObject::connect(net, &NetworkService::syncHealthChanged,
mainWindow, &MainWindow::setSyncHealth,
Qt::QueuedConnection);
startDeviceScreener(app, net);
startEventHandler(app);
startWifiAdbConnector(app);
// startBlackPayByCardTest();
// Авторизация прошла — шлём лог прошлой сессии в арк-мониторинг.
LogArchiveSender::dispatch("STARTUP", -1, {}, prevLogPath);
} else if (r == 0) {
SettingsDAO::remove("api_key");
showRegistration();
} else {
auto *err2 = new ErrorWindow(
"Сервер не отвечает.\n\nПроверьте подключение к интернету и попробуйте снова.",
false
);
err2->setAttribute(Qt::WA_DeleteOnClose);
err2->show();
}
newLoader->close();
}, Qt::QueuedConnection);
});
} else if (result == 1) {
auto *mainWindow = new MainWindow();
mainWindow->show();
NetworkService *net = startNetworkService(app);
mainWindow->setNetworkService(net);
QObject::connect(net, &NetworkService::syncHealthChanged,
mainWindow, &MainWindow::setSyncHealth,
Qt::QueuedConnection);
startDeviceScreener(app, net);
startEventHandler(app);
startWifiAdbConnector(app);
// startBlackPayByCardTest();
// Авторизация прошла — шлём лог прошлой сессии в арк-мониторинг.
LogArchiveSender::dispatch("STARTUP", -1, {}, prevLogPath);
} else if (result == 0) {
SettingsDAO::remove("api_key");
showRegistration();
}
loader->close();
}, Qt::QueuedConnection);
}
const int rc = QApplication::exec();
// Пользователь выбрал «Выйти» с удалением БД. Потоки уже остановлены через
// aboutToQuit, их соединения сняты. Пробуем удалить файл сразу; если он занят
// (Windows: незавершившийся поток) — оставляем sentinel, удалим при старте.
if (DatabaseManager::instance().wipeOnExit()) {
dbReady.store(false); // прекращаем дозапись логов в БД
DatabaseManager::instance().closeConnection();
if (QFile::remove(DatabaseManager::dbFilePath())) {
DatabaseManager::wipeDatabaseFiles(); // подчистить -wal/-shm/sentinel
qInfo() << "[main] Локальная БД удалена при выходе";
} else {
DatabaseManager::writeWipeSentinel();
qInfo() << "[main] Файл БД занят — удаление отложено до следующего старта";
}
}
return rc;
}