refactor(ipc): Abstract IPC server logic

This commit is contained in:
Nick Bolton 2025-12-05 16:48:29 +00:00
parent e4bafaf88f
commit 2e13074580
5 changed files with 202 additions and 145 deletions

View file

@ -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
)

View file

@ -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 <QLocalServer>
#include <QLocalSocket>
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<QLocalSocket *>(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<QLocalSocket *>(sender());
LOG_DEBUG("ipc server client disconnected");
m_clients.remove(clientSocket);
clientSocket->deleteLater();
}
void DaemonIpcServer::handleErrorOccurred()
{
const auto clientSocket = qobject_cast<QLocalSocket *>(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

View file

@ -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 <QObject>
#include <QSet>
#include "IpcServer.h"
#include <QObject>
#include <QString>
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<QLocalSocket *> m_clients;
};
} // namespace deskflow::core::ipc

View file

@ -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 <QLocalServer>
#include <QLocalSocket>
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<QLocalSocket *>(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<QLocalSocket *>(sender());
LOG_DEBUG("ipc server client disconnected");
m_clients.remove(clientSocket);
clientSocket->deleteLater();
}
void IpcServer::handleErrorOccurred()
{
const auto clientSocket = qobject_cast<QLocalSocket *>(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

View file

@ -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 <QObject>
#include <QSet>
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<QLocalSocket *> m_clients;
QString m_serverName;
};
} // namespace deskflow::core::ipc