From 6a1e8f3edb7651074b52a21c9bba03411ea81711 Mon Sep 17 00:00:00 2001 From: sithlord48 Date: Sun, 5 Apr 2026 21:29:55 -0400 Subject: [PATCH 01/24] chore: remove unused QMessageBox includes and Foward Declaration --- src/lib/gui/MainWindow.h | 1 - src/lib/gui/config/ServerConfig.cpp | 1 - src/lib/gui/core/ClientConnection.cpp | 1 - src/lib/gui/core/ServerConnection.cpp | 1 - 4 files changed, 4 deletions(-) diff --git a/src/lib/gui/MainWindow.h b/src/lib/gui/MainWindow.h index ec35a91d0dda..12c229ace645 100644 --- a/src/lib/gui/MainWindow.h +++ b/src/lib/gui/MainWindow.h @@ -41,7 +41,6 @@ class QComboBox; class QTabWidget; class QCheckBox; class QRadioButton; -class QMessageBox; class QAbstractButton; class QLocalServer; diff --git a/src/lib/gui/config/ServerConfig.cpp b/src/lib/gui/config/ServerConfig.cpp index b878e489af6a..aff460f48a00 100644 --- a/src/lib/gui/config/ServerConfig.cpp +++ b/src/lib/gui/config/ServerConfig.cpp @@ -12,7 +12,6 @@ #include "common/Settings.h" #include -#include #include using enum ScreenConfig::Modifier; diff --git a/src/lib/gui/core/ClientConnection.cpp b/src/lib/gui/core/ClientConnection.cpp index 1d2d2ae6ce68..499cc1e0e655 100644 --- a/src/lib/gui/core/ClientConnection.cpp +++ b/src/lib/gui/core/ClientConnection.cpp @@ -10,7 +10,6 @@ #include "common/Settings.h" #include -#include namespace deskflow::gui { diff --git a/src/lib/gui/core/ServerConnection.cpp b/src/lib/gui/core/ServerConnection.cpp index d1ff8075e301..94baa2ef17e8 100644 --- a/src/lib/gui/core/ServerConnection.cpp +++ b/src/lib/gui/core/ServerConnection.cpp @@ -10,7 +10,6 @@ #include "ServerMessage.h" #include "common/Settings.h" -#include #include namespace deskflow::gui { From 632b64f12ff9add3ed179e61db167d7e6915fc53 Mon Sep 17 00:00:00 2001 From: sithlord48 Date: Sun, 5 Apr 2026 21:37:12 -0400 Subject: [PATCH 02/24] chore: MainWindow remove unused forward declariations --- src/lib/gui/MainWindow.h | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/lib/gui/MainWindow.h b/src/lib/gui/MainWindow.h index 12c229ace645..82bf075c3193 100644 --- a/src/lib/gui/MainWindow.h +++ b/src/lib/gui/MainWindow.h @@ -9,14 +9,10 @@ #pragma once -#include #include -#include #include #include -#include #include -#include #include "VersionChecker.h" #include "config/ServerConfig.h" @@ -32,16 +28,6 @@ class QAction; class QMenu; -class QLabel; -class QLineEdit; -class QGroupBox; -class QPushButton; -class QTextEdit; -class QComboBox; -class QTabWidget; -class QCheckBox; -class QRadioButton; -class QAbstractButton; class QLocalServer; class DeskflowApplication; @@ -66,9 +52,6 @@ class MainWindow : public QMainWindow Q_OBJECT - friend class DeskflowApplication; - friend class SettingsDialog; - public: enum class LogLevel { From 047451696aac0514abfb849ee02d5f69336e6ab5 Mon Sep 17 00:00:00 2001 From: sithlord48 Date: Sat, 4 Apr 2026 13:12:21 -0400 Subject: [PATCH 03/24] refactor: Messages::showFirstConnectionMessage check the settings for the values --- src/lib/gui/MainWindow.cpp | 8 +------- src/lib/gui/Messages.cpp | 10 ++++++---- src/lib/gui/Messages.h | 2 +- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/lib/gui/MainWindow.cpp b/src/lib/gui/MainWindow.cpp index 9e36eca6bf17..b13e58b176c2 100644 --- a/src/lib/gui/MainWindow.cpp +++ b/src/lib/gui/MainWindow.cpp @@ -858,13 +858,7 @@ void MainWindow::showFirstConnectedMessage() if (Settings::value(Settings::Gui::ShownFirstConnectedMessage).toBool()) return; Settings::setValue(Settings::Gui::ShownFirstConnectedMessage, true); - - const auto isServer = m_coreProcess.mode() == CoreMode::Server; - const auto closeToTray = Settings::value(Settings::Gui::CloseToTray).toBool(); - - using ProcessMode = Settings::ProcessMode; - const auto enableService = Settings::value(Settings::Core::ProcessMode).value() == ProcessMode::Service; - messages::showFirstConnectedMessage(this, closeToTray, enableService, isServer); + messages::showFirstConnectedMessage(this); } void MainWindow::updateStatus() diff --git a/src/lib/gui/Messages.cpp b/src/lib/gui/Messages.cpp index f9c8df874fe2..7a81df80aa12 100644 --- a/src/lib/gui/Messages.cpp +++ b/src/lib/gui/Messages.cpp @@ -142,12 +142,11 @@ void showFirstServerStartMessage(QWidget *parent) ); } -void showFirstConnectedMessage(QWidget *parent, bool closeToTray, bool enableService, bool isServer) +void showFirstConnectedMessage(QWidget *parent) { - auto message = QObject::tr("

%1 is now connected!

").arg(kAppName); - if (isServer) { + if (Settings::value(Settings::Core::CoreMode).value() == Settings::Server) { message.append( QObject::tr( "

Try moving your mouse to your other computer. Once there, go ahead " @@ -159,7 +158,10 @@ void showFirstConnectedMessage(QWidget *parent, bool closeToTray, bool enableSer message.append(QObject::tr("

Try controlling this computer remotely.

")); } - if (!closeToTray && !enableService) { + using ProcessMode = Settings::ProcessMode; + + if (Settings::value(Settings::Core::ProcessMode).value() == ProcessMode::Desktop && + !Settings::value(Settings::Gui::CloseToTray).toBool()) { message.append( QObject::tr( "

