From d03c59768c680f052dff6e7a7918bbf990b0f743 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 15 Jan 2026 23:04:10 -0800 Subject: [PATCH] io/ipchandler: add signal listener support --- changelog/next.md | 2 + src/io/CMakeLists.txt | 2 +- src/io/ipc.cpp | 12 ++++ src/io/ipc.hpp | 25 ++++++-- src/io/ipccomm.cpp | 108 ++++++++++++++++++++++++++++++-- src/io/ipccomm.hpp | 50 +++++++++++++++ src/io/ipchandler.cpp | 120 +++++++++++++++++++++++++++++++++--- src/io/ipchandler.hpp | 82 +++++++++++++++++++++++- src/ipc/ipc.cpp | 6 ++ src/ipc/ipccommand.hpp | 1 + src/launch/command.cpp | 4 ++ src/launch/launch_p.hpp | 2 + src/launch/parsecommand.cpp | 11 ++++ 13 files changed, 405 insertions(+), 20 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 0cdff57..cab03e6 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -22,6 +22,7 @@ set shell id. - Added pipewire audio peak detection. - Added initial support for network management. - Added support for grabbing focus from popup windows. +- Added support for IPC signal listeners. ## Other Changes @@ -40,6 +41,7 @@ set shell id. - Fixed missing signals for system tray item title and description updates. - Fixed asynchronous loaders not working after reload. - Fixed asynchronous loaders not working before window creation. +- Fixed memory leak in IPC handlers. ## Packaging Changes diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt index 17628d3..991beaa 100644 --- a/src/io/CMakeLists.txt +++ b/src/io/CMakeLists.txt @@ -24,7 +24,7 @@ qt_add_qml_module(quickshell-io qs_add_module_deps_light(quickshell-io Quickshell) install_qml_module(quickshell-io) -target_link_libraries(quickshell-io PRIVATE Qt::Quick) +target_link_libraries(quickshell-io PRIVATE Qt::Quick quickshell-ipc) target_link_libraries(quickshell PRIVATE quickshell-ioplugin) qs_module_pch(quickshell-io) diff --git a/src/io/ipc.cpp b/src/io/ipc.cpp index 768299e..c381567 100644 --- a/src/io/ipc.cpp +++ b/src/io/ipc.cpp @@ -190,6 +190,14 @@ QString WirePropertyDefinition::toString() const { return "property " % this->name % ": " % this->type; } +QString WireSignalDefinition::toString() const { + if (this->rettype.isEmpty()) { + return "signal " % this->name % "()"; + } else { + return "signal " % this->name % "(" % this->retname % ": " % this->rettype % ')'; + } +} + QString WireTargetDefinition::toString() const { QString accum = "target " % this->name; @@ -201,6 +209,10 @@ QString WireTargetDefinition::toString() const { accum += "\n " % prop.toString(); } + for (const auto& sig: this->signalFunctions) { + accum += "\n " % sig.toString(); + } + return accum; } diff --git a/src/io/ipc.hpp b/src/io/ipc.hpp index d2b865a..32486d6 100644 --- a/src/io/ipc.hpp +++ b/src/io/ipc.hpp @@ -146,14 +146,31 @@ struct WirePropertyDefinition { DEFINE_SIMPLE_DATASTREAM_OPS(WirePropertyDefinition, data.name, data.type); -struct WireTargetDefinition { +struct WireSignalDefinition { QString name; - QVector functions; - QVector properties; + QString retname; + QString rettype; [[nodiscard]] QString toString() const; }; -DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions, data.properties); +DEFINE_SIMPLE_DATASTREAM_OPS(WireSignalDefinition, data.name, data.retname, data.rettype); + +struct WireTargetDefinition { + QString name; + QVector functions; + QVector properties; + QVector signalFunctions; + + [[nodiscard]] QString toString() const; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS( + WireTargetDefinition, + data.name, + data.functions, + data.properties, + data.signalFunctions +); } // namespace qs::io::ipc diff --git a/src/io/ipccomm.cpp b/src/io/ipccomm.cpp index 6c7e4f6..03b688a 100644 --- a/src/io/ipccomm.cpp +++ b/src/io/ipccomm.cpp @@ -1,9 +1,11 @@ #include "ipccomm.hpp" +#include #include #include #include #include +#include #include #include @@ -18,10 +20,6 @@ using namespace qs::ipc; namespace qs::io::ipc::comm { -struct NoCurrentGeneration: std::monostate {}; -struct TargetNotFound: std::monostate {}; -struct EntryNotFound: std::monostate {}; - using QueryResponse = std::variant< std::monostate, NoCurrentGeneration, @@ -313,4 +311,106 @@ int getProperty(IpcClient* client, const QString& target, const QString& propert return -1; } +int listenToSignal(IpcClient* client, const QString& target, const QString& signal, bool once) { + if (target.isEmpty()) { + qCCritical(logBare) << "Target required to listen for signals."; + return -1; + } else if (signal.isEmpty()) { + qCCritical(logBare) << "Signal required to listen."; + return -1; + } + + client->sendMessage(IpcCommand(SignalListenCommand {.target = target, .signal = signal})); + + while (true) { + SignalListenResponse slot; + if (!client->waitForResponse(slot)) return -1; + + if (std::holds_alternative(slot)) { + auto& result = std::get(slot); + QTextStream(stdout) << result.response << Qt::endl; + if (once) return 0; + else continue; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Target not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Signal 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; + } + break; + } + + return -1; +} + +void SignalListenCommand::exec(qs::ipc::IpcServerConnection* conn) { + 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* signal = handler->findSignal(this->signal); + if (!signal) { + resp << EntryNotFound(); + return; + } + + new RemoteSignalListener(conn, *this); + } else { + conn->respond(SignalListenResponse(NoCurrentGeneration())); + } +} + +RemoteSignalListener::RemoteSignalListener( + qs::ipc::IpcServerConnection* conn, + SignalListenCommand command +) + : conn(conn) + , command(std::move(command)) { + conn->setParent(this); + + QObject::connect( + IpcSignalRemoteListener::instance(), + &IpcSignalRemoteListener::triggered, + this, + &RemoteSignalListener::onSignal + ); + + QObject::connect( + conn, + &qs::ipc::IpcServerConnection::destroyed, + this, + &RemoteSignalListener::onConnDestroyed + ); + + qCDebug(logIpc) << "Remote listener created for" << this->command.target << this->command.signal + << ":" << this; +} + +RemoteSignalListener::~RemoteSignalListener() { + qCDebug(logIpc) << "Destroying remote listener" << this; +} + +void RemoteSignalListener::onSignal( + const QString& target, + const QString& signal, + const QString& value +) { + if (target != this->command.target || signal != this->command.signal) return; + qCDebug(logIpc) << "Remote signal" << signal << "triggered on" << target << "with value" << value; + + this->conn->respond(SignalListenResponse(SignalResponse {.response = value})); +} + +void RemoteSignalListener::onConnDestroyed() { this->deleteLater(); } + } // namespace qs::io::ipc::comm diff --git a/src/io/ipccomm.hpp b/src/io/ipccomm.hpp index bc7dbf9..ac12979 100644 --- a/src/io/ipccomm.hpp +++ b/src/io/ipccomm.hpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include "../ipc/ipc.hpp" @@ -48,4 +50,52 @@ DEFINE_SIMPLE_DATASTREAM_OPS(StringPropReadCommand, data.target, data.property); int getProperty(qs::ipc::IpcClient* client, const QString& target, const QString& property); +struct SignalListenCommand { + QString target; + QString signal; + + void exec(qs::ipc::IpcServerConnection* conn); +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(SignalListenCommand, data.target, data.signal); + +int listenToSignal( + qs::ipc::IpcClient* client, + const QString& target, + const QString& signal, + bool once +); + +struct NoCurrentGeneration: std::monostate {}; +struct TargetNotFound: std::monostate {}; +struct EntryNotFound: std::monostate {}; + +struct SignalResponse { + QString response; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(SignalResponse, data.response); + +using SignalListenResponse = std:: + variant; + +class RemoteSignalListener: public QObject { + Q_OBJECT; + +public: + explicit RemoteSignalListener(qs::ipc::IpcServerConnection* conn, SignalListenCommand command); + + ~RemoteSignalListener() override; + + Q_DISABLE_COPY_MOVE(RemoteSignalListener); + +private slots: + void onSignal(const QString& target, const QString& signal, const QString& value); + void onConnDestroyed(); + +private: + qs::ipc::IpcServerConnection* conn; + SignalListenCommand command; +}; + } // namespace qs::io::ipc::comm diff --git a/src/io/ipchandler.cpp b/src/io/ipchandler.cpp index 5ffa0ad..e80cf4b 100644 --- a/src/io/ipchandler.cpp +++ b/src/io/ipchandler.cpp @@ -1,5 +1,7 @@ #include "ipchandler.hpp" #include +#include +#include #include #include @@ -139,6 +141,75 @@ WirePropertyDefinition IpcProperty::wireDef() const { return wire; } +WireSignalDefinition IpcSignal::wireDef() const { + WireSignalDefinition wire; + wire.name = this->signal.name(); + if (this->targetSlot != IpcSignalListener::SLOT_VOID) { + wire.retname = this->signal.parameterNames().value(0); + if (this->targetSlot == IpcSignalListener::SLOT_STRING) wire.rettype = "string"; + else if (this->targetSlot == IpcSignalListener::SLOT_INT) wire.rettype = "int"; + else if (this->targetSlot == IpcSignalListener::SLOT_BOOL) wire.rettype = "bool"; + else if (this->targetSlot == IpcSignalListener::SLOT_REAL) wire.rettype = "real"; + else if (this->targetSlot == IpcSignalListener::SLOT_COLOR) wire.rettype = "color"; + } + return wire; +} + +// NOLINTBEGIN (cppcoreguidelines-interfaces-global-init) +// clang-format off +const int IpcSignalListener::SLOT_VOID = IpcSignalListener::staticMetaObject.indexOfSlot("invokeVoid()"); +const int IpcSignalListener::SLOT_STRING = IpcSignalListener::staticMetaObject.indexOfSlot("invokeString(QString)"); +const int IpcSignalListener::SLOT_INT = IpcSignalListener::staticMetaObject.indexOfSlot("invokeInt(int)"); +const int IpcSignalListener::SLOT_BOOL = IpcSignalListener::staticMetaObject.indexOfSlot("invokeBool(bool)"); +const int IpcSignalListener::SLOT_REAL = IpcSignalListener::staticMetaObject.indexOfSlot("invokeReal(double)"); +const int IpcSignalListener::SLOT_COLOR = IpcSignalListener::staticMetaObject.indexOfSlot("invokeColor(QColor)"); +// clang-format on +// NOLINTEND + +bool IpcSignal::resolve(QString& error) { + if (this->signal.parameterCount() > 1) { + error = "Due to technical limitations, IPC signals can have at most one argument."; + return false; + } + + auto slot = IpcSignalListener::SLOT_VOID; + + if (this->signal.parameterCount() == 1) { + auto paramType = this->signal.parameterType(0); + if (paramType == QMetaType::QString) slot = IpcSignalListener::SLOT_STRING; + else if (paramType == QMetaType::Int) slot = IpcSignalListener::SLOT_INT; + else if (paramType == QMetaType::Bool) slot = IpcSignalListener::SLOT_BOOL; + else if (paramType == QMetaType::Double) slot = IpcSignalListener::SLOT_REAL; + else if (paramType == QMetaType::QColor) slot = IpcSignalListener::SLOT_COLOR; + else { + error = QString("Type of argument (%2: %3) cannot be used across IPC.") + .arg(this->signal.parameterNames().value(0)) + .arg(QMetaType(paramType).name()); + + return false; + } + } + + this->targetSlot = slot; + return true; +} + +void IpcSignal::connectListener(IpcHandler* handler) { + if (this->targetSlot == -1) { + qFatal() << "Tried to connect unresolved IPC signal"; + } + + this->listener = std::make_shared(this->signal.name()); + QMetaObject::connect(handler, this->signal.methodIndex(), this->listener.get(), this->targetSlot); + + QObject::connect( + this->listener.get(), + &IpcSignalListener::triggered, + handler, + &IpcHandler::onSignalTriggered + ); +} + IpcCallStorage::IpcCallStorage(const IpcFunction& function): returnSlot(function.returnType) { for (const auto& arg: function.argumentTypes) { this->argumentSlots.emplace_back(arg); @@ -172,16 +243,28 @@ void IpcHandler::onPostReload() { // 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; + if (method.methodType() == QMetaMethod::Slot) { + auto ipcFunc = IpcFunction(method); + QString error; - 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); + } + } else if (method.methodType() == QMetaMethod::Signal) { + qmlDebug(this) << "Signal detected: " << method.name(); + auto ipcSig = IpcSignal(method); + QString error; - if (!ipcFunc.resolve(error)) { - qmlWarning(this).nospace().noquote() - << "Error parsing function \"" << method.name() << "\": " << error; - } else { - this->functionMap.insert(method.name(), ipcFunc); + if (!ipcSig.resolve(error)) { + qmlWarning(this).nospace().noquote() + << "Error parsing signal \"" << method.name() << "\": " << error; + } else { + ipcSig.connectListener(this); + this->signalMap.emplace(method.name(), std::move(ipcSig)); + } } } @@ -222,6 +305,11 @@ IpcHandlerRegistry* IpcHandlerRegistry::forGeneration(EngineGeneration* generati return dynamic_cast(ext); } +void IpcHandler::onSignalTriggered(const QString& signal, const QString& value) const { + emit IpcSignalRemoteListener::instance() + -> triggered(this->registeredState.target, signal, value); +} + void IpcHandler::updateRegistration(bool destroying) { if (!this->complete) return; @@ -324,6 +412,10 @@ WireTargetDefinition IpcHandler::wireDef() const { wire.properties += prop.wireDef(); } + for (const auto& sig: this->signalMap.values()) { + wire.signalFunctions += sig.wireDef(); + } + return wire; } @@ -368,6 +460,13 @@ IpcProperty* IpcHandler::findProperty(const QString& name) { else return &*itr; } +IpcSignal* IpcHandler::findSignal(const QString& name) { + auto itr = this->signalMap.find(name); + + if (itr == this->signalMap.end()) return nullptr; + else return &*itr; +} + IpcHandler* IpcHandlerRegistry::findHandler(const QString& target) { return this->handlers.value(target); } @@ -382,4 +481,9 @@ QVector IpcHandlerRegistry::wireTargets() const { return wire; } +IpcSignalRemoteListener* IpcSignalRemoteListener::instance() { + static auto* instance = new IpcSignalRemoteListener(); + return instance; +} + } // namespace qs::io::ipc diff --git a/src/io/ipchandler.hpp b/src/io/ipchandler.hpp index 4c5d9bc..eb274e3 100644 --- a/src/io/ipchandler.hpp +++ b/src/io/ipchandler.hpp @@ -1,8 +1,10 @@ #pragma once #include +#include #include +#include #include #include #include @@ -67,6 +69,54 @@ public: const IpcType* type = nullptr; }; +class IpcSignalListener: public QObject { + Q_OBJECT; + +public: + IpcSignalListener(QString signal): signal(std::move(signal)) {} + + static const int SLOT_VOID; + static const int SLOT_STRING; + static const int SLOT_INT; + static const int SLOT_BOOL; + static const int SLOT_REAL; + static const int SLOT_COLOR; + +signals: + void triggered(const QString& signal, const QString& value); + +private slots: + void invokeVoid() { this->triggered(this->signal, "void"); } + void invokeString(const QString& value) { this->triggered(this->signal, value); } + void invokeInt(int value) { this->triggered(this->signal, QString::number(value)); } + void invokeBool(bool value) { this->triggered(this->signal, value ? "true" : "false"); } + void invokeReal(double value) { this->triggered(this->signal, QString::number(value)); } + void invokeColor(QColor value) { this->triggered(this->signal, value.name(QColor::HexArgb)); } + +private: + QString signal; +}; + +class IpcHandler; + +class IpcSignal { +public: + explicit IpcSignal(QMetaMethod signal): signal(signal) {} + + bool resolve(QString& error); + + [[nodiscard]] WireSignalDefinition wireDef() const; + + QMetaMethod signal; + int targetSlot = -1; + + void connectListener(IpcHandler* handler); + +private: + void connectListener(QObject* handler, IpcSignalListener* listener) const; + std::shared_ptr listener; +}; + class IpcHandlerRegistry; ///! Handler for IPC message calls. @@ -100,6 +150,11 @@ class IpcHandlerRegistry; /// - `real` will be converted to a string and returned. /// - `color` will be converted to a hex string in the form `#AARRGGBB` and returned. /// +/// #### Signals +/// IPC handler signals can be observed remotely using `qs ipc wait` (one call) +/// and `qs ipc listen` (many calls). IPC signals may have zero or one argument, where +/// the argument is one of the types listed above, or no arguments for void. +/// /// #### Example /// The following example creates ipc functions to control and retrieve the appearance /// of a Rectangle. @@ -119,10 +174,18 @@ class IpcHandlerRegistry; /// /// 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 setRadius(radius: int): void { +/// rect.radius = radius; +/// this.radiusChanged(radius); +/// } +/// /// function getRadius(): int { return rect.radius; } +/// +/// signal radiusChanged(newRadius: int); /// } /// } /// ``` @@ -136,6 +199,7 @@ class IpcHandlerRegistry; /// function getAngle(): real /// function setRadius(radius: int): void /// function getRadius(): int +/// signal radiusChanged(newRadius: int) /// ``` /// /// and then invoked using `qs ipc call`. @@ -179,14 +243,15 @@ public: QString listMembers(qsizetype indent); [[nodiscard]] IpcFunction* findFunction(const QString& name); [[nodiscard]] IpcProperty* findProperty(const QString& name); + [[nodiscard]] IpcSignal* findSignal(const QString& name); [[nodiscard]] WireTargetDefinition wireDef() const; signals: void enabledChanged(); void targetChanged(); -private slots: - //void handleIpcPropertyChange(); +public slots: + void onSignalTriggered(const QString& signal, const QString& value) const; private: void updateRegistration(bool destroying = false); @@ -204,6 +269,7 @@ private: QHash functionMap; QHash propertyMap; + QHash signalMap; friend class IpcHandlerRegistry; }; @@ -227,4 +293,14 @@ private: QHash> knownHandlers; }; +class IpcSignalRemoteListener: public QObject { + Q_OBJECT; + +public: + static IpcSignalRemoteListener* instance(); + +signals: + void triggered(const QString& target, const QString& signal, const QString& value); +}; + } // namespace qs::io::ipc diff --git a/src/ipc/ipc.cpp b/src/ipc/ipc.cpp index 0196359..32d8482 100644 --- a/src/ipc/ipc.cpp +++ b/src/ipc/ipc.cpp @@ -61,6 +61,7 @@ IpcServerConnection::IpcServerConnection(QLocalSocket* socket, IpcServer* server void IpcServerConnection::onDisconnected() { qCInfo(logIpc) << "IPC connection disconnected" << this; + delete this; } void IpcServerConnection::onReadyRead() { @@ -84,6 +85,11 @@ void IpcServerConnection::onReadyRead() { ); if (!this->stream.commitTransaction()) return; + + // async connections reparent + if (dynamic_cast(this->parent()) != nullptr) { + delete this; + } } IpcClient::IpcClient(const QString& path) { diff --git a/src/ipc/ipccommand.hpp b/src/ipc/ipccommand.hpp index b221b46..105ce1e 100644 --- a/src/ipc/ipccommand.hpp +++ b/src/ipc/ipccommand.hpp @@ -16,6 +16,7 @@ using IpcCommand = std::variant< IpcKillCommand, qs::io::ipc::comm::QueryMetadataCommand, qs::io::ipc::comm::StringCallCommand, + qs::io::ipc::comm::SignalListenCommand, qs::io::ipc::comm::StringPropReadCommand>; } // namespace qs::ipc diff --git a/src/launch/command.cpp b/src/launch/command.cpp index 1a58cb8..d867584 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -411,6 +411,10 @@ int ipcCommand(CommandState& cmd) { return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.name); } else if (*cmd.ipc.getprop) { return qs::io::ipc::comm::getProperty(&client, *cmd.ipc.target, *cmd.ipc.name); + } else if (*cmd.ipc.wait) { + return qs::io::ipc::comm::listenToSignal(&client, *cmd.ipc.target, *cmd.ipc.name, true); + } else if (*cmd.ipc.listen) { + return qs::io::ipc::comm::listenToSignal(&client, *cmd.ipc.target, *cmd.ipc.name, false); } else { QVector arguments; for (auto& arg: cmd.ipc.arguments) { diff --git a/src/launch/launch_p.hpp b/src/launch/launch_p.hpp index a186ddb..f666e7a 100644 --- a/src/launch/launch_p.hpp +++ b/src/launch/launch_p.hpp @@ -74,6 +74,8 @@ struct CommandState { CLI::App* show = nullptr; CLI::App* call = nullptr; CLI::App* getprop = nullptr; + CLI::App* wait = nullptr; + CLI::App* listen = nullptr; bool showOld = false; QStringOption target; QStringOption name; diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp index 0776f58..fc43b6b 100644 --- a/src/launch/parsecommand.cpp +++ b/src/launch/parsecommand.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include // NOLINT: Need to include this for impls of some CLI11 classes @@ -226,6 +227,16 @@ int parseCommand(int argc, char** argv, CommandState& state) { ->allow_extra_args(); } + auto signalCmd = [&](std::string cmd, std::string desc) { + auto* scmd = sub->add_subcommand(std::move(cmd), std::move(desc)); + scmd->add_option("target", state.ipc.target, "The target to listen on."); + scmd->add_option("signal", state.ipc.name, "The signal to listen for."); + return scmd; + }; + + state.ipc.wait = signalCmd("wait", "Wait for one IpcHandler signal."); + state.ipc.listen = signalCmd("listen", "Listen for IpcHandler signals."); + { auto* prop = sub->add_subcommand("prop", "Manipulate IpcHandler properties.")->require_subcommand();