158 lines
6.0 KiB
C++
158 lines
6.0 KiB
C++
#include "LogArchiveSender.h"
|
|
|
|
#include <QByteArray>
|
|
#include <QCoreApplication>
|
|
#include <QDateTime>
|
|
#include <QDebug>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QProcess>
|
|
#include <QRegularExpression>
|
|
#include <QStringList>
|
|
#include <QThread>
|
|
|
|
#include "dao/SettingsDAO.h"
|
|
#include "net/NetworkService.h"
|
|
|
|
namespace {
|
|
|
|
// Запускается в worker-потоке. Возвращает байты zip или пустой массив.
|
|
QByteArray buildArchive() {
|
|
const QString tmpDir = QDir::tempPath() + QStringLiteral("/arc_logs_auto_") +
|
|
QString::number(QDateTime::currentMSecsSinceEpoch());
|
|
if (!LogArchiveSender::collectInto(tmpDir)) {
|
|
QDir(tmpDir).removeRecursively();
|
|
return {};
|
|
}
|
|
|
|
const QString archivePath = tmpDir + ".zip";
|
|
QFile::remove(archivePath);
|
|
QProcess archiver;
|
|
archiver.setWorkingDirectory(tmpDir);
|
|
#ifdef Q_OS_WIN
|
|
archiver.start("powershell", QStringList()
|
|
<< "-NoProfile" << "-Command"
|
|
<< QString("Compress-Archive -Path '%1/*' -DestinationPath '%2' -Force").arg(tmpDir, archivePath));
|
|
#else
|
|
archiver.start("zip", QStringList() << "-r" << archivePath << ".");
|
|
#endif
|
|
archiver.waitForFinished(30000);
|
|
|
|
QDir(tmpDir).removeRecursively();
|
|
|
|
if (!QFile::exists(archivePath)) {
|
|
qWarning() << "[LogArchiveSender] archive not created at" << archivePath;
|
|
return {};
|
|
}
|
|
QFile zip(archivePath);
|
|
if (!zip.open(QIODevice::ReadOnly)) {
|
|
qWarning() << "[LogArchiveSender] cannot open archive" << archivePath;
|
|
QFile::remove(archivePath);
|
|
return {};
|
|
}
|
|
const QByteArray bytes = zip.readAll();
|
|
zip.close();
|
|
QFile::remove(archivePath);
|
|
return bytes;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool LogArchiveSender::collectInto(const QString &destDir) {
|
|
const QString logPath = QCoreApplication::applicationDirPath() + "/application.log";
|
|
|
|
if (!QFile::exists(logPath)) {
|
|
qWarning() << "[LogArchiveSender] log file not found — nothing to collect";
|
|
return false;
|
|
}
|
|
|
|
QDir().mkpath(destDir);
|
|
bool any = false;
|
|
QFile srcLog(logPath);
|
|
if (srcLog.open(QIODevice::ReadOnly)) {
|
|
const QDateTime cutoff = QDateTime::currentDateTime().addSecs(-3600);
|
|
|
|
// Binary search for the first line with ts >= cutoff.
|
|
// Relies on monotonically non-decreasing timestamps in the log.
|
|
qint64 lo = 0, hi = srcLog.size();
|
|
while (lo < hi) {
|
|
const qint64 mid = lo + (hi - lo) / 2;
|
|
srcLog.seek(mid);
|
|
if (mid > 0) srcLog.readLine(); // align to next line boundary
|
|
const qint64 linePos = srcLog.pos();
|
|
if (linePos >= hi) { hi = mid; continue; }
|
|
const QByteArray probe = srcLog.readLine();
|
|
if (probe.isEmpty()) { hi = mid; continue; }
|
|
const QDateTime ts = QDateTime::fromString(
|
|
QString::fromLatin1(probe.left(23)), "yyyy-MM-dd hh:mm:ss.zzz");
|
|
if (!ts.isValid() || ts < cutoff)
|
|
lo = srcLog.pos(); // this line is old, search upper half
|
|
else
|
|
hi = linePos; // this line is new, search lower half
|
|
}
|
|
srcLog.seek(lo);
|
|
|
|
QFile dstLog(destDir + "/application.log");
|
|
if (dstLog.open(QIODevice::WriteOnly)) {
|
|
while (!srcLog.atEnd())
|
|
dstLog.write(srcLog.readLine());
|
|
any = true;
|
|
}
|
|
} else {
|
|
if (QFile::copy(logPath, destDir + "/application.log")) any = true;
|
|
}
|
|
return any;
|
|
}
|
|
|
|
bool LogArchiveSender::dispatch(const QString &label, int eventId, const QString &extraCaptionHtml) {
|
|
const QString apiToken = SettingsDAO::get("api_key");
|
|
const QDateTime now = QDateTime::currentDateTime();
|
|
const QString timestamp = now.toString(Qt::ISODate);
|
|
|
|
// api-токен в caption больше не пишем (общий на нескольких операторов);
|
|
// идентификация идёт по Profile/Material/Phone из extraCaptionHtml.
|
|
const QString desktopId = SettingsDAO::get("desktop_id");
|
|
QStringList head{QStringLiteral("[LOGS]")};
|
|
if (!label.isEmpty()) head << QStringLiteral("[%1]").arg(label.toHtmlEscaped());
|
|
QString text = QStringLiteral("<b>%1</b>\n%2")
|
|
.arg(head.join(QStringLiteral(" ")), timestamp.toHtmlEscaped());
|
|
if (!desktopId.isEmpty())
|
|
text += QStringLiteral("\nDesktop: %1").arg(desktopId.toHtmlEscaped());
|
|
if (!extraCaptionHtml.isEmpty())
|
|
text += QStringLiteral("\n") + extraCaptionHtml;
|
|
|
|
QString tokenSafe = apiToken;
|
|
tokenSafe.replace(QRegularExpression(R"([^\w\-.])"), "_");
|
|
const QString stamp = now.toString("yyyy-MM-dd_HH-mm-ss");
|
|
QStringList parts{QStringLiteral("LOGS")};
|
|
if (!tokenSafe.isEmpty()) parts << tokenSafe;
|
|
parts << stamp;
|
|
if (!label.isEmpty()) parts << label;
|
|
if (eventId > 0) parts << QStringLiteral("ev%1").arg(eventId);
|
|
const QString archiveFilename = parts.join(QStringLiteral("_")) + QStringLiteral(".zip");
|
|
|
|
auto *worker = QThread::create([text, archiveFilename]() {
|
|
const QByteArray archive = buildArchive();
|
|
if (archive.isEmpty()) {
|
|
qWarning() << "[LogArchiveSender] empty archive, skipping dispatch";
|
|
return;
|
|
}
|
|
|
|
auto *thread = new QThread;
|
|
auto *svc = new NetworkService;
|
|
svc->moveToThread(thread);
|
|
QObject::connect(thread, &QThread::started, svc,
|
|
[svc, text, archive, archiveFilename]() {
|
|
svc->postMonitoringDump(text, archive, QByteArray(), archiveFilename);
|
|
emit svc->finished();
|
|
});
|
|
QObject::connect(svc, &NetworkService::finished, thread, &QThread::quit);
|
|
QObject::connect(svc, &NetworkService::finished, svc, &QObject::deleteLater);
|
|
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
|
|
thread->start();
|
|
});
|
|
QObject::connect(worker, &QThread::finished, worker, &QObject::deleteLater);
|
|
worker->start();
|
|
return true;
|
|
}
|