File indexing completed on 2024-04-28 04:37:00
0001 /* 0002 SPDX-FileCopyrightText: 2012 Ivan Shapovalov <intelfx100@gmail.com> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "outputexecutejob.h" 0008 #include "outputmodel.h" 0009 #include "outputdelegate.h" 0010 #include "debug.h" 0011 #include <interfaces/icore.h> 0012 #include <interfaces/iruntime.h> 0013 #include <interfaces/iruntimecontroller.h> 0014 #include <util/environmentprofilelist.h> 0015 #include <util/processlinemaker.h> 0016 #include <KProcess> 0017 #include <KLocalizedString> 0018 #include <KShell> 0019 #include <QFileInfo> 0020 #include <QDir> 0021 0022 namespace KDevelop 0023 { 0024 0025 class OutputExecuteJobPrivate 0026 { 0027 public: 0028 explicit OutputExecuteJobPrivate( KDevelop::OutputExecuteJob* owner ); 0029 0030 void childProcessStdout(); 0031 void childProcessStderr(); 0032 0033 void emitProgress(const IFilterStrategy::Progress& progress); 0034 0035 QString joinCommandLine() const; 0036 QString jobDisplayName() const; 0037 0038 template< typename T > 0039 static void mergeEnvironment( QProcessEnvironment& dest, const T& src ); 0040 QProcessEnvironment effectiveEnvironment(const QUrl& workingDirectory) const; 0041 QStringList effectiveCommandLine() const; 0042 0043 OutputExecuteJob* m_owner; 0044 0045 KProcess* m_process; 0046 ProcessLineMaker* m_lineMaker; 0047 OutputExecuteJob::JobStatus m_status; 0048 OutputExecuteJob::JobProperties m_properties; 0049 OutputModel::OutputFilterStrategy m_filteringStrategy; 0050 QScopedPointer<IFilterStrategy> m_filteringStrategyPtr; 0051 QStringList m_arguments; 0052 QStringList m_privilegedExecutionCommand; 0053 QUrl m_workingDirectory; 0054 QString m_environmentProfile; 0055 QHash<QString, QString> m_environmentOverrides; 0056 QString m_jobName; 0057 bool m_outputStarted; 0058 bool m_executeOnHost = false; 0059 bool m_checkExitCode = true; 0060 }; 0061 0062 OutputExecuteJobPrivate::OutputExecuteJobPrivate( OutputExecuteJob* owner ) : 0063 m_owner( owner ), 0064 m_process( new KProcess( m_owner ) ), 0065 m_lineMaker( new ProcessLineMaker( m_owner ) ), // do not assign process to the line maker as we'll feed it data ourselves 0066 m_status( OutputExecuteJob::JobNotStarted ), 0067 m_properties( OutputExecuteJob::DisplayStdout ), 0068 m_filteringStrategy( OutputModel::NoFilter ), 0069 m_outputStarted( false ) 0070 { 0071 } 0072 0073 OutputExecuteJob::OutputExecuteJob( QObject* parent, OutputJob::OutputJobVerbosity verbosity ): 0074 OutputJob( parent, verbosity ), 0075 d_ptr(new OutputExecuteJobPrivate(this)) 0076 { 0077 Q_D(OutputExecuteJob); 0078 0079 d->m_process->setOutputChannelMode( KProcess::SeparateChannels ); 0080 0081 connect( d->m_process, QOverload<int,QProcess::ExitStatus>::of(&QProcess::finished), 0082 this, &OutputExecuteJob::childProcessExited ); 0083 connect( d->m_process, &QProcess::errorOccurred, 0084 this, &OutputExecuteJob::childProcessError ); 0085 connect( d->m_process, &KProcess::readyReadStandardOutput, 0086 this, [this] { Q_D(OutputExecuteJob); d->childProcessStdout(); } ); 0087 connect( d->m_process, &KProcess::readyReadStandardError, 0088 this, [this] { Q_D(OutputExecuteJob); d->childProcessStderr(); } ); 0089 } 0090 0091 OutputExecuteJob::~OutputExecuteJob() 0092 { 0093 Q_D(OutputExecuteJob); 0094 0095 // indicates if process is running and survives kill, then we cannot do anything 0096 bool killSuccessful = d->m_process->state() == QProcess::NotRunning; 0097 if( !killSuccessful ) { 0098 killSuccessful = doKill(); 0099 } 0100 0101 Q_ASSERT( d->m_process->state() == QProcess::NotRunning || !killSuccessful ); 0102 } 0103 0104 OutputExecuteJob::JobStatus OutputExecuteJob::status() const 0105 { 0106 Q_D(const OutputExecuteJob); 0107 0108 return d->m_status; 0109 } 0110 0111 OutputModel* OutputExecuteJob::model() const 0112 { 0113 return qobject_cast<OutputModel*>(OutputJob::model()); 0114 } 0115 0116 QStringList OutputExecuteJob::commandLine() const 0117 { 0118 Q_D(const OutputExecuteJob); 0119 0120 return d->m_arguments; 0121 } 0122 0123 OutputExecuteJob& OutputExecuteJob::operator<<( const QString& argument ) 0124 { 0125 Q_D(OutputExecuteJob); 0126 0127 d->m_arguments << argument; 0128 return *this; 0129 } 0130 0131 OutputExecuteJob& OutputExecuteJob::operator<<( const QStringList& arguments ) 0132 { 0133 Q_D(OutputExecuteJob); 0134 0135 d->m_arguments << arguments; 0136 return *this; 0137 } 0138 0139 QStringList OutputExecuteJob::privilegedExecutionCommand() const 0140 { 0141 Q_D(const OutputExecuteJob); 0142 0143 return d->m_privilegedExecutionCommand; 0144 } 0145 0146 void OutputExecuteJob::setPrivilegedExecutionCommand( const QStringList& command ) 0147 { 0148 Q_D(OutputExecuteJob); 0149 0150 d->m_privilegedExecutionCommand = command; 0151 } 0152 0153 void OutputExecuteJob::setJobName( const QString& name ) 0154 { 0155 Q_D(OutputExecuteJob); 0156 0157 d->m_jobName = name; 0158 0159 const QString jobDisplayName = d->jobDisplayName(); 0160 setObjectName(jobDisplayName); 0161 setTitle(jobDisplayName); 0162 setToolTitle(jobDisplayName); 0163 } 0164 0165 QUrl OutputExecuteJob::workingDirectory() const 0166 { 0167 Q_D(const OutputExecuteJob); 0168 0169 return d->m_workingDirectory; 0170 } 0171 0172 void OutputExecuteJob::setWorkingDirectory( const QUrl& url ) 0173 { 0174 Q_D(OutputExecuteJob); 0175 0176 d->m_workingDirectory = url; 0177 } 0178 0179 void OutputExecuteJob::start() 0180 { 0181 Q_D(OutputExecuteJob); 0182 0183 Q_ASSERT( d->m_status == JobNotStarted ); 0184 d->m_status = JobRunning; 0185 0186 const bool isBuilder = d->m_properties.testFlag( IsBuilderHint ); 0187 0188 const QUrl effectiveWorkingDirectory = workingDirectory(); 0189 if( effectiveWorkingDirectory.isEmpty() ) { 0190 if( d->m_properties.testFlag( NeedWorkingDirectory ) ) { 0191 // A directory is not given, but we need it. 0192 setError( InvalidWorkingDirectoryError ); 0193 if( isBuilder ) { 0194 setErrorText( i18n( "No build directory specified for a builder job." ) ); 0195 } else { 0196 setErrorText( i18n( "No working directory specified for a process." ) ); 0197 } 0198 emitResult(); 0199 return; 0200 } 0201 0202 setModel( new OutputModel ); 0203 } else { 0204 // Basic sanity checks. 0205 if( !effectiveWorkingDirectory.isValid() ) { 0206 setError( InvalidWorkingDirectoryError ); 0207 if( isBuilder ) { 0208 setErrorText( i18n( "Invalid build directory '%1'", effectiveWorkingDirectory.toDisplayString(QUrl::PreferLocalFile) ) ); 0209 } else { 0210 setErrorText( i18n( "Invalid working directory '%1'", effectiveWorkingDirectory.toDisplayString(QUrl::PreferLocalFile) ) ); 0211 } 0212 emitResult(); 0213 return; 0214 } else if( !effectiveWorkingDirectory.isLocalFile() ) { 0215 setError( InvalidWorkingDirectoryError ); 0216 if( isBuilder ) { 0217 setErrorText( i18n( "Build directory '%1' is not a local path", effectiveWorkingDirectory.toDisplayString(QUrl::PreferLocalFile) ) ); 0218 } else { 0219 setErrorText( i18n( "Working directory '%1' is not a local path", effectiveWorkingDirectory.toDisplayString(QUrl::PreferLocalFile) ) ); 0220 } 0221 emitResult(); 0222 return; 0223 } 0224 0225 QFileInfo workingDirInfo( effectiveWorkingDirectory.toLocalFile() ); 0226 if( !workingDirInfo.isDir() ) { 0227 // If a working directory does not actually exist, either bail out or create it empty, 0228 // depending on what we need by properties. 0229 // We use a dedicated bool variable since !isDir() may also mean that it exists, 0230 // but is not a directory, or a symlink to an inexistent object. 0231 bool successfullyCreated = false; 0232 if( !d->m_properties.testFlag( CheckWorkingDirectory ) ) { 0233 successfullyCreated = QDir().mkdir( effectiveWorkingDirectory.toLocalFile() ); 0234 } 0235 if( !successfullyCreated ) { 0236 setError( InvalidWorkingDirectoryError ); 0237 if( isBuilder ) { 0238 setErrorText( i18n( "Build directory '%1' does not exist or is not a directory", effectiveWorkingDirectory.toDisplayString(QUrl::PreferLocalFile) ) ); 0239 } else { 0240 setErrorText( i18n( "Working directory '%1' does not exist or is not a directory", effectiveWorkingDirectory.toDisplayString(QUrl::PreferLocalFile) ) ); 0241 } 0242 emitResult(); 0243 return; 0244 } 0245 } 0246 0247 setModel( new OutputModel( effectiveWorkingDirectory ) ); 0248 } 0249 Q_ASSERT( model() ); 0250 0251 if (d->m_filteringStrategyPtr) { 0252 model()->setFilteringStrategy(d->m_filteringStrategyPtr.take()); 0253 } else { 0254 model()->setFilteringStrategy(d->m_filteringStrategy); 0255 } 0256 0257 setDelegate( new OutputDelegate ); 0258 0259 connect(model(), &OutputModel::progress, this, [this](const IFilterStrategy::Progress& progress) { 0260 Q_D(OutputExecuteJob); 0261 d->emitProgress(progress); 0262 }); 0263 0264 // Slots hasRawStdout() and hasRawStderr() are responsible 0265 // for feeding raw data to the line maker; so property-based channel filtering is implemented there. 0266 if( d->m_properties.testFlag( PostProcessOutput ) ) { 0267 connect( d->m_lineMaker, &ProcessLineMaker::receivedStdoutLines, 0268 this, &OutputExecuteJob::postProcessStdout ); 0269 connect( d->m_lineMaker, &ProcessLineMaker::receivedStderrLines, 0270 this, &OutputExecuteJob::postProcessStderr ); 0271 } else { 0272 connect( d->m_lineMaker, &ProcessLineMaker::receivedStdoutLines, model(), 0273 &OutputModel::appendLines ); 0274 connect( d->m_lineMaker, &ProcessLineMaker::receivedStderrLines, model(), 0275 &OutputModel::appendLines ); 0276 } 0277 0278 if( !d->m_properties.testFlag( NoSilentOutput ) || verbosity() != Silent ) { 0279 d->m_outputStarted = true; 0280 startOutput(); 0281 } 0282 0283 if( !effectiveWorkingDirectory.isEmpty() ) { 0284 d->m_process->setWorkingDirectory( effectiveWorkingDirectory.toLocalFile() ); 0285 } 0286 0287 d->m_process->setProcessEnvironment( d->effectiveEnvironment(effectiveWorkingDirectory) ); 0288 0289 if (!d->effectiveCommandLine().isEmpty()) { 0290 d->m_process->setProgram( d->effectiveCommandLine() ); 0291 // there is no way to input data in the output view so redirect stdin to the null device 0292 d->m_process->setStandardInputFile(QProcess::nullDevice()); 0293 qCDebug(OUTPUTVIEW) << "Starting:" << d->effectiveCommandLine() << d->m_process->program() << "in" << d->m_process->workingDirectory(); 0294 if (d->m_executeOnHost) { 0295 d->m_process->start(); 0296 } else { 0297 KDevelop::ICore::self()->runtimeController()->currentRuntime()->startProcess(d->m_process); 0298 } 0299 model()->appendLine(d->m_process->workingDirectory() + QLatin1String("> ") + KShell::joinArgs(d->m_process->program())); 0300 } else { 0301 QString errorMessage = i18n("Failed to specify program to start: %1", d->joinCommandLine()); 0302 model()->appendLine( i18n( "*** %1 ***", errorMessage) ); 0303 setErrorText(errorMessage); 0304 setError( FailedShownError ); 0305 emitResult(); 0306 } 0307 } 0308 0309 bool OutputExecuteJob::doKill() 0310 { 0311 Q_D(OutputExecuteJob); 0312 0313 const int terminateKillTimeout = 1000; // msecs 0314 0315 if( d->m_status != JobRunning ) { 0316 return true; 0317 } 0318 d->m_status = JobCanceled; 0319 0320 d->m_process->terminate(); 0321 bool terminated = d->m_process->waitForFinished( terminateKillTimeout ); 0322 if( !terminated ) { 0323 d->m_process->kill(); 0324 terminated = d->m_process->waitForFinished( terminateKillTimeout ); 0325 } 0326 d->m_lineMaker->flushBuffers(); 0327 if( terminated ) { 0328 model()->appendLine( i18n( "*** Killed process ***" ) ); 0329 } else { 0330 // It survived SIGKILL, leave it alone... 0331 qCWarning(OUTPUTVIEW) << "Could not kill the running process:" << d->m_process->error(); 0332 model()->appendLine( i18n( "*** Warning: could not kill the process ***" ) ); 0333 return false; 0334 } 0335 return true; 0336 } 0337 0338 void OutputExecuteJob::childProcessError( QProcess::ProcessError processError ) 0339 { 0340 Q_D(OutputExecuteJob); 0341 0342 // This can be called twice: one time via an error() signal, and second - from childProcessExited(). 0343 // Avoid doing things in second time. 0344 if( d->m_status != OutputExecuteJob::JobRunning ) 0345 return; 0346 d->m_status = OutputExecuteJob::JobFailed; 0347 0348 qCWarning(OUTPUTVIEW) << "process error:" << processError << d->m_process->errorString() 0349 << ", the command line:" << d->joinCommandLine(); 0350 0351 QString errorValue; 0352 switch( processError ) { 0353 case QProcess::FailedToStart: 0354 errorValue = i18n("%1 has failed to start", commandLine().at(0)); 0355 break; 0356 0357 case QProcess::Crashed: 0358 errorValue = i18n("%1 has crashed", commandLine().at(0)); 0359 break; 0360 0361 case QProcess::ReadError: 0362 errorValue = i18n("Read error"); 0363 break; 0364 0365 case QProcess::WriteError: 0366 errorValue = i18n("Write error"); 0367 break; 0368 0369 case QProcess::Timedout: 0370 errorValue = i18n("Waiting for the process has timed out"); 0371 break; 0372 0373 case QProcess::UnknownError: 0374 errorValue = i18n("Exit code %1", d->m_process->exitCode()); 0375 break; 0376 } 0377 0378 // Show the tool view if it's hidden for the user to be able to diagnose errors. 0379 if( !d->m_outputStarted ) { 0380 d->m_outputStarted = true; 0381 startOutput(); 0382 } 0383 0384 setError( FailedShownError ); 0385 setErrorText( errorValue ); 0386 d->m_lineMaker->flushBuffers(); 0387 model()->appendLine( i18n("*** Failure: %1 ***", errorValue) ); 0388 emitResult(); 0389 } 0390 0391 void OutputExecuteJob::childProcessExited( int exitCode, QProcess::ExitStatus exitStatus ) 0392 { 0393 Q_D(OutputExecuteJob); 0394 0395 if( d->m_status != JobRunning ) 0396 return; 0397 0398 if( exitStatus == QProcess::CrashExit ) { 0399 childProcessError( QProcess::Crashed ); 0400 } else if ( d->m_checkExitCode && exitCode != 0 ) { 0401 childProcessError( QProcess::UnknownError ); 0402 } else { 0403 d->m_status = JobSucceeded; 0404 d->m_lineMaker->flushBuffers(); 0405 model()->appendLine( i18n("*** Finished ***") ); 0406 emitResult(); 0407 } 0408 } 0409 0410 void OutputExecuteJobPrivate::childProcessStdout() 0411 { 0412 QByteArray out = m_process->readAllStandardOutput(); 0413 qCDebug(OUTPUTVIEW) << out; 0414 if( m_properties.testFlag( OutputExecuteJob::DisplayStdout ) ) { 0415 m_lineMaker->slotReceivedStdout( out ); 0416 } 0417 } 0418 0419 void OutputExecuteJobPrivate::childProcessStderr() 0420 { 0421 QByteArray err = m_process->readAllStandardError(); 0422 qCDebug(OUTPUTVIEW) << err; 0423 if( m_properties.testFlag( OutputExecuteJob::DisplayStderr ) ) { 0424 m_lineMaker->slotReceivedStderr( err ); 0425 } 0426 } 0427 0428 void OutputExecuteJobPrivate::emitProgress(const IFilterStrategy::Progress& progress) 0429 { 0430 if (progress.percent != -1) { 0431 m_owner->emitPercent(progress.percent, 100); 0432 } 0433 if (!progress.status.isEmpty()) { 0434 emit m_owner->infoMessage(m_owner, progress.status); 0435 } 0436 } 0437 0438 void OutputExecuteJob::postProcessStdout( const QStringList& lines ) 0439 { 0440 model()->appendLines( lines ); 0441 } 0442 0443 void OutputExecuteJob::postProcessStderr( const QStringList& lines ) 0444 { 0445 model()->appendLines( lines ); 0446 } 0447 0448 void OutputExecuteJob::setFilteringStrategy( OutputModel::OutputFilterStrategy strategy ) 0449 { 0450 Q_D(OutputExecuteJob); 0451 0452 d->m_filteringStrategy = strategy; 0453 0454 // clear the other 0455 d->m_filteringStrategyPtr.reset(nullptr); 0456 } 0457 0458 void OutputExecuteJob::setFilteringStrategy(IFilterStrategy* filterStrategy) 0459 { 0460 Q_D(OutputExecuteJob); 0461 0462 d->m_filteringStrategyPtr.reset(filterStrategy); 0463 0464 // clear the other 0465 d->m_filteringStrategy = OutputModel::NoFilter; 0466 } 0467 0468 OutputExecuteJob::JobProperties OutputExecuteJob::properties() const 0469 { 0470 Q_D(const OutputExecuteJob); 0471 0472 return d->m_properties; 0473 } 0474 0475 void OutputExecuteJob::setProperties( OutputExecuteJob::JobProperties properties, bool override ) 0476 { 0477 Q_D(OutputExecuteJob); 0478 0479 if( override ) { 0480 d->m_properties = properties; 0481 } else { 0482 d->m_properties |= properties; 0483 } 0484 } 0485 0486 void OutputExecuteJob::unsetProperties( OutputExecuteJob::JobProperties properties ) 0487 { 0488 Q_D(OutputExecuteJob); 0489 0490 d->m_properties &= ~properties; 0491 } 0492 0493 QString OutputExecuteJob::environmentProfile() const 0494 { 0495 Q_D(const OutputExecuteJob); 0496 0497 return d->m_environmentProfile; 0498 } 0499 0500 void OutputExecuteJob::setEnvironmentProfile( const QString& profile ) 0501 { 0502 Q_D(OutputExecuteJob); 0503 0504 d->m_environmentProfile = profile; 0505 } 0506 0507 void OutputExecuteJob::addEnvironmentOverride( const QString& name, const QString& value ) 0508 { 0509 Q_D(OutputExecuteJob); 0510 0511 d->m_environmentOverrides[name] = value; 0512 } 0513 0514 void OutputExecuteJob::removeEnvironmentOverride( const QString& name ) 0515 { 0516 Q_D(OutputExecuteJob); 0517 0518 d->m_environmentOverrides.remove( name ); 0519 } 0520 0521 0522 void OutputExecuteJob::setExecuteOnHost(bool executeHost) 0523 { 0524 Q_D(OutputExecuteJob); 0525 0526 d->m_executeOnHost = executeHost; 0527 } 0528 0529 bool OutputExecuteJob::executeOnHost() const 0530 { 0531 Q_D(const OutputExecuteJob); 0532 0533 return d->m_executeOnHost; 0534 } 0535 0536 bool OutputExecuteJob::checkExitCode() const 0537 { 0538 Q_D(const OutputExecuteJob); 0539 0540 return d->m_checkExitCode; 0541 } 0542 0543 void OutputExecuteJob::setCheckExitCode(bool check) 0544 { 0545 Q_D(OutputExecuteJob); 0546 0547 d->m_checkExitCode = check; 0548 } 0549 0550 template< typename T > 0551 void OutputExecuteJobPrivate::mergeEnvironment( QProcessEnvironment& dest, const T& src ) 0552 { 0553 for( typename T::const_iterator it = src.begin(); it != src.end(); ++it ) { 0554 dest.insert( it.key(), it.value() ); 0555 } 0556 } 0557 0558 QProcessEnvironment OutputExecuteJobPrivate::effectiveEnvironment(const QUrl& workingDirectory) const 0559 { 0560 const EnvironmentProfileList environmentProfiles(KSharedConfig::openConfig()); 0561 QString environmentProfile = m_owner->environmentProfile(); 0562 if( environmentProfile.isEmpty() ) { 0563 environmentProfile = environmentProfiles.defaultProfileName(); 0564 } 0565 QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); 0566 auto userEnv = environmentProfiles.variables(environmentProfile); 0567 expandVariables(userEnv, environment); 0568 0569 OutputExecuteJobPrivate::mergeEnvironment( environment, userEnv ); 0570 OutputExecuteJobPrivate::mergeEnvironment( environment, m_environmentOverrides ); 0571 if( m_properties.testFlag( OutputExecuteJob::PortableMessages ) ) { 0572 environment.remove( QStringLiteral( "LC_ALL" ) ); 0573 environment.insert( QStringLiteral( "LC_MESSAGES" ), QStringLiteral( "C" ) ); 0574 } 0575 if (!workingDirectory.isEmpty() && environment.contains(QStringLiteral("PWD"))) { 0576 // also update the environment variable for the cwd, otherwise scripts can break easily 0577 environment.insert(QStringLiteral("PWD"), workingDirectory.toLocalFile()); 0578 } 0579 return environment; 0580 } 0581 0582 QString OutputExecuteJobPrivate::joinCommandLine() const 0583 { 0584 return KShell::joinArgs( effectiveCommandLine() ); 0585 } 0586 0587 QStringList OutputExecuteJobPrivate::effectiveCommandLine() const 0588 { 0589 // If we need to use a su-like helper, invoke it as 0590 // "helper -- our command line". 0591 QStringList privilegedCommand = m_owner->privilegedExecutionCommand(); 0592 if( !privilegedCommand.isEmpty() ) { 0593 return QStringList() << m_owner->privilegedExecutionCommand() << QStringLiteral("--") << m_owner->commandLine(); 0594 } else { 0595 return m_owner->commandLine(); 0596 } 0597 } 0598 0599 QString OutputExecuteJobPrivate::jobDisplayName() const 0600 { 0601 const QString joinedCommandLine = joinCommandLine(); 0602 if( m_properties.testFlag( OutputExecuteJob::AppendProcessString ) ) { 0603 if( !m_jobName.isEmpty() ) { 0604 return m_jobName + QLatin1String(": ") + joinedCommandLine; 0605 } else { 0606 return joinedCommandLine; 0607 } 0608 } else { 0609 return m_jobName; 0610 } 0611 } 0612 0613 } // namespace KDevelop 0614 0615 #include "moc_outputexecutejob.cpp"