Skip to content

Commit

Permalink
[api] Add new class to run a process in a blocking manner without
Browse files Browse the repository at this point in the history
QEventLoop related issues on the main thread

This class should be used whenever a blocking process run is required.
Unlike implementations which rely on QApplication::processEvents()
or creation of a QEventLoop, this class is completely thread safe
and can be used on either the main thread or background threads without
issue.

Execution supports use of a QgsFeedback object to support termination
of the process.
  • Loading branch information
nyalldawson committed Jan 6, 2021
1 parent d1761bd commit a39e162
Show file tree
Hide file tree
Showing 5 changed files with 372 additions and 0 deletions.
75 changes: 75 additions & 0 deletions python/core/auto_generated/qgsrunprocess.sip.in
Expand Up @@ -51,6 +51,81 @@ consecutive double quotes represent the quote character itself.
void dialogGone();
};


class QgsBlockingProcess : QObject
{
%Docstring
A thread safe class for performing blocking (sync) execution of external processes.

This class should be used whenever a blocking process run is required. Unlike implementations
which rely on QApplication.processEvents() or creation of a QEventLoop, this class is completely
thread safe and can be used on either the main thread or background threads without issue.

.. versionadded:: 3.18
%End

%TypeHeaderCode
#include "qgsrunprocess.h"
%End
public:

QgsBlockingProcess( const QString &program, const QStringList &arguments );
%Docstring
Constructor for the given ``program``, with the specified list of ``arguments``.

After construction, call :py:func:`~QgsBlockingProcess.run` to start the process execution.
%End


void setStdOutHandler( SIP_PYCALLABLE / AllowNone / );
%Docstring
Sets a handler function to call whenever content is written by the process to stdout.
%End
%MethodCode
Py_BEGIN_ALLOW_THREADS

sipCpp->setStdOutHandler( [a0]( const QByteArray &arg )
{
SIP_BLOCK_THREADS
Py_XDECREF( sipCallMethod( NULL, a0, "D", &arg, sipType_QByteArray, NULL ) );
SIP_UNBLOCK_THREADS
} );

Py_END_ALLOW_THREADS
%End


void setStdErrHandler( SIP_PYCALLABLE / AllowNone / );
%Docstring
Sets a ``handler`` function to call whenever content is written by the process to stderr.
%End
%MethodCode
Py_BEGIN_ALLOW_THREADS

sipCpp->setStdErrHandler( [a0]( const QByteArray &arg )
{
SIP_BLOCK_THREADS
Py_XDECREF( sipCallMethod( NULL, a0, "D", &arg, sipType_QByteArray, NULL ) );
SIP_UNBLOCK_THREADS
} );

Py_END_ALLOW_THREADS
%End

int run( QgsFeedback *feedback = 0 );
%Docstring
Runs the process, and blocks until execution finishes.

The optional ``feedback`` argument can be used to specify a feedback object for cancellation/process termination.

After execution completes, the process' result code will be returned.
%End

};




