diff options
author | Uwe Klotz <uklotz@mixxx.org> | 2021-01-10 16:37:38 +0100 |
---|---|---|
committer | Uwe Klotz <uklotz@mixxx.org> | 2021-01-10 16:37:38 +0100 |
commit | 0d974db280f2eaf969a79d34080051f683d9c6fa (patch) | |
tree | daa064812d027267c4491d05c7e6469400d11739 /src/network | |
parent | aef1e1e2a96cbf922f8364d83c33e37c0168168d (diff) | |
parent | 39465acc94e51acbdff636f63264a4cc4023531a (diff) |
Merge branch '2.3' of git@github.com:mixxxdj/mixxx.git into main
# Conflicts:
# src/preferences/dialog/dlgprefsound.cpp
# src/soundio/soundmanagerconfig.h
# src/track/cueinfo.cpp
# src/track/cueinfo.h
Diffstat (limited to 'src/network')
-rw-r--r-- | src/network/jsonwebtask.cpp | 152 | ||||
-rw-r--r-- | src/network/jsonwebtask.h | 51 | ||||
-rw-r--r-- | src/network/networktask.cpp | 83 | ||||
-rw-r--r-- | src/network/networktask.h | 96 | ||||
-rw-r--r-- | src/network/webtask.cpp | 300 | ||||
-rw-r--r-- | src/network/webtask.h | 221 |
6 files changed, 582 insertions, 321 deletions
diff --git a/src/network/jsonwebtask.cpp b/src/network/jsonwebtask.cpp index 0c141c0394..961d3a0543 100644 --- a/src/network/jsonwebtask.cpp +++ b/src/network/jsonwebtask.cpp @@ -29,64 +29,55 @@ const QString JSON_CONTENT_TYPE = "application/json"; const QMimeType JSON_MIME_TYPE = QMimeDatabase().mimeTypeForName(JSON_CONTENT_TYPE); -QMimeType readContentType( - const QNetworkReply* reply) { - DEBUG_ASSERT(reply); - const QVariant contentTypeHeader = reply->header(QNetworkRequest::ContentTypeHeader); - if (!contentTypeHeader.isValid() || contentTypeHeader.isNull()) { - kLogger.warning() - << "Missing content type header"; - return QMimeType(); - } - const QString contentTypeString = contentTypeHeader.toString(); - const QString contentTypeWithoutParams = contentTypeString.left(contentTypeString.indexOf(';')); - const QMimeType contentType = QMimeDatabase().mimeTypeForName(contentTypeWithoutParams); - if (!contentType.isValid()) { - kLogger.warning() - << "Unknown content type" - << contentTypeWithoutParams; - } - return contentType; -} - -bool readJsonContent( +/// If parsing fails the functions returns std::nullopt and optionally +/// the response content in pInvalidResponseContent for further processing. +std::optional<QJsonDocument> readJsonContent( QNetworkReply* reply, - QJsonDocument* jsonContent) { + std::pair<QMimeType, QByteArray>* pInvalidResponseContent = nullptr) { DEBUG_ASSERT(reply); - DEBUG_ASSERT(jsonContent); DEBUG_ASSERT(JSON_MIME_TYPE.isValid()); - const auto contentType = readContentType(reply); + auto contentType = WebTask::readContentType(*reply); + auto optContentData = WebTask::readContentData(reply); if (contentType != JSON_MIME_TYPE) { - kLogger.warning() - << "Unexpected content type" - << contentType; - return false; + kLogger.debug() + << "Received content type" + << contentType + << "instead of" + << JSON_MIME_TYPE; + if (pInvalidResponseContent) { + *pInvalidResponseContent = std::make_pair( + std::move(contentType), + optContentData.value_or(QByteArray{})); + } + return std::nullopt; } - QByteArray jsonData = reply->readAll(); - if (jsonData.isEmpty()) { - kLogger.warning() - << "Empty reply"; - return false; + if (!optContentData || optContentData->isEmpty()) { + kLogger.debug() << "Empty JSON network reply"; + return QJsonDocument{}; } QJsonParseError parseError; - const auto jsonDoc = QJsonDocument::fromJson( - jsonData, + auto jsonDocument = QJsonDocument::fromJson( + *optContentData, &parseError); // QJsonDocument::fromJson() returns a non-null document // if parsing succeeds and otherwise null on error. The // parse error must only be evaluated if the returned // document is null! - if (jsonDoc.isNull() && + if (jsonDocument.isNull() && parseError.error != QJsonParseError::NoError) { kLogger.warning() << "Failed to parse JSON data:" << parseError.errorString() << "at offset" << parseError.offset; - return false; + if (pInvalidResponseContent) { + *pInvalidResponseContent = std::make_pair( + std::move(contentType), + std::move(*optContentData)); + } + return std::nullopt; } - *jsonContent = jsonDoc; - return true; + return jsonDocument; } // TODO: Allow to customize headers and attributes? @@ -110,43 +101,39 @@ QNetworkRequest newRequest( QDebug operator<<(QDebug dbg, const JsonWebResponse& arg) { return dbg - << "CustomWebResponse{" - << static_cast<const WebResponse&>(arg) - << arg.content - << '}'; + << "WebResponseWithContent{" + << arg.m_response + << arg.m_content + << '}'; } JsonWebTask::JsonWebTask( QNetworkAccessManager* networkAccessManager, - const QUrl& baseUrl, + QUrl baseUrl, JsonWebRequest&& request, QObject* parent) : WebTask(networkAccessManager, parent), - m_baseUrl(baseUrl), - m_request(std::move(request)) { + m_baseUrl(std::move(baseUrl)), + m_request(request) { std::call_once(registerMetaTypesOnceFlag, registerMetaTypesOnce); DEBUG_ASSERT(!m_baseUrl.isEmpty()); } void JsonWebTask::onFinished( - JsonWebResponse&& response) { + const JsonWebResponse& jsonResponse) { kLogger.info() << this - << "Response received" - << response.replyUrl - << response.statusCode - << response.content; + << "Received JSON response" + << jsonResponse; deleteLater(); } void JsonWebTask::onFinishedCustom( - CustomWebResponse&& response) { + const WebResponseWithContent& customResponse) { kLogger.info() << this - << "Custom response received" - << response.replyUrl - << response.statusCode - << response.content; + << "Received custom response" + << customResponse; deleteLater(); } @@ -230,12 +217,24 @@ QNetworkReply* JsonWebTask::doStartNetworkRequest( DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); DEBUG_ASSERT(networkAccessManager); - DEBUG_ASSERT(m_baseUrl.isValid()); + VERIFY_OR_DEBUG_ASSERT(m_baseUrl.isValid()) { + kLogger.warning() << "Invalid base URL" << m_baseUrl; + return nullptr; + } QUrl url = m_baseUrl; url.setPath(m_request.path); + VERIFY_OR_DEBUG_ASSERT(url.isValid()) { + kLogger.warning() << "Invalid request path" << m_request.path; + return nullptr; + } if (!m_request.query.isEmpty()) { url.setQuery(m_request.query); + VERIFY_OR_DEBUG_ASSERT(url.isValid()) { + kLogger.warning() << "Invalid query string" << m_request.query.toString(); + return nullptr; + } } + // Already validated while composing (see above) DEBUG_ASSERT(url.isValid()); return sendNetworkRequest( @@ -248,26 +247,31 @@ QNetworkReply* JsonWebTask::doStartNetworkRequest( void JsonWebTask::doNetworkReplyFinished( QNetworkReply* finishedNetworkReply, HttpStatusCode statusCode) { - QJsonDocument content; - if (statusCode != kHttpStatusCodeInvalid && - finishedNetworkReply->bytesAvailable() > 0 && - !readJsonContent(finishedNetworkReply, &content)) { - onFinishedCustom(CustomWebResponse{ - WebResponse{ - finishedNetworkReply->url(), - statusCode}, - finishedNetworkReply->readAll()}); - } else { - onFinished(JsonWebResponse{ - WebResponse{ - finishedNetworkReply->url(), - statusCode}, - std::move(content)}); + DEBUG_ASSERT(finishedNetworkReply); + std::optional<QJsonDocument> optJsonContent; + auto webResponse = WebResponse{ + finishedNetworkReply->url(), + finishedNetworkReply->request().url(), + statusCode}; + if (statusCode != kHttpStatusCodeInvalid) { + std::pair<QMimeType, QByteArray> contentTypeAndBytes; + optJsonContent = readJsonContent(finishedNetworkReply, &contentTypeAndBytes); + if (!optJsonContent) { + // Failed to read JSON content + onFinishedCustom(WebResponseWithContent{ + std::move(webResponse), + std::move(contentTypeAndBytes.first), + std::move(contentTypeAndBytes.second)}); + return; + } } + onFinished(JsonWebResponse{ + std::move(webResponse), + optJsonContent.value_or(QJsonDocument{})}); } void JsonWebTask::emitFailed( - network::JsonWebResponse&& response) { + const network::JsonWebResponse& response) { VERIFY_OR_DEBUG_ASSERT( isSignalFuncConnected(&JsonWebTask::failed)) { kLogger.warning() @@ -277,7 +281,7 @@ void JsonWebTask::emitFailed( deleteLater(); return; } - emit failed(std::move(response)); + emit failed(response); } } // namespace network diff --git a/src/network/jsonwebtask.h b/src/network/jsonwebtask.h index 544fd97760..916be0cc99 100644 --- a/src/network/jsonwebtask.h +++ b/src/network/jsonwebtask.h @@ -25,28 +25,45 @@ struct JsonWebRequest final { QJsonDocument content; }; -struct JsonWebResponse : public WebResponse { +class JsonWebResponse final { public: static void registerMetaType(); JsonWebResponse() = default; JsonWebResponse( - WebResponse response, - QJsonDocument content) - : WebResponse(std::move(response)), - content(std::move(content)) { + WebResponse&& response, + QJsonDocument&& content) + : m_response(response), + m_content(content) { } JsonWebResponse(const JsonWebResponse&) = default; JsonWebResponse(JsonWebResponse&&) = default; - ~JsonWebResponse() override = default; JsonWebResponse& operator=(const JsonWebResponse&) = default; JsonWebResponse& operator=(JsonWebResponse&&) = default; - QJsonDocument content; -}; + bool isStatusCodeSuccess() const { + return m_response.isStatusCodeSuccess(); + } -QDebug operator<<(QDebug dbg, const JsonWebResponse& arg); + HttpStatusCode statusCode() const { + return m_response.statusCode(); + } + + const QUrl& replyUrl() const { + return m_response.replyUrl(); + } + + const QJsonDocument& content() const { + return m_content; + } + + friend QDebug operator<<(QDebug dbg, const JsonWebResponse& arg); + + private: + WebResponse m_response; + QJsonDocument m_content; +}; class JsonWebTask : public WebTask { Q_OBJECT @@ -54,7 +71,7 @@ class JsonWebTask : public WebTask { public: JsonWebTask( QNetworkAccessManager* networkAccessManager, - const QUrl& baseUrl, + QUrl baseUrl, JsonWebRequest&& request, QObject* parent = nullptr); ~JsonWebTask() override = default; @@ -72,16 +89,18 @@ class JsonWebTask : public WebTask { const QJsonDocument& content); void emitFailed( - network::JsonWebResponse&& response); + const network::JsonWebResponse& response); private: - // Handle the response and ensure that the task eventually - // gets deleted. The default implementation discards the - // response and deletes the task. + /// Handle the response and ensure that the task eventually + /// gets deleted. + /// + /// Could be overridden by derived classes. The default + /// implementation discards the response and deletes the task. virtual void onFinished( - JsonWebResponse&& response); + const JsonWebResponse& jsonResponse); virtual void onFinishedCustom( - CustomWebResponse&& response); + const WebResponseWithContent& customResponse); QNetworkReply* doStartNetworkRequest( QNetworkAccessManager* networkAccessManager, diff --git a/src/network/networktask.cpp b/src/network/networktask.cpp new file mode 100644 index 0000000000..4aac5191bc --- /dev/null +++ b/src/network/networktask.cpp @@ -0,0 +1,83 @@ +#include "network/networktask.h" + +#include "moc_networktask.cpp" +#include "util/counter.h" +#include "util/logger.h" +#include "util/thread_affinity.h" + +namespace mixxx { + +namespace network { + +namespace { + +const Logger kLogger("mixxx::network::NetworkTask"); + +// count = even number (ctor + dtor) +// sum = 0 (no memory leaks) +Counter s_instanceCounter(QStringLiteral("mixxx::network::NetworkTask")); + +} // anonymous namespace + +NetworkTask::NetworkTask( + QNetworkAccessManager* networkAccessManager, + QObject* parent) + : QObject(parent), + m_networkAccessManagerWeakPtr(networkAccessManager) { + s_instanceCounter.increment(1); +} + +NetworkTask::~NetworkTask() { + s_instanceCounter.increment(-1); +} + +void NetworkTask::invokeStart(int timeoutMillis) { + QMetaObject::invokeMethod( + this, +#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) + "slotStart", + Qt::AutoConnection, + Q_ARG(int, timeoutMillis) +#else + [this, timeoutMillis] { + this->slotStart(timeoutMillis); + } +#endif + ); +} + +void NetworkTask::invokeAbort() { + QMetaObject::invokeMethod( + this, +#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) + "slotAbort" +#else + [this] { + this->slotAbort(); + } +#endif + ); +} + +void NetworkTask::abort() { + DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); + slotAbort(); +} + +void NetworkTask::emitAborted( + const QUrl& requestUrl) { + VERIFY_OR_DEBUG_ASSERT( + isSignalFuncConnected(&NetworkTask::aborted)) { + kLogger.warning() + << this + << "Unhandled abort signal" + << requestUrl; + deleteLater(); + return; + } + emit aborted(requestUrl); +} + +} // namespace network + +} // namespace mixxx diff --git a/src/network/networktask.h b/src/network/networktask.h new file mode 100644 index 0000000000..410b977977 --- /dev/null +++ b/src/network/networktask.h @@ -0,0 +1,96 @@ +#pragma once + +#include <QMetaMethod> +#include <QNetworkAccessManager> +#include <QNetworkReply> +#include <QUrl> + +#include "util/assert.h" +#include "util/qt.h" + +namespace mixxx { + +namespace network { + +/// A transient task for performing a single generic network request +/// asynchronously. +/// +/// The results are transmitted by emitting signals. At least one +/// of the signal receivers is responsible for destroying the task +/// by invoking QObject::deleteLater(). If no receiver is connected +/// at the time the finalization signal is emitted then the task +/// will destroy itself. +class NetworkTask : public QObject { + Q_OBJECT + + protected: + explicit NetworkTask( + QNetworkAccessManager* networkAccessManager, + QObject* parent = nullptr); + virtual ~NetworkTask(); + + /// Cancel the task by aborting the pending network request. + /// + /// This function is NOT thread-safe and must only be called from + /// the event loop thread. + void abort(); + + // Required to allow derived classes to invoke abort() + // on other instances of this class. + void static abortThis(NetworkTask* pThis) { + DEBUG_ASSERT(pThis); + pThis->abort(); + } + + template<typename S> + bool isSignalFuncConnected( + S signalFunc) { + const QMetaMethod signal = QMetaMethod::fromSignal(signalFunc); + return isSignalConnected(signal); + } + + /// Send an aborted signal with the optional request URL if available. + void emitAborted( + const QUrl& requestUrl = QUrl{}); + + /// All member variables must only be accessed from + /// the event loop thread!! + const SafeQPointer<QNetworkAccessManager> m_networkAccessManagerWeakPtr; + + public: + static constexpr int kNoTimeout = 0; + + /// Start a new task by sending a network request. + /// + /// timeoutMillis <= 0: No timeout (unlimited) + /// timeoutMillis > 0: Implicitly aborted after timeout expired + /// + /// This function is thread-safe and could be invoked from any thread. + void invokeStart( + int timeoutMillis = kNoTimeout); + + /// Cancel the task by aborting the pending network request. + /// + /// This function is thread-safe and could be invoked from any thread. + void invokeAbort(); + + public slots: + virtual void slotStart( + int timeoutMillis) = 0; + virtual void slotAbort() = 0; + + signals: + /// The receiver is responsible for deleting the task in the + /// corresponding slot handler!! Otherwise the task will remain + /// in memory as a dysfunctional zombie until its parent object + /// is finally deleted. If no receiver is connected the task + /// will be deleted implicitly. + + /// Client-side abort + void aborted( + const QUrl& requestUrl); +}; + +} // namespace network + +} // namespace mixxx diff --git a/src/network/webtask.cpp b/src/network/webtask.cpp index 02d2f037b6..30d910b800 100644 --- a/src/network/webtask.cpp +++ b/src/network/webtask.cpp @@ -1,11 +1,10 @@ #include "network/webtask.h" +#include <QMimeDatabase> #include <QTimerEvent> #include <mutex> // std::once_flag #include "moc_webtask.cpp" -#include "util/assert.h" -#include "util/counter.h" #include "util/logger.h" #include "util/thread_affinity.h" @@ -19,26 +18,32 @@ const Logger kLogger("mixxx::network::WebTask"); constexpr int kInvalidTimerId = -1; -// count = even number (ctor + dtor) -// sum = 0 (no memory leaks) -Counter s_instanceCounter(QStringLiteral("mixxx::network::WebTask")); - std::once_flag registerMetaTypesOnceFlag; void registerMetaTypesOnce() { WebResponse::registerMetaType(); - CustomWebResponse::registerMetaType(); + WebResponseWithContent::registerMetaType(); } int readStatusCode( - const QNetworkReply* networkReply) { + const QNetworkReply& networkReply) { const QVariant statusCodeAttr = - networkReply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + networkReply.attribute(QNetworkRequest::HttpStatusCodeAttribute); + if (!statusCodeAttr.isValid()) { + // No status code available + return kHttpStatusCodeInvalid; + } + VERIFY_OR_DEBUG_ASSERT(statusCodeAttr.canConvert<int>()) { + kLogger.warning() + << "Invalid status code attribute" + << statusCodeAttr; + return kHttpStatusCodeInvalid; + } bool statusCodeValid = false; const int statusCode = statusCodeAttr.toInt(&statusCodeValid); VERIFY_OR_DEBUG_ASSERT(statusCodeValid && HttpStatusCode_isValid(statusCode)) { kLogger.warning() - << "Invalid or missing status code attribute" + << "Failed to read status code attribute" << statusCodeAttr; return kHttpStatusCodeInvalid; } @@ -54,123 +59,138 @@ int readStatusCode( QDebug operator<<(QDebug dbg, const WebResponse& arg) { return dbg << "WebResponse{" - << arg.replyUrl - << arg.statusCode + << arg.m_requestUrl + << arg.m_replyUrl + << arg.m_statusCode << '}'; } -/*static*/ void CustomWebResponse::registerMetaType() { - qRegisterMetaType<CustomWebResponse>("mixxx::network::CustomWebResponse"); +/*static*/ void WebResponseWithContent::registerMetaType() { + qRegisterMetaType<WebResponseWithContent>("mixxx::network::WebResponseWithContent"); } -QDebug operator<<(QDebug dbg, const CustomWebResponse& arg) { +QDebug operator<<(QDebug dbg, const WebResponseWithContent& arg) { return dbg - << "CustomWebResponse{" - << static_cast<const WebResponse&>(arg) - << arg.content + << "WebResponseWithContent{" + << arg.m_response + << arg.m_contentType + << arg.m_contentData << '}'; } +//static +QMimeType WebTask::readContentType( + const QNetworkReply& reply) { + const QVariant contentTypeHeader = reply.header(QNetworkRequest::ContentTypeHeader); + if (!contentTypeHeader.isValid() || contentTypeHeader.isNull()) { + if (reply.isReadable() && reply.bytesAvailable() > 0) { + kLogger.warning() + << "Missing content type header"; + } + return QMimeType(); + } + const QString contentTypeString = contentTypeHeader.toString(); + const QString contentTypeWithoutParams = contentTypeString.left(contentTypeString.indexOf(';')); + const QMimeType contentType = QMimeDatabase().mimeTypeForName(contentTypeWithoutParams); + if (!contentType.isValid()) { + kLogger.warning() + << "Unknown content type" + << contentTypeWithoutParams; + } + return contentType; +} + +//static +std::optional<QByteArray> WebTask::readContentData( + QNetworkReply* reply) { + if (!reply->isReadable()) { + return std::nullopt; + } + return reply->readAll(); +} + WebTask::WebTask( QNetworkAccessManager* networkAccessManager, QObject* parent) - : QObject(parent), - m_networkAccessManagerWeakPtr(networkAccessManager), - m_timeoutTimerId(kInvalidTimerId), - m_state(State::Idle) { + : NetworkTask(networkAccessManager, parent), + m_state(State::Idle), + m_timeoutTimerId(kInvalidTimerId) { std::call_once(registerMetaTypesOnceFlag, registerMetaTypesOnce); - s_instanceCounter.increment(1); -} - -WebTask::~WebTask() { - s_instanceCounter.increment(-1); } void WebTask::onNetworkError( - QUrl&& requestUrl, QNetworkReply::NetworkError errorCode, - QString&& errorString, - QByteArray&& errorContent) { + const QString& errorString, + const WebResponseWithContent& responseWithContent) { + DEBUG_ASSERT(m_state == State::Pending); DEBUG_ASSERT(m_timeoutTimerId == kInvalidTimerId); - VERIFY_OR_DEBUG_ASSERT(m_state == State::Pending) { - return; - } DEBUG_ASSERT(errorCode != QNetworkReply::NoError); switch (errorCode) { case QNetworkReply::OperationCanceledError: + // Client-side abort or timeout m_state = State::Aborted; break; case QNetworkReply::TimeoutError: + // Network or server-side timeout m_state = State::TimedOut; break; default: m_state = State::Failed; } + DEBUG_ASSERT(hasTerminated()); + + if (m_state == State::Aborted) { + emitAborted(responseWithContent.requestUrl()); + } else { + emitNetworkError( + errorCode, + errorString, + responseWithContent); + } +} +void WebTask::emitNetworkError( + QNetworkReply::NetworkError errorCode, + const QString& errorString, + const WebResponseWithContent& responseWithContent) { VERIFY_OR_DEBUG_ASSERT( isSignalFuncConnected(&WebTask::networkError)) { kLogger.warning() << this - << "Unhandled network error:" - << requestUrl + << "Unhandled network error signal" << errorCode << errorString - << errorContent; + << responseWithContent; deleteLater(); return; } emit networkError( - std::move(requestUrl), errorCode, - std::move(errorString), - std::move(errorContent)); -} - -void WebTask::invokeStart(int timeoutMillis) { - QMetaObject::invokeMethod( - this, -#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) - "slotStart", - Qt::AutoConnection, - Q_ARG(int, timeoutMillis) -#else - [this, timeoutMillis] { - this->slotStart(timeoutMillis); - } -#endif - ); -} - -void WebTask::invokeAbort() { - QMetaObject::invokeMethod( - this, -#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) - "slotAbort" -#else - [this] { - this->slotAbort(); - } -#endif - ); + errorString, + responseWithContent); } void WebTask::slotStart(int timeoutMillis) { DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); - VERIFY_OR_DEBUG_ASSERT(m_state != State::Pending) { + if (m_state == State::Pending) { + kLogger.warning() + << "Task is still busy and cannot be started again"; return; } + + // Reset state DEBUG_ASSERT(!m_pendingNetworkReplyWeakPtr); + DEBUG_ASSERT(m_timeoutTimerId == kInvalidTimerId); m_state = State::Idle; auto* const pNetworkAccessManager = m_networkAccessManagerWeakPtr.data(); VERIFY_OR_DEBUG_ASSERT(pNetworkAccessManager) { m_state = State::Pending; onNetworkError( - QUrl(), QNetworkReply::NetworkSessionFailedError, tr("No network access"), - QByteArray()); + WebResponseWithContent{}); return; } DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(pNetworkAccessManager); @@ -178,23 +198,31 @@ void WebTask::slotStart(int timeoutMillis) { kLogger.debug() << this << "Starting..."; + m_timer.start(); m_pendingNetworkReplyWeakPtr = doStartNetworkRequest( pNetworkAccessManager, timeoutMillis); // Still idle, because we are in the same thread. - // The callee is not supposed to abort a request - // before it has beeen started successfully. + // The derived class is not allowed to abort a request + // during the callback before it has beeen started + // successfully. Instead it should return nullptr + // to abort the task immediately. DEBUG_ASSERT(m_state == State::Idle); if (!m_pendingNetworkReplyWeakPtr) { kLogger.debug() - << "Network task has not been started"; + << this + << "Network request has not been started"; + m_state = State::Aborted; + emitAborted(/*request URL is unknown*/); return; } + m_state = State::Pending; DEBUG_ASSERT(m_timeoutTimerId == kInvalidTimerId); - if (timeoutMillis > 0) { + if (timeoutMillis != kNoTimeout) { + DEBUG_ASSERT(timeoutMillis > 0); m_timeoutTimerId = startTimer(timeoutMillis); DEBUG_ASSERT(m_timeoutTimerId != kInvalidTimerId); } @@ -208,49 +236,65 @@ void WebTask::slotStart(int timeoutMillis) { Qt::UniqueConnection); } -void WebTask::abort() { +void WebTask::slotAbort() { DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); if (m_state != State::Pending) { DEBUG_ASSERT(m_timeoutTimerId == kInvalidTimerId); + if (m_state == State::Idle) { + kLogger.debug() + << this + << "Cannot abort idle task"; + } else { + DEBUG_ASSERT(hasTerminated()); + kLogger.debug() + << this + << "Cannot abort terminated task"; + } return; } - auto* const pPendingNetworkReply = m_pendingNetworkReplyWeakPtr.data(); - VERIFY_OR_DEBUG_ASSERT(pPendingNetworkReply) { - DEBUG_ASSERT(m_timeoutTimerId == kInvalidTimerId); - return; - } - if (m_timeoutTimerId != kInvalidTimerId) { - killTimer(m_timeoutTimerId); - m_timeoutTimerId = kInvalidTimerId; - } - m_state = State::Aborting; + kLogger.debug() << this << "Aborting..."; - if (pPendingNetworkReply->isRunning()) { - pPendingNetworkReply->abort(); - doNetworkReplyAborted(pPendingNetworkReply); - // Suspend and await finished signal - return; + + if (m_timeoutTimerId != kInvalidTimerId) { + killTimer(m_timeoutTimerId); + m_timeoutTimerId = kInvalidTimerId; } - doNetworkReplyAborted(pPendingNetworkReply); - m_state = State::Aborted; - const auto requestUrl = pPendingNetworkReply->request().url(); - VERIFY_OR_DEBUG_ASSERT( - isSignalFuncConnected(&WebTask::aborted)) { - kLogger.warning() + + auto* const pPendingNetworkReply = m_pendingNetworkReplyWeakPtr.data(); + QUrl requestUrl; + if (pPendingNetworkReply) { + if (pPendingNetworkReply->isRunning()) { + kLogger.debug() + << this + << "Aborting pending network reply after" + << m_timer.elapsed().toIntegerMillis() + << "ms"; + pPendingNetworkReply->abort(); + // Aborting a pending reply will immediately emit a network + // error signal that gets handled in this thread before + // continuing with the next statements. + DEBUG_ASSERT(hasTerminated()); + DEBUG_ASSERT(!m_pendingNetworkReplyWeakPtr); + return; + } + kLogger.debug() << this - << "Unhandled abort signal" - << requestUrl; - deleteLater(); - return; + << "Aborted pending network reply after" + << m_timer.elapsed().toIntegerMillis() + << "ms"; + // Save the request URL for emitting the signal (see below) + requestUrl = pPendingNetworkReply->request().url(); + // Ensure that the aborted reply is scheduled for deletion when leaving + // this scope. + const auto pendingNetworkReplyDeleter = ScopedDeleteLater(pPendingNetworkReply); + m_pendingNetworkReplyWeakPtr.clear(); + doNetworkReplyAborted(pPendingNetworkReply); } - emit aborted( - std::move(requestUrl)); -} -void WebTask::slotAbort() { - abort(); + m_state = State::Aborted; + emitAborted(requestUrl); } void WebTask::timerEvent(QTimerEvent* event) { @@ -260,23 +304,22 @@ void WebTask::timerEvent(QTimerEvent* event) { VERIFY_OR_DEBUG_ASSERT(timerId == m_timeoutTimerId) { return; } + killTimer(m_timeoutTimerId); m_timeoutTimerId = kInvalidTimerId; - VERIFY_OR_DEBUG_ASSERT(m_state == State::Pending) { - return; - } - auto* const pPendingNetworkReply = m_pendingNetworkReplyWeakPtr.data(); - VERIFY_OR_DEBUG_ASSERT(pPendingNetworkReply) { + + if (hasTerminated()) { + DEBUG_ASSERT(!m_pendingNetworkReplyWeakPtr); return; } - if (pPendingNetworkReply->isFinished()) { - // Nothing to do - } + DEBUG_ASSERT(m_state == State::Pending); + kLogger.info() << this << "Aborting after timed out"; - DEBUG_ASSERT(pPendingNetworkReply->isRunning()); - pPendingNetworkReply->abort(); + // Trigger the regular abort workflow after a client-side + // timeout occurred + slotAbort(); } void WebTask::slotNetworkReplyFinished() { @@ -286,7 +329,8 @@ void WebTask::slotNetworkReplyFinished() { VERIFY_OR_DEBUG_ASSERT(pFinishedNetworkReply) { return; } - const auto deleteFinishedNetworkReply = ScopedDeleteLater(pFinishedNetworkReply); + // Ensure that the received reply gets deleted eventually + const auto finishedNetworkReplyDeleter = ScopedDeleteLater(pFinishedNetworkReply); if (kLogger.debugEnabled()) { if (pFinishedNetworkReply->url() == pFinishedNetworkReply->request().url()) { @@ -316,28 +360,38 @@ void WebTask::slotNetworkReplyFinished() { << "pending =" << pPendingNetworkReply; return; } + m_pendingNetworkReplyWeakPtr.clear(); - VERIFY_OR_DEBUG_ASSERT(m_state == State::Pending) { - DEBUG_ASSERT(m_timeoutTimerId == kInvalidTimerId); - return; - } + DEBUG_ASSERT(m_state == State::Pending); + kLogger.debug() + << this + |