QGIS API Documentation 3.27.0-Master (95e00c50d2)
qgsrunprocess.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsrunprocess.cpp
3
4 A class that runs an external program
5
6 -------------------
7 begin : Jan 2005
8 copyright : (C) 2005 by Gavin Macaulay
9 email : gavin at macaulay dot co dot nz
10 ***************************************************************************/
11
12/***************************************************************************
13 * *
14 * This program is free software; you can redistribute it and/or modify *
15 * it under the terms of the GNU General Public License as published by *
16 * the Free Software Foundation; either version 2 of the License, or *
17 * (at your option) any later version. *
18 * *
19 ***************************************************************************/
20
21#include "qgsrunprocess.h"
22
23#include "qgslogger.h"
24#include "qgsmessageoutput.h"
25#include "qgsfeedback.h"
26#include "qgsapplication.h"
27#include "qgis.h"
28#include <QProcess>
29#include <QTextCodec>
30#include <QMessageBox>
31#include <QApplication>
32
33#if QT_CONFIG(process)
34QgsRunProcess::QgsRunProcess( const QString &action, bool capture )
35
36{
37 // Make up a string from the command and arguments that we'll use
38 // for display purposes
39 QgsDebugMsg( "Running command: " + action );
40
41 mCommand = action;
42
43#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
44 QStringList arguments = QProcess::splitCommand( action );
45 const QString command = arguments.value( 0 );
46 if ( !arguments.isEmpty() )
47 arguments.removeFirst();
48#endif
49
50 mProcess = new QProcess;
51
52 if ( capture )
53 {
54 connect( mProcess, &QProcess::errorOccurred, this, &QgsRunProcess::processError );
55 connect( mProcess, &QProcess::readyReadStandardOutput, this, &QgsRunProcess::stdoutAvailable );
56 connect( mProcess, &QProcess::readyReadStandardError, this, &QgsRunProcess::stderrAvailable );
57 // We only care if the process has finished if we are capturing
58 // the output from the process, hence this connect() call is
59 // inside the capture if() statement.
60 connect( mProcess, static_cast < void ( QProcess::* )( int, QProcess::ExitStatus ) >( &QProcess::finished ), this, &QgsRunProcess::processExit );
61
62 // Use QgsMessageOutput for displaying output to user
63 // It will delete itself when the dialog box is closed.
65 mOutput->setTitle( action );
66 mOutput->setMessage( tr( "<b>Starting %1…</b>" ).arg( action ), QgsMessageOutput::MessageHtml );
67 mOutput->showMessage( false ); // non-blocking
68
69 // get notification of delete if it's derived from QObject
70 QObject *mOutputObj = dynamic_cast<QObject *>( mOutput );
71 if ( mOutputObj )
72 {
73 connect( mOutputObj, &QObject::destroyed, this, &QgsRunProcess::dialogGone );
74 }
75
76 // start the process!
77#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
78 mProcess->start( action );
79#else
80 mProcess->start( command, arguments );
81#endif
82 }
83 else
84 {
85#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
86 if ( ! mProcess->startDetached( action ) ) // let the program run by itself
87#else
88 if ( ! QProcess::startDetached( command, arguments ) ) // let the program run by itself
89#endif
90 {
91 QMessageBox::critical( nullptr, tr( "Action" ),
92 tr( "Unable to run command\n%1" ).arg( action ),
93 QMessageBox::Ok, Qt::NoButton );
94 }
95 // We're not capturing the output from the process, so we don't
96 // need to exist anymore.
97 die();
98 }
99}
100
101QgsRunProcess::~QgsRunProcess()
102{
103 delete mProcess;
104}
105
106void QgsRunProcess::die()
107{
108 // safe way to do "delete this" for QObjects
109 deleteLater();
110}
111
112void QgsRunProcess::stdoutAvailable()
113{
114 const QByteArray bytes( mProcess->readAllStandardOutput() );
115 QTextCodec *codec = QTextCodec::codecForLocale();
116 const QString line( codec->toUnicode( bytes ) );
117
118 // Add the new output to the dialog box
119 mOutput->appendMessage( line );
120}
121
122void QgsRunProcess::stderrAvailable()
123{
124 const QByteArray bytes( mProcess->readAllStandardOutput() );
125 QTextCodec *codec = QTextCodec::codecForLocale();
126 const QString line( codec->toUnicode( bytes ) );
127
128 // Add the new output to the dialog box, but color it red
129 mOutput->appendMessage( "<font color=red>" + line + "</font>" );
130}
131
132void QgsRunProcess::processExit( int, QProcess::ExitStatus )
133{
134 // Because we catch the dialog box going (the dialogGone()
135 // function), and delete this instance, control will only pass to
136 // this function if the dialog box still exists when the process
137 // exits, so it's always safe to use the pointer to the dialog box
138 // (unless it was never created in the first case, which is what the
139 // test against 0 is for).
140
141 if ( mOutput )
142 {
143 mOutput->appendMessage( "<b>" + tr( "Done" ) + "</b>" );
144 }
145
146 // Since the dialog box takes care of deleting itself, and the
147 // process has gone, there's no need for this instance to stay
148 // around, so we disappear too.
149 die();
150}
151
152void QgsRunProcess::dialogGone()
153{
154 // The dialog has gone, so the user is no longer interested in the
155 // output from the process. Since the process will run happily
156 // without the QProcess object, this instance and its data can then
157 // go too, but disconnect the signals to prevent further functions in this
158 // class being called after it has been deleted (Qt seems not to be
159 // disconnecting them itself)
160
161 mOutput = nullptr;
162
163 disconnect( mProcess, &QProcess::errorOccurred, this, &QgsRunProcess::processError );
164 disconnect( mProcess, &QProcess::readyReadStandardOutput, this, &QgsRunProcess::stdoutAvailable );
165 disconnect( mProcess, &QProcess::readyReadStandardError, this, &QgsRunProcess::stderrAvailable );
166 disconnect( mProcess, static_cast < void ( QProcess::* )( int, QProcess::ExitStatus ) >( &QProcess::finished ), this, &QgsRunProcess::processExit );
167
168 die();
169}
170
171void QgsRunProcess::processError( QProcess::ProcessError err )
172{
173 if ( err == QProcess::FailedToStart )
174 {
175 QgsMessageOutput *output = mOutput ? mOutput : QgsMessageOutput::createMessageOutput();
176 output->setMessage( tr( "Unable to run command %1" ).arg( mCommand ), QgsMessageOutput::MessageText );
177 // Didn't work, so no need to hang around
178 die();
179 }
180 else
181 {
182 QgsDebugMsg( "Got error: " + QString( "%d" ).arg( err ) );
183 }
184}
185
186QStringList QgsRunProcess::splitCommand( const QString &command )
187{
188#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
189 return QProcess::splitCommand( command );
190#else
191 // taken from Qt 5.15's implementation
192 QStringList args;
193 QString tmp;
194 int quoteCount = 0;
195 bool inQuote = false;
196
197 // handle quoting. tokens can be surrounded by double quotes
198 // "hello world". three consecutive double quotes represent
199 // the quote character itself.
200 for ( int i = 0; i < command.size(); ++i )
201 {
202 if ( command.at( i ) == QLatin1Char( '"' ) )
203 {
204 ++quoteCount;
205 if ( quoteCount == 3 )
206 {
207 // third consecutive quote
208 quoteCount = 0;
209 tmp += command.at( i );
210 }
211 continue;
212 }
213 if ( quoteCount )
214 {
215 if ( quoteCount == 1 )
216 inQuote = !inQuote;
217 quoteCount = 0;
218 }
219 if ( !inQuote && command.at( i ).isSpace() )
220 {
221 if ( !tmp.isEmpty() )
222 {
223 args += tmp;
224 tmp.clear();
225 }
226 }
227 else
228 {
229 tmp += command.at( i );
230 }
231 }
232 if ( !tmp.isEmpty() )
233 args += tmp;
234
235 return args;
236#endif
237}
238#else
239QgsRunProcess::QgsRunProcess( const QString &action, bool )
240{
241 Q_UNUSED( action )
242 QgsDebugMsg( "Skipping command: " + action );
243}
244
245QgsRunProcess::~QgsRunProcess()
246{
247}
248
249QStringList QgsRunProcess::splitCommand( const QString & )
250{
251 return QStringList();
252}
253#endif
254
255
256//
257// QgsBlockingProcess
258//
259
260#if QT_CONFIG(process)
261QgsBlockingProcess::QgsBlockingProcess( const QString &process, const QStringList &arguments )
262 : QObject()
263 , mProcess( process )
264 , mArguments( arguments )
265{
266
267}
268
269int QgsBlockingProcess::run( QgsFeedback *feedback )
270{
271 const bool requestMadeFromMainThread = QThread::currentThread() == QCoreApplication::instance()->thread();
272
273 int result = 0;
274 QProcess::ExitStatus exitStatus = QProcess::NormalExit;
275 QProcess::ProcessError error = QProcess::UnknownError;
276
277 const std::function<void()> runFunction = [ this, &result, &exitStatus, &error, feedback]()
278 {
279 // this function will always be run in worker threads -- either the blocking call is being made in a worker thread,
280 // or the blocking call has been made from the main thread and we've fired up a new thread for this function
281 Q_ASSERT( QThread::currentThread() != QgsApplication::instance()->thread() );
282
283 QProcess p;
284 const QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
285 p.setProcessEnvironment( env );
286
287 QEventLoop loop;
288 // connecting to aboutToQuit avoids an on-going process to remain stalled
289 // when QThreadPool::globalInstance()->waitForDone()
290 // is called at process termination
291 connect( qApp, &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit, Qt::DirectConnection );
292
293 if ( feedback )
294 QObject::connect( feedback, &QgsFeedback::canceled, &p, [ &p]
295 {
296#ifdef Q_OS_WIN
297 // From the qt docs:
298 // "Console applications on Windows that do not run an event loop, or whose
299 // event loop does not handle the WM_CLOSE message, can only be terminated by calling kill()."
300 p.kill();
301#else
302 p.terminate();
303#endif
304 } );
305 connect( &p, qOverload< int, QProcess::ExitStatus >( &QProcess::finished ), this, [&loop, &result, &exitStatus]( int res, QProcess::ExitStatus st )
306 {
307 result = res;
308 exitStatus = st;
309 loop.quit();
310 }, Qt::DirectConnection );
311
312 connect( &p, &QProcess::readyReadStandardOutput, &p, [&p, this]
313 {
314 const QByteArray ba = p.readAllStandardOutput();
315 mStdoutHandler( ba );
316 } );
317 connect( &p, &QProcess::readyReadStandardError, &p, [&p, this]
318 {
319 const QByteArray ba = p.readAllStandardError();
320 mStderrHandler( ba );
321 } );
322 p.start( mProcess, mArguments, QProcess::Unbuffered | QProcess::ReadWrite );
323 if ( !p.waitForStarted() )
324 {
325 result = 1;
326 exitStatus = QProcess::NormalExit;
327 error = p.error();
328 }
329 else
330 {
331 loop.exec();
332 }
333
334 mStdoutHandler( p.readAllStandardOutput() );
335 mStderrHandler( p.readAllStandardError() );
336 };
337
338 if ( requestMadeFromMainThread )
339 {
340 std::unique_ptr<ProcessThread> processThread = std::make_unique<ProcessThread>( runFunction );
341 processThread->start();
342 // wait for thread to gracefully exit
343 processThread->wait();
344 }
345 else
346 {
347 runFunction();
348 }
349
350 mExitStatus = exitStatus;
351 mProcessError = error;
352 return result;
353}
354
355QProcess::ExitStatus QgsBlockingProcess::exitStatus() const
356{
357 return mExitStatus;
358};
359
360QProcess::ProcessError QgsBlockingProcess::processError() const
361{
362 return mProcessError;
363};
364#endif // QT_CONFIG(process)
static QgsApplication * instance()
Returns the singleton instance of the QgsApplication.
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition: qgsfeedback.h:45
void canceled()
Internal routines can connect to this signal if they use event loop.
Interface for showing messages from QGIS in GUI independent way.
static QgsMessageOutput * createMessageOutput()
function that returns new class derived from QgsMessageOutput (don't forget to delete it then if show...
virtual void setMessage(const QString &message, MessageType msgType)=0
Sets message, it won't be displayed until.
static QStringList splitCommand(const QString &command)
Splits the string command into a list of tokens, and returns the list.
#define QgsDebugMsg(str)
Definition: qgslogger.h:38