1
0
forked from BRT/arc
arc/services/LogArchiveSender.cpp

194 lines
8.2 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 &sourceLogPath) {
const QString tmpDir = QDir::tempPath() + QStringLiteral("/arc_logs_auto_") +
QString::number(QDateTime::currentMSecsSinceEpoch());
if (!LogArchiveSender::collectInto(tmpDir, sourceLogPath)) {
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 &sourceLogPath) {
const bool wholeFile = !sourceLogPath.isEmpty();
const QString logPath = wholeFile
? sourceLogPath
: QCoreApplication::applicationDirPath() + "/application.log";
if (!QFile::exists(logPath)) {
qWarning() << "[LogArchiveSender] log file not found — nothing to collect:" << logPath;
return false;
}
QDir().mkpath(destDir);
// Явно заданный файл (напр. лог прошлой сессии) шлём целиком — обрезка по
// последнему часу относительно «сейчас» здесь не нужна и только урезала бы
// давно закрытую сессию.
if (wholeFile)
return QFile::copy(logPath, destDir + "/application.log");
bool any = false;
QFile srcLog(logPath);
if (srcLog.open(QIODevice::ReadOnly)) {
const QDateTime cutoff = QDateTime::currentDateTime().addSecs(-3600);
const qint64 size = srcLog.size();
// Find the byte offset of the first log entry within the last hour by
// walking the file backwards in blocks from EOF.
//
// We can't binary-search this: error dumps log multi-line payloads
// (JSON, ADB screen XML, stack traces) whose continuation lines carry no
// leading timestamp. A binary probe landing on such a line parsed an
// invalid date, the old code treated that as "old" and advanced its
// marker — and when the very last line of the file was a continuation
// line (the usual case on error) the marker ran off the end and the dump
// came out empty. Scanning from the tail also keeps this cheap on a huge
// log: we stop the moment a block contains an entry older than the cutoff
// and never touch the older prefix.
static const qint64 BLOCK = 1 << 20; // 1 MiB
qint64 startPos = size; // lowest offset of an in-window entry; == size => none yet
qint64 hi = size; // region [hi, size) already scanned
bool sawOld = false;
while (hi > 0 && !sawOld) {
const qint64 lo = qMax<qint64>(0, hi - BLOCK);
srcLog.seek(lo);
if (lo > 0) srcLog.readLine(); // skip the partial line owned by the earlier block
qint64 pos = srcLog.pos();
while (pos < hi) {
const QByteArray probe = srcLog.readLine();
if (probe.isEmpty()) break;
const QDateTime ts = QDateTime::fromString(
QString::fromLatin1(probe.left(23)), "yyyy-MM-dd hh:mm:ss.zzz");
if (ts.isValid()) {
if (ts >= cutoff) { if (pos < startPos) startPos = pos; }
else sawOld = true; // an older entry → earlier blocks are older too
}
pos = srcLog.pos();
}
hi = lo;
}
// Nothing within the last hour → ship the whole file rather than an empty
// one; stale error context still beats no log at all.
srcLog.seek(startPos < size ? startPos : 0);
QFile dstLog(destDir + "/application.log");
if (dstLog.open(QIODevice::WriteOnly)) {
while (!srcLog.atEnd())
dstLog.write(srcLog.readLine());
dstLog.flush();
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 &sourceLogPath) {
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, sourceLogPath]() {
const QByteArray archive = buildArchive(sourceLogPath);
// Исходник прошлой сессии уже скопирован в архив — удаляем, чтобы файлы
// не плодились между запусками (zip и tmp-каталог чистит buildArchive).
if (!sourceLogPath.isEmpty())
QFile::remove(sourceLogPath);
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;
}