diff options
Diffstat (limited to 'src/lib/buildgraph/commandexecutor.cpp')
-rw-r--r-- | src/lib/buildgraph/commandexecutor.cpp | 376 |
1 files changed, 376 insertions, 0 deletions
diff --git a/src/lib/buildgraph/commandexecutor.cpp b/src/lib/buildgraph/commandexecutor.cpp new file mode 100644 index 000000000..7a72a6c98 --- /dev/null +++ b/src/lib/buildgraph/commandexecutor.cpp @@ -0,0 +1,376 @@ +/************************************************************************** +** +** This file is part of the Qt Build Suite +** +** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation (info@qt.nokia.com) +** +** +** GNU Lesser General Public License Usage +** +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this file. +** Please review the following information to ensure the GNU Lesser General +** Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU General +** Public License version 3.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of this +** file. +** Please review the following information to ensure the GNU General +** Public License version 3.0 requirements will be met: +** http://www.gnu.org/copyleft/gpl.html. +** +** Other Usage +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +**************************************************************************/ +#include "commandexecutor.h" +#include "command.h" +#include "buildgraph.h" +#include "processoutput.h" + +#include <buildgraph/artifact.h> +#include <buildgraph/transformer.h> +#include <tools/logger.h> + +#include <QtConcurrentRun> +#include <QDebug> +#include <QFutureWatcher> +#include <QProcess> +#include <QScriptEngine> +#include <QThread> +#include <QTemporaryFile> + +namespace qbs { + +struct JavaScriptCommandFutureResult +{ + bool success; + QString errorMessage; +}; + +class JavaScriptCommandFutureWatcher : public QFutureWatcher<JavaScriptCommandFutureResult> +{ +public: + JavaScriptCommandFutureWatcher(QObject *parent) + : QFutureWatcher<JavaScriptCommandFutureResult>(parent) + {} +}; + +CommandExecutor::CommandExecutor(QObject *parent) + : QObject(parent) + , m_processCommand(0) + , m_process(0) + , m_mainThreadScriptEngine(0) + , m_transformer(0) + , m_jsCommand(0) + , m_jsFutureWatcher(0) +{ + connect(&m_process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(onProcessError(QProcess::ProcessError))); + connect(&m_process, SIGNAL(finished(int)), this, SLOT(onProcessFinished(int))); +} + +void CommandExecutor::setProcessEnvironment(const QProcessEnvironment &processEnvironment) +{ + m_process.setProcessEnvironment(processEnvironment); +} + +void CommandExecutor::waitForFinished() +{ + if (m_process.state() == QProcess::Running) + m_process.waitForFinished(-1); + if (m_jsFutureWatcher && m_jsFutureWatcher->isRunning()) + m_jsFutureWatcher->waitForFinished(); +} + +void CommandExecutor::start(Transformer *transformer, AbstractCommand *cmd) +{ + m_processCommand = 0; + m_jsCommand = 0; + + switch (cmd->type()) { + case AbstractCommand::AbstractCommandType: + qWarning("CommandExecutor can't execute abstract commands."); + return; + case AbstractCommand::ProcessCommandType: + m_processCommand = static_cast<ProcessCommand*>(cmd); + startProcessCommand(); + return; + case AbstractCommand::JavaScriptCommandType: + m_jsCommand = static_cast<JavaScriptCommand*>(cmd); + m_transformer = transformer; + startJavaScriptCommand(); + return; + } + + emit error("CommandExecutor: unknown command type."); + return; +} + +static QHash<QString, TextColor> setupColorTable() +{ + QHash<QString, TextColor> colorTable; + colorTable["compiler"] = TextColorDefault; + colorTable["linker"] = TextColorDarkGreen; + colorTable["codegen"] = TextColorDarkYellow; + return colorTable; +} + +void CommandExecutor::printCommandInfo(AbstractCommand *cmd) +{ + if (!cmd->description().isEmpty()) { + static QHash<QString, TextColor> colorTable = setupColorTable(); + qbsInfo() << DontPrintLogLevel << LogOutputStdOut + << colorTable.value(cmd->highlight(), TextColorDefault) + << cmd->description(); + } +} + +void CommandExecutor::startProcessCommand() +{ + Q_ASSERT(m_process.state() == QProcess::NotRunning); + + printCommandInfo(m_processCommand); + if (!m_processCommand->isSilent()) { + QString commandLine = m_processCommand->program() + QLatin1Char(' ') + m_processCommand->arguments().join(" "); + qbsInfo() << DontPrintLogLevel << LogOutputStdOut << commandLine; + } + if (qbsLogLevel(LoggerDebug)) { + qbsDebug() << "[EXEC] " << m_processCommand->program() + QLatin1Char(' ') + m_processCommand->arguments().join(" "); + } + + // Automatically use response files, if the command line gets to long. + QStringList arguments = m_processCommand->arguments(); + if (!m_processCommand->responseFileUsagePrefix().isEmpty()) { + int commandLineLength = m_processCommand->program().length() + 1; + for (int i = m_processCommand->arguments().count(); --i >= 0;) + commandLineLength += m_processCommand->arguments().at(i).length(); + if (m_processCommand->responseFileThreshold() >= 0 && commandLineLength > m_processCommand->responseFileThreshold()) { + if (qbsLogLevel(LoggerDebug)) + qbsDebug("[EXEC] Using response file. Threshold is %d. Command line length %d.", m_processCommand->responseFileThreshold(), commandLineLength); + + // The QTemporaryFile keeps a handle on the file, even if closed. + // On Windows, some commands (e.g. msvc link.exe) won't accept that. + // We need to delete the file manually, later. + QTemporaryFile responseFile; + responseFile.setAutoRemove(false); + responseFile.setFileTemplate(QDir::tempPath() + "/qbsresp"); + if (!responseFile.open()) { + QString errorMessage = "Cannot create response file."; + emit error(errorMessage); + return; + } + for (int i = 0; i < m_processCommand->arguments().count(); ++i) { + responseFile.write(m_processCommand->arguments().at(i).toLocal8Bit()); + responseFile.write("\n"); + } + responseFile.close(); + arguments.clear(); + arguments += QDir::toNativeSeparators(m_processCommand->responseFileUsagePrefix() + responseFile.fileName()); + if (qbsLogLevel(LoggerDebug)) + qbsDebug("[EXEC] command line with response file: %s %s", qPrintable(m_processCommand->program()), qPrintable(arguments.join(" "))); + } + } + + m_process.setWorkingDirectory(m_processCommand->workingDir()); + m_process.start(m_processCommand->program(), arguments); +} + +QByteArray CommandExecutor::filterProcessOutput(const QByteArray &output, const QString &filterFunctionSource) +{ + if (filterFunctionSource.isEmpty()) + return output; + + QScriptValue filterFunction = m_mainThreadScriptEngine->evaluate("var f = " + filterFunctionSource + "; f"); + if (!filterFunction.isFunction()) { + emit error(QString("Error in filter function: %1.\n%2").arg(filterFunctionSource, filterFunction.toString())); + return output; + } + + QScriptValue outputArg = m_mainThreadScriptEngine->newArray(1); + outputArg.setProperty(0, m_mainThreadScriptEngine->toScriptValue(QString::fromLatin1(output))); + QScriptValue filteredOutput = filterFunction.call(m_mainThreadScriptEngine->undefinedValue(), outputArg); + if (filteredOutput.isError()) { + emit error(QString("Error when calling ouput filter function: %1").arg(filteredOutput.toString())); + return output; + } + + return filteredOutput.toString().toLocal8Bit(); +} + +void CommandExecutor::sendProcessOutput(bool logCommandLine) +{ + QString commandLine = m_processCommand->program(); + if (!m_processCommand->arguments().isEmpty()) { + commandLine += ' '; + commandLine += m_processCommand->arguments().join(" "); + } + + QByteArray processStdOut = filterProcessOutput(m_process.readAllStandardOutput(), m_processCommand->stdoutFilterFunction()); + QByteArray processStdErr = filterProcessOutput(m_process.readAllStandardError(), m_processCommand->stderrFilterFunction()); + + bool processOutputEmpty = processStdOut.isEmpty() && processStdErr.isEmpty(); + if (logCommandLine || !processOutputEmpty) { + qbsInfo() << DontPrintLogLevel << commandLine << (processOutputEmpty ? "" : "\n") + << processStdOut << processStdErr; + } + + ProcessOutput processOutput; + processOutput.setCommandLine(commandLine); + processOutput.setStandardOutput(processStdOut); + processOutput.setStandardError(processStdErr); + Logger::instance().sendProcessOutput(processOutput); +} + +void CommandExecutor::onProcessError(QProcess::ProcessError processError) +{ + removeResponseFile(); + sendProcessOutput(true); + QString errorMessage; + switch (processError) { + case QProcess::FailedToStart: + errorMessage = "Process could not be started."; + break; + case QProcess::Crashed: + errorMessage = "Process crashed."; + break; + case QProcess::Timedout: + errorMessage = "Process timed out."; + break; + case QProcess::ReadError: + errorMessage = "Error when reading process output."; + break; + case QProcess::WriteError: + errorMessage = "Error when writing to process."; + break; + default: + errorMessage = "Unknown process error."; + break; + } + emit error(errorMessage); +} + +void CommandExecutor::onProcessFinished(int exitCode) +{ + removeResponseFile(); + bool errorOccurred = exitCode > m_processCommand->maxExitCode(); + sendProcessOutput(errorOccurred); + if (errorOccurred) { + QString msg = "Process failed with exit code %1."; + emit error(msg.arg(exitCode)); + return; + } + + emit finished(); +} + +class JSRunner +{ +public: + typedef JavaScriptCommandFutureResult result_type; + + JSRunner(JavaScriptCommand *jsCommand) + : m_jsCommand(jsCommand) + {} + + JavaScriptCommandFutureResult operator() (Transformer *transformer) + { + result_type result; + result.success = true; + QThread *currentThread = QThread::currentThread(); + QScriptEngine *scriptEngine = m_scriptEnginesPerThread.value(currentThread); + if (!scriptEngine) { + scriptEngine = new QScriptEngine(); + m_scriptEnginesPerThread.insert(currentThread, scriptEngine); + + // import script extension plugins + foreach (const QString &name, scriptEngine->availableExtensions()) { + if (!name.startsWith("qbs")) + continue; + QScriptValue e = scriptEngine->importExtension(name); + if (e.isError()) { + qbsWarning("JS thread %x, unable to load %s into QScriptEngine: %s", + (void*)currentThread, + qPrintable(name), + qPrintable(e.toString())); + } + qbsDebug("JS thread %x, script plugin loaded: %s", (void*)currentThread, qPrintable(name)); + } + } + + QString trafoPtrStr = QString::number((qulonglong)transformer); + if (scriptEngine->globalObject().property("_qbs_transformer_ptr").toString() != trafoPtrStr) { + scriptEngine->globalObject().setProperty("_qbs_transformer_ptr", scriptEngine->toScriptValue(trafoPtrStr)); + + Artifact *someOutputArtifact = *transformer->outputs.begin(); + if (someOutputArtifact->product) { + ResolvedProduct::Ptr product = someOutputArtifact->product->rProduct; + BuildGraph::setupScriptEngineForProduct(scriptEngine, product, transformer->rule); + } + transformer->setupInputs(scriptEngine, scriptEngine->globalObject()); + transformer->setupOutputs(scriptEngine, scriptEngine->globalObject()); + } + + scriptEngine->pushContext(); + for (QVariantMap::const_iterator it = m_jsCommand->properties().constBegin(); it != m_jsCommand->properties().constEnd(); ++it) + scriptEngine->currentContext()->activationObject().setProperty(it.key(), scriptEngine->toScriptValue(it.value())); + scriptEngine->evaluate(m_jsCommand->sourceCode()); + if (scriptEngine->hasUncaughtException()) { + result.success = false; + result.errorMessage = scriptEngine->uncaughtException().toString(); + } + scriptEngine->popContext(); + return result; + } + +private: + static QHash<QThread *, QScriptEngine *> m_scriptEnginesPerThread; + JavaScriptCommand *m_jsCommand; +}; + +QHash<QThread *, QScriptEngine *> JSRunner::m_scriptEnginesPerThread; + +void CommandExecutor::startJavaScriptCommand() +{ + printCommandInfo(m_jsCommand); + QFuture<JSRunner::result_type> future = QtConcurrent::run(JSRunner(m_jsCommand), m_transformer); + if (!m_jsFutureWatcher) { + m_jsFutureWatcher = new JavaScriptCommandFutureWatcher(this); + connect(m_jsFutureWatcher, SIGNAL(finished()), SLOT(onJavaScriptCommandFinished())); + } + m_jsFutureWatcher->setFuture(future); +} + +void CommandExecutor::onJavaScriptCommandFinished() +{ + JavaScriptCommandFutureResult result = m_jsFutureWatcher->future().result(); + if (result.success) { + emit finished(); + } else { + qbsInfo() << DontPrintLogLevel << "JS context:\n" << m_jsCommand->properties(); + qbsInfo() << DontPrintLogLevel << "JS code:\n" << m_jsCommand->sourceCode(); + QString msg = "Error while executing JavaScriptCommand: "; + msg += result.errorMessage; + emit error(msg); + } +} + +void CommandExecutor::removeResponseFile() +{ + if (m_responseFileName.isEmpty()) + return; + QFile::remove(m_responseFileName); + m_responseFileName.clear(); +} + +} // namespace qbs |