From f45d298b6611f29a8b0d9022c91a18da5d9501bc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 3 Mar 2024 01:26:43 -0800 Subject: [PATCH] feat(socket): add SocketServer and Socket.write --- src/core/reload.cpp | 10 ++++ src/core/reload.hpp | 15 +++++ src/core/rootwrapper.cpp | 19 ++++-- src/core/socket.cpp | 126 ++++++++++++++++++++++++++++++++++++++- src/core/socket.hpp | 85 ++++++++++++++++++++++++++ 5 files changed, 248 insertions(+), 7 deletions(-) diff --git a/src/core/reload.cpp b/src/core/reload.cpp index 187fc7a..8768dc7 100644 --- a/src/core/reload.cpp +++ b/src/core/reload.cpp @@ -75,3 +75,13 @@ QObject* Reloadable::getChildByReloadId(QObject* parent, const QString& reloadId return nullptr; } + +void PostReloadHook::postReloadTree(QObject* root) { + for (auto* child: root->children()) { + PostReloadHook::postReloadTree(child); + } + + if (auto* self = dynamic_cast(root)) { + self->onPostReload(); + } +} diff --git a/src/core/reload.hpp b/src/core/reload.hpp index ff15fad..f757cc3 100644 --- a/src/core/reload.hpp +++ b/src/core/reload.hpp @@ -111,3 +111,18 @@ private: QList mChildren; }; + +/// Hook that runs after the old widget tree is dropped during a reload. +class PostReloadHook { +public: + PostReloadHook() = default; + virtual ~PostReloadHook() = default; + PostReloadHook(PostReloadHook&&) = default; + PostReloadHook(const PostReloadHook&) = default; + PostReloadHook& operator=(PostReloadHook&&) = default; + PostReloadHook& operator=(const PostReloadHook&) = default; + + virtual void onPostReload() = 0; + + static void postReloadTree(QObject* root); +}; diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index ca63c85..c88825d 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -7,8 +7,10 @@ #include #include #include +#include #include +#include "reload.hpp" #include "shell.hpp" #include "watcher.hpp" @@ -48,14 +50,21 @@ void RootWrapper::reloadGraph(bool hard) { component.completeCreate(); - newRoot->onReload(hard ? nullptr : this->root); + auto* oldRoot = this->root; + this->root = newRoot; - if (this->root != nullptr) { - this->root->deleteLater(); - this->root = nullptr; + this->root->onReload(hard ? nullptr : oldRoot); + + if (oldRoot != nullptr) { + oldRoot->deleteLater(); + + QTimer::singleShot(0, [this, newRoot]() { + if (this->root == newRoot) PostReloadHook::postReloadTree(this->root); + }); + } else { + PostReloadHook::postReloadTree(newRoot); } - this->root = newRoot; this->onConfigChanged(); } diff --git a/src/core/socket.cpp b/src/core/socket.cpp index 9610848..e2ecfb5 100644 --- a/src/core/socket.cpp +++ b/src/core/socket.cpp @@ -1,19 +1,24 @@ #include "socket.hpp" #include +#include +#include #include +#include #include +#include +#include #include #include "datastream.hpp" void Socket::setSocket(QLocalSocket* socket) { if (this->socket != nullptr) this->socket->deleteLater(); - this->socket = socket; - socket->setParent(this); if (socket != nullptr) { + socket->setParent(this); + // clang-format off QObject::connect(this->socket, &QLocalSocket::connected, this, &Socket::onSocketConnected); QObject::connect(this->socket, &QLocalSocket::disconnected, this, &Socket::onSocketDisconnected); @@ -75,3 +80,120 @@ void Socket::connectPathSocket() { this->socket->connectToServer(QIODevice::ReadWrite); } } + +void Socket::write(const QString& data) { + if (this->socket != nullptr) { + this->socket->write(data.toUtf8()); + } +} + +SocketServer::~SocketServer() { this->disableServer(); } + +void SocketServer::onPostReload() { + this->postReload = true; + if (this->isActivatable()) this->enableServer(); +} + +bool SocketServer::isActive() const { return this->server != nullptr; } + +void SocketServer::setActive(bool active) { + this->activeTarget = active; + if (active == (this->server != nullptr)) return; + + if (active) { + if (this->isActivatable()) this->enableServer(); + } else this->disableServer(); +} + +QString SocketServer::path() const { return this->mPath; } + +void SocketServer::setPath(QString path) { + if (this->mPath == path) return; + this->mPath = std::move(path); + emit this->pathChanged(); + + if (this->isActivatable()) this->enableServer(); +} + +QQmlComponent* SocketServer::handler() const { return this->mHandler; } + +void SocketServer::setHandler(QQmlComponent* handler) { + if (this->mHandler != nullptr) this->mHandler->deleteLater(); + this->mHandler = handler; + + if (handler != nullptr) { + handler->setParent(this); + } +} + +bool SocketServer::isActivatable() { + return this->server == nullptr && this->postReload && this->activeTarget && !this->mPath.isEmpty() + && this->handler() != nullptr; +} + +void SocketServer::enableServer() { + this->disableServer(); + + this->server = new QLocalServer(this); + QObject::connect( + this->server, + &QLocalServer::newConnection, + this, + &SocketServer::onNewConnection + ); + + if (!this->server->listen(this->mPath)) { + qWarning() << "could not start socket server at" << this->mPath; + this->disableServer(); + } + + this->activeTarget = false; + emit this->activeStatusChanged(); +} + +void SocketServer::disableServer() { + auto wasActive = this->server != nullptr; + + if (this->server != nullptr) { + for (auto* socket: this->mSockets) { + socket->deleteLater(); + } + + this->mSockets.clear(); + this->server->deleteLater(); + this->server = nullptr; + } + + if (this->mPath != nullptr) { + if (QFile::exists(this->mPath) && !QFile::remove(this->mPath)) { + qWarning() << "failed to delete socket file at" << this->mPath; + } + } + + if (wasActive) emit this->activeStatusChanged(); +} + +void SocketServer::onNewConnection() { + if (auto* connection = this->server->nextPendingConnection()) { + auto* instanceObj = this->mHandler->create(QQmlEngine::contextForObject(this)); + auto* instance = qobject_cast(instanceObj); + + if (instance == nullptr) { + qWarning() << "SocketServer.handler does not create a Socket. Dropping connection."; + if (instanceObj != nullptr) instanceObj->deleteLater(); + connection->deleteLater(); + return; + } + + this->mSockets.append(instance); + instance->setParent(this); + + if (instance->isConnected()) { + qWarning() << "SocketServer.handler created a socket with an existing connection. Dropping " + "new connection."; + connection->deleteLater(); + } else { + instance->setSocket(connection); + } + } +} diff --git a/src/core/socket.hpp b/src/core/socket.hpp index 9742e1d..6a390b0 100644 --- a/src/core/socket.hpp +++ b/src/core/socket.hpp @@ -1,10 +1,15 @@ #pragma once +#include #include #include +#include +#include +#include #include #include "datastream.hpp" +#include "reload.hpp" ///! Unix socket listener. class Socket: public DataStream { @@ -24,6 +29,9 @@ class Socket: public DataStream { public: explicit Socket(QObject* parent = nullptr): DataStream(parent) {} + /// Write data to the socket. Does nothing if not connected. + Q_INVOKABLE void write(const QString& data); + // takes ownership void setSocket(QLocalSocket* socket); @@ -56,3 +64,80 @@ private: bool targetConnected = false; QString mPath; }; + +///! Unix socket server. +/// #### Example +/// ```qml +/// SocketServer { +/// active: true +/// path: "/path/too/socket.sock" +/// handler: Socket { +/// onConnectedChanged: { +/// console.log(connected ? "new connection!" : "connection dropped!") +/// } +/// parser: SplitParser { +/// onRead: message => console.log(`read message from socket: ${message}`) +/// } +/// } +/// } +/// ``` +class SocketServer + : public QObject + , public PostReloadHook { + Q_OBJECT; + /// If the socket server is currently active. Defaults to false. + /// + /// Setting this to false will destory all active connections and delete + /// the socket file on disk. + /// + /// If path is empty setting this property will have no effect. + Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeStatusChanged); + /// The path to create the socket server at. + /// + /// Setting this property while the server is active will have no effect. + Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged); + /// Connection handler component. Must creeate a `Socket`. + /// + /// The created socket should not set `connected` or `path` or the incoming + /// socket connection will be dropped (they will be set by the socket server.) + /// Setting `connected` to false on the created socket after connection will + /// close and delete it. + Q_PROPERTY(QQmlComponent* handler READ handler WRITE setHandler NOTIFY handlerChanged); + QML_ELEMENT; + +public: + explicit SocketServer(QObject* parent = nullptr): QObject(parent) {} + ~SocketServer() override; + Q_DISABLE_COPY_MOVE(SocketServer); + + void onPostReload() override; + + [[nodiscard]] bool isActive() const; + void setActive(bool active); + + [[nodiscard]] QString path() const; + void setPath(QString path); + + [[nodiscard]] QQmlComponent* handler() const; + void setHandler(QQmlComponent* handler); + +signals: + void activeStatusChanged(); + void pathChanged(); + void handlerChanged(); + +private slots: + void onNewConnection(); + +private: + bool isActivatable(); + void enableServer(); + void disableServer(); + + QLocalServer* server = nullptr; + QQmlComponent* mHandler = nullptr; + QList mSockets; + bool activeTarget = false; + bool postReload = false; + QString mPath; +};