From 2e130745801f40a9fe28bea61fd11e87ecbfd27a Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Fri, 5 Dec 2025 16:48:29 +0000 Subject: [PATCH] refactor(ipc): Abstract IPC server logic --- src/lib/deskflow/CMakeLists.txt | 2 + src/lib/deskflow/ipc/DaemonIpcServer.cpp | 133 ++++------------------- src/lib/deskflow/ipc/DaemonIpcServer.h | 40 ++----- src/lib/deskflow/ipc/IpcServer.cpp | 116 ++++++++++++++++++++ src/lib/deskflow/ipc/IpcServer.h | 56 ++++++++++ 5 files changed, 202 insertions(+), 145 deletions(-) create mode 100644 src/lib/deskflow/ipc/IpcServer.cpp create mode 100644 src/lib/deskflow/ipc/IpcServer.h diff --git a/src/lib/deskflow/CMakeLists.txt b/src/lib/deskflow/CMakeLists.txt index 46da197eb..e224ab28f 100644 --- a/src/lib/deskflow/CMakeLists.txt +++ b/src/lib/deskflow/CMakeLists.txt @@ -79,6 +79,8 @@ add_library(${lib_name} STATIC ${PLATFORM_CODE} StreamChunker.h languages/LanguageManager.cpp languages/LanguageManager.h + ipc/IpcServer.cpp + ipc/IpcServer.h ipc/DaemonIpcServer.cpp ipc/DaemonIpcServer.h ) diff --git a/src/lib/deskflow/ipc/DaemonIpcServer.cpp b/src/lib/deskflow/ipc/DaemonIpcServer.cpp index c719dd291..7d6c40f27 100644 --- a/src/lib/deskflow/ipc/DaemonIpcServer.cpp +++ b/src/lib/deskflow/ipc/DaemonIpcServer.cpp @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2025 Symless Ltd. + * SPDX-FileCopyrightText: (C) 2025-2026 Symless Ltd. * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception */ @@ -9,7 +9,6 @@ #include "base/Log.h" #include "common/Constants.h" -#include #include namespace deskflow::core::ipc { @@ -18,107 +17,28 @@ const auto kAckMessage = "ok"; const auto kErrorMessage = "error"; DaemonIpcServer::DaemonIpcServer(QObject *parent, const QString &logFilename) - : QObject(parent), - m_logFilename(logFilename), - m_server{new QLocalServer(this)} // NOSONAR - Qt memory + : IpcServer(parent, kDaemonIpcName), + m_logFilename(logFilename) { // do nothing } -DaemonIpcServer::~DaemonIpcServer() -{ - m_server->close(); -} - -void DaemonIpcServer::listen() -{ - // Daemon runs as system, but GUI runs as regular user, so we need to allow world access. - m_server->setSocketOptions(QLocalServer::WorldAccessOption); - - connect(m_server, &QLocalServer::newConnection, this, &DaemonIpcServer::handleNewConnection); - QLocalServer::removeServer(kDaemonIpcName); - if (m_server->listen(kDaemonIpcName)) { - LOG_DEBUG("ipc server listening on: %s", kDaemonIpcName); - } else { - LOG_ERR("ipc server failed to listen on: %s", kDaemonIpcName); - } -} - -void DaemonIpcServer::handleNewConnection() -{ - QLocalSocket *clientSocket = m_server->nextPendingConnection(); - if (!clientSocket) { - LOG_ERR("ipc server failed to get new connection"); - return; - } - - LOG_DEBUG("ipc server got new connection"); - m_clients.insert(clientSocket); - - connect(clientSocket, &QLocalSocket::readyRead, this, &DaemonIpcServer::handleReadyRead); - connect(clientSocket, &QLocalSocket::disconnected, this, &DaemonIpcServer::handleDisconnected); - connect(clientSocket, &QLocalSocket::errorOccurred, this, &DaemonIpcServer::handleErrorOccurred); -} - -void DaemonIpcServer::handleReadyRead() -{ - const auto clientSocket = qobject_cast(sender()); - LOG_DEBUG1("ipc server ready to read data"); - - QByteArray data = clientSocket->readAll(); - if (data.isEmpty()) { - LOG_WARN("ipc server got empty message"); - return; - } - - // we don't handle incomplete messages yet; each socket read must have delimiters. - if (!data.contains('\n')) { - LOG_WARN("ipc server got incomplete message: %s", data.constData()); - return; - } - - // each message is delimited by a newline to keep the protocol super simple. - while (data.contains('\n')) { - const auto index = data.indexOf('\n'); - QByteArray messageData = data.left(index); - data.remove(0, index + 1); - QString message = QString::fromUtf8(messageData); - processMessage(clientSocket, message); - } -} - -void DaemonIpcServer::handleDisconnected() -{ - const auto clientSocket = qobject_cast(sender()); - LOG_DEBUG("ipc server client disconnected"); - m_clients.remove(clientSocket); - clientSocket->deleteLater(); -} - -void DaemonIpcServer::handleErrorOccurred() -{ - const auto clientSocket = qobject_cast(sender()); - LOG_ERR("ipc server client error: %s", clientSocket->errorString().toUtf8().constData()); - m_clients.remove(clientSocket); - clientSocket->deleteLater(); -} - void DaemonIpcServer::processMessage(QLocalSocket *clientSocket, const QString &message) { - LOG_DEBUG1("ipc server got message: %s", message.toUtf8().constData()); + LOG_DEBUG1("daemon ipc server got message: %s", message.toUtf8().constData()); const auto parts = message.split('='); if (parts.size() < 1) { - LOG_ERR("ipc server got invalid message: %s", message.toUtf8().constData()); + LOG_ERR("daemon ipc server got invalid message: %s", message.toUtf8().constData()); writeToClientSocket(clientSocket, kErrorMessage); return; } const auto &command = parts[0]; if (command == "hello") { // NOSONAR - if-init is confusing here - LOG_DEBUG("ipc server got hello message, sending hello back"); + LOG_DEBUG("daemon ipc server got hello message, sending hello back"); writeToClientSocket(clientSocket, "hello"); } else if (command == "noop") { - LOG_DEBUG("ipc server got noop message"); + LOG_DEBUG("daemon ipc server got noop message"); writeToClientSocket(clientSocket, kAckMessage); } else if (command == "logLevel") { processLogLevel(clientSocket, parts); @@ -127,22 +47,22 @@ void DaemonIpcServer::processMessage(QLocalSocket *clientSocket, const QString & } else if (command == "command") { processCommand(clientSocket, parts); } else if (command == "start") { - LOG_DEBUG("ipc server got start message"); + LOG_DEBUG("daemon ipc server got start message"); Q_EMIT startProcessRequested(); writeToClientSocket(clientSocket, kAckMessage); } else if (command == "stop") { - LOG_DEBUG("ipc server got stop message"); + LOG_DEBUG("daemon ipc server got stop message"); Q_EMIT stopProcessRequested(); writeToClientSocket(clientSocket, kAckMessage); } else if (command == "logPath") { - LOG_DEBUG("ipc server got log path request"); + LOG_DEBUG("daemon ipc server got log path request"); writeToClientSocket(clientSocket, "logPath=" + m_logFilename.toUtf8()); } else if (command == "clearSettings") { - LOG_DEBUG("ipc server got clear settings message"); + LOG_DEBUG("daemon ipc server got clear settings message"); Q_EMIT clearSettingsRequested(); writeToClientSocket(clientSocket, kAckMessage); } else { - LOG_WARN("ipc server got unknown message: %s", message.toUtf8().constData()); + LOG_WARN("daemon ipc server got unknown message: %s", message.toUtf8().constData()); } clientSocket->flush(); @@ -151,19 +71,19 @@ void DaemonIpcServer::processMessage(QLocalSocket *clientSocket, const QString & void DaemonIpcServer::processLogLevel(QLocalSocket *&clientSocket, const QStringList &messageParts) { if (messageParts.size() < 2) { - LOG_ERR("ipc server got invalid log level message"); + LOG_ERR("daemon ipc server got invalid log level message"); writeToClientSocket(clientSocket, kErrorMessage); return; } const auto &logLevel = messageParts[1]; if (logLevel.isEmpty()) { - LOG_ERR("ipc server got empty log level"); + LOG_ERR("daemon ipc server got empty log level"); writeToClientSocket(clientSocket, kErrorMessage); return; } - LOG_DEBUG("ipc server got new log level: %s", logLevel.toUtf8().constData()); + LOG_DEBUG("daemon ipc server got new log level: %s", logLevel.toUtf8().constData()); Q_EMIT logLevelChanged(logLevel); writeToClientSocket(clientSocket, kAckMessage); } @@ -171,19 +91,19 @@ void DaemonIpcServer::processLogLevel(QLocalSocket *&clientSocket, const QString void DaemonIpcServer::processElevate(QLocalSocket *&clientSocket, const QStringList &messageParts) { if (messageParts.size() < 2) { - LOG_ERR("ipc server got invalid elevate message"); + LOG_ERR("daemon ipc server got invalid elevate message"); writeToClientSocket(clientSocket, kErrorMessage); return; } const auto &elevate = messageParts[1]; if (elevate != "yes" && elevate != "no") { - LOG_ERR("ipc server got invalid elevate value: %s", elevate.toUtf8().constData()); + LOG_ERR("daemon ipc server got invalid elevate value: %s", elevate.toUtf8().constData()); writeToClientSocket(clientSocket, kErrorMessage); return; } - LOG_DEBUG("ipc server got new elevate value: %s", elevate.toUtf8().constData()); + LOG_DEBUG("daemon ipc server got new elevate value: %s", elevate.toUtf8().constData()); Q_EMIT elevateModeChanged(elevate == "yes"); writeToClientSocket(clientSocket, kAckMessage); } @@ -191,32 +111,21 @@ void DaemonIpcServer::processElevate(QLocalSocket *&clientSocket, const QStringL void DaemonIpcServer::processCommand(QLocalSocket *&clientSocket, const QStringList &messageParts) { if (messageParts.size() < 2) { - LOG_ERR("ipc server got invalid command message"); + LOG_ERR("daemon ipc server got invalid command message"); writeToClientSocket(clientSocket, kErrorMessage); return; } const auto &command = messageParts[1]; if (command.isEmpty()) { - LOG_ERR("ipc server got empty command"); + LOG_ERR("daemon ipc server got empty command"); writeToClientSocket(clientSocket, kErrorMessage); return; } - LOG_DEBUG("ipc server got new command: %s", command.toUtf8().constData()); + LOG_DEBUG("daemon ipc server got new command: %s", command.toUtf8().constData()); Q_EMIT commandChanged(command); writeToClientSocket(clientSocket, kAckMessage); } -void DaemonIpcServer::writeToClientSocket(QLocalSocket *&clientSocket, const QString &message) const -{ - QByteArray messageData = message.toUtf8() + '\n'; - qint64 bytesWritten = clientSocket->write(messageData); - if (bytesWritten != messageData.size()) { - LOG_ERR("ipc server failed to write full message to client socket"); - } else { - LOG_DEBUG1("ipc server wrote message to client socket: %s", message.toUtf8().constData()); - } -} - } // namespace deskflow::core::ipc diff --git a/src/lib/deskflow/ipc/DaemonIpcServer.h b/src/lib/deskflow/ipc/DaemonIpcServer.h index 3bcebbf2d..f92cdac24 100644 --- a/src/lib/deskflow/ipc/DaemonIpcServer.h +++ b/src/lib/deskflow/ipc/DaemonIpcServer.h @@ -1,61 +1,35 @@ /* * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2025 Symless Ltd. + * SPDX-FileCopyrightText: (C) 2025-2026 Symless Ltd. * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception */ #pragma once -#include -#include +#include "IpcServer.h" + +#include +#include -class QLocalServer; class QLocalSocket; namespace deskflow::core::ipc { -class DaemonIpcServer : public QObject +class DaemonIpcServer : public IpcServer { Q_OBJECT public: explicit DaemonIpcServer(QObject *parent, const QString &logFilename); - ~DaemonIpcServer() override; - - void listen(); - -Q_SIGNALS: - void logLevelChanged(const QString &logLevel); - void elevateModeChanged(bool elevate); - void commandChanged(const QString &command); - void startProcessRequested(); - void stopProcessRequested(); - void clearSettingsRequested(); private: - void processMessage(QLocalSocket *clientSocket, const QString &message); + void processMessage(QLocalSocket *clientSocket, const QString &message) override; void processLogLevel(QLocalSocket *&clientSocket, const QStringList &messageParts); void processElevate(QLocalSocket *&clientSocket, const QStringList &messageParts); void processCommand(QLocalSocket *&clientSocket, const QStringList &messageParts); - /**! - * Write a message to the client socket and append a newline character. - * - * \param clientSocket The client socket to write to. - * \param message The message to write (without trailing newline). - */ - void writeToClientSocket(QLocalSocket *&clientSocket, const QString &message) const; - -private Q_SLOTS: - void handleNewConnection(); - void handleReadyRead(); - void handleDisconnected(); - void handleErrorOccurred(); - private: const QString m_logFilename; - QLocalServer *m_server; - QSet m_clients; }; } // namespace deskflow::core::ipc diff --git a/src/lib/deskflow/ipc/IpcServer.cpp b/src/lib/deskflow/ipc/IpcServer.cpp new file mode 100644 index 000000000..93d1011d8 --- /dev/null +++ b/src/lib/deskflow/ipc/IpcServer.cpp @@ -0,0 +1,116 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2025-2026 Symless Ltd. + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#include "IpcServer.h" + +#include "base/Log.h" + +#include +#include + +namespace deskflow::core::ipc { + +const auto kAckMessage = "ok"; +const auto kErrorMessage = "error"; + +IpcServer::IpcServer(QObject *parent, const QString &serverName) + : QObject(parent), + m_server{new QLocalServer(this)}, // NOSONAR - Qt memory + m_serverName(serverName) +{ + // do nothing +} + +IpcServer::~IpcServer() +{ + m_server->close(); +} + +void IpcServer::listen() +{ + // IPC server normally runs as system, but GUI runs as regular user, so we need to allow world access. + m_server->setSocketOptions(QLocalServer::WorldAccessOption); + + connect(m_server, &QLocalServer::newConnection, this, &IpcServer::handleNewConnection); + QLocalServer::removeServer(m_serverName); + if (m_server->listen(m_serverName)) { + LOG_DEBUG("ipc server listening on: %s", m_serverName.toUtf8().constData()); + } else { + LOG_ERR("ipc server failed to listen on: %s", m_serverName.toUtf8().constData()); + } +} + +void IpcServer::handleNewConnection() +{ + QLocalSocket *clientSocket = m_server->nextPendingConnection(); + if (!clientSocket) { + LOG_ERR("ipc server failed to get new connection"); + return; + } + + LOG_DEBUG("ipc server got new connection"); + m_clients.insert(clientSocket); + + connect(clientSocket, &QLocalSocket::readyRead, this, &IpcServer::handleReadyRead); + connect(clientSocket, &QLocalSocket::disconnected, this, &IpcServer::handleDisconnected); + connect(clientSocket, &QLocalSocket::errorOccurred, this, &IpcServer::handleErrorOccurred); +} + +void IpcServer::handleReadyRead() +{ + const auto clientSocket = qobject_cast(sender()); + LOG_DEBUG1("ipc server ready to read data"); + + QByteArray data = clientSocket->readAll(); + if (data.isEmpty()) { + LOG_WARN("ipc server got empty message"); + return; + } + + // we don't handle incomplete messages yet; each socket read must have delimiters. + if (!data.contains('\n')) { + LOG_WARN("ipc server got incomplete message: %s", data.constData()); + return; + } + + // each message is delimited by a newline to keep the protocol super simple. + while (data.contains('\n')) { + const auto index = data.indexOf('\n'); + QByteArray messageData = data.left(index); + data.remove(0, index + 1); + QString message = QString::fromUtf8(messageData); + processMessage(clientSocket, message); + } +} + +void IpcServer::handleDisconnected() +{ + const auto clientSocket = qobject_cast(sender()); + LOG_DEBUG("ipc server client disconnected"); + m_clients.remove(clientSocket); + clientSocket->deleteLater(); +} + +void IpcServer::handleErrorOccurred() +{ + const auto clientSocket = qobject_cast(sender()); + LOG_ERR("ipc server client error: %s", clientSocket->errorString().toUtf8().constData()); + m_clients.remove(clientSocket); + clientSocket->deleteLater(); +} + +void IpcServer::writeToClientSocket(QLocalSocket *&clientSocket, const QString &message) const +{ + QByteArray messageData = message.toUtf8() + '\n'; + qint64 bytesWritten = clientSocket->write(messageData); + if (bytesWritten != messageData.size()) { + LOG_ERR("ipc server failed to write full message to client socket"); + } else { + LOG_DEBUG1("ipc server wrote message to client socket: %s", message.toUtf8().constData()); + } +} + +} // namespace deskflow::core::ipc diff --git a/src/lib/deskflow/ipc/IpcServer.h b/src/lib/deskflow/ipc/IpcServer.h new file mode 100644 index 000000000..f08cfa712 --- /dev/null +++ b/src/lib/deskflow/ipc/IpcServer.h @@ -0,0 +1,56 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2025-2026 Symless Ltd. + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#pragma once + +#include +#include + +class QLocalServer; +class QLocalSocket; + +namespace deskflow::core::ipc { + +class IpcServer : public QObject +{ + Q_OBJECT + +public: + explicit IpcServer(QObject *parent, const QString &serverName); + ~IpcServer() override; + + void listen(); + +Q_SIGNALS: + void logLevelChanged(const QString &logLevel); + void elevateModeChanged(bool elevate); + void commandChanged(const QString &command); + void startProcessRequested(); + void stopProcessRequested(); + void clearSettingsRequested(); + +protected: + /**! + * Write a message to the client socket and append a newline character. + * + * \param clientSocket The client socket to write to. + * \param message The message to write (without trailing newline). + */ + void writeToClientSocket(QLocalSocket *&clientSocket, const QString &message) const; + +private: + virtual void processMessage(QLocalSocket *clientSocket, const QString &message) = 0; + void handleNewConnection(); + void handleReadyRead(); + void handleDisconnected(); + void handleErrorOccurred(); + + QLocalServer *m_server; + QSet m_clients; + QString m_serverName; +}; + +} // namespace deskflow::core::ipc