1
0
forked from BRT/arc
arc/android/dump/TaskDumpRecorder.cpp

350 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "TaskDumpRecorder.h"
#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QMetaObject>
#include <QPointer>
#include <QProcess>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QTextStream>
#include <QThread>
#include <QThreadStorage>
#include <qDebug>
#include "adb/AdbUtils.h"
#include "dao/SettingsDAO.h"
#include "net/NetworkService.h"
#include "LogArchiveSender.h"
namespace {
QThreadStorage<TaskDumpRecorder *> &tlsRecorder() {
static QThreadStorage<TaskDumpRecorder *> tls;
return tls;
}
QPointer<NetworkService> &networkServiceRef() {
static QPointer<NetworkService> ref;
return ref;
}
QString dumpsRoot() {
// Пишем в пользовательский temp — UAC/Program Files не мешают.
QString base = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
if (base.isEmpty()) base = QDir::tempPath();
return base + "/arc_dumps";
}
QString sanitize(const QString &raw, int maxLen = 40) {
static const QRegularExpression re(QStringLiteral("[^\\p{L}\\p{N}_-]+"));
QString s = raw;
s.replace(re, "_");
if (s.size() > maxLen) s = s.left(maxLen);
while (s.endsWith('_')) s.chop(1);
while (s.startsWith('_')) s = s.mid(1);
return s;
}
QString extractScreenTag(const QString &xml) {
static const QRegularExpression re(QStringLiteral("text=\"([^\"]{4,40})\""));
static const QRegularExpression noiseRe(QStringLiteral("^[0-9\\s,\\.\\-+₽$€%]+$"));
auto it = re.globalMatch(xml);
while (it.hasNext()) {
QString text = it.next().captured(1).trimmed();
if (text.isEmpty() || noiseRe.match(text).hasMatch()) continue;
QString tag = sanitize(text);
if (!tag.isEmpty()) return tag;
}
return QStringLiteral("screen");
}
bool zipFolder(const QString &folder, const QString &archivePath) {
#ifdef Q_OS_WIN
const QString cmd = QString(
"Compress-Archive -Path '%1\\*' -DestinationPath '%2' -Force"
).arg(QDir::toNativeSeparators(folder), QDir::toNativeSeparators(archivePath));
QProcess p;
p.start("powershell", {"-NoProfile", "-Command", cmd});
if (!p.waitForStarted(5000) || !p.waitForFinished(180000)) {
qWarning() << "[TaskDumpRecorder] zip failed:" << p.errorString();
return false;
}
if (p.exitCode() != 0) {
qWarning() << "[TaskDumpRecorder] zip exit=" << p.exitCode()
<< "stderr:" << QString::fromUtf8(p.readAllStandardError()).trimmed();
return false;
}
return true;
#else
const QFileInfo fi(folder);
QProcess p;
p.setWorkingDirectory(fi.absolutePath());
p.start("zip", {"-rq", archivePath, fi.fileName()});
if (!p.waitForStarted(5000) || !p.waitForFinished(180000)) {
qWarning() << "[TaskDumpRecorder] zip failed:" << p.errorString();
return false;
}
return p.exitCode() == 0;
#endif
}
QByteArray readFileBytes(const QString &path) {
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) return {};
QByteArray bytes = f.readAll();
f.close();
return bytes;
}
} // namespace
void TaskDumpRecorder::setNetworkService(NetworkService *service) {
networkServiceRef() = service;
}
NetworkService *TaskDumpRecorder::networkService() {
return networkServiceRef().data();
}
TaskDumpRecorder *TaskDumpRecorder::current() {
return tlsRecorder().hasLocalData() ? tlsRecorder().localData() : nullptr;
}
void TaskDumpRecorder::begin(const QString &bank, const QString &taskKind,
const QString &adbDeviceId, const TaskDumpMeta &meta) {
if (auto *prev = current()) {
prev->finishInternal(QStringLiteral("interrupted (new task started before previous finished)"));
// Не вызываем `delete prev` — QThreadStorage<T*> сам удалит предыдущий
// указатель при замене через setLocalData.
}
auto *rec = new TaskDumpRecorder(bank, taskKind, adbDeviceId, meta);
tlsRecorder().setLocalData(rec);
}
void TaskDumpRecorder::finish(const QString &errorMessage) {
auto *rec = current();
if (!rec) return;
rec->finishInternal(errorMessage);
// setLocalData(nullptr) сам вызовет delete на текущем указателе.
tlsRecorder().setLocalData(nullptr);
}
TaskDumpRecorder::TaskDumpRecorder(const QString &bank, const QString &taskKind,
const QString &adbDeviceId, const TaskDumpMeta &meta)
: m_bank(sanitize(bank)),
m_taskKind(sanitize(taskKind)),
m_adbDeviceIdRaw(adbDeviceId),
m_adbDeviceIdSafe(sanitize(adbDeviceId)),
m_meta(meta) {
// Короткий суффикс для имени папки — чтобы визуально отличать задачи,
// но не раздувать путь. Берём materialId (первые 8 символов — UUID segment)
// + сумму, если есть.
QString folderTail;
if (!m_meta.materialId.isEmpty()) {
folderTail = sanitize(m_meta.materialId.left(8));
}
if (m_meta.amount != 0) {
const QString amt = QString::number(static_cast<qint64>(m_meta.amount));
folderTail = folderTail.isEmpty() ? amt : folderTail + "_" + amt;
}
const QString ts = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
const QString sub = folderTail.isEmpty()
? QString("%1_%2").arg(ts, m_adbDeviceIdSafe)
: QString("%1_%2_%3").arg(ts, m_adbDeviceIdSafe, folderTail);
m_folder = QString("%1/%2_%3/%4")
.arg(dumpsRoot(), m_bank, m_taskKind, sub);
if (!QDir().mkpath(m_folder)) {
qWarning() << "[TaskDumpRecorder] failed to mkpath" << m_folder;
m_folder.clear();
return;
}
qDebug() << "[TaskDumpRecorder] begin:" << m_folder;
}
QString TaskDumpRecorder::buildCaption(const QString &errorMessage) const {
const QString timestamp = QDateTime::currentDateTime().toString(Qt::ISODate);
// api-токен в caption больше не пишем: он общий на нескольких операторов и
// не позволяет отличить, чей профиль упал. Идентифицируем по UUID профиля,
// материалу и телефону профиля (см. m_meta).
QStringList head{QStringLiteral("[TASK_ERROR]")};
head << QStringLiteral("[%1]").arg(m_bank.toHtmlEscaped());
head << QStringLiteral("[%1]").arg(m_taskKind.toHtmlEscaped());
const QString desktopId = SettingsDAO::get("desktop_id");
QString body;
if (!desktopId.isEmpty())
body += QStringLiteral("Desktop: %1\n").arg(desktopId.toHtmlEscaped());
body += QStringLiteral("Device: %1").arg(m_adbDeviceIdRaw.toHtmlEscaped());
if (!m_meta.androidId.isEmpty())
body += QStringLiteral("\nAndroidId: %1").arg(m_meta.androidId.toHtmlEscaped());
if (!m_meta.bankProfileId.isEmpty())
body += QStringLiteral("\nProfile: %1").arg(m_meta.bankProfileId.toHtmlEscaped());
if (!m_meta.materialId.isEmpty())
body += QStringLiteral("\nMaterial: %1").arg(m_meta.materialId.toHtmlEscaped());
if (!m_meta.profilePhone.isEmpty())
body += QStringLiteral("\nPhone: %1").arg(m_meta.profilePhone.toHtmlEscaped());
if (m_meta.amount != 0)
body += QStringLiteral("\nAmount: %1").arg(static_cast<qint64>(m_meta.amount));
if (!m_meta.phone.isEmpty())
body += QStringLiteral("\nRecipient: %1").arg(m_meta.phone.toHtmlEscaped());
if (!m_meta.card.isEmpty())
body += QStringLiteral("\nCard: %1").arg(m_meta.card.toHtmlEscaped());
body += QStringLiteral("\nError: %1").arg(errorMessage.left(800).toHtmlEscaped());
return QStringLiteral("<b>%1</b>\n%2\n%3")
.arg(head.join(QStringLiteral(" ")), timestamp.toHtmlEscaped(), body);
}
void TaskDumpRecorder::saveXml(int counter, const QString &xml) {
if (m_folder.isEmpty() || xml.isEmpty()) return;
const QString tag = extractScreenTag(xml);
const QString fname = QString("%1/%2_%3.xml")
.arg(m_folder,
QString("%1").arg(counter, 4, 10, QChar('0')),
tag);
QFile f(fname);
if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
f.write(xml.toUtf8());
f.close();
} else {
qWarning() << "[TaskDumpRecorder] failed to write" << fname << f.errorString();
}
}
void TaskDumpRecorder::finishInternal(const QString &errorMessage) {
if (m_finished) return;
m_finished = true;
if (m_folder.isEmpty()) return;
if (errorMessage.isEmpty()) {
QDir(m_folder).removeRecursively();
qDebug() << "[TaskDumpRecorder] success — removed" << m_folder;
return;
}
qWarning() << "[TaskDumpRecorder] task failed:" << errorMessage
<< "folder:" << m_folder;
// Финальный скриншот текущего экрана
if (!m_adbDeviceIdRaw.isEmpty()) {
const QByteArray png = AdbUtils::captureScreenshotBytes(m_adbDeviceIdRaw);
if (!png.isEmpty()) {
QFile f(m_folder + "/_final_screen.png");
if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
f.write(png);
f.close();
}
}
}
// Метаданные
{
QFile ef(m_folder + "/_error.txt");
if (ef.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
QTextStream ts(&ef);
ts.setEncoding(QStringConverter::Utf8);
const QString desktopId = SettingsDAO::get("desktop_id");
if (!desktopId.isEmpty()) ts << "desktop_id: " << desktopId << "\n";
ts << "bank: " << m_bank << "\n"
<< "task: " << m_taskKind << "\n"
<< "device: " << m_adbDeviceIdRaw << "\n";
if (!m_meta.androidId.isEmpty()) ts << "android_id: " << m_meta.androidId << "\n";
if (!m_meta.bankProfileId.isEmpty()) ts << "profile_id: " << m_meta.bankProfileId << "\n";
if (!m_meta.profilePhone.isEmpty()) ts << "profile_phone: " << m_meta.profilePhone << "\n";
if (!m_meta.materialId.isEmpty()) ts << "material_id: " << m_meta.materialId << "\n";
if (m_meta.amount != 0) ts << "amount: " << static_cast<qint64>(m_meta.amount) << "\n";
if (!m_meta.phone.isEmpty()) ts << "phone: " << m_meta.phone << "\n";
if (!m_meta.card.isEmpty()) ts << "card: " << m_meta.card << "\n";
ts << "time: " << QDateTime::currentDateTime().toString(Qt::ISODate) << "\n"
<< "error: " << errorMessage << "\n";
ef.close();
}
}
const QString folder = m_folder;
const QString archivePath = m_folder + ".zip";
const QString text = buildCaption(errorMessage);
// Имя файла архива на relay: <bank>_<task>_<timestamp>.zip — чтобы дамп
// сразу читался по банку и типу задачи без открытия caption'а.
const QString stamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
const QString archiveFilename =
QStringLiteral("%1_%2_%3.zip").arg(m_bank, m_taskKind, stamp);
// Финальный скриншот шлём отдельным полем `image`, поэтому сразу читаем
// его в память, пока папка ещё цела.
QByteArray finalScreenshot;
{
const QString screenPath = folder + "/_final_screen.png";
if (QFile::exists(screenPath)) {
finalScreenshot = readFileBytes(screenPath);
}
}
QThread *bg = QThread::create([folder, archivePath, text, finalScreenshot, archiveFilename]() {
// Кладём application.log + БД (+WAL/SHM) прямо в папку задачи (подкаталог
// logs/), чтобы скрины задачи и логи ушли одним общим архивом, а не двумя
// отдельными сообщениями на relay.
LogArchiveSender::collectInto(folder + "/logs");
QByteArray archiveBytes;
if (zipFolder(folder, archivePath)) {
archiveBytes = readFileBytes(archivePath);
if (archiveBytes.isEmpty()) {
qWarning() << "[TaskDumpRecorder] zip is empty, sending text-only dump for" << folder;
}
} else {
qWarning() << "[TaskDumpRecorder] zip failed, sending text-only dump for" << folder;
}
QDir(folder).removeRecursively();
QFile::remove(archivePath);
NetworkService *svc = networkServiceRef().data();
if (svc) {
qDebug() << "[TaskDumpRecorder] dispatching dump to NetworkService"
<< "archive.size=" << archiveBytes.size()
<< "image.size=" << finalScreenshot.size()
<< "text.size=" << text.size();
const bool dispatched = QMetaObject::invokeMethod(svc, "postMonitoringDump",
Qt::QueuedConnection,
Q_ARG(QString, text),
Q_ARG(QByteArray, archiveBytes),
Q_ARG(QByteArray, finalScreenshot),
Q_ARG(QString, archiveFilename));
if (!dispatched) {
qCritical() << "[TaskDumpRecorder] invokeMethod(postMonitoringDump) FAILED — "
"slot not found or wrong signature for" << folder;
}
} else {
qWarning() << "[TaskDumpRecorder] no NetworkService — dump dropped for" << folder
<< "(setNetworkService was not called at app startup?)";
}
});
QObject::connect(bg, &QThread::finished, bg, &QObject::deleteLater);
bg->start();
}
TaskDumpRecorder::Scope::Scope(const QString &bank, const QString &taskKind,
const QString &adbDeviceId, const TaskDumpMeta &meta,
const QString *errorPtr)
: m_errorPtr(errorPtr), m_owned(false) {
// Вложенные скоупы не перетирают рекордер родителя — иначе внутренний
// скрипт прервал бы запись внешней задачи и сгенерировал ложный дамп
// с пометкой "interrupted".
if (TaskDumpRecorder::current() == nullptr) {
TaskDumpRecorder::begin(bank, taskKind, adbDeviceId, meta);
m_owned = true;
}
}
TaskDumpRecorder::Scope::~Scope() {
if (m_owned) {
TaskDumpRecorder::finish(m_errorPtr ? *m_errorPtr : QString());
}
}