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"
diff --git a/src/apps/deskflow-core/deskflow-core.cpp b/src/apps/deskflow-core/deskflow-core.cpp
index 052b380e8a84..143b41ccb17c 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
*/
@@ -15,27 +15,41 @@
#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"
#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)
- // 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
@@ -86,13 +100,21 @@ 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));
+
+ 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);
+
+ const auto exitCode = QCoreApplication::exec();
+ coreThread.wait();
- return s_exitSuccess;
+ LOG_DEBUG("core exited, code: %d", exitCode);
+ return exitCode;
}
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);
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);
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..5952013f7cc0 100644
--- a/src/lib/client/Client.cpp
+++ b/src/lib/client/Client.cpp
@@ -21,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
@@ -92,10 +95,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
@@ -131,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..a04aa35e293b 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
@@ -124,6 +125,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();
@@ -138,7 +140,12 @@ ServerProxy::ConnectionResult ServerProxy::parseHandshakeMessage(const uint8_t *
// handshake is complete
m_parser = &ServerProxy::parseMessage;
- checkMissedLanguages();
+
+ 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));
+ }
+
m_client->handshakeComplete();
}
@@ -168,25 +175,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();
@@ -498,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);
@@ -818,40 +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");
- }
-}
-
-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())
- );
+ LOG_DEBUG1("active server layout is empty");
}
}
diff --git a/src/lib/client/ServerProxy.h b/src/lib/client/ServerProxy.h
index 2c638b8ca523..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;
@@ -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 *);
@@ -124,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/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/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/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/App.cpp b/src/lib/deskflow/App.cpp
index 4548960672cd..36a00b6f74d8 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
*/
@@ -23,7 +23,6 @@
#include
#if defined(Q_OS_MAC)
-#include "platform/OSXCocoaApp.h"
#include
#endif
@@ -57,48 +56,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()
@@ -151,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/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;
diff --git a/src/lib/deskflow/CMakeLists.txt b/src/lib/deskflow/CMakeLists.txt
index 46da197eb980..a0bc4c2367a1 100644
--- a/src/lib/deskflow/CMakeLists.txt
+++ b/src/lib/deskflow/CMakeLists.txt
@@ -77,10 +77,16 @@ 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
ipc/DaemonIpcServer.h
+ ipc/CoreIpc.cpp
+ ipc/CoreIpc.h
+ ipc/CoreIpcServer.cpp
+ ipc/CoreIpcServer.h
)
target_link_libraries(${lib_name} PUBLIC common Qt6::Core Qt6::Network)
diff --git a/src/lib/deskflow/ClientApp.cpp b/src/lib/deskflow/ClientApp.cpp
index 7dcbf131b68c..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"
@@ -37,9 +38,6 @@
#endif
#if defined(Q_OS_MAC)
-#include "base/TMethodJob.h"
-#include "mt/Thread.h"
-#include "platform/OSXCocoaApp.h"
#include "platform/OSXScreen.h"
#endif
@@ -162,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); });
@@ -173,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;
@@ -226,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());
}
@@ -330,17 +328,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/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/ServerApp.cpp b/src/lib/deskflow/ServerApp.cpp
index 15c27a480019..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"
@@ -43,9 +44,6 @@
#endif
#if defined(Q_OS_MAC)
-#include "base/TMethodJob.h"
-#include "mt/Thread.h"
-#include "platform/OSXCocoaApp.h"
#include "platform/OSXScreen.h"
#endif
@@ -373,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) {
@@ -541,18 +540,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/deskflow/ipc/CoreIpc.cpp b/src/lib/deskflow/ipc/CoreIpc.cpp
new file mode 100644
index 000000000000..b48f1554da94
--- /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(QStringLiteral("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
new file mode 100644
index 000000000000..f0ad0a39ddd5
--- /dev/null
+++ b/src/lib/deskflow/ipc/CoreIpcServer.cpp
@@ -0,0 +1,37 @@
+/*
+ * 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 {
+
+static CoreIpcServer *s_instance = nullptr;
+
+CoreIpcServer::CoreIpcServer(QObject *parent) : IpcServer(parent, kCoreIpcName, QStringLiteral("core"))
+{
+ 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)
+{
+ 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..bfdc822f00b6
--- /dev/null
+++ b/src/lib/deskflow/ipc/CoreIpcServer.h
@@ -0,0 +1,31 @@
+/*
+ * 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);
+
+ static CoreIpcServer &instance();
+
+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 c719dd29149f..cd15de9afbb8 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,152 +17,56 @@ 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, QStringLiteral("daemon")),
+ m_logFilename(logFilename)
{
// do nothing
}
-DaemonIpcServer::~DaemonIpcServer()
+void DaemonIpcServer::processCommand(QLocalSocket *clientSocket, const QString &command, const QStringList &parts)
{
- 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());
- const auto parts = message.split('=');
- if (parts.size() < 1) {
- LOG_ERR("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");
- writeToClientSocket(clientSocket, "hello");
- } else if (command == "noop") {
- LOG_DEBUG("ipc server got noop message");
- writeToClientSocket(clientSocket, kAckMessage);
- } else 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") {
- processCommand(clientSocket, parts);
- } else if (command == "start") {
- LOG_DEBUG("ipc server got start message");
+ } else if (command == QStringLiteral("command")) {
+ processCommandMessage(clientSocket, parts);
+ } else if (command == QStringLiteral("start")) {
+ 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");
+ } else if (command == QStringLiteral("stop")) {
+ 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");
- writeToClientSocket(clientSocket, "logPath=" + m_logFilename.toUtf8());
- } else if (command == "clearSettings") {
- LOG_DEBUG("ipc server got clear settings message");
+ } else if (command == QStringLiteral("logPath")) {
+ LOG_DEBUG("daemon ipc server got log path request");
+ 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);
} else {
- LOG_WARN("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)
{
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];
+ const auto &logLevel = messageParts.at(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,52 +74,41 @@ 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());
+ 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);
return;
}
- LOG_DEBUG("ipc server got new elevate value: %s", elevate.toUtf8().constData());
- Q_EMIT elevateModeChanged(elevate == "yes");
+ LOG_DEBUG("daemon ipc server got new elevate value: %s", elevate.toUtf8().constData());
+ Q_EMIT elevateModeChanged(elevate == QStringLiteral("yes"));
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("ipc server got invalid command message");
+ LOG_ERR("daemon ipc server got invalid command message");
writeToClientSocket(clientSocket, kErrorMessage);
return;
}
- const auto &command = messageParts[1];
+ const auto &command = messageParts.at(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..a4d205b5fd7f 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 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);
-
- /**!
- * 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();
+ void processCommandMessage(QLocalSocket *&clientSocket, const QStringList &messageParts);
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..616a3f3884b0
--- /dev/null
+++ b/src/lib/deskflow/ipc/IpcServer.cpp
@@ -0,0 +1,193 @@
+/*
+ * 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 "common/VersionInfo.h"
+
+#include
+#include
+
+namespace deskflow::core::ipc {
+
+IpcServer::IpcServer(QObject *parent, const QString &serverName, const QString &typeName)
+ : QObject(parent),
+ m_server{new QLocalServer(this)}, // NOSONAR - Qt memory
+ m_serverName(serverName),
+ m_typeName(typeName.toUtf8())
+{
+ // 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("%s ipc server listening on: %s", m_typeName.constData(), m_serverName.toUtf8().constData());
+ } else {
+ LOG_ERR("%s ipc server failed to listen on: %s", m_typeName.constData(), m_serverName.toUtf8().constData());
+ }
+}
+
+void IpcServer::handleNewConnection()
+{
+ QLocalSocket *clientSocket = m_server->nextPendingConnection();
+ if (!clientSocket) {
+ LOG_ERR("%s ipc server failed to get new connection", m_typeName.constData());
+ return;
+ }
+
+ LOG_DEBUG("%s ipc server got new connection", m_typeName.constData());
+ 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("%s ipc server ready to read data", m_typeName.constData());
+
+ QByteArray data = clientSocket->readAll();
+ if (data.isEmpty()) {
+ 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("%s ipc server got incomplete message: %s", m_typeName.constData(), 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("%s ipc server client disconnected", m_typeName.constData());
+ m_clients.remove(clientSocket);
+ clientSocket->deleteLater();
+}
+
+void IpcServer::handleErrorOccurred()
+{
+ const auto clientSocket = qobject_cast(sender());
+ 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("%s ipc server got message: %s", m_typeName.constData(), message.toUtf8().constData());
+ 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, QStringLiteral("error"));
+ return;
+ }
+
+ 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");
+ clientSocket->flush();
+ clientSocket->disconnectFromServer();
+ return;
+ }
+
+ const auto versionId = QStringLiteral("%1+%2").arg(kVersion, kVersionGitSha);
+ 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 (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();
+ return;
+ }
+
+ LOG_DEBUG("%s ipc server sending hello back", m_typeName.constData());
+ 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());
+ for (const auto &pending : std::as_const(m_pendingMessages)) {
+ LOG_DEBUG1("%s ipc server replaying: %s", m_typeName.constData(), pending.toUtf8().constData());
+ writeToClientSocket(clientSocket, pending);
+ }
+ m_pendingMessages.clear();
+ } else if (command == QStringLiteral("noop")) {
+ LOG_DEBUG("%s ipc server got noop message", m_typeName.constData());
+ writeToClientSocket(clientSocket, QStringLiteral("ok"));
+ } else {
+ processCommand(clientSocket, command, parts);
+ }
+
+ clientSocket->flush();
+}
+
+void IpcServer::broadcastCommand(const QString &command, const QString &args)
+{
+ const auto message = args.isEmpty() ? command : QStringLiteral("%1=%2").arg(command, args);
+
+ if (m_clients.isEmpty()) {
+ LOG_DEBUG1(
+ "%s ipc server has no clients, message queued: %s", m_typeName.constData(), message.toUtf8().constData()
+ );
+ m_pendingMessages.append(message);
+ return;
+ }
+
+ 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();
+ }
+}
+
+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("%s ipc server failed to write full message to client socket", m_typeName.constData());
+ } else {
+ LOG_DEBUG1(
+ "%s ipc server wrote message to client socket: %s", m_typeName.constData(), 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..e26000a699fe
--- /dev/null
+++ b/src/lib/deskflow/ipc/IpcServer.h
@@ -0,0 +1,60 @@
+/*
+ * 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, const QString &typeName);
+ ~IpcServer() override;
+
+ void listen();
+ void broadcastCommand(const QString &command, const QString &args = "");
+
+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:
+ 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();
+ void handleErrorOccurred();
+
+ QLocalServer *m_server;
+ QSet m_clients;
+ QString m_serverName;
+ QStringList m_pendingMessages;
+ QByteArray m_typeName;
+};
+
+} // namespace deskflow::core::ipc
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 3cd1f19fa73b..000000000000
--- a/src/lib/deskflow/languages/LanguageManager.h
+++ /dev/null
@@ -1,63 +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 {
-
-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/gui/CMakeLists.txt b/src/lib/gui/CMakeLists.txt
index 0df69a0d4c9e..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
@@ -81,8 +74,12 @@ 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
+ ipc/CoreIpcClient.cpp
+ ipc/CoreIpcClient.h
validators/AliasValidator.cpp
validators/AliasValidator.h
validators/ComputerNameValidator.cpp
diff --git a/src/lib/gui/MainWindow.cpp b/src/lib/gui/MainWindow.cpp
index 9e36eca6bf17..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
@@ -58,8 +59,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,14 +268,12 @@ 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_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::connectedClientsChanged, this, &MainWindow::serverClientsChanged);
+ 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);
+ 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);
@@ -429,8 +426,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);
@@ -597,12 +592,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;
}
//////////////////////////////////////////////////////////////////////////////
@@ -754,35 +749,87 @@ 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;
+
+ if (!isVisible() || m_clientErrorVisible)
+ return;
+
+ 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)
+ );
- const qsizetype midStart = line.indexOf(tlsPeerMessage);
- if (midStart == -1)
+ m_clientErrorVisible = false;
+}
+
+void MainWindow::handleMissingKeyboardLayouts(const QString &layouts)
+{
+ if (Settings::value(Settings::Gui::IgnoreMissingKeyboardLayouts).toBool())
return;
- const auto sha256Text = line.mid(midStart + msgLen).remove(':');
+ 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(':');
const Fingerprint sha256 = {QCryptographicHash::Sha256, QByteArray::fromHex(sha256Text.toLatin1())};
const bool isClient = m_coreProcess.mode() == CoreMode::Client;
@@ -858,13 +905,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()
@@ -922,7 +963,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();
@@ -964,7 +1005,7 @@ void MainWindow::changeEvent(QEvent *e)
updateModeControlLabels();
updateNetworkInfo();
updateStatus();
- serverClientsChanged(m_serverConnection.connectedClients());
+ serverClientsChanged({});
updateText();
}
}
@@ -1172,34 +1213,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, bool usePeerAuth)
-{
- showAndActivate();
- bool result = deskflow::gui::messages::showNewClientPrompt(this, clientName, usePeerAuth);
- 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 ec35a91d0dda..1ec5d97f6767 100644
--- a/src/lib/gui/MainWindow.h
+++ b/src/lib/gui/MainWindow.h
@@ -9,21 +9,15 @@
#pragma once
-#include
#include
-#include
#include
#include
-#include
#include
-#include
#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
@@ -32,17 +26,6 @@
class QAction;
class QMenu;
-class QLabel;
-class QLineEdit;
-class QGroupBox;
-class QPushButton;
-class QTextEdit;
-class QComboBox;
-class QTabWidget;
-class QCheckBox;
-class QRadioButton;
-class QMessageBox;
-class QAbstractButton;
class QLocalServer;
class DeskflowApplication;
@@ -67,9 +50,6 @@ class MainWindow : public QMainWindow
Q_OBJECT
- friend class DeskflowApplication;
- friend class SettingsDialog;
-
public:
enum class LogLevel
{
@@ -136,9 +116,10 @@ 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 handleMissingKeyboardLayouts(const QString &layouts);
void closeEvent(QCloseEvent *event) override;
void secureSocket(bool secureSocket);
void connectSlots();
@@ -158,17 +139,10 @@ class MainWindow : public QMainWindow
void daemonIpcClientConnectionFailed();
void toggleCanRunCore(bool enableButtons);
void remoteHostChanged(const QString &newRemoteHost);
- void handleNewClientPromptRequest(const QString &clientName, bool usePeerAuth);
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.
@@ -195,8 +169,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/Messages.cpp b/src/lib/gui/Messages.cpp
index f9c8df874fe2..e9ca50cde9e7 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 "
@@ -182,9 +184,10 @@ void showFirstConnectedMessage(QWidget *parent, bool closeToTray, bool enableSer
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 5e36aec34dbf..858887e0e016 100644
--- a/src/lib/gui/Messages.h
+++ b/src/lib/gui/Messages.h
@@ -21,11 +21,11 @@ void raiseCriticalDialog();
void showFirstServerStartMessage(QWidget *parent);
-void showFirstConnectedMessage(QWidget *parent, bool closeToTray, bool enableService, bool isServer);
+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/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/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.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/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 1d2d2ae6ce68..000000000000
--- a/src/lib/gui/core/ClientConnection.cpp
+++ /dev/null
@@ -1,76 +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
-#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 1e7a7a2ad222..ef634dfef331 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)
@@ -22,6 +23,7 @@
#include
#include
#include
+#include
#include
#include
@@ -97,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)}
{
@@ -111,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) {
@@ -138,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)
@@ -189,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());
}
}
@@ -233,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
@@ -270,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)
@@ -292,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);
}
}
@@ -365,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) {
@@ -384,6 +428,12 @@ void CoreProcess::stop(std::optional processModeOption)
qInfo("stopping core process (%s mode)", qPrintable(processModeToString(processMode)));
+ if (m_coreIpcClient) {
+ m_coreIpcClient->disconnectFromServer();
+ m_coreIpcClient->deleteLater();
+ m_coreIpcClient = nullptr;
+ }
+
if (m_processState == ProcessState::Starting) {
qDebug("core process is starting, cancelling");
setProcessState(ProcessState::Stopped);
@@ -478,35 +528,45 @@ 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);
+ }
+ } 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);
+ } else if (command == "missingKeyboardLayouts") {
+ Q_EMIT missingKeyboardLayouts(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)
@@ -526,46 +586,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()
@@ -586,9 +630,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 71632010f52c..71cebca01360 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
@@ -20,16 +20,15 @@
namespace deskflow::gui {
namespace ipc {
+class CoreIpcClient;
class DaemonIpcClient;
-}
+} // namespace ipc
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:
@@ -39,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);
@@ -88,12 +87,19 @@ 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);
+ void unrecognisedClient(const QString &clientName);
+ 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);
void onProcessReadyReadStandardOutput();
void onProcessReadyReadStandardError();
+ void onCoreIpcMessageReceived(const QString &command, const QString &args);
void daemonIpcClientConnected();
private:
@@ -104,21 +110,16 @@ 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;
+ const ServerConfig &m_serverConfig;
QString m_address;
ProcessState m_processState = ProcessState::Stopped;
ConnectionState m_connectionState = ConnectionState::Disconnected;
@@ -127,7 +128,7 @@ 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;
QProcess *m_process = nullptr;
diff --git a/src/lib/gui/core/ServerConnection.cpp b/src/lib/gui/core/ServerConnection.cpp
deleted file mode 100644
index d1ff8075e301..000000000000
--- a/src/lib/gui/core/ServerConnection.cpp
+++ /dev/null
@@ -1,111 +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
-#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;
- const bool tlsEnabled = Settings::value(Settings::Security::TlsEnabled).toBool();
- const bool requireCerts = Settings::value(Settings::Security::CheckPeers).toBool();
- Q_EMIT requestNewClientPrompt(clientName, tlsEnabled && requireCerts);
-}
-
-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 8fa698649e25..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, bool peerAuthRequired);
- 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/CoreIpcClient.cpp b/src/lib/gui/ipc/CoreIpcClient.cpp
new file mode 100644
index 000000000000..5315a6854f35
--- /dev/null
+++ b/src/lib/gui/ipc/CoreIpcClient.cpp
@@ -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
+ */
+
+#include "CoreIpcClient.h"
+
+#include "common/Constants.h"
+
+#include
+#include
+#include
+#include
+
+namespace deskflow::gui::ipc {
+
+CoreIpcClient::CoreIpcClient(QObject *parent) : IpcClient(parent, kCoreIpcName, QStringLiteral("core"))
+{
+ // do nothing
+}
+
+void CoreIpcClient::processCommand(const QString &command, const QStringList &parts)
+{
+ const auto args = parts.size() >= 2 ? parts.at(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
new file mode 100644
index 000000000000..bccb1ccd4e9f
--- /dev/null
+++ b/src/lib/gui/ipc/CoreIpcClient.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 "IpcClient.h"
+
+#include
+
+namespace deskflow::gui::ipc {
+
+class CoreIpcClient : public IpcClient
+{
+ Q_OBJECT
+
+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 2b00c68bdb9d..82b3459934f4 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
*/
@@ -9,248 +9,46 @@
#include "common/Constants.h"
#include
-#include
-#include
-#include
-#include
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, QStringLiteral("daemon"))
{
- connect(m_socket, &QLocalSocket::disconnected, this, &DaemonIpcClient::handleDisconnected);
- connect(m_socket, &QLocalSocket::errorOccurred, this, &DaemonIpcClient::handleErrorOccurred);
}
-bool DaemonIpcClient::connectToServer()
+void DaemonIpcClient::sendLogLevel(const QString &logLevel)
{
- 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;
+ sendMessage(QStringLiteral("logLevel=%1").arg(logLevel));
}
-void DaemonIpcClient::disconnectFromServer()
+void DaemonIpcClient::sendStartProcess(const QString &command, bool elevate)
{
- 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;
+ const auto elevateStr = elevate ? QStringLiteral("yes") : QStringLiteral("no");
+ sendMessage(QStringLiteral("elevate=%1").arg(elevateStr));
+ sendMessage(QStringLiteral("command=%1").arg(command));
+ sendMessage(QStringLiteral("start"));
}
-void DaemonIpcClient::handleDisconnected()
+void DaemonIpcClient::sendStopProcess()
{
- qDebug() << "daemon ipc client disconnected from server";
- if (m_state == State::Connected) {
- Q_EMIT connectionFailed();
- }
-
- m_state = State::Unconnected;
+ sendMessage(QStringLiteral("stop"));
}
-void DaemonIpcClient::handleErrorOccurred()
+void DaemonIpcClient::sendClearSettings()
{
- qWarning() << "daemon ipc client error:" << m_socket->errorString();
- disconnectFromServer();
-
- if (m_state == State::Connected) {
- Q_EMIT connectionFailed();
- }
+ sendMessage(QStringLiteral("clearSettings"));
}
-bool DaemonIpcClient::sendMessage(const QString &message, const QString &expectAck, const bool expectConnected)
+void DaemonIpcClient::requestLogPath()
{
- 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;
+ sendMessage(QStringLiteral("logPath"));
}
-bool DaemonIpcClient::keepAlive()
+void DaemonIpcClient::processCommand(const QString &command, const QStringList &parts)
{
- if (!isConnected() && !connectToServer()) {
- qWarning() << "daemon ipc client keep alive failed to connect";
- return false;
+ if (command == QStringLiteral("logPath") && parts.size() == 2) {
+ Q_EMIT logPathReceived(parts.at(1));
}
-
- if (!sendMessage("noop")) {
- qWarning() << "daemon ipc client keep alive ping failed, reconnecting";
- connectToServer();
- return false;
- }
-
- return true;
-}
-
-bool DaemonIpcClient::sendLogLevel(const QString &logLevel)
-{
- if (!keepAlive())
- return false;
-
- sendMessage("logLevel=" + logLevel);
- return true;
-}
-
-bool 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");
-}
-
-bool DaemonIpcClient::sendStopProcess()
-{
- return sendMessage("stop");
-}
-
-QString DaemonIpcClient::requestLogPath()
-{
- if (!keepAlive())
- return QString();
-
- if (!sendMessage("logPath", QString())) {
- return QString();
- }
-
- if (!m_socket->waitForReadyRead(kTimeout)) {
- qWarning() << "daemon ipc client failed to read log path response";
- return QString();
- }
-
- QByteArray response = m_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();
- }
-
- return parts[1];
-}
-
-bool DaemonIpcClient::sendClearSettings()
-{
- return sendMessage("clearSettings");
}
} // namespace deskflow::gui::ipc
diff --git a/src/lib/gui/ipc/DaemonIpcClient.h b/src/lib/gui/ipc/DaemonIpcClient.h
index d5cd5ea0e530..9b72bfd487e2 100644
--- a/src/lib/gui/ipc/DaemonIpcClient.h
+++ b/src/lib/gui/ipc/DaemonIpcClient.h
@@ -1,62 +1,34 @@
/*
* 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;
- }
+ void sendLogLevel(const QString &logLevel);
+ void sendStartProcess(const QString &command, bool elevate);
+ void sendStopProcess();
+ void sendClearSettings();
+ void requestLogPath();
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);
+ void logPathReceived(const QString &logPath);
-private:
- QLocalSocket *m_socket;
- QMutex m_mutex;
- State m_state{State::Unconnected};
+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
new file mode 100644
index 000000000000..79a5d118759f
--- /dev/null
+++ b/src/lib/gui/ipc/IpcClient.cpp
@@ -0,0 +1,208 @@
+/*
+ * 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 "common/VersionInfo.h"
+
+#include
+#include
+#include
+
+namespace deskflow::gui::ipc {
+
+IpcClient::IpcClient(QObject *parent, const QString &socketName, const QString &typeName)
+ : QObject(parent),
+ m_socket{new QLocalSocket(this)},
+ 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);
+ connect(m_socket, &QLocalSocket::readyRead, this, &IpcClient::handleReadyRead);
+}
+
+void IpcClient::connectToServer()
+{
+ if (m_state == State::Connecting) {
+ qWarning().noquote() << QStringLiteral("%1 ipc client already connecting to server").arg(m_typeName);
+ return;
+ }
+
+ if (m_state != State::Unconnected) {
+ qDebug().noquote() << QStringLiteral("%1 ipc client not in unconnected state, disconnecting").arg(m_typeName);
+ disconnectFromServer();
+ }
+
+ if (m_socket->state() != QLocalSocket::UnconnectedState) {
+ qWarning().noquote(
+ ) << QStringLiteral("%1 ipc client socket not in unconnected state, disconnecting").arg(m_typeName);
+ disconnectFromServer();
+ }
+
+ m_retryCount = 0;
+ attemptConnection();
+}
+
+void IpcClient::attemptConnection()
+{
+ 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(retryLimit));
+ m_state = State::Unconnected;
+ Q_EMIT connectionFailed();
+ return;
+ }
+
+ if (m_retryCount == 0) {
+ qDebug().noquote() << QStringLiteral("%1 ipc client connecting to server: %2").arg(m_typeName, m_socketName);
+ } else {
+ qDebug().noquote() << QStringLiteral("%1 ipc client retrying connection, attempt: %2")
+ .arg(m_typeName, QString::number(m_retryCount + 1));
+ }
+
+ m_state = State::Connecting;
+ m_retryCount++;
+
+ connect(
+ m_socket, &QLocalSocket::connected, this,
+ [this] {
+ const auto versionId = QStringLiteral("%1+%2").arg(kVersion, kVersionGitSha);
+ 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
+ );
+
+ connect(
+ m_socket, &QLocalSocket::errorOccurred, this,
+ [this] {
+ 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);
+ },
+ Qt::SingleShotConnection
+ );
+
+ m_socket->connectToServer(m_socketName);
+}
+
+void IpcClient::disconnectFromServer()
+{
+ m_state = State::Disconnecting;
+ qDebug().noquote() << QStringLiteral("%1 ipc client disconnecting from server").arg(m_typeName);
+ m_socket->disconnectFromServer();
+ m_state = State::Unconnected;
+}
+
+void IpcClient::handleDisconnected()
+{
+ if (m_state == State::Connecting) {
+ return;
+ }
+
+ qDebug().noquote() << QStringLiteral("%1 ipc client disconnected from server").arg(m_typeName);
+ 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().noquote() << QStringLiteral("%1 ipc client error: %2").arg(m_typeName, m_socket->errorString());
+
+ if (m_state == State::Connected) {
+ disconnectFromServer();
+ Q_EMIT connectionFailed();
+ }
+}
+
+void IpcClient::handleReadyRead()
+{
+ 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().noquote() << QStringLiteral("%1 ipc client message: %2").arg(m_typeName, message);
+ const auto parts = message.split('=');
+ if (parts.isEmpty()) {
+ qWarning().noquote() << QStringLiteral("%1 ipc client got invalid message: %2").arg(m_typeName, message);
+ continue;
+ }
+
+ if (m_state == State::Connecting) {
+ handleHandshakeMessage(parts);
+ continue;
+ }
+
+ processCommand(parts.at(0), parts);
+ }
+
+ if (!data.isEmpty()) {
+ m_readBuffer = data;
+ }
+}
+
+void IpcClient::handleHandshakeMessage(const QStringList &parts)
+{
+ 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) != QStringLiteral("hello")) {
+ 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);
+ 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();
+ Q_EMIT connectionFailed();
+ return;
+ }
+
+ m_state = State::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().noquote() << QStringLiteral("%1 cannot send command, ipc client not connected").arg(m_typeName);
+ return;
+ }
+
+ m_socket->write(message.toUtf8() + "\n");
+ 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
new file mode 100644
index 000000000000..231b1600f064
--- /dev/null
+++ b/src/lib/gui/ipc/IpcClient.h
@@ -0,0 +1,68 @@
+/*
+ * 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
+
+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, const QString &typeName);
+ void connectToServer();
+ void disconnectFromServer();
+
+ bool isConnected() const
+ {
+ return m_state == State::Connected;
+ }
+
+Q_SIGNALS:
+ void connected();
+ void connectionFailed();
+
+private Q_SLOTS:
+ void handleDisconnected();
+ void handleErrorOccurred();
+ void handleReadyRead();
+
+protected:
+ virtual void processCommand(const QString &command, const QStringList &parts)
+ {
+ Q_UNUSED(command)
+ Q_UNUSED(parts)
+ }
+
+ void sendMessage(const QString &message);
+
+private:
+ void attemptConnection();
+ void handleHandshakeMessage(const QStringList &parts);
+
+ QLocalSocket *m_socket;
+ State m_state{State::Unconnected};
+ QString m_socketName;
+ QByteArray m_readBuffer;
+ int m_retryCount{0};
+ QString m_typeName;
+};
+
+} // 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/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");
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;
}
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/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/lib/server/Server.cpp b/src/lib/server/Server.cpp
index 3ddf06537a71..8e55e6b58836 100644
--- a/src/lib/server/Server.cpp
+++ b/src/lib/server/Server.cpp
@@ -18,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"
@@ -233,7 +234,8 @@ 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());
+ ipcSendToClient("unrecognisedClient", QString::fromStdString(client->getName()));
closeClient(client, kMsgEUnknown);
return;
}
@@ -245,7 +247,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 +295,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 +1301,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;
//@}
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)
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);
-};
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