diff options
Diffstat (limited to 'src/app/qbs')
-rw-r--r-- | src/app/qbs/commandlinefrontend.cpp | 6 | ||||
-rw-r--r-- | src/app/qbs/parser/commandlineparser.cpp | 1 | ||||
-rw-r--r-- | src/app/qbs/parser/commandpool.cpp | 3 | ||||
-rw-r--r-- | src/app/qbs/parser/commandtype.h | 2 | ||||
-rw-r--r-- | src/app/qbs/parser/parsercommand.cpp | 23 | ||||
-rw-r--r-- | src/app/qbs/parser/parsercommand.h | 14 | ||||
-rw-r--r-- | src/app/qbs/qbs.pro | 8 | ||||
-rw-r--r-- | src/app/qbs/qbs.qbs | 8 | ||||
-rw-r--r-- | src/app/qbs/session.cpp | 751 | ||||
-rw-r--r-- | src/app/qbs/session.h | 51 | ||||
-rw-r--r-- | src/app/qbs/sessionpacket.cpp | 113 | ||||
-rw-r--r-- | src/app/qbs/sessionpacket.h | 70 | ||||
-rw-r--r-- | src/app/qbs/sessionpacketreader.cpp | 85 | ||||
-rw-r--r-- | src/app/qbs/sessionpacketreader.h | 70 | ||||
-rw-r--r-- | src/app/qbs/stdinreader.cpp | 146 | ||||
-rw-r--r-- | src/app/qbs/stdinreader.h | 66 |
16 files changed, 1416 insertions, 1 deletions
diff --git a/src/app/qbs/commandlinefrontend.cpp b/src/app/qbs/commandlinefrontend.cpp index 95c3c10bc..8be06f3af 100644 --- a/src/app/qbs/commandlinefrontend.cpp +++ b/src/app/qbs/commandlinefrontend.cpp @@ -40,6 +40,7 @@ #include "application.h" #include "consoleprogressobserver.h" +#include "session.h" #include "status.h" #include "parser/commandlineoption.h" #include "../shared/logging/consolelogger.h" @@ -129,6 +130,10 @@ void CommandLineFrontend::start() throw ErrorInfo(error); } break; + case SessionCommandType: { + startSession(); + return; + } default: break; } @@ -418,6 +423,7 @@ void CommandLineFrontend::handleProjectsResolved() break; case HelpCommandType: case VersionCommandType: + case SessionCommandType: Q_ASSERT_X(false, Q_FUNC_INFO, "Impossible."); } } diff --git a/src/app/qbs/parser/commandlineparser.cpp b/src/app/qbs/parser/commandlineparser.cpp index 3c25c51e2..052f6b92f 100644 --- a/src/app/qbs/parser/commandlineparser.cpp +++ b/src/app/qbs/parser/commandlineparser.cpp @@ -386,6 +386,7 @@ QList<Command *> CommandLineParser::CommandLineParserPrivate::allCommands() cons commandPool.getCommand(DumpNodesTreeCommandType), commandPool.getCommand(ListProductsCommandType), commandPool.getCommand(VersionCommandType), + commandPool.getCommand(SessionCommandType), commandPool.getCommand(HelpCommandType)}; } diff --git a/src/app/qbs/parser/commandpool.cpp b/src/app/qbs/parser/commandpool.cpp index a49608c56..1362a563c 100644 --- a/src/app/qbs/parser/commandpool.cpp +++ b/src/app/qbs/parser/commandpool.cpp @@ -95,6 +95,9 @@ qbs::Command *CommandPool::getCommand(CommandType type) const case VersionCommandType: command = new VersionCommand(m_optionPool); break; + case SessionCommandType: + command = new SessionCommand(m_optionPool); + break; } } return command; diff --git a/src/app/qbs/parser/commandtype.h b/src/app/qbs/parser/commandtype.h index a8c618933..7d70356e7 100644 --- a/src/app/qbs/parser/commandtype.h +++ b/src/app/qbs/parser/commandtype.h @@ -45,7 +45,7 @@ enum CommandType { ResolveCommandType, BuildCommandType, CleanCommandType, RunCommandType, ShellCommandType, StatusCommandType, UpdateTimestampsCommandType, DumpNodesTreeCommandType, InstallCommandType, HelpCommandType, GenerateCommandType, ListProductsCommandType, - VersionCommandType, + VersionCommandType, SessionCommandType, }; } // namespace qbs diff --git a/src/app/qbs/parser/parsercommand.cpp b/src/app/qbs/parser/parsercommand.cpp index 9485b0878..c7185a725 100644 --- a/src/app/qbs/parser/parsercommand.cpp +++ b/src/app/qbs/parser/parsercommand.cpp @@ -593,4 +593,27 @@ void VersionCommand::parseNext(QStringList &input) throwError(Tr::tr("This command takes no arguments.")); } +QString SessionCommand::shortDescription() const +{ + return Tr::tr("Starts a session for an IDE."); +} + +QString SessionCommand::longDescription() const +{ + QString description = Tr::tr("qbs %1\n").arg(representation()); + return description += Tr::tr("Communicates on stdin and stdout via a JSON-based API.\n" + "Intended for use with other tools, such as IDEs.\n"); +} + +QString SessionCommand::representation() const +{ + return QLatin1String("session"); +} + +void SessionCommand::parseNext(QStringList &input) +{ + QBS_CHECK(!input.empty()); + throwError(Tr::tr("This command takes no arguments.")); +} + } // namespace qbs diff --git a/src/app/qbs/parser/parsercommand.h b/src/app/qbs/parser/parsercommand.h index 649563ba1..8998d38e6 100644 --- a/src/app/qbs/parser/parsercommand.h +++ b/src/app/qbs/parser/parsercommand.h @@ -261,6 +261,20 @@ private: void parseNext(QStringList &input) override; }; +class SessionCommand : public Command +{ +public: + SessionCommand(CommandLineOptionPool &optionPool) : Command(optionPool) {} + +private: + CommandType type() const override { return SessionCommandType; } + QString shortDescription() const override; + QString longDescription() const override; + QString representation() const override; + QList<CommandLineOption::Type> supportedOptions() const override { return {}; } + void parseNext(QStringList &input) override; +}; + } // namespace qbs #endif // QBS_PARSER_COMMAND_H diff --git a/src/app/qbs/qbs.pro b/src/app/qbs/qbs.pro index ac9d6f0ca..e9f0061c6 100644 --- a/src/app/qbs/qbs.pro +++ b/src/app/qbs/qbs.pro @@ -6,6 +6,10 @@ TARGET = qbs SOURCES += main.cpp \ ctrlchandler.cpp \ application.cpp \ + session.cpp \ + sessionpacket.cpp \ + sessionpacketreader.cpp \ + stdinreader.cpp \ status.cpp \ consoleprogressobserver.cpp \ commandlinefrontend.cpp \ @@ -14,6 +18,10 @@ SOURCES += main.cpp \ HEADERS += \ ctrlchandler.h \ application.h \ + session.h \ + sessionpacket.h \ + sessionpacketreader.h \ + stdinreader.h \ status.h \ consoleprogressobserver.h \ commandlinefrontend.h \ diff --git a/src/app/qbs/qbs.qbs b/src/app/qbs/qbs.qbs index f22fe5de5..91357445e 100644 --- a/src/app/qbs/qbs.qbs +++ b/src/app/qbs/qbs.qbs @@ -23,8 +23,16 @@ QbsApp { "main.cpp", "qbstool.cpp", "qbstool.h", + "session.cpp", + "session.h", + "sessionpacket.cpp", + "sessionpacket.h", + "sessionpacketreader.cpp", + "sessionpacketreader.h", "status.cpp", "status.h", + "stdinreader.cpp", + "stdinreader.h", ] Group { name: "parser" diff --git a/src/app/qbs/session.cpp b/src/app/qbs/session.cpp new file mode 100644 index 000000000..0b93aff49 --- /dev/null +++ b/src/app/qbs/session.cpp @@ -0,0 +1,751 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** 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 "session.h" + +#include "sessionpacket.h" +#include "sessionpacketreader.h" + +#include <api/jobs.h> +#include <api/project.h> +#include <api/projectdata.h> +#include <api/runenvironment.h> +#include <logging/ilogsink.h> +#include <tools/buildoptions.h> +#include <tools/cleanoptions.h> +#include <tools/error.h> +#include <tools/installoptions.h> +#include <tools/jsonhelper.h> +#include <tools/preferences.h> +#include <tools/processresult.h> +#include <tools/qbsassert.h> +#include <tools/settings.h> +#include <tools/setupprojectparameters.h> +#include <tools/stringconstants.h> + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qdir.h> +#include <QtCore/qjsonarray.h> +#include <QtCore/qjsonobject.h> +#include <QtCore/qobject.h> +#include <QtCore/qprocess.h> + +#include <algorithm> +#include <cstdlib> +#include <iostream> +#include <memory> + +namespace qbs { +namespace Internal { + +class SessionLogSink : public QObject, public ILogSink +{ + Q_OBJECT +signals: + void newMessage(const QJsonObject &msg); + +private: + void doPrintMessage(LoggerLevel, const QString &message, const QString &) override + { + QJsonObject msg; + msg.insert(StringConstants::type(), QLatin1String("log-data")); + msg.insert(StringConstants::messageKey(), message); + emit newMessage(msg); + } + + void doPrintWarning(const ErrorInfo &warning) override + { + QJsonObject msg; + static const QString warningString(QLatin1String("warning")); + msg.insert(StringConstants::type(), warningString); + msg.insert(warningString, warning.toJson()); + emit newMessage(msg); + } +}; + +class Session : public QObject +{ + Q_OBJECT +public: + Session(); + +private: + enum class ProjectDataMode { Never, Always, OnlyIfChanged }; + ProjectDataMode dataModeFromRequest(const QJsonObject &request); + QStringList modulePropertiesFromRequest(const QJsonObject &request); + void insertProjectDataIfNecessary( + QJsonObject &reply, + ProjectDataMode dataMode, + const ProjectData &oldProjectData, + bool includeTopLevelData + ); + void setLogLevelFromRequest(const QJsonObject &request); + bool checkNormalRequestPrerequisites(const char *replyType); + + void sendPacket(const QJsonObject &message); + void setupProject(const QJsonObject &request); + void buildProject(const QJsonObject &request); + void cleanProject(const QJsonObject &request); + void installProject(const QJsonObject &request); + void addFiles(const QJsonObject &request); + void removeFiles(const QJsonObject &request); + void getRunEnvironment(const QJsonObject &request); + void getGeneratedFilesForSources(const QJsonObject &request); + void releaseProject(); + void cancelCurrentJob(); + void quitSession(); + + void sendErrorReply(const char *replyType, const QString &message); + void sendErrorReply(const char *replyType, const ErrorInfo &error); + void insertErrorInfoIfNecessary(QJsonObject &reply, const ErrorInfo &error); + void connectProgressSignals(AbstractJob *job); + QList<ProductData> getProductsByName(const QStringList &productNames) const; + ProductData getProductByName(const QString &productName) const; + + struct ProductSelection { + ProductSelection(Project::ProductSelection s) : selection(s) {} + ProductSelection(const QList<ProductData> &p) : products(p) {} + + Project::ProductSelection selection = Project::ProductSelectionDefaultOnly; + QList<ProductData> products; + }; + ProductSelection getProductSelection(const QJsonObject &request); + + struct FileUpdateData { + QJsonObject createErrorReply(const char *type, const QString &mainMessage) const; + + ProductData product; + GroupData group; + QStringList filePaths; + ErrorInfo error; + }; + FileUpdateData prepareFileUpdate(const QJsonObject &request); + + SessionPacketReader m_packetReader; + Project m_project; + ProjectData m_projectData; + SessionLogSink m_logSink; + std::unique_ptr<Settings> m_settings; + QJsonObject m_resolveRequest; + QStringList m_moduleProperties; + AbstractJob *m_currentJob = nullptr; +}; + +void startSession() +{ + const auto session = new Session; + QObject::connect(qApp, &QCoreApplication::aboutToQuit, session, [session] { delete session; }); +} + +Session::Session() +{ + sendPacket(SessionPacket::helloMessage()); + connect(&m_logSink, &SessionLogSink::newMessage, this, &Session::sendPacket); + connect(&m_packetReader, &SessionPacketReader::errorOccurred, + this, [](const QString &msg) { + std::cerr << qPrintable(tr("Error: %1").arg(msg)); + qApp->exit(EXIT_FAILURE); + }); + connect(&m_packetReader, &SessionPacketReader::packetReceived, this, [this](const QJsonObject &packet) { + // qDebug() << "got packet:" << packet; // Uncomment for debugging. + const QString type = packet.value(StringConstants::type()).toString(); + if (type == QLatin1String("resolve-project")) + setupProject(packet); + else if (type == QLatin1String("build-project")) + buildProject(packet); + else if (type == QLatin1String("clean-project")) + cleanProject(packet); + else if (type == QLatin1String("install-project")) + installProject(packet); + else if (type == QLatin1String("add-files")) + addFiles(packet); + else if (type == QLatin1String("remove-files")) + removeFiles(packet); + else if (type == QLatin1String("get-run-environment")) + getRunEnvironment(packet); + else if (type == QLatin1String("get-generated-files-for-sources")) + getGeneratedFilesForSources(packet); + else if (type == QLatin1String("release-project")) + releaseProject(); + else if (type == QLatin1String("quit")) + quitSession(); + else if (type == QLatin1String("cancel-job")) + cancelCurrentJob(); + else + sendErrorReply("protocol-error", tr("Unknown request type '%1'.").arg(type)); + }); + m_packetReader.start(); +} + +Session::ProjectDataMode Session::dataModeFromRequest(const QJsonObject &request) +{ + const QString modeString = request.value(QLatin1String("data-mode")).toString(); + if (modeString == QLatin1String("only-if-changed")) + return ProjectDataMode::OnlyIfChanged; + if (modeString == QLatin1String("always")) + return ProjectDataMode::Always; + return ProjectDataMode::Never; +} + +void Session::sendPacket(const QJsonObject &message) +{ + std::cout << SessionPacket::createPacket(message).constData() << std::flush; +} + +void Session::setupProject(const QJsonObject &request) +{ + if (m_currentJob) { + if (qobject_cast<SetupProjectJob *>(m_currentJob) + && m_currentJob->state() == AbstractJob::StateCanceling) { + m_resolveRequest = std::move(request); + return; + } + sendErrorReply("project-resolved", + tr("Cannot start resolving while another job is still running.")); + return; + } + m_moduleProperties = modulePropertiesFromRequest(request); + auto params = SetupProjectParameters::fromJson(request); + const ProjectDataMode dataMode = dataModeFromRequest(request); + m_settings.reset(new Settings(params.settingsDirectory())); + const Preferences prefs(m_settings.get()); + const QString appDir = QDir::cleanPath(QCoreApplication::applicationDirPath()); + params.setSearchPaths(prefs.searchPaths(appDir + QLatin1String( + "/" QBS_RELATIVE_SEARCH_PATH))); + params.setPluginPaths(prefs.pluginPaths(appDir + QLatin1String( + "/" QBS_RELATIVE_PLUGINS_PATH))); + params.setLibexecPath(appDir + QLatin1String("/" QBS_RELATIVE_LIBEXEC_PATH)); + params.setOverrideBuildGraphData(true); + setLogLevelFromRequest(request); + SetupProjectJob * const setupJob = m_project.setupProject(params, &m_logSink, this); + m_currentJob = setupJob; + connectProgressSignals(setupJob); + connect(setupJob, &AbstractJob::finished, this, + [this, setupJob, dataMode](bool success) { + if (!m_resolveRequest.isEmpty()) { // Canceled job was superseded. + const QJsonObject newRequest = std::move(m_resolveRequest); + m_resolveRequest = QJsonObject(); + m_currentJob->deleteLater(); + m_currentJob = nullptr; + setupProject(newRequest); + return; + } + const ProjectData oldProjectData = m_projectData; + m_project = setupJob->project(); + m_projectData = m_project.projectData(); + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String("project-resolved")); + if (success) + insertProjectDataIfNecessary(reply, dataMode, oldProjectData, true); + else + insertErrorInfoIfNecessary(reply, setupJob->error()); + sendPacket(reply); + m_currentJob->deleteLater(); + m_currentJob = nullptr; + }); +} + +void Session::buildProject(const QJsonObject &request) +{ + if (!checkNormalRequestPrerequisites("build-done")) + return; + const ProductSelection productSelection = getProductSelection(request); + setLogLevelFromRequest(request); + auto options = BuildOptions::fromJson(request); + options.setSettingsDirectory(m_settings->baseDirectory()); + BuildJob * const buildJob = productSelection.products.empty() + ? m_project.buildAllProducts(options, productSelection.selection, this) + : m_project.buildSomeProducts(productSelection.products, options, this); + m_currentJob = buildJob; + m_moduleProperties = modulePropertiesFromRequest(request); + const ProjectDataMode dataMode = dataModeFromRequest(request); + connectProgressSignals(buildJob); + connect(buildJob, &BuildJob::reportCommandDescription, this, + [this](const QString &highlight, const QString &message) { + QJsonObject descData; + descData.insert(StringConstants::type(), QLatin1String("command-description")); + descData.insert(QLatin1String("highlight"), highlight); + descData.insert(StringConstants::messageKey(), message); + sendPacket(descData); + }); + connect(buildJob, &BuildJob::reportProcessResult, this, [this](const ProcessResult &result) { + if (result.success() && result.stdOut().isEmpty() && result.stdErr().isEmpty()) + return; + QJsonObject resultData = result.toJson(); + resultData.insert(StringConstants::type(), QLatin1String("process-result")); + sendPacket(resultData); + }); + connect(buildJob, &BuildJob::finished, this, + [this, dataMode](bool success) { + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String("project-built")); + const ProjectData oldProjectData = m_projectData; + m_projectData = m_project.projectData(); + if (success) + insertProjectDataIfNecessary(reply, dataMode, oldProjectData, false); + else + insertErrorInfoIfNecessary(reply, m_currentJob->error()); + sendPacket(reply); + m_currentJob->deleteLater(); + m_currentJob = nullptr; + }); +} + +void Session::cleanProject(const QJsonObject &request) +{ + if (!checkNormalRequestPrerequisites("project-cleaned")) + return; + setLogLevelFromRequest(request); + const ProductSelection productSelection = getProductSelection(request); + const auto options = CleanOptions::fromJson(request); + m_currentJob = productSelection.products.empty() + ? m_project.cleanAllProducts(options, this) + : m_project.cleanSomeProducts(productSelection.products, options, this); + connectProgressSignals(m_currentJob); + connect(m_currentJob, &AbstractJob::finished, this, [this](bool success) { + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String("project-cleaned")); + if (!success) + insertErrorInfoIfNecessary(reply, m_currentJob->error()); + sendPacket(reply); + m_currentJob->deleteLater(); + m_currentJob = nullptr; + }); +} + +void Session::installProject(const QJsonObject &request) +{ + if (!checkNormalRequestPrerequisites("install-done")) + return; + setLogLevelFromRequest(request); + const ProductSelection productSelection = getProductSelection(request); + const auto options = InstallOptions::fromJson(request); + m_currentJob = productSelection.products.empty() + ? m_project.installAllProducts(options, productSelection.selection, this) + : m_project.installSomeProducts(productSelection.products, options, this); + connectProgressSignals(m_currentJob); + connect(m_currentJob, &AbstractJob::finished, this, [this](bool success) { + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String("install-done")); + if (!success) + insertErrorInfoIfNecessary(reply, m_currentJob->error()); + sendPacket(reply); + m_currentJob->deleteLater(); + m_currentJob = nullptr; + }); +} + +void Session::addFiles(const QJsonObject &request) +{ + const FileUpdateData data = prepareFileUpdate(request); + if (data.error.hasError()) { + sendPacket(data.createErrorReply("files-added", tr("Failed to add files to project: %1") + .arg(data.error.toString()))); + return; + } + ErrorInfo error; + QStringList failedFiles; +#ifdef QBS_ENABLE_PROJECT_FILE_UPDATES + for (const QString &filePath : data.filePaths) { + const ErrorInfo e = m_project.addFiles(data.product, data.group, {filePath}); + if (e.hasError()) { + for (const ErrorItem &ei : e.items()) + error.append(ei); + failedFiles.push_back(filePath); + } + } +#endif + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String("files-added")); + insertErrorInfoIfNecessary(reply, error); + + if (failedFiles.size() != data.filePaths.size()) { + // Note that Project::addFiles() directly changes the existing project data object, so + // there's no need to retrieve it from m_project. + insertProjectDataIfNecessary(reply, ProjectDataMode::Always, {}, false); + } + + if (!failedFiles.isEmpty()) + reply.insert(QLatin1String("failed-files"), QJsonArray::fromStringList(failedFiles)); + sendPacket(reply); +} + +void Session::removeFiles(const QJsonObject &request) +{ + const FileUpdateData data = prepareFileUpdate(request); + if (data.error.hasError()) { + sendPacket(data.createErrorReply("files-removed", + tr("Failed to remove files from project: %1") + .arg(data.error.toString()))); + return; + } + ErrorInfo error; + QStringList failedFiles; +#ifdef QBS_ENABLE_PROJECT_FILE_UPDATES + for (const QString &filePath : data.filePaths) { + const ErrorInfo e = m_project.removeFiles(data.product, data.group, {filePath}); + if (e.hasError()) { + for (const ErrorItem &ei : e.items()) + error.append(ei); + failedFiles.push_back(filePath); + } + } +#endif + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String("files-removed")); + insertErrorInfoIfNecessary(reply, error); + if (failedFiles.size() != data.filePaths.size()) + insertProjectDataIfNecessary(reply, ProjectDataMode::Always, {}, false); + if (!failedFiles.isEmpty()) + reply.insert(QLatin1String("failed-files"), QJsonArray::fromStringList(failedFiles)); + sendPacket(reply); +} + +void Session::connectProgressSignals(AbstractJob *job) +{ + static QString maxProgressString(QLatin1String("max-progress")); + connect(job, &AbstractJob::taskStarted, this, + [this](const QString &description, int maxProgress) { + QJsonObject msg; + msg.insert(StringConstants::type(), QLatin1String("task-started")); + msg.insert(StringConstants::descriptionProperty(), description); + msg.insert(maxProgressString, maxProgress); + sendPacket(msg); + }); + connect(job, &AbstractJob::totalEffortChanged, this, [this](int maxProgress) { + QJsonObject msg; + msg.insert(StringConstants::type(), QLatin1String("new-max-progress")); + msg.insert(maxProgressString, maxProgress); + sendPacket(msg); + }); + connect(job, &AbstractJob::taskProgress, this, [this](int progress) { + QJsonObject msg; + msg.insert(StringConstants::type(), QLatin1String("task-progress")); + msg.insert(QLatin1String("progress"), progress); + sendPacket(msg); + }); +} + +static QList<ProductData> getProductsByNameForProject(const ProjectData &project, + QStringList &productNames) +{ + QList<ProductData> products; + if (productNames.empty()) + return products; + for (const ProductData &p : project.products()) { + for (auto it = productNames.begin(); it != productNames.end(); ++it) { + if (*it == p.fullDisplayName()) { + products << p; + productNames.erase(it); + if (productNames.empty()) + return products; + break; + } + } + } + for (const ProjectData &p : project.subProjects()) { + products << getProductsByNameForProject(p, productNames); + if (productNames.empty()) + break; + } + return products; +} + +QList<ProductData> Session::getProductsByName(const QStringList &productNames) const +{ + QStringList remainingNames = productNames; + return getProductsByNameForProject(m_projectData, remainingNames); +} + +ProductData Session::getProductByName(const QString &productName) const +{ + const QList<ProductData> products = getProductsByName({productName}); + return products.empty() ? ProductData() : products.first(); +} + +void Session::getRunEnvironment(const QJsonObject &request) +{ + const char * const replyType = "run-environment"; + if (!checkNormalRequestPrerequisites(replyType)) + return; + const QString productName = request.value(QLatin1String("product")).toString(); + const ProductData product = getProductByName(productName); + if (!product.isValid()) { + sendErrorReply(replyType, tr("No such product '%1'.").arg(productName)); + return; + } + const auto inEnv = fromJson<QProcessEnvironment>( + request.value(QLatin1String("base-environment"))); + const QStringList config = fromJson<QStringList>(request.value(QLatin1String("config"))); + const RunEnvironment runEnv = m_project.getRunEnvironment(product, InstallOptions(), inEnv, + config, m_settings.get()); + ErrorInfo error; + const QProcessEnvironment outEnv = runEnv.runEnvironment(&error); + if (error.hasError()) { + sendErrorReply(replyType, error); + return; + } + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String(replyType)); + QJsonObject outEnvObj; + const QStringList keys = outEnv.keys(); + for (const QString &key : keys) + outEnvObj.insert(key, outEnv.value(key)); + reply.insert(QLatin1String("full-environment"), outEnvObj); + sendPacket(reply); +} + +void Session::getGeneratedFilesForSources(const QJsonObject &request) +{ + const char * const replyType = "generated-files-for-sources"; + if (!checkNormalRequestPrerequisites(replyType)) + return; + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String(replyType)); + const QJsonArray specs = request.value(StringConstants::productsKey()).toArray(); + QJsonArray resultProducts; + for (const QJsonValue &p : specs) { + const QJsonObject productObject = p.toObject(); + const ProductData product = getProductByName( + productObject.value(StringConstants::fullDisplayNameKey()).toString()); + if (!product.isValid()) + continue; + QJsonObject resultProduct; + resultProduct.insert(StringConstants::fullDisplayNameKey(), product.fullDisplayName()); + QJsonArray results; + const QJsonArray requests = productObject.value(QLatin1String("requests")).toArray(); + for (const QJsonValue &r : requests) { + const QJsonObject request = r.toObject(); + const QString filePath = request.value(QLatin1String("source-file")).toString(); + const QStringList tags = fromJson<QStringList>(request.value(QLatin1String("tags"))); + const bool recursive = request.value(QLatin1String("recursive")).toBool(); + const QStringList generatedFiles = m_project.generatedFiles(product, filePath, + recursive, tags); + if (!generatedFiles.isEmpty()) { + QJsonObject result; + result.insert(QLatin1String("source-file"), filePath); + result.insert(QLatin1String("generated-files"), + QJsonArray::fromStringList(generatedFiles)); + results << result; + } + } + if (!results.isEmpty()) { + resultProduct.insert(QLatin1String("results"), results); + resultProducts << resultProduct; + } + } + reply.insert(StringConstants::productsKey(), resultProducts); + sendPacket(reply); +} + +void Session::releaseProject() +{ + const char * const replyType = "project-released"; + if (!m_project.isValid()) { + sendErrorReply(replyType, tr("No open project.")); + return; + } + if (m_currentJob) { + m_currentJob->disconnect(this); + m_currentJob->cancel(); + m_currentJob = nullptr; + } + m_project = Project(); + m_projectData = ProjectData(); + m_resolveRequest = QJsonObject(); + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String(replyType)); + sendPacket(reply); +} + +void Session::cancelCurrentJob() +{ + if (m_currentJob) { + if (!m_resolveRequest.isEmpty()) + m_resolveRequest = QJsonObject(); + m_currentJob->cancel(); + } +} + +Session::ProductSelection Session::getProductSelection(const QJsonObject &request) +{ + const QJsonValue productSelection = request.value(StringConstants::productsKey()); + if (productSelection.isArray()) + return ProductSelection(getProductsByName(fromJson<QStringList>(productSelection))); + return ProductSelection(productSelection.toString() == QLatin1String("all") + ? Project::ProductSelectionWithNonDefault + : Project::ProductSelectionDefaultOnly); +} + +Session::FileUpdateData Session::prepareFileUpdate(const QJsonObject &request) +{ + FileUpdateData data; + const QString productName = request.value(QLatin1String("product")).toString(); + data.product = getProductByName(productName); + if (data.product.isValid()) { + const QString groupName = request.value(QLatin1String("group")).toString(); + for (const GroupData &g : data.product.groups()) { + if (g.name() == groupName) { + data.group = g; + break; + } + } + if (!data.group.isValid()) + data.error = tr("Group '%1' not found in product '%2'.").arg(groupName, productName); + } else { + data.error = tr("Product '%1' not found in project.").arg(productName); + } + const QJsonArray filesArray = request.value(QLatin1String("files")).toArray(); + for (const QJsonValue &v : filesArray) + data.filePaths << v.toString(); + if (m_currentJob) + data.error = tr("Cannot update the list of source files while a job is running."); + if (!m_project.isValid()) + data.error = tr("No valid project. You need to resolve first."); +#ifndef QBS_ENABLE_PROJECT_FILE_UPDATES + data.error = ErrorInfo(tr("Project file updates are not enabled in this build of qbs.")); +#endif + return data; +} + +void Session::insertProjectDataIfNecessary(QJsonObject &reply, ProjectDataMode dataMode, + const ProjectData &oldProjectData, bool includeTopLevelData) +{ + const bool sendProjectData = dataMode == ProjectDataMode::Always + || (dataMode == ProjectDataMode::OnlyIfChanged && m_projectData != oldProjectData); + if (!sendProjectData) + return; + QJsonObject projectData = m_projectData.toJson(m_moduleProperties); + if (includeTopLevelData) { + QJsonArray buildSystemFiles; + for (const QString &f : m_project.buildSystemFiles()) + buildSystemFiles.push_back(f); + projectData.insert(StringConstants::buildDirectoryKey(), m_projectData.buildDirectory()); + projectData.insert(QLatin1String("build-system-files"), buildSystemFiles); + const Project::BuildGraphInfo bgInfo = m_project.getBuildGraphInfo(); + projectData.insert(QLatin1String("build-graph-file-path"), bgInfo.bgFilePath); + projectData.insert(QLatin1String("profile-data"), + QJsonObject::fromVariantMap(bgInfo.profileData)); + projectData.insert(QLatin1String("overridden-properties"), + QJsonObject::fromVariantMap(bgInfo.overriddenProperties)); + } + reply.insert(QLatin1String("project-data"), projectData); +} + +void Session::setLogLevelFromRequest(const QJsonObject &request) +{ + const QString logLevelString = request.value(QLatin1String("log-level")).toString(); + if (logLevelString.isEmpty()) + return; + for (const LoggerLevel l : {LoggerError, LoggerWarning, LoggerInfo, LoggerDebug, LoggerTrace}) { + if (logLevelString == logLevelName(l)) { + m_logSink.setLogLevel(l); + return; + } + } +} + +bool Session::checkNormalRequestPrerequisites(const char *replyType) +{ + if (m_currentJob) { + sendErrorReply(replyType, tr("Another job is still running.")); + return false; + } + if (!m_project.isValid()) { + sendErrorReply(replyType, tr("No valid project. You need to resolve first.")); + return false; + } + return true; +} + +QStringList Session::modulePropertiesFromRequest(const QJsonObject &request) +{ + return fromJson<QStringList>(request.value(StringConstants::modulePropertiesKey())); +} + +void Session::sendErrorReply(const char *replyType, const ErrorInfo &error) +{ + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String(replyType)); + insertErrorInfoIfNecessary(reply, error); + sendPacket(reply); +} + +void Session::sendErrorReply(const char *replyType, const QString &message) +{ + sendErrorReply(replyType, ErrorInfo(message)); +} + +void Session::insertErrorInfoIfNecessary(QJsonObject &reply, const ErrorInfo &error) +{ + if (error.hasError()) + reply.insert(QLatin1String("error"), error.toJson()); +} + +void Session::quitSession() +{ + m_logSink.disconnect(this); + m_packetReader.disconnect(this); + if (m_currentJob) { + m_currentJob->disconnect(this); + connect(m_currentJob, &AbstractJob::finished, qApp, QCoreApplication::quit); + m_currentJob->cancel(); + } else { + qApp->quit(); + } +} + +QJsonObject Session::FileUpdateData::createErrorReply(const char *type, + const QString &mainMessage) const +{ + QBS_ASSERT(error.hasError(), return QJsonObject()); + ErrorInfo error(mainMessage); + for (const ErrorItem &ei : error.items()) + error.append(ei); + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String(type)); + reply.insert(QLatin1String("error"), error.toJson()); + reply.insert(QLatin1String("failed-files"), QJsonArray::fromStringList(filePaths)); + return reply; +} + +} // namespace Internal +} // namespace qbs + +#include <session.moc> diff --git a/src/app/qbs/session.h b/src/app/qbs/session.h new file mode 100644 index 000000000..ebbc93b1f --- /dev/null +++ b/src/app/qbs/session.h @@ -0,0 +1,51 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** 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$ +** +****************************************************************************/ + +#ifndef QBS_SESSION_H +#define QBS_SESSION_H + +namespace qbs { +namespace Internal { + +void startSession(); + +} // namespace Internal +} // namespace qbs + +#endif // Include guard diff --git a/src/app/qbs/sessionpacket.cpp b/src/app/qbs/sessionpacket.cpp new file mode 100644 index 000000000..ce9fdaf76 --- /dev/null +++ b/src/app/qbs/sessionpacket.cpp @@ -0,0 +1,113 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** 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 "sessionpacket.h" + +#include <tools/qbsassert.h> +#include <tools/stringconstants.h> +#include <tools/version.h> + +#include <QtCore/qdebug.h> +#include <QtCore/qjsondocument.h> +#include <QtCore/qjsonvalue.h> +#include <QtCore/qstring.h> + +namespace qbs { +namespace Internal { + +const QByteArray packetStart = "qbsmsg:"; + +SessionPacket::Status SessionPacket::parseInput(QByteArray &input) +{ + //qDebug() << m_expectedPayloadLength << m_payload << 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(); + QBS_ASSERT(bytesToAdd >= 0, return Status::Invalid); + m_payload += input.left(bytesToAdd); + input.remove(0, bytesToAdd); + return isComplete() ? Status::Complete : Status::Incomplete; +} + +QJsonObject SessionPacket::retrievePacket() +{ + QBS_ASSERT(isComplete(), return QJsonObject()); + const auto packet = QJsonDocument::fromJson(QByteArray::fromBase64(m_payload)).object(); + m_payload.clear(); + m_expectedPayloadLength = -1; + return packet; +} + +QByteArray SessionPacket::createPacket(const QJsonObject &packet) +{ + const QByteArray jsonData = QJsonDocument(packet).toJson(QJsonDocument::Compact).toBase64(); + return QByteArray(packetStart).append(QByteArray::number(jsonData.length())).append('\n') + .append(jsonData); +} + +QJsonObject SessionPacket::helloMessage() +{ + return QJsonObject{ + {StringConstants::type(), QLatin1String("hello")}, + {QLatin1String("api-level"), 1}, + {QLatin1String("api-compat-level"), 1} + }; +} + +bool SessionPacket::isComplete() const +{ + return m_payload.length() == m_expectedPayloadLength; +} + +} // namespace Internal +} // namespace qbs diff --git a/src/app/qbs/sessionpacket.h b/src/app/qbs/sessionpacket.h new file mode 100644 index 000000000..d919ff340 --- /dev/null +++ b/src/app/qbs/sessionpacket.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** 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$ +** +****************************************************************************/ + +#ifndef QBS_SESSIONPACKET_H +#define QBS_SESSIONPACKET_H + +#include <QtCore/qbytearray.h> +#include <QtCore/qjsonobject.h> + +namespace qbs { +namespace Internal { + +class SessionPacket +{ +public: + enum class Status { Incomplete, Complete, Invalid }; + Status parseInput(QByteArray &input); + + QJsonObject retrievePacket(); + + static QByteArray createPacket(const QJsonObject &packet); + static QJsonObject helloMessage(); + +private: + bool isComplete() const; + + QByteArray m_payload; + int m_expectedPayloadLength = -1; +}; + +} // namespace Internal +} // namespace qbs + +#endif // Include guard diff --git a/src/app/qbs/sessionpacketreader.cpp b/src/app/qbs/sessionpacketreader.cpp new file mode 100644 index 000000000..fe4b73f69 --- /dev/null +++ b/src/app/qbs/sessionpacketreader.cpp @@ -0,0 +1,85 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** 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 "sessionpacketreader.h" + +#include "sessionpacket.h" +#include "stdinreader.h" + +namespace qbs { +namespace Internal { + +class SessionPacketReader::Private +{ +public: + QByteArray incomingData; + SessionPacket currentPacket; +}; + +SessionPacketReader::SessionPacketReader(QObject *parent) : QObject(parent), d(new Private) { } + +SessionPacketReader::~SessionPacketReader() +{ + delete d; +} + +void SessionPacketReader::start() +{ + StdinReader * const stdinReader = StdinReader::create(this); + connect(stdinReader, &StdinReader::errorOccurred, this, &SessionPacketReader::errorOccurred); + connect(stdinReader, &StdinReader::dataAvailable, this, [this](const QByteArray &data) { + d->incomingData += data; + while (!d->incomingData.isEmpty()) { + switch (d->currentPacket.parseInput(d->incomingData)) { + case SessionPacket::Status::Invalid: + emit errorOccurred(tr("Received invalid input.")); + return; + case SessionPacket::Status::Complete: + emit packetReceived(d->currentPacket.retrievePacket()); + break; + case SessionPacket::Status::Incomplete: + return; + } + } + }); + stdinReader->start(); +} + +} // namespace Internal +} // namespace qbs diff --git a/src/app/qbs/sessionpacketreader.h b/src/app/qbs/sessionpacketreader.h new file mode 100644 index 000000000..87d70cf39 --- /dev/null +++ b/src/app/qbs/sessionpacketreader.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** 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$ +** +****************************************************************************/ + +#ifndef QBS_SESSIONPACKETREADER_H +#define QBS_SESSIONPACKETREADER_H + +#include <QtCore/qjsonobject.h> +#include <QtCore/qobject.h> + +namespace qbs { +namespace Internal { + +class SessionPacketReader : public QObject +{ + Q_OBJECT +public: + explicit SessionPacketReader(QObject *parent = nullptr); + ~SessionPacketReader(); + + void start(); + +signals: + void packetReceived(const QJsonObject &packet); + void errorOccurred(const QString &msg); + +private: + class Private; + Private * const d; +}; + +} // namespace Internal +} // namespace qbs + +#endif // Include guard diff --git a/src/app/qbs/stdinreader.cpp b/src/app/qbs/stdinreader.cpp new file mode 100644 index 000000000..4f784505d --- /dev/null +++ b/src/app/qbs/stdinreader.cpp @@ -0,0 +1,146 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** 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 "stdinreader.h" + +#include <tools/hostosinfo.h> + +#include <QtCore/qfile.h> +#include <QtCore/qsocketnotifier.h> + +#include <cerrno> +#include <cstring> + +#ifdef Q_OS_WIN32 +#include <qt_windows.h> +#include <QtCore/qtimer.h> +#else +#include <fcntl.h> +#endif + +namespace qbs { +namespace Internal { + +class UnixStdinReader : public StdinReader +{ +public: + UnixStdinReader(QObject *parent) : StdinReader(parent), m_notifier(0, QSocketNotifier::Read) {} + +private: + void start() override + { + if (!m_stdIn.open(stdin, QIODevice::ReadOnly)) { + emit errorOccurred(tr("Cannot read from standard input.")); + return; + } + const auto emitError = [this] { + emit errorOccurred(tr("Failed to make standard input non-blocking: %1") + .arg(QLatin1String(std::strerror(errno)))); + }; +#ifdef Q_OS_UNIX + const int flags = fcntl(0, F_GETFL, 0); + if (flags == -1) { + emitError(); + return; + } + if (fcntl(0, F_SETFL, flags | O_NONBLOCK)) { + emitError(); + return; + } +#endif + connect(&m_notifier, &QSocketNotifier::activated, this, [this] { + emit dataAvailable(m_stdIn.readAll()); + }); + } + + QFile m_stdIn; + QSocketNotifier m_notifier; +}; + +class WindowsStdinReader : public StdinReader +{ +public: + WindowsStdinReader(QObject *parent) : StdinReader(parent) {} + +private: + void start() override + { +#ifdef Q_OS_WIN32 + m_stdinHandle = GetStdHandle(STD_INPUT_HANDLE); + if (!m_stdinHandle) { + emit errorOccurred(tr("Failed to create handle for standard input.")); + return; + } + + // A timer seems slightly less awful than to block in a thread + // (how would we abort that one?), but ideally we'd like + // to have a signal-based approach like in the Unix variant. + const auto timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, [this] { + char buf[1024]; + DWORD bytesAvail; + PeekNamedPipe(m_stdinHandle, nullptr, 0, nullptr, &bytesAvail, nullptr); + while (bytesAvail > 0) { + DWORD bytesRead; + ReadFile(m_stdinHandle, buf, std::min<DWORD>(bytesAvail, sizeof buf), &bytesRead, + nullptr); + emit dataAvailable(QByteArray(buf, bytesRead)); + bytesAvail -= bytesRead; + } + }); + timer->start(10); +#endif + } + +#ifdef Q_OS_WIN32 + HANDLE m_stdinHandle; +#endif +}; + +StdinReader *StdinReader::create(QObject *parent) +{ + if (HostOsInfo::isWindowsHost()) + return new WindowsStdinReader(parent); + return new UnixStdinReader(parent); +} + +StdinReader::StdinReader(QObject *parent) : QObject(parent) { } + +} // namespace Internal +} // namespace qbs diff --git a/src/app/qbs/stdinreader.h b/src/app/qbs/stdinreader.h new file mode 100644 index 000000000..b3737e5ae --- /dev/null +++ b/src/app/qbs/stdinreader.h @@ -0,0 +1,66 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** 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$ +** +****************************************************************************/ + +#ifndef QBS_STDINREADER_H +#define QBS_STDINREADER_H + +#include <QtCore/qobject.h> + +namespace qbs { +namespace Internal { + +class StdinReader : public QObject +{ + Q_OBJECT +public: + static StdinReader *create(QObject *parent); + virtual void start() = 0; + +signals: + void errorOccurred(const QString &error); + void dataAvailable(const QByteArray &data); + +protected: + explicit StdinReader(QObject *parent); +}; + +} // namespace Internal +} // namespace qbs + +#endif // Include guard |