#include #include #include #include #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 #include #ifdef Q_OS_WIN #include #include #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 #include #include #include #include #include #include #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 #include #include 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 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 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 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-поток голодает → зависание через 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(); 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; }