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 untrusted user: 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";