1
0
forked from BRT/arc
arc/android/dump/TaskDumpRecorder.cpp
slava 44e60fc09f Introduce TaskDumpRecorder for centralized task dump management:
- Add TaskDumpRecorder to save task-related XML dumps and upload failure info to Telegram.
- Integrate TaskDumpRecorder into `PayByPhoneScript` and `PayByCardScript` for Dushanbe and Ozon banks.
- Extend ADB utilities to record XML dumps with TaskDumpRecorder and refine keyboard dismissal logic.
- Enhance freeze detection with consecutive dump checks in CommonScript and payment scripts.
- Update mock server profiles and scenario configurations with new material IDs.
2026-04-26 19:22:40 +07:00

262 lines
9.9 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 <QProcess>
#include <QRegularExpression>
#include <QTextStream>
#include <QThread>
#include <QThreadStorage>
#include <qDebug>
#include "adb/AdbUtils.h"
namespace {
QThreadStorage<TaskDumpRecorder *> &tlsRecorder() {
static QThreadStorage<TaskDumpRecorder *> tls;
return tls;
}
constexpr const char *kBotToken = "8256716069:AAFgZRB_0Y6KTpqFQmCxIlZiWdY5m2dR2D8";
constexpr const char *kChatId = "-5177086220";
QString dumpsRoot() {
return QCoreApplication::applicationDirPath() + "/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 runSync(const QString &program, const QStringList &args, int timeoutMs = 180000) {
QProcess p;
p.start(program, args);
if (!p.waitForStarted(5000)) {
qWarning() << "[TaskDumpRecorder] failed to start:" << program << args
<< p.errorString();
return false;
}
if (!p.waitForFinished(timeoutMs)) {
qWarning() << "[TaskDumpRecorder] timeout:" << program << args;
p.kill();
return false;
}
if (p.exitCode() != 0) {
qWarning() << "[TaskDumpRecorder]" << program << "exit=" << p.exitCode()
<< "stderr:" << QString::fromUtf8(p.readAllStandardError()).trimmed();
return false;
}
return true;
}
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));
return runSync("powershell", {"-NoProfile", "-Command", cmd});
#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
}
void uploadToTelegram(const QString &archivePath, const QString &caption) {
const QString url = QString("https://api.telegram.org/bot%1/sendDocument").arg(kBotToken);
const QStringList args = {
"-sS", "--max-time", "120",
"-F", QString("chat_id=%1").arg(kChatId),
"-F", QString("document=@%1").arg(archivePath),
"-F", QString("caption=%1").arg(caption),
url
};
runSync("curl", args, 150000);
}
} // namespace
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 {
QString s = QString("Bank: %1 | Task: %2 | Device: %3")
.arg(m_bank, m_taskKind, m_adbDeviceIdRaw);
if (!m_meta.materialId.isEmpty()) s += QString("\nMaterial ID: %1").arg(m_meta.materialId);
if (m_meta.amount != 0) s += QString("\nAmount: %1").arg(static_cast<qint64>(m_meta.amount));
if (!m_meta.phone.isEmpty()) s += QString("\nPhone: %1").arg(m_meta.phone);
if (!m_meta.card.isEmpty()) s += QString("\nCard: %1").arg(m_meta.card);
s += QString("\nError: %1").arg(errorMessage.left(800));
return s;
}
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);
ts << "bank: " << m_bank << "\n"
<< "task: " << m_taskKind << "\n"
<< "device: " << m_adbDeviceIdRaw << "\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 caption = buildCaption(errorMessage);
QThread *bg = QThread::create([folder, archivePath, caption]() {
if (zipFolder(folder, archivePath)) {
uploadToTelegram(archivePath, caption);
} else {
qWarning() << "[TaskDumpRecorder] zip failed, skipping Telegram upload for" << folder;
}
QDir(folder).removeRecursively();
QFile::remove(archivePath);
});
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) {
TaskDumpRecorder::begin(bank, taskKind, adbDeviceId, meta);
}
TaskDumpRecorder::Scope::~Scope() {
TaskDumpRecorder::finish(m_errorPtr ? *m_errorPtr : QString());
}