From 66c9b6607ce6368c936fec8ff8d593d298e90766 Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Thu, 26 Mar 2026 12:34:18 +0000 Subject: [PATCH] feat(ipc): Messaging for connection states via IPC (async client) --- src/lib/base/Log.h | 2 - src/lib/client/Client.cpp | 4 +- src/lib/deskflow/CMakeLists.txt | 2 + src/lib/deskflow/ClientApp.cpp | 13 +- src/lib/deskflow/ServerApp.cpp | 4 +- src/lib/deskflow/ipc/CoreIpc.cpp | 28 ++++ src/lib/deskflow/ipc/CoreIpc.h | 14 ++ src/lib/deskflow/ipc/CoreIpcServer.cpp | 11 +- src/lib/deskflow/ipc/CoreIpcServer.h | 2 + src/lib/deskflow/ipc/IpcServer.cpp | 25 ++++ src/lib/deskflow/ipc/IpcServer.h | 2 + src/lib/gui/MainWindow.cpp | 1 + src/lib/gui/core/CoreProcess.cpp | 198 +++++++++++++------------ src/lib/gui/core/CoreProcess.h | 12 +- src/lib/gui/ipc/CoreIpcClient.cpp | 8 +- src/lib/gui/ipc/CoreIpcClient.h | 6 + src/lib/gui/ipc/DaemonIpcClient.cpp | 85 +++-------- src/lib/gui/ipc/DaemonIpcClient.h | 16 +- src/lib/gui/ipc/IpcClient.cpp | 187 +++++++++++------------ src/lib/gui/ipc/IpcClient.h | 19 ++- src/lib/net/SecureSocket.cpp | 6 +- src/lib/net/SslLogger.cpp | 10 +- src/lib/server/ClientProxy1_0.cpp | 4 +- src/lib/server/Server.cpp | 24 ++- src/lib/server/Server.h | 1 + 25 files changed, 376 insertions(+), 308 deletions(-) create mode 100644 src/lib/deskflow/ipc/CoreIpc.cpp create mode 100644 src/lib/deskflow/ipc/CoreIpc.h diff --git a/src/lib/base/Log.h b/src/lib/base/Log.h index eb9a53e33..2697bc8f6 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 634f340dd..64a2a95cc 100644 --- a/src/lib/client/Client.cpp +++ b/src/lib/client/Client.cpp @@ -14,6 +14,7 @@ #include "base/NetworkProtocol.h" #include "client/ServerProxy.h" #include "common/Settings.h" +#include "deskflow/ipc/CoreIpc.h" #include "deskflow/Clipboard.h" #include "deskflow/IPlatformScreen.h" #include "deskflow/PacketStreamFilter.h" @@ -92,10 +93,11 @@ void Client::connect(size_t addressIndex) // m_serverAddress will be null if the hostname address is not reolved if (m_serverAddress.getAddress() != nullptr) { // to help users troubleshoot, show server host name (issue: 60) - LOG_IPC( + LOG_DEBUG( "connecting to '%s': %s:%i", m_serverAddress.getHostname().c_str(), ARCH->addrToString(m_serverAddress.getAddress()).c_str(), m_serverAddress.getPort() ); + ipcSendConnectionState(deskflow::core::ConnectionState::Connecting); } // create the socket diff --git a/src/lib/deskflow/CMakeLists.txt b/src/lib/deskflow/CMakeLists.txt index 97021968e..2d8ab2a0b 100644 --- a/src/lib/deskflow/CMakeLists.txt +++ b/src/lib/deskflow/CMakeLists.txt @@ -83,6 +83,8 @@ add_library(${lib_name} STATIC ${PLATFORM_CODE} ipc/IpcServer.h ipc/DaemonIpcServer.cpp ipc/DaemonIpcServer.h + ipc/CoreIpc.cpp + ipc/CoreIpc.h ipc/CoreIpcServer.cpp ipc/CoreIpcServer.h ) diff --git a/src/lib/deskflow/ClientApp.cpp b/src/lib/deskflow/ClientApp.cpp index 08cce6d1b..702ad9763 100644 --- a/src/lib/deskflow/ClientApp.cpp +++ b/src/lib/deskflow/ClientApp.cpp @@ -17,6 +17,7 @@ #include "common/Settings.h" #include "deskflow/Screen.h" #include "deskflow/ScreenException.h" +#include "deskflow/ipc/CoreIpc.h" #include "net/NetworkAddress.h" #include "net/SocketException.h" #include "net/SocketMultiplexer.h" @@ -159,10 +160,8 @@ void ClientApp::handleClientRestart(const Event &, EventQueueTimer *timer) void ClientApp::scheduleClientRestart(double retryTime) { - if (Settings::value(Settings::Client::DynamicConnectionRetry).toBool()) - LOG_IPC("retry in %.0f seconds", retryTime); - else - LOG_DEBUG("retry in %.0f seconds", retryTime); + LOG_DEBUG("retry in %.0f seconds", retryTime); + ipcSendToClient("retryIn", QString::number(retryTime, 'f', 0)); // install a timer and handler to retry later EventQueueTimer *timer = getEvents()->newOneShotTimer(retryTime, nullptr); getEvents()->addHandler(EventTypes::Timer, timer, [this, timer](const auto &e) { handleClientRestart(e, timer); }); @@ -170,7 +169,8 @@ void ClientApp::scheduleClientRestart(double retryTime) void ClientApp::handleClientConnected() { - LOG_IPC("connected to server"); + LOG_DEBUG("connected to server"); + ipcSendConnectionState(deskflow::core::ConnectionState::Connected); // Reset server index on successful connection m_currentServerIndex = 0; m_lastServerAddressIndex = 0; @@ -223,7 +223,8 @@ void ClientApp::handleClientRefused(const Event &e) void ClientApp::handleClientDisconnected() { m_retryCount = 0; - LOG_IPC("disconnected from server"); + LOG_DEBUG("disconnected from server"); + ipcSendConnectionState(deskflow::core::ConnectionState::Disconnected); if (!m_suspended) { scheduleClientRestart(retryTime()); } diff --git a/src/lib/deskflow/ServerApp.cpp b/src/lib/deskflow/ServerApp.cpp index 3b5c2e58b..4efe27f10 100644 --- a/src/lib/deskflow/ServerApp.cpp +++ b/src/lib/deskflow/ServerApp.cpp @@ -18,6 +18,7 @@ #include "deskflow/ProtocolTypes.h" #include "deskflow/Screen.h" #include "deskflow/ScreenException.h" +#include "deskflow/ipc/CoreIpc.h" #include "net/SocketException.h" #include "net/SocketMultiplexer.h" #include "net/TCPSocketFactory.h" @@ -370,7 +371,8 @@ bool ServerApp::startServer() listener->setServer(m_server); m_server->setListener(listener); m_listener = listener; - LOG_IPC("started server, waiting for clients"); + LOG_DEBUG("started server, waiting for clients"); + ipcSendConnectionState(deskflow::core::ConnectionState::Listening); m_serverState = Started; return true; } catch (SocketAddressInUseException &e) { diff --git a/src/lib/deskflow/ipc/CoreIpc.cpp b/src/lib/deskflow/ipc/CoreIpc.cpp new file mode 100644 index 000000000..7dad7ca71 --- /dev/null +++ b/src/lib/deskflow/ipc/CoreIpc.cpp @@ -0,0 +1,28 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2026 Symless Ltd. + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#include "CoreIpc.h" + +#include "CoreIpcServer.h" + +#include + +void ipcSendToClient(const QString &command, const QString &args) +{ + // Queued because callers may not be on the main thread, + // and QLocalSocket can only be written to from its owning thread. + auto &server = deskflow::core::ipc::CoreIpcServer::instance(); + QMetaObject::invokeMethod( + &server, [command, args] { deskflow::core::ipc::CoreIpcServer::instance().broadcastCommand(command, args); }, + Qt::QueuedConnection + ); +} + +void ipcSendConnectionState(deskflow::core::ConnectionState state) +{ + const auto metaEnum = QMetaEnum::fromType(); + ipcSendToClient("connectionState", metaEnum.valueToKey(static_cast(state))); +} diff --git a/src/lib/deskflow/ipc/CoreIpc.h b/src/lib/deskflow/ipc/CoreIpc.h new file mode 100644 index 000000000..738055cf0 --- /dev/null +++ b/src/lib/deskflow/ipc/CoreIpc.h @@ -0,0 +1,14 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2026 Symless Ltd. + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#pragma once + +#include "common/Enums.h" + +#include + +void ipcSendToClient(const QString &command, const QString &args = ""); +void ipcSendConnectionState(deskflow::core::ConnectionState state); diff --git a/src/lib/deskflow/ipc/CoreIpcServer.cpp b/src/lib/deskflow/ipc/CoreIpcServer.cpp index c67363070..4ead60f9f 100644 --- a/src/lib/deskflow/ipc/CoreIpcServer.cpp +++ b/src/lib/deskflow/ipc/CoreIpcServer.cpp @@ -13,9 +13,18 @@ namespace deskflow::core::ipc { +static CoreIpcServer *s_instance = nullptr; + CoreIpcServer::CoreIpcServer(QObject *parent) : IpcServer(parent, kCoreIpcName) { - // do nothing + assert(s_instance == nullptr); + s_instance = this; +} + +CoreIpcServer &CoreIpcServer::instance() +{ + assert(s_instance != nullptr); + return *s_instance; } void CoreIpcServer::processCommand(QLocalSocket *clientSocket, const QString &command, const QStringList &parts) diff --git a/src/lib/deskflow/ipc/CoreIpcServer.h b/src/lib/deskflow/ipc/CoreIpcServer.h index d0f0397b6..bfdc822f0 100644 --- a/src/lib/deskflow/ipc/CoreIpcServer.h +++ b/src/lib/deskflow/ipc/CoreIpcServer.h @@ -22,6 +22,8 @@ class CoreIpcServer : public IpcServer public: explicit CoreIpcServer(QObject *parent); + static CoreIpcServer &instance(); + private: void processCommand(QLocalSocket *clientSocket, const QString &command, const QStringList &parts) override; }; diff --git a/src/lib/deskflow/ipc/IpcServer.cpp b/src/lib/deskflow/ipc/IpcServer.cpp index 1cf1467af..131c1d93d 100644 --- a/src/lib/deskflow/ipc/IpcServer.cpp +++ b/src/lib/deskflow/ipc/IpcServer.cpp @@ -112,6 +112,14 @@ void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &messag if (const auto &command = parts.at(0); command == "hello") { LOG_DEBUG("ipc server got hello message, sending hello back"); writeToClientSocket(clientSocket, "hello"); + + // Replay messages that were queued before any clients connected. + LOG_DEBUG1("ipc server replaying %d pending messages", m_pendingMessages.size()); + for (const auto &pending : std::as_const(m_pendingMessages)) { + LOG_DEBUG1("ipc server replaying: %s", pending.toUtf8().constData()); + writeToClientSocket(clientSocket, pending); + } + m_pendingMessages.clear(); } else if (command == "noop") { LOG_DEBUG("ipc server got noop message"); writeToClientSocket(clientSocket, "ok"); @@ -122,6 +130,23 @@ void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &messag clientSocket->flush(); } +void IpcServer::broadcastCommand(const QString &command, const QString &args) +{ + const auto message = args.isEmpty() ? command : command + "=" + args; + + if (m_clients.isEmpty()) { + LOG_DEBUG1("ipc server has no clients, message queued: %s", message.toUtf8().constData()); + m_pendingMessages.append(message); + return; + } + + LOG_DEBUG1("ipc server broadcasting message to %d clients: %s", m_clients.size(), message.toUtf8().constData()); + for (auto *client : std::as_const(m_clients)) { + writeToClientSocket(client, message); + client->flush(); + } +} + void IpcServer::writeToClientSocket(QLocalSocket *&clientSocket, const QString &message) const { QByteArray messageData = message.toUtf8() + '\n'; diff --git a/src/lib/deskflow/ipc/IpcServer.h b/src/lib/deskflow/ipc/IpcServer.h index 7f2c8ffc2..3e2a874b9 100644 --- a/src/lib/deskflow/ipc/IpcServer.h +++ b/src/lib/deskflow/ipc/IpcServer.h @@ -23,6 +23,7 @@ public: ~IpcServer() override; void listen(); + void broadcastCommand(const QString &command, const QString &args = ""); Q_SIGNALS: void logLevelChanged(const QString &logLevel); @@ -52,6 +53,7 @@ private: QLocalServer *m_server; QSet m_clients; QString m_serverName; + QStringList m_pendingMessages; }; } // namespace deskflow::core::ipc diff --git a/src/lib/gui/MainWindow.cpp b/src/lib/gui/MainWindow.cpp index 6a9eb77f6..f2f396740 100644 --- a/src/lib/gui/MainWindow.cpp +++ b/src/lib/gui/MainWindow.cpp @@ -271,6 +271,7 @@ void MainWindow::connectSlots() connect(&m_serverConnection, &ServerConnection::configureClient, this, &MainWindow::serverConnectionConfigureClient); connect(&m_serverConnection, &ServerConnection::clientsChanged, this, &MainWindow::serverClientsChanged); + connect(&m_coreProcess, &CoreProcess::connectedClientsChanged, this, &MainWindow::serverClientsChanged); connect( &m_serverConnection, &ServerConnection::requestNewClientPrompt, this, &MainWindow::handleNewClientPromptRequest ); diff --git a/src/lib/gui/core/CoreProcess.cpp b/src/lib/gui/core/CoreProcess.cpp index 26eed77ad..ef61cbe57 100644 --- a/src/lib/gui/core/CoreProcess.cpp +++ b/src/lib/gui/core/CoreProcess.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -112,6 +113,7 @@ CoreProcess::CoreProcess(const IServerConfig &serverConfig) connect( m_daemonIpcClient, &ipc::DaemonIpcClient::connectionFailed, this, &CoreProcess::daemonIpcClientConnectionFailed ); + connect(m_daemonIpcClient, &ipc::DaemonIpcClient::logPathReceived, this, &CoreProcess::setupDaemonLogTail); connect(&m_retryTimer, &QTimer::timeout, this, [this] { if (m_processState == ProcessState::RetryPending) { @@ -139,20 +141,7 @@ void CoreProcess::onProcessReadyReadStandardError() void CoreProcess::daemonIpcClientConnected() { applyLogLevel(); - - const auto logPath = requestDaemonLogPath(); - if (logPath.isEmpty()) { - qWarning() << "daemon no log path"; - return; - } - - qDebug() << "daemon log path:" << logPath; - if (m_daemonFileTail) { - m_daemonFileTail->setWatchedFile(logPath); - } else { - m_daemonFileTail = new FileTail(logPath, this); - connect(m_daemonFileTail, &FileTail::newLine, this, &CoreProcess::handleLogLines); - } + m_daemonIpcClient->requestLogPath(); } void CoreProcess::onProcessFinished(int exitCode, QProcess::ExitStatus) @@ -190,9 +179,7 @@ void CoreProcess::applyLogLevel() const auto processMode = Settings::value(Settings::Core::ProcessMode).value(); if (processMode == ProcessMode::Service) { qDebug() << "setting daemon log level:" << Settings::logLevelText(); - if (!m_daemonIpcClient->sendLogLevel(Settings::logLevelText())) { - qWarning() << "failed to set daemon ipc log level"; - } + m_daemonIpcClient->sendLogLevel(Settings::logLevelText()); } } @@ -234,15 +221,22 @@ void CoreProcess::startProcessFromDaemon(const QStringList &args) } QString commandQuoted = makeQuotedArgs(m_appPath, args); - qInfo("running command: %s", qPrintable(commandQuoted)); - if (!m_daemonIpcClient->sendStartProcess(commandQuoted, Settings::value(Settings::Daemon::Elevate).toBool())) { - qWarning("cannot start process, ipc command failed"); - return; - } + auto sendStart = [this, commandQuoted] { + m_daemonIpcClient->sendStartProcess(commandQuoted, Settings::value(Settings::Daemon::Elevate).toBool()); + setProcessState(ProcessState::Started); + }; - setProcessState(ProcessState::Started); + if (m_daemonIpcClient->isConnected()) { + sendStart(); + } else { + connect( + m_daemonIpcClient, &ipc::DaemonIpcClient::connected, this, sendStart, + static_cast(Qt::SingleShotConnection | Qt::QueuedConnection) + ); + m_daemonIpcClient->connectToServer(); + } } void CoreProcess::stopForegroundProcess() const @@ -271,12 +265,20 @@ void CoreProcess::stopProcessFromDaemon() qFatal("core process must be in stopping state"); } - if (!m_daemonIpcClient->sendStopProcess()) { - qWarning("cannot stop process, ipc command failed"); - return; - } + auto sendStop = [this] { + m_daemonIpcClient->sendStopProcess(); + setProcessState(ProcessState::Stopped); + }; - setProcessState(ProcessState::Stopped); + if (m_daemonIpcClient->isConnected()) { + sendStop(); + } else { + connect( + m_daemonIpcClient, &ipc::DaemonIpcClient::connected, this, sendStop, + static_cast(Qt::SingleShotConnection | Qt::QueuedConnection) + ); + m_daemonIpcClient->connectToServer(); + } } void CoreProcess::handleLogLines(const QString &text) @@ -293,9 +295,23 @@ void CoreProcess::handleLogLines(const QString &text) if (line.contains("calling TIS/TSM in non-main thread environment")) { continue; } + + // the core process is not allowed to show the permission prompt + // (called "notification permission") and the notification log line is emitted from + // deep inside cocoa code in the core binary to stdout, so it can't be sent over + // ipc from the core to the gui and instead the gui has to parse the core output. + static const QString needle = "OSX Notification: "; + if (line.contains(needle) && line.contains('|')) { + const int delimiterPosition = line.indexOf('|'); + const int start = line.indexOf(needle); + const QString title = line.mid(start + needle.length(), delimiterPosition - start - needle.length()); + const QString body = line.mid(delimiterPosition + 1, line.length() - delimiterPosition); + if (!showOSXNotification(title, body)) { + qDebug("osx notification was not shown"); + } + } #endif - checkLogLine(line); Q_EMIT logLine(line); } } @@ -366,6 +382,33 @@ void CoreProcess::start(std::optional processModeOption) qInfo().noquote() << "log file:" << logFile; } + // Wired before the start calls so it catches Started from both sync (desktop) and async (service) paths. + connect( + this, &CoreProcess::processStateChanged, this, + [this](ProcessState state) { + if (state != ProcessState::Started) { + return; + } + + // Delay briefly to give the core process time to start its IPC server. + QTimer::singleShot(kRetryDelay, this, [this] { + if (m_processState != ProcessState::Started) { + return; + } + + m_coreIpcClient = new ipc::CoreIpcClient(this); + connect(m_coreIpcClient, &ipc::CoreIpcClient::commandReceived, this, &CoreProcess::onCoreIpcMessageReceived); + connect(m_coreIpcClient, &ipc::CoreIpcClient::connected, this, [] { qInfo("connected to core ipc server"); }); + connect(m_coreIpcClient, &ipc::CoreIpcClient::connectionFailed, this, [] { + qWarning("failed to establish core ipc connection"); + }); + + m_coreIpcClient->connectToServer(); + }); + }, + static_cast(Qt::SingleShotConnection | Qt::QueuedConnection) + ); + if (processMode == ProcessMode::Desktop) { startForegroundProcess(args); } else if (processMode == ProcessMode::Service) { @@ -373,21 +416,6 @@ void CoreProcess::start(std::optional processModeOption) startProcessFromDaemon(args); } - // Don't block the main GUI render thread when connecting to the Core IPC server. - QTimer::singleShot(kRetryDelay, this, [this] { - if (m_processState != ProcessState::Started) { - qWarning("core process failed to start, skipping core ipc connection"); - return; - } - - m_coreIpcClient = new ipc::CoreIpcClient(this); - if (m_coreIpcClient->connectToServer()) { - qInfo("connected to core ipc server"); - } else { - qWarning("failed to establish core ipc connection"); - } - }); - m_lastProcessMode = processMode; } @@ -500,35 +528,27 @@ void CoreProcess::setProcessState(ProcessState state) Q_EMIT processStateChanged(state); } -void CoreProcess::checkLogLine(const QString &line) +void CoreProcess::onCoreIpcMessageReceived(const QString &command, const QString &args) { - using enum ConnectionState; - - if (line.contains("connected to server") || line.contains("has connected")) { - m_connections++; - setConnectionState(Connected); - } else if (line.contains("started server")) { - m_connections = 0; - setConnectionState(Listening); - } else if (line.contains("disconnected from server") || line.contains("process exited")) { - m_connections = 0; - setConnectionState(Disconnected); - } else if (line.contains("connecting to")) { - setConnectionState(Connecting); - } else if (line.contains("has disconnected")) { - m_connections--; - if (m_connections < 1) { - setConnectionState(Listening); + if (command == "connectionState") { + const auto metaEnum = QMetaEnum::fromType(); + bool ok = false; + const auto state = static_cast(metaEnum.keyToValue(args.toUtf8().constData(), &ok)); + if (!ok) { + qWarning("core ipc got unknown connection state: %s", args.toUtf8().constData()); + return; + } + setConnectionState(state); + } else if (command == "connectedClients") { + const auto clients = args.isEmpty() ? QStringList() : args.split(","); + Q_EMIT connectedClientsChanged(clients); + } else if (command == "secureSocket") { + Q_EMIT secureSocket(true); + if (args != m_secureSocketVersion) { + m_secureSocketVersion = args; + Q_EMIT securityLevelChanged(args); } } - - checkSecureSocket(line); - - // server and client processes are not allowed to show notifications. - // process the log from it and show notification from deskflow instead. -#ifdef Q_OS_MACOS - checkOSXNotification(line); -#endif } bool CoreProcess::checkSecureSocket(const QString &line) @@ -548,46 +568,30 @@ bool CoreProcess::checkSecureSocket(const QString &line) return true; } -#ifdef Q_OS_MACOS -void CoreProcess::checkOSXNotification(const QString &line) -{ - static const QString needle = "OSX Notification: "; - if (line.contains(needle) && line.contains('|')) { - int delimiterPosition = line.indexOf('|'); - int start = line.indexOf(needle); - QString title = line.mid(start + needle.length(), delimiterPosition - start - needle.length()); - QString body = line.mid(delimiterPosition + 1, line.length() - delimiterPosition); - if (!showOSXNotification(title, body)) { - qDebug("osx notification was not shown"); - } - } -} -#endif - QString CoreProcess::correctedAddress(const QString &address) const { return wrapIpv6(address.simplified()); } -QString CoreProcess::requestDaemonLogPath() +void CoreProcess::setupDaemonLogTail(const QString &logPath) { - qDebug() << "requesting daemon log path"; - const auto logPath = m_daemonIpcClient->requestLogPath(); - if (logPath.isEmpty()) { - qCritical() << "failed to get daemon log path"; - return QString(); - } + qDebug() << "daemon log path:" << logPath; if (QFileInfo logFile(logPath); !logFile.isFile()) { auto file = QFile(logPath); if (!file.open(QFile::ReadWrite)) { qCritical() << "daemon log path file can not be written:" << logPath; - return QString(); + return; } file.write(""); // Create an empty file } - return logPath; + if (m_daemonFileTail) { + m_daemonFileTail->setWatchedFile(logPath); + } else { + m_daemonFileTail = new FileTail(logPath, this); + connect(m_daemonFileTail, &FileTail::newLine, this, &CoreProcess::handleLogLines); + } } void CoreProcess::clearSettings() @@ -608,9 +612,7 @@ void CoreProcess::clearSettings() void CoreProcess::retryDaemon() { - if (m_daemonIpcClient->connectToServer()) { - qInfo("successfully reconnected to daemon"); - } + m_daemonIpcClient->connectToServer(); } } // namespace deskflow::gui diff --git a/src/lib/gui/core/CoreProcess.h b/src/lib/gui/core/CoreProcess.h index 4db375da9..25abd0ac7 100644 --- a/src/lib/gui/core/CoreProcess.h +++ b/src/lib/gui/core/CoreProcess.h @@ -22,7 +22,7 @@ namespace deskflow::gui { namespace ipc { class CoreIpcClient; class DaemonIpcClient; -} +} // namespace ipc class CoreProcess : public QObject { @@ -89,12 +89,14 @@ Q_SIGNALS: void processStateChanged(deskflow::core::ProcessState state); void secureSocket(bool enabled); void daemonIpcClientConnectionFailed(); + void connectedClientsChanged(const QStringList &clients); void securityLevelChanged(QString securityLevel); private Q_SLOTS: void onProcessFinished(int exitCode, QProcess::ExitStatus); void onProcessReadyReadStandardOutput(); void onProcessReadyReadStandardError(); + void onCoreIpcMessageReceived(const QString &command, const QString &args); void daemonIpcClientConnected(); private: @@ -105,20 +107,15 @@ private: QPair persistServerConfig() const; void setConnectionState(ConnectionState state); void setProcessState(ProcessState state); - void checkLogLine(const QString &line); bool checkSecureSocket(const QString &line); void handleLogLines(const QString &text); QString correctedAddress(const QString &address) const; - QString requestDaemonLogPath(); + void setupDaemonLogTail(const QString &logPath); static QString makeQuotedArgs(const QString &app, const QStringList &args); static QString processModeToString(const Settings::ProcessMode mode); static QString processStateToString(const CoreProcess::ProcessState state); static QString wrapIpv6(const QString &address); -#ifdef Q_OS_MACOS - void checkOSXNotification(const QString &line); -#endif - const IServerConfig &m_serverConfig; QString m_address; ProcessState m_processState = ProcessState::Stopped; @@ -128,7 +125,6 @@ private: QString m_secureSocketVersion; std::optional m_lastProcessMode = std::nullopt; QTimer m_retryTimer; - int m_connections = 0; deskflow::gui::ipc::CoreIpcClient *m_coreIpcClient = nullptr; deskflow::gui::ipc::DaemonIpcClient *m_daemonIpcClient = nullptr; FileTail *m_daemonFileTail = nullptr; diff --git a/src/lib/gui/ipc/CoreIpcClient.cpp b/src/lib/gui/ipc/CoreIpcClient.cpp index 309c762af..047761efd 100644 --- a/src/lib/gui/ipc/CoreIpcClient.cpp +++ b/src/lib/gui/ipc/CoreIpcClient.cpp @@ -15,11 +15,15 @@ namespace deskflow::gui::ipc { -const auto kTimeout = 1000; - CoreIpcClient::CoreIpcClient(QObject *parent) : IpcClient(parent, kCoreIpcName) { // do nothing } +void CoreIpcClient::processCommand(const QString &command, const QStringList &parts) +{ + const auto args = parts.size() >= 2 ? parts[1] : QString(); + Q_EMIT commandReceived(command, args); +} + } // namespace deskflow::gui::ipc diff --git a/src/lib/gui/ipc/CoreIpcClient.h b/src/lib/gui/ipc/CoreIpcClient.h index eb412ef59..bccb1ccd4 100644 --- a/src/lib/gui/ipc/CoreIpcClient.h +++ b/src/lib/gui/ipc/CoreIpcClient.h @@ -18,6 +18,12 @@ class CoreIpcClient : public IpcClient public: explicit CoreIpcClient(QObject *parent = nullptr); + +Q_SIGNALS: + void commandReceived(const QString &command, const QString &args); + +protected: + void processCommand(const QString &command, const QStringList &parts) override; }; } // namespace deskflow::gui::ipc diff --git a/src/lib/gui/ipc/DaemonIpcClient.cpp b/src/lib/gui/ipc/DaemonIpcClient.cpp index 036badb34..124967eac 100644 --- a/src/lib/gui/ipc/DaemonIpcClient.cpp +++ b/src/lib/gui/ipc/DaemonIpcClient.cpp @@ -9,93 +9,46 @@ #include "common/Constants.h" #include -#include -#include -#include -#include namespace deskflow::gui::ipc { -const auto kTimeout = 1000; - DaemonIpcClient::DaemonIpcClient(QObject *parent) : IpcClient(parent, kDaemonIpcName) { } -bool DaemonIpcClient::sendLogLevel(const QString &logLevel) +void DaemonIpcClient::sendLogLevel(const QString &logLevel) { - if (!keepAlive()) - return false; - sendMessage("logLevel=" + logLevel); - return true; } -bool DaemonIpcClient::sendStartProcess(const QString &command, bool elevate) +void DaemonIpcClient::sendStartProcess(const QString &command, bool elevate) { - if (!keepAlive()) - return false; - - if (!sendMessage("elevate=" + (elevate ? QStringLiteral("yes") : QStringLiteral("no")))) { - return false; - } - - if (!sendMessage("command=" + command)) { - return false; - } - - return sendMessage("start"); + const auto elevateStr = elevate ? QStringLiteral("yes") : QStringLiteral("no"); + sendMessage("elevate=" + elevateStr); + sendMessage("command=" + command); + sendMessage("start"); } -bool DaemonIpcClient::sendStopProcess() +void DaemonIpcClient::sendStopProcess() { - return sendMessage("stop"); + sendMessage("stop"); } -QString DaemonIpcClient::requestLogPath() +void DaemonIpcClient::sendClearSettings() { - if (!keepAlive()) - return QString(); - - if (!sendMessage("logPath", QString())) { - return QString(); - } - - if (!socket()->waitForReadyRead(kTimeout)) { - qWarning() << "daemon ipc client failed to read log path response"; - return QString(); - } - - QByteArray response = socket()->readAll(); - if (response.isEmpty()) { - qWarning() << "daemon ipc client got empty log path response"; - return QString(); - } - - QString responseData = QString::fromUtf8(response); - if (responseData.isEmpty()) { - qWarning() << "daemon ipc client failed to convert log path response to string"; - return QString(); - } - - // Trimming removes newline from end of message. - QStringList parts = responseData.trimmed().split("="); - if (parts.size() != 2) { - qWarning() << "daemon ipc client got invalid log path response:" << responseData; - return QString(); - } - - if (parts[0] != "logPath") { - qWarning() << "daemon ipc client got unexpected log path response:" << responseData; - return QString(); - } - - return parts[1]; + sendMessage("clearSettings"); } -bool DaemonIpcClient::sendClearSettings() +void DaemonIpcClient::requestLogPath() { - return sendMessage("clearSettings"); + sendMessage("logPath"); +} + +void DaemonIpcClient::processCommand(const QString &command, const QStringList &parts) +{ + if (command == "logPath" && parts.size() == 2) { + Q_EMIT logPathReceived(parts[1]); + } } } // namespace deskflow::gui::ipc diff --git a/src/lib/gui/ipc/DaemonIpcClient.h b/src/lib/gui/ipc/DaemonIpcClient.h index 182570316..9b72bfd48 100644 --- a/src/lib/gui/ipc/DaemonIpcClient.h +++ b/src/lib/gui/ipc/DaemonIpcClient.h @@ -18,11 +18,17 @@ class DaemonIpcClient : public IpcClient public: explicit DaemonIpcClient(QObject *parent = nullptr); - bool sendLogLevel(const QString &logLevel); - bool sendStartProcess(const QString &command, bool elevate); - bool sendStopProcess(); - bool sendClearSettings(); - QString requestLogPath(); + void sendLogLevel(const QString &logLevel); + void sendStartProcess(const QString &command, bool elevate); + void sendStopProcess(); + void sendClearSettings(); + void requestLogPath(); + +Q_SIGNALS: + void logPathReceived(const QString &logPath); + +protected: + void processCommand(const QString &command, const QStringList &parts) override; }; } // namespace deskflow::gui::ipc diff --git a/src/lib/gui/ipc/IpcClient.cpp b/src/lib/gui/ipc/IpcClient.cpp index 8edcc2d8d..1dfdd482b 100644 --- a/src/lib/gui/ipc/IpcClient.cpp +++ b/src/lib/gui/ipc/IpcClient.cpp @@ -8,9 +8,7 @@ #include #include -#include -#include -#include +#include namespace deskflow::gui::ipc { @@ -24,13 +22,14 @@ IpcClient::IpcClient(QObject *parent, const QString &socketName) { connect(m_socket, &QLocalSocket::disconnected, this, &IpcClient::handleDisconnected); connect(m_socket, &QLocalSocket::errorOccurred, this, &IpcClient::handleErrorOccurred); + connect(m_socket, &QLocalSocket::readyRead, this, &IpcClient::handleReadyRead); } -bool IpcClient::connectToServer() +void IpcClient::connectToServer() { if (m_state == State::Connecting) { qWarning() << "ipc client already connecting to server"; - return false; + return; } if (m_state != State::Unconnected) { @@ -43,139 +42,129 @@ bool IpcClient::connectToServer() disconnectFromServer(); } - for (int i = 0; i < kRetryLimit; ++i) { - if (i == 0) { - qDebug() << "ipc client connecting to server:" << m_socketName; - } else { - qDebug() << "ipc client retrying connection, attempt:" << i + 1; - } + m_retryCount = 0; + attemptConnection(); +} - m_state = State::Connecting; - m_socket->connectToServer(m_socketName); - - if (!m_socket->waitForConnected(kTimeout)) { - qWarning() << "ipc client failed to connect"; - disconnectFromServer(); - continue; - } - - if (!sendMessage("hello", "hello", false)) { - qWarning() << "ipc client failed to send hello"; - disconnectFromServer(); - continue; - } - - m_state = State::Connected; - qDebug() << "ipc client connected"; - Q_EMIT connected(); - return true; +void IpcClient::attemptConnection() +{ + if (m_retryCount >= kRetryLimit) { + qWarning() << "ipc client failed to connect after" << kRetryLimit << "attempts"; + m_state = State::Unconnected; + Q_EMIT connectionFailed(); + return; } - qWarning() << "ipc client failed to connect after" << kRetryLimit << "attempts"; - disconnectFromServer(); - Q_EMIT connectionFailed(); - return false; + if (m_retryCount == 0) { + qDebug() << "ipc client connecting to server:" << m_socketName; + } else { + qDebug() << "ipc client retrying connection, attempt:" << m_retryCount + 1; + } + + m_state = State::Connecting; + m_retryCount++; + + connect( + m_socket, &QLocalSocket::connected, this, + [this] { + m_socket->write("hello\n"); + qDebug() << "ipc client sent hello"; + }, + Qt::SingleShotConnection + ); + + connect( + m_socket, &QLocalSocket::errorOccurred, this, + [this] { + qWarning() << "ipc client failed to connect:" << m_socket->errorString(); + m_socket->disconnectFromServer(); + m_state = State::Unconnected; + QTimer::singleShot(0, this, &IpcClient::attemptConnection); + }, + Qt::SingleShotConnection + ); + + m_socket->connectToServer(m_socketName); } void IpcClient::disconnectFromServer() { - QMutexLocker locker(&m_mutex); - m_state = State::Disconnecting; qDebug() << "ipc client disconnecting from server"; m_socket->disconnectFromServer(); - - if (m_socket->state() != QLocalSocket::UnconnectedState) { - qDebug() << "ipc client waiting for socket to disconnect"; - m_socket->waitForDisconnected(kTimeout); - qDebug() << "ipc client disconnected from server"; - } else { - qDebug() << "ipc client socket already disconnected"; - } - m_state = State::Unconnected; } void IpcClient::handleDisconnected() { - qDebug() << "ipc client disconnected from server"; - if (m_state == State::Connected) { - Q_EMIT connectionFailed(); + if (m_state == State::Connecting) { + return; } + qDebug() << "ipc client disconnected from server"; + const auto wasConnected = m_state == State::Connected; m_state = State::Unconnected; + + if (wasConnected) { + Q_EMIT connectionFailed(); + } } void IpcClient::handleErrorOccurred() { + if (m_state == State::Connecting) { + return; + } + qWarning() << "ipc client error:" << m_socket->errorString(); - disconnectFromServer(); if (m_state == State::Connected) { + disconnectFromServer(); Q_EMIT connectionFailed(); } } -bool IpcClient::sendMessage(const QString &message, const QString &expectAck, const bool expectConnected) +void IpcClient::handleReadyRead() { - QMutexLocker locker(&m_mutex); + QByteArray data = m_readBuffer + m_socket->readAll(); + m_readBuffer.clear(); - if (expectConnected && !isConnected()) { - qWarning() << "cannot send command, ipc client not connected"; - return false; + while (data.contains('\n')) { + const auto index = data.indexOf('\n'); + const auto message = QString::fromUtf8(data.left(index)); + data.remove(0, index + 1); + + qDebug("ipc client message: %s", message.toUtf8().constData()); + const auto parts = message.split('='); + if (parts.isEmpty()) { + qWarning("ipc client got invalid message: %s", message.toUtf8().constData()); + continue; + } + + if (m_state == State::Connecting && parts.at(0) == "hello") { + m_state = State::Connected; + qDebug() << "ipc client connected"; + Q_EMIT connected(); + continue; + } + + processCommand(parts.at(0), parts); } - QByteArray messageData = message.toUtf8() + "\n"; - m_socket->write(messageData); - if (!m_socket->waitForBytesWritten(kTimeout)) { - qWarning() << "ipc client failed to write command"; - return false; + if (!data.isEmpty()) { + m_readBuffer = data; } - - if (!expectAck.isEmpty()) { - qDebug() << "ipc client waiting for ack:" << expectAck; - - if (!m_socket->waitForReadyRead(kTimeout)) { - qWarning() << "ipc client socket ready read timed out"; - return false; - } - - QByteArray response = m_socket->readAll(); - if (response.isEmpty()) { - qWarning() << "ipc client got empty response"; - return false; - } - - QString responseData = QString::fromUtf8(response); - if (responseData.isEmpty()) { - qWarning() << "ipc client failed to convert response to string"; - return false; - } - - if (responseData != expectAck + "\n") { - qWarning() << "ipc client got unexpected response:" << responseData; - return false; - } - } - - qDebug() << "ipc client sent message:" << messageData; - return true; } -bool IpcClient::keepAlive() +void IpcClient::sendMessage(const QString &message) { - if (!isConnected() && !connectToServer()) { - qWarning() << "ipc client keep alive failed to connect"; - return false; + if (m_state != State::Connected) { + qWarning() << "cannot send command, ipc client not connected"; + return; } - if (!sendMessage("noop")) { - qWarning() << "ipc client keep alive ping failed, reconnecting"; - connectToServer(); - return false; - } - - return true; + m_socket->write(message.toUtf8() + "\n"); + qDebug() << "ipc client sent message:" << message; } } // namespace deskflow::gui::ipc diff --git a/src/lib/gui/ipc/IpcClient.h b/src/lib/gui/ipc/IpcClient.h index ebe12c805..8e4068c5c 100644 --- a/src/lib/gui/ipc/IpcClient.h +++ b/src/lib/gui/ipc/IpcClient.h @@ -6,7 +6,6 @@ #pragma once -#include #include class QLocalSocket; @@ -28,7 +27,7 @@ class IpcClient : public QObject public: explicit IpcClient(QObject *parent, const QString &socketName); - bool connectToServer(); + void connectToServer(); void disconnectFromServer(); bool isConnected() const @@ -43,21 +42,25 @@ Q_SIGNALS: private Q_SLOTS: void handleDisconnected(); void handleErrorOccurred(); + void handleReadyRead(); protected: - bool keepAlive(); - bool sendMessage(const QString &message, const QString &expectAck = "ok", const bool expectConnected = true); - - QLocalSocket *socket() const + virtual void processCommand(const QString &command, const QStringList &parts) { - return m_socket; + Q_UNUSED(command) + Q_UNUSED(parts) } + void sendMessage(const QString &message); + private: + void attemptConnection(); + QLocalSocket *m_socket; State m_state{State::Unconnected}; QString m_socketName; - QMutex m_mutex; + QByteArray m_readBuffer; + int m_retryCount{0}; }; } // namespace deskflow::gui::ipc diff --git a/src/lib/net/SecureSocket.cpp b/src/lib/net/SecureSocket.cpp index 3651443ea..3198f9690 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 06bace07e..4bc037064 100644 --- a/src/lib/net/SslLogger.cpp +++ b/src/lib/net/SslLogger.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -105,13 +106,12 @@ void SslLogger::logSecureConnectInfo(const SSL *ssl) std::istream_iterator{iss}, std::istream_iterator{} }; if (parts.size() > 2) { - // log the section containing the protocol version - LOG_INFO("network encryption protocol: %s", parts[1].c_str()); + LOG_DEBUG("network encryption protocol: %s", parts[1].c_str()); + ipcSendToClient("secureSocket", parts[1].c_str()); } else { - // log the error in spliting then display the whole description rather - // then nothing LOG_ERR("could not split cipher for protocol"); - LOG_INFO("network encryption protocol: %s", msg); + LOG_DEBUG("network encryption protocol: %s", msg); + ipcSendToClient("secureSocket", msg); } } else { LOG_ERR("could not get secure socket cipher"); diff --git a/src/lib/server/ClientProxy1_0.cpp b/src/lib/server/ClientProxy1_0.cpp index a59056055..3becfaa56 100644 --- a/src/lib/server/ClientProxy1_0.cpp +++ b/src/lib/server/ClientProxy1_0.cpp @@ -185,7 +185,7 @@ bool ClientProxy1_0::parseMessage(const uint8_t *code) void ClientProxy1_0::handleDisconnect() { - LOG_IPC("client \"%s\" has disconnected", getName().c_str()); + LOG_DEBUG("client \"%s\" has disconnected", getName().c_str()); disconnect(); } @@ -198,7 +198,7 @@ void ClientProxy1_0::handleWriteError() void ClientProxy1_0::handleFlatline() { // didn't get a heartbeat fast enough. assume client is dead. - LOG_IPC("client \"%s\" is dead", getName().c_str()); + LOG_DEBUG("client \"%s\" is dead", getName().c_str()); disconnect(); } diff --git a/src/lib/server/Server.cpp b/src/lib/server/Server.cpp index 3ddf06537..df505ea95 100644 --- a/src/lib/server/Server.cpp +++ b/src/lib/server/Server.cpp @@ -11,6 +11,7 @@ #include "base/IEventQueue.h" #include "base/Log.h" #include "deskflow/AppUtil.h" +#include "deskflow/ipc/CoreIpc.h" #include "deskflow/DeskflowException.h" #include "deskflow/IPlatformScreen.h" #include "deskflow/OptionTypes.h" @@ -233,7 +234,7 @@ void Server::adoptClient(BaseClientProxy *client) // name must be in our configuration if (!m_config->isScreen(client->getName())) { - LOG_IPC("unrecognised client name \"%s\", check server config", client->getName().c_str()); + LOG_WARN("unrecognised client name \"%s\", check server config", client->getName().c_str()); closeClient(client, kMsgEUnknown); return; } @@ -245,7 +246,9 @@ void Server::adoptClient(BaseClientProxy *client) closeClient(client, kMsgEBusy); return; } - LOG_IPC("client \"%s\" has connected", getName(client).c_str()); + LOG_DEBUG("client \"%s\" has connected", getName(client).c_str()); + ipcSendConnectionState(deskflow::core::ConnectionState::Connected); + sendConnectedClientsIpc(); // send configuration options to client sendOptions(client); @@ -291,6 +294,18 @@ void Server::getClients(std::vector &list) const } } +void Server::sendConnectedClientsIpc() const +{ + const auto primaryName = getName(m_primaryClient); + QStringList clientList; + for (const auto &[name, _] : m_clients) { + if (name != primaryName) { + clientList.append(QString::fromStdString(name)); + } + } + ipcSendToClient("connectedClients", clientList.join(",")); +} + std::string Server::getName(const BaseClientProxy *client) const { std::string name = m_config->getCanonicalName(client->getName()); @@ -1285,6 +1300,11 @@ void Server::handleClientDisconnected(BaseClientProxy *client) removeActiveClient(client); removeOldClient(client); + // m_clients always contains the primary (server) screen, so 1 means no remote clients. + using enum deskflow::core::ConnectionState; + ipcSendConnectionState(m_clients.size() <= 1 ? Listening : Connected); + sendConnectedClientsIpc(); + delete client; } diff --git a/src/lib/server/Server.h b/src/lib/server/Server.h index 3fc7d76ea..1e937f545 100644 --- a/src/lib/server/Server.h +++ b/src/lib/server/Server.h @@ -197,6 +197,7 @@ public: Set the \c list to the names of the currently connected clients. */ void getClients(std::vector &list) const; + void sendConnectedClientsIpc() const; //@}