- 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.
262 lines
9.9 KiB
C++
262 lines
9.9 KiB
C++
#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());
|
||
}
|