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