Skip to content

Commit 66ad9d9

Browse files
authored
Merge pull request #7055 from m-kuhn/wfsNoMainThreadEventLoopb
Avoid running QEventLoop on main thread (in WFS provider)
2 parents 6b08eff + 478d6d0 commit 66ad9d9

File tree

7 files changed

+181
-31
lines changed

7 files changed

+181
-31
lines changed

.ci/travis/linux/docker-build-test.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ cmake \
5555
-DWITH_BINDINGS=ON \
5656
-DWITH_SERVER=ON \
5757
-DDISABLE_DEPRECATED=ON \
58+
-DPYTHON_TEST_WRAPPER="timeout -sSIGSEGV 55s"\
5859
-DCXX_EXTRA_FLAGS=${CLANG_WARNINGS} ..
5960
echo "travis_fold:end:cmake"
6061

python/core/auto_generated/qgsnetworkaccessmanager.sip.in

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,26 @@ that the fallback proxy should not be used for, then no proxy will be used.
3232
#include "qgsnetworkaccessmanager.h"
3333
%End
3434
public:
35-
static QgsNetworkAccessManager *instance();
35+
36+
static QgsNetworkAccessManager *instance( Qt::ConnectionType connectionType = Qt::BlockingQueuedConnection );
37+
%Docstring
38+
Returns a pointer to the active QgsNetworkAccessManager
39+
for the current thread.
40+
41+
With the ``connectionType`` parameter it is possible to setup the default connection
42+
type that is used to handle signals that might require user interaction and therefore
43+
need to be handled on the main thread. See in-depth discussion below.
44+
45+
:param connectionType: In most cases the default of using a ``Qt.BlockingQueuedConnection``
46+
is ok, to make a background thread wait for the main thread to answer such a request is
47+
fine and anything else is dangerous.
48+
However, in case the request was started on the main thread, one should execute a
49+
local event loop in a helper thread and freeze the main thread for the duration of the
50+
download. In this case, if an authentication request is sent from the background thread
51+
network access manager, the background thread should be blocked, the main thread be woken
52+
up, processEvents() executed once, the main thread frozen again and the background thread
53+
continued.
54+
%End
3655

3756
QgsNetworkAccessManager( QObject *parent = 0 );
3857

@@ -76,9 +95,13 @@ Gets name for QNetworkRequest.CacheLoadControl
7695
Gets QNetworkRequest.CacheLoadControl from name
7796
%End
7897

79-
void setupDefaultProxyAndCache();
98+
void setupDefaultProxyAndCache( Qt::ConnectionType connectionType = Qt::BlockingQueuedConnection );
8099
%Docstring
81-
Setup the NAM according to the user's settings
100+
Setup the QgsNetworkAccessManager (NAM) according to the user's settings.
101+
The ``connectionType`` sets up the default connection type that is used to
102+
handle signals that might require user interaction and therefore
103+
need to be handled on the main thread. See in-depth discussion in the documentation
104+
for the constructor of this class.
82105
%End
83106

84107
bool useSystemProxy() const;

src/core/qgsnetworkaccessmanager.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class QgsNetworkProxyFactory : public QNetworkProxyFactory
102102
//
103103
// Static calls to enforce singleton behavior
104104
//
105-
QgsNetworkAccessManager *QgsNetworkAccessManager::instance()
105+
QgsNetworkAccessManager *QgsNetworkAccessManager::instance( Qt::ConnectionType connectionType )
106106
{
107107
static QThreadStorage<QgsNetworkAccessManager> sInstances;
108108
QgsNetworkAccessManager *nam = &sInstances.localData();
@@ -111,7 +111,7 @@ QgsNetworkAccessManager *QgsNetworkAccessManager::instance()
111111
sMainNAM = nam;
112112

113113
if ( !nam->mInitialized )
114-
nam->setupDefaultProxyAndCache();
114+
nam->setupDefaultProxyAndCache( connectionType );
115115

116116
return nam;
117117
}
@@ -281,7 +281,7 @@ QNetworkRequest::CacheLoadControl QgsNetworkAccessManager::cacheLoadControlFromN
281281
return QNetworkRequest::PreferNetwork;
282282
}
283283

