/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Copyright (C) 2019 Jochen Ulrich ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qbs. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "processcommandexecutor.h" #include "artifact.h" #include "rulecommands.h" #include "transformer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace qbs { namespace Internal { ProcessCommandExecutor::ProcessCommandExecutor(const Logger &logger, QObject *parent) : AbstractCommandExecutor(logger, parent) { connect(&m_process, &QbsProcess::errorOccurred, this, &ProcessCommandExecutor::onProcessError); connect(&m_process, static_cast(&QbsProcess::finished), this, &ProcessCommandExecutor::onProcessFinished); } static QProcessEnvironment mergeEnvironments(const QProcessEnvironment &baseEnv, const QProcessEnvironment &additionalEnv) { QProcessEnvironment env = baseEnv; static const QStringList pathListVariables{ StringConstants::pathEnvVar(), QStringLiteral("LD_LIBRARY_PATH"), QStringLiteral("DYLD_LIBRARY_PATH"), QStringLiteral("DYLD_FRAMEWORK_PATH"), }; const auto keys = additionalEnv.keys(); for (const QString &key : keys) { QString newValue = additionalEnv.value(key); if (pathListVariables.contains(key, HostOsInfo::fileNameCaseSensitivity())) { const QString &oldValue = baseEnv.value(key); if (!oldValue.isEmpty()) newValue.append(HostOsInfo::pathListSeparator()).append(oldValue); } env.insert(key, newValue); } return env; } void ProcessCommandExecutor::doSetup() { ProcessCommand * const cmd = processCommand(); const QString program = ExecutableFinder(transformer()->product(), transformer()->product()->buildEnvironment) .findExecutable(cmd->program(), cmd->workingDir()); cmd->clearRelevantEnvValues(); const auto keys = cmd->relevantEnvVars(); for (const QString &key : keys) cmd->addRelevantEnvValue(key, transformer()->product()->buildEnvironment.value(key)); m_commandEnvironment = mergeEnvironments(m_buildEnvironment, cmd->environment()); m_program = program; m_arguments = cmd->arguments(); m_shellInvocation = shellQuote(QDir::toNativeSeparators(m_program), m_arguments); } bool ProcessCommandExecutor::doStart() { QBS_ASSERT(m_process.state() == QProcess::NotRunning, return false); const ProcessCommand * const cmd = processCommand(); m_process.setProcessEnvironment(m_commandEnvironment); QStringList arguments = m_arguments; if (dryRun() && !cmd->ignoreDryRun()) { QTimer::singleShot(0, this, [this] { emit finished(); }); // Don't call back on the caller. return false; } const QString workingDir = QDir::fromNativeSeparators(cmd->workingDir()); if (!workingDir.isEmpty()) { FileInfo fi(workingDir); if (!fi.exists() || !fi.isDir()) { emit finished(ErrorInfo(Tr::tr("The working directory '%1' for process '%2' " "is invalid.").arg(QDir::toNativeSeparators(workingDir), QDir::toNativeSeparators(m_program)), cmd->codeLocation())); return false; } } // Automatically use response files, if the command line gets to long. if (!cmd->responseFileUsagePrefix().isEmpty()) { const int commandLineLength = m_shellInvocation.length(); if (cmd->responseFileThreshold() >= 0 && commandLineLength > cmd->responseFileThreshold()) { qCDebug(lcExec) << QString::fromUtf8("Using response file. " "Threshold is %1. Command line length %2.") .arg(cmd->responseFileThreshold()).arg(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() + QLatin1String("/qbsresp")); if (!responseFile.open()) { emit finished(ErrorInfo(Tr::tr("Cannot create response file '%1'.") .arg(responseFile.fileName()))); return false; } const auto separator = cmd->responseFileSeparator().toUtf8(); for (int i = cmd->responseFileArgumentIndex(); i < cmd->arguments().size(); ++i) { const QString arg = cmd->arguments().at(i); if (arg.startsWith(cmd->responseFileUsagePrefix())) { QFile f(arg.mid(cmd->responseFileUsagePrefix().size())); if (!f.open(QIODevice::ReadOnly)) { emit finished(ErrorInfo(Tr::tr("Cannot open command file '%1'.") .arg(QDir::toNativeSeparators(f.fileName())))); return false; } responseFile.write(f.readAll()); } else { responseFile.write(qbs::Internal::shellQuote(arg).toLocal8Bit()); } responseFile.write(separator); } responseFile.close(); m_responseFileName = responseFile.fileName(); arguments = arguments.mid(0, std::min(cmd->responseFileArgumentIndex(), arguments.size())); arguments += QDir::toNativeSeparators(cmd->responseFileUsagePrefix() + responseFile.fileName()); } } qCDebug(lcExec) << "Running external process; full command line is:" << m_shellInvocation; const QProcessEnvironment &additionalVariables = cmd->environment(); qCDebug(lcExec) << "Additional environment:" << additionalVariables.toStringList(); m_process.setWorkingDirectory(workingDir); m_process.start(m_program, arguments); return true; } void ProcessCommandExecutor::cancel(const qbs::ErrorInfo &reason) { // We don't want this command to be reported as failing, since we explicitly terminated it. disconnect(this, &ProcessCommandExecutor::reportProcessResult, nullptr, nullptr); m_cancelReason = reason; m_process.cancel(); } QString ProcessCommandExecutor::filterProcessOutput(const QByteArray &_output, const QString &filterFunctionSource) { QString output = QString::fromLocal8Bit(_output); if (filterFunctionSource.isEmpty()) return output; JSContext * const ctx = scriptEngine()->context(); const ScopedJsValue scope(ctx, JS_NewObjectProto(ctx, scriptEngine()->globalObject())); for (auto it = command()->properties().constBegin(); it != command()->properties().constEnd(); ++it) { setJsProperty(ctx, scope, it.key(), scriptEngine()->toScriptValue(it.value())); } TemporaryGlobalObjectSetter tgos(scriptEngine(), scope); const ScopedJsValue filterFunction( ctx, scriptEngine()->evaluate(JsValueOwner::Caller, QLatin1String("var f = ") + filterFunctionSource + QLatin1String("; f"))); if (!JS_IsFunction(scriptEngine()->context(), filterFunction)) { logger().printWarning(ErrorInfo(Tr::tr("Error in filter function: %1.\n%2") .arg(filterFunctionSource, getJsString(scriptEngine()->context(), filterFunction)))); return output; } const ScopedJsValue outputArg(ctx, scriptEngine()->toScriptValue(output)); JSValue outputArgForCall = outputArg; const ScopedJsValue filteredOutput( ctx, JS_Call(ctx, filterFunction, JS_UNDEFINED, 1, &outputArgForCall)); if (const JsException ex = scriptEngine()->checkAndClearException({})) { ErrorInfo err = ex.toErrorInfo(); err.prepend(Tr::tr("Error when calling output filter function")); logger().printWarning(err); return output; } return getJsString(ctx, filteredOutput); } static QProcess::ProcessError saveToFile(const QString &filePath, const QByteArray &content) { QBS_ASSERT(!filePath.isEmpty(), return QProcess::WriteError); QFile f(filePath); if (!f.open(QIODevice::WriteOnly)) return QProcess::WriteError; if (f.write(content) != content.size()) return QProcess::WriteError; f.close(); return f.error() == QFileDevice::NoError ? QProcess::UnknownError : QProcess::WriteError; } void ProcessCommandExecutor::getProcessOutput(bool stdOut, ProcessResult &result) { QByteArray content; QString filterFunction; QString redirectPath; QStringList *target; if (stdOut) { content = m_process.readAllStandardOutput(); filterFunction = processCommand()->stdoutFilterFunction(); redirectPath = processCommand()->stdoutFilePath(); target = &result.d->stdOut; } else { content = m_process.readAllStandardError(); filterFunction = processCommand()->stderrFilterFunction(); redirectPath = processCommand()->stderrFilePath(); target = &result.d->stdErr; } QString contentString = filterProcessOutput(content, filterFunction); if (!redirectPath.isEmpty()) { const QByteArray dataToWrite = filterFunction.isEmpty() ? content : contentString.toLocal8Bit(); const QProcess::ProcessError error = saveToFile(redirectPath, dataToWrite); if (result.error() == QProcess::UnknownError && error != QProcess::UnknownError) result.d->error = error; } else { if (!contentString.isEmpty() && contentString.endsWith(QLatin1Char('\n'))) contentString.chop(1); *target = contentString.split(QLatin1Char('\n'), Qt::SkipEmptyParts); } } void ProcessCommandExecutor::sendProcessOutput() { ProcessResult result; result.d->executableFilePath = m_program; result.d->arguments = m_arguments; result.d->workingDirectory = m_process.workingDirectory(); if (result.workingDirectory().isEmpty()) result.d->workingDirectory = QDir::currentPath(); result.d->exitCode = m_process.exitCode(); result.d->error = m_process.error(); QString errorString = m_process.errorString(); getProcessOutput(true, result); getProcessOutput(false, result); const bool processError = result.error() != QProcess::UnknownError; const bool failureExit = quint32(m_process.exitCode()) > quint32(processCommand()->maxExitCode()); const bool cancelledWithError = m_cancelReason.hasError(); result.d->success = !processError && !failureExit && !cancelledWithError; emit reportProcessResult(result); if (Q_UNLIKELY(cancelledWithError)) { emit finished(m_cancelReason); } else if (Q_UNLIKELY(processError)) { emit finished(ErrorInfo(errorString)); } else if (Q_UNLIKELY(failureExit)) { emit finished(ErrorInfo(Tr::tr("Process failed with exit code %1.") .arg(m_process.exitCode()))); } else { emit finished(); } } void ProcessCommandExecutor::onProcessError() { if (scriptEngine()->isActive()) { qCDebug(lcExec) << "Command error while rule execution is pausing. " "Delaying slot execution."; QTimer::singleShot(0, this, &ProcessCommandExecutor::onProcessError); return; } if (m_cancelReason.hasError()) return; // Ignore. Cancel reasons will be handled by on ProcessFinished(). switch (m_process.error()) { case QProcess::FailedToStart: { removeResponseFile(); const QString binary = QDir::toNativeSeparators(processCommand()->program()); QString errorPrefixString; #ifdef Q_OS_UNIX if (QFileInfo(binary).isExecutable()) { const QString interpreter(shellInterpreter(binary)); if (!interpreter.isEmpty()) { errorPrefixString = Tr::tr("%1: bad interpreter: ").arg(interpreter); } } #endif emit finished(ErrorInfo(Tr::tr("The process '%1' could not be started: %2. " "The full command line invocation was: %3") .arg(binary, errorPrefixString + m_process.errorString(), m_shellInvocation))); break; } case QProcess::Crashed: break; // Ignore. Will be handled by onProcessFinished(). default: logger().qbsWarning() << "QProcess error: " << m_process.errorString(); } } void ProcessCommandExecutor::onProcessFinished() { if (scriptEngine()->isActive()) { qCDebug(lcExec) << "Command finished while rule execution is pausing. " "Delaying slot execution."; QTimer::singleShot(0, this, &ProcessCommandExecutor::onProcessFinished); return; } removeResponseFile(); sendProcessOutput(); } static QString environmentVariableString(const QString &key, const QString &value) { QString str; if (HostOsInfo::isWindowsHost()) str += QStringLiteral("set "); str += shellQuote(key + QLatin1Char('=') + value); if (HostOsInfo::isWindowsHost()) str += QLatin1Char('\n'); else str += QLatin1Char(' '); return str; } void ProcessCommandExecutor::doReportCommandDescription(const QString &productName) { if (m_echoMode == CommandEchoModeCommandLine || m_echoMode == CommandEchoModeCommandLineWithEnvironment) { QString fullInvocation; if (m_echoMode == CommandEchoModeCommandLineWithEnvironment) { QStringList keys = m_commandEnvironment.keys(); keys.sort(); for (const QString &key : std::as_const(keys)) fullInvocation += environmentVariableString(key, m_commandEnvironment.value(key)); } fullInvocation += m_shellInvocation; emit reportCommandDescription(command()->highlight(), !command()->extendedDescription().isEmpty() ? command()->extendedDescription() : fullInvocation); return; } AbstractCommandExecutor::doReportCommandDescription(productName); } void ProcessCommandExecutor::removeResponseFile() { if (m_responseFileName.isEmpty()) return; QFile::remove(m_responseFileName); m_responseFileName.clear(); } ProcessCommand *ProcessCommandExecutor::processCommand() const { return static_cast(command()); } } // namespace Internal } // namespace qbs