From 58d00f37d46569832168084f17671952b88a395c Mon Sep 17 00:00:00 2001 From: Jarek Kobus Date: Wed, 7 Jul 2021 11:36:03 +0200 Subject: [PATCH] Initial import of processlauncher copy from qbs project Change-Id: I9d646dd1a820e4e69c808998d9f12ff2f5dabe6d Reviewed-by: Qt CI Bot Reviewed-by: hjk --- src/libs/utils/CMakeLists.txt | 3 + src/libs/utils/launcherinterface.cpp | 169 +++++++++++ src/libs/utils/launcherinterface.h | 72 +++++ src/libs/utils/launcherpackets.cpp | 160 ++++++++++ src/libs/utils/launcherpackets.h | 161 ++++++++++ src/libs/utils/launchersocket.cpp | 141 +++++++++ src/libs/utils/launchersocket.h | 77 +++++ src/libs/utils/utils-lib.pri | 6 + src/libs/utils/utils.qbs | 6 + src/tools/CMakeLists.txt | 1 + src/tools/processlauncher/CMakeLists.txt | 14 + src/tools/processlauncher/launcherlogging.cpp | 32 ++ src/tools/processlauncher/launcherlogging.h | 38 +++ .../processlauncher/launchersockethandler.cpp | 281 ++++++++++++++++++ .../processlauncher/launchersockethandler.h | 75 +++++ .../processlauncher/processlauncher-main.cpp | 57 ++++ src/tools/processlauncher/processlauncher.pro | 21 ++ src/tools/processlauncher/processlauncher.qbs | 28 ++ src/tools/tools.pro | 1 + src/tools/tools.qbs | 1 + 20 files changed, 1344 insertions(+) create mode 100644 src/libs/utils/launcherinterface.cpp create mode 100644 src/libs/utils/launcherinterface.h create mode 100644 src/libs/utils/launcherpackets.cpp create mode 100644 src/libs/utils/launcherpackets.h create mode 100644 src/libs/utils/launchersocket.cpp create mode 100644 src/libs/utils/launchersocket.h create mode 100644 src/tools/processlauncher/CMakeLists.txt create mode 100644 src/tools/processlauncher/launcherlogging.cpp create mode 100644 src/tools/processlauncher/launcherlogging.h create mode 100644 src/tools/processlauncher/launchersockethandler.cpp create mode 100644 src/tools/processlauncher/launchersockethandler.h create mode 100644 src/tools/processlauncher/processlauncher-main.cpp create mode 100644 src/tools/processlauncher/processlauncher.pro create mode 100644 src/tools/processlauncher/processlauncher.qbs diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index e78809ff850..41eed8d531f 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -76,6 +76,9 @@ add_qtc_library(Utils itemviews.cpp itemviews.h json.cpp json.h jsontreeitem.cpp jsontreeitem.h + launcherinterface.cpp launcherinterface.h + launcherpackets.cpp launcherpackets.h + launchersocket.cpp launchersocket.h layoutbuilder.cpp layoutbuilder.h linecolumn.cpp linecolumn.h link.cpp link.h diff --git a/src/libs/utils/launcherinterface.cpp b/src/libs/utils/launcherinterface.cpp new file mode 100644 index 00000000000..eb04120c2cf --- /dev/null +++ b/src/libs/utils/launcherinterface.cpp @@ -0,0 +1,169 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "launcherinterface.h" + +#include "launcherpackets.h" +#include "launchersocket.h" + +#include +#include +#include +#include +#include + +#ifdef Q_OS_UNIX +#include +#endif + +namespace Utils { + +namespace Internal { + +class LauncherProcess : public QProcess +{ +public: + LauncherProcess(QObject *parent) : QProcess(parent) + { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) && defined(Q_OS_UNIX) + setChildProcessModifier([this] { setupChildProcess_impl(); }); +#endif + } + +private: +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + void setupChildProcess() override + { + setupChildProcess_impl(); + } +#endif + + void setupChildProcess_impl() + { +#ifdef Q_OS_UNIX + const auto pid = static_cast(processId()); + setpgid(pid, pid); +#endif + } +}; + +} // namespace Internal + +using namespace Utils::Internal; + +static QString launcherSocketName() +{ + return QStringLiteral("qtcreator_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(m_server->errorString()); + return; + } + m_process = new LauncherProcess(this); + connect(m_process, &QProcess::errorOccurred, this, &LauncherInterface::handleProcessError); + connect(m_process, + static_cast(&QProcess::finished), + this, &LauncherInterface::handleProcessFinished); + connect(m_process, &QProcess::readyReadStandardError, + this, &LauncherInterface::handleProcessStderr); + m_process->start(qApp->applicationDirPath() + QLatin1Char('/') + + QLatin1String(RELATIVE_LIBEXEC_PATH) + + QLatin1String("/qtcreator_processlauncher"), + QStringList(m_server->fullServerName())); +} + +void LauncherInterface::doStop() +{ + if (--m_startRequests > 0) + return; + m_server->close(); + if (!m_process) + return; + m_process->disconnect(); + 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) { + const QString launcherPathForUser + = QDir::toNativeSeparators(QDir::cleanPath(m_process->program())); + emit errorOccurred(QCoreApplication::translate("Utils::LauncherSocket", + "Failed to start process launcher at '%1': %2") + .arg(launcherPathForUser, m_process->errorString())); + } +} + +void LauncherInterface::handleProcessFinished() +{ + emit errorOccurred(QCoreApplication::translate("Utils::LauncherSocket", + "Process launcher closed unexpectedly: %1") + .arg(m_process->errorString())); +} + +void LauncherInterface::handleProcessStderr() +{ + qDebug() << "[launcher]" << m_process->readAllStandardError(); +} + +} // namespace Utils diff --git a/src/libs/utils/launcherinterface.h b/src/libs/utils/launcherinterface.h new file mode 100644 index 00000000000..5844aa8ce0e --- /dev/null +++ b/src/libs/utils/launcherinterface.h @@ -0,0 +1,72 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "utils_global.h" + +#include + +QT_BEGIN_NAMESPACE +class QLocalServer; +QT_END_NAMESPACE + +namespace Utils { +namespace Internal { +class LauncherProcess; +class LauncherSocket; +} + +class QTCREATOR_UTILS_EXPORT LauncherInterface : public QObject +{ + Q_OBJECT +public: + static LauncherInterface &instance(); + ~LauncherInterface() override; + + static void startLauncher() { instance().doStart(); } + static void stopLauncher() { instance().doStop(); } + static Internal::LauncherSocket *socket() { return instance().m_socket; } + +signals: + void errorOccurred(const QString &error); + +private: + LauncherInterface(); + + void doStart(); + void doStop(); + void handleNewConnection(); + void handleProcessError(); + void handleProcessFinished(); + void handleProcessStderr(); + + QLocalServer * const m_server; + Internal::LauncherSocket *const m_socket; + Internal::LauncherProcess *m_process = nullptr; + int m_startRequests = 0; +}; + +} // namespace Utils diff --git a/src/libs/utils/launcherpackets.cpp b/src/libs/utils/launcherpackets.cpp new file mode 100644 index 00000000000..15f9e1d607a --- /dev/null +++ b/src/libs/utils/launcherpackets.cpp @@ -0,0 +1,160 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "launcherpackets.h" + +#include +#include + +namespace Utils { +namespace Internal { + +LauncherPacket::~LauncherPacket() = default; + +QByteArray LauncherPacket::serialize() const +{ + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + stream << static_cast(0) << static_cast(type) << token; + doSerialize(stream); + stream.device()->reset(); + stream << static_cast(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(error) << errorString; +} + +void ProcessErrorPacket::doDeserialize(QDataStream &stream) +{ + quint8 e; + stream >> e; + error = static_cast(e); + stream >> errorString; +} + + +ProcessFinishedPacket::ProcessFinishedPacket(quintptr token) + : LauncherPacket(LauncherPacketType::ProcessFinished, token) +{ +} + +void ProcessFinishedPacket::doSerialize(QDataStream &stream) const +{ + stream << errorString << stdOut << stdErr + << static_cast(exitStatus) << static_cast(error) + << exitCode; +} + +void ProcessFinishedPacket::doDeserialize(QDataStream &stream) +{ + stream >> errorString >> stdOut >> stdErr; + quint8 val; + stream >> val; + exitStatus = static_cast(val); + stream >> val; + error = static_cast(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(1 + sizeof(quintptr)); + if (m_sizeOfNextPacket == -1) { + if (m_stream.device()->bytesAvailable() < static_cast(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(type); + m_stream >> m_token; + m_packetData = m_stream.device()->read(m_sizeOfNextPacket - commonPayloadSize); + m_sizeOfNextPacket = -1; + return true; +} + +} // namespace Internal +} // namespace Utils diff --git a/src/libs/utils/launcherpackets.h b/src/libs/utils/launcherpackets.h new file mode 100644 index 00000000000..7692cdde552 --- /dev/null +++ b/src/libs/utils/launcherpackets.h @@ -0,0 +1,161 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QByteArray; +QT_END_NAMESPACE + +namespace Utils { +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 = LauncherPacketType::Shutdown; + quintptr m_token = 0; + QByteArray m_packetData; + int m_sizeOfNextPacket = -1; +}; + +class LauncherPacket +{ +public: + virtual ~LauncherPacket(); + + template 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 = 0; + +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 = QProcess::UnknownError; + 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::ExitStatus::NormalExit; + QProcess::ProcessError error = QProcess::ProcessError::UnknownError; + int exitCode = 0; + +private: + void doSerialize(QDataStream &stream) const override; + void doDeserialize(QDataStream &stream) override; +}; + +} // namespace Internal +} // namespace Utils + +Q_DECLARE_METATYPE(Utils::Internal::LauncherPacketType); diff --git a/src/libs/utils/launchersocket.cpp b/src/libs/utils/launchersocket.cpp new file mode 100644 index 00000000000..71e791fa063 --- /dev/null +++ b/src/libs/utils/launchersocket.cpp @@ -0,0 +1,141 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "launchersocket.h" + +#include "qtcassert.h" + +#include +#include +#include + +namespace Utils { +namespace Internal { + +LauncherSocket::LauncherSocket(QObject *parent) : QObject(parent) +{ + qRegisterMetaType(); + qRegisterMetaType("quintptr"); +} + +void LauncherSocket::sendData(const QByteArray &data) +{ + if (!isReady()) + return; + std::lock_guard locker(m_requestsMutex); + m_requests.push_back(data); + if (m_requests.size() == 1) + QTimer::singleShot(0, this, &LauncherSocket::handleRequests); +} + +void LauncherSocket::shutdown() +{ + const auto socket = m_socket.exchange(nullptr); + if (!socket) + return; + socket->disconnect(); + socket->write(ShutdownPacket().serialize()); + socket->waitForBytesWritten(1000); + socket->deleteLater(); +} + +void LauncherSocket::setSocket(QLocalSocket *socket) +{ + QTC_ASSERT(!m_socket, return); + m_socket.store(socket); + m_packetParser.setDevice(m_socket); + connect(m_socket, +#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) + static_cast(&QLocalSocket::error), +#else + &QLocalSocket::errorOccurred, +#endif + this, &LauncherSocket::handleSocketError); + connect(m_socket, &QLocalSocket::readyRead, + this, &LauncherSocket::handleSocketDataAvailable); + connect(m_socket, &QLocalSocket::disconnected, + this, &LauncherSocket::handleSocketDisconnected); + emit ready(); +} + +void LauncherSocket::handleSocketError() +{ + auto socket = m_socket.load(); + if (socket->error() != QLocalSocket::PeerClosedError) + handleError(QCoreApplication::translate("Utils::LauncherSocket", + "Socket error: %1").arg(socket->errorString())); +} + +void LauncherSocket::handleSocketDataAvailable() +{ + try { + if (!m_packetParser.parse()) + return; + } catch (const PacketParser::InvalidPacketSizeException &e) { + handleError(QCoreApplication::translate("Utils::LauncherSocket", + "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(QCoreApplication::translate("Utils::LauncherSocket", + "Internal protocol error: invalid packet type %1.") + .arg(static_cast(m_packetParser.type()))); + return; + } + handleSocketDataAvailable(); +} + +void LauncherSocket::handleSocketDisconnected() +{ + handleError(QCoreApplication::translate("Utils::LauncherSocket", + "Launcher socket closed unexpectedly")); +} + +void LauncherSocket::handleError(const QString &error) +{ + const auto socket = m_socket.exchange(nullptr); + socket->disconnect(); + socket->deleteLater(); + emit errorOccurred(error); +} + +void LauncherSocket::handleRequests() +{ + const auto socket = m_socket.load(); + QTC_ASSERT(socket, return); + std::lock_guard locker(m_requestsMutex); + for (const QByteArray &request : qAsConst(m_requests)) + socket->write(request); + m_requests.clear(); +} + +} // namespace Internal +} // namespace Utils diff --git a/src/libs/utils/launchersocket.h b/src/libs/utils/launchersocket.h new file mode 100644 index 00000000000..0090121894e --- /dev/null +++ b/src/libs/utils/launchersocket.h @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "launcherpackets.h" + +#include + +#include +#include + +QT_BEGIN_NAMESPACE +class QLocalSocket; +QT_END_NAMESPACE + +namespace Utils { +class LauncherInterface; + +namespace Internal { + +class LauncherSocket : public QObject +{ + Q_OBJECT + friend class Utils::LauncherInterface; +public: + bool isReady() const { return m_socket.load(); } + void sendData(const QByteArray &data); + +signals: + void ready(); + void errorOccurred(const QString &error); + void packetArrived(Utils::Internal::LauncherPacketType type, quintptr token, + const QByteArray &payload); + +private: + LauncherSocket(QObject *parent); + + void setSocket(QLocalSocket *socket); + void shutdown(); + + void handleSocketError(); + void handleSocketDataAvailable(); + void handleSocketDisconnected(); + void handleError(const QString &error); + void handleRequests(); + + std::atomic m_socket{nullptr}; + PacketParser m_packetParser; + std::vector m_requests; + std::mutex m_requestsMutex; +}; + +} // namespace Internal +} // namespace Utils diff --git a/src/libs/utils/utils-lib.pri b/src/libs/utils/utils-lib.pri index 872675f306d..a8b8ccd5852 100644 --- a/src/libs/utils/utils-lib.pri +++ b/src/libs/utils/utils-lib.pri @@ -142,6 +142,9 @@ SOURCES += \ $$PWD/layoutbuilder.cpp \ $$PWD/variablechooser.cpp \ $$PWD/futuresynchronizer.cpp \ + $$PWD/launcherinterface.cpp \ + $$PWD/launcherpackets.cpp \ + $$PWD/launchersocket.cpp \ $$PWD/qtcsettings.cpp \ $$PWD/link.cpp \ $$PWD/linecolumn.cpp \ @@ -302,6 +305,9 @@ HEADERS += \ $$PWD/variablechooser.h \ $$PWD/set_algorithm.h \ $$PWD/futuresynchronizer.h \ + $$PWD/launcherinterface.h \ + $$PWD/launcherpackets.h \ + $$PWD/launchersocket.h \ $$PWD/qtcsettings.h FORMS += $$PWD/filewizardpage.ui \ diff --git a/src/libs/utils/utils.qbs b/src/libs/utils/utils.qbs index d9211602086..b2c6bfe57f8 100644 --- a/src/libs/utils/utils.qbs +++ b/src/libs/utils/utils.qbs @@ -159,6 +159,12 @@ Project { "json.h", "jsontreeitem.cpp", "jsontreeitem.h", + "launcherinterface.cpp", + "launcherinterface.h", + "launcherpackets.cpp", + "launcherpackets.h", + "launchersocket.cpp", + "launchersocket.h", "layoutbuilder.cpp", "layoutbuilder.h", "linecolumn.cpp", diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index 1719a99aff9..4b6266a86d2 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -33,6 +33,7 @@ if (APPLE) add_subdirectory(iostool) endif() +add_subdirectory(processlauncher) add_subdirectory(qml2puppet) add_subdirectory(qtcdebugger) ## windows only # add_subdirectory(qtcrashhandler) diff --git a/src/tools/processlauncher/CMakeLists.txt b/src/tools/processlauncher/CMakeLists.txt new file mode 100644 index 00000000000..51c2e885676 --- /dev/null +++ b/src/tools/processlauncher/CMakeLists.txt @@ -0,0 +1,14 @@ +set(UTILSDIR "${PROJECT_SOURCE_DIR}/src/libs/utils") + +add_qtc_executable(qtcreator_processlauncher + INCLUDES "${UTILSDIR}" + DEPENDS Qt5::Core Qt5::Network + SOURCES + launcherlogging.cpp + launcherlogging.h + launchersockethandler.cpp + launchersockethandler.h + processlauncher-main.cpp + ${UTILSDIR}/launcherpackets.cpp + ${UTILSDIR}/launcherpackets.h + ) diff --git a/src/tools/processlauncher/launcherlogging.cpp b/src/tools/processlauncher/launcherlogging.cpp new file mode 100644 index 00000000000..607979ea0b1 --- /dev/null +++ b/src/tools/processlauncher/launcherlogging.cpp @@ -0,0 +1,32 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "launcherlogging.h" + +namespace Utils { +namespace Internal { +Q_LOGGING_CATEGORY(launcherLog, "qtc.utils.launcher", QtWarningMsg) +} +} diff --git a/src/tools/processlauncher/launcherlogging.h b/src/tools/processlauncher/launcherlogging.h new file mode 100644 index 00000000000..2d9a8d3c399 --- /dev/null +++ b/src/tools/processlauncher/launcherlogging.h @@ -0,0 +1,38 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include +#include + +namespace Utils { +namespace Internal { +Q_DECLARE_LOGGING_CATEGORY(launcherLog) +template void logDebug(const T &msg) { qCDebug(launcherLog) << msg; } +template void logWarn(const T &msg) { qCWarning(launcherLog) << msg; } +template void logError(const T &msg) { qCCritical(launcherLog) << msg; } +} +} diff --git a/src/tools/processlauncher/launchersockethandler.cpp b/src/tools/processlauncher/launchersockethandler.cpp new file mode 100644 index 00000000000..6d146d91ed1 --- /dev/null +++ b/src/tools/processlauncher/launchersockethandler.cpp @@ -0,0 +1,281 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "launchersockethandler.h" + +#include "launcherlogging.h" + +#include +#include +#include +#include + +namespace Utils { +namespace Internal { + +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(QString serverPath, QObject *parent) + : QObject(parent), + m_serverPath(std::move(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, +#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) + static_cast(&QLocalSocket::error), +#else + &QLocalSocket::errorOccurred, +#endif + this, &LauncherSocketHandler::handleSocketError); + m_socket->connectToServer(m_serverPath); +} + +void LauncherSocketHandler::handleSocketData() +{ + try { + if (!m_packetParser.parse()) + return; + } catch (const PacketParser::InvalidPacketSizeException &e) { + logWarn(QStringLiteral("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(QStringLiteral("Internal protocol error: invalid packet type %1.") + .arg(static_cast(m_packetParser.type()))); + return; + } + handleSocketData(); +} + +void LauncherSocketHandler::handleSocketError() +{ + if (m_socket->error() != QLocalSocket::PeerClosedError) { + logError(QStringLiteral("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(); + qApp->quit(); +} + +void LauncherSocketHandler::handleProcessError() +{ + Process * proc = senderProcess(); + if (proc->error() != QProcess::FailedToStart) + return; + proc->stopStopProcedure(); + ProcessErrorPacket packet(proc->token()); + packet.error = proc->error(); + packet.errorString = proc->errorString(); + sendPacket(packet); +} + +void LauncherSocketHandler::handleProcessFinished() +{ + Process * proc = senderProcess(); + proc->stopStopProcedure(); + ProcessFinishedPacket packet(proc->token()); + packet.error = proc->error(); + packet.errorString = proc->errorString(); + packet.exitCode = proc->exitCode(); + packet.exitStatus = proc->exitStatus(); + packet.stdErr = proc->readAllStandardError(); + packet.stdOut = proc->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. + Process * proc = senderProcess(); + proc->disconnect(); + m_processes.remove(proc->token()); + ProcessFinishedPacket packet(proc->token()); + packet.error = QProcess::Crashed; + packet.exitCode = -1; + packet.exitStatus = QProcess::CrashExit; + packet.stdErr = proc->readAllStandardError(); + packet.stdOut = proc->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( + 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) +{ + const auto p = new Process(token, this); + connect(p, &QProcess::errorOccurred, this, &LauncherSocketHandler::handleProcessError); + connect(p, static_cast(&QProcess::finished), + this, &LauncherSocketHandler::handleProcessFinished); + connect(p, &Process::failedToStop, this, &LauncherSocketHandler::handleStopFailure); + return p; +} + +Process *LauncherSocketHandler::senderProcess() const +{ + return static_cast(sender()); +} + +} // namespace Internal +} // namespace Utils + +#include diff --git a/src/tools/processlauncher/launchersockethandler.h b/src/tools/processlauncher/launchersockethandler.h new file mode 100644 index 00000000000..6ad400d2b62 --- /dev/null +++ b/src/tools/processlauncher/launchersockethandler.h @@ -0,0 +1,75 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include + +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QLocalSocket; +QT_END_NAMESPACE + +namespace Utils { +namespace Internal { +class Process; + +class LauncherSocketHandler : public QObject +{ + Q_OBJECT +public: + explicit LauncherSocketHandler(QString socketPath, QObject *parent = nullptr); + ~LauncherSocketHandler() override; + + 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 m_processes; +}; + +} // namespace Internal +} // namespace Utils diff --git a/src/tools/processlauncher/processlauncher-main.cpp b/src/tools/processlauncher/processlauncher-main.cpp new file mode 100644 index 00000000000..7dc4cefd986 --- /dev/null +++ b/src/tools/processlauncher/processlauncher-main.cpp @@ -0,0 +1,57 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "launcherlogging.h" +#include "launchersockethandler.h" + +#include +#include + +#ifdef Q_OS_WIN +#include + +BOOL WINAPI consoleCtrlHandler(DWORD) +{ + // Ignore Ctrl-C / Ctrl-Break. QtCreator will tell us to exit gracefully. + return TRUE; +} +#endif + +int main(int argc, char *argv[]) +{ +#ifdef Q_OS_WIN + SetConsoleCtrlHandler(consoleCtrlHandler, TRUE); +#endif + + QCoreApplication app(argc, argv); + if (app.arguments().size() != 2) { + Utils::Internal::logError("Need exactly one argument (path to socket)"); + return 1; + } + + Utils::Internal::LauncherSocketHandler launcher(app.arguments().constLast()); + QTimer::singleShot(0, &launcher, &Utils::Internal::LauncherSocketHandler::start); + return app.exec(); +} diff --git a/src/tools/processlauncher/processlauncher.pro b/src/tools/processlauncher/processlauncher.pro new file mode 100644 index 00000000000..38089a50954 --- /dev/null +++ b/src/tools/processlauncher/processlauncher.pro @@ -0,0 +1,21 @@ +include(../../qtcreatortool.pri) + +TARGET = qtcreator_processlauncher +CONFIG += console c++17 +CONFIG -= app_bundle +QT = core network + +UTILS_DIR = $$PWD/../../libs/utils + +INCLUDEPATH += $$UTILS_DIR + +HEADERS += \ + launcherlogging.h \ + launchersockethandler.h \ + $$UTILS_DIR/launcherpackets.h + +SOURCES += \ + launcherlogging.cpp \ + launchersockethandler.cpp \ + processlauncher-main.cpp \ + $$UTILS_DIR/launcherpackets.cpp diff --git a/src/tools/processlauncher/processlauncher.qbs b/src/tools/processlauncher/processlauncher.qbs new file mode 100644 index 00000000000..2303db8b421 --- /dev/null +++ b/src/tools/processlauncher/processlauncher.qbs @@ -0,0 +1,28 @@ +import qbs +import qbs.FileInfo + +QtcTool { + name: "qtcreator_processlauncher" + + Depends { name: "Qt.network" } + + cpp.includePaths: base.concat(pathToUtils) + + files: [ + "launcherlogging.cpp", + "launcherlogging.h", + "launchersockethandler.cpp", + "launchersockethandler.h", + "processlauncher-main.cpp", + ] + + property string pathToUtils: sourceDirectory + "/../../libs/utils" + Group { + name: "protocol sources" + prefix: pathToUtils + '/' + files: [ + "launcherpackets.cpp", + "launcherpackets.h", + ] + } +} diff --git a/src/tools/tools.pro b/src/tools/tools.pro index 5bfe2195f41..cd11c4e761a 100644 --- a/src/tools/tools.pro +++ b/src/tools/tools.pro @@ -7,6 +7,7 @@ SUBDIRS = qtpromaker \ valgrindfake \ 3rdparty \ buildoutputparser \ + processlauncher \ qtc-askpass isEmpty(QTC_SKIP_SDKTOOL): SUBDIRS += sdktool diff --git a/src/tools/tools.qbs b/src/tools/tools.qbs index 36ced1a132d..2c65d60e230 100644 --- a/src/tools/tools.qbs +++ b/src/tools/tools.qbs @@ -7,6 +7,7 @@ Project { "buildoutputparser/buildoutputparser.qbs", "clangbackend/clangbackend.qbs", "cplusplustools.qbs", + "processlauncher/processlauncher.qbs", "qml2puppet/qml2puppet.qbs", "qtcdebugger/qtcdebugger.qbs", "qtcreatorcrashhandler/qtcreatorcrashhandler.qbs",