diff --git a/outputview/outputexecutejob.cpp b/outputview/outputexecutejob.cpp index e7e56c81e2..fe90f7d13c 100644 --- a/outputview/outputexecutejob.cpp +++ b/outputview/outputexecutejob.cpp @@ -1,490 +1,493 @@ /* This file is part of KDevelop Copyright 2012 Ivan Shapovalov This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "outputexecutejob.h" #include "outputmodel.h" #include "outputdelegate.h" #include #include #include #include #include #include #include #include namespace KDevelop { class OutputExecuteJobPrivate { public: OutputExecuteJobPrivate( KDevelop::OutputExecuteJob* owner ); + void childProcessStdout(); + void childProcessStderr(); + QString joinCommandLine() const; QString getJobName(); static void mergeEnvironment( QProcessEnvironment& dest, const QMap& src ); QProcessEnvironment effectiveEnvironment() const; QStringList effectiveCommandLine() const; OutputExecuteJob* m_owner; KProcess* m_process; ProcessLineMaker* m_lineMaker; OutputExecuteJob::JobStatus m_status; OutputExecuteJob::JobProperties m_properties; OutputModel::OutputFilterStrategy m_filteringStrategy; QStringList m_arguments; QStringList m_privilegedExecutionCommand; KUrl m_workingDirectory; QString m_environmentProfile; QMap m_environmentOverrides; QString m_jobName; bool m_outputStarted; QByteArray m_processStdout; QByteArray m_processStderr; }; OutputExecuteJobPrivate::OutputExecuteJobPrivate( OutputExecuteJob* owner ) : m_owner( owner ), m_process( new KProcess( m_owner ) ), m_lineMaker( new ProcessLineMaker( m_owner ) ), // do not assign process to the line maker as we'll feed it data ourselves m_status( OutputExecuteJob::JobNotStarted ), m_properties( OutputExecuteJob::DisplayStdout ), m_filteringStrategy( OutputModel::NoFilter ), m_arguments(), m_privilegedExecutionCommand(), m_workingDirectory(), m_environmentProfile(), m_environmentOverrides(), m_jobName(), m_outputStarted( false ), m_processStdout(), m_processStderr() { } OutputExecuteJob::OutputExecuteJob( QObject* parent, OutputJob::OutputJobVerbosity verbosity ): OutputJob( parent, verbosity ), d( new OutputExecuteJobPrivate( this ) ) { d->m_process->setOutputChannelMode( KProcess::SeparateChannels ); d->m_process->setTextModeEnabled( true ); connect( d->m_process, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(childProcessExited(int,QProcess::ExitStatus)) ); connect( d->m_process, SIGNAL(error(QProcess::ProcessError)), SLOT(childProcessError(QProcess::ProcessError)) ); connect( d->m_process, SIGNAL(readyReadStandardOutput()), SLOT(childProcessStdout()) ); connect( d->m_process, SIGNAL(readyReadStandardError()), SLOT(childProcessStderr()) ); } OutputExecuteJob::~OutputExecuteJob() { if( d->m_process->state() != QProcess::NotRunning ) { doKill(); } Q_ASSERT( d->m_process->state() == QProcess::NotRunning ); delete d; } OutputExecuteJob::JobStatus OutputExecuteJob::status() const { return d->m_status; } OutputModel* OutputExecuteJob::model() const { return dynamic_cast ( OutputJob::model() ); } QStringList OutputExecuteJob::commandLine() const { return d->m_arguments; } OutputExecuteJob& OutputExecuteJob::operator<<( const QString& argument ) { d->m_arguments << argument; return *this; } OutputExecuteJob& OutputExecuteJob::operator<<( const QStringList& arguments ) { d->m_arguments << arguments; return *this; } QStringList OutputExecuteJob::privilegedExecutionCommand() const { return d->m_privilegedExecutionCommand; } void OutputExecuteJob::setPrivilegedExecutionCommand( const QStringList& command ) { d->m_privilegedExecutionCommand = command; } void OutputExecuteJob::setJobName( const QString& name ) { d->m_jobName = name; QString jobName = d->getJobName(); setObjectName( jobName ); setTitle( jobName ); } KUrl OutputExecuteJob::workingDirectory() const { return d->m_workingDirectory; } void OutputExecuteJob::setWorkingDirectory( const KUrl& url ) { d->m_workingDirectory = url; } void OutputExecuteJob::start() { Q_ASSERT( d->m_status == JobNotStarted ); d->m_status = JobRunning; const bool isBuilder = d->m_properties.testFlag( IsBuilderHint ); const KUrl effectiveWorkingDirectory = workingDirectory(); if( effectiveWorkingDirectory.isEmpty() ) { if( d->m_properties.testFlag( NeedWorkingDirectory ) ) { // A directory is not given, but we need it. setError( InvalidWorkingDirectoryError ); if( isBuilder ) { setErrorText( i18n( "No build directory specified for a builder job." ) ); } else { setErrorText( i18n( "No working directory specified for a process." ) ); } return emitResult(); } setModel( new OutputModel ); } else { // Basic sanity checks. if( !effectiveWorkingDirectory.isValid() ) { setError( InvalidWorkingDirectoryError ); if( isBuilder ) { setErrorText( i18n( "Invalid build directory '%1'", effectiveWorkingDirectory.prettyUrl() ) ); } else { setErrorText( i18n( "Invalid working directory '%1'", effectiveWorkingDirectory.prettyUrl() ) ); } return emitResult(); } else if( !effectiveWorkingDirectory.isLocalFile() ) { setError( InvalidWorkingDirectoryError ); if( isBuilder ) { setErrorText( i18n( "Build directory '%1' is not a local path", effectiveWorkingDirectory.prettyUrl() ) ); } else { setErrorText( i18n( "Working directory '%1' is not a local path", effectiveWorkingDirectory.prettyUrl() ) ); } return emitResult(); } QFileInfo workingDirInfo( effectiveWorkingDirectory.toLocalFile() ); if( !workingDirInfo.isDir() ) { // If a working directory does not actually exist, either bail out or create it empty, // depending on what we need by properties. // We use a dedicated bool variable since !isDir() may also mean that it exists, // but is not a directory, or a symlink to an inexistent object. bool successfullyCreated = false; if( !d->m_properties.testFlag( CheckWorkingDirectory ) ) { successfullyCreated = QDir( effectiveWorkingDirectory.directory() ).mkdir( effectiveWorkingDirectory.fileName() ); } if( !successfullyCreated ) { setError( InvalidWorkingDirectoryError ); if( isBuilder ) { setErrorText( i18n( "Build directory '%1' does not exist or is not a directory", effectiveWorkingDirectory.prettyUrl() ) ); } else { setErrorText( i18n( "Working directory '%1' does not exist or is not a directory", effectiveWorkingDirectory.prettyUrl() ) ); } return emitResult(); } } setModel( new OutputModel( effectiveWorkingDirectory ) ); } Q_ASSERT( model() ); model()->setFilteringStrategy( d->m_filteringStrategy ); setDelegate( new OutputDelegate ); // Slots hasRawStdout() and hasRawStderr() are responsible // for feeding raw data to the line maker; so property-based channel filtering is implemented there. if( d->m_properties.testFlag( PostProcessOutput ) ) { connect( d->m_lineMaker, SIGNAL(receivedStdoutLines(QStringList)), SLOT(postProcessStdout(QStringList)) ); connect( d->m_lineMaker, SIGNAL(receivedStderrLines(QStringList)), SLOT(postProcessStderr(QStringList)) ); } else { connect( d->m_lineMaker, SIGNAL(receivedStdoutLines(QStringList)), model(), SLOT(appendLines(QStringList)) ); connect( d->m_lineMaker, SIGNAL(receivedStderrLines(QStringList)), model(), SLOT(appendLines(QStringList)) ); } if( !d->m_properties.testFlag( NoSilentOutput ) || verbosity() != Silent ) { d->m_outputStarted = true; startOutput(); } const QString joinedCommandLine = d->joinCommandLine(); QString headerLine; if( !effectiveWorkingDirectory.isEmpty() ) { headerLine = effectiveWorkingDirectory.toLocalFile( KUrl::RemoveTrailingSlash ) + "> " + joinedCommandLine; } else { headerLine = joinedCommandLine; } model()->appendLine( headerLine ); if( !effectiveWorkingDirectory.isEmpty() ) { d->m_process->setWorkingDirectory( effectiveWorkingDirectory.toLocalFile() ); } d->m_process->setProcessEnvironment( d->effectiveEnvironment() ); d->m_process->setProgram( d->effectiveCommandLine() ); d->m_process->start(); } bool OutputExecuteJob::doKill() { const int terminateKillTimeout = 1000; // msecs if( d->m_status != JobRunning ) return true; d->m_status = JobCanceled; d->m_process->terminate(); bool terminated = d->m_process->waitForFinished( terminateKillTimeout ); if( !terminated ) { d->m_process->kill(); terminated = d->m_process->waitForFinished( terminateKillTimeout ); } d->m_lineMaker->flushBuffers(); if( terminated ) { model()->appendLine( i18n( "*** Aborted ***" ) ); } else { // It survived SIGKILL, leave it alone... model()->appendLine( i18n( "*** Warning: could not kill the process ***" ) ); } return true; } void OutputExecuteJob::childProcessError( QProcess::ProcessError processError ) { // This can be called twice: one time via an error() signal, and second - from childProcessExited(). // Avoid doing things in second time. if( d->m_status != OutputExecuteJob::JobRunning ) return; d->m_status = OutputExecuteJob::JobFailed; QString errorValue; switch( processError ) { case QProcess::FailedToStart: errorValue = i18n("%1 has failed to start", d->m_arguments.first()); break; case QProcess::Crashed: errorValue = i18n("%1 has crashed", d->m_arguments.first()); break; case QProcess::ReadError: errorValue = i18n("Read error"); break; case QProcess::WriteError: errorValue = i18n("Write error"); break; case QProcess::Timedout: errorValue = i18n("Waiting for the process has timed out"); break; default: case QProcess::UnknownError: errorValue = i18n("Exit code %1", d->m_process->exitCode()); break; } // Show the toolview if it's hidden for the user to be able to diagnose errors. if( !d->m_outputStarted ) { d->m_outputStarted = true; startOutput(); } setError( FailedShownError ); setErrorText( errorValue ); d->m_lineMaker->flushBuffers(); model()->appendLine( i18n("*** Failure: %1 ***", errorValue) ); emitResult(); } void OutputExecuteJob::childProcessExited( int exitCode, QProcess::ExitStatus exitStatus ) { if( d->m_status != JobRunning ) return; if( exitStatus == QProcess::CrashExit ) { childProcessError( QProcess::Crashed ); } else if ( exitCode != 0 ) { childProcessError( QProcess::UnknownError ); } else { d->m_status = JobSucceeded; d->m_lineMaker->flushBuffers(); model()->appendLine( i18n("*** Finished ***") ); emitResult(); } } -void OutputExecuteJob::childProcessStdout() +void OutputExecuteJobPrivate::childProcessStdout() { - QByteArray out = d->m_process->readAllStandardOutput(); - if( d->m_properties.testFlag( AccumulateStdout ) ) { - d->m_processStdout += out; + QByteArray out = m_process->readAllStandardOutput(); + if( m_properties.testFlag( OutputExecuteJob::AccumulateStdout ) ) { + m_processStdout += out; } - if( d->m_properties.testFlag( DisplayStdout ) ) { - d->m_lineMaker->slotReceivedStdout( out ); + if( m_properties.testFlag( OutputExecuteJob::DisplayStdout ) ) { + m_lineMaker->slotReceivedStdout( out ); } } -void OutputExecuteJob::childProcessStderr() +void OutputExecuteJobPrivate::childProcessStderr() { - QByteArray err = d->m_process->readAllStandardError(); - if( d->m_properties.testFlag( AccumulateStderr ) ) { - d->m_processStderr += err; + QByteArray err = m_process->readAllStandardError(); + if( m_properties.testFlag( OutputExecuteJob::AccumulateStderr ) ) { + m_processStderr += err; } - if( d->m_properties.testFlag( DisplayStderr ) ) { - d->m_lineMaker->slotReceivedStderr( err ); + if( m_properties.testFlag( OutputExecuteJob::DisplayStderr ) ) { + m_lineMaker->slotReceivedStderr( err ); } } void OutputExecuteJob::postProcessStdout( const QStringList& lines ) { model()->appendLines( lines ); } void OutputExecuteJob::postProcessStderr( const QStringList& lines ) { model()->appendLines( lines ); } void OutputExecuteJob::setFilteringStrategy( OutputModel::OutputFilterStrategy strategy ) { d->m_filteringStrategy = strategy; } OutputExecuteJob::JobProperties OutputExecuteJob::properties() const { return d->m_properties; } void OutputExecuteJob::setProperties( OutputExecuteJob::JobProperties properties, bool override ) { if( override ) { d->m_properties = properties; } else { d->m_properties |= properties; } } void OutputExecuteJob::unsetProperties( OutputExecuteJob::JobProperties properties ) { d->m_properties &= ~properties; } QString OutputExecuteJob::environmentProfile() const { return d->m_environmentProfile; } void OutputExecuteJob::setEnvironmentProfile( const QString& profile ) { d->m_environmentProfile = profile; } void OutputExecuteJob::addEnvironmentOverride( const QString& name, const QString& value ) { d->m_environmentOverrides[name] = value; } void OutputExecuteJob::removeEnvironmentOverride( const QString& name ) { d->m_environmentOverrides.remove( name ); } void OutputExecuteJobPrivate::mergeEnvironment( QProcessEnvironment& dest, const QMap& src ) { for( QMap::const_iterator it = src.begin(); it != src.end(); ++it ) { dest.insert( it.key(), it.value() ); } } QProcessEnvironment OutputExecuteJobPrivate::effectiveEnvironment() const { QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); const EnvironmentGroupList environmentGroup( KGlobal::config() ); OutputExecuteJobPrivate::mergeEnvironment( environment, environmentGroup.variables( m_owner->environmentProfile() ) ); OutputExecuteJobPrivate::mergeEnvironment( environment, m_environmentOverrides ); if( m_properties.testFlag( OutputExecuteJob::PortableMessages ) ) { environment.remove( "LC_ALL" ); environment.insert( "LC_MESSAGES", "C" ); } return environment; } QString OutputExecuteJobPrivate::joinCommandLine() const { return KShell::joinArgs( effectiveCommandLine() ); } QStringList OutputExecuteJobPrivate::effectiveCommandLine() const { // If we need to use a su-like helper, invoke it as // "helper -- our command line". QStringList privilegedCommand = m_owner->privilegedExecutionCommand(); if( !privilegedCommand.isEmpty() ) { return QStringList() << m_owner->privilegedExecutionCommand() << "--" << m_owner->commandLine(); } else { return m_owner->commandLine(); } } QString OutputExecuteJobPrivate::getJobName() { const QString joinedCommandLine = joinCommandLine(); if( m_properties.testFlag( OutputExecuteJob::AppendProcessString ) ) { if( !m_jobName.isEmpty() ) { return m_jobName + ": " + joinedCommandLine; } else { return joinedCommandLine; } } else { return m_jobName; } } } // namespace KDevelop #include "outputexecutejob.moc" \ No newline at end of file diff --git a/outputview/outputexecutejob.h b/outputview/outputexecutejob.h index 18f4070995..43999bcc16 100644 --- a/outputview/outputexecutejob.h +++ b/outputview/outputexecutejob.h @@ -1,250 +1,252 @@ /* This file is part of KDevelop C opyright 2012 Ivan Shapoval*ov This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef OUTPUTEXECUTEJOB_H #define OUTPUTEXECUTEJOB_H #include "outputjob.h" #include "outputmodel.h" #include class KUrl; class KProcess; namespace KDevelop { class ProcessLineMaker; class OutputExecuteJobPrivate; class KDEVPLATFORMOUTPUTVIEW_EXPORT OutputExecuteJob : public OutputJob { Q_OBJECT public: enum JobStatus { JobRunning = 0, /**< The job is running */ JobSucceeded = 1, /**< The job has succeeded */ JobCanceled = 2, /**< The job has been cancelled */ JobFailed = 3, /**< The job has failed */ JobNotStarted = 4 /**< The job hasn't been started so far */ }; enum { InvalidWorkingDirectoryError = OutputJob::UserDefinedError, UserDefinedError }; enum JobProperty { AppendProcessString = 0x001, /**< Whether to append a process string to the user-specified job name */ NeedWorkingDirectory = 0x002, /**< Whether to require a non-empty working directory to be provided */ CheckWorkingDirectory = 0x004, /**< Whether to check that the working directory actually exists (and not to create it if needed) */ PortableMessages = 0x008, /**< Whether to set LC_MESSAGES=C in the process' environment */ DisplayStdout = 0x010, /**< Whether to pass process' stdout to the output model */ AccumulateStdout = 0x020, /**< Whether to save process' stdout to the internal byte-array buffer for further consumption */ DisplayStderr = 0x040, /**< Whether to pass process' stderr to the output model */ AccumulateStderr = 0x080, /**< Whether to save process' stderr to the internal byte-array buffer for further consumption */ NoSilentOutput = 0x100, /**< Whether to call \ref startOutput() only if verbosity is \ref OutputJob::Verbose */ PostProcessOutput = 0x200, /**< Whether to connect line maker's signals to \ref postProcessStdout() and \ref postProcessStderr() */ IsBuilderHint = 0x400, /**< Whether to use builder-specific messages to talk to user (e. g. "build directory" instead of "working directory" */ }; Q_FLAGS(JobProperty JobProperties) Q_DECLARE_FLAGS(JobProperties, JobProperty) OutputExecuteJob( QObject* parent = 0, OutputJobVerbosity verbosity = OutputJob::Verbose ); virtual ~OutputExecuteJob(); /** * Get the job's status (associated with the process). * * @returns The job's status. * @see JobStatus */ JobStatus status() const; /** * Get the job's output model. * * @returns The job's output model, downcasted to \ref OutputModel */ OutputModel* model() const; /** * Returns a working directory for the job's process. * * @returns URL which has been set through \ref setWorkingDirectory(); empty URL if unset. */ virtual KUrl workingDirectory() const; /** * Set a working directory for the job's process. * Effective if \ref workingDirectory() hasn't been overridden. * * @param directory a valid local directory URL, or an empty URL to unset. */ void setWorkingDirectory( const KUrl& directory ); /** * Get process' command line. * * @returns The command line for the process, with first element in list being the program path. */ virtual QStringList commandLine() const; /** * Append an element to the command line argument list for this process. * If no executable is set yet, it will be set instead. * Effective if \ref commandLine() hasn't been overridden. * * @param argument the argument to add */ OutputExecuteJob& operator<<( const QString& argument ); /** * Append a list of elements to the command line argument list for this process. * If no executable is set yet, it will be set from the first argument in given list. * Effective if \ref commandLine() hasn't been overridden. * * @param arguments the arguments to add */ OutputExecuteJob& operator<<( const QStringList& arguments ); /** * Get the privilege escalation command ("su", "sudo", etc.) used for the job's process. * * @returns The privilege escalation command name and arguments; empty list if not set. */ virtual QStringList privilegedExecutionCommand() const; /** * Set the privilege escalation command ("su", "sudo", etc.) which will be used for the job's process. * Effective if \ref privilegedExecutionCommand() hasn't been overridden. * * @param command The privilege escalation command's name and arguments; empty list to unset. * @see privilegedCommand */ void setPrivilegedExecutionCommand( const QStringList& command ); /** * A convenience function to set the job name. * * Calls \ref setTitle() and \ref setObjectName(). * * @note If you need the command-line to be appended to the job name, * make sure that it is already configured upon calling this function. * * @param name The name to set; empty string to use default (process string). */ void setJobName( const QString& name ); /** * Set the filtering strategy for the output model. */ void setFilteringStrategy( OutputModel::OutputFilterStrategy strategy ); /** * Get the current properties of the job. * * @note Default-set properties are: \ref DisplayStdout. */ virtual JobProperties properties() const; /** * Set properties of the job. * Effective if \ref properties() hasn't been overridden. * * @param properties Which flags to add to the job. * @param override Whether to assign instead of doing bitwise OR. * @see JobProperties, properties(), unsetProperties() */ void setProperties( JobProperties properties, bool override = false ); /** * Unset properties of the job. * * @param properties Which flags to remove from the job * @see JobProperties, properties(), setProperties() */ void unsetProperties( JobProperties properties ); /** * Add a variable to the job's process environment. * * The variables added with this method override ones from the system environment and * the global environment profile, but are overridden by "PortableMessages" property. * * @param name The name of a variable to add * @param value The value of a variable to add; empty string to unset. */ void addEnvironmentOverride( const QString& name, const QString& value ); /** * Remove a variable from the override set. * * @param name The name of a variable to remove. * @note This does not force a variable to empty value; this is to undo the overriding itself. */ void removeEnvironmentOverride( const QString& name ); /** * Get the global environment profile name for the job's process. * * @returns The environment profile name to use in the job's process; empty if unset. */ virtual QString environmentProfile() const; /** * Set the environment profile name for the job's process. * Effective if \ref environmentProfile() hasn't been overridden. * * @param profile The name of profile to set. */ void setEnvironmentProfile( const QString& profile ); virtual void start(); protected: virtual bool doKill(); protected slots: // Redefine these functions if you want to post-process the output somehow // before it hits the output model. // Default implementations for either function call "model()->appendLines( lines );". // Do the same if you need the output to be visible. virtual void postProcessStdout( const QStringList& lines ); virtual void postProcessStderr( const QStringList& lines ); + // Redefine these functions if you want to handle process' exit codes in a special manner. + // One possible usage is in "cvs diff" job which returns 1 on success. + virtual void childProcessExited( int exitCode, QProcess::ExitStatus exitStatus ); + virtual void childProcessError( QProcess::ProcessError processError ); + private: OutputExecuteJobPrivate* d; -private slots: - void childProcessExited( int exitCode, QProcess::ExitStatus exitStatus ); - void childProcessError( QProcess::ProcessError processError ); - void childProcessStdout(); - void childProcessStderr(); + Q_PRIVATE_SLOT(d, void childProcessStdout()); + Q_PRIVATE_SLOT(d, void childProcessStderr()); }; } // namespace KDevelop Q_DECLARE_OPERATORS_FOR_FLAGS(KDevelop::OutputExecuteJob::JobProperties); #endif // OUTPUTEXECUTEJOB_H \ No newline at end of file