284-
void QgsNetworkAccessManager::setupDefaultProxyAndCache()
284+
void QgsNetworkAccessManager::setupDefaultProxyAndCache( Qt::ConnectionType connectionType )
285285
{
286286
mInitialized = true;
287287
mUseSystemProxy = false;
@@ -292,19 +292,19 @@ void QgsNetworkAccessManager::setupDefaultProxyAndCache()
292292
{
293293
connect( this, &QNetworkAccessManager::authenticationRequired,
294294
sMainNAM, &QNetworkAccessManager::authenticationRequired,
295-
Qt::BlockingQueuedConnection );
295+
connectionType );
296296

297297
connect( this, &QNetworkAccessManager::proxyAuthenticationRequired,
298298
sMainNAM, &QNetworkAccessManager::proxyAuthenticationRequired,
299-
Qt::BlockingQueuedConnection );
299+
connectionType );
300300

301301
connect( this, &QgsNetworkAccessManager::requestTimedOut,
302302
sMainNAM, &QgsNetworkAccessManager::requestTimedOut );
303303

304304
#ifndef QT_NO_SSL
305305
connect( this, &QNetworkAccessManager::sslErrors,
306306
sMainNAM, &QNetworkAccessManager::sslErrors,
307-
Qt::BlockingQueuedConnection );
307+
connectionType );
308308
#endif
309309
}
310310

src/core/qgsnetworkaccessmanager.h

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,26 @@ class CORE_EXPORT QgsNetworkAccessManager : public QNetworkAccessManager
4949
Q_OBJECT
5050

5151
public:
52-
//! returns a pointer to the single instance
53-
// and creates that instance on the first call.
54-
static QgsNetworkAccessManager *instance();
52+
53+
/**
54+
* Returns a pointer to the active QgsNetworkAccessManager
55+
* for the current thread.
56+
*
57+
* With the \a connectionType parameter it is possible to setup the default connection
58+
* type that is used to handle signals that might require user interaction and therefore
59+
* need to be handled on the main thread. See in-depth discussion below.
60+
*
61+
* \param connectionType In most cases the default of using a ``Qt::BlockingQueuedConnection``
62+
* is ok, to make a background thread wait for the main thread to answer such a request is
63+
* fine and anything else is dangerous.
64+
* However, in case the request was started on the main thread, one should execute a
65+
* local event loop in a helper thread and freeze the main thread for the duration of the
66+
* download. In this case, if an authentication request is sent from the background thread
67+
* network access manager, the background thread should be blocked, the main thread be woken
68+
* up, processEvents() executed once, the main thread frozen again and the background thread
69+
* continued.
70+
*/
71+
static QgsNetworkAccessManager *instance( Qt::ConnectionType connectionType = Qt::BlockingQueuedConnection );
5572

5673
QgsNetworkAccessManager( QObject *parent = nullptr );
5774

@@ -79,8 +96,14 @@ class CORE_EXPORT QgsNetworkAccessManager : public QNetworkAccessManager
7996
//! Gets QNetworkRequest::CacheLoadControl from name
8097
static QNetworkRequest::CacheLoadControl cacheLoadControlFromName( const QString &name );
8198

82-
//! Setup the NAM according to the user's settings
83-
void setupDefaultProxyAndCache();
99+
/**
100+
* Setup the QgsNetworkAccessManager (NAM) according to the user's settings.
101+
* The \a connectionType sets up the default connection type that is used to
102+
* handle signals that might require user interaction and therefore
103+
* need to be handled on the main thread. See in-depth discussion in the documentation
104+
* for the constructor of this class.
105+
*/
106+
void setupDefaultProxyAndCache( Qt::ConnectionType connectionType = Qt::BlockingQueuedConnection );
84107

