#include "LogArchiveSender.h" #include #include #include #include #include #include #include #include #include #include #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(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("%1\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; }