Branch data Line data Source code
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)
34 : 0 : QgsRunProcess::QgsRunProcess( const QString &action, bool capture )
35 : :
36 : 0 : {
37 : : // Make up a string from the command and arguments that we'll use
38 : : // for display purposes
39 : 0 : QgsDebugMsg( "Running command: " + action );
40 : :
41 : 0 : mCommand = action;
42 : :
43 : : #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
44 : 0 : QStringList arguments = QProcess::splitCommand( action );
45 : 0 : const QString command = arguments.value( 0 );
46 : 0 : if ( !arguments.isEmpty() )
47 : 0 : arguments.removeFirst();
48 : : #endif
49 : :
50 : 0 : mProcess = new QProcess;
51 : :
52 : 0 : if ( capture )
53 : : {
54 : 0 : connect( mProcess, &QProcess::errorOccurred, this, &QgsRunProcess::processError );
55 : 0 : connect( mProcess, &QProcess::readyReadStandardOutput, this, &QgsRunProcess::stdoutAvailable );
56 : 0 : 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 : 0 : 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.
64 : 0 : mOutput = QgsMessageOutput::createMessageOutput();
65 : 0 : mOutput->setTitle( action );
66 : 0 : mOutput->setMessage( tr( "<b>Starting %1…</b>" ).arg( action ), QgsMessageOutput::MessageHtml );
67 : 0 : mOutput->showMessage( false ); // non-blocking
68 : :
69 : : // get notification of delete if it's derived from QObject
70 : 0 : QObject *mOutputObj = dynamic_cast<QObject *>( mOutput );
71 : 0 : if ( mOutputObj )
72 : : {
73 : 0 : connect( mOutputObj, &QObject::destroyed, this, &QgsRunProcess::dialogGone );
74 : 0 : }
75 : :
76 : : // start the process!
77 : : #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
78 : : mProcess->start( action );
79 : : #else
80 : 0 : mProcess->start( command, arguments );
81 : 0 : #endif
82 : 0 : }
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 : 0 : if ( ! mProcess->startDetached( command, arguments ) ) // let the program run by itself
89 : : #endif
90 : : {
91 : 0 : QMessageBox::critical( nullptr, tr( "Action" ),
92 : 0 : tr( "Unable to run command\n%1" ).arg( action ),
93 : : QMessageBox::Ok, Qt::NoButton );
94 : 0 : }
95 : : // We're not capturing the output from the process, so we don't
96 : : // need to exist anymore.
97 : 0 : die();
98 : : }
99 : 0 : }
100 : :
101 : 0 : QgsRunProcess::~QgsRunProcess()
102 : 0 : {
103 : 0 : delete mProcess;
104 : 0 : }
105 : :
106 : 0 : void QgsRunProcess::die()
107 : : {
108 : : // safe way to do "delete this" for QObjects
109 : 0 : deleteLater();
110 : 0 : }
111 : :
112 : 0 : void QgsRunProcess::stdoutAvailable()
113 : : {
114 : 0 : QByteArray bytes( mProcess->readAllStandardOutput() );
115 : 0 : QTextCodec *codec = QTextCodec::codecForLocale();
116 : 0 : QString line( codec->toUnicode( bytes ) );
117 : :
118 : : // Add the new output to the dialog box
119 : 0 : mOutput->appendMessage( line );
120 : 0 : }
121 : :
122 : 0 : void QgsRunProcess::stderrAvailable()
123 : : {
124 : 0 : QByteArray bytes( mProcess->readAllStandardOutput() );
125 : 0 : QTextCodec *codec = QTextCodec::codecForLocale();
126 : 0 : QString line( codec->toUnicode( bytes ) );
127 : :
128 : : // Add the new output to the dialog box, but color it red
129 : 0 : mOutput->appendMessage( "<font color=red>" + line + "</font>" );
130 : 0 : }
131 : :
132 : 0 : void 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 : 0 : if ( mOutput )
142 : : {
143 : 0 : mOutput->appendMessage( "<b>" + tr( "Done" ) + "</b>" );
144 : 0 : }
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 : 0 : die();
150 : 0 : }
151 : :
152 : 0 : void 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 : 0 : mOutput = nullptr;
162 : :
163 : 0 : disconnect( mProcess, &QProcess::errorOccurred, this, &QgsRunProcess::processError );
164 : 0 : disconnect( mProcess, &QProcess::readyReadStandardOutput, this, &QgsRunProcess::stdoutAvailable );
165 : 0 : disconnect( mProcess, &QProcess::readyReadStandardError, this, &QgsRunProcess::stderrAvailable );
166 : 0 : disconnect( mProcess, static_cast < void ( QProcess::* )( int, QProcess::ExitStatus ) >( &QProcess::finished ), this, &QgsRunProcess::processExit );
167 : :
168 : 0 : die();
169 : 0 : }
170 : :
171 : 0 : void QgsRunProcess::processError( QProcess::ProcessError err )
172 : : {
173 : 0 : if ( err == QProcess::FailedToStart )
174 : : {
175 : 0 : QgsMessageOutput *output = mOutput ? mOutput : QgsMessageOutput::createMessageOutput();
176 : 0 : output->setMessage( tr( "Unable to run command %1" ).arg( mCommand ), QgsMessageOutput::MessageText );
177 : : // Didn't work, so no need to hang around
178 : 0 : die();
179 : 0 : }
180 : : else
181 : : {
182 : 0 : QgsDebugMsg( "Got error: " + QString( "%d" ).arg( err ) );
183 : : }
184 : 0 : }
185 : :
186 : 0 : QStringList QgsRunProcess::splitCommand( const QString &command )
187 : : {
188 : : #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
189 : 0 : 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 : 0 : if ( command.at( i ) == QLatin1Char( '"' ) )
203 : 0 : {
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
239 : : QgsRunProcess::QgsRunProcess( const QString &action, bool )
240 : : {
241 : : Q_UNUSED( action )
242 : : QgsDebugMsg( "Skipping command: " + action );
243 : : }
244 : :
245 : : QgsRunProcess::~QgsRunProcess()
246 : : {
247 : : }
248 : :
249 : : QStringList QgsRunProcess::splitCommand( const QString & )
250 : : {
251 : : return QStringList();
252 : : }
253 : : #endif
254 : :
255 : :
256 : : //
257 : : // QgsBlockingProcess
258 : : //
259 : :
260 : : #if QT_CONFIG(process)
261 : 0 : QgsBlockingProcess::QgsBlockingProcess( const QString &process, const QStringList &arguments )
262 : 0 : : QObject()
263 : 0 : , mProcess( process )
264 : 0 : , mArguments( arguments )
265 : 0 : {
266 : :
267 : 0 : }
268 : :
269 : 0 : int QgsBlockingProcess::run( QgsFeedback *feedback )
270 : : {
271 : 0 : const bool requestMadeFromMainThread = QThread::currentThread() == QCoreApplication::instance()->thread();
272 : :
273 : 0 : int result = 0;
274 : 0 : QProcess::ExitStatus exitStatus = QProcess::NormalExit;
275 : 0 : QProcess::ProcessError error = QProcess::UnknownError;
276 : :
277 : 0 : 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 : 0 : QProcess p;
284 : 0 : QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
285 : 0 : p.setProcessEnvironment( env );
286 : :
287 : 0 : 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 : 0 : connect( qApp, &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit, Qt::DirectConnection );
292 : :
293 : 0 : if ( feedback )
294 : 0 : 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 : 0 : p.terminate();
303 : : #endif
304 : 0 : } );
305 : 0 : connect( &p, qOverload< int, QProcess::ExitStatus >( &QProcess::finished ), this, [&loop, &result, &exitStatus]( int res, QProcess::ExitStatus st )
306 : : {
307 : 0 : result = res;
308 : 0 : exitStatus = st;
309 : 0 : loop.quit();
310 : 0 : }, Qt::DirectConnection );
311 : :
312 : 0 : connect( &p, &QProcess::readyReadStandardOutput, &p, [&p, this]
313 : : {
314 : 0 : QByteArray ba = p.readAllStandardOutput();
315 : 0 : mStdoutHandler( ba );
316 : 0 : } );
317 : 0 : connect( &p, &QProcess::readyReadStandardError, &p, [&p, this]
318 : : {
319 : 0 : QByteArray ba = p.readAllStandardError();
320 : 0 : mStderrHandler( ba );
321 : 0 : } );
322 : 0 : p.start( mProcess, mArguments, QProcess::Unbuffered | QProcess::ReadWrite );
323 : 0 : if ( !p.waitForStarted() )
324 : : {
325 : 0 : result = 1;
326 : 0 : exitStatus = QProcess::NormalExit;
327 : 0 : error = p.error();
328 : 0 : }
329 : : else
330 : : {
331 : 0 : loop.exec();
332 : : }
333 : :
334 : 0 : mStdoutHandler( p.readAllStandardOutput() );
335 : 0 : mStderrHandler( p.readAllStandardError() );
336 : 0 : };
337 : :
338 : 0 : if ( requestMadeFromMainThread )
339 : : {
340 : 0 : std::unique_ptr<ProcessThread> processThread = std::make_unique<ProcessThread>( runFunction );
341 : 0 : processThread->start();
342 : : // wait for thread to gracefully exit
343 : 0 : processThread->wait();
344 : 0 : }
345 : : else
346 : : {
347 : 0 : runFunction();
348 : : }
349 : :
350 : 0 : mExitStatus = exitStatus;
351 : 0 : mProcessError = error;
352 : 0 : return result;
353 : 0 : }
354 : :
355 : 0 : QProcess::ExitStatus QgsBlockingProcess::exitStatus() const
356 : : {
357 : 0 : return mExitStatus;
358 : : };
359 : :
360 : 0 : QProcess::ProcessError QgsBlockingProcess::processError() const
361 : : {
362 : 0 : return mProcessError;
363 : : };
364 : : #endif // QT_CONFIG(process)
|