/************************************************************************
* This file has been generated automatically from *
* *
Expand Down
86 changes: 86 additions & 0 deletions src/core/qgsrunprocess.cpp
Expand Up @@ -22,9 +22,13 @@

#include "qgslogger.h"
#include "qgsmessageoutput.h"
#include "qgsfeedback.h"
#include "qgsapplication.h"
#include "qgis.h"
#include <QProcess>
#include <QTextCodec>
#include <QMessageBox>
#include <QApplication>

#if QT_CONFIG(process)
QgsRunProcess::QgsRunProcess( const QString &action, bool capture )
Expand Down Expand Up @@ -247,3 +251,85 @@ QStringList QgsRunProcess::splitCommand( const QString & )
return QStringList();
}
#endif


//
// QgsBlockingProcess
//

QgsBlockingProcess::QgsBlockingProcess( const QString &process, const QStringList &arguments )
: QObject()
, mProcess( process )
, mArguments( arguments )
{

}

int QgsBlockingProcess::run( QgsFeedback *feedback )
{
const bool requestMadeFromMainThread = QThread::currentThread() == QCoreApplication::instance()->thread();

int result = 0;
QProcess::ExitStatus status = QProcess::NormalExit;

std::function<void()> runFunction = [ this, &result, &status, feedback]()
{
// this function will always be run in worker threads -- either the blocking call is being made in a worker thread,
// or the blocking call has been made from the main thread and we've fired up a new thread for this function
Q_ASSERT( QThread::currentThread() != QgsApplication::instance()->thread() );

QProcess p;
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
p.setProcessEnvironment( env );

QEventLoop loop;
// connecting to aboutToQuit avoids an on-going process to remain stalled
// when QThreadPool::globalInstance()->waitForDone()
// is called at process termination
connect( qApp, &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit, Qt::DirectConnection );

if ( feedback )
QObject::connect( feedback, &QgsFeedback::canceled, &p, [ &p]
{
p.terminate();
} );
connect( &p, qgis::overload< int, QProcess::ExitStatus >::of( &QProcess::finished ), this, [&loop, &result, &status]( int res, QProcess::ExitStatus st )
{
result = res;
status = st;
loop.quit();
}, Qt::DirectConnection );

connect( &p, &QProcess::readyReadStandardOutput, this, [&p, this]
{
QByteArray ba = p.readAllStandardOutput();
mStdoutHandler( ba );
} );
connect( &p, &QProcess::readyReadStandardError, this, [&p, this]
{
QByteArray ba = p.readAllStandardError();
mStderrHandler( ba );
} );
p.start( mProcess, mArguments, QProcess::Unbuffered | QProcess::ReadWrite );

loop.exec();

mStdoutHandler( p.readAllStandardOutput() );
mStderrHandler( p.readAllStandardError() );
};

if ( requestMadeFromMainThread )
{
std::unique_ptr<ProcessThread> processThread = qgis::make_unique<ProcessThread>( runFunction );
processThread->start();
// wait for thread to gracefully exit
processThread->wait();
}
else
{
runFunction();
}

return result;
};

124 changes: 124 additions & 0 deletions src/core/qgsrunprocess.h
Expand Up @@ -23,10 +23,12 @@

#include <QObject>
#include <QProcess>
#include <QThread>

#include "qgis_core.h"
#include "qgis_sip.h"

class QgsFeedback;
class QgsMessageOutput;

/**
Expand Down Expand Up @@ -85,4 +87,126 @@ class CORE_EXPORT QgsRunProcess: public QObject SIP_NODEFAULTCTORS
#endif // !(QT_CONFIG(process)
};


/**
* A thread safe class for performing blocking (sync) execution of external processes.
*
* This class should be used whenever a blocking process run is required. Unlike implementations
* which rely on QApplication::processEvents() or creation of a QEventLoop, this class is completely
* thread safe and can be used on either the main thread or background threads without issue.
*
* \ingroup core
* \since QGIS 3.18
*/
class CORE_EXPORT QgsBlockingProcess : public QObject
{
Q_OBJECT

public:

/**
* Constructor for the given \a program, with the specified list of \a arguments.
*
* After construction, call run() to start the process execution.
*/
QgsBlockingProcess( const QString &program, const QStringList &arguments );

#ifndef SIP_RUN

/**
* Sets a \a handler function to call whenever content is written by the process to stdout.
*/
void setStdOutHandler( const std::function< void( const QByteArray & ) > &handler ) { mStdoutHandler = handler; }
#else

/**
* Sets a handler function to call whenever content is written by the process to stdout.
*/
void setStdOutHandler( SIP_PYCALLABLE / AllowNone / );
% MethodCode
Py_BEGIN_ALLOW_THREADS

sipCpp->setStdOutHandler( [a0]( const QByteArray &arg )
{
SIP_BLOCK_THREADS
Py_XDECREF( sipCallMethod( NULL, a0, "D", &arg, sipType_QByteArray, NULL ) );
SIP_UNBLOCK_THREADS
} );

Py_END_ALLOW_THREADS
% End
#endif

#ifndef SIP_RUN

/**
* Sets a \a handler function to call whenever content is written by the process to stderr.
*/
void setStdErrHandler( const std::function< void( const QByteArray & ) > &handler ) { mStderrHandler = handler; }
#else

/**
* Sets a \a handler function to call whenever content is written by the process to stderr.
*/
void setStdErrHandler( SIP_PYCALLABLE / AllowNone / );
% MethodCode
Py_BEGIN_ALLOW_THREADS

sipCpp->setStdErrHandler( [a0]( const QByteArray &arg )
{
SIP_BLOCK_THREADS
Py_XDECREF( sipCallMethod( NULL, a0, "D", &arg, sipType_QByteArray, NULL ) );
SIP_UNBLOCK_THREADS
} );

Py_END_ALLOW_THREADS
% End
#endif

/**
* Runs the process, and blocks until execution finishes.
*
* The optional \a feedback argument can be used to specify a feedback object for cancellation/process termination.
*
* After execution completes, the process' result code will be returned.
*/
int run( QgsFeedback *feedback = nullptr );

private:

QString mProcess;
QStringList mArguments;
std::function< void( const QByteArray & ) > mStdoutHandler;
std::function< void( const QByteArray & ) > mStderrHandler;

};


///@cond PRIVATE
#ifndef SIP_RUN

class ProcessThread : public QThread
{
Q_OBJECT

public:
ProcessThread( const std::function<void()> &function, QObject *parent = nullptr )
: QThread( parent )
, mFunction( function )
{
}

void run() override
{
mFunction();
}

private:
std::function<void()> mFunction;
};

#endif
///@endcond


#endif
1 change: 1 addition & 0 deletions tests/src/python/CMakeLists.txt
Expand Up @@ -34,6 +34,7 @@ ADD_PYTHON_TEST(PyQgsBatchGeocodeAlgorithm test_qgsgeocoderalgorithm.py)
ADD_PYTHON_TEST(PyQgsBearingUtils test_qgsbearingutils.py)
ADD_PYTHON_TEST(PyQgsBinaryWidget test_qgsbinarywidget.py)
ADD_PYTHON_TEST(PyQgsBlendModes test_qgsblendmodes.py)
ADD_PYTHON_TEST(PyQgsBlockingProcess test_qgsblockingprocess.py)
ADD_PYTHON_TEST(PyQgsBlockingNetworkRequest test_qgsblockingnetworkrequest.py)
ADD_PYTHON_TEST(PyQgsBookmarkManager test_qgsbookmarkmanager.py)
ADD_PYTHON_TEST(PyQgsBookmarkModel test_qgsbookmarkmodel.py)
Expand Down

0 comments on commit a39e162

Please sign in to comment.