From 5e2fb1455146792e42539d9147ce51d8100c2a4b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 13 Sep 2024 02:44:33 -0700 Subject: [PATCH] io/ipchandler: add IpcHandler and qs msg Also reworks the whole ipc system to use serialized variants. --- src/core/generation.cpp | 16 ++ src/core/generation.hpp | 13 ++ src/core/ipc.cpp | 38 +++-- src/core/ipc.hpp | 168 +++++++++++++++++++-- src/core/ipccommand.hpp | 20 +++ src/core/main.cpp | 71 +++++++++ src/io/CMakeLists.txt | 3 + src/io/ipc.cpp | 180 ++++++++++++++++++++++ src/io/ipc.hpp | 139 +++++++++++++++++ src/io/ipccomm.cpp | 236 +++++++++++++++++++++++++++++ src/io/ipccomm.hpp | 39 +++++ src/io/ipchandler.cpp | 324 ++++++++++++++++++++++++++++++++++++++++ src/io/ipchandler.hpp | 207 +++++++++++++++++++++++++ src/io/module.md | 1 + 14 files changed, 1428 insertions(+), 27 deletions(-) create mode 100644 src/core/ipccommand.hpp create mode 100644 src/io/ipc.cpp create mode 100644 src/io/ipc.hpp create mode 100644 src/io/ipccomm.cpp create mode 100644 src/io/ipccomm.hpp create mode 100644 src/io/ipchandler.cpp create mode 100644 src/io/ipchandler.hpp diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 5f21a19a..32018d67 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -67,6 +67,10 @@ void EngineGeneration::destroy() { this->watcher = nullptr; } + for (auto* extension: this->extensions.values()) { + delete extension; + } + if (this->root != nullptr) { QObject::connect(this->root, &QObject::destroyed, this, [this]() { // prevent further js execution between garbage collection and engine destruction. @@ -285,6 +289,18 @@ void EngineGeneration::incubationControllerDestroyed() { } } +void EngineGeneration::registerExtension(const void* key, EngineGenerationExt* extension) { + if (this->extensions.contains(key)) { + delete this->extensions.value(key); + } + + this->extensions.insert(key, extension); +} + +EngineGenerationExt* EngineGeneration::findExtension(const void* key) { + return this->extensions.value(key); +} + void EngineGeneration::quit() { this->shouldTerminate = true; this->destroy(); diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 54863752..823ca82a 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +21,13 @@ class RootWrapper; class QuickshellGlobal; +class EngineGenerationExt { +public: + EngineGenerationExt() = default; + virtual ~EngineGenerationExt() = default; + Q_DISABLE_COPY_MOVE(EngineGenerationExt); +}; + class EngineGeneration: public QObject { Q_OBJECT; @@ -35,6 +43,10 @@ public: void registerIncubationController(QQmlIncubationController* controller); void deregisterIncubationController(QQmlIncubationController* controller); + // takes ownership + void registerExtension(const void* key, EngineGenerationExt* extension); + EngineGenerationExt* findExtension(const void* key); + static EngineGeneration* findEngineGeneration(QQmlEngine* engine); static EngineGeneration* findObjectGeneration(QObject* object); @@ -78,6 +90,7 @@ private: void postReload(); void assignIncubationController(); QVector> incubationControllers; + QHash extensions; bool destroying = false; bool shouldTerminate = false; diff --git a/src/core/ipc.cpp b/src/core/ipc.cpp index ca14834a..dd2cd1e8 100644 --- a/src/core/ipc.cpp +++ b/src/core/ipc.cpp @@ -1,6 +1,8 @@ #include "ipc.hpp" #include +#include +#include #include #include #include @@ -8,6 +10,7 @@ #include #include "generation.hpp" +#include "ipccommand.hpp" #include "paths.hpp" namespace qs::ipc { @@ -62,20 +65,21 @@ void IpcServerConnection::onReadyRead() { this->stream.startTransaction(); this->stream.startTransaction(); - auto command = IpcCommand::Unknown; + IpcCommand command; this->stream >> command; if (!this->stream.commitTransaction()) return; - switch (command) { - case IpcCommand::Kill: - qInfo() << "Exiting due to IPC request."; - EngineGeneration::currentGeneration()->quit(); - break; - default: - qCCritical(logIpc) << "Received invalid IPC command from" << this; - this->socket->disconnectFromServer(); - break; - } + std::visit( + [this](Command& command) { + if constexpr (std::is_same_v) { + qCCritical(logIpc) << "Received invalid IPC command from" << this; + this->socket->disconnectFromServer(); + } else { + command.exec(this); + } + }, + command + ); if (!this->stream.commitTransaction()) return; } @@ -94,11 +98,7 @@ bool IpcClient::isConnected() const { return this->socket.isValid(); } void IpcClient::waitForConnected() { this->socket.waitForConnected(); } void IpcClient::waitForDisconnected() { this->socket.waitForDisconnected(); } -void IpcClient::kill() { - qCDebug(logIpc) << "Sending kill command..."; - this->stream << IpcCommand::Kill; - this->socket.flush(); -} +void IpcClient::kill() { this->sendMessage(IpcCommand(IpcKillCommand())); } void IpcClient::onError(QLocalSocket::LocalSocketError error) { qCCritical(logIpc) << "Socket Error" << error; @@ -116,4 +116,10 @@ int IpcClient::connect(const QString& id, const std::functionquit(); +} + } // namespace qs::ipc diff --git a/src/core/ipc.hpp b/src/core/ipc.hpp index 9738f4b9..77bff913 100644 --- a/src/core/ipc.hpp +++ b/src/core/ipc.hpp @@ -1,17 +1,133 @@ #pragma once +#include #include +#include +#include +#include +#include #include #include +#include +#include #include #include +#include + +template +constexpr void assertSerializable() { + // monostate being zero ensures transactional reads wont break + static_assert( + std::is_same_v>, std::monostate>, + "Serialization of variants without std::monostate at index 0 is disallowed." + ); + + static_assert( + sizeof...(Types) <= std::numeric_limits::max(), + "Serialization of variants that can't fit the tag in a uint8 is disallowed." + ); +} + +template +QDataStream& operator<<(QDataStream& stream, const std::variant& variant) { + assertSerializable(); + + if (variant.valueless_by_exception()) { + stream << static_cast(0); // must be monostate + } else { + stream << static_cast(variant.index()); + std::visit([&](const T& value) { stream << value; }, variant); + } + + return stream; +} + +template +constexpr bool forEachTypeIndex(const auto& f) { + return [&](std::index_sequence) { + return (f(std::in_place_index_t()) || ...); + }(std::index_sequence_for()); +} + +template +std::variant createIndexedOrMonostate(size_t index, std::variant& variant) { + assertSerializable(); + + const auto initialized = + forEachTypeIndex([index, &variant](std::in_place_index_t) { + if (index == Index) { + variant.template emplace(); + return true; + } else { + return false; + } + }); + + if (!initialized) { + variant = std::monostate(); + } + + return variant; +} + +template +QDataStream& operator>>(QDataStream& stream, std::variant& variant) { + assertSerializable(); + + quint8 index = 0; + stream >> index; + + createIndexedOrMonostate(index, variant); + std::visit([&](T& value) { stream >> value; }, variant); + + return stream; +} + +template +QDataStream& streamInValues(QDataStream& stream, const Types&... types) { + return (stream << ... << types); +} + +template +QDataStream& streamOutValues(QDataStream& stream, Types&... types) { + return (stream >> ... >> types); +} + +// NOLINTBEGIN +#define DEFINE_SIMPLE_DATASTREAM_OPS(Type, ...) \ + inline QDataStream& operator<<(QDataStream& stream, const Type& __VA_OPT__(data)) { \ + return streamInValues(stream __VA_OPT__(, __VA_ARGS__)); \ + } \ + \ + inline QDataStream& operator>>(QDataStream& stream, Type& __VA_OPT__(data)) { \ + return streamOutValues(stream __VA_OPT__(, __VA_ARGS__)); \ + } +// NOLINTEND + +DEFINE_SIMPLE_DATASTREAM_OPS(std::monostate); namespace qs::ipc { -enum class IpcCommand : quint8 { - Unknown = 0, - Kill, +Q_DECLARE_LOGGING_CATEGORY(logIpc); + +template +class MessageStream { +public: + explicit MessageStream(QDataStream* stream, QLocalSocket* socket) + : stream(stream) + , socket(socket) {} + + template + MessageStream& operator<<(V value) { + *this->stream << T(value); + this->socket->flush(); + return *this; + } + +private: + QDataStream* stream; + QLocalSocket* socket; }; class IpcServer: public QObject { @@ -35,13 +151,24 @@ class IpcServerConnection: public QObject { public: explicit IpcServerConnection(QLocalSocket* socket, IpcServer* server); + template + void respond(const T& message) { + this->stream << message; + this->socket->flush(); + } + + template + MessageStream responseStream() { + return MessageStream(&this->stream, this->socket); + } + + // public for access by nonlocal handlers + QLocalSocket* socket; + QDataStream stream; + private slots: void onDisconnected(); void onReadyRead(); - -private: - QLocalSocket* socket; - QDataStream stream; }; class IpcClient: public QObject { @@ -56,19 +183,38 @@ public: void kill(); + template + void sendMessage(const T& message) { + this->stream << message; + this->socket.flush(); + } + + template + bool waitForResponse(T& slot) { + while (this->socket.waitForReadyRead(-1)) { + this->stream.startTransaction(); + this->stream >> slot; + if (!this->stream.commitTransaction()) continue; + return true; + } + + qCCritical(logIpc) << "Error occurred while waiting for response."; + return false; + } + [[nodiscard]] static int connect(const QString& id, const std::function& callback); + // public for access by nonlocal handlers + QLocalSocket socket; + QDataStream stream; + signals: void connected(); void disconnected(); private slots: static void onError(QLocalSocket::LocalSocketError error); - -private: - QLocalSocket socket; - QDataStream stream; }; } // namespace qs::ipc diff --git a/src/core/ipccommand.hpp b/src/core/ipccommand.hpp new file mode 100644 index 00000000..c2e5059f --- /dev/null +++ b/src/core/ipccommand.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include "../io/ipccomm.hpp" +#include "ipc.hpp" + +namespace qs::ipc { + +struct IpcKillCommand: std::monostate { + static void exec(IpcServerConnection* /*unused*/); +}; + +using IpcCommand = std::variant< + std::monostate, + IpcKillCommand, + qs::io::ipc::comm::QueryMetadataCommand, + qs::io::ipc::comm::StringCallCommand>; + +} // namespace qs::ipc diff --git a/src/core/main.cpp b/src/core/main.cpp index 5353a480..8549deb4 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include // NOLINT: Need to include this for impls of some CLI11 classes @@ -37,6 +38,7 @@ #include #include +#include "../io/ipccomm.hpp" #include "build.hpp" #include "common.hpp" #include "instanceinfo.hpp" @@ -157,10 +159,18 @@ struct CommandState { bool json = false; } output; + struct { + bool info = false; + QStringOption target; + QStringOption function; + std::vector arguments; + } ipc; + struct { CLI::App* log = nullptr; CLI::App* list = nullptr; CLI::App* kill = nullptr; + CLI::App* msg = nullptr; } subcommand; struct { @@ -174,6 +184,7 @@ struct CommandState { int readLogFile(CommandState& cmd); int listInstances(CommandState& cmd); int killInstances(CommandState& cmd); +int msgInstance(CommandState& cmd); int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication); struct LaunchArgs { @@ -268,6 +279,10 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { }; auto cli = CLI::App(); + + // Require 0-1 subcommands. Without this, positionals can be parsed as more subcommands. + cli.require_subcommand(0, 1); + addConfigSelection(&cli); addLoggingOptions(&cli, false); addDebugOptions(&cli); @@ -331,6 +346,34 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { state.subcommand.kill = sub; } + { + auto* sub = cli.add_subcommand("msg", "Send messages to IpcHandlers.")->require_option(); + + auto* target = sub->add_option("target", state.ipc.target, "The target to message."); + + auto* function = sub->add_option("function", state.ipc.function) + ->description("The function to call in the target.") + ->needs(target); + + auto* arguments = sub->add_option("arguments", state.ipc.arguments) + ->description("Arguments to the called function.") + ->needs(function) + ->allow_extra_args(); + + sub->add_flag("-i,--info", state.ipc.info) + ->description("Print information about a function or target if given, or all available " + "targets if not.") + ->excludes(arguments); + + auto* instance = addInstanceSelection(sub); + addConfigSelection(sub)->excludes(instance); + addLoggingOptions(sub, false, true); + + sub->require_option(); + + state.subcommand.msg = sub; + } + CLI11_PARSE(cli, argc, argv); // Has to happen before extra threads are spawned. @@ -389,6 +432,8 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { return listInstances(state); } else if (*state.subcommand.kill) { return killInstances(state); + } else if (*state.subcommand.msg) { + return msgInstance(state); } else { return launchFromCommand(state, coreApplication); } @@ -647,6 +692,32 @@ int killInstances(CommandState& cmd) { }); } +int msgInstance(CommandState& cmd) { + InstanceLockInfo instance; + auto r = selectInstance(cmd, &instance); + if (r != 0) return r; + + return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { + if (cmd.ipc.info) { + return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.function); + } else { + QVector arguments; + for (auto& arg: cmd.ipc.arguments) { + arguments += *arg; + } + + return qs::io::ipc::comm::callFunction( + &client, + *cmd.ipc.target, + *cmd.ipc.function, + arguments + ); + } + + return -1; + }); +} + template QString base36Encode(T number) { const QString digits = "0123456789abcdefghijklmnopqrstuvwxyz"; diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt index 7113cd7d..389b8a6f 100644 --- a/src/io/CMakeLists.txt +++ b/src/io/CMakeLists.txt @@ -2,6 +2,9 @@ qt_add_library(quickshell-io STATIC datastream.cpp process.cpp fileview.cpp + ipccomm.cpp + ipc.cpp + ipchandler.cpp ) add_library(quickshell-io-init OBJECT init.cpp) diff --git a/src/io/ipc.cpp b/src/io/ipc.cpp new file mode 100644 index 00000000..37a37eb3 --- /dev/null +++ b/src/io/ipc.cpp @@ -0,0 +1,180 @@ +#include "ipc.hpp" +#include + +#include +#include +#include + +namespace qs::io::ipc { + +const VoidIpcType VoidIpcType::INSTANCE {}; +const StringIpcType StringIpcType::INSTANCE {}; +const IntIpcType IntIpcType::INSTANCE {}; +const BoolIpcType BoolIpcType::INSTANCE {}; +const DoubleIpcType DoubleIpcType::INSTANCE {}; +const ColorIpcType ColorIpcType::INSTANCE {}; + +const IpcType* IpcType::ipcType(const QMetaType& metaType) { + if (metaType.id() == QMetaType::Void) return &VoidIpcType::INSTANCE; + if (metaType.id() == QMetaType::QString) return &StringIpcType::INSTANCE; + if (metaType.id() == QMetaType::Int) return &IntIpcType::INSTANCE; + if (metaType.id() == QMetaType::Bool) return &BoolIpcType::INSTANCE; + if (metaType.id() == QMetaType::Double) return &DoubleIpcType::INSTANCE; + if (metaType.id() == QMetaType::QColor) return &ColorIpcType::INSTANCE; + return nullptr; +} + +IpcTypeSlot::IpcTypeSlot(IpcTypeSlot&& other) noexcept { *this = std::move(other); } + +IpcTypeSlot& IpcTypeSlot::operator=(IpcTypeSlot&& other) noexcept { + this->mType = other.mType; + this->storage = other.storage; + other.mType = nullptr; + other.storage = nullptr; + return *this; +} + +IpcTypeSlot::~IpcTypeSlot() { this->replace(nullptr); } + +const IpcType* IpcTypeSlot::type() const { return this->mType; } + +void* IpcTypeSlot::get() { + if (this->storage == nullptr) { + this->storage = this->mType->createStorage(); + } + + return this->storage; +} + +QGenericArgument IpcTypeSlot::asGenericArgument() { + if (this->mType) { + return QGenericArgument(this->mType->genericArgumentName(), this->get()); + } else { + return QGenericArgument(); + } +} + +QGenericReturnArgument IpcTypeSlot::asGenericReturnArgument() { + if (this->mType) { + return QGenericReturnArgument(this->mType->genericArgumentName(), this->get()); + } else { + return QGenericReturnArgument(); + } +} + +void IpcTypeSlot::replace(void* value) { + if (this->storage != nullptr) { + this->mType->destroyStorage(this->storage); + } + + this->storage = value; +} + +const char* VoidIpcType::name() const { return "void"; } +const char* VoidIpcType::genericArgumentName() const { return "void"; } + +// string +const char* StringIpcType::name() const { return "string"; } +const char* StringIpcType::genericArgumentName() const { return "QString"; } +void* StringIpcType::fromString(const QString& string) const { return new QString(string); } +QString StringIpcType::toString(void* slot) const { return *static_cast(slot); } +void* StringIpcType::createStorage() const { return new QString(); } +void StringIpcType::destroyStorage(void* slot) const { delete static_cast(slot); } + +// int +const char* IntIpcType::name() const { return "int"; } +const char* IntIpcType::genericArgumentName() const { return "int"; } + +void* IntIpcType::fromString(const QString& string) const { + auto ok = false; + auto v = string.toInt(&ok); + + return ok ? new int(v) : nullptr; +} + +QString IntIpcType::toString(void* slot) const { return QString::number(*static_cast(slot)); } + +void* IntIpcType::createStorage() const { return new int(); } +void IntIpcType::destroyStorage(void* slot) const { delete static_cast(slot); } + +// bool +const char* BoolIpcType::name() const { return "bool"; } +const char* BoolIpcType::genericArgumentName() const { return "bool"; } + +void* BoolIpcType::fromString(const QString& string) const { + if (string == "true") return new bool(true); + if (string == "false") return new bool(false); + + auto isInt = false; + auto iv = string.toInt(&isInt); + + return isInt ? new bool(iv != 0) : nullptr; +} + +QString BoolIpcType::toString(void* slot) const { + return *static_cast(slot) ? "true" : "false"; +} + +void* BoolIpcType::createStorage() const { return new bool(); } +void BoolIpcType::destroyStorage(void* slot) const { delete static_cast(slot); } + +// double +const char* DoubleIpcType::name() const { return "real"; } +const char* DoubleIpcType::genericArgumentName() const { return "double"; } + +void* DoubleIpcType::fromString(const QString& string) const { + auto ok = false; + auto v = string.toDouble(&ok); + + return ok ? new double(v) : nullptr; +} + +QString DoubleIpcType::toString(void* slot) const { + return QString::number(*static_cast(slot)); +} + +void* DoubleIpcType::createStorage() const { return new double(); } +void DoubleIpcType::destroyStorage(void* slot) const { delete static_cast(slot); } + +// color +const char* ColorIpcType::name() const { return "color"; } +const char* ColorIpcType::genericArgumentName() const { return "QColor"; } + +void* ColorIpcType::fromString(const QString& string) const { + auto color = QColor::fromString(string); + + if (!color.isValid() && !string.startsWith('#')) { + color = QColor::fromString('#' % string); + } + + return color.isValid() ? new QColor(color) : nullptr; +} + +QString ColorIpcType::toString(void* slot) const { + return static_cast(slot)->name(QColor::HexArgb); +} + +void* ColorIpcType::createStorage() const { return new bool(); } +void ColorIpcType::destroyStorage(void* slot) const { delete static_cast(slot); } + +QString WireFunctionDefinition::toString() const { + QString paramString; + for (const auto& [name, type]: this->arguments) { + if (!paramString.isEmpty()) paramString += ", "; + paramString += name % ": " % type; + } + + return "function " % this->name % '(' % paramString % "): " % this->returnType; +} + +QString WireTargetDefinition::toString() const { + QString accum = "target " % this->name; + + for (const auto& func: this->functions) { + accum += "\n " % func.toString(); + } + + return accum; +} + +} // namespace qs::io::ipc diff --git a/src/io/ipc.hpp b/src/io/ipc.hpp new file mode 100644 index 00000000..50a94759 --- /dev/null +++ b/src/io/ipc.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include + +#include "../core/ipc.hpp" + +namespace qs::io::ipc { + +class IpcTypeSlot; + +class IpcType { +public: + IpcType() = default; + virtual ~IpcType() = default; + IpcType(const IpcType&) = default; + IpcType(IpcType&&) = default; + IpcType& operator=(const IpcType&) = default; + IpcType& operator=(IpcType&&) = default; + + [[nodiscard]] virtual const char* name() const = 0; + [[nodiscard]] virtual const char* genericArgumentName() const = 0; + [[nodiscard]] virtual void* fromString(const QString& /*string*/) const { return nullptr; } + [[nodiscard]] virtual QString toString(void* /*slot*/) const { return ""; } + [[nodiscard]] virtual void* createStorage() const { return nullptr; } + virtual void destroyStorage(void* /*slot*/) const {} + + static const IpcType* ipcType(const QMetaType& metaType); +}; + +class IpcTypeSlot { +public: + explicit IpcTypeSlot(const IpcType* type = nullptr): mType(type) {} + ~IpcTypeSlot(); + Q_DISABLE_COPY(IpcTypeSlot); + IpcTypeSlot(IpcTypeSlot&& other) noexcept; + IpcTypeSlot& operator=(IpcTypeSlot&& other) noexcept; + + [[nodiscard]] const IpcType* type() const; + [[nodiscard]] void* get(); + [[nodiscard]] QGenericArgument asGenericArgument(); + [[nodiscard]] QGenericReturnArgument asGenericReturnArgument(); + + void replace(void* value); + +private: + const IpcType* mType = nullptr; + void* storage = nullptr; +}; + +class VoidIpcType: public IpcType { +public: + [[nodiscard]] const char* name() const override; + [[nodiscard]] const char* genericArgumentName() const override; + + static const VoidIpcType INSTANCE; +}; + +class StringIpcType: public IpcType { +public: + [[nodiscard]] const char* name() const override; + [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] void* fromString(const QString& string) const override; + [[nodiscard]] QString toString(void* slot) const override; + [[nodiscard]] void* createStorage() const override; + void destroyStorage(void* slot) const override; + + static const StringIpcType INSTANCE; +}; + +class IntIpcType: public IpcType { +public: + [[nodiscard]] const char* name() const override; + [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] void* fromString(const QString& string) const override; + [[nodiscard]] QString toString(void* slot) const override; + [[nodiscard]] void* createStorage() const override; + void destroyStorage(void* slot) const override; + + static const IntIpcType INSTANCE; +}; + +class BoolIpcType: public IpcType { +public: + [[nodiscard]] const char* name() const override; + [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] void* fromString(const QString& string) const override; + [[nodiscard]] QString toString(void* slot) const override; + [[nodiscard]] void* createStorage() const override; + void destroyStorage(void* slot) const override; + + static const BoolIpcType INSTANCE; +}; + +class DoubleIpcType: public IpcType { +public: + [[nodiscard]] const char* name() const override; + [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] void* fromString(const QString& string) const override; + [[nodiscard]] QString toString(void* slot) const override; + [[nodiscard]] void* createStorage() const override; + void destroyStorage(void* slot) const override; + + static const DoubleIpcType INSTANCE; +}; + +class ColorIpcType: public IpcType { +public: + [[nodiscard]] const char* name() const override; + [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] void* fromString(const QString& string) const override; + [[nodiscard]] QString toString(void* slot) const override; + [[nodiscard]] void* createStorage() const override; + void destroyStorage(void* slot) const override; + + static const ColorIpcType INSTANCE; +}; + +struct WireFunctionDefinition { + QString name; + QString returnType; + QVector> arguments; + + [[nodiscard]] QString toString() const; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(WireFunctionDefinition, data.name, data.returnType, data.arguments); + +struct WireTargetDefinition { + QString name; + QVector functions; + + [[nodiscard]] QString toString() const; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions); + +} // namespace qs::io::ipc diff --git a/src/io/ipccomm.cpp b/src/io/ipccomm.cpp new file mode 100644 index 00000000..56381260 --- /dev/null +++ b/src/io/ipccomm.cpp @@ -0,0 +1,236 @@ +#include "ipccomm.hpp" +#include +#include + +#include +#include +#include +#include +#include + +#include "../core/generation.hpp" +#include "../core/ipc.hpp" +#include "../core/ipccommand.hpp" +#include "../core/logging.hpp" +#include "ipc.hpp" +#include "ipchandler.hpp" + +using namespace qs::ipc; + +namespace qs::io::ipc::comm { + +struct NoCurrentGeneration: std::monostate {}; +struct TargetNotFound: std::monostate {}; +struct FunctionNotFound: std::monostate {}; + +using QueryResponse = std::variant< + std::monostate, + NoCurrentGeneration, + TargetNotFound, + FunctionNotFound, + QVector, + WireTargetDefinition, + WireFunctionDefinition>; + +void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const { + auto resp = conn->responseStream(); + + if (auto* generation = EngineGeneration::currentGeneration()) { + auto* registry = IpcHandlerRegistry::forGeneration(generation); + + if (this->target.isEmpty()) { + resp << registry->wireTargets(); + } else { + auto* handler = registry->findHandler(this->target); + + if (handler) { + if (this->function.isEmpty()) { + resp << handler->wireDef(); + } else { + auto* func = handler->findFunction(this->function); + + if (func) { + resp << func->wireDef(); + } else { + resp << FunctionNotFound(); + } + } + } else { + resp << TargetNotFound(); + } + } + } else { + resp << NoCurrentGeneration(); + } +} + +int queryMetadata(IpcClient* client, const QString& target, const QString& function) { + client->sendMessage(IpcCommand(QueryMetadataCommand {.target = target, .function = function})); + + QueryResponse slot; + if (!client->waitForResponse(slot)) return -1; + + if (std::holds_alternative>(slot)) { + const auto& targets = std::get>(slot); + + for (const auto& target: targets) { + qCInfo(logBare).noquote() << target.toString(); + } + + return 0; + } else if (std::holds_alternative(slot)) { + qCInfo(logBare).noquote() << std::get(slot).toString(); + } else if (std::holds_alternative(slot)) { + qCInfo(logBare).noquote() << std::get(slot).toString(); + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Target not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Function not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Not ready to accept queries yet."; + } else { + qCCritical(logIpc) << "Received invalid IPC response from" << client; + } + + return -1; +} + +struct ArgParseFailed { + WireFunctionDefinition definition; + bool isCountMismatch = false; + quint8 paramIndex = 0; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS( + ArgParseFailed, + data.definition, + data.isCountMismatch, + data.paramIndex +); + +struct Completed { + bool isVoid = false; + QString returnValue; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(Completed, data.isVoid, data.returnValue); + +using StringCallResponse = std::variant< + std::monostate, + NoCurrentGeneration, + TargetNotFound, + FunctionNotFound, + ArgParseFailed, + Completed>; + +void StringCallCommand::exec(qs::ipc::IpcServerConnection* conn) const { + auto resp = conn->responseStream(); + + if (auto* generation = EngineGeneration::currentGeneration()) { + auto* registry = IpcHandlerRegistry::forGeneration(generation); + + auto* handler = registry->findHandler(this->target); + if (!handler) { + resp << TargetNotFound(); + return; + } + + auto* func = handler->findFunction(this->function); + if (!func) { + resp << FunctionNotFound(); + return; + } + + if (func->argumentTypes.length() != this->arguments.length()) { + resp << ArgParseFailed { + .definition = func->wireDef(), + .isCountMismatch = true, + }; + + return; + } + + auto storage = IpcCallStorage(*func); + for (auto i = 0; i < this->arguments.length(); i++) { + if (!storage.setArgumentStr(i, this->arguments.value(i))) { + resp << ArgParseFailed { + .definition = func->wireDef(), + .paramIndex = static_cast(i), + }; + + return; + } + } + + func->invoke(handler, storage); + + resp << Completed { + .isVoid = func->returnType == &VoidIpcType::INSTANCE, + .returnValue = storage.getReturnStr(), + }; + } else { + conn->respond(StringCallResponse(NoCurrentGeneration())); + } +} + +int callFunction( + IpcClient* client, + const QString& target, + const QString& function, + const QVector& arguments +) { + if (target.isEmpty()) { + qCCritical(logBare) << "Target required to send message."; + return -1; + } else if (function.isEmpty()) { + qCCritical(logBare) << "Function required to send message."; + return -1; + } + + client->sendMessage( + IpcCommand(StringCallCommand {.target = target, .function = function, .arguments = arguments}) + ); + + StringCallResponse slot; + if (!client->waitForResponse(slot)) return -1; + + if (std::holds_alternative(slot)) { + auto& result = std::get(slot); + if (!result.isVoid) { + QTextStream(stdout) << result.returnValue << Qt::endl; + } + + return 0; + } else if (std::holds_alternative(slot)) { + auto& error = std::get(slot); + + if (error.isCountMismatch) { + auto correctCount = error.definition.arguments.length(); + + qCCritical(logBare).nospace() + << "Too " << (correctCount < arguments.length() ? "many" : "few") + << " arguments provided (" << correctCount << " required but " << arguments.length() + << " were provided.)"; + } else { + const auto& provided = arguments.at(error.paramIndex); + const auto& definition = error.definition.arguments.at(error.paramIndex); + + qCCritical(logBare).nospace() + << "Unable to parse argument " << (error.paramIndex + 1) << " as " << definition.second + << ". Provided argument: " << provided; + } + + qCCritical(logBare).noquote() << "Function definition:" << error.definition.toString(); + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Target not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Function not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Not ready to accept queries yet."; + } else { + qCCritical(logIpc) << "Received invalid IPC response from" << client; + } + + return -1; +} +} // namespace qs::io::ipc::comm diff --git a/src/io/ipccomm.hpp b/src/io/ipccomm.hpp new file mode 100644 index 00000000..7b1ec02a --- /dev/null +++ b/src/io/ipccomm.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#include "../core/ipc.hpp" + +namespace qs::io::ipc::comm { + +struct QueryMetadataCommand { + QString target; + QString function; + + void exec(qs::ipc::IpcServerConnection* conn) const; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(QueryMetadataCommand, data.target, data.function); + +struct StringCallCommand { + QString target; + QString function; + QVector arguments; + + void exec(qs::ipc::IpcServerConnection* conn) const; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(StringCallCommand, data.target, data.function, data.arguments); + +void handleMsg(qs::ipc::IpcServerConnection* conn); +int queryMetadata(qs::ipc::IpcClient* client, const QString& target, const QString& function); + +int callFunction( + qs::ipc::IpcClient* client, + const QString& target, + const QString& function, + const QVector& arguments +); + +} // namespace qs::io::ipc::comm diff --git a/src/io/ipchandler.cpp b/src/io/ipchandler.cpp new file mode 100644 index 00000000..d2a549b2 --- /dev/null +++ b/src/io/ipchandler.cpp @@ -0,0 +1,324 @@ +#include "ipchandler.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/generation.hpp" +#include "ipc.hpp" + +namespace qs::io::ipc { + +bool IpcFunction::resolve(QString& error) { + if (this->method.parameterCount() > 10) { + error = "Due to technical limitations, IPC functions can only have 10 arguments."; + return false; + } + + for (auto i = 0; i < this->method.parameterCount(); i++) { + const auto& metaType = this->method.parameterMetaType(i); + const auto* type = IpcType::ipcType(metaType); + + if (type == nullptr) { + error = QString("Type of argument %1 (%2: %3) cannot be used across IPC.") + .arg(i + 1) + .arg(this->method.parameterNames().value(i)) + .arg(metaType.name()); + + return false; + } + + this->argumentTypes.append(type); + } + + const auto& metaType = this->method.returnMetaType(); + const auto* type = IpcType::ipcType(metaType); + + if (type == nullptr) { + // void and var get mixed by qml engine in return types + if (metaType.id() == QMetaType::QVariant) type = &VoidIpcType::INSTANCE; + + if (type == nullptr) { + error = QString("Return type (%1) cannot be used across IPC.").arg(metaType.name()); + return false; + } + } + + this->returnType = type; + + return true; +} + +void IpcFunction::invoke(QObject* target, IpcCallStorage& storage) const { + auto getArg = [&](size_t i) { + return i < storage.argumentSlots.size() ? storage.argumentSlots.at(i).asGenericArgument() + : QGenericArgument(); + }; + + this->method.invoke( + target, + storage.returnSlot.asGenericReturnArgument(), + getArg(0), + getArg(1), + getArg(2), + getArg(3), + getArg(4), + getArg(5), + getArg(6), + getArg(7), + getArg(8), + getArg(9) + ); +} + +QString IpcFunction::toString() const { + QString paramString; + auto paramNames = this->method.parameterNames(); + for (auto i = 0; i < this->argumentTypes.length(); i++) { + paramString += paramNames.value(i) % ": " % this->argumentTypes.value(i)->name(); + + if (i + 1 != this->argumentTypes.length()) { + paramString += ", "; + } + } + + return "function " % this->method.name() % '(' % paramString % "): " % this->returnType->name(); +} + +WireFunctionDefinition IpcFunction::wireDef() const { + WireFunctionDefinition wire; + wire.name = this->method.name(); + wire.returnType = this->returnType->name(); + + auto paramNames = this->method.parameterNames(); + for (auto i = 0; i < this->argumentTypes.length(); i++) { + wire.arguments += qMakePair(paramNames.value(i), this->argumentTypes.value(i)->name()); + } + + return wire; +} + +IpcCallStorage::IpcCallStorage(const IpcFunction& function): returnSlot(function.returnType) { + for (const auto& arg: function.argumentTypes) { + this->argumentSlots.emplace_back(arg); + } +} + +bool IpcCallStorage::setArgumentStr(size_t i, const QString& value) { + auto& slot = this->argumentSlots.at(i); + + auto* data = slot.type()->fromString(value); + slot.replace(data); + return data != nullptr; +} + +QString IpcCallStorage::getReturnStr() { + return this->returnSlot.type()->toString(this->returnSlot.get()); +} + +IpcHandler::~IpcHandler() { + if (this->registeredState.enabled) { + this->targetState.enabled = false; + this->updateRegistration(true); + } +} + +void IpcHandler::onPostReload() { + const auto& smeta = IpcHandler::staticMetaObject; + const auto* meta = this->metaObject(); + + // Start at the first function following IpcHandler's slots, + // which should handle inheritance on the qml side. + for (auto i = smeta.methodCount(); i != meta->methodCount(); i++) { + const auto& method = meta->method(i); + if (method.methodType() != QMetaMethod::Slot) continue; + + auto ipcFunc = IpcFunction(method); + QString error; + + if (!ipcFunc.resolve(error)) { + qmlWarning(this).nospace().noquote() + << "Error parsing function \"" << method.name() << "\": " << error; + } else { + this->functionMap.insert(method.name(), ipcFunc); + } + } + + this->complete = true; + this->updateRegistration(); + + if (this->targetState.enabled && this->targetState.target.isEmpty()) { + qmlWarning(this) << "This IPC handler is enabled but no target is set. This means it is " + "effectively inoperable."; + } +} + +IpcHandlerRegistry* IpcHandlerRegistry::forGeneration(EngineGeneration* generation) { + static const int key = 0; + auto* ext = generation->findExtension(&key); + + if (!ext) { + ext = new IpcHandlerRegistry(); + generation->registerExtension(&key, ext); + } + + return dynamic_cast(ext); +} + +void IpcHandler::updateRegistration(bool destroying) { + if (!this->complete) return; + + auto* generation = EngineGeneration::findObjectGeneration(this); + + if (!generation) { + if (!destroying) { + qmlWarning(this) << "Unable to identify engine generation, cannot register."; + } + + return; + } + + auto* registry = IpcHandlerRegistry::forGeneration(generation); + + if (this->registeredState.enabled) { + registry->deregisterHandler(this); + } + + if (this->targetState.enabled && !this->targetState.target.isEmpty()) { + registry->registerHandler(this); + } +} + +bool IpcHandler::enabled() const { return this->targetState.enabled; } + +void IpcHandler::setEnabled(bool enabled) { + if (enabled != this->targetState.enabled) { + this->targetState.enabled = enabled; + emit this->enabledChanged(); + this->updateRegistration(); + } +} + +QString IpcHandler::target() const { return this->targetState.target; } + +void IpcHandler::setTarget(const QString& target) { + if (target != this->targetState.target) { + this->targetState.target = target; + emit this->targetChanged(); + this->updateRegistration(); + } +} + +void IpcHandlerRegistry::registerHandler(IpcHandler* handler) { + // inserting a new vec if not present is the desired behavior + auto& targetVec = this->knownHandlers[handler->targetState.target]; + targetVec.append(handler); + + if (this->handlers.contains(handler->targetState.target)) { + qmlWarning(handler) << "Handler was registered but will not be used because another handler " + "is registered for target " + << handler->targetState.target; + } else { + this->handlers.insert(handler->targetState.target, handler); + } + + handler->registeredState = handler->targetState; + handler->registeredState.enabled = true; +} + +void IpcHandlerRegistry::deregisterHandler(IpcHandler* handler) { + auto& targetVec = this->knownHandlers[handler->registeredState.target]; + targetVec.removeOne(handler); + + if (this->handlers.value(handler->registeredState.target) == handler) { + if (targetVec.isEmpty()) { + this->handlers.remove(handler->registeredState.target); + } else { + this->handlers.insert(handler->registeredState.target, targetVec.first()); + } + } + + handler->registeredState = {.enabled = false, .target = ""}; +} + +QString IpcHandler::listMembers(qsizetype indent) { + auto indentStr = QString(indent, ' '); + QString accum; + + for (const auto& func: this->functionMap.values()) { + if (!accum.isEmpty()) accum += '\n'; + accum += indentStr % func.toString(); + } + + return accum; +} + +WireTargetDefinition IpcHandler::wireDef() const { + WireTargetDefinition wire; + wire.name = this->registeredState.target; + + for (const auto& func: this->functionMap.values()) { + wire.functions += func.wireDef(); + } + + return wire; +} + +QString IpcHandlerRegistry::listMembers(const QString& target, qsizetype indent) { + if (auto* handler = this->handlers.value(target)) { + return handler->listMembers(indent); + } else { + QString accum; + + for (auto* handler: this->knownHandlers.value(target)) { + if (!accum.isEmpty()) accum += '\n'; + accum += handler->listMembers(indent); + } + + return accum; + } +} + +QString IpcHandlerRegistry::listTargets(qsizetype indent) { + auto indentStr = QString(indent, ' '); + QString accum; + + for (const auto& target: this->knownHandlers.keys()) { + if (!accum.isEmpty()) accum += '\n'; + accum += indentStr % "Target " % target % '\n' % this->listMembers(target, indent + 2); + } + + return accum; +} + +IpcFunction* IpcHandler::findFunction(const QString& name) { + auto itr = this->functionMap.find(name); + + if (itr == this->functionMap.end()) return nullptr; + else return &*itr; +} + +IpcHandler* IpcHandlerRegistry::findHandler(const QString& target) { + return this->handlers.value(target); +} + +QVector IpcHandlerRegistry::wireTargets() const { + QVector wire; + + for (const auto* handler: this->handlers.values()) { + wire += handler->wireDef(); + } + + return wire; +} + +} // namespace qs::io::ipc diff --git a/src/io/ipchandler.hpp b/src/io/ipchandler.hpp new file mode 100644 index 00000000..df920334 --- /dev/null +++ b/src/io/ipchandler.hpp @@ -0,0 +1,207 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/generation.hpp" +#include "../core/reload.hpp" +#include "ipc.hpp" + +namespace qs::io::ipc { + +class IpcCallStorage; + +class IpcFunction { +public: + explicit IpcFunction(QMetaMethod method): method(method) {} + + bool resolve(QString& error); + void invoke(QObject* target, IpcCallStorage& storage) const; + + [[nodiscard]] QString toString() const; + [[nodiscard]] WireFunctionDefinition wireDef() const; + + QMetaMethod method; + QVector argumentTypes; + const IpcType* returnType = nullptr; +}; + +class IpcCallStorage { +public: + explicit IpcCallStorage(const IpcFunction& function); + + bool setArgumentStr(size_t i, const QString& value); + [[nodiscard]] QString getReturnStr(); + +private: + std::vector argumentSlots; + IpcTypeSlot returnSlot; + + friend class IpcFunction; +}; + +class IpcHandlerRegistry; + +///! Handler for IPC message calls. +/// Each IpcHandler is registered into a per-instance map by its unique @@target. +/// Functions defined on the IpcHandler can be called by `qs msg`. +/// +/// #### Handler Functions +/// IPC handler functions can be called by `qs msg` as long as they have at most 10 +/// arguments, and all argument types along with the return type are listed below. +/// +/// **Argument and return types must be explicitly specified or they will not +/// be registered.** +/// +/// ##### Arguments +/// - `string` will be passed to the parameter as is. +/// - `int` will only accept parameters that can be parsed as an integer. +/// - `bool` will only accept parameters that are "true", "false", or an integer, +/// where 0 will be converted to false, and anything else to true. +/// - `real` will only accept parameters that can be parsed as a number with +/// or without a decimal. +/// - `color` will accept [named colors] or hex strings (RGB, RRGGBB, AARRGGBB) with +/// an optional `#` prefix. +/// +/// [named colors]: https://doc.qt.io/qt-6/qml-color.html#svg-color-reference +/// +/// ##### Return Type +/// - `void` will return nothing. +/// - `string` will be returned as is. +/// - `int` will be converted to a string and returned. +/// - `bool` will be converted to "true" or "false" and returned. +/// - `real` will be converted to a string and returned. +/// - `color` will be converted to a hex string in the form `#AARRGGBB` and returned. +/// +/// #### Example +/// The following example creates ipc functions to control and retrieve the appearance +/// of a Rectangle. +/// +/// ```qml +/// FloatingWindow { +/// Rectangle { +/// id: rect +/// anchors.centerIn: parent +/// width: 100 +/// height: 100 +/// color: "red" +/// } +/// +/// IpcHandler { +/// target: "rect" +/// +/// function setColor(color: color): void { rect.color = color; } +/// function getColor(): color { return rect.color; } +/// function setAngle(angle: real): void { rect.rotation = angle; } +/// function getAngle(): real { return rect.rotation; } +/// function setRadius(radius: int): void { rect.radius = radius; } +/// function getRadius(): int { return rect.radius; } +/// } +/// } +/// ``` +/// The list of registered targets can be inspected using `qs msg -i`. +/// ```sh +/// $ qs msg -i +/// target rect +/// function setColor(color: color): void +/// function getColor(): color +/// function setAngle(angle: real): void +/// function getAngle(): real +/// function setRadius(radius: int): void +/// function getRadius(): int +/// ``` +/// +/// and then invoked using `qs msg`. +/// ```sh +/// $ qs msg rect setColor orange +/// $ qs msg rect setAngle 40.5 +/// $ qs msg rect setRadius 30 +/// $ qs msg rect getColor +/// #ffffa500 +/// $ qs msg rect getAngle +/// 40.5 +/// $ qs msg rect getRadius +/// 30 +/// ``` +class IpcHandler + : public QObject + , public PostReloadHook { + Q_OBJECT; + /// If the handler should be able to receive calls. Defaults to true. + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged); + /// The target this handler should be accessible from. + /// Required and must be unique. May be changed at runtime. + Q_PROPERTY(QString target READ target WRITE setTarget NOTIFY targetChanged); + QML_ELEMENT; + +public: + explicit IpcHandler(QObject* parent = nullptr): QObject(parent) {}; + ~IpcHandler() override; + Q_DISABLE_COPY_MOVE(IpcHandler); + + void onPostReload() override; + + [[nodiscard]] bool enabled() const; + void setEnabled(bool enabled); + + [[nodiscard]] QString target() const; + void setTarget(const QString& target); + + QString listMembers(qsizetype indent); + [[nodiscard]] IpcFunction* findFunction(const QString& name); + [[nodiscard]] WireTargetDefinition wireDef() const; + +signals: + void enabledChanged(); + void targetChanged(); + +private: + void updateRegistration(bool destroying = false); + + struct RegistrationState { + bool enabled = false; + QString target; + }; + + RegistrationState registeredState; + RegistrationState targetState {.enabled = true}; + bool complete = false; + + QHash functionMap; + + friend class IpcHandlerRegistry; +}; + +class IpcHandlerRegistry: public EngineGenerationExt { +public: + static IpcHandlerRegistry* forGeneration(EngineGeneration* generation); + + void registerHandler(IpcHandler* handler); + void deregisterHandler(IpcHandler* handler); + + QString listMembers(const QString& target, qsizetype indent); + QString listTargets(qsizetype indent); + + IpcHandler* findHandler(const QString& target); + + [[nodiscard]] QVector wireTargets() const; + +private: + QHash handlers; + QHash> knownHandlers; +}; + +} // namespace qs::io::ipc diff --git a/src/io/module.md b/src/io/module.md index 8af3799e..8c9e510c 100644 --- a/src/io/module.md +++ b/src/io/module.md @@ -5,5 +5,6 @@ headers = [ "socket.hpp", "process.hpp", "fileview.hpp", + "ipchandler.hpp", ] -----