85108
//! Returns whether the system proxy should be used
86109
bool useSystemProxy() const { return mUseSystemProxy; }

src/providers/wfs/qgswfscapabilities.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@
3030
QgsWfsCapabilities::QgsWfsCapabilities( const QString &uri )
3131
: QgsWfsRequest( QgsWFSDataSourceURI( uri ) )
3232
{
33-
connect( this, &QgsWfsRequest::downloadFinished, this, &QgsWfsCapabilities::capabilitiesReplyFinished );
33+
// Using Qt::DirectConnection since the download might be running on a different thread.
34+
// In this case, the request was sent from the main thread and is executed with the main
35+
// thread being blocked in future.waitForFinished() so we can run code on this object which
36+
// lives in the main thread without risking havoc.
37+
connect( this, &QgsWfsRequest::downloadFinished, this, &QgsWfsCapabilities::capabilitiesReplyFinished, Qt::DirectConnection );
3438
}
3539

3640
bool QgsWfsCapabilities::requestCapabilities( bool synchronous, bool forceRefresh )

src/providers/wfs/qgswfsrequest.cpp

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
#include <QEventLoop>
2525
#include <QNetworkCacheMetaData>
2626
#include <QCryptographicHash> // just for testin file:// fake_qgis_http_endpoint hack
27+
#include <QFuture>
28+
#include <QtConcurrent>
2729

2830
const qint64 READ_BUFFER_SIZE_HINT = 1024 * 1024;
2931

@@ -126,26 +128,95 @@ bool QgsWfsRequest::sendGET( const QUrl &url, bool synchronous, bool forceRefres
126128
request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
127129
}
128130

129-
mReply = QgsNetworkAccessManager::instance()->get( request );
130-
mReply->setReadBufferSize( READ_BUFFER_SIZE_HINT );
131-
if ( !mUri.auth().setAuthorizationReply( mReply ) )
131+
QWaitCondition waitCondition;
132+
QMutex waitConditionMutex;
133+
134+
std::function<bool()> downloaderFunction = [ this, request, synchronous, &waitConditionMutex, &waitCondition ]()
132135
{
133-
mErrorCode = QgsWfsRequest::NetworkError;
134-
mErrorMessage = errorMessageFailedAuth();
135-
QgsMessageLog::logMessage( mErrorMessage, tr( "WFS" ) );
136-
return false;
137-
}
138-
connect( mReply, &QNetworkReply::finished, this, &QgsWfsRequest::replyFinished );
139-
connect( mReply, &QNetworkReply::downloadProgress, this, &QgsWfsRequest::replyProgress );
136+
if ( QThread::currentThread() != QgsApplication::instance()->thread() )
137+
QgsNetworkAccessManager::instance( Qt::DirectConnection );
140138

141-
if ( !synchronous )
142-
return true;
139+
bool success = true;
140+
mReply = QgsNetworkAccessManager::instance()->get( request );
143141

