330 lines
13 KiB
C++
330 lines
13 KiB
C++
#include "FileLogWindow.h"
|
||
|
||
#include <QCoreApplication>
|
||
#include <QDateTime>
|
||
#include <QDir>
|
||
#include <QFile>
|
||
#include <QFileInfo>
|
||
#include <QHBoxLayout>
|
||
#include <QHeaderView>
|
||
#include <QMessageBox>
|
||
#include <QMetaObject>
|
||
#include <QRegularExpression>
|
||
#include <QSettings>
|
||
#include <QProcess>
|
||
#include <QPixmap>
|
||
#include <QVBoxLayout>
|
||
#include <memory>
|
||
|
||
#include "dump/TaskDumpRecorder.h"
|
||
#include "dao/GeneralLogDAO.h"
|
||
#include "dao/SettingsDAO.h"
|
||
#include "log/LogDetailWindow.h"
|
||
#include "net/NetworkService.h"
|
||
|
||
static constexpr int COL_ID = 0;
|
||
static constexpr int COL_TIME = 1;
|
||
static constexpr int COL_LEVEL = 2;
|
||
static constexpr int COL_SOURCE = 3;
|
||
static constexpr int COL_MESSAGE = 4;
|
||
|
||
FileLogWindow::FileLogWindow(QWidget *parent) : QDialog(parent) {
|
||
setWindowTitle("Логи");
|
||
resize(1200, 650);
|
||
|
||
m_table = new QTableWidget(this);
|
||
m_table->setColumnCount(5);
|
||
m_table->setHorizontalHeaderLabels({"#", "Время", "Уровень", "Источник", "Сообщение"});
|
||
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||
m_table->setSelectionMode(QAbstractItemView::SingleSelection);
|
||
m_table->verticalHeader()->hide();
|
||
m_table->horizontalHeader()->setStretchLastSection(true);
|
||
m_table->setColumnWidth(COL_ID, 45);
|
||
m_table->setColumnWidth(COL_TIME, 145);
|
||
m_table->setColumnWidth(COL_LEVEL, 80);
|
||
m_table->setColumnWidth(COL_SOURCE, 250);
|
||
|
||
// Панель скриншота
|
||
m_screenshotLabel = new QLabel(this);
|
||
m_screenshotLabel->setAlignment(Qt::AlignCenter);
|
||
m_screenshotLabel->setMinimumWidth(300);
|
||
m_screenshotLabel->setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc;");
|
||
m_screenshotLabel->setText("Выберите запись\nдля просмотра скриншота");
|
||
|
||
auto *splitter = new QSplitter(Qt::Horizontal, this);
|
||
splitter->addWidget(m_table);
|
||
splitter->addWidget(m_screenshotLabel);
|
||
splitter->setStretchFactor(0, 3);
|
||
splitter->setStretchFactor(1, 1);
|
||
|
||
m_showAllCheck = new QCheckBox("Показать все (включая DEBUG/INFO)", this);
|
||
|
||
m_prevBtn = new QPushButton("←", this);
|
||
m_nextBtn = new QPushButton("→", this);
|
||
m_pageLabel = new QLabel(this);
|
||
m_pageLabel->setAlignment(Qt::AlignCenter);
|
||
m_pageLabel->setMinimumWidth(140);
|
||
|
||
auto *refreshBtn = new QPushButton("Обновить", this);
|
||
m_sendBtn = new QPushButton("Отправить", this);
|
||
m_sendBtn->setStyleSheet("background-color: #0088cc; color: white; border-radius: 4px; padding: 4px 12px;");
|
||
|
||
auto *paginationLayout = new QHBoxLayout;
|
||
paginationLayout->addWidget(m_prevBtn);
|
||
paginationLayout->addWidget(m_pageLabel);
|
||
paginationLayout->addWidget(m_nextBtn);
|
||
paginationLayout->addStretch();
|
||
paginationLayout->addWidget(m_showAllCheck);
|
||
paginationLayout->addWidget(refreshBtn);
|
||
paginationLayout->addWidget(m_sendBtn);
|
||
|
||
auto *mainLayout = new QVBoxLayout(this);
|
||
mainLayout->addWidget(splitter, 1);
|
||
mainLayout->addLayout(paginationLayout);
|
||
|
||
connect(m_prevBtn, &QPushButton::clicked, this, [this]() {
|
||
if (m_page > 0) { --m_page; loadPage(); }
|
||
});
|
||
connect(m_nextBtn, &QPushButton::clicked, this, [this]() {
|
||
if (m_page < m_totalPages - 1) { ++m_page; loadPage(); }
|
||
});
|
||
connect(refreshBtn, &QPushButton::clicked, this, [this]() {
|
||
m_page = 0;
|
||
loadPage();
|
||
});
|
||
connect(m_showAllCheck, &QCheckBox::toggled, this, [this]() {
|
||
m_page = 0;
|
||
loadPage();
|
||
});
|
||
connect(m_table, &QTableWidget::cellClicked, this, [this](int row, int) {
|
||
showScreenshot(row);
|
||
});
|
||
connect(m_table, &QTableWidget::cellDoubleClicked, this, [this](int row, int) {
|
||
if (row >= 0 && row < m_entries.size()) {
|
||
auto *detail = new LogDetailWindow(m_entries[row], this);
|
||
detail->setAttribute(Qt::WA_DeleteOnClose);
|
||
detail->show();
|
||
}
|
||
});
|
||
connect(m_sendBtn, &QPushButton::clicked, this, &FileLogWindow::sendLogsToBackend);
|
||
|
||
loadPage();
|
||
}
|
||
|
||
QStringList FileLogWindow::currentLevelFilter() const {
|
||
if (m_showAllCheck->isChecked())
|
||
return {};
|
||
return {"WARNING", "CRITICAL", "FATAL"};
|
||
}
|
||
|
||
void FileLogWindow::loadPage() {
|
||
const QStringList levels = currentLevelFilter();
|
||
const int total = GeneralLogDAO::getTotalCount(levels);
|
||
m_totalPages = qMax(1, (total + m_pageSize - 1) / m_pageSize);
|
||
m_page = qBound(0, m_page, m_totalPages - 1);
|
||
|
||
m_entries = GeneralLogDAO::getLogs(m_page, m_pageSize, levels);
|
||
|
||
m_table->setRowCount(static_cast<int>(m_entries.size()));
|
||
for (int i = 0; i < m_entries.size(); ++i) {
|
||
const auto &e = m_entries[i];
|
||
m_table->setItem(i, COL_ID, new QTableWidgetItem(QString::number(e.id)));
|
||
m_table->setItem(i, COL_TIME, new QTableWidgetItem(
|
||
e.timestamp.toLocalTime().toString("dd.MM.yy HH:mm:ss")));
|
||
m_table->setItem(i, COL_LEVEL, new QTableWidgetItem(e.level));
|
||
m_table->setItem(i, COL_SOURCE, new QTableWidgetItem(e.source));
|
||
m_table->setItem(i, COL_MESSAGE, new QTableWidgetItem(e.message));
|
||
}
|
||
|
||
updatePagination();
|
||
}
|
||
|
||
void FileLogWindow::showScreenshot(int row) {
|
||
if (row < 0 || row >= m_entries.size()) return;
|
||
const QByteArray &data = m_entries[row].screenshot;
|
||
if (data.isEmpty()) {
|
||
m_screenshotLabel->setText("Нет скриншота");
|
||
return;
|
||
}
|
||
|
||
QPixmap pixmap;
|
||
pixmap.loadFromData(data);
|
||
if (pixmap.isNull()) {
|
||
m_screenshotLabel->setText("Ошибка загрузки");
|
||
return;
|
||
}
|
||
|
||
m_screenshotLabel->setPixmap(pixmap.scaled(
|
||
m_screenshotLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||
}
|
||
|
||
void FileLogWindow::updatePagination() {
|
||
m_pageLabel->setText(QString("Страница %1 из %2").arg(m_page + 1).arg(m_totalPages));
|
||
m_prevBtn->setEnabled(m_page > 0);
|
||
m_nextBtn->setEnabled(m_page < m_totalPages - 1);
|
||
}
|
||
|
||
void FileLogWindow::sendLogsToBackend() {
|
||
qDebug() << "[sendLogsToBackend] step 1: checking log file";
|
||
const QString logPath = QCoreApplication::applicationDirPath() + "/application.log";
|
||
if (!QFile::exists(logPath)) {
|
||
QMessageBox::warning(this, "Ошибка", "Файл application.log не найден");
|
||
return;
|
||
}
|
||
|
||
// Резолвим путь к БД из config.ini (как DatabaseManager): [db]/dbPath,
|
||
// с разворотом '~' в домашнюю директорию и приведением к абсолютному пути
|
||
// относительно applicationDirPath, чтобы не зависеть от текущего CWD.
|
||
const QSettings settings("config.ini", QSettings::IniFormat);
|
||
QString dbPath = settings.value("db/dbPath").toString();
|
||
if (dbPath.startsWith("~")) {
|
||
dbPath.replace(0, 1, QDir::homePath());
|
||
}
|
||
if (!dbPath.isEmpty() && QFileInfo(dbPath).isRelative()) {
|
||
dbPath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(dbPath);
|
||
}
|
||
if (dbPath.isEmpty() || !QFile::exists(dbPath)) {
|
||
QMessageBox::warning(this, "Ошибка",
|
||
"Файл БД не найден: " + (dbPath.isEmpty() ? QStringLiteral("<пусто>") : dbPath));
|
||
return;
|
||
}
|
||
|
||
NetworkService *svc = TaskDumpRecorder::networkService();
|
||
if (!svc) {
|
||
QMessageBox::warning(this, "Ошибка",
|
||
"NetworkService не инициализирован — отправка на бэкенд недоступна.");
|
||
return;
|
||
}
|
||
|
||
m_sendBtn->setEnabled(false);
|
||
m_sendBtn->setText("Подготовка...");
|
||
|
||
qDebug() << "[sendLogsToBackend] step 2: creating tmp dir";
|
||
const QString tmpDir = QDir::tempPath() + "/arc_logs_export";
|
||
QDir(tmpDir).removeRecursively();
|
||
QDir().mkpath(tmpDir);
|
||
|
||
// 1. application.log целиком
|
||
QFile::copy(logPath, tmpDir + "/application.log");
|
||
|
||
// 2. БД + WAL/SHM-сайдкары (если есть) — иначе при включённом WAL
|
||
// снимок будет рассогласованным.
|
||
const QString dbBaseName = QFileInfo(dbPath).fileName();
|
||
QFile::copy(dbPath, tmpDir + "/" + dbBaseName);
|
||
for (const QString &suffix : {QStringLiteral("-wal"), QStringLiteral("-shm")}) {
|
||
const QString sidecar = dbPath + suffix;
|
||
if (QFile::exists(sidecar)) {
|
||
QFile::copy(sidecar, tmpDir + "/" + dbBaseName + suffix);
|
||
}
|
||
}
|
||
|
||
qDebug() << "[sendLogsToBackend] step 3: creating archive";
|
||
const QString archivePath = QDir::tempPath() + "/arc_logs.zip";
|
||
QFile::remove(archivePath);
|
||
#ifdef Q_OS_WIN
|
||
QProcess archiver;
|
||
archiver.setWorkingDirectory(tmpDir);
|
||
archiver.start("powershell", QStringList()
|
||
<< "-NoProfile" << "-Command"
|
||
<< QString("Compress-Archive -Path '%1/*' -DestinationPath '%2' -Force").arg(tmpDir, archivePath));
|
||
if (!archiver.waitForFinished(30000)) {
|
||
qWarning() << "[sendLogsToBackend] powershell archiver timeout/error:" << archiver.errorString();
|
||
}
|
||
qDebug() << "[sendLogsToBackend] archiver exit code:" << archiver.exitCode()
|
||
<< "stdout:" << archiver.readAllStandardOutput()
|
||
<< "stderr:" << archiver.readAllStandardError();
|
||
#else
|
||
QProcess archiver;
|
||
archiver.setWorkingDirectory(tmpDir);
|
||
archiver.start("zip", QStringList() << "-r" << archivePath << ".");
|
||
archiver.waitForFinished(30000);
|
||
#endif
|
||
|
||
QDir(tmpDir).removeRecursively();
|
||
|
||
if (!QFile::exists(archivePath)) {
|
||
QMessageBox::warning(this, "Ошибка", "Не удалось создать архив");
|
||
m_sendBtn->setEnabled(true);
|
||
m_sendBtn->setText("Отправить");
|
||
return;
|
||
}
|
||
|
||
QFile zipFile(archivePath);
|
||
if (!zipFile.open(QIODevice::ReadOnly)) {
|
||
QMessageBox::warning(this, "Ошибка", "Не удалось открыть архив");
|
||
QFile::remove(archivePath);
|
||
m_sendBtn->setEnabled(true);
|
||
m_sendBtn->setText("Отправить");
|
||
return;
|
||
}
|
||
const QByteArray archiveBytes = zipFile.readAll();
|
||
zipFile.close();
|
||
QFile::remove(archivePath);
|
||
|
||
if (archiveBytes.isEmpty()) {
|
||
QMessageBox::warning(this, "Ошибка", "Архив пуст — отправлять нечего");
|
||
m_sendBtn->setEnabled(true);
|
||
m_sendBtn->setText("Отправить");
|
||
return;
|
||
}
|
||
|
||
const QString apiToken = SettingsDAO::get("api_key");
|
||
const QDateTime now = QDateTime::currentDateTime();
|
||
const QString timestamp = now.toString(Qt::ISODate);
|
||
QString header = QStringLiteral("[LOGS]");
|
||
if (!apiToken.isEmpty()) header += QStringLiteral(" [%1]").arg(apiToken.toHtmlEscaped());
|
||
const QString text = QStringLiteral("<b>%1</b>\n%2").arg(header, timestamp.toHtmlEscaped());
|
||
|
||
QString filenameSafeToken = apiToken;
|
||
filenameSafeToken.replace(QRegularExpression(R"([^\w\-.])"), "_");
|
||
const QString filenameStamp = now.toString("yyyy-MM-dd_HH-mm-ss");
|
||
const QString archiveFilename = filenameSafeToken.isEmpty()
|
||
? QStringLiteral("LOGS_%1.zip").arg(filenameStamp)
|
||
: QStringLiteral("LOGS_%1_%2.zip").arg(filenameSafeToken, filenameStamp);
|
||
|
||
qDebug() << "[sendLogsToBackend] dispatching to NetworkService::postMonitoringDump"
|
||
<< "archive.size=" << archiveBytes.size() << "filename=" << archiveFilename;
|
||
|
||
// Подписываемся на результат ДО диспатча. svc живёт в потоке paymentThread —
|
||
// соединение автоматически становится QueuedConnection, лямбда исполнится в
|
||
// UI-потоке. Контекст = this ⇒ при закрытии окна Qt сам отключит слот (без
|
||
// обращения к висячему указателю). Фильтруем по своему archiveFilename:
|
||
// синглтон общий, параллельно могут уходить и другие дампы (TaskDumpRecorder).
|
||
auto handled = std::make_shared<bool>(false);
|
||
auto conn = std::make_shared<QMetaObject::Connection>();
|
||
*conn = connect(svc, &NetworkService::monitoringDumpFinished, this,
|
||
[this, conn, handled, expected = archiveFilename](
|
||
bool ok, const QString &detail, const QString &fn) {
|
||
if (fn != expected || *handled) return;
|
||
*handled = true;
|
||
QObject::disconnect(*conn);
|
||
m_sendBtn->setEnabled(true);
|
||
m_sendBtn->setText("Отправить");
|
||
if (ok) {
|
||
QMessageBox::information(this, "Готово", "Логи отправлены на бэкенд.");
|
||
} else {
|
||
QMessageBox::warning(this, "Ошибка",
|
||
"Не удалось отправить логи: " + detail);
|
||
}
|
||
});
|
||
|
||
m_sendBtn->setText("Отправка...");
|
||
|
||
const bool dispatched = QMetaObject::invokeMethod(svc, "postMonitoringDump",
|
||
Qt::QueuedConnection,
|
||
Q_ARG(QString, text),
|
||
Q_ARG(QByteArray, archiveBytes),
|
||
Q_ARG(QByteArray, QByteArray()),
|
||
Q_ARG(QString, archiveFilename));
|
||
|
||
if (!dispatched) {
|
||
*handled = true;
|
||
QObject::disconnect(*conn);
|
||
m_sendBtn->setEnabled(true);
|
||
m_sendBtn->setText("Отправить");
|
||
QMessageBox::warning(this, "Ошибка",
|
||
"Не удалось поставить отправку в очередь NetworkService.");
|
||
}
|
||
}
|