As you do not have the setting enabled to keep %1 running in " diff --git a/src/lib/gui/Messages.h b/src/lib/gui/Messages.h index 5e36aec34dbf..d2bb88c8711c 100644 --- a/src/lib/gui/Messages.h +++ b/src/lib/gui/Messages.h @@ -21,7 +21,7 @@ void raiseCriticalDialog(); void showFirstServerStartMessage(QWidget *parent); -void showFirstConnectedMessage(QWidget *parent, bool closeToTray, bool enableService, bool isServer); +void showFirstConnectedMessage(QWidget *parent); void showCloseReminder(QWidget *parent); From d8e9ac27f5e5f53308c9ee58344c246b2b7a2170 Mon Sep 17 00:00:00 2001 From: sithlord48 Date: Sat, 4 Apr 2026 14:29:49 -0400 Subject: [PATCH 04/24] refactor: Messages::showNewClientPrompt check if server auth is needed when the message is shown not in the signal --- src/lib/gui/MainWindow.cpp | 4 ++-- src/lib/gui/MainWindow.h | 2 +- src/lib/gui/Messages.cpp | 5 +++-- src/lib/gui/Messages.h | 2 +- src/lib/gui/core/ServerConnection.cpp | 4 +--- src/lib/gui/core/ServerConnection.h | 2 +- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/lib/gui/MainWindow.cpp b/src/lib/gui/MainWindow.cpp index b13e58b176c2..4903d33c74c1 100644 --- a/src/lib/gui/MainWindow.cpp +++ b/src/lib/gui/MainWindow.cpp @@ -1187,10 +1187,10 @@ void MainWindow::showClientError(deskflow::client::ErrorType error, const QStrin m_clientErrorVisible = false; } -void MainWindow::handleNewClientPromptRequest(const QString &clientName, bool usePeerAuth) +void MainWindow::handleNewClientPromptRequest(const QString &clientName) { showAndActivate(); - bool result = deskflow::gui::messages::showNewClientPrompt(this, clientName, usePeerAuth); + bool result = deskflow::gui::messages::showNewClientPrompt(this, clientName); m_serverConnection.handleNewClientResult(clientName, result); } diff --git a/src/lib/gui/MainWindow.h b/src/lib/gui/MainWindow.h index 82bf075c3193..21c688f70b0d 100644 --- a/src/lib/gui/MainWindow.h +++ b/src/lib/gui/MainWindow.h @@ -140,7 +140,7 @@ class MainWindow : public QMainWindow void daemonIpcClientConnectionFailed(); void toggleCanRunCore(bool enableButtons); void remoteHostChanged(const QString &newRemoteHost); - void handleNewClientPromptRequest(const QString &clientName, bool usePeerAuth); + void handleNewClientPromptRequest(const QString &clientName); void updateIpLabel(const QStringList &addresses); void updateTimeoutDelay(int newDelay); diff --git a/src/lib/gui/Messages.cpp b/src/lib/gui/Messages.cpp index 7a81df80aa12..e9ca50cde9e7 100644 --- a/src/lib/gui/Messages.cpp +++ b/src/lib/gui/Messages.cpp @@ -184,9 +184,10 @@ void showFirstConnectedMessage(QWidget *parent) QMessageBox::information(parent, title, message); } -bool showNewClientPrompt(QWidget *parent, const QString &clientName, bool serverRequiresPeerAuth) +bool showNewClientPrompt(QWidget *parent, const QString &clientName) { - if (serverRequiresPeerAuth) { + if (Settings::value(Settings::Security::TlsEnabled).toBool() && + Settings::value(Settings::Security::CheckPeers).toBool()) { // When peer auth is enabled you will be prompted to allow the connection before seeing this dialog. // This is why we do not show a dialog with an option to ignore the new client QMessageBox::information( diff --git a/src/lib/gui/Messages.h b/src/lib/gui/Messages.h index d2bb88c8711c..858887e0e016 100644 --- a/src/lib/gui/Messages.h +++ b/src/lib/gui/Messages.h @@ -25,7 +25,7 @@ void showFirstConnectedMessage(QWidget *parent); void showCloseReminder(QWidget *parent); -bool showNewClientPrompt(QWidget *parent, const QString &clientName, bool serverRequiresPeerAuth = false); +bool showNewClientPrompt(QWidget *parent, const QString &clientName); bool showClearSettings(QWidget *parent); diff --git a/src/lib/gui/core/ServerConnection.cpp b/src/lib/gui/core/ServerConnection.cpp index 94baa2ef17e8..f1ba9357d1c4 100644 --- a/src/lib/gui/core/ServerConnection.cpp +++ b/src/lib/gui/core/ServerConnection.cpp @@ -82,9 +82,7 @@ void ServerConnection::handleNewClient(const QString &clientName) } m_messageShowing = true; - const bool tlsEnabled = Settings::value(Settings::Security::TlsEnabled).toBool(); - const bool requireCerts = Settings::value(Settings::Security::CheckPeers).toBool(); - Q_EMIT requestNewClientPrompt(clientName, tlsEnabled && requireCerts); + Q_EMIT requestNewClientPrompt(clientName); } void ServerConnection::handleNewClientResult(const QString &clientName, bool acceptClient) diff --git a/src/lib/gui/core/ServerConnection.h b/src/lib/gui/core/ServerConnection.h index 8fa698649e25..fa656479f051 100644 --- a/src/lib/gui/core/ServerConnection.h +++ b/src/lib/gui/core/ServerConnection.h @@ -31,7 +31,7 @@ class ServerConnection : public QObject void handleNewClientResult(const QString &clientName, bool acceptClient); Q_SIGNALS: - void requestNewClientPrompt(const QString &clientName, bool peerAuthRequired); + void requestNewClientPrompt(const QString &clientName); void configureClient(const QString &clientName); void clientsChanged(const QStringList &clients); From 8a77e5f1301a298dac3c8261b07008e8fd0526ab Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Fri, 5 Dec 2025 18:09:25 +0000 Subject: [PATCH 05/24] refactor(core): Convert app to Qt app --- src/apps/deskflow-core/deskflow-core.cpp | 35 +++++++--- src/lib/deskflow/App.cpp | 89 +++++++++++++++--------- src/lib/deskflow/App.h | 9 ++- 3 files changed, 87 insertions(+), 46 deletions(-) diff --git a/src/apps/deskflow-core/deskflow-core.cpp b/src/apps/deskflow-core/deskflow-core.cpp index 052b380e8a84..e4d1b37ea028 100644 --- a/src/apps/deskflow-core/deskflow-core.cpp +++ b/src/apps/deskflow-core/deskflow-core.cpp @@ -1,7 +1,7 @@ /* * Deskflow -- mouse and keyboard sharing utility * SPDX-FileCopyrightText: (C) 2025 Chris Rizzitello - * SPDX-FileCopyrightText: (C) 2012 - 2016 Symless Ltd. + * SPDX-FileCopyrightText: (C) 2012 - 2016, 2025 - 2026 Symless Ltd. * SPDX-FileCopyrightText: (C) 2002 Chris Schoeneman * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception */ @@ -21,15 +21,27 @@ #include #endif +#include #include #include #include +#include void showHelp(const CoreArgParser &parser) { QTextStream(stdout) << parser.helpText(); } +App *createApp(const CoreArgParser &parser, EventQueue &events, const QString &processName) +{ + if (parser.serverMode()) { + return new ServerApp(&events, processName); + } else if (parser.clientMode()) { + return new ClientApp(&events, processName); + } + return nullptr; +} + int main(int argc, char **argv) { #if defined(Q_OS_WIN) @@ -86,13 +98,18 @@ int main(int argc, char **argv) EventQueue events; const auto processName = QFileInfo(argv[0]).fileName(); - if (parser.serverMode()) { - ServerApp app(&events, processName); - return app.run(); - } else if (parser.clientMode()) { - ClientApp app(&events, processName); - return app.run(); - } + App *coreApp = createApp(parser, events, processName); + + QCoreApplication app(argc, argv); + QCoreApplication::setApplicationName(QStringLiteral("%1 Core").arg(kAppName)); + + QThread coreThread; + QObject::connect(&coreThread, &QThread::finished, &app, &QCoreApplication::quit); + coreApp->run(coreThread); + + const auto exitCode = QCoreApplication::exec(); + coreThread.wait(); - return s_exitSuccess; + LOG_DEBUG("core exited, code: %d", exitCode); + return exitCode; } diff --git a/src/lib/deskflow/App.cpp b/src/lib/deskflow/App.cpp index 4548960672cd..f6e23b60aab2 100644 --- a/src/lib/deskflow/App.cpp +++ b/src/lib/deskflow/App.cpp @@ -1,6 +1,6 @@ /* * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2012 - 2025 Symless Ltd. + * SPDX-FileCopyrightText: (C) 2012 - 2026 Symless Ltd. * SPDX-FileCopyrightText: (C) 2002 Chris Schoeneman * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception */ @@ -57,48 +57,69 @@ App::~App() s_instance = nullptr; } -int App::run() +void App::run(QThread &coreThread) { + LOG_NOTE("starting core"); + + // Important: Move the daemon app to the daemon thread before creating any more Qt objects + // owned by the daemon app, as they will be created on the daemon thread. + moveToThread(&coreThread); + + connect(&coreThread, &QThread::started, this, [this, &coreThread]() { + LOG_DEBUG("core thread started"); + #if MAC_OS_X_VERSION_10_7 - // dock hide only supported on lion :( - ProcessSerialNumber psn = {0, kCurrentProcess}; + // dock hide only supported on lion :( + ProcessSerialNumber psn = {0, kCurrentProcess}; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - GetCurrentProcess(&psn); + GetCurrentProcess(&psn); #pragma GCC diagnostic pop - TransformProcessType(&psn, kProcessTransformToBackgroundApplication); + TransformProcessType(&psn, kProcessTransformToBackgroundApplication); #endif - // install application in to arch - appUtil().adoptApp(this); - - // HACK: fail by default (saves us setting result in each catch) - int result = s_exitFailed; - - try { - result = appUtil().run(); - } catch (ExitAppException &e) { - // instead of showing a nasty error, just exit with the error code. - // not sure if i like this behaviour, but it's probably better than - // using the exit(int) function! - result = e.getCode(); - } catch (DisplayInvalidException &die) { - LOG_CRIT("a display invalid exception error occurred: %s\n", die.what()); - // display invalid exceptions can occur when going to sleep. When this - // process exits, the UI will restart us instantly. We don't really want - // that behevior, so we quies for a bit - Arch::sleep(10); - } catch (std::runtime_error &re) { - LOG_CRIT("a runtime error occurred: %s\n", re.what()); - } catch (std::exception &e) { - LOG_CRIT("an error occurred: %s\n", e.what()); - } catch (...) { - LOG_CRIT("an unknown error occurred\n"); - } - - return result; + // install application in to arch + appUtil().adoptApp(this); + + // HACK: fail by default (saves us setting result in each catch) + int result = s_exitFailed; + + try { + result = appUtil().run(); + } catch (ExitAppException &e) { + // instead of showing a nasty error, just exit with the error code. + // not sure if i like this behaviour, but it's probably better than + // using the exit(int) function! + result = e.getCode(); + } catch (DisplayInvalidException &die) { + LOG_CRIT("a display invalid exception error occurred: %s\n", die.what()); + // display invalid exceptions can occur when going to sleep. When this + // process exits, the UI will restart us instantly. We don't really want + // that behevior, so we quies for a bit + Arch::sleep(10); + } catch (std::runtime_error &re) { + LOG_CRIT("a runtime error occurred: %s\n", re.what()); + } catch (std::exception &e) { + LOG_CRIT("an error occurred: %s\n", e.what()); + } catch (...) { + LOG_CRIT("an unknown error occurred\n"); + } + + if (result == s_exitSuccess) { + LOG_INFO("core stopped successfully"); + } else { + // TODO: surface error code to main thread somehow + LOG_ERR("core stopped with error code: %d", result); + } + + coreThread.quit(); + LOG_DEBUG("core thread finished"); + }); + + LOG_DEBUG("starting core thread"); + coreThread.start(); } void App::setupFileLogging() diff --git a/src/lib/deskflow/App.h b/src/lib/deskflow/App.h index 52baded3f304..c3846d8fe0dc 100644 --- a/src/lib/deskflow/App.h +++ b/src/lib/deskflow/App.h @@ -1,7 +1,7 @@ /* * Deskflow -- mouse and keyboard sharing utility * SPDX-FileCopyrightText: (C) 2026 Deskflow Developers - * SPDX-FileCopyrightText: (C) 2012 - 2025 Symless Ltd. + * SPDX-FileCopyrightText: (C) 2012 - 2026 Symless Ltd. * SPDX-FileCopyrightText: (C) 2002 Chris Schoeneman * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception */ @@ -19,6 +19,9 @@ #include "deskflow/unix/AppUtilUnix.h" #endif +#include +#include + #include #include @@ -30,7 +33,7 @@ class FileLogOutputter; class IEventQueue; class SocketMultiplexer; -class App : public IApp +class App : public QObject, private IApp { public: class XNoEiSupport : public std::runtime_error @@ -72,7 +75,7 @@ class App : public IApp return m_appUtil; } - int run(); + void run(QThread &coreThread); void setupFileLogging(); void loggingFilterWarning() const; void initApp() override; From 29352d97b3ebf709258332c4d2cf69eac933f73f Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Thu, 26 Mar 2026 15:00:26 +0000 Subject: [PATCH 06/24] fix(core): Destroy temporary QCoreApplication before creating the real one --- src/apps/deskflow-core/deskflow-core.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/apps/deskflow-core/deskflow-core.cpp b/src/apps/deskflow-core/deskflow-core.cpp index e4d1b37ea028..3da8edd3d8b7 100644 --- a/src/apps/deskflow-core/deskflow-core.cpp +++ b/src/apps/deskflow-core/deskflow-core.cpp @@ -45,9 +45,10 @@ App *createApp(const CoreArgParser &parser, EventQueue &events, const QString &p int main(int argc, char **argv) { #if defined(Q_OS_WIN) - // HACK to make sure settings gets the correct qApp path - QCoreApplication m(argc, argv); - m.deleteLater(); + { + // HACK to make sure settings gets the correct qApp path + QCoreApplication m(argc, argv); + } ArchMiscWindows::setInstanceWin32(GetModuleHandle(nullptr)); #endif From aaef600ffa538f775a9dfff74d61516a6d8a8013 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Mon, 30 Mar 2026 19:08:08 +0100 Subject: [PATCH 07/24] fix(mac): Remove Cocoa app loop and fix macOS event tap for Qt threading --- src/lib/deskflow/App.cpp | 4 ---- src/lib/deskflow/ClientApp.cpp | 13 ------------- src/lib/deskflow/ServerApp.cpp | 14 -------------- src/lib/platform/OSXEventQueueBuffer.cpp | 15 +++++---------- src/lib/platform/OSXEventQueueBuffer.h | 1 - src/lib/platform/OSXScreen.h | 3 +++ src/lib/platform/OSXScreen.mm | 24 ++++++++++++++++++++++-- 7 files changed, 30 insertions(+), 44 deletions(-) diff --git a/src/lib/deskflow/App.cpp b/src/lib/deskflow/App.cpp index f6e23b60aab2..36a00b6f74d8 100644 --- a/src/lib/deskflow/App.cpp +++ b/src/lib/deskflow/App.cpp @@ -23,7 +23,6 @@ #include #if defined(Q_OS_MAC) -#include "platform/OSXCocoaApp.h" #include #endif @@ -172,7 +171,4 @@ void App::handleScreenError() const void App::runEventsLoop(const void *) { m_events->loop(); -#if defined(Q_OS_MAC) - stopCocoaLoop(); -#endif } diff --git a/src/lib/deskflow/ClientApp.cpp b/src/lib/deskflow/ClientApp.cpp index 7dcbf131b68c..08cce6d1bc93 100644 --- a/src/lib/deskflow/ClientApp.cpp +++ b/src/lib/deskflow/ClientApp.cpp @@ -37,9 +37,6 @@ #endif #if defined(Q_OS_MAC) -#include "base/TMethodJob.h" -#include "mt/Thread.h" -#include "platform/OSXCocoaApp.h" #include "platform/OSXScreen.h" #endif @@ -330,17 +327,7 @@ int ClientApp::mainLoop() // later. the timer installed by startClient() will take care of // that. -#if defined(Q_OS_MAC) - Thread thread(new TMethodJob(this, &ClientApp::runEventsLoop, nullptr)); - - // wait until carbon loop is ready - OSXScreen *screen = dynamic_cast(m_clientScreen->getPlatformScreen()); - screen->waitForCarbonLoop(); - - runCocoaApp(); -#else getEvents()->loop(); -#endif // close down LOG_DEBUG("stopping client"); diff --git a/src/lib/deskflow/ServerApp.cpp b/src/lib/deskflow/ServerApp.cpp index 15c27a480019..3b5c2e58bc24 100644 --- a/src/lib/deskflow/ServerApp.cpp +++ b/src/lib/deskflow/ServerApp.cpp @@ -43,9 +43,6 @@ #endif #if defined(Q_OS_MAC) -#include "base/TMethodJob.h" -#include "mt/Thread.h" -#include "platform/OSXCocoaApp.h" #include "platform/OSXScreen.h" #endif @@ -541,18 +538,7 @@ int ServerApp::mainLoop() // later. the timer installed by startServer() will take care of // that. -#if defined(Q_OS_MAC) - - Thread thread(new TMethodJob(this, &ServerApp::runEventsLoop, nullptr)); - - // wait until carbon loop is ready - OSXScreen *screen = dynamic_cast(m_serverScreen->getPlatformScreen()); - screen->waitForCarbonLoop(); - - runCocoaApp(); -#else getEvents()->loop(); -#endif // close down LOG_DEBUG("stopping server"); diff --git a/src/lib/platform/OSXEventQueueBuffer.cpp b/src/lib/platform/OSXEventQueueBuffer.cpp index 9389e4ef9e62..ada237c92ae4 100644 --- a/src/lib/platform/OSXEventQueueBuffer.cpp +++ b/src/lib/platform/OSXEventQueueBuffer.cpp @@ -58,16 +58,11 @@ IEventQueueBuffer::Type OSXEventQueueBuffer::getEvent(Event &event, uint32_t &da bool OSXEventQueueBuffer::addEvent(uint32_t dataID) { - // Use GCD to dispatch event addition on the main queue - dispatch_async(dispatch_get_main_queue(), ^{ - std::scoped_lock lock{this->m_mutex}; - LOG_DEBUG2("adding user event with dataID: %u", dataID); - this->m_dataQueue.push(dataID); - this->m_cond.notify_one(); - LOG_DEBUG2("user event added to queue, dataID=%u", dataID); - }); - - // Always return true since dispatch_async does not fail under normal conditions + std::scoped_lock lock{m_mutex}; + LOG_DEBUG2("adding user event with dataID: %u", dataID); + m_dataQueue.push(dataID); + m_cond.notify_one(); + LOG_DEBUG2("user event added to queue, dataID=%u", dataID); return true; } diff --git a/src/lib/platform/OSXEventQueueBuffer.h b/src/lib/platform/OSXEventQueueBuffer.h index 0057aa2ebe27..02efcfed69e8 100644 --- a/src/lib/platform/OSXEventQueueBuffer.h +++ b/src/lib/platform/OSXEventQueueBuffer.h @@ -12,7 +12,6 @@ #include "base/IEventQueueBuffer.h" #include -#include #include #include diff --git a/src/lib/platform/OSXScreen.h b/src/lib/platform/OSXScreen.h index 87b5f4f5b170..37c48da29a4d 100644 --- a/src/lib/platform/OSXScreen.h +++ b/src/lib/platform/OSXScreen.h @@ -21,6 +21,7 @@ #include #include #include +#include #include extern "C" @@ -292,6 +293,8 @@ class OSXScreen : public PlatformScreen // Quartz input event support CFMachPortRef m_eventTapPort; CFRunLoopSourceRef m_eventTapRLSR; + std::thread m_eventTapThread; + CFRunLoopRef m_eventTapRunLoop = nullptr; // for double click coalescing. double m_lastClickTime; diff --git a/src/lib/platform/OSXScreen.mm b/src/lib/platform/OSXScreen.mm index 32d23273e27d..bd6b8829d194 100644 --- a/src/lib/platform/OSXScreen.mm +++ b/src/lib/platform/OSXScreen.mm @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -686,7 +687,20 @@ if (m_eventTapPort) { m_eventTapRLSR = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, m_eventTapPort, 0); if (m_eventTapRLSR) { - CFRunLoopAddSource(CFRunLoopGetCurrent(), m_eventTapRLSR, kCFRunLoopDefaultMode); + // Run the event tap on a dedicated thread with its own CFRunLoop so it fires + // independently of whatever event loop the calling thread runs (e.g. QCoreApplication). + // Use a semaphore to ensure m_eventTapRunLoop is set before enable() returns. + auto sem = dispatch_semaphore_create(0); + m_eventTapThread = std::thread([this, sem]() { + m_eventTapRunLoop = CFRunLoopGetCurrent(); + CFRunLoopAddSource(m_eventTapRunLoop, m_eventTapRLSR, kCFRunLoopDefaultMode); + dispatch_semaphore_signal(sem); + CFRunLoopRun(); + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), m_eventTapRLSR, kCFRunLoopDefaultMode); + m_eventTapRunLoop = nullptr; + }); + dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); + dispatch_release(sem); } else { LOG_ERR("failed to create a CFRunLoopSourceRef for the quartz event tap"); } @@ -701,8 +715,14 @@ // FIXME -- stop watching jump zones, stop capturing input + if (m_eventTapRunLoop) { + CFRunLoopStop(m_eventTapRunLoop); + } + if (m_eventTapThread.joinable()) { + m_eventTapThread.join(); + } + if (m_eventTapRLSR) { - CFRunLoopRemoveSource(CFRunLoopGetCurrent(), m_eventTapRLSR, kCFRunLoopDefaultMode); CFRelease(m_eventTapRLSR); m_eventTapRLSR = nullptr; } From b0985a2740454a4448a92bcefa5320185b48ac1e Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Thu, 26 Mar 2026 11:28:16 +0000 Subject: [PATCH 08/24] fix(unix): Add foreign Qt threds in legacy POSIX thread impl --- src/lib/arch/unix/ArchMultithreadPosix.cpp | 16 +++++++++++++++- src/lib/arch/unix/ArchMultithreadPosix.h | 1 + 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/lib/arch/unix/ArchMultithreadPosix.cpp b/src/lib/arch/unix/ArchMultithreadPosix.cpp index 44e00b0c5b07..154cd975b04e 100644 --- a/src/lib/arch/unix/ArchMultithreadPosix.cpp +++ b/src/lib/arch/unix/ArchMultithreadPosix.cpp @@ -517,13 +517,27 @@ void ArchMultithreadPosix::startSignalHandler() ArchThreadImpl *ArchMultithreadPosix::find(pthread_t thread) { - ArchThreadImpl *impl = findNoRef(thread); + ArchThreadImpl *impl = findNoRefOrInsert(thread); if (impl != nullptr) { refThread(impl); } return impl; } +ArchThreadImpl *ArchMultithreadPosix::findNoRefOrInsert(pthread_t thread) +{ + ArchThreadImpl *impl = findNoRef(thread); + if (impl == nullptr) { + // create thread for calling thread which isn't in our list and + // add it to the list. this can happen when a foreign thread + // (e.g. a Qt thread) calls into the arch layer. + impl = new ArchThreadImpl; + impl->m_thread = thread; + insert(impl); + } + return impl; +} + ArchThreadImpl *ArchMultithreadPosix::findNoRef(pthread_t thread) { // linear search diff --git a/src/lib/arch/unix/ArchMultithreadPosix.h b/src/lib/arch/unix/ArchMultithreadPosix.h index 893a6519b4c9..4d22afaebb22 100644 --- a/src/lib/arch/unix/ArchMultithreadPosix.h +++ b/src/lib/arch/unix/ArchMultithreadPosix.h @@ -84,6 +84,7 @@ class ArchMultithreadPosix : public IArchMultithread ArchThreadImpl *find(pthread_t thread); ArchThreadImpl *findNoRef(pthread_t thread); + ArchThreadImpl *findNoRefOrInsert(pthread_t thread); void insert(ArchThreadImpl *thread); void erase(const ArchThreadImpl *thread); From eebe5fd55f101173ea2efa98c0e4784e59efd01e Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Thu, 26 Mar 2026 11:18:43 +0000 Subject: [PATCH 09/24] refactor(win32): Rename Windows thread finding methods for consistency --- src/lib/arch/win32/ArchMultithreadWindows.cpp | 39 +++++++++---------- src/lib/arch/win32/ArchMultithreadWindows.h | 2 +- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/lib/arch/win32/ArchMultithreadWindows.cpp b/src/lib/arch/win32/ArchMultithreadWindows.cpp index eec29773d185..0b8877610bcb 100644 --- a/src/lib/arch/win32/ArchMultithreadWindows.cpp +++ b/src/lib/arch/win32/ArchMultithreadWindows.cpp @@ -108,7 +108,7 @@ ArchMultithreadWindows::~ArchMultithreadWindows() void ArchMultithreadWindows::setNetworkDataForCurrentThread(void *data) { std::scoped_lock lock{m_threadMutex}; - ArchThreadImpl *thread = findNoRef(GetCurrentThreadId()); + ArchThreadImpl *thread = findNoRefOrInsert(GetCurrentThreadId()); thread->m_networkData = data; } @@ -121,7 +121,7 @@ void *ArchMultithreadWindows::getNetworkDataForThread(ArchThread thread) HANDLE ArchMultithreadWindows::getCancelEventForCurrentThread() { std::scoped_lock lock{m_threadMutex}; - ArchThreadImpl *thread = findNoRef(GetCurrentThreadId()); + ArchThreadImpl *thread = findNoRefOrInsert(GetCurrentThreadId()); return thread->m_cancel; } @@ -305,7 +305,7 @@ void ArchMultithreadWindows::closeThread(ArchThread thread) // remove thread from list { std::scoped_lock lock{m_threadMutex}; - assert(findNoRefOrCreate(thread->m_id) == thread); + assert(findNoRef(thread->m_id) == thread); erase(thread); } @@ -390,7 +390,7 @@ void ArchMultithreadWindows::testCancelThread() { // find current thread std::scoped_lock lock{m_threadMutex}; - ArchThreadImpl *thread = findNoRef(GetCurrentThreadId()); + ArchThreadImpl *thread = findNoRefOrInsert(GetCurrentThreadId()); // test cancel on thread testCancelThreadImpl(thread); @@ -404,7 +404,7 @@ bool ArchMultithreadWindows::wait(ArchThread target, double timeout) { std::scoped_lock lock{m_threadMutex}; // find current thread - self = findNoRef(GetCurrentThreadId()); + self = findNoRefOrInsert(GetCurrentThreadId()); // ignore wait if trying to wait on ourself if (target == self) { return false; @@ -497,7 +497,7 @@ void ArchMultithreadWindows::raiseSignal(ThreadSignal signal) ArchThreadImpl *ArchMultithreadWindows::find(DWORD id) { - ArchThreadImpl *impl = findNoRef(id); + ArchThreadImpl *impl = findNoRefOrInsert(id); if (impl != nullptr) { refThread(impl); } @@ -506,7 +506,17 @@ ArchThreadImpl *ArchMultithreadWindows::find(DWORD id) ArchThreadImpl *ArchMultithreadWindows::findNoRef(DWORD id) { - ArchThreadImpl *impl = findNoRefOrCreate(id); + for (ThreadList::const_iterator index = m_threadList.begin(); index != m_threadList.end(); ++index) { + if ((*index)->m_id == id) { + return *index; + } + } + return nullptr; +} + +ArchThreadImpl *ArchMultithreadWindows::findNoRefOrInsert(DWORD id) +{ + ArchThreadImpl *impl = findNoRef(id); if (impl == nullptr) { // create thread for calling thread which isn't in our list and // add it to the list. this won't normally happen but it can if @@ -520,23 +530,12 @@ ArchThreadImpl *ArchMultithreadWindows::findNoRef(DWORD id) return impl; } -ArchThreadImpl *ArchMultithreadWindows::findNoRefOrCreate(DWORD id) -{ - // linear search - for (ThreadList::const_iterator index = m_threadList.begin(); index != m_threadList.end(); ++index) { - if ((*index)->m_id == id) { - return *index; - } - } - return nullptr; -} - void ArchMultithreadWindows::insert(ArchThreadImpl *thread) { assert(thread != nullptr); // thread shouldn't already be on the list - assert(findNoRefOrCreate(thread->m_id) == nullptr); + assert(findNoRef(thread->m_id) == nullptr); // append to list m_threadList.push_back(thread); @@ -555,7 +554,7 @@ void ArchMultithreadWindows::erase(ArchThreadImpl *thread) void ArchMultithreadWindows::refThread(ArchThreadImpl *thread) { assert(thread != nullptr); - assert(findNoRefOrCreate(thread->m_id) != nullptr); + assert(findNoRef(thread->m_id) != nullptr); ++thread->m_refCount; } diff --git a/src/lib/arch/win32/ArchMultithreadWindows.h b/src/lib/arch/win32/ArchMultithreadWindows.h index d34dac8faaf4..6e9cee5e39d2 100644 --- a/src/lib/arch/win32/ArchMultithreadWindows.h +++ b/src/lib/arch/win32/ArchMultithreadWindows.h @@ -89,7 +89,7 @@ class ArchMultithreadWindows : public IArchMultithread private: ArchThreadImpl *find(DWORD id); ArchThreadImpl *findNoRef(DWORD id); - ArchThreadImpl *findNoRefOrCreate(DWORD id); + ArchThreadImpl *findNoRefOrInsert(DWORD id); void insert(ArchThreadImpl *thread); void erase(ArchThreadImpl *thread); From e4bafaf88fa41a1e44cecd22c7e6f92595842e04 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Tue, 31 Mar 2026 10:12:42 +0100 Subject: [PATCH 10/24] refactor(win32): More consistent Windows desk switch log line --- src/lib/platform/MSWindowsDesks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index a47b57aba66e..99b73368040e 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -811,7 +811,7 @@ void MSWindowsDesks::checkDesk() LOG_DEBUG("switched to desk \"%ls\"", name.c_str()); bool syncKeys = false; if (isDeskAccessible(desk)) { - LOG_DEBUG("desktop is accessible - syncing keyboard state after desk switch"); + LOG_DEBUG("desktop is accessible, syncing keyboard state after desk switch"); syncKeys = true; } else { LOG_DEBUG("desktop is inaccessible"); From 2e130745801f40a9fe28bea61fd11e87ecbfd27a Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Fri, 5 Dec 2025 16:48:29 +0000 Subject: [PATCH 11/24] 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 | 38 +------ src/lib/deskflow/ipc/IpcServer.cpp | 116 ++++++++++++++++++++ src/lib/deskflow/ipc/IpcServer.h | 56 ++++++++++ 5 files changed, 201 insertions(+), 144 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 46da197eb980..e224ab28fb61 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 c719dd29149f..7d6c40f2774d 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 3bcebbf2d48f..f92cdac24bc4 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 "IpcServer.h" + #include -#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 000000000000..93d1011d8445 --- /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 000000000000..f08cfa712064 --- /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 From 1ec36645e1b26488ba9a53c4cfe51b46d7e8c1a4 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Fri, 5 Dec 2025 16:31:30 +0000 Subject: [PATCH 12/24] 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 0df69a0d4c9e..4416920dc08f 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 4903d33c74c1..6a9eb77f65d6 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 9729db220ee0..2d0a64b1ff9d 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 2b00c68bdb9d..036badb34ce6 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 d5cd5ea0e530..18257031634c 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 000000000000..8edcc2d8dc22 --- /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 000000000000..ebe12c80529d --- /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 From 62ccb543dcfe9c47dde20202b12ca65cbc5247e8 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Fri, 5 Dec 2025 17:45:09 +0000 Subject: [PATCH 13/24] feat(ipc): Core IPC server and connected client --- src/apps/deskflow-core/deskflow-core.cpp | 4 ++++ src/lib/common/Constants.h.in | 1 + src/lib/deskflow/CMakeLists.txt | 2 ++ src/lib/deskflow/ipc/CoreIpcServer.cpp | 28 +++++++++++++++++++++++ src/lib/deskflow/ipc/CoreIpcServer.h | 29 ++++++++++++++++++++++++ src/lib/deskflow/ipc/DaemonIpcServer.cpp | 27 ++++------------------ src/lib/deskflow/ipc/DaemonIpcServer.h | 4 ++-- src/lib/deskflow/ipc/IpcServer.cpp | 26 ++++++++++++++++++--- src/lib/deskflow/ipc/IpcServer.h | 3 ++- src/lib/gui/CMakeLists.txt | 2 ++ src/lib/gui/core/CoreProcess.cpp | 22 ++++++++++++++++++ src/lib/gui/core/CoreProcess.h | 2 ++ src/lib/gui/ipc/CoreIpcClient.cpp | 25 ++++++++++++++++++++ src/lib/gui/ipc/CoreIpcClient.h | 23 +++++++++++++++++++ 14 files changed, 170 insertions(+), 28 deletions(-) create mode 100644 src/lib/deskflow/ipc/CoreIpcServer.cpp create mode 100644 src/lib/deskflow/ipc/CoreIpcServer.h create mode 100644 src/lib/gui/ipc/CoreIpcClient.cpp create mode 100644 src/lib/gui/ipc/CoreIpcClient.h diff --git a/src/apps/deskflow-core/deskflow-core.cpp b/src/apps/deskflow-core/deskflow-core.cpp index 3da8edd3d8b7..143b41ccb17c 100644 --- a/src/apps/deskflow-core/deskflow-core.cpp +++ b/src/apps/deskflow-core/deskflow-core.cpp @@ -15,6 +15,7 @@ #include "common/ExitCodes.h" #include "deskflow/ClientApp.h" #include "deskflow/ServerApp.h" +#include "deskflow/ipc/CoreIpcServer.h" #if defined(Q_OS_WIN) #include "arch/win32/ArchMiscWindows.h" @@ -104,6 +105,9 @@ int main(int argc, char **argv) QCoreApplication app(argc, argv); QCoreApplication::setApplicationName(QStringLiteral("%1 Core").arg(kAppName)); + const auto ipcServer = new deskflow::core::ipc::CoreIpcServer(&app); // NOSONAR - Qt managed + ipcServer->listen(); + QThread coreThread; QObject::connect(&coreThread, &QThread::finished, &app, &QCoreApplication::quit); coreApp->run(coreThread); diff --git a/src/lib/common/Constants.h.in b/src/lib/common/Constants.h.in index 7d9545973c5c..1aa97f1c0ca4 100644 --- a/src/lib/common/Constants.h.in +++ b/src/lib/common/Constants.h.in @@ -21,6 +21,7 @@ const auto kCopyright = // "Copyright (C) 2002-2009 Chris Schoeneman"; const auto kCoreBinName = "@CORE_BINARY@"; +const auto kCoreIpcName = "@CMAKE_PROJECT_NAME@-core"; #ifdef _WIN32 diff --git a/src/lib/deskflow/CMakeLists.txt b/src/lib/deskflow/CMakeLists.txt index e224ab28fb61..97021968e4ab 100644 --- a/src/lib/deskflow/CMakeLists.txt +++ b/src/lib/deskflow/CMakeLists.txt @@ -83,6 +83,8 @@ add_library(${lib_name} STATIC ${PLATFORM_CODE} ipc/IpcServer.h ipc/DaemonIpcServer.cpp ipc/DaemonIpcServer.h + ipc/CoreIpcServer.cpp + ipc/CoreIpcServer.h ) target_link_libraries(${lib_name} PUBLIC common Qt6::Core Qt6::Network) diff --git a/src/lib/deskflow/ipc/CoreIpcServer.cpp b/src/lib/deskflow/ipc/CoreIpcServer.cpp new file mode 100644 index 000000000000..c673630708b9 --- /dev/null +++ b/src/lib/deskflow/ipc/CoreIpcServer.cpp @@ -0,0 +1,28 @@ +/* + * 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 "CoreIpcServer.h" + +#include "base/Log.h" +#include "common/Constants.h" + +#include + +namespace deskflow::core::ipc { + +CoreIpcServer::CoreIpcServer(QObject *parent) : IpcServer(parent, kCoreIpcName) +{ + // do nothing +} + +void CoreIpcServer::processCommand(QLocalSocket *clientSocket, const QString &command, const QStringList &parts) +{ + Q_UNUSED(clientSocket) + Q_UNUSED(parts) + LOG_WARN("core ipc server got unknown command: %s", command.toUtf8().constData()); +} + +} // namespace deskflow::core::ipc diff --git a/src/lib/deskflow/ipc/CoreIpcServer.h b/src/lib/deskflow/ipc/CoreIpcServer.h new file mode 100644 index 000000000000..d0f0397b644b --- /dev/null +++ b/src/lib/deskflow/ipc/CoreIpcServer.h @@ -0,0 +1,29 @@ +/* + * 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 "IpcServer.h" + +#include +#include + +class QLocalSocket; + +namespace deskflow::core::ipc { + +class CoreIpcServer : public IpcServer +{ + Q_OBJECT + +public: + explicit CoreIpcServer(QObject *parent); + +private: + void processCommand(QLocalSocket *clientSocket, const QString &command, const QStringList &parts) override; +}; + +} // namespace deskflow::core::ipc diff --git a/src/lib/deskflow/ipc/DaemonIpcServer.cpp b/src/lib/deskflow/ipc/DaemonIpcServer.cpp index 7d6c40f2774d..366151a4129e 100644 --- a/src/lib/deskflow/ipc/DaemonIpcServer.cpp +++ b/src/lib/deskflow/ipc/DaemonIpcServer.cpp @@ -23,29 +23,14 @@ DaemonIpcServer::DaemonIpcServer(QObject *parent, const QString &logFilename) // do nothing } -void DaemonIpcServer::processMessage(QLocalSocket *clientSocket, const QString &message) +void DaemonIpcServer::processCommand(QLocalSocket *clientSocket, const QString &command, const QStringList &parts) { - LOG_DEBUG1("daemon ipc server got message: %s", message.toUtf8().constData()); - const auto parts = message.split('='); - if (parts.size() < 1) { - 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("daemon ipc server got hello message, sending hello back"); - writeToClientSocket(clientSocket, "hello"); - } else if (command == "noop") { - LOG_DEBUG("daemon ipc server got noop message"); - writeToClientSocket(clientSocket, kAckMessage); - } else if (command == "logLevel") { + if (command == "logLevel") { processLogLevel(clientSocket, parts); } else if (command == "elevate") { processElevate(clientSocket, parts); } else if (command == "command") { - processCommand(clientSocket, parts); + processCommandMessage(clientSocket, parts); } else if (command == "start") { LOG_DEBUG("daemon ipc server got start message"); Q_EMIT startProcessRequested(); @@ -62,10 +47,8 @@ void DaemonIpcServer::processMessage(QLocalSocket *clientSocket, const QString & Q_EMIT clearSettingsRequested(); writeToClientSocket(clientSocket, kAckMessage); } else { - LOG_WARN("daemon ipc server got unknown message: %s", message.toUtf8().constData()); + LOG_WARN("daemon ipc server got unknown command: %s", command.toUtf8().constData()); } - - clientSocket->flush(); } void DaemonIpcServer::processLogLevel(QLocalSocket *&clientSocket, const QStringList &messageParts) @@ -108,7 +91,7 @@ void DaemonIpcServer::processElevate(QLocalSocket *&clientSocket, const QStringL writeToClientSocket(clientSocket, kAckMessage); } -void DaemonIpcServer::processCommand(QLocalSocket *&clientSocket, const QStringList &messageParts) +void DaemonIpcServer::processCommandMessage(QLocalSocket *&clientSocket, const QStringList &messageParts) { if (messageParts.size() < 2) { LOG_ERR("daemon ipc server got invalid command message"); diff --git a/src/lib/deskflow/ipc/DaemonIpcServer.h b/src/lib/deskflow/ipc/DaemonIpcServer.h index f92cdac24bc4..a4d205b5fd7f 100644 --- a/src/lib/deskflow/ipc/DaemonIpcServer.h +++ b/src/lib/deskflow/ipc/DaemonIpcServer.h @@ -23,10 +23,10 @@ class DaemonIpcServer : public IpcServer explicit DaemonIpcServer(QObject *parent, const QString &logFilename); private: - void processMessage(QLocalSocket *clientSocket, const QString &message) override; + void processCommand(QLocalSocket *clientSocket, const QString &command, const QStringList &parts) override; void processLogLevel(QLocalSocket *&clientSocket, const QStringList &messageParts); void processElevate(QLocalSocket *&clientSocket, const QStringList &messageParts); - void processCommand(QLocalSocket *&clientSocket, const QStringList &messageParts); + void processCommandMessage(QLocalSocket *&clientSocket, const QStringList &messageParts); private: const QString m_logFilename; diff --git a/src/lib/deskflow/ipc/IpcServer.cpp b/src/lib/deskflow/ipc/IpcServer.cpp index 93d1011d8445..1cf1467af0f0 100644 --- a/src/lib/deskflow/ipc/IpcServer.cpp +++ b/src/lib/deskflow/ipc/IpcServer.cpp @@ -13,9 +13,6 @@ 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 @@ -102,6 +99,29 @@ void IpcServer::handleErrorOccurred() clientSocket->deleteLater(); } +void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &message) +{ + LOG_DEBUG1("ipc server got message: %s", message.toUtf8().constData()); + const auto parts = message.split('='); + if (parts.isEmpty()) { + LOG_ERR("ipc server got invalid message: %s", message.toUtf8().constData()); + writeToClientSocket(clientSocket, "error"); + return; + } + + if (const auto &command = parts.at(0); command == "hello") { + LOG_DEBUG("ipc server got hello message, sending hello back"); + writeToClientSocket(clientSocket, "hello"); + } else if (command == "noop") { + LOG_DEBUG("ipc server got noop message"); + writeToClientSocket(clientSocket, "ok"); + } else { + processCommand(clientSocket, command, parts); + } + + clientSocket->flush(); +} + void IpcServer::writeToClientSocket(QLocalSocket *&clientSocket, const QString &message) const { QByteArray messageData = message.toUtf8() + '\n'; diff --git a/src/lib/deskflow/ipc/IpcServer.h b/src/lib/deskflow/ipc/IpcServer.h index f08cfa712064..7f2c8ffc22e4 100644 --- a/src/lib/deskflow/ipc/IpcServer.h +++ b/src/lib/deskflow/ipc/IpcServer.h @@ -42,7 +42,8 @@ class IpcServer : public QObject void writeToClientSocket(QLocalSocket *&clientSocket, const QString &message) const; private: - virtual void processMessage(QLocalSocket *clientSocket, const QString &message) = 0; + void processMessage(QLocalSocket *clientSocket, const QString &message); + virtual void processCommand(QLocalSocket *clientSocket, const QString &command, const QStringList &parts) = 0; void handleNewConnection(); void handleReadyRead(); void handleDisconnected(); diff --git a/src/lib/gui/CMakeLists.txt b/src/lib/gui/CMakeLists.txt index 4416920dc08f..5d46fea13507 100644 --- a/src/lib/gui/CMakeLists.txt +++ b/src/lib/gui/CMakeLists.txt @@ -85,6 +85,8 @@ add_library(${target} STATIC ipc/IpcClient.h ipc/DaemonIpcClient.cpp ipc/DaemonIpcClient.h + ipc/CoreIpcClient.cpp + ipc/CoreIpcClient.h validators/AliasValidator.cpp validators/AliasValidator.h validators/ComputerNameValidator.cpp diff --git a/src/lib/gui/core/CoreProcess.cpp b/src/lib/gui/core/CoreProcess.cpp index 1e7a7a2ad222..26eed77ad12d 100644 --- a/src/lib/gui/core/CoreProcess.cpp +++ b/src/lib/gui/core/CoreProcess.cpp @@ -8,6 +8,7 @@ #include "CoreProcess.h" #include "common/ExitCodes.h" +#include "gui/ipc/CoreIpcClient.h" #include "gui/ipc/DaemonIpcClient.h" #if defined(Q_OS_MACOS) @@ -372,6 +373,21 @@ void CoreProcess::start(std::optional processModeOption) startProcessFromDaemon(args); } + // Don't block the main GUI render thread when connecting to the Core IPC server. + QTimer::singleShot(kRetryDelay, this, [this] { + if (m_processState != ProcessState::Started) { + qWarning("core process failed to start, skipping core ipc connection"); + return; + } + + m_coreIpcClient = new ipc::CoreIpcClient(this); + if (m_coreIpcClient->connectToServer()) { + qInfo("connected to core ipc server"); + } else { + qWarning("failed to establish core ipc connection"); + } + }); + m_lastProcessMode = processMode; } @@ -384,6 +400,12 @@ void CoreProcess::stop(std::optional processModeOption) qInfo("stopping core process (%s mode)", qPrintable(processModeToString(processMode))); + if (m_coreIpcClient) { + m_coreIpcClient->disconnectFromServer(); + delete m_coreIpcClient; + m_coreIpcClient = nullptr; + } + if (m_processState == ProcessState::Starting) { qDebug("core process is starting, cancelling"); setProcessState(ProcessState::Stopped); diff --git a/src/lib/gui/core/CoreProcess.h b/src/lib/gui/core/CoreProcess.h index 71632010f52c..4db375da98c3 100644 --- a/src/lib/gui/core/CoreProcess.h +++ b/src/lib/gui/core/CoreProcess.h @@ -20,6 +20,7 @@ namespace deskflow::gui { namespace ipc { +class CoreIpcClient; class DaemonIpcClient; } @@ -128,6 +129,7 @@ private Q_SLOTS: std::optional m_lastProcessMode = std::nullopt; QTimer m_retryTimer; int m_connections = 0; + deskflow::gui::ipc::CoreIpcClient *m_coreIpcClient = nullptr; deskflow::gui::ipc::DaemonIpcClient *m_daemonIpcClient = nullptr; FileTail *m_daemonFileTail = nullptr; QProcess *m_process = nullptr; diff --git a/src/lib/gui/ipc/CoreIpcClient.cpp b/src/lib/gui/ipc/CoreIpcClient.cpp new file mode 100644 index 000000000000..309c762afb05 --- /dev/null +++ b/src/lib/gui/ipc/CoreIpcClient.cpp @@ -0,0 +1,25 @@ +/* + * 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 "CoreIpcClient.h" + +#include "common/Constants.h" + +#include +#include +#include +#include + +namespace deskflow::gui::ipc { + +const auto kTimeout = 1000; + +CoreIpcClient::CoreIpcClient(QObject *parent) : IpcClient(parent, kCoreIpcName) +{ + // do nothing +} + +} // namespace deskflow::gui::ipc diff --git a/src/lib/gui/ipc/CoreIpcClient.h b/src/lib/gui/ipc/CoreIpcClient.h new file mode 100644 index 000000000000..eb412ef594a1 --- /dev/null +++ b/src/lib/gui/ipc/CoreIpcClient.h @@ -0,0 +1,23 @@ +/* + * 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 "IpcClient.h" + +#include + +namespace deskflow::gui::ipc { + +class CoreIpcClient : public IpcClient +{ + Q_OBJECT + +public: + explicit CoreIpcClient(QObject *parent = nullptr); +}; + +} // namespace deskflow::gui::ipc From 66c9b6607ce6368c936fec8ff8d593d298e90766 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Thu, 26 Mar 2026 12:34:18 +0000 Subject: [PATCH 14/24] feat(ipc): Messaging for connection states via IPC (async client) --- src/lib/base/Log.h | 2 - src/lib/client/Client.cpp | 4 +- src/lib/deskflow/CMakeLists.txt | 2 + src/lib/deskflow/ClientApp.cpp | 13 +- src/lib/deskflow/ServerApp.cpp | 4 +- src/lib/deskflow/ipc/CoreIpc.cpp | 28 ++++ src/lib/deskflow/ipc/CoreIpc.h | 14 ++ src/lib/deskflow/ipc/CoreIpcServer.cpp | 11 +- src/lib/deskflow/ipc/CoreIpcServer.h | 2 + src/lib/deskflow/ipc/IpcServer.cpp | 25 ++++ src/lib/deskflow/ipc/IpcServer.h | 2 + src/lib/gui/MainWindow.cpp | 1 + src/lib/gui/core/CoreProcess.cpp | 198 +++++++++++++------------ src/lib/gui/core/CoreProcess.h | 12 +- src/lib/gui/ipc/CoreIpcClient.cpp | 8 +- src/lib/gui/ipc/CoreIpcClient.h | 6 + src/lib/gui/ipc/DaemonIpcClient.cpp | 83 +++-------- src/lib/gui/ipc/DaemonIpcClient.h | 16 +- src/lib/gui/ipc/IpcClient.cpp | 181 +++++++++++----------- src/lib/gui/ipc/IpcClient.h | 19 ++- src/lib/net/SecureSocket.cpp | 6 +- src/lib/net/SslLogger.cpp | 10 +- src/lib/server/ClientProxy1_0.cpp | 4 +- src/lib/server/Server.cpp | 24 ++- src/lib/server/Server.h | 1 + 25 files changed, 372 insertions(+), 304 deletions(-) create mode 100644 src/lib/deskflow/ipc/CoreIpc.cpp create mode 100644 src/lib/deskflow/ipc/CoreIpc.h diff --git a/src/lib/base/Log.h b/src/lib/base/Log.h index eb9a53e33392..2697bc8f6b58 100644 --- a/src/lib/base/Log.h +++ b/src/lib/base/Log.h @@ -199,7 +199,6 @@ otherwise it expands to a call that doesn't. // end, then we resort to using non-numerical chars. this still works (since // to deduce the number we subtract octal \060, so '/' is -1, and ':' is 10 -#define CLOG_IPC CLOG_TRACE "%z\056" // char is '' ? #define CLOG_PRINT CLOG_TRACE "%z\057" // char is '/' #define CLOG_CRIT CLOG_TRACE "%z\060" // char is '0' #define CLOG_ERR CLOG_TRACE "%z\061" @@ -210,7 +209,6 @@ otherwise it expands to a call that doesn't. #define CLOG_DEBUG1 CLOG_TRACE "%z\066" #define CLOG_DEBUG2 CLOG_TRACE "%z\067" -#define LOG_IPC(...) LOG((CLOG_IPC __VA_ARGS__)) #define LOG_PRINT(...) LOG((CLOG_PRINT __VA_ARGS__)) #define LOG_CRIT(...) LOG((CLOG_CRIT __VA_ARGS__)) #define LOG_ERR(...) LOG((CLOG_ERR __VA_ARGS__)) diff --git a/src/lib/client/Client.cpp b/src/lib/client/Client.cpp index 634f340dd378..64a2a95cc6ab 100644 --- a/src/lib/client/Client.cpp +++ b/src/lib/client/Client.cpp @@ -14,6 +14,7 @@ #include "base/NetworkProtocol.h" #include "client/ServerProxy.h" #include "common/Settings.h" +#include "deskflow/ipc/CoreIpc.h" #include "deskflow/Clipboard.h" #include "deskflow/IPlatformScreen.h" #include "deskflow/PacketStreamFilter.h" @@ -92,10 +93,11 @@ void Client::connect(size_t addressIndex) // m_serverAddress will be null if the hostname address is not reolved if (m_serverAddress.getAddress() != nullptr) { // to help users troubleshoot, show server host name (issue: 60) - LOG_IPC( + LOG_DEBUG( "connecting to '%s': %s:%i", m_serverAddress.getHostname().c_str(), ARCH->addrToString(m_serverAddress.getAddress()).c_str(), m_serverAddress.getPort() ); + ipcSendConnectionState(deskflow::core::ConnectionState::Connecting); } // create the socket diff --git a/src/lib/deskflow/CMakeLists.txt b/src/lib/deskflow/CMakeLists.txt index 97021968e4ab..2d8ab2a0b6de 100644 --- a/src/lib/deskflow/CMakeLists.txt +++ b/src/lib/deskflow/CMakeLists.txt @@ -83,6 +83,8 @@ add_library(${lib_name} STATIC ${PLATFORM_CODE} ipc/IpcServer.h ipc/DaemonIpcServer.cpp ipc/DaemonIpcServer.h + ipc/CoreIpc.cpp + ipc/CoreIpc.h ipc/CoreIpcServer.cpp ipc/CoreIpcServer.h ) diff --git a/src/lib/deskflow/ClientApp.cpp b/src/lib/deskflow/ClientApp.cpp index 08cce6d1bc93..702ad9763a26 100644 --- a/src/lib/deskflow/ClientApp.cpp +++ b/src/lib/deskflow/ClientApp.cpp @@ -17,6 +17,7 @@ #include "common/Settings.h" #include "deskflow/Screen.h" #include "deskflow/ScreenException.h" +#include "deskflow/ipc/CoreIpc.h" #include "net/NetworkAddress.h" #include "net/SocketException.h" #include "net/SocketMultiplexer.h" @@ -159,10 +160,8 @@ void ClientApp::handleClientRestart(const Event &, EventQueueTimer *timer) void ClientApp::scheduleClientRestart(double retryTime) { - if (Settings::value(Settings::Client::DynamicConnectionRetry).toBool()) - LOG_IPC("retry in %.0f seconds", retryTime); - else - LOG_DEBUG("retry in %.0f seconds", retryTime); + LOG_DEBUG("retry in %.0f seconds", retryTime); + ipcSendToClient("retryIn", QString::number(retryTime, 'f', 0)); // install a timer and handler to retry later EventQueueTimer *timer = getEvents()->newOneShotTimer(retryTime, nullptr); getEvents()->addHandler(EventTypes::Timer, timer, [this, timer](const auto &e) { handleClientRestart(e, timer); }); @@ -170,7 +169,8 @@ void ClientApp::scheduleClientRestart(double retryTime) void ClientApp::handleClientConnected() { - LOG_IPC("connected to server"); + LOG_DEBUG("connected to server"); + ipcSendConnectionState(deskflow::core::ConnectionState::Connected); // Reset server index on successful connection m_currentServerIndex = 0; m_lastServerAddressIndex = 0; @@ -223,7 +223,8 @@ void ClientApp::handleClientRefused(const Event &e) void ClientApp::handleClientDisconnected() { m_retryCount = 0; - LOG_IPC("disconnected from server"); + LOG_DEBUG("disconnected from server"); + ipcSendConnectionState(deskflow::core::ConnectionState::Disconnected); if (!m_suspended) { scheduleClientRestart(retryTime()); } diff --git a/src/lib/deskflow/ServerApp.cpp b/src/lib/deskflow/ServerApp.cpp index 3b5c2e58bc24..4efe27f1098d 100644 --- a/src/lib/deskflow/ServerApp.cpp +++ b/src/lib/deskflow/ServerApp.cpp @@ -18,6 +18,7 @@ #include "deskflow/ProtocolTypes.h" #include "deskflow/Screen.h" #include "deskflow/ScreenException.h" +#include "deskflow/ipc/CoreIpc.h" #include "net/SocketException.h" #include "net/SocketMultiplexer.h" #include "net/TCPSocketFactory.h" @@ -370,7 +371,8 @@ bool ServerApp::startServer() listener->setServer(m_server); m_server->setListener(listener); m_listener = listener; - LOG_IPC("started server, waiting for clients"); + LOG_DEBUG("started server, waiting for clients"); + ipcSendConnectionState(deskflow::core::ConnectionState::Listening); m_serverState = Started; return true; } catch (SocketAddressInUseException &e) { diff --git a/src/lib/deskflow/ipc/CoreIpc.cpp b/src/lib/deskflow/ipc/CoreIpc.cpp new file mode 100644 index 000000000000..7dad7ca71775 --- /dev/null +++ b/src/lib/deskflow/ipc/CoreIpc.cpp @@ -0,0 +1,28 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2026 Symless Ltd. + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#include "CoreIpc.h" + +#include "CoreIpcServer.h" + +#include + +void ipcSendToClient(const QString &command, const QString &args) +{ + // Queued because callers may not be on the main thread, + // and QLocalSocket can only be written to from its owning thread. + auto &server = deskflow::core::ipc::CoreIpcServer::instance(); + QMetaObject::invokeMethod( + &server, [command, args] { deskflow::core::ipc::CoreIpcServer::instance().broadcastCommand(command, args); }, + Qt::QueuedConnection + ); +} + +void ipcSendConnectionState(deskflow::core::ConnectionState state) +{ + const auto metaEnum = QMetaEnum::fromType(); + ipcSendToClient("connectionState", metaEnum.valueToKey(static_cast(state))); +} diff --git a/src/lib/deskflow/ipc/CoreIpc.h b/src/lib/deskflow/ipc/CoreIpc.h new file mode 100644 index 000000000000..738055cf03b6 --- /dev/null +++ b/src/lib/deskflow/ipc/CoreIpc.h @@ -0,0 +1,14 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2026 Symless Ltd. + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#pragma once + +#include "common/Enums.h" + +#include + +void ipcSendToClient(const QString &command, const QString &args = ""); +void ipcSendConnectionState(deskflow::core::ConnectionState state); diff --git a/src/lib/deskflow/ipc/CoreIpcServer.cpp b/src/lib/deskflow/ipc/CoreIpcServer.cpp index c673630708b9..4ead60f9f8d9 100644 --- a/src/lib/deskflow/ipc/CoreIpcServer.cpp +++ b/src/lib/deskflow/ipc/CoreIpcServer.cpp @@ -13,9 +13,18 @@ namespace deskflow::core::ipc { +static CoreIpcServer *s_instance = nullptr; + CoreIpcServer::CoreIpcServer(QObject *parent) : IpcServer(parent, kCoreIpcName) { - // do nothing + assert(s_instance == nullptr); + s_instance = this; +} + +CoreIpcServer &CoreIpcServer::instance() +{ + assert(s_instance != nullptr); + return *s_instance; } void CoreIpcServer::processCommand(QLocalSocket *clientSocket, const QString &command, const QStringList &parts) diff --git a/src/lib/deskflow/ipc/CoreIpcServer.h b/src/lib/deskflow/ipc/CoreIpcServer.h index d0f0397b644b..bfdc822f00b6 100644 --- a/src/lib/deskflow/ipc/CoreIpcServer.h +++ b/src/lib/deskflow/ipc/CoreIpcServer.h @@ -22,6 +22,8 @@ class CoreIpcServer : public IpcServer public: explicit CoreIpcServer(QObject *parent); + static CoreIpcServer &instance(); + private: void processCommand(QLocalSocket *clientSocket, const QString &command, const QStringList &parts) override; }; diff --git a/src/lib/deskflow/ipc/IpcServer.cpp b/src/lib/deskflow/ipc/IpcServer.cpp index 1cf1467af0f0..131c1d93d2fc 100644 --- a/src/lib/deskflow/ipc/IpcServer.cpp +++ b/src/lib/deskflow/ipc/IpcServer.cpp @@ -112,6 +112,14 @@ void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &messag if (const auto &command = parts.at(0); command == "hello") { LOG_DEBUG("ipc server got hello message, sending hello back"); writeToClientSocket(clientSocket, "hello"); + + // Replay messages that were queued before any clients connected. + LOG_DEBUG1("ipc server replaying %d pending messages", m_pendingMessages.size()); + for (const auto &pending : std::as_const(m_pendingMessages)) { + LOG_DEBUG1("ipc server replaying: %s", pending.toUtf8().constData()); + writeToClientSocket(clientSocket, pending); + } + m_pendingMessages.clear(); } else if (command == "noop") { LOG_DEBUG("ipc server got noop message"); writeToClientSocket(clientSocket, "ok"); @@ -122,6 +130,23 @@ void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &messag clientSocket->flush(); } +void IpcServer::broadcastCommand(const QString &command, const QString &args) +{ + const auto message = args.isEmpty() ? command : command + "=" + args; + + if (m_clients.isEmpty()) { + LOG_DEBUG1("ipc server has no clients, message queued: %s", message.toUtf8().constData()); + m_pendingMessages.append(message); + return; + } + + LOG_DEBUG1("ipc server broadcasting message to %d clients: %s", m_clients.size(), message.toUtf8().constData()); + for (auto *client : std::as_const(m_clients)) { + writeToClientSocket(client, message); + client->flush(); + } +} + void IpcServer::writeToClientSocket(QLocalSocket *&clientSocket, const QString &message) const { QByteArray messageData = message.toUtf8() + '\n'; diff --git a/src/lib/deskflow/ipc/IpcServer.h b/src/lib/deskflow/ipc/IpcServer.h index 7f2c8ffc22e4..3e2a874b9ef0 100644 --- a/src/lib/deskflow/ipc/IpcServer.h +++ b/src/lib/deskflow/ipc/IpcServer.h @@ -23,6 +23,7 @@ class IpcServer : public QObject ~IpcServer() override; void listen(); + void broadcastCommand(const QString &command, const QString &args = ""); Q_SIGNALS: void logLevelChanged(const QString &logLevel); @@ -52,6 +53,7 @@ class IpcServer : public QObject QLocalServer *m_server; QSet m_clients; QString m_serverName; + QStringList m_pendingMessages; }; } // namespace deskflow::core::ipc diff --git a/src/lib/gui/MainWindow.cpp b/src/lib/gui/MainWindow.cpp index 6a9eb77f65d6..f2f396740f62 100644 --- a/src/lib/gui/MainWindow.cpp +++ b/src/lib/gui/MainWindow.cpp @@ -271,6 +271,7 @@ void MainWindow::connectSlots() connect(&m_serverConnection, &ServerConnection::configureClient, this, &MainWindow::serverConnectionConfigureClient); connect(&m_serverConnection, &ServerConnection::clientsChanged, this, &MainWindow::serverClientsChanged); + connect(&m_coreProcess, &CoreProcess::connectedClientsChanged, this, &MainWindow::serverClientsChanged); connect( &m_serverConnection, &ServerConnection::requestNewClientPrompt, this, &MainWindow::handleNewClientPromptRequest ); diff --git a/src/lib/gui/core/CoreProcess.cpp b/src/lib/gui/core/CoreProcess.cpp index 26eed77ad12d..ef61cbe57594 100644 --- a/src/lib/gui/core/CoreProcess.cpp +++ b/src/lib/gui/core/CoreProcess.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -112,6 +113,7 @@ CoreProcess::CoreProcess(const IServerConfig &serverConfig) connect( m_daemonIpcClient, &ipc::DaemonIpcClient::connectionFailed, this, &CoreProcess::daemonIpcClientConnectionFailed ); + connect(m_daemonIpcClient, &ipc::DaemonIpcClient::logPathReceived, this, &CoreProcess::setupDaemonLogTail); connect(&m_retryTimer, &QTimer::timeout, this, [this] { if (m_processState == ProcessState::RetryPending) { @@ -139,20 +141,7 @@ void CoreProcess::onProcessReadyReadStandardError() void CoreProcess::daemonIpcClientConnected() { applyLogLevel(); - - const auto logPath = requestDaemonLogPath(); - if (logPath.isEmpty()) { - qWarning() << "daemon no log path"; - return; - } - - qDebug() << "daemon log path:" << logPath; - if (m_daemonFileTail) { - m_daemonFileTail->setWatchedFile(logPath); - } else { - m_daemonFileTail = new FileTail(logPath, this); - connect(m_daemonFileTail, &FileTail::newLine, this, &CoreProcess::handleLogLines); - } + m_daemonIpcClient->requestLogPath(); } void CoreProcess::onProcessFinished(int exitCode, QProcess::ExitStatus) @@ -190,9 +179,7 @@ void CoreProcess::applyLogLevel() const auto processMode = Settings::value(Settings::Core::ProcessMode).value(); if (processMode == ProcessMode::Service) { qDebug() << "setting daemon log level:" << Settings::logLevelText(); - if (!m_daemonIpcClient->sendLogLevel(Settings::logLevelText())) { - qWarning() << "failed to set daemon ipc log level"; - } + m_daemonIpcClient->sendLogLevel(Settings::logLevelText()); } } @@ -234,15 +221,22 @@ void CoreProcess::startProcessFromDaemon(const QStringList &args) } QString commandQuoted = makeQuotedArgs(m_appPath, args); - qInfo("running command: %s", qPrintable(commandQuoted)); - if (!m_daemonIpcClient->sendStartProcess(commandQuoted, Settings::value(Settings::Daemon::Elevate).toBool())) { - qWarning("cannot start process, ipc command failed"); - return; - } + auto sendStart = [this, commandQuoted] { + m_daemonIpcClient->sendStartProcess(commandQuoted, Settings::value(Settings::Daemon::Elevate).toBool()); + setProcessState(ProcessState::Started); + }; - setProcessState(ProcessState::Started); + if (m_daemonIpcClient->isConnected()) { + sendStart(); + } else { + connect( + m_daemonIpcClient, &ipc::DaemonIpcClient::connected, this, sendStart, + static_cast(Qt::SingleShotConnection | Qt::QueuedConnection) + ); + m_daemonIpcClient->connectToServer(); + } } void CoreProcess::stopForegroundProcess() const @@ -271,12 +265,20 @@ void CoreProcess::stopProcessFromDaemon() qFatal("core process must be in stopping state"); } - if (!m_daemonIpcClient->sendStopProcess()) { - qWarning("cannot stop process, ipc command failed"); - return; - } + auto sendStop = [this] { + m_daemonIpcClient->sendStopProcess(); + setProcessState(ProcessState::Stopped); + }; - setProcessState(ProcessState::Stopped); + if (m_daemonIpcClient->isConnected()) { + sendStop(); + } else { + connect( + m_daemonIpcClient, &ipc::DaemonIpcClient::connected, this, sendStop, + static_cast(Qt::SingleShotConnection | Qt::QueuedConnection) + ); + m_daemonIpcClient->connectToServer(); + } } void CoreProcess::handleLogLines(const QString &text) @@ -293,9 +295,23 @@ void CoreProcess::handleLogLines(const QString &text) if (line.contains("calling TIS/TSM in non-main thread environment")) { continue; } + + // the core process is not allowed to show the permission prompt + // (called "notification permission") and the notification log line is emitted from + // deep inside cocoa code in the core binary to stdout, so it can't be sent over + // ipc from the core to the gui and instead the gui has to parse the core output. + static const QString needle = "OSX Notification: "; + if (line.contains(needle) && line.contains('|')) { + const int delimiterPosition = line.indexOf('|'); + const int start = line.indexOf(needle); + const QString title = line.mid(start + needle.length(), delimiterPosition - start - needle.length()); + const QString body = line.mid(delimiterPosition + 1, line.length() - delimiterPosition); + if (!showOSXNotification(title, body)) { + qDebug("osx notification was not shown"); + } + } #endif - checkLogLine(line); Q_EMIT logLine(line); } } @@ -366,6 +382,33 @@ void CoreProcess::start(std::optional processModeOption) qInfo().noquote() << "log file:" << logFile; } + // Wired before the start calls so it catches Started from both sync (desktop) and async (service) paths. + connect( + this, &CoreProcess::processStateChanged, this, + [this](ProcessState state) { + if (state != ProcessState::Started) { + return; + } + + // Delay briefly to give the core process time to start its IPC server. + QTimer::singleShot(kRetryDelay, this, [this] { + if (m_processState != ProcessState::Started) { + return; + } + + m_coreIpcClient = new ipc::CoreIpcClient(this); + connect(m_coreIpcClient, &ipc::CoreIpcClient::commandReceived, this, &CoreProcess::onCoreIpcMessageReceived); + connect(m_coreIpcClient, &ipc::CoreIpcClient::connected, this, [] { qInfo("connected to core ipc server"); }); + connect(m_coreIpcClient, &ipc::CoreIpcClient::connectionFailed, this, [] { + qWarning("failed to establish core ipc connection"); + }); + + m_coreIpcClient->connectToServer(); + }); + }, + static_cast(Qt::SingleShotConnection | Qt::QueuedConnection) + ); + if (processMode == ProcessMode::Desktop) { startForegroundProcess(args); } else if (processMode == ProcessMode::Service) { @@ -373,21 +416,6 @@ void CoreProcess::start(std::optional processModeOption) startProcessFromDaemon(args); } - // Don't block the main GUI render thread when connecting to the Core IPC server. - QTimer::singleShot(kRetryDelay, this, [this] { - if (m_processState != ProcessState::Started) { - qWarning("core process failed to start, skipping core ipc connection"); - return; - } - - m_coreIpcClient = new ipc::CoreIpcClient(this); - if (m_coreIpcClient->connectToServer()) { - qInfo("connected to core ipc server"); - } else { - qWarning("failed to establish core ipc connection"); - } - }); - m_lastProcessMode = processMode; } @@ -500,35 +528,27 @@ void CoreProcess::setProcessState(ProcessState state) Q_EMIT processStateChanged(state); } -void CoreProcess::checkLogLine(const QString &line) +void CoreProcess::onCoreIpcMessageReceived(const QString &command, const QString &args) { - using enum ConnectionState; - - if (line.contains("connected to server") || line.contains("has connected")) { - m_connections++; - setConnectionState(Connected); - } else if (line.contains("started server")) { - m_connections = 0; - setConnectionState(Listening); - } else if (line.contains("disconnected from server") || line.contains("process exited")) { - m_connections = 0; - setConnectionState(Disconnected); - } else if (line.contains("connecting to")) { - setConnectionState(Connecting); - } else if (line.contains("has disconnected")) { - m_connections--; - if (m_connections < 1) { - setConnectionState(Listening); + if (command == "connectionState") { + const auto metaEnum = QMetaEnum::fromType(); + bool ok = false; + const auto state = static_cast(metaEnum.keyToValue(args.toUtf8().constData(), &ok)); + if (!ok) { + qWarning("core ipc got unknown connection state: %s", args.toUtf8().constData()); + return; + } + setConnectionState(state); + } else if (command == "connectedClients") { + const auto clients = args.isEmpty() ? QStringList() : args.split(","); + Q_EMIT connectedClientsChanged(clients); + } else if (command == "secureSocket") { + Q_EMIT secureSocket(true); + if (args != m_secureSocketVersion) { + m_secureSocketVersion = args; + Q_EMIT securityLevelChanged(args); } } - - checkSecureSocket(line); - - // server and client processes are not allowed to show notifications. - // process the log from it and show notification from deskflow instead. -#ifdef Q_OS_MACOS - checkOSXNotification(line); -#endif } bool CoreProcess::checkSecureSocket(const QString &line) @@ -548,46 +568,30 @@ bool CoreProcess::checkSecureSocket(const QString &line) return true; } -#ifdef Q_OS_MACOS -void CoreProcess::checkOSXNotification(const QString &line) -{ - static const QString needle = "OSX Notification: "; - if (line.contains(needle) && line.contains('|')) { - int delimiterPosition = line.indexOf('|'); - int start = line.indexOf(needle); - QString title = line.mid(start + needle.length(), delimiterPosition - start - needle.length()); - QString body = line.mid(delimiterPosition + 1, line.length() - delimiterPosition); - if (!showOSXNotification(title, body)) { - qDebug("osx notification was not shown"); - } - } -} -#endif - QString CoreProcess::correctedAddress(const QString &address) const { return wrapIpv6(address.simplified()); } -QString CoreProcess::requestDaemonLogPath() +void CoreProcess::setupDaemonLogTail(const QString &logPath) { - qDebug() << "requesting daemon log path"; - const auto logPath = m_daemonIpcClient->requestLogPath(); - if (logPath.isEmpty()) { - qCritical() << "failed to get daemon log path"; - return QString(); - } + qDebug() << "daemon log path:" << logPath; if (QFileInfo logFile(logPath); !logFile.isFile()) { auto file = QFile(logPath); if (!file.open(QFile::ReadWrite)) { qCritical() << "daemon log path file can not be written:" << logPath; - return QString(); + return; } file.write(""); // Create an empty file } - return logPath; + if (m_daemonFileTail) { + m_daemonFileTail->setWatchedFile(logPath); + } else { + m_daemonFileTail = new FileTail(logPath, this); + connect(m_daemonFileTail, &FileTail::newLine, this, &CoreProcess::handleLogLines); + } } void CoreProcess::clearSettings() @@ -608,9 +612,7 @@ void CoreProcess::clearSettings() void CoreProcess::retryDaemon() { - if (m_daemonIpcClient->connectToServer()) { - qInfo("successfully reconnected to daemon"); - } + m_daemonIpcClient->connectToServer(); } } // namespace deskflow::gui diff --git a/src/lib/gui/core/CoreProcess.h b/src/lib/gui/core/CoreProcess.h index 4db375da98c3..25abd0ac7960 100644 --- a/src/lib/gui/core/CoreProcess.h +++ b/src/lib/gui/core/CoreProcess.h @@ -22,7 +22,7 @@ namespace deskflow::gui { namespace ipc { class CoreIpcClient; class DaemonIpcClient; -} +} // namespace ipc class CoreProcess : public QObject { @@ -89,12 +89,14 @@ class CoreProcess : public QObject void processStateChanged(deskflow::core::ProcessState state); void secureSocket(bool enabled); void daemonIpcClientConnectionFailed(); + void connectedClientsChanged(const QStringList &clients); void securityLevelChanged(QString securityLevel); private Q_SLOTS: void onProcessFinished(int exitCode, QProcess::ExitStatus); void onProcessReadyReadStandardOutput(); void onProcessReadyReadStandardError(); + void onCoreIpcMessageReceived(const QString &command, const QString &args); void daemonIpcClientConnected(); private: @@ -105,20 +107,15 @@ private Q_SLOTS: QPair persistServerConfig() const; void setConnectionState(ConnectionState state); void setProcessState(ProcessState state); - void checkLogLine(const QString &line); bool checkSecureSocket(const QString &line); void handleLogLines(const QString &text); QString correctedAddress(const QString &address) const; - QString requestDaemonLogPath(); + void setupDaemonLogTail(const QString &logPath); static QString makeQuotedArgs(const QString &app, const QStringList &args); static QString processModeToString(const Settings::ProcessMode mode); static QString processStateToString(const CoreProcess::ProcessState state); static QString wrapIpv6(const QString &address); -#ifdef Q_OS_MACOS - void checkOSXNotification(const QString &line); -#endif - const IServerConfig &m_serverConfig; QString m_address; ProcessState m_processState = ProcessState::Stopped; @@ -128,7 +125,6 @@ private Q_SLOTS: QString m_secureSocketVersion; std::optional m_lastProcessMode = std::nullopt; QTimer m_retryTimer; - int m_connections = 0; deskflow::gui::ipc::CoreIpcClient *m_coreIpcClient = nullptr; deskflow::gui::ipc::DaemonIpcClient *m_daemonIpcClient = nullptr; FileTail *m_daemonFileTail = nullptr; diff --git a/src/lib/gui/ipc/CoreIpcClient.cpp b/src/lib/gui/ipc/CoreIpcClient.cpp index 309c762afb05..047761efdd08 100644 --- a/src/lib/gui/ipc/CoreIpcClient.cpp +++ b/src/lib/gui/ipc/CoreIpcClient.cpp @@ -15,11 +15,15 @@ namespace deskflow::gui::ipc { -const auto kTimeout = 1000; - CoreIpcClient::CoreIpcClient(QObject *parent) : IpcClient(parent, kCoreIpcName) { // do nothing } +void CoreIpcClient::processCommand(const QString &command, const QStringList &parts) +{ + const auto args = parts.size() >= 2 ? parts[1] : QString(); + Q_EMIT commandReceived(command, args); +} + } // namespace deskflow::gui::ipc diff --git a/src/lib/gui/ipc/CoreIpcClient.h b/src/lib/gui/ipc/CoreIpcClient.h index eb412ef594a1..bccb1ccd4e9f 100644 --- a/src/lib/gui/ipc/CoreIpcClient.h +++ b/src/lib/gui/ipc/CoreIpcClient.h @@ -18,6 +18,12 @@ class CoreIpcClient : public IpcClient public: explicit CoreIpcClient(QObject *parent = nullptr); + +Q_SIGNALS: + void commandReceived(const QString &command, const QString &args); + +protected: + void processCommand(const QString &command, const QStringList &parts) override; }; } // namespace deskflow::gui::ipc diff --git a/src/lib/gui/ipc/DaemonIpcClient.cpp b/src/lib/gui/ipc/DaemonIpcClient.cpp index 036badb34ce6..124967eac37d 100644 --- a/src/lib/gui/ipc/DaemonIpcClient.cpp +++ b/src/lib/gui/ipc/DaemonIpcClient.cpp @@ -9,93 +9,46 @@ #include "common/Constants.h" #include -#include -#include -#include -#include namespace deskflow::gui::ipc { -const auto kTimeout = 1000; - DaemonIpcClient::DaemonIpcClient(QObject *parent) : IpcClient(parent, kDaemonIpcName) { } -bool DaemonIpcClient::sendLogLevel(const QString &logLevel) +void DaemonIpcClient::sendLogLevel(const QString &logLevel) { - if (!keepAlive()) - return false; - sendMessage("logLevel=" + logLevel); - return true; } -bool DaemonIpcClient::sendStartProcess(const QString &command, bool elevate) +void DaemonIpcClient::sendStartProcess(const QString &command, bool elevate) { - if (!keepAlive()) - return false; - - if (!sendMessage("elevate=" + (elevate ? QStringLiteral("yes") : QStringLiteral("no")))) { - return false; - } - - if (!sendMessage("command=" + command)) { - return false; - } - - return sendMessage("start"); + const auto elevateStr = elevate ? QStringLiteral("yes") : QStringLiteral("no"); + sendMessage("elevate=" + elevateStr); + sendMessage("command=" + command); + sendMessage("start"); } -bool DaemonIpcClient::sendStopProcess() +void DaemonIpcClient::sendStopProcess() { - return sendMessage("stop"); + sendMessage("stop"); } -QString DaemonIpcClient::requestLogPath() +void DaemonIpcClient::sendClearSettings() { - if (!keepAlive()) - return QString(); - - if (!sendMessage("logPath", QString())) { - return QString(); - } - - if (!socket()->waitForReadyRead(kTimeout)) { - qWarning() << "daemon ipc client failed to read log path response"; - return QString(); - } - - QByteArray response = socket()->readAll(); - if (response.isEmpty()) { - qWarning() << "daemon ipc client got empty log path response"; - return QString(); - } - - QString responseData = QString::fromUtf8(response); - if (responseData.isEmpty()) { - qWarning() << "daemon ipc client failed to convert log path response to string"; - return QString(); - } - - // 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; - return QString(); - } - - if (parts[0] != "logPath") { - qWarning() << "daemon ipc client got unexpected log path response:" << responseData; - return QString(); - } + sendMessage("clearSettings"); +} - return parts[1]; +void DaemonIpcClient::requestLogPath() +{ + sendMessage("logPath"); } -bool DaemonIpcClient::sendClearSettings() +void DaemonIpcClient::processCommand(const QString &command, const QStringList &parts) { - return sendMessage("clearSettings"); + if (command == "logPath" && parts.size() == 2) { + Q_EMIT logPathReceived(parts[1]); + } } } // namespace deskflow::gui::ipc diff --git a/src/lib/gui/ipc/DaemonIpcClient.h b/src/lib/gui/ipc/DaemonIpcClient.h index 18257031634c..9b72bfd487e2 100644 --- a/src/lib/gui/ipc/DaemonIpcClient.h +++ b/src/lib/gui/ipc/DaemonIpcClient.h @@ -18,11 +18,17 @@ class DaemonIpcClient : public IpcClient public: explicit DaemonIpcClient(QObject *parent = nullptr); - bool sendLogLevel(const QString &logLevel); - bool sendStartProcess(const QString &command, bool elevate); - bool sendStopProcess(); - bool sendClearSettings(); - QString requestLogPath(); + void sendLogLevel(const QString &logLevel); + void sendStartProcess(const QString &command, bool elevate); + void sendStopProcess(); + void sendClearSettings(); + void requestLogPath(); + +Q_SIGNALS: + void logPathReceived(const QString &logPath); + +protected: + void processCommand(const QString &command, const QStringList &parts) override; }; } // namespace deskflow::gui::ipc diff --git a/src/lib/gui/ipc/IpcClient.cpp b/src/lib/gui/ipc/IpcClient.cpp index 8edcc2d8dc22..1dfdd482b1f6 100644 --- a/src/lib/gui/ipc/IpcClient.cpp +++ b/src/lib/gui/ipc/IpcClient.cpp @@ -8,9 +8,7 @@ #include #include -#include -#include -#include +#include namespace deskflow::gui::ipc { @@ -24,13 +22,14 @@ IpcClient::IpcClient(QObject *parent, const QString &socketName) { connect(m_socket, &QLocalSocket::disconnected, this, &IpcClient::handleDisconnected); connect(m_socket, &QLocalSocket::errorOccurred, this, &IpcClient::handleErrorOccurred); + connect(m_socket, &QLocalSocket::readyRead, this, &IpcClient::handleReadyRead); } -bool IpcClient::connectToServer() +void IpcClient::connectToServer() { if (m_state == State::Connecting) { qWarning() << "ipc client already connecting to server"; - return false; + return; } if (m_state != State::Unconnected) { @@ -43,139 +42,129 @@ bool IpcClient::connectToServer() 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; - } + m_retryCount = 0; + attemptConnection(); +} - if (!sendMessage("hello", "hello", false)) { - qWarning() << "ipc client failed to send hello"; - disconnectFromServer(); - continue; - } +void IpcClient::attemptConnection() +{ + if (m_retryCount >= kRetryLimit) { + qWarning() << "ipc client failed to connect after" << kRetryLimit << "attempts"; + m_state = State::Unconnected; + Q_EMIT connectionFailed(); + return; + } - m_state = State::Connected; - qDebug() << "ipc client connected"; - Q_EMIT connected(); - return true; + if (m_retryCount == 0) { + qDebug() << "ipc client connecting to server:" << m_socketName; + } else { + qDebug() << "ipc client retrying connection, attempt:" << m_retryCount + 1; } - qWarning() << "ipc client failed to connect after" << kRetryLimit << "attempts"; - disconnectFromServer(); - Q_EMIT connectionFailed(); - return false; + m_state = State::Connecting; + m_retryCount++; + + connect( + m_socket, &QLocalSocket::connected, this, + [this] { + m_socket->write("hello\n"); + qDebug() << "ipc client sent hello"; + }, + Qt::SingleShotConnection + ); + + connect( + m_socket, &QLocalSocket::errorOccurred, this, + [this] { + qWarning() << "ipc client failed to connect:" << m_socket->errorString(); + m_socket->disconnectFromServer(); + m_state = State::Unconnected; + QTimer::singleShot(0, this, &IpcClient::attemptConnection); + }, + Qt::SingleShotConnection + ); + + m_socket->connectToServer(m_socketName); } 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(); + if (m_state == State::Connecting) { + return; } + qDebug() << "ipc client disconnected from server"; + const auto wasConnected = m_state == State::Connected; m_state = State::Unconnected; + + if (wasConnected) { + Q_EMIT connectionFailed(); + } } void IpcClient::handleErrorOccurred() { + if (m_state == State::Connecting) { + return; + } + qWarning() << "ipc client error:" << m_socket->errorString(); - disconnectFromServer(); if (m_state == State::Connected) { + disconnectFromServer(); Q_EMIT connectionFailed(); } } -bool IpcClient::sendMessage(const QString &message, const QString &expectAck, const bool expectConnected) +void IpcClient::handleReadyRead() { - 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; + QByteArray data = m_readBuffer + m_socket->readAll(); + m_readBuffer.clear(); + + while (data.contains('\n')) { + const auto index = data.indexOf('\n'); + const auto message = QString::fromUtf8(data.left(index)); + data.remove(0, index + 1); + + qDebug("ipc client message: %s", message.toUtf8().constData()); + const auto parts = message.split('='); + if (parts.isEmpty()) { + qWarning("ipc client got invalid message: %s", message.toUtf8().constData()); + continue; } - QString responseData = QString::fromUtf8(response); - if (responseData.isEmpty()) { - qWarning() << "ipc client failed to convert response to string"; - return false; + if (m_state == State::Connecting && parts.at(0) == "hello") { + m_state = State::Connected; + qDebug() << "ipc client connected"; + Q_EMIT connected(); + continue; } - if (responseData != expectAck + "\n") { - qWarning() << "ipc client got unexpected response:" << responseData; - return false; - } + processCommand(parts.at(0), parts); } - qDebug() << "ipc client sent message:" << messageData; - return true; + if (!data.isEmpty()) { + m_readBuffer = data; + } } -bool IpcClient::keepAlive() +void IpcClient::sendMessage(const QString &message) { - 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; + if (m_state != State::Connected) { + qWarning() << "cannot send command, ipc client not connected"; + return; } - return true; + m_socket->write(message.toUtf8() + "\n"); + qDebug() << "ipc client sent message:" << message; } } // namespace deskflow::gui::ipc diff --git a/src/lib/gui/ipc/IpcClient.h b/src/lib/gui/ipc/IpcClient.h index ebe12c80529d..8e4068c5c9cf 100644 --- a/src/lib/gui/ipc/IpcClient.h +++ b/src/lib/gui/ipc/IpcClient.h @@ -6,7 +6,6 @@ #pragma once -#include #include class QLocalSocket; @@ -28,7 +27,7 @@ class IpcClient : public QObject public: explicit IpcClient(QObject *parent, const QString &socketName); - bool connectToServer(); + void connectToServer(); void disconnectFromServer(); bool isConnected() const @@ -43,21 +42,25 @@ class IpcClient : public QObject private Q_SLOTS: void handleDisconnected(); void handleErrorOccurred(); + void handleReadyRead(); protected: - bool keepAlive(); - bool sendMessage(const QString &message, const QString &expectAck = "ok", const bool expectConnected = true); - - QLocalSocket *socket() const + virtual void processCommand(const QString &command, const QStringList &parts) { - return m_socket; + Q_UNUSED(command) + Q_UNUSED(parts) } + void sendMessage(const QString &message); + private: + void attemptConnection(); + QLocalSocket *m_socket; State m_state{State::Unconnected}; QString m_socketName; - QMutex m_mutex; + QByteArray m_readBuffer; + int m_retryCount{0}; }; } // namespace deskflow::gui::ipc diff --git a/src/lib/net/SecureSocket.cpp b/src/lib/net/SecureSocket.cpp index 3651443ea63d..3198f96908bf 100644 --- a/src/lib/net/SecureSocket.cpp +++ b/src/lib/net/SecureSocket.cpp @@ -12,6 +12,7 @@ #include "base/IEventQueue.h" #include "base/Log.h" #include "common/Settings.h" +#include "deskflow/ipc/CoreIpc.h" #include "mt/Lock.h" #include "net/FingerprintDatabase.h" #include "net/TCPSocket.h" @@ -625,8 +626,9 @@ bool SecureSocket::verifyCertFingerprint(const QString &FingerprintDatabasePath) if (!sha256.isValid()) return false; - // Gui Must Parse this line, DO NOT CHANGE - LOG_IPC("peer fingerprint: %s", qPrintable(deskflow::formatSSLFingerprint(sha256.data, false))); + const auto fingerprint = deskflow::formatSSLFingerprint(sha256.data, false); + LOG_DEBUG("peer fingerprint: %s", qPrintable(fingerprint)); + ipcSendToClient("peerFingerprint", fingerprint); QFile file(FingerprintDatabasePath); diff --git a/src/lib/net/SslLogger.cpp b/src/lib/net/SslLogger.cpp index 06bace07efb7..4bc03706444b 100644 --- a/src/lib/net/SslLogger.cpp +++ b/src/lib/net/SslLogger.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -105,13 +106,12 @@ void SslLogger::logSecureConnectInfo(const SSL *ssl) std::istream_iterator{iss}, std::istream_iterator{} }; if (parts.size() > 2) { - // log the section containing the protocol version - LOG_INFO("network encryption protocol: %s", parts[1].c_str()); + LOG_DEBUG("network encryption protocol: %s", parts[1].c_str()); + ipcSendToClient("secureSocket", parts[1].c_str()); } else { - // log the error in spliting then display the whole description rather - // then nothing LOG_ERR("could not split cipher for protocol"); - LOG_INFO("network encryption protocol: %s", msg); + LOG_DEBUG("network encryption protocol: %s", msg); + ipcSendToClient("secureSocket", msg); } } else { LOG_ERR("could not get secure socket cipher"); diff --git a/src/lib/server/ClientProxy1_0.cpp b/src/lib/server/ClientProxy1_0.cpp index a59056055f8a..3becfaa563e3 100644 --- a/src/lib/server/ClientProxy1_0.cpp +++ b/src/lib/server/ClientProxy1_0.cpp @@ -185,7 +185,7 @@ bool ClientProxy1_0::parseMessage(const uint8_t *code) void ClientProxy1_0::handleDisconnect() { - LOG_IPC("client \"%s\" has disconnected", getName().c_str()); + LOG_DEBUG("client \"%s\" has disconnected", getName().c_str()); disconnect(); } @@ -198,7 +198,7 @@ void ClientProxy1_0::handleWriteError() void ClientProxy1_0::handleFlatline() { // didn't get a heartbeat fast enough. assume client is dead. - LOG_IPC("client \"%s\" is dead", getName().c_str()); + LOG_DEBUG("client \"%s\" is dead", getName().c_str()); disconnect(); } diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index 3ddf06537a71..df505ea9533d 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -11,6 +11,7 @@ #include "base/IEventQueue.h" #include "base/Log.h" #include "deskflow/AppUtil.h" +#include "deskflow/ipc/CoreIpc.h" #include "deskflow/DeskflowException.h" #include "deskflow/IPlatformScreen.h" #include "deskflow/OptionTypes.h" @@ -233,7 +234,7 @@ void Server::adoptClient(BaseClientProxy *client) // name must be in our configuration if (!m_config->isScreen(client->getName())) { - LOG_IPC("unrecognised client name \"%s\", check server config", client->getName().c_str()); + LOG_WARN("unrecognised client name \"%s\", check server config", client->getName().c_str()); closeClient(client, kMsgEUnknown); return; } @@ -245,7 +246,9 @@ void Server::adoptClient(BaseClientProxy *client) closeClient(client, kMsgEBusy); return; } - LOG_IPC("client \"%s\" has connected", getName(client).c_str()); + LOG_DEBUG("client \"%s\" has connected", getName(client).c_str()); + ipcSendConnectionState(deskflow::core::ConnectionState::Connected); + sendConnectedClientsIpc(); // send configuration options to client sendOptions(client); @@ -291,6 +294,18 @@ void Server::getClients(std::vector &list) const } } +void Server::sendConnectedClientsIpc() const +{ + const auto primaryName = getName(m_primaryClient); + QStringList clientList; + for (const auto &[name, _] : m_clients) { + if (name != primaryName) { + clientList.append(QString::fromStdString(name)); + } + } + ipcSendToClient("connectedClients", clientList.join(",")); +} + std::string Server::getName(const BaseClientProxy *client) const { std::string name = m_config->getCanonicalName(client->getName()); @@ -1285,6 +1300,11 @@ void Server::handleClientDisconnected(BaseClientProxy *client) removeActiveClient(client); removeOldClient(client); + // m_clients always contains the primary (server) screen, so 1 means no remote clients. + using enum deskflow::core::ConnectionState; + ipcSendConnectionState(m_clients.size() <= 1 ? Listening : Connected); + sendConnectedClientsIpc(); + delete client; } diff --git a/src/lib/server/Server.h b/src/lib/server/Server.h index 3fc7d76ea628..1e937f545bf6 100644 --- a/src/lib/server/Server.h +++ b/src/lib/server/Server.h @@ -197,6 +197,7 @@ class Server Set the \c list to the names of the currently connected clients. */ void getClients(std::vector &list) const; + void sendConnectedClientsIpc() const; //@} From 59a47471131b69dc5469a22aac89a7a19bdc61c8 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Thu, 26 Mar 2026 14:17:56 +0000 Subject: [PATCH 15/24] feat(ipc): Unrecognised IPC client, retry messages, and TLS fingerprint Also adds version checking for IPC server and client --- src/lib/client/Client.cpp | 9 +- src/lib/client/Client.h | 3 +- src/lib/client/ServerProxy.cpp | 9 +- src/lib/common/Enums.h | 10 ++ src/lib/deskflow/ipc/IpcServer.cpp | 16 ++- src/lib/gui/CMakeLists.txt | 7 - src/lib/gui/MainWindow.cpp | 111 +++++++-------- src/lib/gui/MainWindow.h | 20 +-- src/lib/gui/config/IServerConfig.h | 28 ---- src/lib/gui/config/ServerConfig.h | 25 +--- src/lib/gui/core/ClientConnection.cpp | 75 ---------- src/lib/gui/core/ClientConnection.h | 48 ------- src/lib/gui/core/CoreProcess.cpp | 20 ++- src/lib/gui/core/CoreProcess.h | 12 +- src/lib/gui/core/ServerConnection.cpp | 108 --------------- src/lib/gui/core/ServerConnection.h | 49 ------- src/lib/gui/core/ServerMessage.cpp | 54 -------- src/lib/gui/core/ServerMessage.h | 32 ----- src/lib/gui/dialogs/SettingsDialog.cpp | 2 +- src/lib/gui/dialogs/SettingsDialog.h | 8 +- src/lib/gui/ipc/IpcClient.cpp | 38 +++-- src/lib/gui/ipc/IpcClient.h | 1 + src/lib/server/Server.cpp | 3 +- src/unittests/gui/core/CMakeLists.txt | 16 --- .../gui/core/ClientConnectionTests.cpp | 130 ------------------ .../gui/core/ClientConnectionTests.h | 27 ---- .../gui/core/ServerConnectionTests.cpp | 101 -------------- .../gui/core/ServerConnectionTests.h | 24 ---- 28 files changed, 165 insertions(+), 821 deletions(-) delete mode 100644 src/lib/gui/config/IServerConfig.h delete mode 100644 src/lib/gui/core/ClientConnection.cpp delete mode 100644 src/lib/gui/core/ClientConnection.h delete mode 100644 src/lib/gui/core/ServerConnection.cpp delete mode 100644 src/lib/gui/core/ServerConnection.h delete mode 100644 src/lib/gui/core/ServerMessage.cpp delete mode 100644 src/lib/gui/core/ServerMessage.h delete mode 100644 src/unittests/gui/core/ClientConnectionTests.cpp delete mode 100644 src/unittests/gui/core/ClientConnectionTests.h delete mode 100644 src/unittests/gui/core/ServerConnectionTests.cpp delete mode 100644 src/unittests/gui/core/ServerConnectionTests.h diff --git a/src/lib/client/Client.cpp b/src/lib/client/Client.cpp index 64a2a95cc6ab..5952013f7cc0 100644 --- a/src/lib/client/Client.cpp +++ b/src/lib/client/Client.cpp @@ -14,7 +14,6 @@ #include "base/NetworkProtocol.h" #include "client/ServerProxy.h" #include "common/Settings.h" -#include "deskflow/ipc/CoreIpc.h" #include "deskflow/Clipboard.h" #include "deskflow/IPlatformScreen.h" #include "deskflow/PacketStreamFilter.h" @@ -22,11 +21,14 @@ #include "deskflow/ProtocolUtil.h" #include "deskflow/Screen.h" #include "deskflow/StreamChunker.h" +#include "deskflow/ipc/CoreIpc.h" #include "net/IDataSocket.h" #include "net/ISocketFactory.h" #include "net/SecureSocket.h" #include "net/TCPSocket.h" +#include + #include #include @@ -133,8 +135,11 @@ void Client::disconnect(const char *msg) } } -void Client::refuseConnection(const char *msg) +void Client::refuseConnection(deskflow::core::ConnectionRefusal reason, const char *msg) { + const auto metaEnum = QMetaEnum::fromType(); + ipcSendToClient("connectionRefused", metaEnum.valueToKey(static_cast(reason))); + cleanup(); if (msg) { diff --git a/src/lib/client/Client.h b/src/lib/client/Client.h index 7249ac284df0..f2f892a36259 100644 --- a/src/lib/client/Client.h +++ b/src/lib/client/Client.h @@ -11,6 +11,7 @@ #include "deskflow/IClient.h" #include "base/EventTypes.h" +#include "common/Enums.h" #include "deskflow/IClipboard.h" #include "net/NetworkAddress.h" @@ -88,7 +89,7 @@ class Client : public IClient Disconnects from the server with an optional error message. Unlike disconnect this function doesn't try to use other ip addresses */ - void refuseConnection(const char *msg); + void refuseConnection(deskflow::core::ConnectionRefusal reason, const char *msg); //! Notify of handshake complete /*! diff --git a/src/lib/client/ServerProxy.cpp b/src/lib/client/ServerProxy.cpp index d2e38860eafb..600f05697adb 100644 --- a/src/lib/client/ServerProxy.cpp +++ b/src/lib/client/ServerProxy.cpp @@ -124,6 +124,7 @@ void ServerProxy::handleData() ServerProxy::ConnectionResult ServerProxy::parseHandshakeMessage(const uint8_t *code) { using enum ConnectionResult; + using enum deskflow::core::ConnectionRefusal; if (memcmp(code, kMsgQInfo, 4) == 0) { queryInfo(); @@ -168,25 +169,25 @@ ServerProxy::ConnectionResult ServerProxy::parseHandshakeMessage(const uint8_t * int32_t minor; ProtocolUtil::readf(m_stream, kMsgEIncompatible + 4, &major, &minor); LOG_ERR("server has incompatible version %d.%d", major, minor); - m_client->refuseConnection("server has incompatible version"); + m_client->refuseConnection(IncompatibleVersion, "server has incompatible version"); return Disconnect; } else if (memcmp(code, kMsgEBusy, 4) == 0) { LOG_ERR("server already has a connected client with name \"%s\"", m_client->getName().c_str()); - m_client->refuseConnection("server already has a connected client with our name"); + m_client->refuseConnection(AlreadyConnected, "server already has a connected client with our name"); return Disconnect; } else if (memcmp(code, kMsgEUnknown, 4) == 0) { LOG_ERR("server refused client with name \"%s\"", m_client->getName().c_str()); - m_client->refuseConnection("server refused client with our name"); + m_client->refuseConnection(UnknownClient, "server refused client with our name"); return Disconnect; } else if (memcmp(code, kMsgEBad, 4) == 0) { LOG_ERR("server disconnected due to a protocol error"); - m_client->refuseConnection("server reported a protocol error"); + m_client->refuseConnection(ProtocolError, "server reported a protocol error"); return Disconnect; } else if (memcmp(code, kMsgDLanguageSynchronisation, 4) == 0) { setServerLanguages(); diff --git a/src/lib/common/Enums.h b/src/lib/common/Enums.h index b9af3c6988aa..7daf1eb77d06 100644 --- a/src/lib/common/Enums.h +++ b/src/lib/common/Enums.h @@ -43,4 +43,14 @@ enum class ConnectionState Listening }; Q_ENUM_NS(ConnectionState) + +enum class ConnectionRefusal +{ + IncompatibleVersion, + AlreadyConnected, + UnknownClient, + ProtocolError +}; +Q_ENUM_NS(ConnectionRefusal) + } // namespace deskflow::core diff --git a/src/lib/deskflow/ipc/IpcServer.cpp b/src/lib/deskflow/ipc/IpcServer.cpp index 131c1d93d2fc..39eff2971a94 100644 --- a/src/lib/deskflow/ipc/IpcServer.cpp +++ b/src/lib/deskflow/ipc/IpcServer.cpp @@ -7,6 +7,7 @@ #include "IpcServer.h" #include "base/Log.h" +#include "common/VersionInfo.h" #include #include @@ -110,8 +111,21 @@ void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &messag } if (const auto &command = parts.at(0); command == "hello") { + const auto versionId = QStringLiteral("%1+%2").arg(kVersion, kVersionGitSha); + const auto clientVersion = parts.size() >= 2 ? parts.at(1) : QString(); + if (clientVersion != versionId) { + LOG_ERR( + "ipc client version mismatch (client: %s, server: %s)", + clientVersion.isEmpty() ? "unknown" : clientVersion.toUtf8().constData(), versionId.toUtf8().constData() + ); + writeToClientSocket(clientSocket, "error"); + clientSocket->flush(); + clientSocket->disconnectFromServer(); + return; + } + LOG_DEBUG("ipc server got hello message, sending hello back"); - writeToClientSocket(clientSocket, "hello"); + writeToClientSocket(clientSocket, QString("hello=%1").arg(versionId)); // Replay messages that were queued before any clients connected. LOG_DEBUG1("ipc server replaying %d pending messages", m_pendingMessages.size()); diff --git a/src/lib/gui/CMakeLists.txt b/src/lib/gui/CMakeLists.txt index 5d46fea13507..e3f7bd9e7a67 100644 --- a/src/lib/gui/CMakeLists.txt +++ b/src/lib/gui/CMakeLists.txt @@ -39,7 +39,6 @@ add_library(${target} STATIC TlsUtility.h VersionChecker.cpp VersionChecker.h - config/IServerConfig.h config/Screen.cpp config/Screen.h config/ScreenConfig.cpp @@ -48,16 +47,10 @@ add_library(${target} STATIC config/ScreenList.h config/ServerConfig.cpp config/ServerConfig.h - core/ClientConnection.cpp - core/ClientConnection.h core/CoreProcess.cpp core/CoreProcess.h core/NetworkMonitor.cpp core/NetworkMonitor.h - core/ServerConnection.cpp - core/ServerConnection.h - core/ServerMessage.cpp - core/ServerMessage.h dialogs/AboutDialog.cpp dialogs/AboutDialog.h dialogs/AboutDialog.ui diff --git a/src/lib/gui/MainWindow.cpp b/src/lib/gui/MainWindow.cpp index f2f396740f62..bd8b7177a32c 100644 --- a/src/lib/gui/MainWindow.cpp +++ b/src/lib/gui/MainWindow.cpp @@ -58,8 +58,6 @@ using namespace deskflow::gui; MainWindow::MainWindow() : ui{std::make_unique()}, m_coreProcess(m_serverConfig), - m_serverConnection(this, m_serverConfig), - m_clientConnection(this), m_trayIcon{new QSystemTrayIcon(this)}, m_guiDupeChecker{new QLocalServer(this)}, m_daemonIpcClient{new ipc::DaemonIpcClient(this)}, @@ -269,15 +267,11 @@ void MainWindow::connectSlots() if (!deskflow::platform::isMac()) connect(m_trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::trayIconActivated); - connect(&m_serverConnection, &ServerConnection::configureClient, this, &MainWindow::serverConnectionConfigureClient); - connect(&m_serverConnection, &ServerConnection::clientsChanged, this, &MainWindow::serverClientsChanged); connect(&m_coreProcess, &CoreProcess::connectedClientsChanged, this, &MainWindow::serverClientsChanged); - connect( - &m_serverConnection, &ServerConnection::requestNewClientPrompt, this, &MainWindow::handleNewClientPromptRequest - ); - - connect(&m_clientConnection, &ClientConnection::requestShowError, this, &MainWindow::showClientError); - connect(&m_clientConnection, &ClientConnection::updateTimeoutDelay, this, &MainWindow::updateTimeoutDelay); + connect(&m_coreProcess, &CoreProcess::unrecognisedClient, this, &MainWindow::handleUnrecognisedClient); + connect(&m_coreProcess, &CoreProcess::connectionRefused, this, &MainWindow::handleConnectionRefused); + connect(&m_coreProcess, &CoreProcess::retryIn, this, &MainWindow::updateTimeoutDelay); + connect(&m_coreProcess, &CoreProcess::peerFingerprint, this, &MainWindow::handlePeerFingerprint); if (Settings::value(Settings::Gui::AutoStartCore).toBool()) { connect(ui->btnToggleCore, &QPushButton::clicked, m_actionStopCore, &QAction::trigger, Qt::UniqueConnection); @@ -430,8 +424,6 @@ void MainWindow::clearSettings() m_networkMonitor->stopMonitoring(); disconnect(&m_coreProcess, nullptr, this, nullptr); - disconnect(&m_serverConnection, nullptr, this, nullptr); - disconnect(&m_clientConnection, nullptr, this, nullptr); disconnect(&m_versionChecker, nullptr, this, nullptr); disconnect(m_guiDupeChecker, nullptr, this, nullptr); disconnect(m_trayIcon, nullptr, this, nullptr); @@ -598,12 +590,12 @@ void MainWindow::updateNetworkInfo() void MainWindow::serverConnectionConfigureClient(const QString &clientName) { - m_serverConnection.serverConfigDialogVisible(true); + m_serverConfigDialogVisible = true; ServerConfigDialog dialog(this, m_serverConfig); if (dialog.addClient(clientName) && dialog.exec() == QDialog::Accepted) { m_coreProcess.restart(); } - m_serverConnection.serverConfigDialogVisible(false); + m_serverConfigDialogVisible = false; } ////////////////////////////////////////////////////////////////////////////// @@ -755,35 +747,64 @@ void MainWindow::setTrayIcon() void MainWindow::handleLogLine(const QString &line) { m_logDock->appendLine(line); - updateFromLogLine(line); } -void MainWindow::updateFromLogLine(const QString &line) +void MainWindow::handleUnrecognisedClient(const QString &clientName) { - checkConnected(line); - checkFingerprint(line); -} + if (m_ignoredClients.contains(clientName)) { + qDebug("ignoring %s:", qPrintable(clientName)); + return; + } -void MainWindow::checkConnected(const QString &line) -{ - if (ui->rbModeServer->isChecked()) { - m_serverConnection.handleLogLine(line); + if (m_newClientPromptShowing || m_serverConfigDialogVisible) + return; + + if (Settings::value(Settings::Server::ExternalConfig).toBool()) + return; + + if (m_serverConfig.isFull() || m_serverConfig.screenExists(clientName)) + return; + + m_newClientPromptShowing = true; + + showAndActivate(); + + if (deskflow::gui::messages::showNewClientPrompt(this, clientName)) { + serverConnectionConfigureClient(clientName); } else { - m_clientConnection.handleLogLine(line); + m_ignoredClients.insert(clientName); } + + m_newClientPromptShowing = false; } -void MainWindow::checkFingerprint(const QString &line) +void MainWindow::handleConnectionRefused(deskflow::core::ConnectionRefusal reason) { - static const auto tlsPeerMessage = QStringLiteral("peer fingerprint: "); - static const qsizetype msgLen = QString(tlsPeerMessage).length(); + if (reason != deskflow::core::ConnectionRefusal::AlreadyConnected) + return; - const qsizetype midStart = line.indexOf(tlsPeerMessage); - if (midStart == -1) + if (!isVisible() || m_clientErrorVisible) return; - const auto sha256Text = line.mid(midStart + msgLen).remove(':'); + m_clientErrorVisible = true; + showAndActivate(); + + const auto address = Settings::value(Settings::Client::RemoteHost).toString(); + QMessageBox::warning( + this, tr("%1 Connection Error").arg(kAppName), + tr("

Failed to connect to the server '%1'.

" + "

A Client with your name is already connected to the server.

" + "Please ensure that you're using a unique name and that only a " + "single instance of the client process is running.

") + .arg(address) + ); + + m_clientErrorVisible = false; +} +void MainWindow::handlePeerFingerprint(const QString &fingerprint) +{ + const auto sha256Text = QString(fingerprint).remove(':'); const Fingerprint sha256 = {QCryptographicHash::Sha256, QByteArray::fromHex(sha256Text.toLatin1())}; const bool isClient = m_coreProcess.mode() == CoreMode::Client; @@ -959,7 +980,7 @@ void MainWindow::changeEvent(QEvent *e) updateModeControlLabels(); updateNetworkInfo(); updateStatus(); - serverClientsChanged(m_serverConnection.connectedClients()); + serverClientsChanged({}); updateText(); } } @@ -1167,34 +1188,6 @@ void MainWindow::remoteHostChanged(const QString &newRemoteHost) } } -void MainWindow::showClientError(deskflow::client::ErrorType error, const QString &address) -{ - if (!isVisible() || m_clientErrorVisible || error != deskflow::client::ErrorType::AlreadyConnected) - return; - - m_clientErrorVisible = true; - - showAndActivate(); - - QMessageBox::warning( - this, tr("%1 Connection Error").arg(kAppName), - tr("

Failed to connect to the server '%1'.

" - "

A Client with your name is already connected to the server.

" - "Please ensure that you're using a unique name and that only a " - "single instance of the client process is running.

") - .arg(address) - ); - - m_clientErrorVisible = false; -} - -void MainWindow::handleNewClientPromptRequest(const QString &clientName) -{ - showAndActivate(); - bool result = deskflow::gui::messages::showNewClientPrompt(this, clientName); - m_serverConnection.handleNewClientResult(clientName, result); -} - void MainWindow::updateIpLabel(const QStringList &addresses) { if (m_coreProcess.mode() != CoreMode::Server) { diff --git a/src/lib/gui/MainWindow.h b/src/lib/gui/MainWindow.h index 21c688f70b0d..fbac2fcc8782 100644 --- a/src/lib/gui/MainWindow.h +++ b/src/lib/gui/MainWindow.h @@ -16,10 +16,8 @@ #include "VersionChecker.h" #include "config/ServerConfig.h" -#include "gui/core/ClientConnection.h" #include "gui/core/CoreProcess.h" #include "gui/core/NetworkMonitor.h" -#include "gui/core/ServerConnection.h" #include "net/Fingerprint.h" #ifdef Q_OS_MACOS @@ -118,9 +116,9 @@ class MainWindow : public QMainWindow void setupTrayIcon(); void applyConfig(); void setTrayIcon(); - void updateFromLogLine(const QString &line); - void checkConnected(const QString &line); - void checkFingerprint(const QString &line); + void handleUnrecognisedClient(const QString &clientName); + void handleConnectionRefused(deskflow::core::ConnectionRefusal reason); + void handlePeerFingerprint(const QString &fingerprint); void closeEvent(QCloseEvent *event) override; void secureSocket(bool secureSocket); void connectSlots(); @@ -140,17 +138,10 @@ class MainWindow : public QMainWindow void daemonIpcClientConnectionFailed(); void toggleCanRunCore(bool enableButtons); void remoteHostChanged(const QString &newRemoteHost); - void handleNewClientPromptRequest(const QString &clientName); void updateIpLabel(const QStringList &addresses); void updateTimeoutDelay(int newDelay); bool canRunCore() const; - /** - * @brief showClientError - * @param error Error Type - * @param address - */ - void showClientError(deskflow::client::ErrorType error, const QString &address); /** * @brief trustedFingerprintDatabase get the FingerprintDatabase for the trusted clients or trusted servers. @@ -177,8 +168,9 @@ class MainWindow : public QMainWindow bool m_clientErrorVisible = false; ServerConfig m_serverConfig; deskflow::gui::CoreProcess m_coreProcess; - deskflow::gui::ServerConnection m_serverConnection; - deskflow::gui::ClientConnection m_clientConnection; + QSet m_ignoredClients; + bool m_newClientPromptShowing = false; + bool m_serverConfigDialogVisible = false; QSize m_expandedSize = QSize(); QStringList m_checkedClients; QStringList m_checkedServers; diff --git a/src/lib/gui/config/IServerConfig.h b/src/lib/gui/config/IServerConfig.h deleted file mode 100644 index 9409183057c2..000000000000 --- a/src/lib/gui/config/IServerConfig.h +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2024 Symless Ltd. - * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception - */ - -#pragma once - -#include -#include - -#include "ScreenList.h" - -namespace deskflow::gui { - -// TODO: Remove this class, when the ServerConnectionTests are ported to QTests -class IServerConfig -{ -public: - virtual ~IServerConfig() = default; - virtual bool isFull() const = 0; - virtual bool screenExists(const QString &screenName) const = 0; - virtual bool save(const QString &fileName) const = 0; - virtual void save(QFile &file) const = 0; - virtual const ScreenList &screens() const = 0; -}; - -} // namespace deskflow::gui diff --git a/src/lib/gui/config/ServerConfig.h b/src/lib/gui/config/ServerConfig.h index ca6f70cf6aa9..80e86966b0f0 100644 --- a/src/lib/gui/config/ServerConfig.h +++ b/src/lib/gui/config/ServerConfig.h @@ -9,7 +9,6 @@ #include "base/NetworkProtocol.h" #include "gui/Hotkey.h" -#include "gui/config/IServerConfig.h" #include "gui/config/ScreenConfig.h" #include "gui/config/ScreenList.h" @@ -31,21 +30,18 @@ const auto kDefaultProtocol = NetworkProtocol::Barrier; } // namespace deskflow::gui -class ServerConfig : public ScreenConfig, public deskflow::gui::IServerConfig +class ServerConfig : public ScreenConfig { friend class ServerConfigDialog; friend QTextStream &operator<<(QTextStream &outStream, const ServerConfig &config); public: explicit ServerConfig(int columns = kDefaultColumns, int rows = kDefaultRows); - ~ServerConfig() override = default; + ~ServerConfig() = default; bool operator==(const ServerConfig &sc) const; - // - // Overrides - // - const ScreenList &screens() const override + const ScreenList &screens() const { return m_Screens; } @@ -131,17 +127,10 @@ class ServerConfig : public ScreenConfig, public deskflow::gui::IServerConfig } static size_t defaultClipboardSharingSize(); - // - // Overrides - // - bool save(const QString &fileName) const override; - bool screenExists(const QString &screenName) const override; - void save(QFile &file) const override; - bool isFull() const override; - - // - // New methods - // + bool save(const QString &fileName) const; + bool screenExists(const QString &screenName) const; + void save(QFile &file) const; + bool isFull() const; void commit(); int numScreens() const; QString getServerName() const; diff --git a/src/lib/gui/core/ClientConnection.cpp b/src/lib/gui/core/ClientConnection.cpp deleted file mode 100644 index 499cc1e0e655..000000000000 --- a/src/lib/gui/core/ClientConnection.cpp +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2025 Deskflow Developers - * SPDX-FileCopyrightText: (C) 2021 Symless Ltd. - * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception - */ - -#include "ClientConnection.h" - -#include "common/Settings.h" - -#include - -namespace deskflow::gui { - -void ClientConnection::handleLogLine(const QString &logLine) -{ - if (logLine.contains("disconnected from server")) { - m_supressMessage = false; - return; - } - - if (logLine.contains("retry in ") && Settings::value(Settings::Client::DynamicConnectionRetry).toBool()) { - auto line = logLine.mid(logLine.indexOf("in ") + 3); - int seconds = line.mid(0, line.indexOf(" ")).toInt(); - Q_EMIT updateTimeoutDelay(seconds); - return; - } - if (logLine.contains("connected to server")) { - m_supressMessage = true; - return; - } - - if (logLine.contains("failed to connect to server")) { - if (m_supressMessage) { - qDebug("message already shown, skipping"); - return; - } - // ignore the message if it's about the server refusing by name as - // this will trigger the server to show an 'add client' dialog. - if (logLine.contains("server refused client with our name")) { - qDebug("ignoring client name refused message"); - return; - } - showMessage(logLine); - } -} - -void ClientConnection::showMessage(const QString &logLine) -{ - using enum deskflow::client::ErrorType; - - if (logLine.isEmpty()) - return; - - const auto address = Settings::value(Settings::Client::RemoteHost).toString(); - auto error = NoError; - - if (logLine.contains("server already has a connected client with our name")) { - error = AlreadyConnected; - } else if (QHostAddress a(address); a.isNull()) { - qDebug("ip not detected, showing hostname error"); - error = HostnameError; - } else { - qDebug("ip detected, showing generic error"); - error = GenericError; - } - - if (error == NoError) - return; - - Q_EMIT requestShowError(error, address); -} - -} // namespace deskflow::gui diff --git a/src/lib/gui/core/ClientConnection.h b/src/lib/gui/core/ClientConnection.h deleted file mode 100644 index 377cbeea0ee1..000000000000 --- a/src/lib/gui/core/ClientConnection.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2025 Deskflow Developers - * SPDX-FileCopyrightText: (C) 2021 Symless Ltd. - * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception - */ - -#pragma once - -#include "common/Enums.h" - -#include -#include - -class QWidget; - -namespace deskflow::gui { - -class ClientConnection : public QObject -{ - Q_OBJECT - -public: - explicit ClientConnection(QWidget *parent) : m_pParent(parent) - { - // do nothing - } - - void handleLogLine(const QString &line); - -Q_SIGNALS: - /** - * @brief requestShowError, This signal is emitted when the client - * connection would like the owning process to report an error message - * @param error the type of error being reported - * @param address of the host - */ - void requestShowError(deskflow::client::ErrorType error, const QString &address); - void updateTimeoutDelay(int newTimeout); - -private: - void showMessage(const QString &logLine); - - QWidget *m_pParent; - bool m_supressMessage = false; -}; - -} // namespace deskflow::gui diff --git a/src/lib/gui/core/CoreProcess.cpp b/src/lib/gui/core/CoreProcess.cpp index ef61cbe57594..935f36081c00 100644 --- a/src/lib/gui/core/CoreProcess.cpp +++ b/src/lib/gui/core/CoreProcess.cpp @@ -99,7 +99,7 @@ QString CoreProcess::wrapIpv6(const QString &address) // CoreProcess // -CoreProcess::CoreProcess(const IServerConfig &serverConfig) +CoreProcess::CoreProcess(const ServerConfig &serverConfig) : m_serverConfig(serverConfig), m_daemonIpcClient{new ipc::DaemonIpcClient(this)} { @@ -430,7 +430,7 @@ void CoreProcess::stop(std::optional processModeOption) if (m_coreIpcClient) { m_coreIpcClient->disconnectFromServer(); - delete m_coreIpcClient; + m_coreIpcClient->deleteLater(); m_coreIpcClient = nullptr; } @@ -548,6 +548,22 @@ void CoreProcess::onCoreIpcMessageReceived(const QString &command, const QString m_secureSocketVersion = args; Q_EMIT securityLevelChanged(args); } + } else if (command == "unrecognisedClient") { + Q_EMIT unrecognisedClient(args); + } else if (command == "connectionRefused") { + const auto metaEnum = QMetaEnum::fromType(); + bool ok = false; + const auto reason = + static_cast(metaEnum.keyToValue(args.toUtf8().constData(), &ok)); + if (ok) { + Q_EMIT connectionRefused(reason); + } else { + qWarning("core ipc got unknown connection refusal: %s", args.toUtf8().constData()); + } + } else if (command == "retryIn") { + Q_EMIT retryIn(args.toInt()); + } else if (command == "peerFingerprint") { + Q_EMIT peerFingerprint(args); } } diff --git a/src/lib/gui/core/CoreProcess.h b/src/lib/gui/core/CoreProcess.h index 25abd0ac7960..45cfa34bea8e 100644 --- a/src/lib/gui/core/CoreProcess.h +++ b/src/lib/gui/core/CoreProcess.h @@ -10,7 +10,7 @@ #include "common/Enums.h" #include "common/Settings.h" #include "gui/FileTail.h" -#include "gui/config/IServerConfig.h" +#include "gui/config/ServerConfig.h" #include #include @@ -29,8 +29,6 @@ class CoreProcess : public QObject using ConnectionState = deskflow::core::ConnectionState; using ProcessMode = Settings::ProcessMode; using ProcessState = deskflow::core::ProcessState; - using IServerConfig = deskflow::gui::IServerConfig; - Q_OBJECT public: @@ -40,7 +38,7 @@ class CoreProcess : public QObject StartFailed }; - explicit CoreProcess(const IServerConfig &serverConfig); + explicit CoreProcess(const ServerConfig &serverConfig); void start(std::optional processMode = std::nullopt); void stop(std::optional processMode = std::nullopt); @@ -91,6 +89,10 @@ class CoreProcess : public QObject void daemonIpcClientConnectionFailed(); void connectedClientsChanged(const QStringList &clients); void securityLevelChanged(QString securityLevel); + void unrecognisedClient(const QString &clientName); + void connectionRefused(deskflow::core::ConnectionRefusal reason); + void retryIn(int seconds); + void peerFingerprint(const QString &fingerprint); private Q_SLOTS: void onProcessFinished(int exitCode, QProcess::ExitStatus); @@ -116,7 +118,7 @@ private Q_SLOTS: static QString processStateToString(const CoreProcess::ProcessState state); static QString wrapIpv6(const QString &address); - const IServerConfig &m_serverConfig; + const ServerConfig &m_serverConfig; QString m_address; ProcessState m_processState = ProcessState::Stopped; ConnectionState m_connectionState = ConnectionState::Disconnected; diff --git a/src/lib/gui/core/ServerConnection.cpp b/src/lib/gui/core/ServerConnection.cpp deleted file mode 100644 index f1ba9357d1c4..000000000000 --- a/src/lib/gui/core/ServerConnection.cpp +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2025 Deskflow Developers - * SPDX-FileCopyrightText: (C) 2021 Symless Ltd. - * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception - */ - -#include "ServerConnection.h" - -#include "ServerMessage.h" -#include "common/Settings.h" - -#include - -namespace deskflow::gui { - -ServerConnection::ServerConnection(QWidget *parent, IServerConfig &serverConfig) - : m_pParent(parent), - m_serverConfig(serverConfig) -{ -} - -void ServerConnection::handleLogLine(const QString &logLine) -{ - ServerMessage message(logLine); - const auto &clientName = message.getClientName(); - - if (m_ignoredClients.contains(clientName)) { - qDebug("ignoring %s:", qPrintable(clientName)); - return; - } - - if (message.isDisconnectedMessage()) { - m_connectedClients.remove(clientName); - Q_EMIT clientsChanged(connectedClients()); - return; - } - - if (message.isConnectedMessage()) { - m_connectedClients.insert(clientName); - Q_EMIT clientsChanged(connectedClients()); - return; - } - - if (!message.isNewClientMessage()) { - return; - } - - if (m_messageShowing) { - qDebug("new client message already shown, skipping for now"); - return; - } - - if (m_serverConfigDialogVisible) { - qDebug("server config dialog visible, skipping new client prompt"); - return; - } - - if (Settings::value(Settings::Server::ExternalConfig).toBool()) { - qDebug("external config enabled, skipping new client prompt"); - return; - } - - if (m_connectedClients.contains(clientName)) { - qDebug("already got request, skipping new client prompt for: %s", qPrintable(clientName)); - return; - } - - handleNewClient(clientName); -} - -void ServerConnection::handleNewClient(const QString &clientName) -{ - if (m_serverConfig.isFull()) { - qDebug("server config full, skipping new client prompt for: %s", qPrintable(clientName)); - return; - } - - if (m_serverConfig.screenExists(clientName)) { - qDebug("client already added, skipping new client prompt for: %s", qPrintable(clientName)); - return; - } - - m_messageShowing = true; - Q_EMIT requestNewClientPrompt(clientName); -} - -void ServerConnection::handleNewClientResult(const QString &clientName, bool acceptClient) -{ - m_messageShowing = false; - if (!acceptClient) { - qDebug("declined dialog, ignoring client: %s", qPrintable(clientName)); - m_ignoredClients.insert(clientName); - return; - } - - qDebug("accepted dialog, adding client: %s", qPrintable(clientName)); - Q_EMIT configureClient(clientName); - m_connectedClients.insert(clientName); - Q_EMIT clientsChanged(connectedClients()); -} - -QStringList ServerConnection::connectedClients() const -{ - return QStringList(m_connectedClients.begin(), m_connectedClients.end()); -} - -} // namespace deskflow::gui diff --git a/src/lib/gui/core/ServerConnection.h b/src/lib/gui/core/ServerConnection.h deleted file mode 100644 index fa656479f051..000000000000 --- a/src/lib/gui/core/ServerConnection.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2025 Deskflow Developers - * SPDX-FileCopyrightText: (C) 2021 Symless Ltd. - * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception - */ - -#pragma once - -#include - -#include "gui/Messages.h" -#include "gui/config/IServerConfig.h" - -namespace deskflow::gui { - -class ServerConnection : public QObject -{ - Q_OBJECT - using IServerConfig = deskflow::gui::IServerConfig; - -public: - explicit ServerConnection(QWidget *parent, IServerConfig &serverConfig); - void handleLogLine(const QString &logLine); - void serverConfigDialogVisible(bool visible) - { - m_serverConfigDialogVisible = visible; - } - - QStringList connectedClients() const; - void handleNewClientResult(const QString &clientName, bool acceptClient); - -Q_SIGNALS: - void requestNewClientPrompt(const QString &clientName); - void configureClient(const QString &clientName); - void clientsChanged(const QStringList &clients); - -private: - void handleNewClient(const QString &clientName); - - QWidget *m_pParent; - IServerConfig &m_serverConfig; - QSet m_connectedClients; - QSet m_ignoredClients; - bool m_messageShowing = false; - bool m_serverConfigDialogVisible = false; -}; - -} // namespace deskflow::gui diff --git a/src/lib/gui/core/ServerMessage.cpp b/src/lib/gui/core/ServerMessage.cpp deleted file mode 100644 index f29308f88060..000000000000 --- a/src/lib/gui/core/ServerMessage.cpp +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2021 Symless Ltd. - * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception - */ - -#include "ServerMessage.h" - -namespace deskflow::gui { - -ServerMessage::ServerMessage(const QString &message) : m_message(message), m_clientName(parseClientName(message)) -{ - // do nothing -} - -bool ServerMessage::isNewClientMessage() const -{ - return m_message.contains("unrecognised client name"); -} - -bool ServerMessage::isExitMessage() const -{ - return m_message.contains("process exited"); -} - -bool ServerMessage::isConnectedMessage() const -{ - return m_message.contains("has connected"); -} - -bool ServerMessage::isDisconnectedMessage() const -{ - return m_message.contains("has disconnected"); -} - -const QString &ServerMessage::getClientName() const -{ - return m_clientName; -} - -QString ServerMessage::parseClientName(const QString &line) const -{ - QString clientName("Unknown"); - - auto nameStart = line.indexOf('"') + 1; - - if (auto nameEnd = line.indexOf('"', nameStart); nameEnd > nameStart) { - clientName = line.mid(nameStart, nameEnd - nameStart); - } - - return clientName; -} - -} // namespace deskflow::gui diff --git a/src/lib/gui/core/ServerMessage.h b/src/lib/gui/core/ServerMessage.h deleted file mode 100644 index f08cbad64576..000000000000 --- a/src/lib/gui/core/ServerMessage.h +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2021 Symless Ltd. - * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception - */ - -#pragma once - -#include - -namespace deskflow::gui { - -class ServerMessage -{ - QString m_message; - QString m_clientName; - -public: - explicit ServerMessage(const QString &message); - - bool isNewClientMessage() const; - bool isExitMessage() const; - bool isConnectedMessage() const; - bool isDisconnectedMessage() const; - - const QString &getClientName() const; - -private: - QString parseClientName(const QString &line) const; -}; - -} // namespace deskflow::gui diff --git a/src/lib/gui/dialogs/SettingsDialog.cpp b/src/lib/gui/dialogs/SettingsDialog.cpp index 12409e37844b..b18623b4efc7 100644 --- a/src/lib/gui/dialogs/SettingsDialog.cpp +++ b/src/lib/gui/dialogs/SettingsDialog.cpp @@ -23,7 +23,7 @@ using namespace deskflow::gui; -SettingsDialog::SettingsDialog(QWidget *parent, const IServerConfig &serverConfig) +SettingsDialog::SettingsDialog(QWidget *parent, const ServerConfig &serverConfig) : QDialog(parent), ui{std::make_unique()}, m_serverConfig(serverConfig) diff --git a/src/lib/gui/dialogs/SettingsDialog.h b/src/lib/gui/dialogs/SettingsDialog.h index a20bb6ea2f47..92075919c12b 100644 --- a/src/lib/gui/dialogs/SettingsDialog.h +++ b/src/lib/gui/dialogs/SettingsDialog.h @@ -9,7 +9,7 @@ #pragma once #include -#include "gui/config/IServerConfig.h" +#include "gui/config/ServerConfig.h" namespace Ui { class SettingsDialog; @@ -17,13 +17,11 @@ class SettingsDialog; class SettingsDialog : public QDialog { - using IServerConfig = deskflow::gui::IServerConfig; - Q_OBJECT public: void extracted(); - SettingsDialog(QWidget *parent, const IServerConfig &serverConfig); + SettingsDialog(QWidget *parent, const ServerConfig &serverConfig); ~SettingsDialog() override; Q_SIGNALS: @@ -86,5 +84,5 @@ class SettingsDialog : public QDialog bool m_interfaceSetOnLoad = false; std::unique_ptr ui; - const IServerConfig &m_serverConfig; + const ServerConfig &m_serverConfig; }; diff --git a/src/lib/gui/ipc/IpcClient.cpp b/src/lib/gui/ipc/IpcClient.cpp index 1dfdd482b1f6..644d9470588c 100644 --- a/src/lib/gui/ipc/IpcClient.cpp +++ b/src/lib/gui/ipc/IpcClient.cpp @@ -6,15 +6,14 @@ #include "IpcClient.h" +#include "common/VersionInfo.h" + #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)}, @@ -48,6 +47,8 @@ void IpcClient::connectToServer() void IpcClient::attemptConnection() { + const auto kRetryLimit = 3; + if (m_retryCount >= kRetryLimit) { qWarning() << "ipc client failed to connect after" << kRetryLimit << "attempts"; m_state = State::Unconnected; @@ -67,8 +68,9 @@ void IpcClient::attemptConnection() connect( m_socket, &QLocalSocket::connected, this, [this] { - m_socket->write("hello\n"); - qDebug() << "ipc client sent hello"; + const auto versionId = QStringLiteral("%1+%2").arg(kVersion, kVersionGitSha); + m_socket->write(QString("hello=%1\n").arg(versionId).toUtf8()); + qDebug() << "ipc client sent hello with version:" << versionId; }, Qt::SingleShotConnection ); @@ -141,10 +143,8 @@ void IpcClient::handleReadyRead() continue; } - if (m_state == State::Connecting && parts.at(0) == "hello") { - m_state = State::Connected; - qDebug() << "ipc client connected"; - Q_EMIT connected(); + if (m_state == State::Connecting) { + handleHandshakeMessage(parts); continue; } @@ -156,6 +156,26 @@ void IpcClient::handleReadyRead() } } +void IpcClient::handleHandshakeMessage(const QStringList &parts) +{ + if (parts.at(0) != "hello") { + return; + } + + const auto versionId = QStringLiteral("%1+%2").arg(kVersion, kVersionGitSha); + const auto serverVersion = parts.size() >= 2 ? parts.at(1) : QString(); + if (serverVersion != versionId) { + qCritical() << "ipc version mismatch (client:" << versionId << "server:" << serverVersion << ")"; + disconnectFromServer(); + Q_EMIT connectionFailed(); + return; + } + + m_state = State::Connected; + qDebug() << "ipc client connected"; + Q_EMIT connected(); +} + void IpcClient::sendMessage(const QString &message) { if (m_state != State::Connected) { diff --git a/src/lib/gui/ipc/IpcClient.h b/src/lib/gui/ipc/IpcClient.h index 8e4068c5c9cf..1f2b296d6db1 100644 --- a/src/lib/gui/ipc/IpcClient.h +++ b/src/lib/gui/ipc/IpcClient.h @@ -55,6 +55,7 @@ private Q_SLOTS: private: void attemptConnection(); + void handleHandshakeMessage(const QStringList &parts); QLocalSocket *m_socket; State m_state{State::Unconnected}; diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index df505ea9533d..8e55e6b58836 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -11,7 +11,6 @@ #include "base/IEventQueue.h" #include "base/Log.h" #include "deskflow/AppUtil.h" -#include "deskflow/ipc/CoreIpc.h" #include "deskflow/DeskflowException.h" #include "deskflow/IPlatformScreen.h" #include "deskflow/OptionTypes.h" @@ -19,6 +18,7 @@ #include "deskflow/ProtocolTypes.h" #include "deskflow/Screen.h" #include "deskflow/StreamChunker.h" +#include "deskflow/ipc/CoreIpc.h" #include "net/TCPSocket.h" #include "server/ClientListener.h" #include "server/ClientProxy.h" @@ -235,6 +235,7 @@ void Server::adoptClient(BaseClientProxy *client) // name must be in our configuration if (!m_config->isScreen(client->getName())) { LOG_WARN("unrecognised client name \"%s\", check server config", client->getName().c_str()); + ipcSendToClient("unrecognisedClient", QString::fromStdString(client->getName())); closeClient(client, kMsgEUnknown); return; } diff --git a/src/unittests/gui/core/CMakeLists.txt b/src/unittests/gui/core/CMakeLists.txt index 73da17091dca..0484b134b72b 100644 --- a/src/unittests/gui/core/CMakeLists.txt +++ b/src/unittests/gui/core/CMakeLists.txt @@ -1,25 +1,9 @@ # SPDX-FileCopyrightText: (C) 2025 Deskflow Developers # SPDX-License-Identifier: MIT -create_test( - NAME ClientConnectionTests - DEPENDS gui - SOURCE ClientConnectionTests.cpp - WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/gui" -) - -create_test( - NAME ServerConnectionTests - DEPENDS gui - SOURCE ServerConnectionTests.cpp - WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/gui" -) - - create_test( NAME NetworkMonitorTests DEPENDS gui SOURCE NetworkMonitorTests.cpp WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/gui" ) - diff --git a/src/unittests/gui/core/ClientConnectionTests.cpp b/src/unittests/gui/core/ClientConnectionTests.cpp deleted file mode 100644 index 7c25fe90d9fd..000000000000 --- a/src/unittests/gui/core/ClientConnectionTests.cpp +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2025 Chris Rizzitello - * SPDX-FileCopyrightText: (C) 2024 Symless Ltd. - * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception - */ - -#include "ClientConnectionTests.h" - -#include "gui/core/ClientConnection.h" -#include - -#include - -using namespace deskflow::gui; - -void ClientConnectionTests::initTestCase() -{ - QDir dir; - QVERIFY(dir.mkpath(m_settingsPath)); - - QFile oldSettings(m_settingsFile); - if (oldSettings.exists()) - oldSettings.remove(); - - Settings::setSettingsFile(m_settingsFile); - Settings::setStateFile(m_stateFile); -} - -void ClientConnectionTests::handleLogLine_alreadyConnected_showError() -{ - ClientConnection clientConnection(nullptr); - const auto serverName = QStringLiteral("test server"); - Settings::setValue(Settings::Client::RemoteHost, serverName); - - QSignalSpy spy(&clientConnection, &ClientConnection::requestShowError); - QVERIFY(spy.isValid()); - - clientConnection.handleLogLine( - "failed to connect to server\n" - "server already has a connected client with our name" - ); - - QCOMPARE(spy.count(), 1); -} - -void ClientConnectionTests::handleLogLine_withHostname_showError() -{ - ClientConnection clientConnection(nullptr); - const auto serverName = QStringLiteral("test server"); - Settings::setValue(Settings::Client::RemoteHost, serverName); - - QSignalSpy spy(&clientConnection, &ClientConnection::requestShowError); - QVERIFY(spy.isValid()); - - clientConnection.handleLogLine("failed to connect to server"); - - QCOMPARE(spy.count(), 1); -} - -void ClientConnectionTests::handleLogLine_withIpAddress_showError() -{ - ClientConnection clientConnection(nullptr); - const auto serverName = QStringLiteral("1.1.1.1"); - Settings::setValue(Settings::Client::RemoteHost, serverName); - - QSignalSpy spy(&clientConnection, &ClientConnection::requestShowError); - QVERIFY(spy.isValid()); - - clientConnection.handleLogLine("failed to connect to server"); - - QCOMPARE(spy.count(), 1); -} - -void ClientConnectionTests::handleLogLine_serverRefusedClient_shouldNotShowError() -{ - ClientConnection clientConnection(nullptr); - - QSignalSpy spy(&clientConnection, &ClientConnection::requestShowError); - QVERIFY(spy.isValid()); - - clientConnection.handleLogLine( - "failed to connect to server\n" - "server refused client with our name" - ); - - QCOMPARE(spy.count(), 0); -} - -void ClientConnectionTests::handleLogLine_connected_shouldPreventFutureError() -{ - ClientConnection clientConnection(nullptr); - clientConnection.handleLogLine("connected to server"); - - QSignalSpy spy(&clientConnection, &ClientConnection::requestShowError); - QVERIFY(spy.isValid()); - - clientConnection.handleLogLine("failed to connect to server"); - - QCOMPARE(spy.count(), 0); -} - -void ClientConnectionTests::handleLogLine_connectToggled_showAfterDisconnect() -{ - ClientConnection clientConnection(nullptr); - clientConnection.handleLogLine("connected to server"); - - QSignalSpy spy(&clientConnection, &ClientConnection::requestShowError); - QVERIFY(spy.isValid()); - - clientConnection.handleLogLine("failed to connect to server"); - clientConnection.handleLogLine("disconnected from server"); - clientConnection.handleLogLine("failed to connect to server"); - - QCOMPARE(spy.count(), 1); -} - -void ClientConnectionTests::handleLogLine_otherMessage_shouldNotShowError() -{ - ClientConnection clientConnection(nullptr); - - QSignalSpy spy(&clientConnection, &ClientConnection::requestShowError); - QVERIFY(spy.isValid()); - - clientConnection.handleLogLine("hello world"); - - QCOMPARE(spy.count(), 0); -} - -QTEST_MAIN(ClientConnectionTests) diff --git a/src/unittests/gui/core/ClientConnectionTests.h b/src/unittests/gui/core/ClientConnectionTests.h deleted file mode 100644 index 33ff0e3cc83e..000000000000 --- a/src/unittests/gui/core/ClientConnectionTests.h +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2025 Chris Rizzitello - * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception - */ - -#include - -class ClientConnectionTests : public QObject -{ - Q_OBJECT -private Q_SLOTS: - // Test are run in order top to bottom - void initTestCase(); - void handleLogLine_alreadyConnected_showError(); - void handleLogLine_withHostname_showError(); - void handleLogLine_withIpAddress_showError(); - void handleLogLine_serverRefusedClient_shouldNotShowError(); - void handleLogLine_connected_shouldPreventFutureError(); - void handleLogLine_connectToggled_showAfterDisconnect(); - void handleLogLine_otherMessage_shouldNotShowError(); - -private: - inline static const QString m_settingsPath = QStringLiteral("tmp/test"); - inline static const QString m_settingsFile = QStringLiteral("%1/Deskflow.conf").arg(m_settingsPath); - inline static const QString m_stateFile = QStringLiteral("%1/Deskflow.state").arg(m_settingsPath); -}; diff --git a/src/unittests/gui/core/ServerConnectionTests.cpp b/src/unittests/gui/core/ServerConnectionTests.cpp deleted file mode 100644 index a40c5583fe9f..000000000000 --- a/src/unittests/gui/core/ServerConnectionTests.cpp +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2025 Chris Rizzitello - * SPDX-FileCopyrightText: (C) 2024 Symless Ltd. - * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception - */ - -#include "ServerConnectionTests.h" - -#include "gui/config/ServerConfig.h" -#include "gui/core/ServerConnection.h" -#include - -#include - -using namespace deskflow::gui; - -class FullServerConfig : public ServerConfig -{ -public: - bool isFull() const override - { - return true; - } -}; - -class ScreenExistsServerConfig : public ServerConfig -{ -public: - bool screenExists(const QString &screenName) const override - { - return true; - } -}; - -void ServerConnectionTests::initTestCase() -{ - QDir dir; - QVERIFY(dir.mkpath(m_settingsPath)); - - QFile oldSettings(m_settingsFile); - if (oldSettings.exists()) - oldSettings.remove(); - - Settings::setSettingsFile(m_settingsFile); - Settings::setStateFile(m_stateFile); -} - -void ServerConnectionTests::handleLogLine_newClient_shouldShowPrompt() -{ - ServerConfig m_serverConfig; - ServerConnection serverConnection(nullptr, m_serverConfig); - - QString clientName = "test client"; - - QSignalSpy spy(&serverConnection, &ServerConnection::requestNewClientPrompt); - QVERIFY(spy.isValid()); - - serverConnection.handleLogLine(R"(unrecognised client name "test client")"); - QCOMPARE(spy.count(), 1); -} - -void ServerConnectionTests::handleLogLine_ignoredClient_shouldNotShowPrompt() -{ - ServerConfig m_serverConfig; - ServerConnection serverConnection(nullptr, m_serverConfig); - QString clientName = "test client"; - serverConnection.handleNewClientResult(clientName, false); - - QSignalSpy spy(&serverConnection, &ServerConnection::requestNewClientPrompt); - QVERIFY(spy.isValid()); - - serverConnection.handleLogLine(R"(unrecognised client name "test client")"); - QCOMPARE(spy.count(), 0); -} - -void ServerConnectionTests::handleLogLine_serverConfigFull_shouldNotShowPrompt() -{ - FullServerConfig m_serverConfig; - ServerConnection serverConnection(nullptr, m_serverConfig); - - QSignalSpy spy(&serverConnection, &ServerConnection::requestNewClientPrompt); - QVERIFY(spy.isValid()); - - serverConnection.handleLogLine(R"(unrecognised client name "test client")"); - QCOMPARE(spy.count(), 0); -} - -void ServerConnectionTests::handleLogLine_screenExists_shouldNotShowPrompt() -{ - ScreenExistsServerConfig m_serverConfig; - ServerConnection serverConnection(nullptr, m_serverConfig); - - QSignalSpy spy(&serverConnection, &ServerConnection::requestNewClientPrompt); - QVERIFY(spy.isValid()); - - serverConnection.handleLogLine(R"(unrecognised client name "test client")"); - QCOMPARE(spy.count(), 0); -} - -QTEST_MAIN(ServerConnectionTests) diff --git a/src/unittests/gui/core/ServerConnectionTests.h b/src/unittests/gui/core/ServerConnectionTests.h deleted file mode 100644 index 558775a25524..000000000000 --- a/src/unittests/gui/core/ServerConnectionTests.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2025 Chris Rizzitello - * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception - */ - -#include - -class ServerConnectionTests : public QObject -{ - Q_OBJECT -private Q_SLOTS: - // Test are run in order top to bottom - void initTestCase(); - void handleLogLine_newClient_shouldShowPrompt(); - void handleLogLine_ignoredClient_shouldNotShowPrompt(); - void handleLogLine_serverConfigFull_shouldNotShowPrompt(); - void handleLogLine_screenExists_shouldNotShowPrompt(); - -private: - inline static const QString m_settingsPath = QStringLiteral("tmp/test"); - inline static const QString m_settingsFile = QStringLiteral("%1/Deskflow.conf").arg(m_settingsPath); - inline static const QString m_stateFile = QStringLiteral("%1/Deskflow.state").arg(m_settingsPath); -}; From 682966fec0bd3393b55078229194004e73558556 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Mon, 30 Mar 2026 19:38:03 +0100 Subject: [PATCH 16/24] feat(ipc): Handle missing keyboard layouts with IPC --- src/lib/client/ServerProxy.cpp | 20 ++++++---------- src/lib/client/ServerProxy.h | 1 - src/lib/common/Settings.h | 3 +++ src/lib/deskflow/languages/LanguageManager.h | 2 ++ src/lib/gui/MainWindow.cpp | 25 ++++++++++++++++++++ src/lib/gui/MainWindow.h | 1 + src/lib/gui/core/CoreProcess.cpp | 2 ++ src/lib/gui/core/CoreProcess.h | 1 + translations/deskflow_es.ts | 12 ++++++++++ translations/deskflow_it.ts | 12 ++++++++++ translations/deskflow_ja.ts | 12 ++++++++++ translations/deskflow_ko.ts | 12 ++++++++++ translations/deskflow_ru.ts | 12 ++++++++++ translations/deskflow_zh_CN.ts | 12 ++++++++++ 14 files changed, 113 insertions(+), 14 deletions(-) diff --git a/src/lib/client/ServerProxy.cpp b/src/lib/client/ServerProxy.cpp index 600f05697adb..c23cd6a0d425 100644 --- a/src/lib/client/ServerProxy.cpp +++ b/src/lib/client/ServerProxy.cpp @@ -18,6 +18,7 @@ #include "deskflow/ProtocolTypes.h" #include "deskflow/ProtocolUtil.h" #include "deskflow/StreamChunker.h" +#include "deskflow/ipc/CoreIpc.h" #include "io/IStream.h" #include @@ -139,7 +140,12 @@ ServerProxy::ConnectionResult ServerProxy::parseHandshakeMessage(const uint8_t * // handshake is complete m_parser = &ServerProxy::parseMessage; - checkMissedLanguages(); + + if (const auto missedKeyboardLayouts = m_languageManager.getMissedLanguages(); !missedKeyboardLayouts.empty()) { + LOG_WARN("server layouts missing on this computer: %s", missedKeyboardLayouts.c_str()); + ipcSendToClient("missingKeyboardLayouts", QString::fromStdString(missedKeyboardLayouts)); + } + m_client->handshakeComplete(); } @@ -844,15 +850,3 @@ void ServerProxy::setActiveServerLanguage(const std::string_view &language) LOG_DEBUG1("active server language is empty"); } } - -void ServerProxy::checkMissedLanguages() const -{ - auto missedLanguages = m_languageManager.getMissedLanguages(); - if (!missedLanguages.empty()) { - LOG( - (CLOG_WARN "You need to install these languages on this computer and restart " - "Deskflow to enable support for multiple languages: %s", - missedLanguages.c_str()) - ); - } -} diff --git a/src/lib/client/ServerProxy.h b/src/lib/client/ServerProxy.h index 2c638b8ca523..59535664018f 100644 --- a/src/lib/client/ServerProxy.h +++ b/src/lib/client/ServerProxy.h @@ -98,7 +98,6 @@ class ServerProxy void secureInputNotification(); void setServerLanguages(); void setActiveServerLanguage(const std::string_view &language); - void checkMissedLanguages() const; private: using MessageParser = ConnectionResult (ServerProxy::*)(const uint8_t *); diff --git a/src/lib/common/Settings.h b/src/lib/common/Settings.h index 5e7ee0f0e88e..59295066ad70 100644 --- a/src/lib/common/Settings.h +++ b/src/lib/common/Settings.h @@ -85,6 +85,7 @@ class Settings : public QObject inline static const auto ShownFirstConnectedMessage = QStringLiteral("gui/shownFirstConnectedMessage"); inline static const auto ShownServerFirstStartMessage = QStringLiteral("gui/shownServerFirstStartMessage"); inline static const auto ShowVersionInTitle = QStringLiteral("gui/showVersionInTitle"); + inline static const auto IgnoreMissingKeyboardLayouts = QStringLiteral("gui/ignoreMissingKeyboardLayouts"); }; struct Log { @@ -243,6 +244,7 @@ class Settings : public QObject , Settings::Gui::ShownFirstConnectedMessage , Settings::Gui::ShownServerFirstStartMessage , Settings::Gui::ShowVersionInTitle + , Settings::Gui::IgnoreMissingKeyboardLayouts , Settings::Security::Certificate , Settings::Security::CheckPeers , Settings::Security::KeySize @@ -258,6 +260,7 @@ class Settings : public QObject , Settings::Gui::ShownFirstConnectedMessage , Settings::Gui::ShownServerFirstStartMessage , Settings::Gui::ShowVersionInTitle + , Settings::Gui::IgnoreMissingKeyboardLayouts , Settings::Core::PreventSleep , Settings::Core::UseWlClipboard , Settings::Core::EnableEnterCommand diff --git a/src/lib/deskflow/languages/LanguageManager.h b/src/lib/deskflow/languages/LanguageManager.h index 3cd1f19fa73b..4aef2db60157 100644 --- a/src/lib/deskflow/languages/LanguageManager.h +++ b/src/lib/deskflow/languages/LanguageManager.h @@ -11,6 +11,8 @@ namespace deskflow::languages { +// TODO: rename class and namespace; "languages" is a misnomer. These are keyboard +// _layouts_ (Windows/X11) or input sources (macOS), not spoken "languages". class LanguageManager { std::vector m_remoteLanguages; diff --git a/src/lib/gui/MainWindow.cpp b/src/lib/gui/MainWindow.cpp index bd8b7177a32c..7d2622a0b942 100644 --- a/src/lib/gui/MainWindow.cpp +++ b/src/lib/gui/MainWindow.cpp @@ -31,6 +31,7 @@ #include "net/FingerprintDatabase.h" #include "widgets/StatusBar.h" +#include #include #include #include @@ -272,6 +273,7 @@ void MainWindow::connectSlots() connect(&m_coreProcess, &CoreProcess::connectionRefused, this, &MainWindow::handleConnectionRefused); connect(&m_coreProcess, &CoreProcess::retryIn, this, &MainWindow::updateTimeoutDelay); connect(&m_coreProcess, &CoreProcess::peerFingerprint, this, &MainWindow::handlePeerFingerprint); + connect(&m_coreProcess, &CoreProcess::missingKeyboardLayouts, this, &MainWindow::handleMissingKeyboardLayouts); if (Settings::value(Settings::Gui::AutoStartCore).toBool()) { connect(ui->btnToggleCore, &QPushButton::clicked, m_actionStopCore, &QAction::trigger, Qt::UniqueConnection); @@ -802,6 +804,29 @@ void MainWindow::handleConnectionRefused(deskflow::core::ConnectionRefusal reaso m_clientErrorVisible = false; } +void MainWindow::handleMissingKeyboardLayouts(const QString &layouts) +{ + if (Settings::value(Settings::Gui::IgnoreMissingKeyboardLayouts).toBool()) + return; + + QMessageBox msgBox(this); + msgBox.setIcon(QMessageBox::Warning); + msgBox.setWindowTitle(tr("Missing Keyboard Layouts")); + msgBox.setText(tr("

