449 lines
19 KiB
C++
449 lines
19 KiB
C++
#include <QApplication>
|
||
#include <QSslSocket>
|
||
#include <QThread>
|
||
|
||
#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 <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 "net/NetworkService.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 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;
|
||
}
|
||
|
||
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();
|
||
}
|
||
}
|
||
|
||
// Дублируем в БД (с защитой от рекурсии), пропуская внутренние 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;
|
||
}
|
||
}
|
||
|
||
// По желанию — дублировать в консоль
|
||
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
|
||
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);
|
||
|
||
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());
|
||
}
|
||
|
||
// Миграции БД
|
||
DatabaseManager::instance().initSchema();
|
||
DatabaseManager::instance().cleanupOldData(30);
|
||
dbReady.store(true);
|
||
|
||
// 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]() {
|
||
auto *registrationWindow = new RegistrationWindow();
|
||
registrationWindow->setAttribute(Qt::WA_DeleteOnClose);
|
||
registrationWindow->show();
|
||
QObject::connect(
|
||
registrationWindow, &RegistrationWindow::onRegisteredSuccess, registrationWindow,
|
||
[registrationWindow, &app]() {
|
||
auto *mainWindow = new MainWindow();
|
||
mainWindow->show();
|
||
registrationWindow->close();
|
||
NetworkService *net = startNetworkService(app);
|
||
QObject::connect(net, &NetworkService::syncHealthChanged,
|
||
mainWindow, &MainWindow::setSyncHealth,
|
||
Qt::QueuedConnection);
|
||
startDeviceScreener(app, net);
|
||
startEventHandler(app);
|
||
// startBlackPayByCardTest();
|
||
}, 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](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]() {
|
||
errorWindow->close();
|
||
auto *newLoader = new LoaderWindow();
|
||
newLoader->setAttribute(Qt::WA_DeleteOnClose);
|
||
newLoader->show();
|
||
QObject::connect(newLoader, &LoaderWindow::loadingFinished, newLoader, [newLoader, &app, showRegistration](const int r) {
|
||
if (r == 1) {
|
||
auto *mainWindow = new MainWindow();
|
||
mainWindow->show();
|
||
NetworkService *net = startNetworkService(app);
|
||
QObject::connect(net, &NetworkService::syncHealthChanged,
|
||
mainWindow, &MainWindow::setSyncHealth,
|
||
Qt::QueuedConnection);
|
||
startDeviceScreener(app, net);
|
||
startEventHandler(app);
|
||
// startBlackPayByCardTest();
|
||
} 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);
|
||
QObject::connect(net, &NetworkService::syncHealthChanged,
|
||
mainWindow, &MainWindow::setSyncHealth,
|
||
Qt::QueuedConnection);
|
||
startDeviceScreener(app, net);
|
||
startEventHandler(app);
|
||
// startBlackPayByCardTest();
|
||
} else if (result == 0) {
|
||
SettingsDAO::remove("api_key");
|
||
showRegistration();
|
||
}
|
||
loader->close();
|
||
}, Qt::QueuedConnection);
|
||
}
|
||
return QApplication::exec();
|
||
}
|