diff options
24 files changed, 1730 insertions, 8 deletions
diff --git a/src/lib/corelib/api/jobs.cpp b/src/lib/corelib/api/jobs.cpp index 6589aa359..b1b5b7e5f 100644 --- a/src/lib/corelib/api/jobs.cpp +++ b/src/lib/corelib/api/jobs.cpp @@ -41,6 +41,7 @@ #include "internaljobs.h" #include "project_p.h" #include <language/language.h> +#include <tools/launcherinterface.h> #include <tools/qbsassert.h> #include <QtCore/qtimer.h> @@ -161,6 +162,8 @@ AbstractJob::~AbstractJob() */ ErrorInfo AbstractJob::error() const { + if (m_error.hasError()) + return m_error; return internalJob()->error(); } @@ -300,6 +303,8 @@ void SetupProjectJob::finish() BuildJob::BuildJob(const Logger &logger, QObject *parent) : AbstractJob(new InternalBuildJob(logger), parent) { + connect(&LauncherInterface::instance(), &LauncherInterface::errorOccurred, + this, &BuildJob::handleLauncherError); InternalBuildJob *job = static_cast<InternalBuildJob *>(internalJob()); connect(job, &BuildGraphTouchingJob::reportCommandDescription, this, &BuildJob::reportCommandDescription); @@ -312,9 +317,21 @@ void BuildJob::build(const TopLevelProjectPtr &project, const QList<ResolvedProd { if (!lockProject(project)) return; + LauncherInterface::startLauncher(); qobject_cast<InternalBuildJob *>(internalJob())->build(project, products, options); } +void BuildJob::handleLauncherError(const ErrorInfo &error) +{ + setError(error); + cancel(); +} + +void BuildJob::finish() +{ + LauncherInterface::stopLauncher(); +} + /*! * \class CleanJob diff --git a/src/lib/corelib/api/jobs.h b/src/lib/corelib/api/jobs.h index cf9f337f2..04a0093c1 100644 --- a/src/lib/corelib/api/jobs.h +++ b/src/lib/corelib/api/jobs.h @@ -81,6 +81,7 @@ protected: Internal::InternalJob *internalJob() const { return m_internalJob; } bool lockProject(const Internal::TopLevelProjectPtr &project); + void setError(const ErrorInfo &error) { m_error = error; } signals: void taskStarted(const QString &description, int maximumProgressValue, qbs::AbstractJob *job); @@ -99,6 +100,7 @@ private: Internal::InternalJob * const m_internalJob; Internal::TopLevelProjectPtr m_project; + ErrorInfo m_error; State m_state; }; @@ -136,6 +138,9 @@ private: void build(const Internal::TopLevelProjectPtr &project, const QList<qbs::Internal::ResolvedProductPtr> &products, const BuildOptions &options); + void handleLauncherError(const ErrorInfo &error); + + void finish(); }; diff --git a/src/lib/corelib/buildgraph/processcommandexecutor.cpp b/src/lib/corelib/buildgraph/processcommandexecutor.cpp index 9eae591eb..befa66563 100644 --- a/src/lib/corelib/buildgraph/processcommandexecutor.cpp +++ b/src/lib/corelib/buildgraph/processcommandexecutor.cpp @@ -71,9 +71,9 @@ namespace Internal { ProcessCommandExecutor::ProcessCommandExecutor(const Logger &logger, QObject *parent) : AbstractCommandExecutor(logger, parent) { - connect(&m_process, static_cast<void (QProcess::*)(QProcess::ProcessError)>(&QProcess::error), + connect(&m_process, static_cast<void (QbsProcess::*)(QProcess::ProcessError)>(&QbsProcess::error), this, &ProcessCommandExecutor::onProcessError); - connect(&m_process, static_cast<void (QProcess::*)(int)>(&QProcess::finished), + connect(&m_process, static_cast<void (QbsProcess::*)(int)>(&QbsProcess::finished), this, &ProcessCommandExecutor::onProcessFinished); } @@ -169,9 +169,7 @@ void ProcessCommandExecutor::cancel() // We don't want this command to be reported as failing, since we explicitly terminated it. disconnect(this, &ProcessCommandExecutor::reportProcessResult, 0, 0); - m_process.terminate(); - if (!m_process.waitForFinished(1000)) - m_process.kill(); + m_process.cancel(); } QString ProcessCommandExecutor::filterProcessOutput(const QByteArray &_output, diff --git a/src/lib/corelib/buildgraph/processcommandexecutor.h b/src/lib/corelib/buildgraph/processcommandexecutor.h index d04f305c3..b96f6a0f1 100644 --- a/src/lib/corelib/buildgraph/processcommandexecutor.h +++ b/src/lib/corelib/buildgraph/processcommandexecutor.h @@ -42,8 +42,8 @@ #include "abstractcommandexecutor.h" -#include <QtCore/qprocess.h> -#include <QtCore/qprocess.h> +#include <tools/qbsprocess.h> + #include <QtCore/qstring.h> namespace qbs { @@ -87,7 +87,7 @@ private: QStringList m_arguments; QString m_shellInvocation; - QProcess m_process; + QbsProcess m_process; QProcessEnvironment m_buildEnvironment; QProcessEnvironment m_commandEnvironment; QString m_responseFileName; diff --git a/src/lib/corelib/corelib.pro b/src/lib/corelib/corelib.pro index f7046582e..0ca361850 100644 --- a/src/lib/corelib/corelib.pro +++ b/src/lib/corelib/corelib.pro @@ -1,6 +1,9 @@ TARGET = qbscore include(../library.pri) +isEmpty(QBS_RELATIVE_LIBEXEC_PATH):QBS_RELATIVE_LIBEXEC_PATH=../libexec/qbs +DEFINES += QBS_RELATIVE_LIBEXEC_PATH=\\\"$${QBS_RELATIVE_LIBEXEC_PATH}\\\" + QT += core-private network script qbs_enable_unit_tests:QT += testlib qbs_enable_project_file_updates: QT += gui diff --git a/src/lib/corelib/corelib.qbs b/src/lib/corelib/corelib.qbs index 85528550b..eee8600a5 100644 --- a/src/lib/corelib/corelib.qbs +++ b/src/lib/corelib/corelib.qbs @@ -18,6 +18,7 @@ QbsLibrary { property stringList projectFileUpdateDefines: qbsbuildconfig.enableProjectFileUpdates ? ["QBS_ENABLE_PROJECT_FILE_UPDATES"] : [] cpp.defines: base.concat([ + 'QBS_RELATIVE_LIBEXEC_PATH="' + qbsbuildconfig.relativeLibexecPath + '"', "QBS_VERSION=\"" + version + "\"", "QT_CREATOR", "QML_BUILD_STATIC_LIB", // needed for QmlJS "SRCDIR=\"" + path + "\"" @@ -360,6 +361,12 @@ QbsLibrary { "jsliterals.cpp", "jsliterals.h", "installoptions.cpp", + "launcherinterface.cpp", + "launcherinterface.h", + "launcherpackets.cpp", + "launcherpackets.h", + "launchersocket.cpp", + "launchersocket.h", "msvcinfo.cpp", "msvcinfo.h", "pathutils.h", @@ -379,6 +386,8 @@ QbsLibrary { "projectgeneratormanager.cpp", "qbsassert.cpp", "qbsassert.h", + "qbsprocess.cpp", + "qbsprocess.h", "qttools.cpp", "qttools.h", "scannerpluginmanager.cpp", diff --git a/src/lib/corelib/tools/launcherinterface.cpp b/src/lib/corelib/tools/launcherinterface.cpp new file mode 100644 index 000000000..7e9679379 --- /dev/null +++ b/src/lib/corelib/tools/launcherinterface.cpp @@ -0,0 +1,148 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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 "launcherinterface.h" + +#include "launcherpackets.h" +#include "launchersocket.h" +#include "qbsassert.h" +#include <logging/logger.h> +#include <logging/translator.h> + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qdebug.h> +#include <QtCore/qprocess.h> +#include <QtNetwork/qlocalserver.h> + +namespace qbs { +namespace Internal { + +static QString launcherSocketName() +{ + return QString::fromLatin1("qbs_processlauncher-%1") + .arg(QString::number(qApp->applicationPid())); +} + +LauncherInterface::LauncherInterface() + : m_server(new QLocalServer(this)), m_socket(new LauncherSocket(this)) +{ + QObject::connect(m_server, &QLocalServer::newConnection, + this, &LauncherInterface::handleNewConnection); +} + +LauncherInterface &LauncherInterface::instance() +{ + static LauncherInterface p; + return p; +} + +LauncherInterface::~LauncherInterface() +{ + m_server->disconnect(); +} + +void LauncherInterface::doStart() +{ + if (++m_startRequests > 1) + return; + const QString &socketName = launcherSocketName(); + QLocalServer::removeServer(socketName); + if (!m_server->listen(socketName)) { + emit errorOccurred(ErrorInfo(m_server->errorString())); + return; + } + m_process = new QProcess(this); + connect(m_process, + static_cast<void (QProcess::*)(QProcess::ProcessError)>(&QProcess::error), + this, &LauncherInterface::handleProcessError); + connect(m_process, static_cast<void (QProcess::*)(int)>(&QProcess::finished), + this, &LauncherInterface::handleProcessFinished); + connect(m_process, &QProcess::readyReadStandardError, + this, &LauncherInterface::handleProcessStderr); + m_process->start(qApp->applicationDirPath() + QLatin1Char('/') + + QLatin1String(QBS_RELATIVE_LIBEXEC_PATH) + + QLatin1String("/qbs_processlauncher"), + QStringList(m_server->fullServerName())); +} + +void LauncherInterface::doStop() +{ + if (--m_startRequests > 0) + return; + m_server->close(); + if (!m_process) + return; + m_process->disconnect(); + if (m_socket->isReady()) + m_socket->shutdown(); + m_process->waitForFinished(3000); + m_process->deleteLater(); + m_process = nullptr; +} + +void LauncherInterface::handleNewConnection() +{ + QLocalSocket * const socket = m_server->nextPendingConnection(); + if (!socket) + return; + m_server->close(); + m_socket->setSocket(socket); +} + +void LauncherInterface::handleProcessError() +{ + if (m_process->error() == QProcess::FailedToStart) + handleProcessFinished(); +} + +void LauncherInterface::handleProcessFinished() +{ + if (!m_socket->isReady()) { + emit errorOccurred(ErrorInfo(m_process->errorString())); + return; + } +} + +void LauncherInterface::handleProcessStderr() +{ + qDebug() << "[launcher]" << m_process->readAllStandardError(); +} + +} // namespace Internal +} // namespace qbs diff --git a/src/lib/corelib/tools/launcherinterface.h b/src/lib/corelib/tools/launcherinterface.h new file mode 100644 index 000000000..6ee0d80b5 --- /dev/null +++ b/src/lib/corelib/tools/launcherinterface.h @@ -0,0 +1,88 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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_LAUNCHERINTERFACE_H +#define QBS_LAUNCHERINTERFACE_H + +#include <QtCore/qobject.h> + +QT_BEGIN_NAMESPACE +class QProcess; +class QLocalServer; +QT_END_NAMESPACE + +namespace qbs { +class ErrorInfo; +namespace Internal { +class LauncherSocket; + +class LauncherInterface : public QObject +{ + Q_OBJECT +public: + static LauncherInterface &instance(); + ~LauncherInterface(); + + static void startLauncher() { instance().doStart(); } + static void stopLauncher() { instance().doStop(); } + static LauncherSocket *socket() { return instance().m_socket; } + +signals: + void errorOccurred(const ErrorInfo &error); + +private: + LauncherInterface(); + + void doStart(); + void doStop(); + void handleNewConnection(); + void handleProcessError(); + void handleProcessFinished(); + void handleProcessStderr(); + + QLocalServer * const m_server; + LauncherSocket * const m_socket; + QProcess * m_process = nullptr; + int m_startRequests = 0; +}; + +} // namespace Internal +} // namespace qbs + +#endif // Include guard diff --git a/src/lib/corelib/tools/launcherpackets.cpp b/src/lib/corelib/tools/launcherpackets.cpp new file mode 100644 index 000000000..9c131bd69 --- /dev/null +++ b/src/lib/corelib/tools/launcherpackets.cpp @@ -0,0 +1,175 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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 "launcherpackets.h" + +#include <QtCore/qbytearray.h> +#include <QtCore/qcoreapplication.h> +#include <QtCore/qendian.h> + +namespace qbs { +namespace Internal { + +LauncherPacket::~LauncherPacket() { } + +QByteArray LauncherPacket::serialize() const +{ + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + stream << static_cast<int>(0) << static_cast<quint8>(type) << token; + doSerialize(stream); + stream.device()->reset(); + stream << static_cast<int>(data.size() - sizeof(int)); + return data; +} + +void LauncherPacket::deserialize(const QByteArray &data) +{ + QDataStream stream(data); + doDeserialize(stream); +} + + +StartProcessPacket::StartProcessPacket(quintptr token) + : LauncherPacket(LauncherPacketType::StartProcess, token) +{ +} + +void StartProcessPacket::doSerialize(QDataStream &stream) const +{ + stream << command << arguments << workingDir << env; +} + +void StartProcessPacket::doDeserialize(QDataStream &stream) +{ + stream >> command >> arguments >> workingDir >> env; +} + + +StopProcessPacket::StopProcessPacket(quintptr token) + : LauncherPacket(LauncherPacketType::StopProcess, token) +{ +} + +void StopProcessPacket::doSerialize(QDataStream &stream) const +{ + Q_UNUSED(stream); +} + +void StopProcessPacket::doDeserialize(QDataStream &stream) +{ + Q_UNUSED(stream); +} + + +ProcessErrorPacket::ProcessErrorPacket(quintptr token) + : LauncherPacket(LauncherPacketType::ProcessError, token) +{ +} + +void ProcessErrorPacket::doSerialize(QDataStream &stream) const +{ + stream << static_cast<quint8>(error) << errorString; +} + +void ProcessErrorPacket::doDeserialize(QDataStream &stream) +{ + quint8 e; + stream >> e; + error = static_cast<QProcess::ProcessError>(e); + stream >> errorString; +} + + +ProcessFinishedPacket::ProcessFinishedPacket(quintptr token) + : LauncherPacket(LauncherPacketType::ProcessFinished, token) +{ +} + +void ProcessFinishedPacket::doSerialize(QDataStream &stream) const +{ + stream << errorString << stdOut << stdErr + << static_cast<quint8>(exitStatus) << static_cast<quint8>(error) + << exitCode; +} + +void ProcessFinishedPacket::doDeserialize(QDataStream &stream) +{ + stream >> errorString >> stdOut >> stdErr; + quint8 val; + stream >> val; + exitStatus = static_cast<QProcess::ExitStatus>(val); + stream >> val; + error = static_cast<QProcess::ProcessError>(val); + stream >> exitCode; +} + +ShutdownPacket::ShutdownPacket() : LauncherPacket(LauncherPacketType::Shutdown, 0) { } +void ShutdownPacket::doSerialize(QDataStream &stream) const { Q_UNUSED(stream); } +void ShutdownPacket::doDeserialize(QDataStream &stream) { Q_UNUSED(stream); } + +void PacketParser::setDevice(QIODevice *device) +{ + m_stream.setDevice(device); + m_sizeOfNextPacket = -1; +} + +bool PacketParser::parse() +{ + static const int commonPayloadSize = static_cast<int>(1 + sizeof LauncherPacket::token); + if (m_sizeOfNextPacket == -1) { + if (m_stream.device()->bytesAvailable() < static_cast<int>(sizeof m_sizeOfNextPacket)) + return false; + m_stream >> m_sizeOfNextPacket; + if (m_sizeOfNextPacket < commonPayloadSize) + throw InvalidPacketSizeException(m_sizeOfNextPacket); + } + if (m_stream.device()->bytesAvailable() < m_sizeOfNextPacket) + return false; + quint8 type; + m_stream >> type; + m_type = static_cast<LauncherPacketType>(type); + m_stream >> m_token; + m_packetData = m_stream.device()->read(m_sizeOfNextPacket - commonPayloadSize); + m_sizeOfNextPacket = -1; + return true; +} + +} // namespace Internal +} // namespace qbs diff --git a/src/lib/corelib/tools/launcherpackets.h b/src/lib/corelib/tools/launcherpackets.h new file mode 100644 index 000000000..0988761e7 --- /dev/null +++ b/src/lib/corelib/tools/launcherpackets.h @@ -0,0 +1,178 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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_LAUNCHERPACKETS_H +#define QBS_LAUNCHERPACKETS_H + +#include <QtCore/qdatastream.h> +#include <QtCore/qprocess.h> +#include <QtCore/qstringlist.h> + +QT_BEGIN_NAMESPACE +class QByteArray; +QT_END_NAMESPACE + +namespace qbs { +namespace Internal { + +enum class LauncherPacketType { + Shutdown, StartProcess, StopProcess, ProcessError, ProcessFinished +}; + +class PacketParser +{ +public: + class InvalidPacketSizeException + { + public: + InvalidPacketSizeException(int size) : size(size) { } + const int size; + }; + + void setDevice(QIODevice *device); + bool parse(); + LauncherPacketType type() const { return m_type; } + quintptr token() const { return m_token; } + const QByteArray &packetData() const { return m_packetData; } + +private: + QDataStream m_stream; + LauncherPacketType m_type; + quintptr m_token; + QByteArray m_packetData; + int m_sizeOfNextPacket = -1; +}; + +class LauncherPacket +{ +public: + virtual ~LauncherPacket(); + + template<class Packet> static Packet extractPacket(quintptr token, const QByteArray &data) + { + Packet p(token); + p.deserialize(data); + return p; + } + + QByteArray serialize() const; + void deserialize(const QByteArray &data); + + const LauncherPacketType type; + const quintptr token; + +protected: + LauncherPacket(LauncherPacketType type, quintptr token) : type(type), token(token) { } + +private: + virtual void doSerialize(QDataStream &stream) const = 0; + virtual void doDeserialize(QDataStream &stream) = 0; +}; + +class StartProcessPacket : public LauncherPacket +{ +public: + StartProcessPacket(quintptr token); + + QString command; + QStringList arguments; + QString workingDir; + QStringList env; + +private: + void doSerialize(QDataStream &stream) const override; + void doDeserialize(QDataStream &stream) override; +}; + +class StopProcessPacket : public LauncherPacket +{ +public: + StopProcessPacket(quintptr token); + +private: + void doSerialize(QDataStream &stream) const override; + void doDeserialize(QDataStream &stream) override; +}; + +class ShutdownPacket : public LauncherPacket +{ +public: + ShutdownPacket(); + +private: + void doSerialize(QDataStream &stream) const override; + void doDeserialize(QDataStream &stream) override; +}; + +class ProcessErrorPacket : public LauncherPacket +{ +public: + ProcessErrorPacket(quintptr token); + + QProcess::ProcessError error; + QString errorString; + +private: + void doSerialize(QDataStream &stream) const override; + void doDeserialize(QDataStream &stream) override; +}; + +class ProcessFinishedPacket : public LauncherPacket +{ +public: + ProcessFinishedPacket(quintptr token); + + QString errorString; + QByteArray stdOut; + QByteArray stdErr; + QProcess::ExitStatus exitStatus; + QProcess::ProcessError error; + int exitCode; + +private: + void doSerialize(QDataStream &stream) const override; + void doDeserialize(QDataStream &stream) override; +}; + +} // namespace Internal +} // namespace qbs + +Q_DECLARE_METATYPE(qbs::Internal::LauncherPacketType); + +#endif // Include guard diff --git a/src/lib/corelib/tools/launchersocket.cpp b/src/lib/corelib/tools/launchersocket.cpp new file mode 100644 index 000000000..fe809601e --- /dev/null +++ b/src/lib/corelib/tools/launchersocket.cpp @@ -0,0 +1,133 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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 "launchersocket.h" + +#include "qbsassert.h" + +#include <logging/translator.h> + +#include <QtCore/qtimer.h> +#include <QtNetwork/qlocalsocket.h> + +namespace qbs { +namespace Internal { + +LauncherSocket::LauncherSocket(QObject *parent) : QObject(parent) +{ + qRegisterMetaType<qbs::Internal::LauncherPacketType>(); + qRegisterMetaType<quintptr>("quintptr"); +} + +void LauncherSocket::sendData(const QByteArray &data) +{ + QMutexLocker locker(&m_requestsMutex); + m_requests << data; + if (m_requests.count() == 1) + QTimer::singleShot(0, this, &LauncherSocket::handleRequests); +} + +void LauncherSocket::shutdown() +{ + m_socket->disconnect(); + m_socket->write(ShutdownPacket().serialize()); + m_socket->waitForBytesWritten(1000); + m_socket->deleteLater(); + m_socket = nullptr; +} + +void LauncherSocket::setSocket(QLocalSocket *socket) +{ + QBS_ASSERT(!m_socket, return); + m_socket = socket; + m_packetParser.setDevice(m_socket); + connect(m_socket, + static_cast<void(QLocalSocket::*)(QLocalSocket::LocalSocketError)>(&QLocalSocket::error), + this, &LauncherSocket::handleSocketError); + connect(m_socket, &QLocalSocket::readyRead, + this, &LauncherSocket::handleSocketDataAvailable); + emit ready(); +} + +void LauncherSocket::handleSocketError() +{ + handleError(Tr::tr("Socket error: %1").arg(m_socket->errorString())); +} + +void LauncherSocket::handleSocketDataAvailable() +{ + try { + if (!m_packetParser.parse()) + return; + } catch (const PacketParser::InvalidPacketSizeException &e) { + handleError(Tr::tr("Internal protocol error: invalid packet size %1.").arg(e.size)); + return; + } + switch (m_packetParser.type()) { + case LauncherPacketType::ProcessError: + case LauncherPacketType::ProcessFinished: + emit packetArrived(m_packetParser.type(), m_packetParser.token(), + m_packetParser.packetData()); + break; + default: + handleError(Tr::tr("Internal protocol error: invalid packet type %1.") + .arg(static_cast<int>(m_packetParser.type()))); + return; + } + handleSocketDataAvailable(); +} + +void LauncherSocket::handleError(const QString &error) +{ + m_socket->disconnect(); + m_socket->deleteLater(); + m_socket = nullptr; + emit errorOccurred(error); +} + +void LauncherSocket::handleRequests() +{ + QMutexLocker locker(&m_requestsMutex); + for (auto it = m_requests.cbegin(); it != m_requests.cend(); ++it) + m_socket->write(*it); + m_requests.clear(); +} + +} // namespace Internal +} // namespace qbs diff --git a/src/lib/corelib/tools/launchersocket.h b/src/lib/corelib/tools/launchersocket.h new file mode 100644 index 000000000..130d9f337 --- /dev/null +++ b/src/lib/corelib/tools/launchersocket.h @@ -0,0 +1,91 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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_LAUNCHERSOCKET_H +#define QBS_LAUNCHERSOCKET_H + +#include "launcherpackets.h" + +#include <QtCore/qbytearraylist.h> +#include <QtCore/qmutex.h> +#include <QtCore/qobject.h> + +QT_BEGIN_NAMESPACE +class QLocalSocket; +QT_END_NAMESPACE + +namespace qbs { +namespace Internal { +class LauncherInterface; + +class LauncherSocket : public QObject +{ + Q_OBJECT + friend class LauncherInterface; +public: + bool isReady() const { return m_socket; } + void sendData(const QByteArray &data); + +signals: + void ready(); + void errorOccurred(const QString &error); + void packetArrived(qbs::Internal::LauncherPacketType type, quintptr token, + const QByteArray &payload); + +private: + LauncherSocket(QObject *parent); + + void setSocket(QLocalSocket *socket); + void shutdown(); + + void handleSocketError(); + void handleSocketDataAvailable(); + void handleError(const QString &error); + void handleRequests(); + + QLocalSocket *m_socket = nullptr; + PacketParser m_packetParser; + QByteArrayList m_requests; + QMutex m_requestsMutex; +}; + +} // namespace Internal +} // namespace qbs + +#endif // Include guard diff --git a/src/lib/corelib/tools/qbsprocess.cpp b/src/lib/corelib/tools/qbsprocess.cpp new file mode 100644 index 000000000..8495d72a0 --- /dev/null +++ b/src/lib/corelib/tools/qbsprocess.cpp @@ -0,0 +1,173 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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 "qbsprocess.h" + +#include "launcherinterface.h" +#include "launchersocket.h" +#include "qbsassert.h" + +#include <logging/translator.h> + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qtimer.h> + +namespace qbs { +namespace Internal { + +QbsProcess::QbsProcess(QObject *parent) : QObject(parent) +{ + connect(LauncherInterface::socket(), &LauncherSocket::ready, + this, &QbsProcess::handleSocketReady); + connect(LauncherInterface::socket(), &LauncherSocket::errorOccurred, + this, &QbsProcess::handleSocketError); + connect(LauncherInterface::socket(), &LauncherSocket::packetArrived, + this, &QbsProcess::handlePacket); +} + +void QbsProcess::start(const QString &command, const QStringList &arguments) +{ + if (m_socketError) { + m_error = QProcess::FailedToStart; + emit error(m_error); + return; + } + m_command = command; + m_arguments = arguments; + m_state = QProcess::Starting; + if (LauncherInterface::socket()->isReady()) + doStart(); +} + +void QbsProcess::doStart() +{ + m_state = QProcess::Running; + StartProcessPacket p(token()); + p.command = m_command; + p.arguments = m_arguments; + p.env = m_environment.toStringList(); + p.workingDir = m_workingDirectory; + sendPacket(p); +} + +void QbsProcess::cancel() +{ + sendPacket(StopProcessPacket(token())); +} + +QByteArray QbsProcess::readAllStandardOutput() +{ + return readAndClear(m_stdout); +} + +QByteArray QbsProcess::readAllStandardError() +{ + return readAndClear(m_stderr); +} + +void QbsProcess::sendPacket(const LauncherPacket &packet) +{ + LauncherInterface::socket()->sendData(packet.serialize()); +} + +QByteArray QbsProcess::readAndClear(QByteArray &data) +{ + const QByteArray tmp = data; + data.clear(); + return tmp; +} + +void QbsProcess::handlePacket(LauncherPacketType type, quintptr token, const QByteArray &payload) +{ + if (token != this->token()) + return; + switch (type) { + case LauncherPacketType::ProcessError: + handleErrorPacket(payload); + break; + case LauncherPacketType::ProcessFinished: + handleFinishedPacket(payload); + break; + default: + QBS_ASSERT(false, break); + } +} + +void QbsProcess::handleSocketReady() +{ + m_socketError = false; + if (m_state == QProcess::Starting) + doStart(); +} + +void QbsProcess::handleSocketError(const QString &message) +{ + m_socketError = true; + m_errorString = Tr::tr("Internal socket error: %1").arg(message); + if (m_state != QProcess::NotRunning) { + m_state = QProcess::NotRunning; + m_error = QProcess::FailedToStart; + emit error(m_error); + } +} + +void QbsProcess::handleErrorPacket(const QByteArray &packetData) +{ + QBS_ASSERT(m_state != QProcess::NotRunning, return); + const auto packet = LauncherPacket::extractPacket<ProcessErrorPacket>(token(), packetData); + m_error = packet.error; + m_errorString = packet.errorString; + m_state = QProcess::NotRunning; + emit error(m_error); +} + +void QbsProcess::handleFinishedPacket(const QByteArray &packetData) +{ + QBS_ASSERT(m_state == QProcess::Running, return); + m_state = QProcess::NotRunning; + const auto packet = LauncherPacket::extractPacket<ProcessFinishedPacket>(token(), packetData); + m_exitCode = packet.exitCode; + m_stdout = packet.stdOut; + m_stderr = packet.stdErr; + m_errorString = packet.errorString; + emit finished(m_exitCode); +} + +} // namespace Internal +} // namespace qbs diff --git a/src/lib/corelib/tools/qbsprocess.h b/src/lib/corelib/tools/qbsprocess.h new file mode 100644 index 000000000..9d2343010 --- /dev/null +++ b/src/lib/corelib/tools/qbsprocess.h @@ -0,0 +1,106 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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_QBSPROCESS_H +#define QBS_QBSPROCESS_H + +#include "launcherpackets.h" + +#include <QtCore/qbytearray.h> +#include <QtCore/qobject.h> +#include <QtCore/qprocess.h> +#include <QtCore/qstringlist.h> + +namespace qbs { +namespace Internal { + +class QbsProcess : public QObject +{ + Q_OBJECT +public: + explicit QbsProcess(QObject *parent = 0); + + QProcess::ProcessState state() const { return m_state; } + void setProcessEnvironment(const QProcessEnvironment &env) { m_environment = env; } + void setWorkingDirectory(const QString &workingDir) { m_workingDirectory = workingDir; } + QString workingDirectory() const { return m_workingDirectory; } + void start(const QString &command, const QStringList &arguments); + void cancel(); + QByteArray readAllStandardOutput(); + QByteArray readAllStandardError(); + int exitCode() const { return m_exitCode; } + QProcess::ProcessError error() const { return m_error; } + QString errorString() const { return m_errorString; } + +signals: + void error(QProcess::ProcessError error); + void finished(int exitCode); + +private: + void doStart(); + void sendPacket(const LauncherPacket &packet); + QByteArray readAndClear(QByteArray &data); + + void handleSocketError(const QString &message); + void handlePacket(qbs::Internal::LauncherPacketType type, quintptr token, + const QByteArray &payload); + void handleErrorPacket(const QByteArray &packetData); + void handleFinishedPacket(const QByteArray &packetData); + void handleSocketReady(); + + quintptr token() const { return reinterpret_cast<quintptr>(this); } + + QString m_command; + QStringList m_arguments; + QProcessEnvironment m_environment; + QString m_workingDirectory; + QByteArray m_stdout; + QByteArray m_stderr; + QString m_errorString; + QProcess::ProcessError m_error = QProcess::UnknownError; + QProcess::ProcessState m_state = QProcess::NotRunning; + int m_exitCode; + int m_connectionAttempts = 0; + bool m_socketError = false; +}; + +} // namespace Internal +} // namespace qbs + +#endif // QBSPROCESS_H diff --git a/src/lib/corelib/tools/tools.pri b/src/lib/corelib/tools/tools.pri index 337deb4a3..a9c8c8bc0 100644 --- a/src/lib/corelib/tools/tools.pri +++ b/src/lib/corelib/tools/tools.pri @@ -15,6 +15,9 @@ HEADERS += \ $$PWD/generateoptions.h \ $$PWD/id.h \ $$PWD/jsliterals.h \ + $$PWD/launcherinterface.h \ + $$PWD/launcherpackets.h \ + $$PWD/launchersocket.h \ $$PWD/msvcinfo.h \ $$PWD/persistence.h \ $$PWD/scannerpluginmanager.h \ @@ -31,6 +34,7 @@ HEADERS += \ $$PWD/processutils.h \ $$PWD/progressobserver.h \ $$PWD/projectgeneratormanager.h \ + $$PWD/qbsprocess.h \ $$PWD/shellutils.h \ $$PWD/toolchains.h \ $$PWD/hostosinfo.h \ @@ -61,6 +65,9 @@ SOURCES += \ $$PWD/generateoptions.cpp \ $$PWD/id.cpp \ $$PWD/jsliterals.cpp \ + $$PWD/launcherinterface.cpp \ + $$PWD/launcherpackets.cpp \ + $$PWD/launchersocket.cpp \ $$PWD/msvcinfo.cpp \ $$PWD/persistence.cpp \ $$PWD/scannerpluginmanager.cpp \ @@ -74,6 +81,7 @@ SOURCES += \ $$PWD/profiling.cpp \ $$PWD/progressobserver.cpp \ $$PWD/projectgeneratormanager.cpp \ + $$PWD/qbsprocess.cpp \ $$PWD/shellutils.cpp \ $$PWD/buildoptions.cpp \ $$PWD/installoptions.cpp \ diff --git a/src/libexec/libexec.pro b/src/libexec/libexec.pro index 967108504..75b1d0844 100644 --- a/src/libexec/libexec.pro +++ b/src/libexec/libexec.pro @@ -1 +1,3 @@ TEMPLATE = subdirs + +SUBDIRS += qbs_processlauncher diff --git a/src/libexec/libexec.qbs b/src/libexec/libexec.qbs index 489864a26..a43e26157 100644 --- a/src/libexec/libexec.qbs +++ b/src/libexec/libexec.qbs @@ -2,5 +2,6 @@ import qbs Project { references: [ + "qbs_processlauncher/qbs_processlauncher.qbs", ] } diff --git a/src/libexec/qbs_processlauncher/launcherlogging.cpp b/src/libexec/qbs_processlauncher/launcherlogging.cpp new file mode 100644 index 000000000..10c42127d --- /dev/null +++ b/src/libexec/qbs_processlauncher/launcherlogging.cpp @@ -0,0 +1,46 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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 "launcherlogging.h" + +namespace qbs { +namespace Internal { +Q_LOGGING_CATEGORY(launcherLog, "qbs.launcher") +} +} diff --git a/src/libexec/qbs_processlauncher/launcherlogging.h b/src/libexec/qbs_processlauncher/launcherlogging.h new file mode 100644 index 000000000..356433a9f --- /dev/null +++ b/src/libexec/qbs_processlauncher/launcherlogging.h @@ -0,0 +1,54 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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_LAUCHERLOGGING_H +#define QBS_LAUCHERLOGGING_H + +#include <QtCore/qloggingcategory.h> +#include <QtCore/qstring.h> + +namespace qbs { +namespace Internal { +Q_DECLARE_LOGGING_CATEGORY(launcherLog) +template<typename T> void logDebug(const T &msg) { qCDebug(launcherLog) << msg; } +template<typename T> void logWarn(const T &msg) { qCWarning(launcherLog) << msg; } +template<typename T> void logError(const T &msg) { qCCritical(launcherLog) << msg; } +} +} + +#endif // Include guard diff --git a/src/libexec/qbs_processlauncher/launchersockethandler.cpp b/src/libexec/qbs_processlauncher/launchersockethandler.cpp new file mode 100644 index 000000000..1f3f96034 --- /dev/null +++ b/src/libexec/qbs_processlauncher/launchersockethandler.cpp @@ -0,0 +1,289 @@ +/**************************************************************************** +** +** Copyright (C) 2017 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 "launchersockethandler.h" + +#include "launcherlogging.h" + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qprocess.h> +#include <QtCore/qtimer.h> +#include <QtNetwork/qlocalsocket.h> + +namespace qbs { +namespace Internal { + +// TODO: add stop state and timer here. +class Process : public QProcess +{ + Q_OBJECT +public: + Process(quintptr token, QObject *parent = nullptr) : + QProcess(parent), m_token(token), m_stopTimer(new QTimer(this)) + { + m_stopTimer->setSingleShot(true); + connect(m_stopTimer, &QTimer::timeout, this, &Process::cancel); + } + + void cancel() + { + switch (m_stopState) { + case StopState::Inactive: + m_stopState = StopState::Terminating; + m_stopTimer->start(3000); + terminate(); + break; + case StopState::Terminating: + m_stopState = StopState::Killing; + m_stopTimer->start(3000); + kill(); + break; + case StopState::Killing: + m_stopState = StopState::Inactive; + emit failedToStop(); + break; + } + } + + void stopStopProcedure() + { + m_stopState = StopState::Inactive; + m_stopTimer->stop(); + } + + quintptr token() const { return m_token; } + +signals: + void failedToStop(); + +private: + const quintptr m_token; + QTimer * const m_stopTimer; + enum class StopState { Inactive, Terminating, Killing } m_stopState = StopState::Inactive; +}; + +LauncherSocketHandler::LauncherSocketHandler(const QString &serverPath, QObject *parent) + : QObject(parent), + m_serverPath(serverPath), + m_socket(new QLocalSocket(this)) +{ + m_packetParser.setDevice(m_socket); +} + +LauncherSocketHandler::~LauncherSocketHandler() +{ + m_socket->disconnect(); + if (m_socket->state() != QLocalSocket::UnconnectedState) { + logWarn("socket handler destroyed while connection was active"); + m_socket->close(); + } + for (auto it = m_processes.cbegin(); it != m_processes.cend(); ++it) + it.value()->disconnect(); +} + +void LauncherSocketHandler::start() +{ + connect(m_socket, &QLocalSocket::disconnected, + this, &LauncherSocketHandler::handleSocketClosed); + connect(m_socket, &QLocalSocket::readyRead, this, &LauncherSocketHandler::handleSocketData); + connect(m_socket, + static_cast<void(QLocalSocket::*)(QLocalSocket::LocalSocketError)>(&QLocalSocket::error), + this, &LauncherSocketHandler::handleSocketError); + m_socket->connectToServer(m_serverPath); +} + +void LauncherSocketHandler::handleSocketData() +{ + try { + if (!m_packetParser.parse()) + return; + } catch (const PacketParser::InvalidPacketSizeException &e) { + logWarn(QString::fromLatin1("Internal protocol error: invalid packet size %1.") + .arg(e.size)); + return; + } + switch (m_packetParser.type()) { + case LauncherPacketType::StartProcess: + handleStartPacket(); + break; + case LauncherPacketType::StopProcess: + handleStopPacket(); + break; + case LauncherPacketType::Shutdown: + handleShutdownPacket(); + return; + default: + logWarn(QString::fromLatin1("Internal protocol error: invalid packet type %1.") + .arg(static_cast<int>(m_packetParser.type()))); + return; + } + handleSocketData(); +} + +void LauncherSocketHandler::handleSocketError() +{ + if (m_socket->error() != QLocalSocket::PeerClosedError) { + logError(QString::fromLatin1("socket error: %1").arg(m_socket->errorString())); + m_socket->disconnect(); + qApp->quit(); + } +} + +void LauncherSocketHandler::handleSocketClosed() +{ + for (auto it = m_processes.cbegin(); it != m_processes.cend(); ++it) { + if (it.value()->state() != QProcess::NotRunning) { + logWarn("client closed connection while process still running"); + break; + } + } + m_socket->disconnect(); +} + +void LauncherSocketHandler::handleProcessError() +{ + if (senderProcess()->error() != QProcess::FailedToStart) + return; + senderProcess()->stopStopProcedure(); + ProcessErrorPacket packet(senderProcess()->token()); + packet.error = senderProcess()->error(); + packet.errorString = senderProcess()->errorString(); + sendPacket(packet); +} + +void LauncherSocketHandler::handleProcessFinished() +{ + senderProcess()->stopStopProcedure(); + ProcessFinishedPacket packet(senderProcess()->token()); + packet.error = senderProcess()->error(); + packet.errorString = senderProcess()->errorString(); + packet.exitCode = senderProcess()->exitCode(); + packet.exitStatus = senderProcess()->exitStatus(); + packet.stdErr = senderProcess()->readAllStandardError(); + packet.stdOut = senderProcess()->readAllStandardOutput(); + sendPacket(packet); +} + +void LauncherSocketHandler::handleStopFailure() +{ + // Process did not react to a kill signal. Rare, but not unheard of. + // Forget about the associated Process object and report process exit to the client. + senderProcess()->disconnect(); + m_processes.remove(senderProcess()->token()); + ProcessFinishedPacket packet(senderProcess()->token()); + packet.error = QProcess::Crashed; + packet.exitCode = -1; + packet.exitStatus = QProcess::CrashExit; + packet.stdErr = senderProcess()->readAllStandardError(); + packet.stdOut = senderProcess()->readAllStandardOutput(); + sendPacket(packet); +} + +void LauncherSocketHandler::handleStartPacket() +{ + Process *& process = m_processes[m_packetParser.token()]; + if (!process) + process = setupProcess(m_packetParser.token()); + if (process->state() != QProcess::NotRunning) { + logWarn("got start request while process was running"); + return; + } + const auto packet = LauncherPacket::extractPacket<StartProcessPacket>( + m_packetParser.token(), + m_packetParser.packetData()); + process->setEnvironment(packet.env); + process->setWorkingDirectory(packet.workingDir); + process->start(packet.command, packet.arguments); +} + +void LauncherSocketHandler::handleStopPacket() +{ + Process * const process = m_processes.value(m_packetParser.token()); + if (!process) { + logWarn("got stop request for unknown process"); + return; + } + if (process->state() == QProcess::NotRunning) { + // This can happen if the process finishes on its own at about the same time the client + // sends the request. + logDebug("got stop request when process was not running"); + return; + } + process->cancel(); +} + +void LauncherSocketHandler::handleShutdownPacket() +{ + logDebug("got shutdown request, closing down"); + for (auto it = m_processes.cbegin(); it != m_processes.cend(); ++it) { + it.value()->disconnect(); + if (it.value()->state() != QProcess::NotRunning) { + logWarn("got shutdown request while process was running"); + it.value()->terminate(); + } + } + m_socket->disconnect(); + qApp->quit(); +} + +void LauncherSocketHandler::sendPacket(const LauncherPacket &packet) +{ + m_socket->write(packet.serialize()); +} + +Process *LauncherSocketHandler::setupProcess(quintptr token) +{ + Process * const p = new Process(token, this); + connect(p, static_cast<void (QProcess::*)(QProcess::ProcessError)>(&QProcess::error), + this, &LauncherSocketHandler::handleProcessError); + connect(p, static_cast<void (QProcess::*)(int)>(&QProcess::finished), + this, &LauncherSocketHandler::handleProcessFinished); + connect(p, &Process::failedToStop, this, &LauncherSocketHandler::handleStopFailure); + return p; +} + +Process *LauncherSocketHandler::senderProcess() const +{ + return static_cast<Process *>(sender()); +} + +} // namespace Internal +} // namespace qbs + +#include <launchersockethandler.moc> diff --git a/src/libexec/qbs_processlauncher/launchersockethandler.h b/src/libexec/qbs_processlauncher/launchersockethandler.h new file mode 100644 index 000000000..e96c02e13 --- /dev/null +++ b/src/libexec/qbs_processlauncher/launchersockethandler.h @@ -0,0 +1,92 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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_LAUNCHERSOCKETHANDLER_H +#define QBS_LAUNCHERSOCKETHANDLER_H + +#include <launcherpackets.h> + +#include <QtCore/qbytearray.h> +#include <QtCore/qhash.h> +#include <QtCore/qobject.h> + +QT_BEGIN_NAMESPACE +class QLocalSocket; +QT_END_NAMESPACE + +namespace qbs { +namespace Internal { +class Process; + +class LauncherSocketHandler : public QObject +{ + Q_OBJECT +public: + explicit LauncherSocketHandler(const QString &socketPath, QObject *parent = nullptr); + ~LauncherSocketHandler(); + + void start(); + +private: + void handleSocketData(); + void handleSocketError(); + void handleSocketClosed(); + void handleProcessError(); + void handleProcessFinished(); + void handleStopFailure(); + + void handleStartPacket(); + void handleStopPacket(); + void handleShutdownPacket(); + + void sendPacket(const LauncherPacket &packet); + + Process *setupProcess(quintptr token); + Process *senderProcess() const; + + const QString m_serverPath; + QLocalSocket * const m_socket; + PacketParser m_packetParser; + QHash<quintptr, Process *> m_processes; +}; + +} // namespace Internal +} // namespace qbs + +#endif // Include guard diff --git a/src/libexec/qbs_processlauncher/processlauncher-main.cpp b/src/libexec/qbs_processlauncher/processlauncher-main.cpp new file mode 100644 index 000000000..9d51c9fa1 --- /dev/null +++ b/src/libexec/qbs_processlauncher/processlauncher-main.cpp @@ -0,0 +1,46 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** 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. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "launcherlogging.h" +#include "launchersockethandler.h" + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qtimer.h> + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + if (app.arguments().count() != 2) { + qbs::Internal::logError("Need exactly one argument (path to socket)"); + return 1; + } + + qbs::Internal::LauncherSocketHandler launcher(app.arguments().last()); + QTimer::singleShot(0, &launcher, &qbs::Internal::LauncherSocketHandler::start); + return app.exec(); +} diff --git a/src/libexec/qbs_processlauncher/qbs_processlauncher.pro b/src/libexec/qbs_processlauncher/qbs_processlauncher.pro new file mode 100644 index 000000000..84142b5c5 --- /dev/null +++ b/src/libexec/qbs_processlauncher/qbs_processlauncher.pro @@ -0,0 +1,21 @@ +include(../libexec.pri) + +TARGET = qbs_processlauncher +CONFIG += console c++11 +CONFIG -= app_bundle +QT = core network + +TOOLS_DIR = $$PWD/../../lib/corelib/tools + +INCLUDEPATH += $$TOOLS_DIR + +HEADERS += \ + launcherlogging.h \ + launchersockethandler.h \ + $$TOOLS_DIR/launcherpackets.h + +SOURCES += \ + launcherlogging.cpp \ + launchersockethandler.cpp \ + processlauncher-main.cpp \ + $$TOOLS_DIR/launcherpackets.cpp diff --git a/src/libexec/qbs_processlauncher/qbs_processlauncher.qbs b/src/libexec/qbs_processlauncher/qbs_processlauncher.qbs new file mode 100644 index 000000000..c641296ad --- /dev/null +++ b/src/libexec/qbs_processlauncher/qbs_processlauncher.qbs @@ -0,0 +1,39 @@ +import qbs +import qbs.FileInfo + +QbsProduct { + type: "application" + name: "qbs_processlauncher" + consoleApplication: true + destinationDirectory: FileInfo.joinPaths(project.buildDirectory, + qbsbuildconfig.libexecInstallDir) + + Depends { name: "Qt.network" } + + cpp.cxxLanguageVersion: "c++11" + cpp.includePaths: base.concat(pathToProtocolSources) + + files: [ + "launcherlogging.cpp", + "launcherlogging.h", + "launchersockethandler.cpp", + "launchersockethandler.h", + "processlauncher-main.cpp", + ] + + property string pathToProtocolSources: sourceDirectory + "/../../lib/corelib/tools" + Group { + name: "protocol sources" + prefix: pathToProtocolSources + '/' + files: [ + "launcherpackets.cpp", + "launcherpackets.h", + ] + } + + Group { + fileTagsFilter: product.type + qbs.install: true + qbs.installDir: qbsbuildconfig.libexecInstallDir + } +} |