144-
QEventLoop loop;
145-
connect( this, &QgsWfsRequest::downloadFinished, &loop, &QEventLoop::quit );
146-
loop.exec( QEventLoop::ExcludeUserInputEvents );
142+
mReply->setReadBufferSize( READ_BUFFER_SIZE_HINT );
143+
if ( !mUri.auth().setAuthorizationReply( mReply ) )
144+
{
145+
mErrorCode = QgsWfsRequest::NetworkError;
146+
mErrorMessage = errorMessageFailedAuth();
147+
QgsMessageLog::logMessage( mErrorMessage, tr( "WFS" ) );
148+
waitCondition.wakeAll();
149+
success = false;
150+
}
151+
else
152+
{
153+
// We are able to use direct connection here, because we
154+
// * either run on the thread mReply lives in, so DirectConnection is standard and safe anyway
155+
// * or the owner thread of mReply is currently not doing anything because it's blocked in future.waitForFinished() (if it is the main thread)
156+
connect( mReply, &QNetworkReply::finished, this, &QgsWfsRequest::replyFinished, Qt::DirectConnection );
157+
connect( mReply, &QNetworkReply::downloadProgress, this, &QgsWfsRequest::replyProgress, Qt::DirectConnection );
147158

148-
return mErrorMessage.isEmpty();
159+
if ( synchronous )
160+
{
161+
auto resumeMainThread = [&waitConditionMutex, &waitCondition]()
162+
{
163+
waitConditionMutex.lock();
164+
waitCondition.wakeAll();
165+
waitConditionMutex.unlock();
166+
167+
waitConditionMutex.lock();
168+
waitCondition.wait( &waitConditionMutex );
169+
waitConditionMutex.unlock();
170+
};
171+
172+
connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::authenticationRequired, this, resumeMainThread, Qt::DirectConnection );
173+
connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::proxyAuthenticationRequired, this, resumeMainThread, Qt::DirectConnection );
174+
175+
#ifndef QT_NO_SSL
176+
connect( QgsNetworkAccessManager::instance(), &QgsNetworkAccessManager::sslErrors, this, resumeMainThread, Qt::DirectConnection );
177+
#endif
178+
QEventLoop loop;
179+
connect( this, &QgsWfsRequest::downloadFinished, &loop, &QEventLoop::quit, Qt::DirectConnection );
180+
loop.exec();
181+
}
182+
}
183+
waitCondition.wakeAll();
184+
return success;
185+
};
186+
187+
bool success;
188+
189+
if ( synchronous && QThread::currentThread() == QApplication::instance()->thread() )
190+
{
191+
std::unique_ptr<DownloaderThread> downloaderThread = qgis::make_unique<DownloaderThread>( downloaderFunction );
192+
downloaderThread->start();
193+
194+
while ( !downloaderThread->isFinished() )
195+
{
196+
waitConditionMutex.lock();
197+
waitCondition.wait( &waitConditionMutex );
198+
waitConditionMutex.unlock();
199+
200+
// If the downloader thread wakes us (the main thread) up and is not yet finished
201+
// he needs the authentication to run.
202+
// The processEvents() call gives the auth manager the chance to show a dialog and
203+
// once done with that, we can wake the downloaderThread again and continue the download.
204+
if ( !downloaderThread->isFinished() )
205+
{
206+
QgsApplication::instance()->processEvents();
207+
waitConditionMutex.lock();
208+
waitCondition.wakeAll();
209+
waitConditionMutex.unlock();
210+
}
211+
}
212+
213+
success = downloaderThread->success();
214+
}
215+
else
216+
{
217+
success = downloaderFunction();
218+
}
219+
return success && mErrorMessage.isEmpty();
149220
}
150221

151222
bool QgsWfsRequest::sendPOST( const QUrl &url, const QString &contentTypeHeader, const QByteArray &data )

src/providers/wfs/qgswfsrequest.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <QNetworkRequest>
2020
#include <QNetworkReply>
2121
#include <QUrl>
22+
#include <QAuthenticator>
2223

2324
#include "qgswfsdatasourceuri.h"
2425

@@ -117,4 +118,31 @@ class QgsWfsRequest : public QObject
117118

118119
};
119120

121+
122+
class DownloaderThread : public QThread
123+
{
124+
Q_OBJECT
125+
126+
public:
127+
DownloaderThread( std::function<bool()> function, QObject *parent = nullptr )
128+
: QThread( parent )
129+
, mFunction( function )
130+
{
131+
}
132+
133+
void run() override
134+
{
135+
mSuccess = mFunction();
136+
}
137+
138+
bool success() const
139+
{
140+
return mSuccess;
141+
}
142+
143+
private:
144+
std::function<bool()> mFunction;
145+
bool mSuccess = false;
146+
};
147+
120148
#endif // QGSWFSREQUEST_H

0 commit comments

Comments
 (0)