io/ipchandler: add IpcHandler and qs msg

Also reworks the whole ipc system to use serialized variants.
This commit is contained in:
outfoxxed 2024-09-13 02:44:33 -07:00
parent 3690812919
commit 5e2fb14551
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
14 changed files with 1428 additions and 27 deletions

View file

@ -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();

View file

@ -3,6 +3,7 @@
#include <qcontainerfwd.h>
#include <qdir.h>
#include <qfilesystemwatcher.h>
#include <qhash.h>
#include <qicon.h>
#include <qobject.h>
#include <qpair.h>
@ -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<QPair<QQmlIncubationController*, QObject*>> incubationControllers;
QHash<const void*, EngineGenerationExt*> extensions;
bool destroying = false;
bool shouldTerminate = false;

View file

@ -1,6 +1,8 @@
#include "ipc.hpp"
#include <functional>
#include <variant>
#include <qbuffer.h>
#include <qlocalserver.h>
#include <qlocalsocket.h>
#include <qlogging.h>
@ -8,6 +10,7 @@
#include <qobject.h>
#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]<typename Command>(Command& command) {
if constexpr (std::is_same_v<std::monostate, Command>) {
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::function<void(IpcClient& cl
callback(client);
return 0;
}
void IpcKillCommand::exec(IpcServerConnection* /*unused*/) {
qInfo() << "Exiting due to IPC request.";
EngineGeneration::currentGeneration()->quit();
}
} // namespace qs::ipc

View file

