diff options
author | Daniel Schürmann <daschuer@mixxx.org> | 2021-01-09 15:20:55 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-09 15:20:55 +0100 |
commit | 39465acc94e51acbdff636f63264a4cc4023531a (patch) | |
tree | 6c152c25178a8e2b89fa609ab17c5443e1c1d36e /src | |
parent | 571fb9a9d1546e5fd7604df3b1618537aeac3832 (diff) | |
parent | 95e86310b9c58661e4abf195edf1370d7d72f17e (diff) |
Merge pull request #3519 from uklotzde/networktask
Networking tasks: Split classes / validate request URLs / Bugfixes
Diffstat (limited to 'src')
-rw-r--r-- | src/musicbrainz/tagfetcher.cpp | 116 | ||||
-rw-r--r-- | src/musicbrainz/tagfetcher.h | 9 | ||||
-rw-r--r-- | src/musicbrainz/web/acoustidlookuptask.cpp | 29 | ||||
-rw-r--r-- | src/musicbrainz/web/acoustidlookuptask.h | 4 | ||||
-rw-r--r-- | src/musicbrainz/web/musicbrainzrecordingstask.cpp | 23 | ||||
-rw-r--r-- | src/musicbrainz/web/musicbrainzrecordingstask.h | 6 | ||||
-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 |
12 files changed, 689 insertions, 401 deletions
diff --git a/src/musicbrainz/tagfetcher.cpp b/src/musicbrainz/tagfetcher.cpp index d347c8e211..8da4e79ef1 100644 --- a/src/musicbrainz/tagfetcher.cpp +++ b/src/musicbrainz/tagfetcher.cpp @@ -49,14 +49,12 @@ void TagFetcher::cancel() { m_fingerprintWatcher.disconnect(this); m_fingerprintWatcher.cancel(); if (m_pAcoustIdTask) { - m_pAcoustIdTask->disconnect(this); - m_pAcoustIdTask->deleteLater(); - m_pAcoustIdTask = nullptr; + m_pAcoustIdTask->invokeAbort(); + DEBUG_ASSERT(!m_pAcoustIdTask); } if (m_pMusicBrainzTask) { - m_pMusicBrainzTask->disconnect(this); - m_pMusicBrainzTask->deleteLater(); - m_pMusicBrainzTask = nullptr; + m_pMusicBrainzTask->invokeAbort(); + DEBUG_ASSERT(!m_pMusicBrainzTask); } } @@ -106,12 +104,19 @@ void TagFetcher::slotFingerprintReady() { void TagFetcher::slotAcoustIdTaskSucceeded( QList<QUuid> recordingIds) { DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); - DEBUG_ASSERT(m_pAcoustIdTask.get() == - qobject_cast<mixxx::AcoustIdLookupTask*>(sender())); + auto* const pAcoustIdTask = m_pAcoustIdTask.get(); + VERIFY_OR_DEBUG_ASSERT(pAcoustIdTask == + qobject_cast<mixxx::AcoustIdLookupTask*>(sender())) { + return; + } + m_pAcoustIdTask = nullptr; + const auto taskDeleter = mixxx::ScopedDeleteLater(pAcoustIdTask); + pAcoustIdTask->disconnect(this); if (recordingIds.isEmpty()) { auto pTrack = std::move(m_pTrack); cancel(); + emit resultAvailable( std::move(pTrack), QList<mixxx::musicbrainz::TrackRelease>()); @@ -144,83 +149,102 @@ void TagFetcher::slotAcoustIdTaskSucceeded( kMusicBrainzTimeoutMillis); } +bool TagFetcher::onAcoustIdTaskTerminated() { + DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); + auto* const pAcoustIdTask = m_pAcoustIdTask.get(); + DEBUG_ASSERT(sender()); + VERIFY_OR_DEBUG_ASSERT(pAcoustIdTask == + qobject_cast<mixxx::AcoustIdLookupTask*>(sender())) { + return false; + } + m_pAcoustIdTask = nullptr; + const auto taskDeleter = mixxx::ScopedDeleteLater(pAcoustIdTask); + pAcoustIdTask->disconnect(this); + return true; +} + void TagFetcher::slotAcoustIdTaskFailed( const mixxx::network::JsonWebResponse& response) { DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); - DEBUG_ASSERT(m_pAcoustIdTask.get() == - qobject_cast<mixxx::AcoustIdLookupTask*>(sender())); + if (!onAcoustIdTaskTerminated()) { + return; + } cancel(); emit networkError( - response.statusCode, + response.statusCode(), "AcoustID", - response.content.toJson(), + response.content().toJson(), -1); } void TagFetcher::slotAcoustIdTaskAborted() { DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); - DEBUG_ASSERT(m_pAcoustIdTask.get() == - qobject_cast<mixxx::AcoustIdLookupTask*>(sender())); + if (!onAcoustIdTaskTerminated()) { + return; + } - auto pTrack = std::move(m_pTrack); cancel(); - - emit resultAvailable( - std::move(pTrack), - QList<mixxx::musicbrainz::TrackRelease>{}); } void TagFetcher::slotAcoustIdTaskNetworkError( - const QUrl& requestUrl, QNetworkReply::NetworkError errorCode, const QString& errorString, - const QByteArray& errorContent) { - Q_UNUSED(requestUrl); - Q_UNUSED(errorContent); + const mixxx::network::WebResponseWithContent& responseWithContent) { + Q_UNUSED(responseWithContent); DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); - DEBUG_ASSERT(m_pAcoustIdTask.get() == - qobject_cast<mixxx::AcoustIdLookupTask*>(sender())); + if (!onAcoustIdTaskTerminated()) { + return; + } cancel(); emit networkError( mixxx::network::kHttpStatusCodeInvalid, - "AcoustID", + QStringLiteral("AcoustID"), errorString, errorCode); } +bool TagFetcher::onMusicBrainzTaskTerminated() { + DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); + auto* const pMusicBrainzTask = m_pMusicBrainzTask.get(); + DEBUG_ASSERT(sender()); + VERIFY_OR_DEBUG_ASSERT(pMusicBrainzTask == + qobject_cast<mixxx::MusicBrainzRecordingsTask*>(sender())) { + return false; + } + m_pMusicBrainzTask = nullptr; + const auto taskDeleter = mixxx::ScopedDeleteLater(pMusicBrainzTask); + pMusicBrainzTask->disconnect(this); + return true; +} + void TagFetcher::slotMusicBrainzTaskAborted() { DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); - DEBUG_ASSERT(m_pMusicBrainzTask.get() == - qobject_cast<mixxx::MusicBrainzRecordingsTask*>(sender())); + if (!onMusicBrainzTaskTerminated()) { + return; + } - auto pTrack = std::move(m_pTrack); cancel(); - - emit resultAvailable( - std::move(pTrack), - QList<mixxx::musicbrainz::TrackRelease>{}); } void TagFetcher::slotMusicBrainzTaskNetworkError( - const QUrl& requestUrl, QNetworkReply::NetworkError errorCode, const QString& errorString, - const QByteArray& errorContent) { - Q_UNUSED(requestUrl); - Q_UNUSED(errorContent); + const mixxx::network::WebResponseWithContent& responseWithContent) { + Q_UNUSED(responseWithContent); DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); - DEBUG_ASSERT(m_pMusicBrainzTask.get() == - qobject_cast<mixxx::MusicBrainzRecordingsTask*>(sender())); + if (!onMusicBrainzTaskTerminated()) { + return; + } cancel(); emit networkError( mixxx::network::kHttpStatusCodeInvalid, - "MusicBrainz", + QStringLiteral("MusicBrainz"), errorString, errorCode); } @@ -230,13 +254,14 @@ void TagFetcher::slotMusicBrainzTaskFailed( int errorCode, const QString& errorMessage) { DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); - DEBUG_ASSERT(m_pMusicBrainzTask.get() == - qobject_cast<mixxx::MusicBrainzRecordingsTask*>(sender())); + if (!onMusicBrainzTaskTerminated()) { + return; + } cancel(); emit networkError( - response.statusCode, + response.statusCode(), "MusicBrainz", errorMessage, errorCode); @@ -245,8 +270,9 @@ void TagFetcher::slotMusicBrainzTaskFailed( void TagFetcher::slotMusicBrainzTaskSucceeded( const QList<mixxx::musicbrainz::TrackRelease>& guessedTrackReleases) { DEBUG_ASSERT_QOBJECT_THREAD_AFFINITY(this); - DEBUG_ASSERT(m_pMusicBrainzTask.get() == - qobject_cast<mixxx::MusicBrainzRecordingsTask*>(sender())); + if (!onMusicBrainzTaskTerminated()) { + return; + } auto pTrack = std::move(m_pTrack); cancel(); diff --git a/src/musicbrainz/tagfetcher.h b/src/musicbrainz/tagfetcher.h index 7d5784fa14..160a854aed 100644 --- a/src/musicbrainz/tagfetcher.h +++ b/src/musicbrainz/tagfetcher.h @@ -48,10 +48,9 @@ class TagFetcher : public QObject { const mixxx::network::JsonWebResponse& response); void slotAcoustIdTaskAborted(); void slotAcoustIdTaskNetworkError( - const QUrl& requestUrl, QNetworkReply::NetworkError errorCode, const QString& errorString, - const QByteArray& errorContent); + const mixxx::network::WebResponseWithContent& responseWithContent); void slotMusicBrainzTaskSucceeded( const QList<mixxx::musicbrainz::TrackRelease>& guessedTrackReleases); @@ -61,12 +60,14 @@ class TagFetcher : public QObject { const QString& errorMessage); void slotMusicBrainzTaskAborted(); void slotMusicBrainzTaskNetworkError( - const QUrl& requestUrl, QNetworkReply::NetworkError errorCode, const QString& errorString, - const QByteArray& errorContent); + const mixxx::network::WebResponseWithContent& responseWithContent); private: + bool onAcoustIdTaskTerminated(); + bool onMusicBrainzTaskTerminated(); + QNetworkAccessManager m_network; QFutureWatcher<QString> m_fingerprintWatcher; diff --git a/src/musicbrainz/web/acoustidlookuptask.cpp b/src/musicbrainz/web/acoustidlookuptask.cpp index c79578a0d4..9319641f75 100644 --- a/src/musicbrainz/web/acoustidlookuptask.cpp +++ b/src/musicbrainz/web/acoustidlookuptask.cpp @@ -113,37 +113,37 @@ QNetworkReply* AcoustIdLookupTask::sendNetworkRequest( } void AcoustIdLookupTask::onFinished( - network::JsonWebResponse&& response) { + const network::JsonWebResponse& response) { if (!response.isStatusCodeSuccess()) { kLogger.warning() << "Request failed with HTTP status code" - << response.statusCode; - emitFailed(std::move(response)); + << response.statusCode(); + emitFailed(response); return; } - VERIFY_OR_DEBUG_ASSERT(response.statusCode == network::kHttpStatusCodeOk) { + VERIFY_OR_DEBUG_ASSERT(response.statusCode() == network::kHttpStatusCodeOk) { kLogger.warning() << "Unexpected HTTP status code" - << response.statusCode; - emitFailed(std::move(response)); + << response.statusCode(); + emitFailed(response); return; } - VERIFY_OR_DEBUG_ASSERT(response.content.isObject()) { + VERIFY_OR_DEBUG_ASSERT(response.content().isObject()) { kLogger.warning() << "Invalid JSON content" - << response.content; - emitFailed(std::move(response)); + << response.content(); + emitFailed(response); return; } - const auto jsonObject = response.content.object(); + const auto jsonObject = response.content().object(); const auto statusText = jsonObject.value(QStringLiteral("status")).toString(); if (statusText != QStringLiteral("ok")) { kLogger.warning() << "Unexpected response status" << statusText; - emitFailed(std::move(response)); + emitFailed(response); return; } @@ -207,11 +207,11 @@ void AcoustIdLookupTask::onFinished( } } } - emitSucceeded(std::move(recordingIds)); + emitSucceeded(recordingIds); } void AcoustIdLookupTask::emitSucceeded( - QList<QUuid>&& recordingIds) { + const QList<QUuid>& recordingIds) { VERIFY_OR_DEBUG_ASSERT( isSignalFuncConnected(&AcoustIdLookupTask::succeeded)) { kLogger.warning() @@ -219,8 +219,7 @@ void AcoustIdLookupTask::emitSucceeded( deleteLater(); return; } - emit succeeded( - std::move(recordingIds)); + emit succeeded(recordingIds); } } // namespace mixxx diff --git a/src/musicbrainz/web/acoustidlookuptask.h b/src/musicbrainz/web/acoustidlookuptask.h index 75e9b827f5..74317b8c10 100644 --- a/src/musicbrainz/web/acoustidlookuptask.h +++ b/src/musicbrainz/web/acoustidlookuptask.h @@ -32,10 +32,10 @@ class AcoustIdLookupTask : public network::JsonWebTask { private: void onFinished( - network::JsonWebResponse&& response) override; + const network::JsonWebResponse& response) override; void emitSucceeded( - QList<QUuid>&& recordingIds); + const QList<QUuid>& recordingIds); const QUrlQuery m_urlQuery; }; diff --git a/src/musicbrainz/web/musicbrainzrecordingstask.cpp b/src/musicbrainz/web/musicbrainzrecordingstask.cpp index 5afec1225e..77b14ba9ee 100644 --- a/src/musicbrainz/web/musicbrainzrecordingstask.cpp +++ b/src/musicbrainz/web/musicbrainzrecordingstask.cpp @@ -71,7 +71,7 @@ MusicBrainzRecordingsTask::MusicBrainzRecordingsTask( : network::WebTask( networkAccessManager, parent), - m_queuedRecordingIds(std::move(recordingIds)), + m_queuedRecordingIds(recordingIds), m_parentTimeoutMillis(0) { musicbrainz::registerMetaTypesOnce(); } @@ -126,9 +126,10 @@ void MusicBrainzRecordingsTask::doNetworkReplyFinished( emitFailed( network::WebResponse( finishedNetworkReply->url(), + finishedNetworkReply->request().url(), statusCode), error.code, - std::move(error.message)); + error.message); return; } @@ -147,9 +148,10 @@ void MusicBrainzRecordingsTask::doNetworkReplyFinished( emitFailed( network::WebResponse( finishedNetworkReply->url(), + finishedNetworkReply->request().url(), statusCode), -1, - "Failed to parse XML response"); + QStringLiteral("Failed to parse XML response")); return; } @@ -158,7 +160,7 @@ void MusicBrainzRecordingsTask::doNetworkReplyFinished( m_finishedRecordingIds.clear(); auto trackReleases = m_trackReleases.values(); m_trackReleases.clear(); - emitSucceeded(std::move(trackReleases)); + emitSucceeded(trackReleases); return; } @@ -168,7 +170,7 @@ void MusicBrainzRecordingsTask::doNetworkReplyFinished( } void MusicBrainzRecordingsTask::emitSucceeded( - QList<musicbrainz::TrackRelease>&& trackReleases) { + const QList<musicbrainz::TrackRelease>& trackReleases) { VERIFY_OR_DEBUG_ASSERT( isSignalFuncConnected(&MusicBrainzRecordingsTask::succeeded)) { kLogger.warning() @@ -176,14 +178,13 @@ void MusicBrainzRecordingsTask::emitSucceeded( deleteLater(); return; } - emit succeeded( - std::move(trackReleases)); + emit succeeded(trackReleases); } void MusicBrainzRecordingsTask::emitFailed( - network::WebResponse&& response, + const network::WebResponse& response, int errorCode, - QString&& errorMessage) { + const QString& errorMessage) { VERIFY_OR_DEBUG_ASSERT( isSignalFuncConnected(&MusicBrainzRecordingsTask::failed)) { kLogger.warning() @@ -195,9 +196,9 @@ void MusicBrainzRecordingsTask::emitFailed( return; } emit failed( - std::move(response), + response, errorCode, - std::move(errorMessage)); + errorMessage); } } // namespace mixxx diff --git a/src/musicbrainz/web/musicbrainzrecordingstask.h b/src/musicbrainz/web/musicbrainzrecordingstask.h index 39678d65ff..3a8451745d 100644 --- a/src/musicbrainz/web/musicbrainzrecordingstask.h +++ b/src/musicbrainz/web/musicbrainzrecordingstask.h @@ -38,11 +38,11 @@ class MusicBrainzRecordingsTask : public network::WebTask { network::HttpStatusCode statusCode) override; void emitSucceeded( - QList<musicbrainz::TrackRelease>&& trackReleases); + const QList<musicbrainz::TrackRelease>& trackReleases); void emitFailed( - network::WebResponse&& response, + const network::WebResponse& response, int errorCode, - QString&& errorMessage); + const QString& errorMessage); void continueWithNextRequest(); diff --git a/src/network/jsonwebtask.cpp b/src/network/jsonwebtask.cpp index 0e355db369..434ab27081 100644 --- a/src/network/jsonwebtask.cpp +++ b/src/network/jsonwebtask.cpp @@ -37,64 +37,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? @@ -115,43 +106,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(); } @@ -244,12 +231,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( @@ -262,26 +261,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() @@ -291,7 +295,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 |