Keyboard layout support requires matching layouts on all computers. " + "The following layouts from the other computer are not installed on this computer:

" + "

%1

" + "

Please install them to enable support for these layouts.

") + .arg(layouts)); + + auto *checkBox = new QCheckBox(tr("Don't show this again"), &msgBox); + msgBox.setCheckBox(checkBox); + msgBox.exec(); + + if (checkBox->isChecked()) { + Settings::setValue(Settings::Gui::IgnoreMissingKeyboardLayouts, true); + } +} + void MainWindow::handlePeerFingerprint(const QString &fingerprint) { const auto sha256Text = QString(fingerprint).remove(':'); diff --git a/src/lib/gui/MainWindow.h b/src/lib/gui/MainWindow.h index fbac2fcc8782..1ec5d97f6767 100644 --- a/src/lib/gui/MainWindow.h +++ b/src/lib/gui/MainWindow.h @@ -119,6 +119,7 @@ class MainWindow : public QMainWindow void handleUnrecognisedClient(const QString &clientName); void handleConnectionRefused(deskflow::core::ConnectionRefusal reason); void handlePeerFingerprint(const QString &fingerprint); + void handleMissingKeyboardLayouts(const QString &layouts); void closeEvent(QCloseEvent *event) override; void secureSocket(bool secureSocket); void connectSlots(); diff --git a/src/lib/gui/core/CoreProcess.cpp b/src/lib/gui/core/CoreProcess.cpp index 935f36081c00..ef634dfef331 100644 --- a/src/lib/gui/core/CoreProcess.cpp +++ b/src/lib/gui/core/CoreProcess.cpp @@ -564,6 +564,8 @@ void CoreProcess::onCoreIpcMessageReceived(const QString &command, const QString Q_EMIT retryIn(args.toInt()); } else if (command == "peerFingerprint") { Q_EMIT peerFingerprint(args); + } else if (command == "missingKeyboardLayouts") { + Q_EMIT missingKeyboardLayouts(args); } } diff --git a/src/lib/gui/core/CoreProcess.h b/src/lib/gui/core/CoreProcess.h index 45cfa34bea8e..71cebca01360 100644 --- a/src/lib/gui/core/CoreProcess.h +++ b/src/lib/gui/core/CoreProcess.h @@ -93,6 +93,7 @@ class CoreProcess : public QObject void connectionRefused(deskflow::core::ConnectionRefusal reason); void retryIn(int seconds); void peerFingerprint(const QString &fingerprint); + void missingKeyboardLayouts(const QString &layouts); private Q_SLOTS: void onProcessFinished(int exitCode, QProcess::ExitStatus); diff --git a/translations/deskflow_es.ts b/translations/deskflow_es.ts index 05dc0f82fbc7..f4d9e94c4594 100644 --- a/translations/deskflow_es.ts +++ b/translations/deskflow_es.ts @@ -559,6 +559,18 @@ Additionally, check you are able to %1 the server config file: %2 Además, verifique que puede %1 el archivo de configuración del servidor: %2 + + Don't show this again + + + + Missing Keyboard Layouts + + + + <p>Keyboard layout support requires matching layouts on all computers. The following layouts from the other computer are not installed on this computer:</p><p><b>%1</b></p><p>Please install them to enable support for these layouts.</p> + + NewScreenWidget diff --git a/translations/deskflow_it.ts b/translations/deskflow_it.ts index f58928c6ce27..7620a385603a 100644 --- a/translations/deskflow_it.ts +++ b/translations/deskflow_it.ts @@ -559,6 +559,18 @@ Additionally, check you are able to %1 the server config file: %2 Inoltre, verifica di poter %1 il file di configurazione del server: %2 + + Don't show this again + + + + Missing Keyboard Layouts + + + + <p>Keyboard layout support requires matching layouts on all computers. The following layouts from the other computer are not installed on this computer:</p><p><b>%1</b></p><p>Please install them to enable support for these layouts.</p> + + NewScreenWidget diff --git a/translations/deskflow_ja.ts b/translations/deskflow_ja.ts index 1484e003d83b..decc3c314f8e 100644 --- a/translations/deskflow_ja.ts +++ b/translations/deskflow_ja.ts @@ -559,6 +559,18 @@ Additionally, check you are able to %1 the server config file: %2 また、サーバー設定ファイルを%1できることを確認してください: %2 + + Don't show this again + + + + Missing Keyboard Layouts + + + + <p>Keyboard layout support requires matching layouts on all computers. The following layouts from the other computer are not installed on this computer:</p><p><b>%1</b></p><p>Please install them to enable support for these layouts.</p> + + NewScreenWidget diff --git a/translations/deskflow_ko.ts b/translations/deskflow_ko.ts index e96f0cdbb3a2..b1e1bdecf48f 100644 --- a/translations/deskflow_ko.ts +++ b/translations/deskflow_ko.ts @@ -559,6 +559,18 @@ Additionally, check you are able to %1 the server config file: %2 또한 서버 구성 파일을 %1할 수 있는지 확인하세요: %2 + + Don't show this again + + + + Missing Keyboard Layouts + + + + <p>Keyboard layout support requires matching layouts on all computers. The following layouts from the other computer are not installed on this computer:</p><p><b>%1</b></p><p>Please install them to enable support for these layouts.</p> + + NewScreenWidget diff --git a/translations/deskflow_ru.ts b/translations/deskflow_ru.ts index 0f6f316aaf0a..de4223837eb0 100644 --- a/translations/deskflow_ru.ts +++ b/translations/deskflow_ru.ts @@ -559,6 +559,18 @@ Additionally, check you are able to %1 the server config file: %2 Также убедитесь, что вы можете %1 файл конфигурации сервера: %2 + + Don't show this again + + + + Missing Keyboard Layouts + + + + <p>Keyboard layout support requires matching layouts on all computers. The following layouts from the other computer are not installed on this computer:</p><p><b>%1</b></p><p>Please install them to enable support for these layouts.</p> + + NewScreenWidget diff --git a/translations/deskflow_zh_CN.ts b/translations/deskflow_zh_CN.ts index cd9fdf2c0f57..30a3384a84e8 100644 --- a/translations/deskflow_zh_CN.ts +++ b/translations/deskflow_zh_CN.ts @@ -559,6 +559,18 @@ Additionally, check you are able to %1 the server config file: %2 另外,请检查您是否能够%1服务器配置文件:%2 + + Don't show this again + + + + Missing Keyboard Layouts + + + + <p>Keyboard layout support requires matching layouts on all computers. The following layouts from the other computer are not installed on this computer:</p><p><b>%1</b></p><p>Please install them to enable support for these layouts.</p> + + NewScreenWidget From 25f3eb3f229e9c934e319c3d68623ba81201eda6 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Tue, 31 Mar 2026 10:06:00 +0100 Subject: [PATCH 17/24] feat(ipc): Differentiate between Daemon and Core IPC logs --- src/lib/deskflow/ipc/CoreIpcServer.cpp | 2 +- src/lib/deskflow/ipc/DaemonIpcServer.cpp | 2 +- src/lib/deskflow/ipc/IpcServer.cpp | 55 +++++++++++++----------- src/lib/deskflow/ipc/IpcServer.h | 3 +- src/lib/gui/ipc/CoreIpcClient.cpp | 2 +- src/lib/gui/ipc/DaemonIpcClient.cpp | 2 +- src/lib/gui/ipc/IpcClient.cpp | 46 +++++++++++--------- src/lib/gui/ipc/IpcClient.h | 3 +- 8 files changed, 65 insertions(+), 50 deletions(-) diff --git a/src/lib/deskflow/ipc/CoreIpcServer.cpp b/src/lib/deskflow/ipc/CoreIpcServer.cpp index 4ead60f9f8d9..f73f726316c6 100644 --- a/src/lib/deskflow/ipc/CoreIpcServer.cpp +++ b/src/lib/deskflow/ipc/CoreIpcServer.cpp @@ -15,7 +15,7 @@ namespace deskflow::core::ipc { static CoreIpcServer *s_instance = nullptr; -CoreIpcServer::CoreIpcServer(QObject *parent) : IpcServer(parent, kCoreIpcName) +CoreIpcServer::CoreIpcServer(QObject *parent) : IpcServer(parent, kCoreIpcName, "core") { assert(s_instance == nullptr); s_instance = this; diff --git a/src/lib/deskflow/ipc/DaemonIpcServer.cpp b/src/lib/deskflow/ipc/DaemonIpcServer.cpp index 366151a4129e..9d34eabca966 100644 --- a/src/lib/deskflow/ipc/DaemonIpcServer.cpp +++ b/src/lib/deskflow/ipc/DaemonIpcServer.cpp @@ -17,7 +17,7 @@ const auto kAckMessage = "ok"; const auto kErrorMessage = "error"; DaemonIpcServer::DaemonIpcServer(QObject *parent, const QString &logFilename) - : IpcServer(parent, kDaemonIpcName), + : IpcServer(parent, kDaemonIpcName, "daemon"), m_logFilename(logFilename) { // do nothing diff --git a/src/lib/deskflow/ipc/IpcServer.cpp b/src/lib/deskflow/ipc/IpcServer.cpp index 39eff2971a94..39c622add335 100644 --- a/src/lib/deskflow/ipc/IpcServer.cpp +++ b/src/lib/deskflow/ipc/IpcServer.cpp @@ -14,10 +14,11 @@ namespace deskflow::core::ipc { -IpcServer::IpcServer(QObject *parent, const QString &serverName) +IpcServer::IpcServer(QObject *parent, const QString &serverName, const QString &typeName) : QObject(parent), m_server{new QLocalServer(this)}, // NOSONAR - Qt memory - m_serverName(serverName) + m_serverName(serverName), + m_typeName(typeName.toUtf8()) { // do nothing } @@ -35,9 +36,9 @@ void IpcServer::listen() 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()); + LOG_DEBUG("%s ipc server listening on: %s", m_typeName.constData(), m_serverName.toUtf8().constData()); } else { - LOG_ERR("ipc server failed to listen on: %s", m_serverName.toUtf8().constData()); + LOG_ERR("%s ipc server failed to listen on: %s", m_typeName.constData(), m_serverName.toUtf8().constData()); } } @@ -45,11 +46,11 @@ void IpcServer::handleNewConnection() { QLocalSocket *clientSocket = m_server->nextPendingConnection(); if (!clientSocket) { - LOG_ERR("ipc server failed to get new connection"); + LOG_ERR("%s ipc server failed to get new connection", m_typeName.constData()); return; } - LOG_DEBUG("ipc server got new connection"); + LOG_DEBUG("%s ipc server got new connection", m_typeName.constData()); m_clients.insert(clientSocket); connect(clientSocket, &QLocalSocket::readyRead, this, &IpcServer::handleReadyRead); @@ -60,17 +61,17 @@ void IpcServer::handleNewConnection() void IpcServer::handleReadyRead() { const auto clientSocket = qobject_cast(sender()); - LOG_DEBUG1("ipc server ready to read data"); + LOG_DEBUG1("%s ipc server ready to read data", m_typeName.constData()); QByteArray data = clientSocket->readAll(); if (data.isEmpty()) { - LOG_WARN("ipc server got empty message"); + LOG_WARN("%s ipc server got empty message", m_typeName.constData()); 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()); + LOG_WARN("%s ipc server got incomplete message: %s", m_typeName.constData(), data.constData()); return; } @@ -87,7 +88,7 @@ void IpcServer::handleReadyRead() void IpcServer::handleDisconnected() { const auto clientSocket = qobject_cast(sender()); - LOG_DEBUG("ipc server client disconnected"); + LOG_DEBUG("%s ipc server client disconnected", m_typeName.constData()); m_clients.remove(clientSocket); clientSocket->deleteLater(); } @@ -95,17 +96,17 @@ void IpcServer::handleDisconnected() void IpcServer::handleErrorOccurred() { const auto clientSocket = qobject_cast(sender()); - LOG_ERR("ipc server client error: %s", clientSocket->errorString().toUtf8().constData()); + LOG_ERR("%s ipc server client error: %s", m_typeName.constData(), clientSocket->errorString().toUtf8().constData()); m_clients.remove(clientSocket); clientSocket->deleteLater(); } void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &message) { - LOG_DEBUG1("ipc server got message: %s", message.toUtf8().constData()); + LOG_DEBUG1("%s ipc server got message: %s", m_typeName.constData(), message.toUtf8().constData()); const auto parts = message.split('='); if (parts.isEmpty()) { - LOG_ERR("ipc server got invalid message: %s", message.toUtf8().constData()); + LOG_ERR("%s ipc server got invalid message: %s", m_typeName.constData(), message.toUtf8().constData()); writeToClientSocket(clientSocket, "error"); return; } @@ -113,29 +114,28 @@ void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &messag if (const auto &command = parts.at(0); command == "hello") { const auto versionId = QStringLiteral("%1+%2").arg(kVersion, kVersionGitSha); const auto clientVersion = parts.size() >= 2 ? parts.at(1) : QString(); + LOG_DEBUG("%s ipc server got hello message (version: %s)", m_typeName.constData(), versionId.toUtf8().constData()); + if (clientVersion != versionId) { - LOG_ERR( - "ipc client version mismatch (client: %s, server: %s)", - clientVersion.isEmpty() ? "unknown" : clientVersion.toUtf8().constData(), versionId.toUtf8().constData() - ); + LOG_ERR("%s ipc client version mismatch (server: %s)", m_typeName.constData(), versionId.toUtf8().constData()); writeToClientSocket(clientSocket, "error"); clientSocket->flush(); clientSocket->disconnectFromServer(); return; } - LOG_DEBUG("ipc server got hello message, sending hello back"); + LOG_DEBUG("%s ipc server sending hello back", m_typeName.constData()); writeToClientSocket(clientSocket, QString("hello=%1").arg(versionId)); // Replay messages that were queued before any clients connected. LOG_DEBUG1("ipc server replaying %d pending messages", m_pendingMessages.size()); for (const auto &pending : std::as_const(m_pendingMessages)) { - LOG_DEBUG1("ipc server replaying: %s", pending.toUtf8().constData()); + LOG_DEBUG1("%s ipc server replaying: %s", m_typeName.constData(), pending.toUtf8().constData()); writeToClientSocket(clientSocket, pending); } m_pendingMessages.clear(); } else if (command == "noop") { - LOG_DEBUG("ipc server got noop message"); + LOG_DEBUG("%s ipc server got noop message", m_typeName.constData()); writeToClientSocket(clientSocket, "ok"); } else { processCommand(clientSocket, command, parts); @@ -149,12 +149,17 @@ void IpcServer::broadcastCommand(const QString &command, const QString &args) const auto message = args.isEmpty() ? command : command + "=" + args; if (m_clients.isEmpty()) { - LOG_DEBUG1("ipc server has no clients, message queued: %s", message.toUtf8().constData()); + LOG_DEBUG1( + "%s ipc server has no clients, message queued: %s", m_typeName.constData(), message.toUtf8().constData() + ); m_pendingMessages.append(message); return; } - LOG_DEBUG1("ipc server broadcasting message to %d clients: %s", m_clients.size(), message.toUtf8().constData()); + LOG_DEBUG1( + "%s ipc server broadcasting message to %d clients: %s", m_typeName.constData(), m_clients.size(), + message.toUtf8().constData() + ); for (auto *client : std::as_const(m_clients)) { writeToClientSocket(client, message); client->flush(); @@ -166,9 +171,11 @@ void IpcServer::writeToClientSocket(QLocalSocket *&clientSocket, const QString & 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"); + LOG_ERR("%s ipc server failed to write full message to client socket", m_typeName.constData()); } else { - LOG_DEBUG1("ipc server wrote message to client socket: %s", message.toUtf8().constData()); + LOG_DEBUG1( + "%s ipc server wrote message to client socket: %s", m_typeName.constData(), message.toUtf8().constData() + ); } } diff --git a/src/lib/deskflow/ipc/IpcServer.h b/src/lib/deskflow/ipc/IpcServer.h index 3e2a874b9ef0..e26000a699fe 100644 --- a/src/lib/deskflow/ipc/IpcServer.h +++ b/src/lib/deskflow/ipc/IpcServer.h @@ -19,7 +19,7 @@ class IpcServer : public QObject Q_OBJECT public: - explicit IpcServer(QObject *parent, const QString &serverName); + explicit IpcServer(QObject *parent, const QString &serverName, const QString &typeName); ~IpcServer() override; void listen(); @@ -54,6 +54,7 @@ class IpcServer : public QObject QSet m_clients; QString m_serverName; QStringList m_pendingMessages; + QByteArray m_typeName; }; } // namespace deskflow::core::ipc diff --git a/src/lib/gui/ipc/CoreIpcClient.cpp b/src/lib/gui/ipc/CoreIpcClient.cpp index 047761efdd08..ccfa2ed5ecd6 100644 --- a/src/lib/gui/ipc/CoreIpcClient.cpp +++ b/src/lib/gui/ipc/CoreIpcClient.cpp @@ -15,7 +15,7 @@ namespace deskflow::gui::ipc { -CoreIpcClient::CoreIpcClient(QObject *parent) : IpcClient(parent, kCoreIpcName) +CoreIpcClient::CoreIpcClient(QObject *parent) : IpcClient(parent, kCoreIpcName, "core") { // do nothing } diff --git a/src/lib/gui/ipc/DaemonIpcClient.cpp b/src/lib/gui/ipc/DaemonIpcClient.cpp index 124967eac37d..454d8f354fd2 100644 --- a/src/lib/gui/ipc/DaemonIpcClient.cpp +++ b/src/lib/gui/ipc/DaemonIpcClient.cpp @@ -12,7 +12,7 @@ namespace deskflow::gui::ipc { -DaemonIpcClient::DaemonIpcClient(QObject *parent) : IpcClient(parent, kDaemonIpcName) +DaemonIpcClient::DaemonIpcClient(QObject *parent) : IpcClient(parent, kDaemonIpcName, "daemon") { } diff --git a/src/lib/gui/ipc/IpcClient.cpp b/src/lib/gui/ipc/IpcClient.cpp index 644d9470588c..19af227bc8e0 100644 --- a/src/lib/gui/ipc/IpcClient.cpp +++ b/src/lib/gui/ipc/IpcClient.cpp @@ -14,10 +14,11 @@ namespace deskflow::gui::ipc { -IpcClient::IpcClient(QObject *parent, const QString &socketName) +IpcClient::IpcClient(QObject *parent, const QString &socketName, const QString &typeName) : QObject(parent), m_socket{new QLocalSocket(this)}, - m_socketName(socketName) // NOSONAR - Qt memory + m_socketName(socketName), // NOSONAR - Qt memory + m_typeName(typeName) { connect(m_socket, &QLocalSocket::disconnected, this, &IpcClient::handleDisconnected); connect(m_socket, &QLocalSocket::errorOccurred, this, &IpcClient::handleErrorOccurred); @@ -27,17 +28,18 @@ IpcClient::IpcClient(QObject *parent, const QString &socketName) void IpcClient::connectToServer() { if (m_state == State::Connecting) { - qWarning() << "ipc client already connecting to server"; + qWarning().noquote() << QStringLiteral("%1 ipc client already connecting to server").arg(m_typeName); return; } if (m_state != State::Unconnected) { - qDebug() << "ipc client not in unconnected state, disconnecting"; + qDebug().noquote() << QStringLiteral("%1 ipc client not in unconnected state, disconnecting").arg(m_typeName); disconnectFromServer(); } if (m_socket->state() != QLocalSocket::UnconnectedState) { - qWarning() << "ipc client socket not in unconnected state, disconnecting"; + qWarning().noquote( + ) << QStringLiteral("%1 ipc client socket not in unconnected state, disconnecting").arg(m_typeName); disconnectFromServer(); } @@ -50,16 +52,18 @@ void IpcClient::attemptConnection() const auto kRetryLimit = 3; if (m_retryCount >= kRetryLimit) { - qWarning() << "ipc client failed to connect after" << kRetryLimit << "attempts"; + qWarning().noquote() << QStringLiteral("%1 ipc client failed to connect after %2 attempts") + .arg(m_typeName, QString::number(kRetryLimit)); m_state = State::Unconnected; Q_EMIT connectionFailed(); return; } if (m_retryCount == 0) { - qDebug() << "ipc client connecting to server:" << m_socketName; + qDebug().noquote() << QStringLiteral("%1 ipc client connecting to server: %2").arg(m_typeName, m_socketName); } else { - qDebug() << "ipc client retrying connection, attempt:" << m_retryCount + 1; + qDebug().noquote() << QStringLiteral("%1 ipc client retrying connection, attempt: %2") + .arg(m_typeName, QString::number(m_retryCount + 1)); } m_state = State::Connecting; @@ -69,8 +73,8 @@ void IpcClient::attemptConnection() m_socket, &QLocalSocket::connected, this, [this] { const auto versionId = QStringLiteral("%1+%2").arg(kVersion, kVersionGitSha); - m_socket->write(QString("hello=%1\n").arg(versionId).toUtf8()); - qDebug() << "ipc client sent hello with version:" << versionId; + m_socket->write(QStringLiteral("hello=%1\n").arg(versionId).toUtf8()); + qDebug().noquote() << QStringLiteral("%1 ipc client sent hello with version: %2").arg(m_typeName, versionId); }, Qt::SingleShotConnection ); @@ -78,7 +82,8 @@ void IpcClient::attemptConnection() connect( m_socket, &QLocalSocket::errorOccurred, this, [this] { - qWarning() << "ipc client failed to connect:" << m_socket->errorString(); + qWarning().noquote( + ) << QStringLiteral("%1 ipc client failed to connect: %2").arg(m_typeName, m_socket->errorString()); m_socket->disconnectFromServer(); m_state = State::Unconnected; QTimer::singleShot(0, this, &IpcClient::attemptConnection); @@ -92,7 +97,7 @@ void IpcClient::attemptConnection() void IpcClient::disconnectFromServer() { m_state = State::Disconnecting; - qDebug() << "ipc client disconnecting from server"; + qDebug().noquote() << QStringLiteral("%1 ipc client disconnecting from server").arg(m_typeName); m_socket->disconnectFromServer(); m_state = State::Unconnected; } @@ -103,7 +108,7 @@ void IpcClient::handleDisconnected() return; } - qDebug() << "ipc client disconnected from server"; + qDebug().noquote() << QStringLiteral("%1 ipc client disconnected from server").arg(m_typeName); const auto wasConnected = m_state == State::Connected; m_state = State::Unconnected; @@ -118,7 +123,7 @@ void IpcClient::handleErrorOccurred() return; } - qWarning() << "ipc client error:" << m_socket->errorString(); + qWarning().noquote() << QStringLiteral("%1 ipc client error: %2").arg(m_typeName, m_socket->errorString()); if (m_state == State::Connected) { disconnectFromServer(); @@ -136,10 +141,10 @@ void IpcClient::handleReadyRead() const auto message = QString::fromUtf8(data.left(index)); data.remove(0, index + 1); - qDebug("ipc client message: %s", message.toUtf8().constData()); + qDebug().noquote() << QStringLiteral("%1 ipc client message: %2").arg(m_typeName, message); const auto parts = message.split('='); if (parts.isEmpty()) { - qWarning("ipc client got invalid message: %s", message.toUtf8().constData()); + qWarning().noquote() << QStringLiteral("%1 ipc client got invalid message: %2").arg(m_typeName, message); continue; } @@ -165,26 +170,27 @@ void IpcClient::handleHandshakeMessage(const QStringList &parts) const auto versionId = QStringLiteral("%1+%2").arg(kVersion, kVersionGitSha); const auto serverVersion = parts.size() >= 2 ? parts.at(1) : QString(); if (serverVersion != versionId) { - qCritical() << "ipc version mismatch (client:" << versionId << "server:" << serverVersion << ")"; + qCritical().noquote( + ) << QStringLiteral("%1 ipc version mismatch (client: %2 , server: %3)").arg(m_typeName, versionId, serverVersion); disconnectFromServer(); Q_EMIT connectionFailed(); return; } m_state = State::Connected; - qDebug() << "ipc client connected"; + qDebug().noquote() << QStringLiteral("%1 ipc client connected").arg(m_typeName); Q_EMIT connected(); } void IpcClient::sendMessage(const QString &message) { if (m_state != State::Connected) { - qWarning() << "cannot send command, ipc client not connected"; + qWarning().noquote() << QStringLiteral("%1 cannot send command, ipc client not connected").arg(m_typeName); return; } m_socket->write(message.toUtf8() + "\n"); - qDebug() << "ipc client sent message:" << message; + qDebug().noquote() << QStringLiteral("%1 ipc client sent message: %2").arg(m_typeName, message); } } // namespace deskflow::gui::ipc diff --git a/src/lib/gui/ipc/IpcClient.h b/src/lib/gui/ipc/IpcClient.h index 1f2b296d6db1..231b1600f064 100644 --- a/src/lib/gui/ipc/IpcClient.h +++ b/src/lib/gui/ipc/IpcClient.h @@ -26,7 +26,7 @@ class IpcClient : public QObject }; public: - explicit IpcClient(QObject *parent, const QString &socketName); + explicit IpcClient(QObject *parent, const QString &socketName, const QString &typeName); void connectToServer(); void disconnectFromServer(); @@ -62,6 +62,7 @@ private Q_SLOTS: QString m_socketName; QByteArray m_readBuffer; int m_retryCount{0}; + QString m_typeName; }; } // namespace deskflow::gui::ipc From 77bee2ddf3af50be240d4fd09414062220dcf6be Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Tue, 31 Mar 2026 10:24:59 +0100 Subject: [PATCH 18/24] feat(ipc): Log more detail on IPC error handling --- src/lib/deskflow/ipc/IpcServer.cpp | 2 +- src/lib/gui/ipc/IpcClient.cpp | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/lib/deskflow/ipc/IpcServer.cpp b/src/lib/deskflow/ipc/IpcServer.cpp index 39c622add335..71607bcc6e64 100644 --- a/src/lib/deskflow/ipc/IpcServer.cpp +++ b/src/lib/deskflow/ipc/IpcServer.cpp @@ -118,7 +118,7 @@ void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &messag if (clientVersion != versionId) { LOG_ERR("%s ipc client version mismatch (server: %s)", m_typeName.constData(), versionId.toUtf8().constData()); - writeToClientSocket(clientSocket, "error"); + writeToClientSocket(clientSocket, QStringLiteral("error=version mismatch, expected: %1").arg(versionId)); clientSocket->flush(); clientSocket->disconnectFromServer(); return; diff --git a/src/lib/gui/ipc/IpcClient.cpp b/src/lib/gui/ipc/IpcClient.cpp index 19af227bc8e0..700e216b1600 100644 --- a/src/lib/gui/ipc/IpcClient.cpp +++ b/src/lib/gui/ipc/IpcClient.cpp @@ -163,6 +163,14 @@ void IpcClient::handleReadyRead() void IpcClient::handleHandshakeMessage(const QStringList &parts) { + if (parts.at(0) == "error") { + const auto detail = parts.size() >= 2 ? parts.at(1) : QString("unknown"); + qCritical().noquote() << m_typeName << "ipc server rejected connection:" << detail; + disconnectFromServer(); + Q_EMIT connectionFailed(); + return; + } + if (parts.at(0) != "hello") { return; } From 7c30add2a1966d2f47bf0e9c608aa916b45f2eab Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Tue, 7 Apr 2026 12:30:33 +0100 Subject: [PATCH 19/24] feat(ipc): Error handling for missing version in IPC handshake --- src/lib/deskflow/ipc/IpcServer.cpp | 15 +++++++++++++-- src/lib/gui/ipc/IpcClient.cpp | 9 ++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/lib/deskflow/ipc/IpcServer.cpp b/src/lib/deskflow/ipc/IpcServer.cpp index 71607bcc6e64..50ebe04f54e1 100644 --- a/src/lib/deskflow/ipc/IpcServer.cpp +++ b/src/lib/deskflow/ipc/IpcServer.cpp @@ -112,12 +112,23 @@ void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &messag } if (const auto &command = parts.at(0); command == "hello") { + if (parts.size() < 2) { + LOG_ERR("%s ipc client hello missing version", m_typeName.constData()); + writeToClientSocket(clientSocket, "error=missing version"); + clientSocket->flush(); + clientSocket->disconnectFromServer(); + return; + } + const auto versionId = QStringLiteral("%1+%2").arg(kVersion, kVersionGitSha); - const auto clientVersion = parts.size() >= 2 ? parts.at(1) : QString(); + const auto clientVersion = parts.at(1); LOG_DEBUG("%s ipc server got hello message (version: %s)", m_typeName.constData(), versionId.toUtf8().constData()); if (clientVersion != versionId) { - LOG_ERR("%s ipc client version mismatch (server: %s)", m_typeName.constData(), versionId.toUtf8().constData()); + LOG_ERR( + "%s ipc client version mismatch (client: %s, server: %s)", m_typeName.constData(), + clientVersion.toUtf8().constData(), versionId.toUtf8().constData() + ); writeToClientSocket(clientSocket, QStringLiteral("error=version mismatch, expected: %1").arg(versionId)); clientSocket->flush(); clientSocket->disconnectFromServer(); diff --git a/src/lib/gui/ipc/IpcClient.cpp b/src/lib/gui/ipc/IpcClient.cpp index 700e216b1600..7418340b43d9 100644 --- a/src/lib/gui/ipc/IpcClient.cpp +++ b/src/lib/gui/ipc/IpcClient.cpp @@ -175,8 +175,15 @@ void IpcClient::handleHandshakeMessage(const QStringList &parts) return; } + if (parts.size() < 2) { + qCritical().noquote() << QStringLiteral("%1 ipc server hello missing version").arg(m_typeName); + disconnectFromServer(); + Q_EMIT connectionFailed(); + return; + } + const auto versionId = QStringLiteral("%1+%2").arg(kVersion, kVersionGitSha); - const auto serverVersion = parts.size() >= 2 ? parts.at(1) : QString(); + const auto serverVersion = parts.at(1); if (serverVersion != versionId) { qCritical().noquote( ) << QStringLiteral("%1 ipc version mismatch (client: %2 , server: %3)").arg(m_typeName, versionId, serverVersion); From 7f2953d422169d9669e844c91f83d8215df40a39 Mon Sep 17 00:00:00 2001 From: sithlord48 Date: Mon, 6 Apr 2026 07:30:23 -0400 Subject: [PATCH 20/24] ci: Remove stale action --- .github/workflows/issue-check-stale.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .github/workflows/issue-check-stale.yml diff --git a/.github/workflows/issue-check-stale.yml b/.github/workflows/issue-check-stale.yml deleted file mode 100644 index 9393987bbc04..000000000000 --- a/.github/workflows/issue-check-stale.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Close stale issues and PRs -on: - schedule: - - cron: "0 5 * * *" - -jobs: - stale-issues: - runs-on: ubuntu-slim - timeout-minutes: 10 - - steps: - - uses: actions/stale@v6 - with: - stale-issue-message: "This issue is stale because it has been open 1 year with no activity. Remove the stale label or add a comment, otherwise this will be closed in 30 days." - stale-pr-message: "This PR is stale because it has been open 1 year with no activity. Remove the stale label or add a comment, otherwise this will be closed in 30 days." - close-issue-message: "This issue was closed because it has been stale for 1 year with no activity." - close-pr-message: "This PR was closed because it has been stale for 1 year with no activity." - days-before-issue-stale: 365 - days-before-pr-stale: 365 - days-before-issue-close: 30 - days-before-pr-close: 30 - stale-issue-label: ":bread: stale" - stale-pr-label: ":bread: stale" From bdb73097781464fab61c34628892d07b17579278 Mon Sep 17 00:00:00 2001 From: sithlord48 Date: Tue, 7 Apr 2026 12:16:13 -0400 Subject: [PATCH 21/24] refactor(ipcClient): declare values in if statement where possible --- src/lib/gui/ipc/IpcClient.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/lib/gui/ipc/IpcClient.cpp b/src/lib/gui/ipc/IpcClient.cpp index 7418340b43d9..37a7ee85665f 100644 --- a/src/lib/gui/ipc/IpcClient.cpp +++ b/src/lib/gui/ipc/IpcClient.cpp @@ -49,11 +49,9 @@ void IpcClient::connectToServer() void IpcClient::attemptConnection() { - const auto kRetryLimit = 3; - - if (m_retryCount >= kRetryLimit) { + if (const int retryLimit = 3; m_retryCount >= retryLimit) { qWarning().noquote() << QStringLiteral("%1 ipc client failed to connect after %2 attempts") - .arg(m_typeName, QString::number(kRetryLimit)); + .arg(m_typeName, QString::number(retryLimit)); m_state = State::Unconnected; Q_EMIT connectionFailed(); return; @@ -183,8 +181,7 @@ void IpcClient::handleHandshakeMessage(const QStringList &parts) } const auto versionId = QStringLiteral("%1+%2").arg(kVersion, kVersionGitSha); - const auto serverVersion = parts.at(1); - if (serverVersion != versionId) { + if (const auto serverVersion = parts.at(1); serverVersion != versionId) { qCritical().noquote( ) << QStringLiteral("%1 ipc version mismatch (client: %2 , server: %3)").arg(m_typeName, versionId, serverVersion); disconnectFromServer(); From d1074f64ae66113a1e0f31875fda4bc5840d65dd Mon Sep 17 00:00:00 2001 From: sithlord48 Date: Wed, 8 Apr 2026 01:02:16 -0400 Subject: [PATCH 22/24] refactor(ipc): use more QStringLiterals --- src/lib/deskflow/ipc/CoreIpc.cpp | 2 +- src/lib/deskflow/ipc/CoreIpcServer.cpp | 2 +- src/lib/deskflow/ipc/DaemonIpcServer.cpp | 22 +++++++++++----------- src/lib/deskflow/ipc/IpcServer.cpp | 12 ++++++------ src/lib/gui/ipc/CoreIpcClient.cpp | 2 +- src/lib/gui/ipc/DaemonIpcClient.cpp | 18 +++++++++--------- src/lib/gui/ipc/IpcClient.cpp | 8 ++++---- 7 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/lib/deskflow/ipc/CoreIpc.cpp b/src/lib/deskflow/ipc/CoreIpc.cpp index 7dad7ca71775..b48f1554da94 100644 --- a/src/lib/deskflow/ipc/CoreIpc.cpp +++ b/src/lib/deskflow/ipc/CoreIpc.cpp @@ -24,5 +24,5 @@ void ipcSendToClient(const QString &command, const QString &args) void ipcSendConnectionState(deskflow::core::ConnectionState state) { const auto metaEnum = QMetaEnum::fromType(); - ipcSendToClient("connectionState", metaEnum.valueToKey(static_cast(state))); + ipcSendToClient(QStringLiteral("connectionState"), metaEnum.valueToKey(static_cast(state))); } diff --git a/src/lib/deskflow/ipc/CoreIpcServer.cpp b/src/lib/deskflow/ipc/CoreIpcServer.cpp index f73f726316c6..f0ad0a39ddd5 100644 --- a/src/lib/deskflow/ipc/CoreIpcServer.cpp +++ b/src/lib/deskflow/ipc/CoreIpcServer.cpp @@ -15,7 +15,7 @@ namespace deskflow::core::ipc { static CoreIpcServer *s_instance = nullptr; -CoreIpcServer::CoreIpcServer(QObject *parent) : IpcServer(parent, kCoreIpcName, "core") +CoreIpcServer::CoreIpcServer(QObject *parent) : IpcServer(parent, kCoreIpcName, QStringLiteral("core")) { assert(s_instance == nullptr); s_instance = this; diff --git a/src/lib/deskflow/ipc/DaemonIpcServer.cpp b/src/lib/deskflow/ipc/DaemonIpcServer.cpp index 9d34eabca966..374a9217bf17 100644 --- a/src/lib/deskflow/ipc/DaemonIpcServer.cpp +++ b/src/lib/deskflow/ipc/DaemonIpcServer.cpp @@ -17,7 +17,7 @@ const auto kAckMessage = "ok"; const auto kErrorMessage = "error"; DaemonIpcServer::DaemonIpcServer(QObject *parent, const QString &logFilename) - : IpcServer(parent, kDaemonIpcName, "daemon"), + : IpcServer(parent, kDaemonIpcName, QStringLiteral("daemon")), m_logFilename(logFilename) { // do nothing @@ -25,24 +25,24 @@ DaemonIpcServer::DaemonIpcServer(QObject *parent, const QString &logFilename) void DaemonIpcServer::processCommand(QLocalSocket *clientSocket, const QString &command, const QStringList &parts) { - if (command == "logLevel") { + if (command == QStringLiteral("logLevel")) { processLogLevel(clientSocket, parts); - } else if (command == "elevate") { + } else if (command == QStringLiteral("elevate")) { processElevate(clientSocket, parts); - } else if (command == "command") { + } else if (command == QStringLiteral("command")) { processCommandMessage(clientSocket, parts); - } else if (command == "start") { + } else if (command == QStringLiteral("start")) { LOG_DEBUG("daemon ipc server got start message"); Q_EMIT startProcessRequested(); writeToClientSocket(clientSocket, kAckMessage); - } else if (command == "stop") { + } else if (command == QStringLiteral("stop")) { LOG_DEBUG("daemon ipc server got stop message"); Q_EMIT stopProcessRequested(); writeToClientSocket(clientSocket, kAckMessage); - } else if (command == "logPath") { + } else if (command == QStringLiteral("logPath")) { LOG_DEBUG("daemon ipc server got log path request"); - writeToClientSocket(clientSocket, "logPath=" + m_logFilename.toUtf8()); - } else if (command == "clearSettings") { + writeToClientSocket(clientSocket, QStringLiteral("logPath=%1").arg(m_logFilename.toUtf8())); + } else if (command == QStringLiteral("clearSettings")) { LOG_DEBUG("daemon ipc server got clear settings message"); Q_EMIT clearSettingsRequested(); writeToClientSocket(clientSocket, kAckMessage); @@ -80,14 +80,14 @@ void DaemonIpcServer::processElevate(QLocalSocket *&clientSocket, const QStringL } const auto &elevate = messageParts[1]; - if (elevate != "yes" && elevate != "no") { + if (elevate != QStringLiteral("yes") && elevate != QStringLiteral("no")) { LOG_ERR("daemon ipc server got invalid elevate value: %s", elevate.toUtf8().constData()); writeToClientSocket(clientSocket, kErrorMessage); return; } LOG_DEBUG("daemon ipc server got new elevate value: %s", elevate.toUtf8().constData()); - Q_EMIT elevateModeChanged(elevate == "yes"); + Q_EMIT elevateModeChanged(elevate == QStringLiteral("yes")); writeToClientSocket(clientSocket, kAckMessage); } diff --git a/src/lib/deskflow/ipc/IpcServer.cpp b/src/lib/deskflow/ipc/IpcServer.cpp index 50ebe04f54e1..616a3f3884b0 100644 --- a/src/lib/deskflow/ipc/IpcServer.cpp +++ b/src/lib/deskflow/ipc/IpcServer.cpp @@ -107,11 +107,11 @@ void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &messag const auto parts = message.split('='); if (parts.isEmpty()) { LOG_ERR("%s ipc server got invalid message: %s", m_typeName.constData(), message.toUtf8().constData()); - writeToClientSocket(clientSocket, "error"); + writeToClientSocket(clientSocket, QStringLiteral("error")); return; } - if (const auto &command = parts.at(0); command == "hello") { + if (const auto &command = parts.at(0); command == QStringLiteral("hello")) { if (parts.size() < 2) { LOG_ERR("%s ipc client hello missing version", m_typeName.constData()); writeToClientSocket(clientSocket, "error=missing version"); @@ -136,7 +136,7 @@ void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &messag } LOG_DEBUG("%s ipc server sending hello back", m_typeName.constData()); - writeToClientSocket(clientSocket, QString("hello=%1").arg(versionId)); + writeToClientSocket(clientSocket, QStringLiteral("hello=%1").arg(versionId)); // Replay messages that were queued before any clients connected. LOG_DEBUG1("ipc server replaying %d pending messages", m_pendingMessages.size()); @@ -145,9 +145,9 @@ void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &messag writeToClientSocket(clientSocket, pending); } m_pendingMessages.clear(); - } else if (command == "noop") { + } else if (command == QStringLiteral("noop")) { LOG_DEBUG("%s ipc server got noop message", m_typeName.constData()); - writeToClientSocket(clientSocket, "ok"); + writeToClientSocket(clientSocket, QStringLiteral("ok")); } else { processCommand(clientSocket, command, parts); } @@ -157,7 +157,7 @@ void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &messag void IpcServer::broadcastCommand(const QString &command, const QString &args) { - const auto message = args.isEmpty() ? command : command + "=" + args; + const auto message = args.isEmpty() ? command : QStringLiteral("%1=%2").arg(command, args); if (m_clients.isEmpty()) { LOG_DEBUG1( diff --git a/src/lib/gui/ipc/CoreIpcClient.cpp b/src/lib/gui/ipc/CoreIpcClient.cpp index ccfa2ed5ecd6..c4633b2abb8c 100644 --- a/src/lib/gui/ipc/CoreIpcClient.cpp +++ b/src/lib/gui/ipc/CoreIpcClient.cpp @@ -15,7 +15,7 @@ namespace deskflow::gui::ipc { -CoreIpcClient::CoreIpcClient(QObject *parent) : IpcClient(parent, kCoreIpcName, "core") +CoreIpcClient::CoreIpcClient(QObject *parent) : IpcClient(parent, kCoreIpcName, QStringLiteral("core")) { // do nothing } diff --git a/src/lib/gui/ipc/DaemonIpcClient.cpp b/src/lib/gui/ipc/DaemonIpcClient.cpp index 454d8f354fd2..41a4bba66c33 100644 --- a/src/lib/gui/ipc/DaemonIpcClient.cpp +++ b/src/lib/gui/ipc/DaemonIpcClient.cpp @@ -12,41 +12,41 @@ namespace deskflow::gui::ipc { -DaemonIpcClient::DaemonIpcClient(QObject *parent) : IpcClient(parent, kDaemonIpcName, "daemon") +DaemonIpcClient::DaemonIpcClient(QObject *parent) : IpcClient(parent, kDaemonIpcName, QStringLiteral("daemon")) { } void DaemonIpcClient::sendLogLevel(const QString &logLevel) { - sendMessage("logLevel=" + logLevel); + sendMessage(QStringLiteral("logLevel=%1").arg(logLevel)); } void DaemonIpcClient::sendStartProcess(const QString &command, bool elevate) { const auto elevateStr = elevate ? QStringLiteral("yes") : QStringLiteral("no"); - sendMessage("elevate=" + elevateStr); - sendMessage("command=" + command); - sendMessage("start"); + sendMessage(QStringLiteral("elevate=%1").arg(elevateStr)); + sendMessage(QStringLiteral("command=%1").arg(command)); + sendMessage(QStringLiteral("start")); } void DaemonIpcClient::sendStopProcess() { - sendMessage("stop"); + sendMessage(QStringLiteral("stop")); } void DaemonIpcClient::sendClearSettings() { - sendMessage("clearSettings"); + sendMessage(QStringLiteral("clearSettings")); } void DaemonIpcClient::requestLogPath() { - sendMessage("logPath"); + sendMessage(QStringLiteral("logPath")); } void DaemonIpcClient::processCommand(const QString &command, const QStringList &parts) { - if (command == "logPath" && parts.size() == 2) { + if (command == QStringLiteral("logPath") && parts.size() == 2) { Q_EMIT logPathReceived(parts[1]); } } diff --git a/src/lib/gui/ipc/IpcClient.cpp b/src/lib/gui/ipc/IpcClient.cpp index 37a7ee85665f..79a5d118759f 100644 --- a/src/lib/gui/ipc/IpcClient.cpp +++ b/src/lib/gui/ipc/IpcClient.cpp @@ -161,15 +161,15 @@ void IpcClient::handleReadyRead() void IpcClient::handleHandshakeMessage(const QStringList &parts) { - if (parts.at(0) == "error") { - const auto detail = parts.size() >= 2 ? parts.at(1) : QString("unknown"); - qCritical().noquote() << m_typeName << "ipc server rejected connection:" << detail; + if (parts.at(0) == QStringLiteral("error")) { + const auto detail = parts.size() >= 2 ? parts.at(1) : QStringLiteral("unknown"); + qCritical().noquote() << QStringLiteral("%1 ipc server rejected connection: %2").arg(m_typeName, detail); disconnectFromServer(); Q_EMIT connectionFailed(); return; } - if (parts.at(0) != "hello") { + if (parts.at(0) != QStringLiteral("hello")) { return; } From e293d4eb8c9a11869510651689d244130bacc1a1 Mon Sep 17 00:00:00 2001 From: sithlord48 Date: Wed, 8 Apr 2026 01:20:02 -0400 Subject: [PATCH 23/24] refactor(ipc): add missing use of at for lists --- src/lib/deskflow/ipc/DaemonIpcServer.cpp | 6 +++--- src/lib/gui/ipc/CoreIpcClient.cpp | 2 +- src/lib/gui/ipc/DaemonIpcClient.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/deskflow/ipc/DaemonIpcServer.cpp b/src/lib/deskflow/ipc/DaemonIpcServer.cpp index 374a9217bf17..cd15de9afbb8 100644 --- a/src/lib/deskflow/ipc/DaemonIpcServer.cpp +++ b/src/lib/deskflow/ipc/DaemonIpcServer.cpp @@ -59,7 +59,7 @@ void DaemonIpcServer::processLogLevel(QLocalSocket *&clientSocket, const QString return; } - const auto &logLevel = messageParts[1]; + const auto &logLevel = messageParts.at(1); if (logLevel.isEmpty()) { LOG_ERR("daemon ipc server got empty log level"); writeToClientSocket(clientSocket, kErrorMessage); @@ -79,7 +79,7 @@ void DaemonIpcServer::processElevate(QLocalSocket *&clientSocket, const QStringL return; } - const auto &elevate = messageParts[1]; + const auto &elevate = messageParts.at(1); if (elevate != QStringLiteral("yes") && elevate != QStringLiteral("no")) { LOG_ERR("daemon ipc server got invalid elevate value: %s", elevate.toUtf8().constData()); writeToClientSocket(clientSocket, kErrorMessage); @@ -99,7 +99,7 @@ void DaemonIpcServer::processCommandMessage(QLocalSocket *&clientSocket, const Q return; } - const auto &command = messageParts[1]; + const auto &command = messageParts.at(1); if (command.isEmpty()) { LOG_ERR("daemon ipc server got empty command"); writeToClientSocket(clientSocket, kErrorMessage); diff --git a/src/lib/gui/ipc/CoreIpcClient.cpp b/src/lib/gui/ipc/CoreIpcClient.cpp index c4633b2abb8c..5315a6854f35 100644 --- a/src/lib/gui/ipc/CoreIpcClient.cpp +++ b/src/lib/gui/ipc/CoreIpcClient.cpp @@ -22,7 +22,7 @@ CoreIpcClient::CoreIpcClient(QObject *parent) : IpcClient(parent, kCoreIpcName, void CoreIpcClient::processCommand(const QString &command, const QStringList &parts) { - const auto args = parts.size() >= 2 ? parts[1] : QString(); + const auto args = parts.size() >= 2 ? parts.at(1) : QString(); Q_EMIT commandReceived(command, args); } diff --git a/src/lib/gui/ipc/DaemonIpcClient.cpp b/src/lib/gui/ipc/DaemonIpcClient.cpp index 41a4bba66c33..82b3459934f4 100644 --- a/src/lib/gui/ipc/DaemonIpcClient.cpp +++ b/src/lib/gui/ipc/DaemonIpcClient.cpp @@ -47,7 +47,7 @@ void DaemonIpcClient::requestLogPath() void DaemonIpcClient::processCommand(const QString &command, const QStringList &parts) { if (command == QStringLiteral("logPath") && parts.size() == 2) { - Q_EMIT logPathReceived(parts[1]); + Q_EMIT logPathReceived(parts.at(1)); } } From 1922f916af0243ec5b2d528072da59338f52dc87 Mon Sep 17 00:00:00 2001 From: sithlord48 Date: Wed, 8 Apr 2026 01:42:19 -0400 Subject: [PATCH 24/24] refactor: Move -> deskflow/languages/LanguageManager -> deskflow/KeyboardLayoutManager rename methods to relfect its use for keyboard layouts not languages --- src/lib/client/ServerProxy.cpp | 30 +++---- src/lib/client/ServerProxy.h | 8 +- src/lib/deskflow/CMakeLists.txt | 4 +- src/lib/deskflow/KeyboardLayoutManager.cpp | 90 +++++++++++++++++++ src/lib/deskflow/KeyboardLayoutManager.h | 63 +++++++++++++ .../deskflow/languages/LanguageManager.cpp | 89 ------------------ src/lib/deskflow/languages/LanguageManager.h | 65 -------------- src/lib/server/ClientProxy1_8.cpp | 16 ++-- src/unittests/deskflow/CMakeLists.txt | 4 +- .../deskflow/KeyboardLayoutManagerTests.cpp | 63 +++++++++++++ ...erTests.h => KeyboardLayoutManagerTests.h} | 12 +-- .../deskflow/LanguageManagerTests.cpp | 63 ------------- 12 files changed, 253 insertions(+), 254 deletions(-) create mode 100644 src/lib/deskflow/KeyboardLayoutManager.cpp create mode 100644 src/lib/deskflow/KeyboardLayoutManager.h delete mode 100644 src/lib/deskflow/languages/LanguageManager.cpp delete mode 100644 src/lib/deskflow/languages/LanguageManager.h create mode 100644 src/unittests/deskflow/KeyboardLayoutManagerTests.cpp rename src/unittests/deskflow/{LanguageManagerTests.h => KeyboardLayoutManagerTests.h} (67%) delete mode 100644 src/unittests/deskflow/LanguageManagerTests.cpp diff --git a/src/lib/client/ServerProxy.cpp b/src/lib/client/ServerProxy.cpp index c23cd6a0d425..a04aa35e293b 100644 --- a/src/lib/client/ServerProxy.cpp +++ b/src/lib/client/ServerProxy.cpp @@ -141,7 +141,7 @@ ServerProxy::ConnectionResult ServerProxy::parseHandshakeMessage(const uint8_t * // handshake is complete m_parser = &ServerProxy::parseMessage; - if (const auto missedKeyboardLayouts = m_languageManager.getMissedLanguages(); !missedKeyboardLayouts.empty()) { + if (const auto missedKeyboardLayouts = m_layoutManager.getMissedLayouts(); !missedKeyboardLayouts.empty()) { LOG_WARN("server layouts missing on this computer: %s", missedKeyboardLayouts.c_str()); ipcSendToClient("missingKeyboardLayouts", QString::fromStdString(missedKeyboardLayouts)); } @@ -505,8 +505,8 @@ void ServerProxy::enter() m_dxMouse = 0; m_dyMouse = 0; m_seqNum = seqNum; - m_serverLanguage = ""; - m_isUserNotifiedAboutLanguageSyncError = false; + m_serverLayout = ""; + m_isUserNotifiedAboutLayoutSyncError = false; // forward m_client->enter(x, y, seqNum, static_cast(mask), false); @@ -825,28 +825,28 @@ void ServerProxy::secureInputNotification() void ServerProxy::setServerLanguages() { - std::string serverLanguages; - ProtocolUtil::readf(m_stream, kMsgDLanguageSynchronisation + 4, &serverLanguages); - m_languageManager.setRemoteLanguages(serverLanguages); + std::string serverLayout; + ProtocolUtil::readf(m_stream, kMsgDLanguageSynchronisation + 4, &serverLayout); + m_layoutManager.setRemoteLayouts(serverLayout); } void ServerProxy::setActiveServerLanguage(const std::string_view &language) { if (!language.empty() && (language.size() > 0)) { - if (m_serverLanguage != language) { - m_isUserNotifiedAboutLanguageSyncError = false; - m_serverLanguage = language; + if (m_serverLayout != language) { + m_isUserNotifiedAboutLayoutSyncError = false; + m_serverLayout = language; } - if (!m_languageManager.isLanguageInstalled(m_serverLanguage)) { - if (!m_isUserNotifiedAboutLanguageSyncError) { - LOG_WARN("current server language is not installed on client"); - m_isUserNotifiedAboutLanguageSyncError = true; + if (!m_layoutManager.isLayoutInstalled(m_serverLayout)) { + if (!m_isUserNotifiedAboutLayoutSyncError) { + LOG_WARN("current server layout is not installed on client"); + m_isUserNotifiedAboutLayoutSyncError = true; } } else { - m_isUserNotifiedAboutLanguageSyncError = false; + m_isUserNotifiedAboutLayoutSyncError = false; } } else { - LOG_DEBUG1("active server language is empty"); + LOG_DEBUG1("active server layout is empty"); } } diff --git a/src/lib/client/ServerProxy.h b/src/lib/client/ServerProxy.h index 59535664018f..149d7b972ea4 100644 --- a/src/lib/client/ServerProxy.h +++ b/src/lib/client/ServerProxy.h @@ -10,7 +10,7 @@ #include "deskflow/ClipboardTypes.h" #include "deskflow/KeyTypes.h" -#include "deskflow/languages/LanguageManager.h" +#include "deskflow/KeyboardLayoutManager.h" class Client; class ClientInfo; @@ -123,7 +123,7 @@ class ServerProxy MessageParser m_parser = &ServerProxy::parseHandshakeMessage; IEventQueue *m_events = nullptr; - std::string m_serverLanguage = ""; - bool m_isUserNotifiedAboutLanguageSyncError = false; - deskflow::languages::LanguageManager m_languageManager; + std::string m_serverLayout = ""; + bool m_isUserNotifiedAboutLayoutSyncError = false; + deskflow::KeyboardLayoutManager m_layoutManager; }; diff --git a/src/lib/deskflow/CMakeLists.txt b/src/lib/deskflow/CMakeLists.txt index 2d8ab2a0b6de..a0bc4c2367a1 100644 --- a/src/lib/deskflow/CMakeLists.txt +++ b/src/lib/deskflow/CMakeLists.txt @@ -77,8 +77,8 @@ add_library(${lib_name} STATIC ${PLATFORM_CODE} ServerApp.h StreamChunker.cpp StreamChunker.h - languages/LanguageManager.cpp - languages/LanguageManager.h + KeyboardLayoutManager.cpp + KeyboardLayoutManager.h ipc/IpcServer.cpp ipc/IpcServer.h ipc/DaemonIpcServer.cpp diff --git a/src/lib/deskflow/KeyboardLayoutManager.cpp b/src/lib/deskflow/KeyboardLayoutManager.cpp new file mode 100644 index 000000000000..b571b8fe39f8 --- /dev/null +++ b/src/lib/deskflow/KeyboardLayoutManager.cpp @@ -0,0 +1,90 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2014 - 2021 Symless Ltd. + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#include "KeyboardLayoutManager.h" +#include "base/Log.h" + +#include + +namespace { + +std::string vectorToString(const std::vector &vector, const std::string_view &delimiter = "") +{ + std::string string; + for (const auto &item : vector) { + if (&item != &vector[0]) { + string += delimiter; + } + string += item; + } + return string; +} + +} // anonymous namespace + +namespace deskflow { + +KeyboardLayoutManager::KeyboardLayoutManager(const std::vector &localLayouts) + : m_localLayouts(localLayouts) +{ + LOG_INFO("local layouts: %s", vectorToString(m_localLayouts, ", ").c_str()); +} + +void KeyboardLayoutManager::setRemoteLayouts(const std::string_view &remoteLayouts) +{ + m_remoteLayouts.clear(); + if (!remoteLayouts.empty()) { + for (size_t i = 0; i <= remoteLayouts.size() - 2; i += 2) { + auto rLangs = remoteLayouts.substr(i, 2); + m_remoteLayouts.emplace_back(rLangs); + } + } + LOG_INFO("remote layouts: %s", vectorToString(m_remoteLayouts, ", ").c_str()); +} + +const std::vector &KeyboardLayoutManager::getRemoteLayouts() const +{ + return m_remoteLayouts; +} + +const std::vector &KeyboardLayoutManager::getLocalLayouts() const +{ + return m_localLayouts; +} + +std::string KeyboardLayoutManager::getMissedLayouts() const +{ + std::string missedLayouts; + + for (const auto &layout : m_remoteLayouts) { + if (!isLayoutInstalled(layout)) { + if (!missedLayouts.empty()) { + missedLayouts += ", "; + } + missedLayouts += layout; + } + } + + return missedLayouts; +} + +std::string KeyboardLayoutManager::getSerializedLocalLayouts() const +{ + return vectorToString(m_localLayouts); +} + +bool KeyboardLayoutManager::isLayoutInstalled(const std::string &layout) const +{ + bool isInstalled = true; + + if (!m_localLayouts.empty()) { + isInstalled = (std::find(m_localLayouts.begin(), m_localLayouts.end(), layout) != m_localLayouts.end()); + } + + return isInstalled; +} + +} // namespace deskflow diff --git a/src/lib/deskflow/KeyboardLayoutManager.h b/src/lib/deskflow/KeyboardLayoutManager.h new file mode 100644 index 000000000000..2e4094e6089e --- /dev/null +++ b/src/lib/deskflow/KeyboardLayoutManager.h @@ -0,0 +1,63 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2014 - 2021 Symless Ltd. + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#pragma once + +#include "deskflow/AppUtil.h" +#include + +namespace deskflow { + +class KeyboardLayoutManager +{ + std::vector m_remoteLayouts; + std::vector m_localLayouts; + +public: + explicit KeyboardLayoutManager( + const std::vector &localLayouts = AppUtil::instance().getKeyboardLayoutList() + ); + + /** + * @brief setRemoteLayouts sets remote layouts + * @param remoteLayouts is a string with sericalized layouts + */ + void setRemoteLayouts(const std::string_view &remoteLayouts); + + /** + * @brief getRemoteLayouts getter for remote layouts + * @return vector of remote layouts + */ + const std::vector &getRemoteLayouts() const; + + /** + * @brief getLocalLayouts getter for local layouts + * @return vector of local layouts + */ + const std::vector &getLocalLayouts() const; + + /** + * @brief getMissedLayouts getter for missed layouts on local machine + * @return difference between remote and local layouts as a coma separated + * string + */ + std::string getMissedLayouts() const; + + /** + * @brief getSerializedLocalLayouts getter for local serialized layouts + * @return serialized local layouts as a string + */ + std::string getSerializedLocalLayouts() const; + + /** + * @brief isLayoutInstalled checks if layout is installed + * @param layout which should be checked + * @return true if the specified layout is installed + */ + bool isLayoutInstalled(const std::string &layout) const; +}; + +} // namespace deskflow diff --git a/src/lib/deskflow/languages/LanguageManager.cpp b/src/lib/deskflow/languages/LanguageManager.cpp deleted file mode 100644 index 6df5339f7c67..000000000000 --- a/src/lib/deskflow/languages/LanguageManager.cpp +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2014 - 2021 Symless Ltd. - * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception - */ - -#include "LanguageManager.h" -#include "base/Log.h" - -#include - -namespace { - -std::string vectorToString(const std::vector &vector, const std::string_view &delimiter = "") -{ - std::string string; - for (const auto &item : vector) { - if (&item != &vector[0]) { - string += delimiter; - } - string += item; - } - return string; -} - -} // anonymous namespace - -namespace deskflow::languages { - -LanguageManager::LanguageManager(const std::vector &localLanguages) : m_localLanguages(localLanguages) -{ - LOG_INFO("local languages: %s", vectorToString(m_localLanguages, ", ").c_str()); -} - -void LanguageManager::setRemoteLanguages(const std::string_view &remoteLanguages) -{ - m_remoteLanguages.clear(); - if (!remoteLanguages.empty()) { - for (size_t i = 0; i <= remoteLanguages.size() - 2; i += 2) { - auto rLangs = remoteLanguages.substr(i, 2); - m_remoteLanguages.emplace_back(rLangs); - } - } - LOG_INFO("remote languages: %s", vectorToString(m_remoteLanguages, ", ").c_str()); -} - -const std::vector &LanguageManager::getRemoteLanguages() const -{ - return m_remoteLanguages; -} - -const std::vector &LanguageManager::getLocalLanguages() const -{ - return m_localLanguages; -} - -std::string LanguageManager::getMissedLanguages() const -{ - std::string missedLanguages; - - for (const auto &language : m_remoteLanguages) { - if (!isLanguageInstalled(language)) { - if (!missedLanguages.empty()) { - missedLanguages += ", "; - } - missedLanguages += language; - } - } - - return missedLanguages; -} - -std::string LanguageManager::getSerializedLocalLanguages() const -{ - return vectorToString(m_localLanguages); -} - -bool LanguageManager::isLanguageInstalled(const std::string &language) const -{ - bool isInstalled = true; - - if (!m_localLanguages.empty()) { - isInstalled = (std::find(m_localLanguages.begin(), m_localLanguages.end(), language) != m_localLanguages.end()); - } - - return isInstalled; -} - -} // namespace deskflow::languages diff --git a/src/lib/deskflow/languages/LanguageManager.h b/src/lib/deskflow/languages/LanguageManager.h deleted file mode 100644 index 4aef2db60157..000000000000 --- a/src/lib/deskflow/languages/LanguageManager.h +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2014 - 2021 Symless Ltd. - * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception - */ - -#pragma once - -#include "deskflow/AppUtil.h" -#include - -namespace deskflow::languages { - -// TODO: rename class and namespace; "languages" is a misnomer. These are keyboard -// _layouts_ (Windows/X11) or input sources (macOS), not spoken "languages". -class LanguageManager -{ - std::vector m_remoteLanguages; - std::vector m_localLanguages; - -public: - explicit LanguageManager( - const std::vector &localLanguages = AppUtil::instance().getKeyboardLayoutList() - ); - - /** - * @brief setRemoteLanguages sets remote languages - * @param remoteLanguages is a string with sericalized languages - */ - void setRemoteLanguages(const std::string_view &remoteLanguages); - - /** - * @brief getRemoteLanguages getter for remote languages - * @return vector of remote languages - */ - const std::vector &getRemoteLanguages() const; - - /** - * @brief getLocalLanguages getter for local languages - * @return vector of local languages - */ - const std::vector &getLocalLanguages() const; - - /** - * @brief getMissedLanguages getter for missed languages on local machine - * @return difference between remote and local languages as a coma separated - * string - */ - std::string getMissedLanguages() const; - - /** - * @brief getSerializedLocalLanguages getter for local serialized languages - * @return serialized local languages as a string - */ - std::string getSerializedLocalLanguages() const; - - /** - * @brief isLanguageInstalled checks if language is installed - * @param language which should be checked - * @return true if the specified language is installed - */ - bool isLanguageInstalled(const std::string &language) const; -}; - -} // namespace deskflow::languages diff --git a/src/lib/server/ClientProxy1_8.cpp b/src/lib/server/ClientProxy1_8.cpp index 7a86073c2c2d..fcdb633520c4 100644 --- a/src/lib/server/ClientProxy1_8.cpp +++ b/src/lib/server/ClientProxy1_8.cpp @@ -5,8 +5,8 @@ */ #include "base/Log.h" +#include "deskflow/KeyboardLayoutManager.h" #include "deskflow/ProtocolUtil.h" -#include "deskflow/languages/LanguageManager.h" #include "ClientProxy1_8.h" @@ -20,11 +20,11 @@ ClientProxy1_8::ClientProxy1_8( void ClientProxy1_8::synchronizeLanguages() const { - deskflow::languages::LanguageManager languageManager; - auto localLanguages = languageManager.getSerializedLocalLanguages(); - if (!localLanguages.empty()) { - LOG_DEBUG1("send server languages to the client: %s", localLanguages.c_str()); - ProtocolUtil::writef(getStream(), kMsgDLanguageSynchronisation, &localLanguages); + deskflow::KeyboardLayoutManager layoutManager; + auto localLayouts = layoutManager.getSerializedLocalLayouts(); + if (!localLayouts.empty()) { + LOG_DEBUG1("send server languages to the client: %s", localLayouts.c_str()); + ProtocolUtil::writef(getStream(), kMsgDLanguageSynchronisation, &localLayouts); } else { LOG_ERR("failed to read server languages"); } @@ -33,8 +33,8 @@ void ClientProxy1_8::synchronizeLanguages() const void ClientProxy1_8::keyDown(KeyID key, KeyModifierMask mask, KeyButton button, const std::string &language) { LOG( - (CLOG_DEBUG1 "send key down to \"%s\" id=%d, mask=0x%04x, button=0x%04x, language=%s", getName().c_str(), key, - mask, button, language.c_str()) + (CLOG_DEBUG1 "send key down to \"%s\" id=%d, mask=0x%04x, button=0x%04x, layout=%s", getName().c_str(), key, mask, + button, language.c_str()) ); ProtocolUtil::writef(getStream(), kMsgDKeyDownLang, key, mask, button, &language); } diff --git a/src/unittests/deskflow/CMakeLists.txt b/src/unittests/deskflow/CMakeLists.txt index f4beb8abe3d6..937a3573f24f 100644 --- a/src/unittests/deskflow/CMakeLists.txt +++ b/src/unittests/deskflow/CMakeLists.txt @@ -46,10 +46,10 @@ create_test( ) create_test( - NAME LanguageManagerTests + NAME KeyboardLayoutManagerTests DEPENDS app LIBS arch base ${extra_libs} - SOURCE LanguageManagerTests.cpp + SOURCE KeyboardLayoutManagerTests.cpp WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/src/lib/deskflow" ) diff --git a/src/unittests/deskflow/KeyboardLayoutManagerTests.cpp b/src/unittests/deskflow/KeyboardLayoutManagerTests.cpp new file mode 100644 index 000000000000..0d602a8d0e42 --- /dev/null +++ b/src/unittests/deskflow/KeyboardLayoutManagerTests.cpp @@ -0,0 +1,63 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2025 Chris Rizzitello + * SPDX-FileCopyrightText: (C) 2014 - 2024 Symless Ltd. + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#include "KeyboardLayoutManagerTests.h" + +#include "deskflow/KeyboardLayoutManager.h" + +void KeyboardLayoutManagerTests::initTestCase() +{ + m_log.setFilter(LogLevel::Debug2); +} + +void KeyboardLayoutManagerTests::remoteLayouts() +{ + std::string remoteLayouts = "ruenuk"; + deskflow::KeyboardLayoutManager manager({"ru", "en", "uk"}); + + manager.setRemoteLayouts(remoteLayouts); + QCOMPARE(manager.getRemoteLayouts(), (std::vector{"ru", "en", "uk"})); + + manager.setRemoteLayouts(std::string()); + QVERIFY(manager.getRemoteLayouts().empty()); +} + +void KeyboardLayoutManagerTests::localLayout() +{ + std::vector localLayouts = {"ru", "en", "uk"}; + deskflow::KeyboardLayoutManager manager(localLayouts); + QCOMPARE(manager.getLocalLayouts(), (std::vector{"ru", "en", "uk"})); +} + +void KeyboardLayoutManagerTests::missedLayout() +{ + std::string remoteLayouts = "ruenuk"; + std::vector localLayouts = {"en"}; + deskflow::KeyboardLayoutManager manager(localLayouts); + + manager.setRemoteLayouts(remoteLayouts); + QCOMPARE(manager.getMissedLayouts(), "ru, uk"); +} + +void KeyboardLayoutManagerTests::layoutInstall() +{ + std::vector localLayouts = {"ru", "en", "uk"}; + deskflow::KeyboardLayoutManager manager(localLayouts); + + QVERIFY(!manager.isLayoutInstalled("us")); + QVERIFY(manager.isLayoutInstalled("en")); +} + +void KeyboardLayoutManagerTests::serializeLocalLayouts() +{ + std::vector localLayouts = {"ru", "en", "uk"}; + deskflow::KeyboardLayoutManager manager(localLayouts); + + QCOMPARE(manager.getSerializedLocalLayouts(), "ruenuk"); +} + +QTEST_MAIN(KeyboardLayoutManagerTests) diff --git a/src/unittests/deskflow/LanguageManagerTests.h b/src/unittests/deskflow/KeyboardLayoutManagerTests.h similarity index 67% rename from src/unittests/deskflow/LanguageManagerTests.h rename to src/unittests/deskflow/KeyboardLayoutManagerTests.h index 9816c8b6a3dd..14ba6ffd7055 100644 --- a/src/unittests/deskflow/LanguageManagerTests.h +++ b/src/unittests/deskflow/KeyboardLayoutManagerTests.h @@ -8,17 +8,17 @@ #include -class LanguageManagerTests : public QObject +class KeyboardLayoutManagerTests : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); // Test are run in order top to bottom - void remoteLanguages(); - void localLanguage(); - void missedLanguage(); - void serializeLocalLanguages(); - void languageInstall(); + void remoteLayouts(); + void localLayout(); + void missedLayout(); + void serializeLocalLayouts(); + void layoutInstall(); private: Log m_log; diff --git a/src/unittests/deskflow/LanguageManagerTests.cpp b/src/unittests/deskflow/LanguageManagerTests.cpp deleted file mode 100644 index 4f1464f1ede0..000000000000 --- a/src/unittests/deskflow/LanguageManagerTests.cpp +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2025 Chris Rizzitello - * SPDX-FileCopyrightText: (C) 2014 - 2024 Symless Ltd. - * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception - */ - -#include "LanguageManagerTests.h" - -#include "deskflow/languages/LanguageManager.h" - -void LanguageManagerTests::initTestCase() -{ - m_log.setFilter(LogLevel::Debug2); -} - -void LanguageManagerTests::remoteLanguages() -{ - std::string remoteLanguages = "ruenuk"; - deskflow::languages::LanguageManager manager({"ru", "en", "uk"}); - - manager.setRemoteLanguages(remoteLanguages); - QCOMPARE(manager.getRemoteLanguages(), (std::vector{"ru", "en", "uk"})); - - manager.setRemoteLanguages(std::string()); - QVERIFY(manager.getRemoteLanguages().empty()); -} - -void LanguageManagerTests::localLanguage() -{ - std::vector localLanguages = {"ru", "en", "uk"}; - deskflow::languages::LanguageManager manager(localLanguages); - QCOMPARE(manager.getLocalLanguages(), (std::vector{"ru", "en", "uk"})); -} - -void LanguageManagerTests::missedLanguage() -{ - std::string remoteLanguages = "ruenuk"; - std::vector localLanguages = {"en"}; - deskflow::languages::LanguageManager manager(localLanguages); - - manager.setRemoteLanguages(remoteLanguages); - QCOMPARE(manager.getMissedLanguages(), "ru, uk"); -} - -void LanguageManagerTests::languageInstall() -{ - std::vector localLanguages = {"ru", "en", "uk"}; - deskflow::languages::LanguageManager manager(localLanguages); - - QVERIFY(!manager.isLanguageInstalled("us")); - QVERIFY(manager.isLanguageInstalled("en")); -} - -void LanguageManagerTests::serializeLocalLanguages() -{ - std::vector localLanguages = {"ru", "en", "uk"}; - deskflow::languages::LanguageManager manager(localLanguages); - - QCOMPARE(manager.getSerializedLocalLanguages(), "ruenuk"); -} - -QTEST_MAIN(LanguageManagerTests)