From 1ec36645e1b26488ba9a53c4cfe51b46d7e8c1a4 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Fri, 5 Dec 2025 16:31:30 +0000 Subject: [PATCH] refactor(ipc): Abstract IPC client logic --- src/lib/gui/CMakeLists.txt | 2 + src/lib/gui/MainWindow.cpp | 2 +- src/lib/gui/TlsUtility.cpp | 2 +- src/lib/gui/ipc/DaemonIpcClient.cpp | 167 +------------------------ src/lib/gui/ipc/DaemonIpcClient.h | 42 +------ src/lib/gui/ipc/IpcClient.cpp | 181 ++++++++++++++++++++++++++++ src/lib/gui/ipc/IpcClient.h | 63 ++++++++++ 7 files changed, 258 insertions(+), 201 deletions(-) create mode 100644 src/lib/gui/ipc/IpcClient.cpp create mode 100644 src/lib/gui/ipc/IpcClient.h diff --git a/src/lib/gui/CMakeLists.txt b/src/lib/gui/CMakeLists.txt index 0df69a0d4..4416920dc 100644 --- a/src/lib/gui/CMakeLists.txt +++ b/src/lib/gui/CMakeLists.txt @@ -81,6 +81,8 @@ add_library(${target} STATIC dialogs/SettingsDialog.cpp dialogs/SettingsDialog.h dialogs/SettingsDialog.ui + ipc/IpcClient.cpp + ipc/IpcClient.h ipc/DaemonIpcClient.cpp ipc/DaemonIpcClient.h validators/AliasValidator.cpp diff --git a/src/lib/gui/MainWindow.cpp b/src/lib/gui/MainWindow.cpp index 4903d33c7..6a9eb77f6 100644 --- a/src/lib/gui/MainWindow.cpp +++ b/src/lib/gui/MainWindow.cpp @@ -916,7 +916,7 @@ void MainWindow::coreProcessStateChanged(ProcessState state) void MainWindow::coreConnectionStateChanged(ConnectionState state) { - qDebug() << "core connection state changed: " << static_cast(state); + qDebug() << "core connection state changed:" << static_cast(state); updateStatus(); diff --git a/src/lib/gui/TlsUtility.cpp b/src/lib/gui/TlsUtility.cpp index 9729db220..2d0a64b1f 100644 --- a/src/lib/gui/TlsUtility.cpp +++ b/src/lib/gui/TlsUtility.cpp @@ -117,7 +117,7 @@ bool generateCertificate() try { deskflow::generatePemSelfSignedCert(certPath, keyLength); } catch (const std::exception &e) { - qCritical() << "failed to generate self-signed pem cert: " << e.what(); + qCritical() << "failed to generate self-signed pem cert:" << e.what(); return false; } qDebug("tls certificate generated"); diff --git a/src/lib/gui/ipc/DaemonIpcClient.cpp b/src/lib/gui/ipc/DaemonIpcClient.cpp index 2b00c68bd..036badb34 100644 --- a/src/lib/gui/ipc/DaemonIpcClient.cpp +++ b/src/lib/gui/ipc/DaemonIpcClient.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 */ @@ -17,164 +17,9 @@ namespace deskflow::gui::ipc { const auto kTimeout = 1000; -const auto kRetryLimit = 3; -DaemonIpcClient::DaemonIpcClient(QObject *parent) - : QObject(parent), - m_socket{new QLocalSocket(this)} // NOSONAR - Qt memory +DaemonIpcClient::DaemonIpcClient(QObject *parent) : IpcClient(parent, kDaemonIpcName) { - connect(m_socket, &QLocalSocket::disconnected, this, &DaemonIpcClient::handleDisconnected); - connect(m_socket, &QLocalSocket::errorOccurred, this, &DaemonIpcClient::handleErrorOccurred); -} - -bool DaemonIpcClient::connectToServer() -{ - if (m_state == State::Connecting) { - qWarning() << "daemon ipc client already connecting to server"; - return false; - } - - if (m_state != State::Unconnected) { - qDebug() << "daemon ipc client not in unconnected state, disconnecting"; - disconnectFromServer(); - } - - if (m_socket->state() != QLocalSocket::UnconnectedState) { - qWarning() << "daemon ipc client socket not in unconnected state, disconnecting"; - disconnectFromServer(); - } - - for (int i = 0; i < kRetryLimit; ++i) { - if (i == 0) { - qDebug() << "daemon ipc client connecting to server:" << kDaemonIpcName; - } else { - qDebug() << "daemon ipc client retrying connection, attempt:" << i + 1; - } - - m_state = State::Connecting; - m_socket->connectToServer(kDaemonIpcName); - - if (!m_socket->waitForConnected(kTimeout)) { - qWarning() << "daemon ipc client failed to connect"; - disconnectFromServer(); - continue; - } - - if (!sendMessage("hello", "hello", false)) { - qWarning() << "daemon ipc client failed to send hello"; - disconnectFromServer(); - continue; - } - - m_state = State::Connected; - qDebug() << "daemon ipc client connected"; - Q_EMIT connected(); - return true; - } - - qWarning() << "daemon ipc client failed to connect after" << kRetryLimit << "attempts"; - disconnectFromServer(); - Q_EMIT connectionFailed(); - return false; -} - -void DaemonIpcClient::disconnectFromServer() -{ - QMutexLocker locker(&m_mutex); - m_state = State::Disconnecting; - qDebug() << "daemon ipc client disconnecting from server"; - m_socket->disconnectFromServer(); - - if (m_socket->state() != QLocalSocket::UnconnectedState) { - qDebug() << "daemon ipc client waiting for socket to disconnect"; - m_socket->waitForDisconnected(kTimeout); - qDebug() << "daemon ipc client disconnected from server"; - } else { - qDebug() << "daemon ipc client socket already disconnected"; - } - - m_state = State::Unconnected; -} - -void DaemonIpcClient::handleDisconnected() -{ - qDebug() << "daemon ipc client disconnected from server"; - if (m_state == State::Connected) { - Q_EMIT connectionFailed(); - } - - m_state = State::Unconnected; -} - -void DaemonIpcClient::handleErrorOccurred() -{ - qWarning() << "daemon ipc client error:" << m_socket->errorString(); - disconnectFromServer(); - - if (m_state == State::Connected) { - Q_EMIT connectionFailed(); - } -} - -bool DaemonIpcClient::sendMessage(const QString &message, const QString &expectAck, const bool expectConnected) -{ - QMutexLocker locker(&m_mutex); - if (expectConnected && !isConnected()) { - qWarning() << "cannot send command, ipc client not connected"; - return false; - } - - QByteArray messageData = message.toUtf8() + "\n"; - m_socket->write(messageData); - if (!m_socket->waitForBytesWritten(kTimeout)) { - qWarning() << "daemon ipc client failed to write command"; - return false; - } - - if (!expectAck.isEmpty()) { - qDebug() << "daemon ipc client waiting for ack: " << expectAck; - - if (!m_socket->waitForReadyRead(kTimeout)) { - qWarning() << "daemon ipc client socket ready read timed out"; - return false; - } - - QByteArray response = m_socket->readAll(); - if (response.isEmpty()) { - qWarning() << "daemon ipc client got empty response"; - return false; - } - - QString responseData = QString::fromUtf8(response); - if (responseData.isEmpty()) { - qWarning() << "daemon ipc client failed to convert response to string"; - return false; - } - - if (responseData != expectAck + "\n") { - qWarning() << "daemon ipc client got unexpected response: " << responseData; - return false; - } - } - - qDebug() << "daemon ipc client sent message: " << messageData; - return true; -} - -bool DaemonIpcClient::keepAlive() -{ - if (!isConnected() && !connectToServer()) { - qWarning() << "daemon ipc client keep alive failed to connect"; - return false; - } - - if (!sendMessage("noop")) { - qWarning() << "daemon ipc client keep alive ping failed, reconnecting"; - connectToServer(); - return false; - } - - return true; } bool DaemonIpcClient::sendLogLevel(const QString &logLevel) @@ -216,12 +61,12 @@ QString DaemonIpcClient::requestLogPath() return QString(); } - if (!m_socket->waitForReadyRead(kTimeout)) { + if (!socket()->waitForReadyRead(kTimeout)) { qWarning() << "daemon ipc client failed to read log path response"; return QString(); } - QByteArray response = m_socket->readAll(); + QByteArray response = socket()->readAll(); if (response.isEmpty()) { qWarning() << "daemon ipc client got empty log path response"; return QString(); @@ -236,12 +81,12 @@ QString DaemonIpcClient::requestLogPath() // Trimming removes newline from end of message. QStringList parts = responseData.trimmed().split("="); if (parts.size() != 2) { - qWarning() << "daemon ipc client got invalid log path response: " << responseData; + qWarning() << "daemon ipc client got invalid log path response:" << responseData; return QString(); } if (parts[0] != "logPath") { - qWarning() << "daemon ipc client got unexpected log path response: " << responseData; + qWarning() << "daemon ipc client got unexpected log path response:" << responseData; return QString(); } diff --git a/src/lib/gui/ipc/DaemonIpcClient.h b/src/lib/gui/ipc/DaemonIpcClient.h index d5cd5ea0e..182570316 100644 --- a/src/lib/gui/ipc/DaemonIpcClient.h +++ b/src/lib/gui/ipc/DaemonIpcClient.h @@ -1,62 +1,28 @@ /* * 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 "IpcClient.h" -class QLocalSocket; +#include namespace deskflow::gui::ipc { -class DaemonIpcClient : public QObject +class DaemonIpcClient : public IpcClient { Q_OBJECT - // Represents underlying socket state and whether the server responded to the hello message. - enum class State - { - Unconnected, - Connecting, - Connected, - Disconnecting, - }; - public: explicit DaemonIpcClient(QObject *parent = nullptr); - bool connectToServer(); - void disconnectFromServer(); bool sendLogLevel(const QString &logLevel); bool sendStartProcess(const QString &command, bool elevate); bool sendStopProcess(); bool sendClearSettings(); QString requestLogPath(); - - bool isConnected() const - { - return m_state == State::Connected; - } - -Q_SIGNALS: - void connected(); - void connectionFailed(); - -private Q_SLOTS: - void handleDisconnected(); - void handleErrorOccurred(); - -private: - bool keepAlive(); - bool sendMessage(const QString &message, const QString &expectAck = "ok", const bool expectConnected = true); - -private: - QLocalSocket *m_socket; - QMutex m_mutex; - State m_state{State::Unconnected}; }; } // namespace deskflow::gui::ipc diff --git a/src/lib/gui/ipc/IpcClient.cpp b/src/lib/gui/ipc/IpcClient.cpp new file mode 100644 index 000000000..8edcc2d8d --- /dev/null +++ b/src/lib/gui/ipc/IpcClient.cpp @@ -0,0 +1,181 @@ +/* + * 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 "IpcClient.h" + +#include +#include +#include +#include +#include + +namespace deskflow::gui::ipc { + +const auto kTimeout = 1000; +const auto kRetryLimit = 3; + +IpcClient::IpcClient(QObject *parent, const QString &socketName) + : QObject(parent), + m_socket{new QLocalSocket(this)}, + m_socketName(socketName) // NOSONAR - Qt memory +{ + connect(m_socket, &QLocalSocket::disconnected, this, &IpcClient::handleDisconnected); + connect(m_socket, &QLocalSocket::errorOccurred, this, &IpcClient::handleErrorOccurred); +} + +bool IpcClient::connectToServer() +{ + if (m_state == State::Connecting) { + qWarning() << "ipc client already connecting to server"; + return false; + } + + if (m_state != State::Unconnected) { + qDebug() << "ipc client not in unconnected state, disconnecting"; + disconnectFromServer(); + } + + if (m_socket->state() != QLocalSocket::UnconnectedState) { + qWarning() << "ipc client socket not in unconnected state, disconnecting"; + disconnectFromServer(); + } + + for (int i = 0; i < kRetryLimit; ++i) { + if (i == 0) { + qDebug() << "ipc client connecting to server:" << m_socketName; + } else { + qDebug() << "ipc client retrying connection, attempt:" << i + 1; + } + + m_state = State::Connecting; + m_socket->connectToServer(m_socketName); + + if (!m_socket->waitForConnected(kTimeout)) { + qWarning() << "ipc client failed to connect"; + disconnectFromServer(); + continue; + } + + if (!sendMessage("hello", "hello", false)) { + qWarning() << "ipc client failed to send hello"; + disconnectFromServer(); + continue; + } + + m_state = State::Connected; + qDebug() << "ipc client connected"; + Q_EMIT connected(); + return true; + } + + qWarning() << "ipc client failed to connect after" << kRetryLimit << "attempts"; + disconnectFromServer(); + Q_EMIT connectionFailed(); + return false; +} + +void IpcClient::disconnectFromServer() +{ + QMutexLocker locker(&m_mutex); + + m_state = State::Disconnecting; + qDebug() << "ipc client disconnecting from server"; + m_socket->disconnectFromServer(); + + if (m_socket->state() != QLocalSocket::UnconnectedState) { + qDebug() << "ipc client waiting for socket to disconnect"; + m_socket->waitForDisconnected(kTimeout); + qDebug() << "ipc client disconnected from server"; + } else { + qDebug() << "ipc client socket already disconnected"; + } + + m_state = State::Unconnected; +} + +void IpcClient::handleDisconnected() +{ + qDebug() << "ipc client disconnected from server"; + if (m_state == State::Connected) { + Q_EMIT connectionFailed(); + } + + m_state = State::Unconnected; +} + +void IpcClient::handleErrorOccurred() +{ + qWarning() << "ipc client error:" << m_socket->errorString(); + disconnectFromServer(); + + if (m_state == State::Connected) { + Q_EMIT connectionFailed(); + } +} + +bool IpcClient::sendMessage(const QString &message, const QString &expectAck, const bool expectConnected) +{ + QMutexLocker locker(&m_mutex); + + if (expectConnected && !isConnected()) { + qWarning() << "cannot send command, ipc client not connected"; + return false; + } + + QByteArray messageData = message.toUtf8() + "\n"; + m_socket->write(messageData); + if (!m_socket->waitForBytesWritten(kTimeout)) { + qWarning() << "ipc client failed to write command"; + return false; + } + + if (!expectAck.isEmpty()) { + qDebug() << "ipc client waiting for ack:" << expectAck; + + if (!m_socket->waitForReadyRead(kTimeout)) { + qWarning() << "ipc client socket ready read timed out"; + return false; + } + + QByteArray response = m_socket->readAll(); + if (response.isEmpty()) { + qWarning() << "ipc client got empty response"; + return false; + } + + QString responseData = QString::fromUtf8(response); + if (responseData.isEmpty()) { + qWarning() << "ipc client failed to convert response to string"; + return false; + } + + if (responseData != expectAck + "\n") { + qWarning() << "ipc client got unexpected response:" << responseData; + return false; + } + } + + qDebug() << "ipc client sent message:" << messageData; + return true; +} + +bool IpcClient::keepAlive() +{ + if (!isConnected() && !connectToServer()) { + qWarning() << "ipc client keep alive failed to connect"; + return false; + } + + if (!sendMessage("noop")) { + qWarning() << "ipc client keep alive ping failed, reconnecting"; + connectToServer(); + return false; + } + + return true; +} + +} // namespace deskflow::gui::ipc diff --git a/src/lib/gui/ipc/IpcClient.h b/src/lib/gui/ipc/IpcClient.h new file mode 100644 index 000000000..ebe12c805 --- /dev/null +++ b/src/lib/gui/ipc/IpcClient.h @@ -0,0 +1,63 @@ +/* + * 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 QLocalSocket; + +namespace deskflow::gui::ipc { + +class IpcClient : public QObject +{ + Q_OBJECT + + // Represents underlying socket state and whether the server responded to the hello message. + enum class State + { + Unconnected, + Connecting, + Connected, + Disconnecting, + }; + +public: + explicit IpcClient(QObject *parent, const QString &socketName); + bool connectToServer(); + void disconnectFromServer(); + + bool isConnected() const + { + return m_state == State::Connected; + } + +Q_SIGNALS: + void connected(); + void connectionFailed(); + +private Q_SLOTS: + void handleDisconnected(); + void handleErrorOccurred(); + +protected: + bool keepAlive(); + bool sendMessage(const QString &message, const QString &expectAck = "ok", const bool expectConnected = true); + + QLocalSocket *socket() const + { + return m_socket; + } + +private: + QLocalSocket *m_socket; + State m_state{State::Unconnected}; + QString m_socketName; + QMutex m_mutex; +}; + +} // namespace deskflow::gui::ipc