@ -1,17 +1,133 @@
#pragma once
#include <cmath>
#include <functional>
#include <limits>
#include <utility>
#include <variant>
#include <qflags.h>
#include <qlocalserver.h>
#include <qlocalsocket.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qtypes.h>
template <typename... Types>
constexpr void assertSerializable() {
// monostate being zero ensures transactional reads wont break
static_assert(
std::is_same_v<std::variant_alternative_t<0, std::variant<Types...>>, std::monostate>,
"Serialization of variants without std::monostate at index 0 is disallowed."
);
static_assert(
sizeof...(Types) <= std::numeric_limits<quint8>::max(),
"Serialization of variants that can't fit the tag in a uint8 is disallowed."
);
}
template <typename... Types>
QDataStream& operator<<(QDataStream& stream, const std::variant<Types...>& variant) {
assertSerializable<Types...>();
if (variant.valueless_by_exception()) {
stream << static_cast<quint8>(0); // must be monostate
} else {
stream << static_cast<quint8>(variant.index());
std::visit([&]<typename T>(const T& value) { stream << value; }, variant);
}
return stream;
}
template <typename... Types>
constexpr bool forEachTypeIndex(const auto& f) {
return [&]<size_t... Index>(std::index_sequence<Index...>) {
return (f(std::in_place_index_t<Index>()) || ...);
}(std::index_sequence_for<Types...>());
}
template <typename... Types>
std::variant<Types...> createIndexedOrMonostate(size_t index, std::variant<Types...>& variant) {
assertSerializable<Types...>();
const auto initialized =
forEachTypeIndex<Types...>([index, &variant]<size_t Index>(std::in_place_index_t<Index>) {
if (index == Index) {
variant.template emplace<Index>();
return true;
} else {
return false;
}
});
if (!initialized) {
variant = std::monostate();
}
return variant;
}
template <typename... Types>
QDataStream& operator>>(QDataStream& stream, std::variant<Types...>& variant) {
assertSerializable<Types...>();
quint8 index = 0;
stream >> index;
createIndexedOrMonostate<Types...>(index, variant);
std::visit([&]<typename T>(T& value) { stream >> value; }, variant);
return stream;
}
template <typename... Types>
QDataStream& streamInValues(QDataStream& stream, const Types&... types) {
return (stream << ... << types);
}
template <typename... Types>
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 <typename T>
class MessageStream {
public:
explicit MessageStream(QDataStream* stream, QLocalSocket* socket)
: stream(stream)
, socket(socket) {}
template <typename V>
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 <typename T>
void respond(const T& message) {
this->stream << message;
this->socket->flush();
}
template <typename T>
MessageStream<T> responseStream() {
return MessageStream<T>(&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 <typename T>
void sendMessage(const T& message) {
this->stream << message;
this->socket.flush();
}
template <typename T>
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<void(IpcClient& client)>& 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

20
src/core/ipccommand.hpp Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include <variant>
#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

View file

@ -6,6 +6,7 @@
#include <cstdlib>
#include <limits>
#include <string>
#include <vector>
#include <CLI/App.hpp>
#include <CLI/CLI.hpp> // NOLINT: Need to include this for impls of some CLI11 classes
@ -37,6 +38,7 @@
#include <qtextstream.h>
#include <unistd.h>
#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<QStringOption> 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<QString> 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 <typename T>
QString base36Encode(T number) {
const QString digits = "0123456789abcdefghijklmnopqrstuvwxyz";

View file

@ -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)

180
src/io/ipc.cpp Normal file
View file

@ -0,0 +1,180 @@
#include "ipc.hpp"
#include <utility>
#include <qcolor.h>
#include <qmetatype.h>
#include <qobjectdefs.h>
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<QString*>(slot); }
void* StringIpcType::createStorage() const { return new QString(); }
void StringIpcType::destroyStorage(void* slot) const { delete static_cast<QString*>(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<int*>(slot)); }
void* IntIpcType::createStorage() const { return new int(); }
void IntIpcType::destroyStorage(void* slot) const { delete static_cast<int*>(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<bool*>(slot) ? "true" : "false";
}
void* BoolIpcType::createStorage() const { return new bool(); }
void BoolIpcType::destroyStorage(void* slot) const { delete static_cast<bool*>(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<double*>(slot));
}
void* DoubleIpcType::createStorage() const { return new double(); }
void DoubleIpcType::destroyStorage(void* slot) const { delete static_cast<double*>(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<QColor*>(slot)->name(QColor::HexArgb);
}
void* ColorIpcType::createStorage() const { return new bool(); }
void ColorIpcType::destroyStorage(void* slot) const { delete static_cast<bool*>(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

139
src/io/ipc.hpp Normal file
View file

@ -0,0 +1,139 @@
#pragma once
#include <qcontainerfwd.h>
#include <qobjectdefs.h>
#include <qtclasshelpermacros.h>
#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<QPair<QString, QString>> arguments;
[[nodiscard]] QString toString() const;
};
DEFINE_SIMPLE_DATASTREAM_OPS(WireFunctionDefinition, data.name, data.returnType, data.arguments);
struct WireTargetDefinition {
QString name;
QVector<WireFunctionDefinition> functions;
[[nodiscard]] QString toString() const;
};
DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions);
} // namespace qs::io::ipc

236
src/io/ipccomm.cpp Normal file
View file

@ -0,0 +1,236 @@
#include "ipccomm.hpp"
#include <cstdio>
#include <variant>
#include <qcontainerfwd.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qtextstream.h>
#include <qtypes.h>
#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>,
WireTargetDefinition,
WireFunctionDefinition>;
void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const {
auto resp = conn->responseStream<QueryResponse>();
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<QVector<WireTargetDefinition>>(slot)) {
const auto& targets = std::get<QVector<WireTargetDefinition>>(slot);
for (const auto& target: targets) {
qCInfo(logBare).noquote() << target.toString();
}
return 0;
} else if (std::holds_alternative<WireTargetDefinition>(slot)) {
qCInfo(logBare).noquote() << std::get<WireTargetDefinition>(slot).toString();
} else if (std::holds_alternative<WireFunctionDefinition>(slot)) {
qCInfo(logBare).noquote() << std::get<WireFunctionDefinition>(slot).toString();
} else if (std::holds_alternative<TargetNotFound>(slot)) {
qCCritical(logBare) << "Target not found.";
} else if (std::holds_alternative<FunctionNotFound>(slot)) {
qCCritical(logBare) << "Function not found.";
} else if (std::holds_alternative<NoCurrentGeneration>(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<StringCallResponse>();
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<quint8>(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<QString>& 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<Completed>(slot)) {
auto& result = std::get<Completed>(slot);
if (!result.isVoid) {
QTextStream(stdout) << result.returnValue << Qt::endl;
}
return 0;
} else if (std::holds_alternative<ArgParseFailed>(slot)) {
auto& error = std::get<ArgParseFailed>(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<TargetNotFound>(slot)) {
qCCritical(logBare) << "Target not found.";
} else if (std::holds_alternative<FunctionNotFound>(slot)) {
qCCritical(logBare) << "Function not found.";
} else if (std::holds_alternative<NoCurrentGeneration>(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

39
src/io/ipccomm.hpp Normal file
View file

@ -0,0 +1,39 @@
#pragma once
#include <qcontainerfwd.h>
#include <qflags.h>
#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<QString> 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<QString>& arguments
);
} // namespace qs::io::ipc::comm

324
src/io/ipchandler.cpp Normal file
View file

@ -0,0 +1,324 @@
#include "ipchandler.hpp"
#include <cstddef>
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qlogging.h>
#include <qmetaobject.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qpair.h>
#include <qqmlinfo.h>
#include <qstringbuilder.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#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<IpcHandlerRegistry*>(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<WireTargetDefinition> IpcHandlerRegistry::wireTargets() const {
QVector<WireTargetDefinition> wire;
for (const auto* handler: this->handlers.values()) {
wire += handler->wireDef();
}
return wire;
}
} // namespace qs::io::ipc

207
src/io/ipchandler.hpp Normal file
View file

@ -0,0 +1,207 @@
#pragma once
#include <cstddef>
#include <vector>
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qhash.h>
#include <qmetaobject.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qqmlintegration.h>
#include <qqmlparserstatus.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qvariant.h>
#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<const IpcType*> 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<IpcTypeSlot> 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<QString, IpcFunction> 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<WireTargetDefinition> wireTargets() const;
private:
QHash<QString, IpcHandler*> handlers;
QHash<QString, QVector<IpcHandler*>> knownHandlers;
};
} // namespace qs::io::ipc

View file

@ -5,5 +5,6 @@ headers = [
"socket.hpp",
"process.hpp",
"fileview.hpp",
"ipchandler.hpp",
]
-----