Files
qt-creator/src/plugins/qbsprojectmanager/qbssession.cpp
Christian Kandeler 151373396f QbsProjectManager: Fix install step
We got the install command name wrong, which caused the installation to
fail. In addition, we forgot to add handling for the "protocol-error"
message from qbs, so the step was hanging, rather than aborting.
Finally, we triggered a number of assertions in
BuildStep::buildConfiguration().
This went all unnoticed for a while, because the install step is not
enabled by default these days.

Change-Id: I906e7e472563d4ad8fc7557bd706a7cb67f9f2ba
Reviewed-by: hjk <hjk@qt.io>
2020-08-27 09:38:55 +00:00

703 lines
25 KiB
C++

/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** 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 General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** 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-3.0.html.
**
****************************************************************************/
#include "qbssession.h"
#include "qbspmlogging.h"
#include "qbsprojectmanagerconstants.h"
#include "qbssettings.h"
#include <coreplugin/messagemanager.h>
#include <projectexplorer/projectexplorerconstants.h>
#include <projectexplorer/taskhub.h>
#include <utils/algorithm.h>
#include <utils/qtcassert.h>
#include <QDir>
#include <QEventLoop>
#include <QJsonArray>
#include <QJsonDocument>
#include <QProcess>
#include <QProcessEnvironment>
#include <QTimer>
using namespace ProjectExplorer;
using namespace Utils;
namespace QbsProjectManager {
namespace Internal {
QStringList arrayToStringList(const QJsonValue &array)
{
return transform<QStringList>(array.toArray(),
[](const QJsonValue &v) { return v.toString(); });
}
const QByteArray packetStart = "qbsmsg:";
class Packet
{
public:
enum class Status { Incomplete, Complete, Invalid };
Status parseInput(QByteArray &input)
{
if (m_expectedPayloadLength == -1) {
const int packetStartOffset = input.indexOf(packetStart);
if (packetStartOffset == -1)
return Status::Incomplete;
const int numberOffset = packetStartOffset + packetStart.length();
const int newLineOffset = input.indexOf('\n', numberOffset);
if (newLineOffset == -1)
return Status::Incomplete;
const QByteArray sizeString = input.mid(numberOffset, newLineOffset - numberOffset);
bool isNumber;
const int payloadLen = sizeString.toInt(&isNumber);
if (!isNumber || payloadLen < 0)
return Status::Invalid;
m_expectedPayloadLength = payloadLen;
input.remove(0, newLineOffset + 1);
}
const int bytesToAdd = m_expectedPayloadLength - m_payload.length();
QTC_ASSERT(bytesToAdd >= 0, return Status::Invalid);
m_payload += input.left(bytesToAdd);
input.remove(0, bytesToAdd);
return isComplete() ? Status::Complete : Status::Incomplete;
}
QJsonObject retrievePacket()
{
QTC_ASSERT(isComplete(), return QJsonObject());
const auto packet = QJsonDocument::fromJson(QByteArray::fromBase64(m_payload)).object();
m_payload.clear();
m_expectedPayloadLength = -1;
return packet;
}
static QByteArray createPacket(const QJsonObject &packet)
{
const QByteArray jsonData = QJsonDocument(packet).toJson().toBase64();
return QByteArray(packetStart).append(QByteArray::number(jsonData.length())).append('\n')
.append(jsonData);
}
private:
bool isComplete() const { return m_payload.length() == m_expectedPayloadLength; }
QByteArray m_payload;
int m_expectedPayloadLength = -1;
};
class PacketReader : public QObject
{
Q_OBJECT
public:
PacketReader(QObject *parent) : QObject(parent) {}
void handleData(const QByteArray &data)
{
m_incomingData += data;
handleData();
}
signals:
void packetReceived(const QJsonObject &packet);
void errorOccurred(const QString &msg);
private:
void handleData()
{
switch (m_currentPacket.parseInput(m_incomingData)) {
case Packet::Status::Invalid:
emit errorOccurred(tr("Received invalid input."));
break;
case Packet::Status::Complete:
emit packetReceived(m_currentPacket.retrievePacket());
handleData();
break;
case Packet::Status::Incomplete:
break;
}
}
QByteArray m_incomingData;
Packet m_currentPacket;
};
class QbsSession::Private
{
public:
QProcess *qbsProcess = nullptr;
PacketReader *packetReader = nullptr;
QJsonObject currentRequest;
QJsonObject projectData;
QEventLoop eventLoop;
QJsonObject reply;
QHash<QString, QStringList> generatedFilesForSources;
optional<Error> lastError;
State state = State::Inactive;
};
QbsSession::QbsSession(QObject *parent) : QObject(parent), d(new Private)
{
initialize();
}
void QbsSession::initialize()
{
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert("QT_FORCE_STDERR_LOGGING", "1");
d->packetReader = new PacketReader(this);
d->qbsProcess = new QProcess(this);
d->qbsProcess->setProcessEnvironment(env);
connect(d->qbsProcess, &QProcess::readyReadStandardOutput, this, [this] {
d->packetReader->handleData(d->qbsProcess->readAllStandardOutput());
});
connect(d->qbsProcess, &QProcess::readyReadStandardError, this, [this] {
qCDebug(qbsPmLog) << "[qbs stderr]: " << d->qbsProcess->readAllStandardError();
});
connect(d->qbsProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError e) {
d->eventLoop.exit(1);
if (state() == State::ShuttingDown || state() == State::Inactive)
return;
switch (e) {
case QProcess::FailedToStart:
setError(Error::QbsFailedToStart);
break;
case QProcess::WriteError:
case QProcess::ReadError:
setError(Error::ProtocolError);
break;
case QProcess::Crashed:
case QProcess::Timedout:
case QProcess::UnknownError:
break;
}
});
connect(d->qbsProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this,
[this] {
d->qbsProcess->deleteLater();
switch (state()) {
case State::Inactive:
break;
case State::ShuttingDown:
setInactive();
break;
case State::Active:
setError(Error::QbsQuit);
break;
case State::Initializing:
setError(Error::ProtocolError);
break;
}
d->qbsProcess = nullptr;
});
connect(d->packetReader, &PacketReader::errorOccurred, this, [this](const QString &msg) {
qCDebug(qbsPmLog) << "session error" << msg;
setError(Error::ProtocolError);
});
connect(d->packetReader, &PacketReader::packetReceived, this, &QbsSession::handlePacket);
d->state = State::Initializing;
const FilePath qbsExe = QbsSettings::qbsExecutableFilePath();
if (qbsExe.isEmpty() || !qbsExe.exists()) {
QTimer::singleShot(0, this, [this] { setError(Error::QbsFailedToStart); });
return;
}
d->qbsProcess->start(qbsExe.toString(), {"session"});
}
void QbsSession::sendQuitPacket()
{
d->qbsProcess->write(Packet::createPacket(QJsonObject{qMakePair(QString("type"),
QJsonValue("quit"))}));
}
QbsSession::~QbsSession()
{
if (d->packetReader)
d->packetReader->disconnect(this);
if (d->qbsProcess) {
d->qbsProcess->disconnect(this);
quit();
if (d->qbsProcess->state() == QProcess::Running && !d->qbsProcess->waitForFinished(10000))
d->qbsProcess->terminate();
if (d->qbsProcess->state() == QProcess::Running && !d->qbsProcess->waitForFinished(10000))
d->qbsProcess->kill();
d->qbsProcess->waitForFinished(1000);
}
delete d;
}
QbsSession::State QbsSession::state() const
{
return d->state;
}
optional<QbsSession::Error> QbsSession::lastError() const
{
return d->lastError;
}
QString QbsSession::errorString(QbsSession::Error error)
{
switch (error) {
case Error::QbsQuit:
return tr("The qbs process quit unexpectedly.");
case Error::QbsFailedToStart:
return tr("The qbs process failed to start.");
case Error::ProtocolError:
return tr("The qbs process sent unexpected data.");
case Error::VersionMismatch:
return tr("The qbs API level is not compatible with what Qt Creator expects.");
}
return QString(); // For dumb compilers.
}
QJsonObject QbsSession::projectData() const
{
return d->projectData;
}
void QbsSession::sendRequest(const QJsonObject &request)
{
QTC_ASSERT(d->currentRequest.isEmpty(),
qDebug() << request.value("type").toString()
<< d->currentRequest.value("type").toString(); return);
d->currentRequest = request;
const QString logLevelFromEnv = qEnvironmentVariable("QBS_LOG_LEVEL");
if (!logLevelFromEnv.isEmpty())
d->currentRequest.insert("log-level", logLevelFromEnv);
if (!qEnvironmentVariableIsEmpty(Constants::QBS_PROFILING_ENV))
d->currentRequest.insert("log-time", true);
if (d->state == State::Active)
sendQueuedRequest();
else if (d->state == State::Inactive)
initialize();
}
void QbsSession::cancelCurrentJob()
{
if (d->state == State::Active)
sendRequest(QJsonObject{qMakePair(QString("type"), QJsonValue("cancel-job"))});
}
void QbsSession::quit()
{
if (d->state == State::ShuttingDown || d->state == State::Inactive)
return;
d->state = State::ShuttingDown;
sendQuitPacket();
}
void QbsSession::requestFilesGeneratedFrom(const QHash<QString, QStringList> &sourceFilesPerProduct)
{
QJsonObject request;
request.insert("type", "get-generated-files-for-sources");
QJsonArray products;
for (auto it = sourceFilesPerProduct.cbegin(); it != sourceFilesPerProduct.cend(); ++it) {
QJsonObject product;
product.insert("full-display-name", it.key());
QJsonArray requests;
for (const QString &sourceFile : it.value())
requests << QJsonObject({qMakePair(QString("source-file"), sourceFile)});
product.insert("requests", requests);
products << product;
}
request.insert("products", products);
sendRequest(request);
}
QStringList QbsSession::filesGeneratedFrom(const QString &sourceFile) const
{
return d->generatedFilesForSources.value(sourceFile);
}
FileChangeResult QbsSession::addFiles(const QStringList &files, const QString &product,
const QString &group)
{
return updateFileList("add-files", files, product, group);
}
FileChangeResult QbsSession::removeFiles(const QStringList &files, const QString &product,
const QString &group)
{
return updateFileList("remove-files", files, product, group);
}
RunEnvironmentResult QbsSession::getRunEnvironment(
const QString &product,
const QProcessEnvironment &baseEnv,
const QStringList &config)
{
d->reply = QJsonObject();
QJsonObject request;
request.insert("type", "get-run-environment");
request.insert("product", product);
QJsonObject inEnv;
const QStringList baseEnvKeys = baseEnv.keys();
for (const QString &key : baseEnvKeys)
inEnv.insert(key, baseEnv.value(key));
request.insert("base-environment", inEnv);
request.insert("config", QJsonArray::fromStringList(config));
sendRequest(request);
QTimer::singleShot(10000, this, [this] { d->eventLoop.exit(1); });
if (d->eventLoop.exec(QEventLoop::ExcludeUserInputEvents) == 1)
return RunEnvironmentResult(ErrorInfo(tr("Request timed out.")));
QProcessEnvironment env;
const QJsonObject outEnv = d->reply.value("full-environment").toObject();
for (auto it = outEnv.begin(); it != outEnv.end(); ++it)
env.insert(it.key(), it.value().toString());
return RunEnvironmentResult(env, getErrorInfo(d->reply));
}
void QbsSession::insertRequestedModuleProperties(QJsonObject &request)
{
request.insert("module-properties", QJsonArray::fromStringList({
"cpp.commonCompilerFlags",
"cpp.compilerVersionMajor",
"cpp.compilerVersionMinor",
"cpp.cLanguageVersion",
"cpp.cxxLanguageVersion",
"cpp.cxxStandardLibrary",
"cpp.defines",
"cpp.distributionIncludePaths",
"cpp.driverFlags",
"cpp.enableExceptions",
"cpp.enableRtti",
"cpp.exceptionHandlingModel",
"cpp.frameworkPaths",
"cpp.includePaths",
"cpp.machineType",
"cpp.minimumDarwinVersion",
"cpp.minimumDarwinVersionCompilerFlag",
"cpp.platformCommonCompilerFlags",
"cpp.platformDriverFlags",
"cpp.platformDefines",
"cpp.positionIndependentCode",
"cpp.systemFrameworkPaths",
"cpp.systemIncludePaths",
"cpp.target",
"cpp.targetArch",
"cpp.useCPrecompiledHeader",
"cpp.useCxxPrecompiledHeader",
"cpp.useObjcPrecompiledHeader",
"cpp.useObjcxxPrecompiledHeader",
"qbs.targetOS",
"qbs.toolchain",
"Qt.core.enableKeywords",
"Qt.core.version",
}));
}
// TODO: We can do better: Give out a (managed) session pointer here. Then we can re-use it
// if the user chooses the imported project, saving the second build graph load.
QbsSession::BuildGraphInfo QbsSession::getBuildGraphInfo(const FilePath &bgFilePath,
const QStringList &requestedProperties)
{
const QFileInfo bgFi = bgFilePath.toFileInfo();
QDir buildRoot = bgFi.dir();
buildRoot.cdUp();
QJsonObject request;
request.insert("type", "resolve-project");
request.insert("restore-behavior", "restore-only");
request.insert("configuration-name", bgFi.completeBaseName());
if (QbsSettings::useCreatorSettingsDirForQbs())
request.insert("settings-directory", QbsSettings::qbsSettingsBaseDir());
request.insert("build-root", buildRoot.path());
request.insert("error-handling-mode", "relaxed");
request.insert("data-mode", "only-if-changed");
request.insert("module-properties", QJsonArray::fromStringList(requestedProperties));
QbsSession session(nullptr);
session.sendRequest(request);
QJsonObject reply;
BuildGraphInfo bgInfo;
bgInfo.bgFilePath = bgFilePath;
QTimer::singleShot(10000, &session, [&session] { session.d->eventLoop.exit(1); });
connect(&session, &QbsSession::errorOccurred, [&] {
bgInfo.error = ErrorInfo(tr("Failed to load qbs build graph."));
});
connect(&session, &QbsSession::projectResolved, [&](const ErrorInfo &error) {
bgInfo.error = error;
session.d->eventLoop.quit();
});
if (session.d->eventLoop.exec(QEventLoop::ExcludeUserInputEvents) == 1) {
bgInfo.error = ErrorInfo(tr("Request timed out."));
return bgInfo;
}
if (bgInfo.error.hasError())
return bgInfo;
bgInfo.profileData = session.projectData().value("profile-data").toObject().toVariantMap();
bgInfo.overriddenProperties = session.projectData().value("overridden-properties").toObject()
.toVariantMap();
QStringList props = requestedProperties;
forAllProducts(session.projectData(), [&](const QJsonObject &product) {
if (props.empty())
return;
for (auto it = props.begin(); it != props.end();) {
const QVariant value = product.value("module-properties").toObject().value(*it);
if (value.isValid()) {
bgInfo.requestedProperties.insert(*it, value);
it = props.erase(it);
} else {
++it;
}
}
});
return bgInfo;
}
void QbsSession::handlePacket(const QJsonObject &packet)
{
const QString type = packet.value("type").toString();
if (type == "hello") {
QTC_CHECK(d->state == State::Initializing);
if (packet.value("api-compat-level").toInt() > 2) {
setError(Error::VersionMismatch);
return;
}
d->state = State::Active;
sendQueuedRequest();
} else if (type == "project-resolved") {
setProjectDataFromReply(packet, true);
emit projectResolved(getErrorInfo(packet));
} else if (type == "project-built") {
setProjectDataFromReply(packet, false);
emit projectBuilt(getErrorInfo(packet));
} else if (type == "project-cleaned") {
emit projectCleaned(getErrorInfo(packet));
} else if (type == "install-done") {
emit projectInstalled(getErrorInfo(packet));
} else if (type == "log-data") {
Core::MessageManager::write("[qbs] " + packet.value("message").toString(),
Core::MessageManager::Silent);
} else if (type == "warning") {
const ErrorInfo errorInfo = ErrorInfo(packet.value("warning").toObject());
// TODO: This loop occurs a lot. Factor it out.
for (const ErrorInfoItem &item : errorInfo.items) {
TaskHub::addTask(BuildSystemTask(Task::Warning, item.description,
item.filePath, item.line));
}
} else if (type == "task-started") {
emit taskStarted(packet.value("description").toString(),
packet.value("max-progress").toInt());
} else if (type == "task-progress") {
emit taskProgress(packet.value("progress").toInt());
} else if (type == "new-max-progress") {
emit maxProgressChanged(packet.value("max-progress").toInt());
} else if (type == "generated-files-for-sources") {
QHash<QString, QStringList> generatedFiles;
for (const QJsonValue &product : packet.value("products").toArray()) {
for (const QJsonValue &r : product.toObject().value("results").toArray()) {
const QJsonObject result = r.toObject();
generatedFiles[result.value("source-file").toString()]
<< arrayToStringList(result.value("generated-files").toArray());
}
}
if (generatedFiles != d->generatedFilesForSources) {
d->generatedFilesForSources = generatedFiles;
emit newGeneratedFilesForSources(generatedFiles);
}
} else if (type == "command-description") {
emit commandDescription(packet.value("message").toString());
} else if (type == "files-added" || type == "files-removed") {
handleFileListUpdated(packet);
} else if (type == "process-result") {
emit processResult(
FilePath::fromString(packet.value("executable-file-path").toString()),
arrayToStringList(packet.value("arguments")),
FilePath::fromString(packet.value("working-directory").toString()),
arrayToStringList(packet.value("stdout")),
arrayToStringList(packet.value("stderr")),
packet.value("success").toBool());
} else if (type == "run-environment") {
d->reply = packet;
d->eventLoop.quit();
} else if (type == "protocol-error") {
const ErrorInfo errorInfo = ErrorInfo(packet.value("error").toObject());
// TODO: This loop occurs a lot. Factor it out.
for (const ErrorInfoItem &item : errorInfo.items) {
TaskHub::addTask(BuildSystemTask(Task::Error, item.description,
item.filePath, item.line));
}
setError(Error::ProtocolError);
}
}
void QbsSession::sendQueuedRequest()
{
sendRequestNow(d->currentRequest);
d->currentRequest = QJsonObject();
}
void QbsSession::sendRequestNow(const QJsonObject &request)
{
QTC_ASSERT(d->state == State::Active, return);
if (!request.isEmpty())
d->qbsProcess->write(Packet::createPacket(request));
}
ErrorInfo QbsSession::getErrorInfo(const QJsonObject &packet)
{
return ErrorInfo(packet.value("error").toObject());
}
void QbsSession::setProjectDataFromReply(const QJsonObject &packet, bool withBuildSystemFiles)
{
const QJsonObject projectData = packet.value("project-data").toObject();
if (!projectData.isEmpty()) {
const QJsonValue buildSystemFiles = d->projectData.value("build-system-files");
d->projectData = projectData;
if (!withBuildSystemFiles)
d->projectData.insert("build-system-files", buildSystemFiles);
}
}
void QbsSession::setError(QbsSession::Error error)
{
d->lastError = error;
setInactive();
emit errorOccurred(error);
}
void QbsSession::setInactive()
{
if (d->state == State::Inactive)
return;
d->state = State::Inactive;
d->qbsProcess->disconnect(this);
d->currentRequest = QJsonObject();
d->packetReader->disconnect(this);
d->packetReader->deleteLater();
d->packetReader = nullptr;
if (d->qbsProcess->state() == QProcess::Running)
sendQuitPacket();
d->qbsProcess = nullptr;
}
FileChangeResult QbsSession::updateFileList(const char *action, const QStringList &files,
const QString &product, const QString &group)
{
if (d->state != State::Active)
return FileChangeResult(files, tr("The qbs session is not in a valid state."));
sendRequestNow(QJsonObject{
{"type", QLatin1String(action)},
{"files", QJsonArray::fromStringList(files)},
{"product", product},
{"group", group}
});
return FileChangeResult(QStringList());
}
void QbsSession::handleFileListUpdated(const QJsonObject &reply)
{
setProjectDataFromReply(reply, false);
const QStringList failedFiles = arrayToStringList(reply.value("failed-files"));
if (!failedFiles.isEmpty()) {
Core::MessageManager::write(tr("Failed to update files in Qbs project: %1.\n"
"The affected files are: \n\t%2")
.arg(getErrorInfo(reply).toString(),
failedFiles.join("\n\t")),
Core::MessageManager::ModeSwitch);
}
emit fileListUpdated();
}
ErrorInfoItem::ErrorInfoItem(const QJsonObject &data)
{
description = data.value("description").toString();
const QJsonObject locationData = data.value("location").toObject();
filePath = FilePath::fromString(locationData.value("file-path").toString());
line = locationData.value("line").toInt(-1);
}
QString ErrorInfoItem::toString() const
{
QString s = filePath.toUserOutput();
if (!s.isEmpty() && line != -1)
s.append(':').append(QString::number(line));
if (!s.isEmpty())
s.append(':');
return s.append(description);
}
ErrorInfo::ErrorInfo(const QJsonObject &data)
{
for (const QJsonValue &v : data.value("items").toArray())
items << ErrorInfoItem(v.toObject());
}
ErrorInfo::ErrorInfo(const QString &msg)
{
items << ErrorInfoItem(msg);
}
QString ErrorInfo::toString() const
{
return transform<QStringList>(items, [](const ErrorInfoItem &i) { return i.toString(); })
.join('\n');
}
void forAllProducts(const QJsonObject &project, const WorkerFunction &productFunction)
{
for (const QJsonValue &p : project.value("products").toArray())
productFunction(p.toObject());
for (const QJsonValue &p : project.value("sub-projects").toArray())
forAllProducts(p.toObject(), productFunction);
}
void forAllArtifacts(const QJsonObject &product, ArtifactType type,
const WorkerFunction &artifactFunction)
{
if (type == ArtifactType::Source || type == ArtifactType::All) {
for (const QJsonValue &g : product.value("groups").toArray())
forAllArtifacts(g.toObject(), artifactFunction);
}
if (type == ArtifactType::Generated || type == ArtifactType::All) {
for (const QJsonValue &v : product.value("generated-artifacts").toArray())
artifactFunction(v.toObject());
}
}
void forAllArtifacts(const QJsonObject &group, const WorkerFunction &artifactFunction)
{
for (const QJsonValue &v : group.value("source-artifacts").toArray())
artifactFunction(v.toObject());
for (const QJsonValue &v : group.value("source-artifacts-from-wildcards").toArray())
artifactFunction(v.toObject());
}
Location locationFromObject(const QJsonObject &o)
{
const QJsonObject loc = o.value("location").toObject();
return Location(FilePath::fromString(loc.value("file-path").toString()),
loc.value("line").toInt());
}
} // namespace Internal
} // namespace QbsProjectManager
#include <qbssession.moc>