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"