Refactor network requests: consolidate retry logic with backoff, handle transient errors, and improve timeout handling across NetworkService methods. Emit result signals for monitoring dumps.
This commit is contained in:
parent
cef3515790
commit
fac49447bf
@ -56,6 +56,42 @@ inline QString networkErrorCategory(QNetworkReply::NetworkError err) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Транзиентная ошибка CONNECT-фазы: сбой произошёл ДО того, как байты запроса
|
||||||
|
// ушли на сервер (соединение не установилось), поэтому повтор безопасен даже для
|
||||||
|
// неидемпотентных POST — дубля быть не может. Сюда же ETIMEDOUT-connect, который
|
||||||
|
// Qt отдаёт как UnknownNetworkError "Connection timed out".
|
||||||
|
inline bool isConnectPhaseTransient(QNetworkReply::NetworkError err) {
|
||||||
|
switch (err) {
|
||||||
|
case QNetworkReply::ConnectionRefusedError:
|
||||||
|
case QNetworkReply::HostNotFoundError:
|
||||||
|
case QNetworkReply::ProxyConnectionRefusedError:
|
||||||
|
case QNetworkReply::ProxyNotFoundError:
|
||||||
|
case QNetworkReply::ProxyTimeoutError:
|
||||||
|
case QNetworkReply::UnknownNetworkError:
|
||||||
|
case QNetworkReply::TemporaryNetworkFailureError:
|
||||||
|
case QNetworkReply::NetworkSessionFailedError:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Более широкий набор транзиентных ошибок — безопасен ТОЛЬКО для идемпотентных
|
||||||
|
// запросов (GET/PATCH/DELETE, аплоады), т.к. сбой мог случиться уже ПОСЛЕ отправки
|
||||||
|
// тела (сервер мог его обработать) → повтор неидемпотентного POST дал бы дубль.
|
||||||
|
inline bool isIdempotentTransient(QNetworkReply::NetworkError err) {
|
||||||
|
if (isConnectPhaseTransient(err)) return true;
|
||||||
|
switch (err) {
|
||||||
|
case QNetworkReply::RemoteHostClosedError:
|
||||||
|
case QNetworkReply::TimeoutError:
|
||||||
|
case QNetworkReply::ProxyConnectionClosedError:
|
||||||
|
case QNetworkReply::ServiceUnavailableError:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Развёрнутое описание сетевой ошибки: HTTP-код + категория или просто
|
// Развёрнутое описание сетевой ошибки: HTTP-код + категория или просто
|
||||||
// категория + Qt errorString. Эндпоинт — без хоста.
|
// категория + Qt errorString. Эндпоинт — без хоста.
|
||||||
inline QString describeNetError(QNetworkReply *reply, const QString &path) {
|
inline QString describeNetError(QNetworkReply *reply, const QString &path) {
|
||||||
|
|||||||
@ -16,6 +16,8 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QTimeZone>
|
#include <QTimeZone>
|
||||||
#include <QMutexLocker>
|
#include <QMutexLocker>
|
||||||
|
#include <QEventLoop>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
#include "dao/BankDAO.h"
|
#include "dao/BankDAO.h"
|
||||||
#include "dao/GeneralLogDAO.h"
|
#include "dao/GeneralLogDAO.h"
|
||||||
@ -34,31 +36,101 @@
|
|||||||
using NetworkErrorUtils::sanitizePath;
|
using NetworkErrorUtils::sanitizePath;
|
||||||
using NetworkErrorUtils::describeNetError;
|
using NetworkErrorUtils::describeNetError;
|
||||||
using NetworkErrorUtils::describeClientTimeout;
|
using NetworkErrorUtils::describeClientTimeout;
|
||||||
|
using NetworkErrorUtils::isConnectPhaseTransient;
|
||||||
|
using NetworkErrorUtils::isIdempotentTransient;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Прокручивает event loop пока reply не завершится либо не сработает client timeout.
|
enum class AwaitOutcome { FinishedClean, TransportError, ClientTimeout };
|
||||||
// true — reply finished (success или network error), false — сработал наш QTimer.
|
|
||||||
bool awaitReply(QNetworkReply *reply, int timeoutMs) {
|
// Прокручивает event loop пока reply не завершится (finished приходит и на успех,
|
||||||
|
// и сразу после errorOccurred) либо не сработает наш QTimer. Раньше здесь отдельно
|
||||||
|
// коннектился errorOccurred → loop.quit(), из-за чего транспортная ошибка считалась
|
||||||
|
// «успешным завершением» и цикл ретраев делал break на первой же Connection timed out.
|
||||||
|
// FinishedClean — reply завершился без ошибки;
|
||||||
|
// TransportError — reply завершился, но reply->error() != NoError;
|
||||||
|
// ClientTimeout — наш таймер сработал раньше, reply ещё в полёте (нужен abort()).
|
||||||
|
AwaitOutcome awaitReply(QNetworkReply *reply, int timeoutMs) {
|
||||||
QEventLoop loop;
|
QEventLoop loop;
|
||||||
QTimer timer;
|
QTimer timer;
|
||||||
timer.setSingleShot(true);
|
timer.setSingleShot(true);
|
||||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||||
QObject::connect(reply, &QNetworkReply::errorOccurred, &loop,
|
|
||||||
[&loop](QNetworkReply::NetworkError) { loop.quit(); });
|
|
||||||
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
|
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||||
timer.start(timeoutMs);
|
timer.start(timeoutMs);
|
||||||
loop.exec();
|
loop.exec();
|
||||||
return timer.isActive();
|
if (!timer.isActive()) return AwaitOutcome::ClientTimeout;
|
||||||
|
return reply->error() == QNetworkReply::NoError
|
||||||
|
? AwaitOutcome::FinishedClean
|
||||||
|
: AwaitOutcome::TransportError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// На client timeout повторяем запрос ещё 2 раза (всего до 3 попыток).
|
// Ретраим при (а) нашем client-timeout и (б) ТРАНЗИЕНТНЫХ транспортных ошибках.
|
||||||
// На HTTP/network ошибках не ретраим — они детерминированы (401/409/refused/etc).
|
// Для неидемпотентных запросов (POST) — только connect-phase сбои (соединение не
|
||||||
constexpr int kMaxRequestAttempts = 3;
|
// установилось ⇒ сервер запрос не видел ⇒ дубля быть не может); для идемпотентных
|
||||||
|
// (GET/PATCH/DELETE/аплоады) набор шире (см. NetworkErrorUtils.h). Детерминированные
|
||||||
|
// HTTP-ошибки (401/404/409, SSL) НЕ ретраим — их разбирает вызывающий.
|
||||||
|
constexpr int kMaxRequestAttempts = 5;
|
||||||
constexpr int kDefaultTimeoutMs = 30000;
|
constexpr int kDefaultTimeoutMs = 30000;
|
||||||
constexpr int kDumpTimeoutMs = 120000;
|
constexpr int kDumpTimeoutMs = 300000; // архив с БД по медленному каналу
|
||||||
// Мониторинг — best-effort: одна попытка, короткий таймаут. Failure не должен
|
// Мониторинг — best-effort: одна попытка, короткий таймаут. Failure не должен
|
||||||
// блокировать потоки и накапливать QThread'ы при недоступном сервере.
|
// блокировать потоки и накапливать QThread'ы при недоступном сервере.
|
||||||
constexpr int kMonitoringTimeoutMs = 10000;
|
constexpr int kMonitoringTimeoutMs = 10000;
|
||||||
|
constexpr int kBackoffBaseMs = 1000;
|
||||||
|
constexpr int kBackoffMaxMs = 4000;
|
||||||
|
|
||||||
|
// Пауза между попытками: 1с, 2с, 4с (cap). Через QEventLoop, а не QThread::sleep —
|
||||||
|
// event loop потока остаётся живым (abort/deleteLater/queued-слоты продолжают идти).
|
||||||
|
void backoffSleep(int attempt) {
|
||||||
|
const int ms = qMin(kBackoffBaseMs << (attempt - 1), kBackoffMaxMs);
|
||||||
|
QEventLoop loop;
|
||||||
|
QTimer::singleShot(ms, &loop, &QEventLoop::quit);
|
||||||
|
loop.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SendOutcome {
|
||||||
|
QNetworkReply *reply = nullptr; // последний reply; вызывающий парсит и делает deleteLater
|
||||||
|
bool clientTimedOut = false; // финальная попытка упёрлась в наш QTimer (reply aborted)
|
||||||
|
QString timeoutDetail; // describeClientTimeout последнего таймаута
|
||||||
|
};
|
||||||
|
|
||||||
|
// Выполняет issueRequest() с таймаутом и ретраями транзиентных сбоев + бэкоффом.
|
||||||
|
// issueRequest должен КАЖДЫЙ раз создавать новый reply (и, для multipart, новый
|
||||||
|
// QHttpMultiPart — он потребляется за попытку). Возвращает последний reply, чтобы
|
||||||
|
// вызывающий сохранил свой разбор ответа/ошибки.
|
||||||
|
SendOutcome sendWithRetry(const std::function<QNetworkReply*()> &issueRequest,
|
||||||
|
int timeoutMs, const QString &path,
|
||||||
|
const char *method, bool idempotent) {
|
||||||
|
QNetworkReply *reply = nullptr;
|
||||||
|
QString timeoutDetail;
|
||||||
|
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
||||||
|
if (reply) { reply->deleteLater(); reply = nullptr; }
|
||||||
|
reply = issueRequest();
|
||||||
|
switch (awaitReply(reply, timeoutMs)) {
|
||||||
|
case AwaitOutcome::FinishedClean:
|
||||||
|
return { reply, false, {} };
|
||||||
|
case AwaitOutcome::TransportError: {
|
||||||
|
const QNetworkReply::NetworkError err = reply->error();
|
||||||
|
const bool transient = isConnectPhaseTransient(err)
|
||||||
|
|| (idempotent && isIdempotentTransient(err));
|
||||||
|
if (transient && attempt < kMaxRequestAttempts) {
|
||||||
|
qCritical().noquote() << "Network transient error (" << method << ") attempt"
|
||||||
|
<< attempt << "/" << kMaxRequestAttempts << ":"
|
||||||
|
<< describeNetError(reply, path) << "— retrying after backoff";
|
||||||
|
backoffSleep(attempt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return { reply, false, {} }; // детерминированная ошибка или попытки исчерпаны
|
||||||
|
}
|
||||||
|
case AwaitOutcome::ClientTimeout:
|
||||||
|
timeoutDetail = describeClientTimeout(reply, path);
|
||||||
|
qCritical().noquote() << "Network timeout (" << method << ") attempt" << attempt
|
||||||
|
<< "/" << kMaxRequestAttempts << ":" << timeoutDetail;
|
||||||
|
reply->abort();
|
||||||
|
if (attempt < kMaxRequestAttempts) { backoffSleep(attempt); continue; }
|
||||||
|
return { reply, true, timeoutDetail };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { reply, true, timeoutDetail }; // недостижимо
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
NetworkService::NetworkService(QObject *parent) : QObject(parent) {
|
NetworkService::NetworkService(QObject *parent) : QObject(parent) {
|
||||||
@ -116,28 +188,24 @@ void addFormMetadata(QHttpMultiPart *multiPart, const QByteArray &fieldValue) {
|
|||||||
multiPart->append(jsonPart);
|
multiPart->append(jsonPart);
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonValue NetworkService::post(QHttpMultiPart *multiPart, const QString &path) {
|
QJsonValue NetworkService::post(const std::function<QHttpMultiPart*()> &buildMultiPart,
|
||||||
|
const QString &path, bool idempotent) {
|
||||||
const QUrl url(path);
|
const QUrl url(path);
|
||||||
const QNetworkRequest request(url);
|
const QNetworkRequest request(url);
|
||||||
|
|
||||||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
const SendOutcome outcome = sendWithRetry(
|
||||||
multiPart->setParent(reply);
|
[&] {
|
||||||
|
QHttpMultiPart *multiPart = buildMultiPart();
|
||||||
QEventLoop loop;
|
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||||||
QTimer timeout;
|
multiPart->setParent(reply);
|
||||||
timeout.setSingleShot(true);
|
return reply;
|
||||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
},
|
||||||
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
|
kDefaultTimeoutMs, path, "POST(mp)", idempotent);
|
||||||
loop.quit();
|
QNetworkReply *reply = outcome.reply;
|
||||||
});
|
|
||||||
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
|
|
||||||
timeout.start(30000);
|
|
||||||
loop.exec();
|
|
||||||
|
|
||||||
QJsonValue result;
|
QJsonValue result;
|
||||||
if (!timeout.isActive()) {
|
if (outcome.clientTimedOut) {
|
||||||
qCritical().noquote() << "Network timeout:" << describeClientTimeout(reply, path);
|
qCritical().noquote() << "Network timeout:" << outcome.timeoutDetail;
|
||||||
reply->abort();
|
|
||||||
} else if (reply->error() == QNetworkReply::NoError) {
|
} else if (reply->error() == QNetworkReply::NoError) {
|
||||||
const QByteArray raw = reply->readAll();
|
const QByteArray raw = reply->readAll();
|
||||||
QJsonParseError err;
|
QJsonParseError err;
|
||||||
@ -167,18 +235,12 @@ QJsonArray NetworkService::get(const QString &path, const QMap<QString, QString>
|
|||||||
|
|
||||||
const QNetworkRequest request(url);
|
const QNetworkRequest request(url);
|
||||||
|
|
||||||
QNetworkReply *reply = nullptr;
|
const SendOutcome outcome = sendWithRetry(
|
||||||
bool timedOut = true;
|
[&] { return m_manager->get(request); },
|
||||||
QString lastTimeoutDetail;
|
kDefaultTimeoutMs, path, "GET", /*idempotent=*/true);
|
||||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
QNetworkReply *reply = outcome.reply;
|
||||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
const bool timedOut = outcome.clientTimedOut;
|
||||||
reply = m_manager->get(request);
|
const QString lastTimeoutDetail = outcome.timeoutDetail;
|
||||||
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
|
||||||
lastTimeoutDetail = describeClientTimeout(reply, path);
|
|
||||||
qCritical().noquote() << "Network timeout (GET) attempt" << attempt
|
|
||||||
<< "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail;
|
|
||||||
reply->abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonArray result;
|
QJsonArray result;
|
||||||
if (timedOut) {
|
if (timedOut) {
|
||||||
@ -245,13 +307,13 @@ void NetworkService::getPayments() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NetworkService::postAppStatus() {
|
void NetworkService::postAppStatus() {
|
||||||
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
const bool isSuccess = post([this] {
|
||||||
|
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||||
addFormMetadata(multiPart, m_json);
|
addFormMetadata(multiPart, m_json);
|
||||||
addFormField(multiPart, "token", m_token);
|
addFormField(multiPart, "token", m_token);
|
||||||
addFormField(multiPart, "androidUniqueName", m_deviceId);
|
addFormField(multiPart, "androidUniqueName", m_deviceId);
|
||||||
|
return multiPart;
|
||||||
const bool isSuccess = post(multiPart, m_urlAppStatusUpdate).toBool();
|
}, m_urlAppStatusUpdate, /*idempotent=*/false).toBool();
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
qCritical() << "Cant post data: postAppStatus";
|
qCritical() << "Cant post data: postAppStatus";
|
||||||
}
|
}
|
||||||
@ -271,18 +333,12 @@ QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body
|
|||||||
const QString reqBodyStr = QJsonDocument(body).toJson(QJsonDocument::Compact);
|
const QString reqBodyStr = QJsonDocument(body).toJson(QJsonDocument::Compact);
|
||||||
const QString shortPath = sanitizePath(path);
|
const QString shortPath = sanitizePath(path);
|
||||||
|
|
||||||
QNetworkReply *reply = nullptr;
|
const SendOutcome outcome = sendWithRetry(
|
||||||
bool timedOut = true;
|
[&] { return m_manager->post(request, bodyBytes); },
|
||||||
QString lastTimeoutDetail;
|
kDefaultTimeoutMs, path, "POST", /*idempotent=*/false);
|
||||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
QNetworkReply *reply = outcome.reply;
|
||||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
const bool timedOut = outcome.clientTimedOut;
|
||||||
reply = m_manager->post(request, bodyBytes);
|
const QString lastTimeoutDetail = outcome.timeoutDetail;
|
||||||
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
|
||||||
lastTimeoutDetail = describeClientTimeout(reply, path);
|
|
||||||
qCritical().noquote() << "Network timeout (POST) attempt" << attempt
|
|
||||||
<< "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail;
|
|
||||||
reply->abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonValue result;
|
QJsonValue result;
|
||||||
m_lastErrorSummary.clear();
|
m_lastErrorSummary.clear();
|
||||||
@ -515,11 +571,11 @@ void NetworkService::finishSetupWithDesktop(const QString &desktopId) {
|
|||||||
|
|
||||||
|
|
||||||
void NetworkService::postEventStatus() {
|
void NetworkService::postEventStatus() {
|
||||||
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
const bool isSuccess = post([this] {
|
||||||
|
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||||
addFormMetadata(multiPart, m_json);
|
addFormMetadata(multiPart, m_json);
|
||||||
|
return multiPart;
|
||||||
const bool isSuccess = post(multiPart, m_urlEventStatusUpdate).toBool();
|
}, m_urlEventStatusUpdate, /*idempotent=*/false).toBool();
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
qCritical() << "Cant post data: postEventStatus";
|
qCritical() << "Cant post data: postEventStatus";
|
||||||
}
|
}
|
||||||
@ -528,14 +584,14 @@ void NetworkService::postEventStatus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NetworkService::updateTransactionData() {
|
void NetworkService::updateTransactionData() {
|
||||||
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
const bool isSuccess = post([this] {
|
||||||
|
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||||
addFormMetadata(multiPart, m_json);
|
addFormMetadata(multiPart, m_json);
|
||||||
addFormField(multiPart, "token", m_token);
|
addFormField(multiPart, "token", m_token);
|
||||||
addFormField(multiPart, "androidUniqueName", m_deviceId);
|
addFormField(multiPart, "androidUniqueName", m_deviceId);
|
||||||
addFormField(multiPart, "lastNumbers", m_extraData["lastNumbers"].toString());
|
addFormField(multiPart, "lastNumbers", m_extraData["lastNumbers"].toString());
|
||||||
|
return multiPart;
|
||||||
const bool isSuccess = post(multiPart, m_urlTransactionUpdate).toBool();
|
}, m_urlTransactionUpdate, /*idempotent=*/false).toBool();
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
qCritical() << "Cant update transaction: updateTransactionData";
|
qCritical() << "Cant update transaction: updateTransactionData";
|
||||||
}
|
}
|
||||||
@ -544,14 +600,14 @@ void NetworkService::updateTransactionData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NetworkService::insertTransactionData() {
|
void NetworkService::insertTransactionData() {
|
||||||
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
const int id = post([this] {
|
||||||
|
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||||
addFormMetadata(multiPart, m_json);
|
addFormMetadata(multiPart, m_json);
|
||||||
addFormField(multiPart, "token", m_token);
|
addFormField(multiPart, "token", m_token);
|
||||||
addFormField(multiPart, "androidUniqueName", m_deviceId);
|
addFormField(multiPart, "androidUniqueName", m_deviceId);
|
||||||
addFormField(multiPart, "lastNumbers", m_extraData["lastNumbers"].toString());
|
addFormField(multiPart, "lastNumbers", m_extraData["lastNumbers"].toString());
|
||||||
|
return multiPart;
|
||||||
const int id = post(multiPart, m_urlTransactionInsert).toInt();
|
}, m_urlTransactionInsert, /*idempotent=*/false).toInt();
|
||||||
if (id > 0) {
|
if (id > 0) {
|
||||||
const int transactionId = m_extraData["transactionId"].toInt();
|
const int transactionId = m_extraData["transactionId"].toInt();
|
||||||
if (!TransactionDAO::updateExternalTransactionId(transactionId, id)) {
|
if (!TransactionDAO::updateExternalTransactionId(transactionId, id)) {
|
||||||
@ -572,18 +628,12 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod
|
|||||||
const QByteArray bodyBytes = QJsonDocument(body).toJson();
|
const QByteArray bodyBytes = QJsonDocument(body).toJson();
|
||||||
const QString reqBodyStr = QJsonDocument(body).toJson(QJsonDocument::Compact);
|
const QString reqBodyStr = QJsonDocument(body).toJson(QJsonDocument::Compact);
|
||||||
|
|
||||||
QNetworkReply *reply = nullptr;
|
const SendOutcome outcome = sendWithRetry(
|
||||||
bool timedOut = true;
|
[&] { return m_manager->sendCustomRequest(request, "PATCH", bodyBytes); },
|
||||||
QString lastTimeoutDetail;
|
kDefaultTimeoutMs, path, "PATCH", /*idempotent=*/true);
|
||||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
QNetworkReply *reply = outcome.reply;
|
||||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
const bool timedOut = outcome.clientTimedOut;
|
||||||
reply = m_manager->sendCustomRequest(request, "PATCH", bodyBytes);
|
const QString lastTimeoutDetail = outcome.timeoutDetail;
|
||||||
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
|
||||||
lastTimeoutDetail = describeClientTimeout(reply, path);
|
|
||||||
qCritical().noquote() << "Network timeout (PATCH) attempt" << attempt
|
|
||||||
<< "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail;
|
|
||||||
reply->abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonValue result;
|
QJsonValue result;
|
||||||
if (timedOut) {
|
if (timedOut) {
|
||||||
@ -657,18 +707,12 @@ QJsonValue NetworkService::deleteJson(const QString &path, bool withAuth) {
|
|||||||
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply *reply = nullptr;
|
const SendOutcome outcome = sendWithRetry(
|
||||||
bool timedOut = true;
|
[&] { return m_manager->deleteResource(request); },
|
||||||
QString lastTimeoutDetail;
|
kDefaultTimeoutMs, path, "DELETE", /*idempotent=*/true);
|
||||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
QNetworkReply *reply = outcome.reply;
|
||||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
const bool timedOut = outcome.clientTimedOut;
|
||||||
reply = m_manager->deleteResource(request);
|
const QString lastTimeoutDetail = outcome.timeoutDetail;
|
||||||
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
|
||||||
lastTimeoutDetail = describeClientTimeout(reply, path);
|
|
||||||
qCritical().noquote() << "Network timeout (DELETE) attempt" << attempt
|
|
||||||
<< "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail;
|
|
||||||
reply->abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonValue result;
|
QJsonValue result;
|
||||||
if (timedOut) {
|
if (timedOut) {
|
||||||
@ -704,23 +748,15 @@ QJsonValue NetworkService::getJson(const QString &path, bool withAuth) {
|
|||||||
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply *reply = nullptr;
|
const SendOutcome outcome = sendWithRetry(
|
||||||
bool timedOut = true;
|
[&] { return m_manager->get(request); },
|
||||||
QString lastTimeoutDetail;
|
kDefaultTimeoutMs, path, "getJson", /*idempotent=*/true);
|
||||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
QNetworkReply *reply = outcome.reply;
|
||||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
|
||||||
reply = m_manager->get(request);
|
|
||||||
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
|
||||||
lastTimeoutDetail = describeClientTimeout(reply, path);
|
|
||||||
qCritical().noquote() << "Network timeout (getJson) attempt" << attempt
|
|
||||||
<< "/" << kMaxRequestAttempts << ":" << lastTimeoutDetail;
|
|
||||||
reply->abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonValue result;
|
QJsonValue result;
|
||||||
if (timedOut) {
|
if (outcome.clientTimedOut) {
|
||||||
qCritical().noquote() << "Network timeout (getJson) after"
|
qCritical().noquote() << "Network timeout (getJson) after"
|
||||||
<< kMaxRequestAttempts << "attempts:" << lastTimeoutDetail;
|
<< kMaxRequestAttempts << "attempts:" << outcome.timeoutDetail;
|
||||||
result = QJsonValue::Undefined;
|
result = QJsonValue::Undefined;
|
||||||
} else if (reply->error() != QNetworkReply::NoError) {
|
} else if (reply->error() != QNetworkReply::NoError) {
|
||||||
qCritical().noquote() << "Network error (GET):" << describeNetError(reply, path)
|
qCritical().noquote() << "Network error (GET):" << describeNetError(reply, path)
|
||||||
@ -900,18 +936,12 @@ void NetworkService::syncBanksIfEmpty() {
|
|||||||
const QString path = QStringLiteral("https://api.brtservice.io/api/rest/info/");
|
const QString path = QStringLiteral("https://api.brtservice.io/api/rest/info/");
|
||||||
QNetworkRequest request{QUrl(path)};
|
QNetworkRequest request{QUrl(path)};
|
||||||
|
|
||||||
QNetworkReply *reply = nullptr;
|
const SendOutcome outcome = sendWithRetry(
|
||||||
bool timedOut = true;
|
[&] { return m_manager->get(request); },
|
||||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
kDefaultTimeoutMs, path, "syncBanksIfEmpty", /*idempotent=*/true);
|
||||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
QNetworkReply *reply = outcome.reply;
|
||||||
reply = m_manager->get(request);
|
|
||||||
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
|
||||||
qWarning().noquote() << "[syncBanksIfEmpty] timeout attempt" << attempt
|
|
||||||
<< "/" << kMaxRequestAttempts;
|
|
||||||
reply->abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timedOut) {
|
if (outcome.clientTimedOut) {
|
||||||
qWarning() << "[syncBanksIfEmpty] timeout after" << kMaxRequestAttempts << "attempts";
|
qWarning() << "[syncBanksIfEmpty] timeout after" << kMaxRequestAttempts << "attempts";
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
return;
|
return;
|
||||||
@ -1416,31 +1446,25 @@ void NetworkService::postDeviceScreenshot() {
|
|||||||
QNetworkRequest request(url);
|
QNetworkRequest request(url);
|
||||||
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
|
||||||
|
|
||||||
QNetworkReply *reply = nullptr;
|
const SendOutcome outcome = sendWithRetry(
|
||||||
bool timedOut = true;
|
[&] {
|
||||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
QHttpPart filePart;
|
||||||
|
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
|
||||||
|
filePart.setHeader(
|
||||||
|
QNetworkRequest::ContentDispositionHeader,
|
||||||
|
QStringLiteral("form-data; name=\"file\"; filename=\"%1.jpg\"").arg(m_deviceId)
|
||||||
|
);
|
||||||
|
filePart.setBody(m_json);
|
||||||
|
multiPart->append(filePart);
|
||||||
|
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||||||
|
multiPart->setParent(reply);
|
||||||
|
return reply;
|
||||||
|
},
|
||||||
|
kDefaultTimeoutMs, url.toString(), "postDeviceScreenshot", /*idempotent=*/true);
|
||||||
|
QNetworkReply *reply = outcome.reply;
|
||||||
|
|
||||||
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
if (outcome.clientTimedOut) {
|
||||||
QHttpPart filePart;
|
|
||||||
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
|
|
||||||
filePart.setHeader(
|
|
||||||
QNetworkRequest::ContentDispositionHeader,
|
|
||||||
QStringLiteral("form-data; name=\"file\"; filename=\"%1.jpg\"").arg(m_deviceId)
|
|
||||||
);
|
|
||||||
filePart.setBody(m_json);
|
|
||||||
multiPart->append(filePart);
|
|
||||||
|
|
||||||
reply = m_manager->post(request, multiPart);
|
|
||||||
multiPart->setParent(reply);
|
|
||||||
|
|
||||||
if (awaitReply(reply, kDefaultTimeoutMs)) { timedOut = false; break; }
|
|
||||||
qWarning() << "[NetworkService] postDeviceScreenshot timeout attempt"
|
|
||||||
<< attempt << "/" << kMaxRequestAttempts << "for device" << device.apiId;
|
|
||||||
reply->abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timedOut) {
|
|
||||||
qWarning() << "[NetworkService] postDeviceScreenshot timeout after"
|
qWarning() << "[NetworkService] postDeviceScreenshot timeout after"
|
||||||
<< kMaxRequestAttempts << "attempts for device" << device.apiId;
|
<< kMaxRequestAttempts << "attempts for device" << device.apiId;
|
||||||
} else if (reply->error() != QNetworkReply::NoError) {
|
} else if (reply->error() != QNetworkReply::NoError) {
|
||||||
@ -1679,7 +1703,7 @@ void NetworkService::postMonitoringLog(const QString &type, const QString &event
|
|||||||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||||||
multiPart->setParent(reply);
|
multiPart->setParent(reply);
|
||||||
|
|
||||||
if (!awaitReply(reply, kMonitoringTimeoutMs)) {
|
if (awaitReply(reply, kMonitoringTimeoutMs) == AwaitOutcome::ClientTimeout) {
|
||||||
qWarning() << "[NetworkService] postMonitoringLog timeout";
|
qWarning() << "[NetworkService] postMonitoringLog timeout";
|
||||||
reply->abort();
|
reply->abort();
|
||||||
} else if (reply->error() != QNetworkReply::NoError) {
|
} else if (reply->error() != QNetworkReply::NoError) {
|
||||||
@ -1703,59 +1727,60 @@ void NetworkService::postMonitoringDump(const QString &text, const QByteArray &a
|
|||||||
|
|
||||||
qDebug() << "[NetworkService] postMonitoringDump POST" << url.toString();
|
qDebug() << "[NetworkService] postMonitoringDump POST" << url.toString();
|
||||||
|
|
||||||
QNetworkReply *reply = nullptr;
|
const SendOutcome outcome = sendWithRetry(
|
||||||
bool timedOut = true;
|
[&] {
|
||||||
for (int attempt = 1; attempt <= kMaxRequestAttempts; ++attempt) {
|
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||||
if (reply) { reply->deleteLater(); reply = nullptr; }
|
addFormField(multiPart, "text", text);
|
||||||
|
if (!archive.isEmpty()) {
|
||||||
|
QHttpPart filePart;
|
||||||
|
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip");
|
||||||
|
filePart.setHeader(
|
||||||
|
QNetworkRequest::ContentDispositionHeader,
|
||||||
|
QStringLiteral("form-data; name=\"file\"; filename=\"%1\"").arg(archiveFilename)
|
||||||
|
);
|
||||||
|
filePart.setBody(archive);
|
||||||
|
multiPart->append(filePart);
|
||||||
|
}
|
||||||
|
if (!image.isEmpty()) {
|
||||||
|
QHttpPart imagePart;
|
||||||
|
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png");
|
||||||
|
imagePart.setHeader(
|
||||||
|
QNetworkRequest::ContentDispositionHeader,
|
||||||
|
QStringLiteral("form-data; name=\"image\"; filename=\"final_screen.png\"")
|
||||||
|
);
|
||||||
|
imagePart.setBody(image);
|
||||||
|
multiPart->append(imagePart);
|
||||||
|
}
|
||||||
|
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||||||
|
multiPart->setParent(reply);
|
||||||
|
return reply;
|
||||||
|
},
|
||||||
|
kDumpTimeoutMs, url.toString(), "postMonitoringDump", /*idempotent=*/true);
|
||||||
|
QNetworkReply *reply = outcome.reply;
|
||||||
|
|
||||||
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
bool ok = false;
|
||||||
addFormField(multiPart, "text", text);
|
QString detail;
|
||||||
if (!archive.isEmpty()) {
|
if (outcome.clientTimedOut) {
|
||||||
QHttpPart filePart;
|
detail = "timeout: " + outcome.timeoutDetail;
|
||||||
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/zip");
|
|
||||||
filePart.setHeader(
|
|
||||||
QNetworkRequest::ContentDispositionHeader,
|
|
||||||
QStringLiteral("form-data; name=\"file\"; filename=\"%1\"").arg(archiveFilename)
|
|
||||||
);
|
|
||||||
filePart.setBody(archive);
|
|
||||||
multiPart->append(filePart);
|
|
||||||
}
|
|
||||||
if (!image.isEmpty()) {
|
|
||||||
QHttpPart imagePart;
|
|
||||||
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png");
|
|
||||||
imagePart.setHeader(
|
|
||||||
QNetworkRequest::ContentDispositionHeader,
|
|
||||||
QStringLiteral("form-data; name=\"image\"; filename=\"final_screen.png\"")
|
|
||||||
);
|
|
||||||
imagePart.setBody(image);
|
|
||||||
multiPart->append(imagePart);
|
|
||||||
}
|
|
||||||
|
|
||||||
reply = m_manager->post(request, multiPart);
|
|
||||||
multiPart->setParent(reply);
|
|
||||||
|
|
||||||
if (awaitReply(reply, kDumpTimeoutMs)) { timedOut = false; break; }
|
|
||||||
qWarning() << "[NetworkService] postMonitoringDump timeout attempt"
|
|
||||||
<< attempt << "/" << kMaxRequestAttempts;
|
|
||||||
reply->abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timedOut) {
|
|
||||||
qWarning() << "[NetworkService] postMonitoringDump timeout after"
|
qWarning() << "[NetworkService] postMonitoringDump timeout after"
|
||||||
<< kMaxRequestAttempts << "attempts";
|
<< kMaxRequestAttempts << "attempts";
|
||||||
} else if (reply->error() != QNetworkReply::NoError) {
|
} else if (reply->error() != QNetworkReply::NoError) {
|
||||||
qWarning() << "[NetworkService] postMonitoringDump failed:" << reply->errorString()
|
detail = describeNetError(reply, url.toString());
|
||||||
|
qWarning() << "[NetworkService] postMonitoringDump failed:" << detail
|
||||||
<< "\n response body:" << reply->readAll();
|
<< "\n response body:" << reply->readAll();
|
||||||
} else {
|
} else {
|
||||||
const QByteArray raw = reply->readAll();
|
const QByteArray raw = reply->readAll();
|
||||||
const QJsonDocument doc = QJsonDocument::fromJson(raw);
|
const QJsonDocument doc = QJsonDocument::fromJson(raw);
|
||||||
if (doc.isObject() && !doc.object().value("success").toBool()) {
|
if (doc.isObject() && !doc.object().value("success").toBool()) {
|
||||||
|
detail = "сервер отклонил: " + QString::fromUtf8(raw).left(300);
|
||||||
qWarning() << "[NetworkService] postMonitoringDump rejected:" << raw;
|
qWarning() << "[NetworkService] postMonitoringDump rejected:" << raw;
|
||||||
} else {
|
} else {
|
||||||
|
ok = true;
|
||||||
qDebug() << "[NetworkService] postMonitoringDump OK:" << raw;
|
qDebug() << "[NetworkService] postMonitoringDump OK:" << raw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
|
emit monitoringDumpFinished(ok, detail, archiveFilename);
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkService::postMonitoringScreenshot(const QString &desktopId,
|
void NetworkService::postMonitoringScreenshot(const QString &desktopId,
|
||||||
@ -1785,7 +1810,7 @@ void NetworkService::postMonitoringScreenshot(const QString &desktopId,
|
|||||||
QNetworkReply *reply = m_manager->post(request, multiPart);
|
QNetworkReply *reply = m_manager->post(request, multiPart);
|
||||||
multiPart->setParent(reply);
|
multiPart->setParent(reply);
|
||||||
|
|
||||||
if (!awaitReply(reply, kMonitoringTimeoutMs)) {
|
if (awaitReply(reply, kMonitoringTimeoutMs) == AwaitOutcome::ClientTimeout) {
|
||||||
qWarning() << "[NetworkService] postMonitoringScreenshot timeout";
|
qWarning() << "[NetworkService] postMonitoringScreenshot timeout";
|
||||||
reply->abort();
|
reply->abort();
|
||||||
} else if (reply->error() != QNetworkReply::NoError) {
|
} else if (reply->error() != QNetworkReply::NoError) {
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
class QWidget;
|
class QWidget;
|
||||||
|
|
||||||
@ -145,6 +146,10 @@ signals:
|
|||||||
// healthy=false при kSyncFailureThreshold подряд неудачных тиков; healthy=true
|
// healthy=false при kSyncFailureThreshold подряд неудачных тиков; healthy=true
|
||||||
// при первом успехе после такой серии. consecutiveFailures даёт текущий счёт.
|
// при первом успехе после такой серии. consecutiveFailures даёт текущий счёт.
|
||||||
void syncHealthChanged(bool healthy, int consecutiveFailures);
|
void syncHealthChanged(bool healthy, int consecutiveFailures);
|
||||||
|
// Результат postMonitoringDump (включая ручную отправку логов из меню).
|
||||||
|
// archiveFilename — ключ, по которому инициатор отличает свой аплоад (синглтон
|
||||||
|
// общий). ok=false ⇒ detail содержит причину (таймаут/сетевая/HTTP-ошибка).
|
||||||
|
void monitoringDumpFinished(bool ok, QString detail, QString archiveFilename);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QByteArray m_json;
|
QByteArray m_json;
|
||||||
@ -207,7 +212,10 @@ private:
|
|||||||
|
|
||||||
QNetworkAccessManager *m_manager;
|
QNetworkAccessManager *m_manager;
|
||||||
|
|
||||||
QJsonValue post(QHttpMultiPart *multiPart, const QString &path);
|
// buildMultiPart вызывается на КАЖДУЮ попытку (multipart потребляется за запрос).
|
||||||
|
// idempotent=false ⇒ ретраим только connect-phase сбои (без риска двойной отправки).
|
||||||
|
QJsonValue post(const std::function<QHttpMultiPart*()> &buildMultiPart,
|
||||||
|
const QString &path, bool idempotent);
|
||||||
QJsonArray get(const QString &path, const QMap<QString, QString> ¶ms);
|
QJsonArray get(const QString &path, const QMap<QString, QString> ¶ms);
|
||||||
QJsonValue getJson(const QString &path, bool withAuth = false);
|
QJsonValue getJson(const QString &path, bool withAuth = false);
|
||||||
QJsonValue postJson(const QString &path, const QJsonObject &body, bool withAuth = false);
|
QJsonValue postJson(const QString &path, const QJsonObject &body, bool withAuth = false);
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include "dump/TaskDumpRecorder.h"
|
#include "dump/TaskDumpRecorder.h"
|
||||||
#include "dao/GeneralLogDAO.h"
|
#include "dao/GeneralLogDAO.h"
|
||||||
@ -284,6 +285,32 @@ void FileLogWindow::sendLogsToBackend() {
|
|||||||
|
|
||||||
qDebug() << "[sendLogsToBackend] dispatching to NetworkService::postMonitoringDump"
|
qDebug() << "[sendLogsToBackend] dispatching to NetworkService::postMonitoringDump"
|
||||||
<< "archive.size=" << archiveBytes.size() << "filename=" << archiveFilename;
|
<< "archive.size=" << archiveBytes.size() << "filename=" << archiveFilename;
|
||||||
|
|
||||||
|
// Подписываемся на результат ДО диспатча. svc живёт в потоке paymentThread —
|
||||||
|
// соединение автоматически становится QueuedConnection, лямбда исполнится в
|
||||||
|
// UI-потоке. Контекст = this ⇒ при закрытии окна Qt сам отключит слот (без
|
||||||
|
// обращения к висячему указателю). Фильтруем по своему archiveFilename:
|
||||||
|
// синглтон общий, параллельно могут уходить и другие дампы (TaskDumpRecorder).
|
||||||
|
auto handled = std::make_shared<bool>(false);
|
||||||
|
auto conn = std::make_shared<QMetaObject::Connection>();
|
||||||
|
*conn = connect(svc, &NetworkService::monitoringDumpFinished, this,
|
||||||
|
[this, conn, handled, expected = archiveFilename](
|
||||||
|
bool ok, const QString &detail, const QString &fn) {
|
||||||
|
if (fn != expected || *handled) return;
|
||||||
|
*handled = true;
|
||||||
|
QObject::disconnect(*conn);
|
||||||
|
m_sendBtn->setEnabled(true);
|
||||||
|
m_sendBtn->setText("Отправить");
|
||||||
|
if (ok) {
|
||||||
|
QMessageBox::information(this, "Готово", "Логи отправлены на бэкенд.");
|
||||||
|
} else {
|
||||||
|
QMessageBox::warning(this, "Ошибка",
|
||||||
|
"Не удалось отправить логи: " + detail);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_sendBtn->setText("Отправка...");
|
||||||
|
|
||||||
const bool dispatched = QMetaObject::invokeMethod(svc, "postMonitoringDump",
|
const bool dispatched = QMetaObject::invokeMethod(svc, "postMonitoringDump",
|
||||||
Qt::QueuedConnection,
|
Qt::QueuedConnection,
|
||||||
Q_ARG(QString, text),
|
Q_ARG(QString, text),
|
||||||
@ -291,15 +318,12 @@ void FileLogWindow::sendLogsToBackend() {
|
|||||||
Q_ARG(QByteArray, QByteArray()),
|
Q_ARG(QByteArray, QByteArray()),
|
||||||
Q_ARG(QString, archiveFilename));
|
Q_ARG(QString, archiveFilename));
|
||||||
|
|
||||||
m_sendBtn->setEnabled(true);
|
|
||||||
m_sendBtn->setText("Отправить");
|
|
||||||
|
|
||||||
if (!dispatched) {
|
if (!dispatched) {
|
||||||
|
*handled = true;
|
||||||
|
QObject::disconnect(*conn);
|
||||||
|
m_sendBtn->setEnabled(true);
|
||||||
|
m_sendBtn->setText("Отправить");
|
||||||
QMessageBox::warning(this, "Ошибка",
|
QMessageBox::warning(this, "Ошибка",
|
||||||
"Не удалось поставить отправку в очередь NetworkService.");
|
"Не удалось поставить отправку в очередь NetworkService.");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QMessageBox::information(this, "Готово",
|
|
||||||
"Логи поставлены в очередь на отправку на бэкенд.");
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user