forked from quickshell/quickshell
		
	feat(socket): add SocketServer and Socket.write
This commit is contained in:
		
							parent
							
								
									f004454395
								
							
						
					
					
						commit
						f45d298b66
					
				
					 5 changed files with 248 additions and 7 deletions
				
			
		| 
						 | 
				
			
			@ -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<PostReloadHook*>(root)) {
 | 
			
		||||
		self->onPostReload();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -111,3 +111,18 @@ private:
 | 
			
		|||
 | 
			
		||||
	QList<QObject*> 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);
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,8 +7,10 @@
 | 
			
		|||
#include <qobject.h>
 | 
			
		||||
#include <qqmlcomponent.h>
 | 
			
		||||
#include <qqmlengine.h>
 | 
			
		||||
#include <qtimer.h>
 | 
			
		||||
#include <qurl.h>
 | 
			
		||||
 | 
			
		||||
#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();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +1,24 @@
 | 
			
		|||
#include "socket.hpp"
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
#include <qfile.h>
 | 
			
		||||
#include <qlocalserver.h>
 | 
			
		||||
#include <qlocalsocket.h>
 | 
			
		||||
#include <qlogging.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qqmlcomponent.h>
 | 
			
		||||
#include <qqmlengine.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
 | 
			
		||||
#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<Socket*>(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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,15 @@
 | 
			
		|||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <qlocalserver.h>
 | 
			
		||||
#include <qlocalsocket.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qqmlcomponent.h>
 | 
			
		||||
#include <qqmlintegration.h>
 | 
			
		||||
#include <qtclasshelpermacros.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
 | 
			
		||||
#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<Socket*> mSockets;
 | 
			
		||||
	bool activeTarget = false;
 | 
			
		||||
	bool postReload = false;
 | 
			
		||||
	QString mPath;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue