QGIS API Documentation  3.22.4-Białowieża (ce8e65e95e)
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)
34 QgsRunProcess::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 ( ! mProcess->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 
101 QgsRunProcess::~QgsRunProcess()
102 {
103  delete mProcess;
104 }
105 
106 void QgsRunProcess::die()
107 {
108  // safe way to do "delete this" for QObjects
109  deleteLater();
110 }
111 
112 void 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 
122 void 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 
132 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  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 
152 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  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 
171 void 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 
186 QStringList 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
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 QgsBlockingProcess::QgsBlockingProcess( const QString &process, const QStringList &arguments )
262  : QObject()
263  , mProcess( process )
264  , mArguments( arguments )
265 {
266 
267 }
268 
269 int 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 
355 QProcess::ExitStatus QgsBlockingProcess::exitStatus() const
356 {
357  return mExitStatus;
358 };
359 
360 QProcess::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