feat(ipc): Messaging for connection states via IPC (async client)

This commit is contained in:
Nick Bolton 2026-03-26 12:34:18 +00:00
parent 62ccb543dc
commit 66c9b6607c
25 changed files with 376 additions and 308 deletions

View file

@ -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 // 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 // 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_PRINT CLOG_TRACE "%z\057" // char is '/'
#define CLOG_CRIT CLOG_TRACE "%z\060" // char is '0' #define CLOG_CRIT CLOG_TRACE "%z\060" // char is '0'
#define CLOG_ERR CLOG_TRACE "%z\061" #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_DEBUG1 CLOG_TRACE "%z\066"
#define CLOG_DEBUG2 CLOG_TRACE "%z\067" #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_PRINT(...) LOG((CLOG_PRINT __VA_ARGS__))
#define LOG_CRIT(...) LOG((CLOG_CRIT __VA_ARGS__)) #define LOG_CRIT(...) LOG((CLOG_CRIT __VA_ARGS__))
#define LOG_ERR(...) LOG((CLOG_ERR __VA_ARGS__)) #define LOG_ERR(...) LOG((CLOG_ERR __VA_ARGS__))

View file

@ -14,6 +14,7 @@
#include "base/NetworkProtocol.h" #include "base/NetworkProtocol.h"
#include "client/ServerProxy.h" #include "client/ServerProxy.h"
#include "common/Settings.h" #include "common/Settings.h"
#include "deskflow/ipc/CoreIpc.h"
#include "deskflow/Clipboard.h" #include "deskflow/Clipboard.h"
#include "deskflow/IPlatformScreen.h" #include "deskflow/IPlatformScreen.h"
#include "deskflow/PacketStreamFilter.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 // m_serverAddress will be null if the hostname address is not reolved
if (m_serverAddress.getAddress() != nullptr) { if (m_serverAddress.getAddress() != nullptr) {
// to help users troubleshoot, show server host name (issue: 60) // to help users troubleshoot, show server host name (issue: 60)
LOG_IPC( LOG_DEBUG(
"connecting to '%s': %s:%i", m_serverAddress.getHostname().c_str(), "connecting to '%s': %s:%i", m_serverAddress.getHostname().c_str(),
ARCH->addrToString(m_serverAddress.getAddress()).c_str(), m_serverAddress.getPort() ARCH->addrToString(m_serverAddress.getAddress()).c_str(), m_serverAddress.getPort()
); );
ipcSendConnectionState(deskflow::core::ConnectionState::Connecting);
} }
// create the socket // create the socket

View file

@ -83,6 +83,8 @@ add_library(${lib_name} STATIC ${PLATFORM_CODE}
ipc/IpcServer.h ipc/IpcServer.h
ipc/DaemonIpcServer.cpp ipc/DaemonIpcServer.cpp
ipc/DaemonIpcServer.h ipc/DaemonIpcServer.h
ipc/CoreIpc.cpp
ipc/CoreIpc.h
ipc/CoreIpcServer.cpp ipc/CoreIpcServer.cpp
ipc/CoreIpcServer.h ipc/CoreIpcServer.h
) )

View file

@ -17,6 +17,7 @@
#include "common/Settings.h" #include "common/Settings.h"
#include "deskflow/Screen.h" #include "deskflow/Screen.h"
#include "deskflow/ScreenException.h" #include "deskflow/ScreenException.h"
#include "deskflow/ipc/CoreIpc.h"
#include "net/NetworkAddress.h" #include "net/NetworkAddress.h"
#include "net/SocketException.h" #include "net/SocketException.h"
#include "net/SocketMultiplexer.h" #include "net/SocketMultiplexer.h"
@ -159,10 +160,8 @@ void ClientApp::handleClientRestart(const Event &, EventQueueTimer *timer)
void ClientApp::scheduleClientRestart(double retryTime) void ClientApp::scheduleClientRestart(double retryTime)
{ {
if (Settings::value(Settings::Client::DynamicConnectionRetry).toBool()) LOG_DEBUG("retry in %.0f seconds", retryTime);
LOG_IPC("retry in %.0f seconds", retryTime); ipcSendToClient("retryIn", QString::number(retryTime, 'f', 0));
else
LOG_DEBUG("retry in %.0f seconds", retryTime);
// install a timer and handler to retry later // install a timer and handler to retry later
EventQueueTimer *timer = getEvents()->newOneShotTimer(retryTime, nullptr); EventQueueTimer *timer = getEvents()->newOneShotTimer(retryTime, nullptr);
getEvents()->addHandler(EventTypes::Timer, timer, [this, timer](const auto &e) { handleClientRestart(e, timer); }); 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() void ClientApp::handleClientConnected()
{ {
LOG_IPC("connected to server"); LOG_DEBUG("connected to server");
ipcSendConnectionState(deskflow::core::ConnectionState::Connected);
// Reset server index on successful connection // Reset server index on successful connection
m_currentServerIndex = 0; m_currentServerIndex = 0;
m_lastServerAddressIndex = 0; m_lastServerAddressIndex = 0;
@ -223,7 +223,8 @@ void ClientApp::handleClientRefused(const Event &e)
void ClientApp::handleClientDisconnected() void ClientApp::handleClientDisconnected()
{ {
m_retryCount = 0; m_retryCount = 0;
LOG_IPC("disconnected from server"); LOG_DEBUG("disconnected from server");
ipcSendConnectionState(deskflow::core::ConnectionState::Disconnected);
if (!m_suspended) { if (!m_suspended) {
scheduleClientRestart(retryTime()); scheduleClientRestart(retryTime());
} }

View file

@ -18,6 +18,7 @@
#include "deskflow/ProtocolTypes.h" #include "deskflow/ProtocolTypes.h"
#include "deskflow/Screen.h" #include "deskflow/Screen.h"
#include "deskflow/ScreenException.h" #include "deskflow/ScreenException.h"
#include "deskflow/ipc/CoreIpc.h"
#include "net/SocketException.h" #include "net/SocketException.h"
#include "net/SocketMultiplexer.h" #include "net/SocketMultiplexer.h"
#include "net/TCPSocketFactory.h" #include "net/TCPSocketFactory.h"
@ -370,7 +371,8 @@ bool ServerApp::startServer()
listener->setServer(m_server); listener->setServer(m_server);
m_server->setListener(listener); m_server->setListener(listener);
m_listener = 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; m_serverState = Started;
return true; return true;
} catch (SocketAddressInUseException &e) { } catch (SocketAddressInUseException &e) {

View file

@ -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 <QMetaEnum>
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<deskflow::core::ConnectionState>();
ipcSendToClient("connectionState", metaEnum.valueToKey(static_cast<int>(state)));
}

View file

@ -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 <QString>
void ipcSendToClient(const QString &command, const QString &args = "");
void ipcSendConnectionState(deskflow::core::ConnectionState state);

View file

@ -13,9 +13,18 @@
namespace deskflow::core::ipc { namespace deskflow::core::ipc {
static CoreIpcServer *s_instance = nullptr;
CoreIpcServer::CoreIpcServer(QObject *parent) : IpcServer(parent, kCoreIpcName) 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) void CoreIpcServer::processCommand(QLocalSocket *clientSocket, const QString &command, const QStringList &parts)

View file

@ -22,6 +22,8 @@ class CoreIpcServer : public IpcServer
public: public:
explicit CoreIpcServer(QObject *parent); explicit CoreIpcServer(QObject *parent);
static CoreIpcServer &instance();
private: private:
void processCommand(QLocalSocket *clientSocket, const QString &command, const QStringList &parts) override; void processCommand(QLocalSocket *clientSocket, const QString &command, const QStringList &parts) override;
}; };

View file

@ -112,6 +112,14 @@ void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &messag
if (const auto &command = parts.at(0); command == "hello") { if (const auto &command = parts.at(0); command == "hello") {
LOG_DEBUG("ipc server got hello message, sending hello back"); LOG_DEBUG("ipc server got hello message, sending hello back");
writeToClientSocket(clientSocket, "hello"); 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") { } else if (command == "noop") {
LOG_DEBUG("ipc server got noop message"); LOG_DEBUG("ipc server got noop message");
writeToClientSocket(clientSocket, "ok"); writeToClientSocket(clientSocket, "ok");
@ -122,6 +130,23 @@ void IpcServer::processMessage(QLocalSocket *clientSocket, const QString &messag
clientSocket->flush(); 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 void IpcServer::writeToClientSocket(QLocalSocket *&clientSocket, const QString &message) const
{ {
QByteArray messageData = message.toUtf8() + '\n'; QByteArray messageData = message.toUtf8() + '\n';

View file

@ -23,6 +23,7 @@ public:
~IpcServer() override; ~IpcServer() override;
void listen(); void listen();
void broadcastCommand(const QString &command, const QString &args = "");
Q_SIGNALS: Q_SIGNALS:
void logLevelChanged(const QString &logLevel); void logLevelChanged(const QString &logLevel);
@ -52,6 +53,7 @@ private:
QLocalServer *m_server; QLocalServer *m_server;
QSet<QLocalSocket *> m_clients; QSet<QLocalSocket *> m_clients;
QString m_serverName; QString m_serverName;
QStringList m_pendingMessages;
}; };
} // namespace deskflow::core::ipc } // namespace deskflow::core::ipc

View file

@ -271,6 +271,7 @@ void MainWindow::connectSlots()
connect(&m_serverConnection, &ServerConnection::configureClient, this, &MainWindow::serverConnectionConfigureClient); connect(&m_serverConnection, &ServerConnection::configureClient, this, &MainWindow::serverConnectionConfigureClient);
connect(&m_serverConnection, &ServerConnection::clientsChanged, this, &MainWindow::serverClientsChanged); connect(&m_serverConnection, &ServerConnection::clientsChanged, this, &MainWindow::serverClientsChanged);
connect(&m_coreProcess, &CoreProcess::connectedClientsChanged, this, &MainWindow::serverClientsChanged);
connect( connect(
&m_serverConnection, &ServerConnection::requestNewClientPrompt, this, &MainWindow::handleNewClientPromptRequest &m_serverConnection, &ServerConnection::requestNewClientPrompt, this, &MainWindow::handleNewClientPromptRequest
); );

View file

@ -23,6 +23,7 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QMetaEnum>
#include <QMutexLocker> #include <QMutexLocker>
#include <QRegularExpression> #include <QRegularExpression>
@ -112,6 +113,7 @@ CoreProcess::CoreProcess(const IServerConfig &serverConfig)
connect( connect(
m_daemonIpcClient, &ipc::DaemonIpcClient::connectionFailed, this, &CoreProcess::daemonIpcClientConnectionFailed m_daemonIpcClient, &ipc::DaemonIpcClient::connectionFailed, this, &CoreProcess::daemonIpcClientConnectionFailed
); );
connect(m_daemonIpcClient, &ipc::DaemonIpcClient::logPathReceived, this, &CoreProcess::setupDaemonLogTail);
connect(&m_retryTimer, &QTimer::timeout, this, [this] { connect(&m_retryTimer, &QTimer::timeout, this, [this] {
if (m_processState == ProcessState::RetryPending) { if (m_processState == ProcessState::RetryPending) {
@ -139,20 +141,7 @@ void CoreProcess::onProcessReadyReadStandardError()
void CoreProcess::daemonIpcClientConnected() void CoreProcess::daemonIpcClientConnected()
{ {
applyLogLevel(); applyLogLevel();
m_daemonIpcClient->requestLogPath();
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);
}
} }
void CoreProcess::onProcessFinished(int exitCode, QProcess::ExitStatus) void CoreProcess::onProcessFinished(int exitCode, QProcess::ExitStatus)
@ -190,9 +179,7 @@ void CoreProcess::applyLogLevel()
const auto processMode = Settings::value(Settings::Core::ProcessMode).value<Settings::ProcessMode>(); const auto processMode = Settings::value(Settings::Core::ProcessMode).value<Settings::ProcessMode>();
if (processMode == ProcessMode::Service) { if (processMode == ProcessMode::Service) {
qDebug() << "setting daemon log level:" << Settings::logLevelText(); qDebug() << "setting daemon log level:" << Settings::logLevelText();
if (!m_daemonIpcClient->sendLogLevel(Settings::logLevelText())) { m_daemonIpcClient->sendLogLevel(Settings::logLevelText());
qWarning() << "failed to set daemon ipc log level";
}
} }
} }
@ -234,15 +221,22 @@ void CoreProcess::startProcessFromDaemon(const QStringList &args)
} }
QString commandQuoted = makeQuotedArgs(m_appPath, args); QString commandQuoted = makeQuotedArgs(m_appPath, args);
qInfo("running command: %s", qPrintable(commandQuoted)); qInfo("running command: %s", qPrintable(commandQuoted));
if (!m_daemonIpcClient->sendStartProcess(commandQuoted, Settings::value(Settings::Daemon::Elevate).toBool())) { auto sendStart = [this, commandQuoted] {
qWarning("cannot start process, ipc command failed"); m_daemonIpcClient->sendStartProcess(commandQuoted, Settings::value(Settings::Daemon::Elevate).toBool());
return; setProcessState(ProcessState::Started);
} };
setProcessState(ProcessState::Started); if (m_daemonIpcClient->isConnected()) {
sendStart();
} else {
connect(
m_daemonIpcClient, &ipc::DaemonIpcClient::connected, this, sendStart,
static_cast<Qt::ConnectionType>(Qt::SingleShotConnection | Qt::QueuedConnection)
);
m_daemonIpcClient->connectToServer();
}
} }
void CoreProcess::stopForegroundProcess() const void CoreProcess::stopForegroundProcess() const
@ -271,12 +265,20 @@ void CoreProcess::stopProcessFromDaemon()
qFatal("core process must be in stopping state"); qFatal("core process must be in stopping state");
} }
if (!m_daemonIpcClient->sendStopProcess()) { auto sendStop = [this] {
qWarning("cannot stop process, ipc command failed"); m_daemonIpcClient->sendStopProcess();
return; setProcessState(ProcessState::Stopped);
} };
setProcessState(ProcessState::Stopped); if (m_daemonIpcClient->isConnected()) {
sendStop();
} else {
connect(
m_daemonIpcClient, &ipc::DaemonIpcClient::connected, this, sendStop,
static_cast<Qt::ConnectionType>(Qt::SingleShotConnection | Qt::QueuedConnection)
);
m_daemonIpcClient->connectToServer();
}
} }
void CoreProcess::handleLogLines(const QString &text) 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")) { if (line.contains("calling TIS/TSM in non-main thread environment")) {
continue; 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 #endif
checkLogLine(line);
Q_EMIT logLine(line); Q_EMIT logLine(line);
} }
} }
@ -366,6 +382,33 @@ void CoreProcess::start(std::optional<ProcessMode> processModeOption)
qInfo().noquote() << "log file:" << logFile; 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::ConnectionType>(Qt::SingleShotConnection | Qt::QueuedConnection)
);
if (processMode == ProcessMode::Desktop) { if (processMode == ProcessMode::Desktop) {
startForegroundProcess(args); startForegroundProcess(args);
} else if (processMode == ProcessMode::Service) { } else if (processMode == ProcessMode::Service) {
@ -373,21 +416,6 @@ void CoreProcess::start(std::optional<ProcessMode> processModeOption)
startProcessFromDaemon(args); 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; m_lastProcessMode = processMode;
} }
@ -500,35 +528,27 @@ void CoreProcess::setProcessState(ProcessState state)
Q_EMIT processStateChanged(state); Q_EMIT processStateChanged(state);
} }
void CoreProcess::checkLogLine(const QString &line) void CoreProcess::onCoreIpcMessageReceived(const QString &command, const QString &args)
{ {
using enum ConnectionState; if (command == "connectionState") {
const auto metaEnum = QMetaEnum::fromType<ConnectionState>();
if (line.contains("connected to server") || line.contains("has connected")) { bool ok = false;
m_connections++; const auto state = static_cast<ConnectionState>(metaEnum.keyToValue(args.toUtf8().constData(), &ok));
setConnectionState(Connected); if (!ok) {
} else if (line.contains("started server")) { qWarning("core ipc got unknown connection state: %s", args.toUtf8().constData());
m_connections = 0; return;
setConnectionState(Listening); }
} else if (line.contains("disconnected from server") || line.contains("process exited")) { setConnectionState(state);
m_connections = 0; } else if (command == "connectedClients") {
setConnectionState(Disconnected); const auto clients = args.isEmpty() ? QStringList() : args.split(",");
} else if (line.contains("connecting to")) { Q_EMIT connectedClientsChanged(clients);
setConnectionState(Connecting); } else if (command == "secureSocket") {
} else if (line.contains("has disconnected")) { Q_EMIT secureSocket(true);
m_connections--; if (args != m_secureSocketVersion) {
if (m_connections < 1) { m_secureSocketVersion = args;
setConnectionState(Listening); 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) bool CoreProcess::checkSecureSocket(const QString &line)
@ -548,46 +568,30 @@ bool CoreProcess::checkSecureSocket(const QString &line)
return true; 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 QString CoreProcess::correctedAddress(const QString &address) const
{ {
return wrapIpv6(address.simplified()); return wrapIpv6(address.simplified());
} }
QString CoreProcess::requestDaemonLogPath() void CoreProcess::setupDaemonLogTail(const QString &logPath)
{ {
qDebug() << "requesting daemon log path"; qDebug() << "daemon log path:" << logPath;
const auto logPath = m_daemonIpcClient->requestLogPath();
if (logPath.isEmpty()) {
qCritical() << "failed to get daemon log path";
return QString();
}
if (QFileInfo logFile(logPath); !logFile.isFile()) { if (QFileInfo logFile(logPath); !logFile.isFile()) {
auto file = QFile(logPath); auto file = QFile(logPath);
if (!file.open(QFile::ReadWrite)) { if (!file.open(QFile::ReadWrite)) {
qCritical() << "daemon log path file can not be written:" << logPath; qCritical() << "daemon log path file can not be written:" << logPath;
return QString(); return;
} }
file.write(""); // Create an empty file 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() void CoreProcess::clearSettings()
@ -608,9 +612,7 @@ void CoreProcess::clearSettings()
void CoreProcess::retryDaemon() void CoreProcess::retryDaemon()
{ {
if (m_daemonIpcClient->connectToServer()) { m_daemonIpcClient->connectToServer();
qInfo("successfully reconnected to daemon");
}
} }
} // namespace deskflow::gui } // namespace deskflow::gui

View file

@ -22,7 +22,7 @@ namespace deskflow::gui {
namespace ipc { namespace ipc {
class CoreIpcClient; class CoreIpcClient;
class DaemonIpcClient; class DaemonIpcClient;
} } // namespace ipc
class CoreProcess : public QObject class CoreProcess : public QObject
{ {
@ -89,12 +89,14 @@ Q_SIGNALS:
void processStateChanged(deskflow::core::ProcessState state); void processStateChanged(deskflow::core::ProcessState state);
void secureSocket(bool enabled); void secureSocket(bool enabled);
void daemonIpcClientConnectionFailed(); void daemonIpcClientConnectionFailed();
void connectedClientsChanged(const QStringList &clients);
void securityLevelChanged(QString securityLevel); void securityLevelChanged(QString securityLevel);
private Q_SLOTS: private Q_SLOTS:
void onProcessFinished(int exitCode, QProcess::ExitStatus); void onProcessFinished(int exitCode, QProcess::ExitStatus);
void onProcessReadyReadStandardOutput(); void onProcessReadyReadStandardOutput();
void onProcessReadyReadStandardError(); void onProcessReadyReadStandardError();
void onCoreIpcMessageReceived(const QString &command, const QString &args);
void daemonIpcClientConnected(); void daemonIpcClientConnected();
private: private:
@ -105,20 +107,15 @@ private:
QPair<bool, QString> persistServerConfig() const; QPair<bool, QString> persistServerConfig() const;
void setConnectionState(ConnectionState state); void setConnectionState(ConnectionState state);
void setProcessState(ProcessState state); void setProcessState(ProcessState state);
void checkLogLine(const QString &line);
bool checkSecureSocket(const QString &line); bool checkSecureSocket(const QString &line);
void handleLogLines(const QString &text); void handleLogLines(const QString &text);
QString correctedAddress(const QString &address) const; QString correctedAddress(const QString &address) const;
QString requestDaemonLogPath(); void setupDaemonLogTail(const QString &logPath);
static QString makeQuotedArgs(const QString &app, const QStringList &args); static QString makeQuotedArgs(const QString &app, const QStringList &args);
static QString processModeToString(const Settings::ProcessMode mode); static QString processModeToString(const Settings::ProcessMode mode);
static QString processStateToString(const CoreProcess::ProcessState state); static QString processStateToString(const CoreProcess::ProcessState state);
static QString wrapIpv6(const QString &address); static QString wrapIpv6(const QString &address);
#ifdef Q_OS_MACOS
void checkOSXNotification(const QString &line);
#endif
const IServerConfig &m_serverConfig; const IServerConfig &m_serverConfig;
QString m_address; QString m_address;
ProcessState m_processState = ProcessState::Stopped; ProcessState m_processState = ProcessState::Stopped;
@ -128,7 +125,6 @@ private:
QString m_secureSocketVersion; QString m_secureSocketVersion;
std::optional<ProcessMode> m_lastProcessMode = std::nullopt; std::optional<ProcessMode> m_lastProcessMode = std::nullopt;
QTimer m_retryTimer; QTimer m_retryTimer;
int m_connections = 0;
deskflow::gui::ipc::CoreIpcClient *m_coreIpcClient = nullptr; deskflow::gui::ipc::CoreIpcClient *m_coreIpcClient = nullptr;
deskflow::gui::ipc::DaemonIpcClient *m_daemonIpcClient = nullptr; deskflow::gui::ipc::DaemonIpcClient *m_daemonIpcClient = nullptr;
FileTail *m_daemonFileTail = nullptr; FileTail *m_daemonFileTail = nullptr;

View file

@ -15,11 +15,15 @@
namespace deskflow::gui::ipc { namespace deskflow::gui::ipc {
const auto kTimeout = 1000;
CoreIpcClient::CoreIpcClient(QObject *parent) : IpcClient(parent, kCoreIpcName) CoreIpcClient::CoreIpcClient(QObject *parent) : IpcClient(parent, kCoreIpcName)
{ {
// do nothing // 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 } // namespace deskflow::gui::ipc

View file

@ -18,6 +18,12 @@ class CoreIpcClient : public IpcClient
public: public:
explicit CoreIpcClient(QObject *parent = nullptr); 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 } // namespace deskflow::gui::ipc

View file

@ -9,93 +9,46 @@
#include "common/Constants.h" #include "common/Constants.h"
#include <QDebug> #include <QDebug>
#include <QLocalSocket>
#include <QMutexLocker>
#include <QObject>
#include <QString>
namespace deskflow::gui::ipc { namespace deskflow::gui::ipc {
const auto kTimeout = 1000;
DaemonIpcClient::DaemonIpcClient(QObject *parent) : IpcClient(parent, kDaemonIpcName) 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); sendMessage("logLevel=" + logLevel);
return true;
} }
bool DaemonIpcClient::sendStartProcess(const QString &command, bool elevate) void DaemonIpcClient::sendStartProcess(const QString &command, bool elevate)
{ {
if (!keepAlive()) const auto elevateStr = elevate ? QStringLiteral("yes") : QStringLiteral("no");
return false; sendMessage("elevate=" + elevateStr);
sendMessage("command=" + command);
if (!sendMessage("elevate=" + (elevate ? QStringLiteral("yes") : QStringLiteral("no")))) { sendMessage("start");
return false;
}
if (!sendMessage("command=" + command)) {
return false;
}
return sendMessage("start");
} }
bool DaemonIpcClient::sendStopProcess() void DaemonIpcClient::sendStopProcess()
{ {
return sendMessage("stop"); sendMessage("stop");
} }
QString DaemonIpcClient::requestLogPath() void DaemonIpcClient::sendClearSettings()
{ {
if (!keepAlive()) sendMessage("clearSettings");
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];
} }
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 } // namespace deskflow::gui::ipc

View file

@ -18,11 +18,17 @@ class DaemonIpcClient : public IpcClient
public: public:
explicit DaemonIpcClient(QObject *parent = nullptr); explicit DaemonIpcClient(QObject *parent = nullptr);
bool sendLogLevel(const QString &logLevel); void sendLogLevel(const QString &logLevel);
bool sendStartProcess(const QString &command, bool elevate); void sendStartProcess(const QString &command, bool elevate);
bool sendStopProcess(); void sendStopProcess();
bool sendClearSettings(); void sendClearSettings();
QString requestLogPath(); void requestLogPath();
Q_SIGNALS:
void logPathReceived(const QString &logPath);
protected:
void processCommand(const QString &command, const QStringList &parts) override;
}; };
} // namespace deskflow::gui::ipc } // namespace deskflow::gui::ipc

View file

@ -8,9 +8,7 @@
#include <QDebug> #include <QDebug>
#include <QLocalSocket> #include <QLocalSocket>
#include <QMutexLocker> #include <QTimer>
#include <QObject>
#include <QString>
namespace deskflow::gui::ipc { 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::disconnected, this, &IpcClient::handleDisconnected);
connect(m_socket, &QLocalSocket::errorOccurred, this, &IpcClient::handleErrorOccurred); 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) { if (m_state == State::Connecting) {
qWarning() << "ipc client already connecting to server"; qWarning() << "ipc client already connecting to server";
return false; return;
} }
if (m_state != State::Unconnected) { if (m_state != State::Unconnected) {
@ -43,139 +42,129 @@ bool IpcClient::connectToServer()
disconnectFromServer(); disconnectFromServer();
} }
for (int i = 0; i < kRetryLimit; ++i) { m_retryCount = 0;
if (i == 0) { attemptConnection();
qDebug() << "ipc client connecting to server:" << m_socketName; }
} else {
qDebug() << "ipc client retrying connection, attempt:" << i + 1;
}
m_state = State::Connecting; void IpcClient::attemptConnection()
m_socket->connectToServer(m_socketName); {
if (m_retryCount >= kRetryLimit) {
if (!m_socket->waitForConnected(kTimeout)) { qWarning() << "ipc client failed to connect after" << kRetryLimit << "attempts";
qWarning() << "ipc client failed to connect"; m_state = State::Unconnected;
disconnectFromServer(); Q_EMIT connectionFailed();
continue; return;
}
if (!sendMessage("hello", "hello", false)) {
qWarning() << "ipc client failed to send hello";
disconnectFromServer();
continue;
}
m_state = State::Connected;
qDebug() << "ipc client connected";
Q_EMIT connected();
return true;
} }
qWarning() << "ipc client failed to connect after" << kRetryLimit << "attempts"; if (m_retryCount == 0) {
disconnectFromServer(); qDebug() << "ipc client connecting to server:" << m_socketName;
Q_EMIT connectionFailed(); } else {
return false; 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() void IpcClient::disconnectFromServer()
{ {
QMutexLocker locker(&m_mutex);
m_state = State::Disconnecting; m_state = State::Disconnecting;
qDebug() << "ipc client disconnecting from server"; qDebug() << "ipc client disconnecting from server";
m_socket->disconnectFromServer(); 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; m_state = State::Unconnected;
} }
void IpcClient::handleDisconnected() void IpcClient::handleDisconnected()
{ {
qDebug() << "ipc client disconnected from server"; if (m_state == State::Connecting) {
if (m_state == State::Connected) { return;
Q_EMIT connectionFailed();
} }
qDebug() << "ipc client disconnected from server";
const auto wasConnected = m_state == State::Connected;
m_state = State::Unconnected; m_state = State::Unconnected;
if (wasConnected) {
Q_EMIT connectionFailed();
}
} }
void IpcClient::handleErrorOccurred() void IpcClient::handleErrorOccurred()
{ {
if (m_state == State::Connecting) {
return;
}
qWarning() << "ipc client error:" << m_socket->errorString(); qWarning() << "ipc client error:" << m_socket->errorString();
disconnectFromServer();
if (m_state == State::Connected) { if (m_state == State::Connected) {
disconnectFromServer();
Q_EMIT connectionFailed(); 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()) { while (data.contains('\n')) {
qWarning() << "cannot send command, ipc client not connected"; const auto index = data.indexOf('\n');
return false; 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"; if (!data.isEmpty()) {
m_socket->write(messageData); m_readBuffer = data;
if (!m_socket->waitForBytesWritten(kTimeout)) {
qWarning() << "ipc client failed to write command";
return false;
} }
if (!expectAck.isEmpty()) {
qDebug() << "ipc client waiting for ack:" << expectAck;
if (!m_socket->waitForReadyRead(kTimeout)) {
qWarning() << "ipc client socket ready read timed out";
return false;
}
QByteArray response = m_socket->readAll();
if (response.isEmpty()) {
qWarning() << "ipc client got empty response";
return false;
}
QString responseData = QString::fromUtf8(response);
if (responseData.isEmpty()) {
qWarning() << "ipc client failed to convert response to string";
return false;
}
if (responseData != expectAck + "\n") {
qWarning() << "ipc client got unexpected response:" << responseData;
return false;
}
}
qDebug() << "ipc client sent message:" << messageData;
return true;
} }
bool IpcClient::keepAlive() void IpcClient::sendMessage(const QString &message)
{ {
if (!isConnected() && !connectToServer()) { if (m_state != State::Connected) {
qWarning() << "ipc client keep alive failed to connect"; qWarning() << "cannot send command, ipc client not connected";
return false; return;
} }
if (!sendMessage("noop")) { m_socket->write(message.toUtf8() + "\n");
qWarning() << "ipc client keep alive ping failed, reconnecting"; qDebug() << "ipc client sent message:" << message;
connectToServer();
return false;
}
return true;
} }
} // namespace deskflow::gui::ipc } // namespace deskflow::gui::ipc

View file

@ -6,7 +6,6 @@
#pragma once #pragma once
#include <QMutex>
#include <QObject> #include <QObject>
class QLocalSocket; class QLocalSocket;
@ -28,7 +27,7 @@ class IpcClient : public QObject
public: public:
explicit IpcClient(QObject *parent, const QString &socketName); explicit IpcClient(QObject *parent, const QString &socketName);
bool connectToServer(); void connectToServer();
void disconnectFromServer(); void disconnectFromServer();
bool isConnected() const bool isConnected() const
@ -43,21 +42,25 @@ Q_SIGNALS:
private Q_SLOTS: private Q_SLOTS:
void handleDisconnected(); void handleDisconnected();
void handleErrorOccurred(); void handleErrorOccurred();
void handleReadyRead();
protected: protected:
bool keepAlive(); virtual void processCommand(const QString &command, const QStringList &parts)
bool sendMessage(const QString &message, const QString &expectAck = "ok", const bool expectConnected = true);
QLocalSocket *socket() const
{ {
return m_socket; Q_UNUSED(command)
Q_UNUSED(parts)
} }
void sendMessage(const QString &message);
private: private:
void attemptConnection();
QLocalSocket *m_socket; QLocalSocket *m_socket;
State m_state{State::Unconnected}; State m_state{State::Unconnected};
QString m_socketName; QString m_socketName;
QMutex m_mutex; QByteArray m_readBuffer;
int m_retryCount{0};
}; };
} // namespace deskflow::gui::ipc } // namespace deskflow::gui::ipc

View file

@ -12,6 +12,7 @@
#include "base/IEventQueue.h" #include "base/IEventQueue.h"
#include "base/Log.h" #include "base/Log.h"
#include "common/Settings.h" #include "common/Settings.h"
#include "deskflow/ipc/CoreIpc.h"
#include "mt/Lock.h" #include "mt/Lock.h"
#include "net/FingerprintDatabase.h" #include "net/FingerprintDatabase.h"
#include "net/TCPSocket.h" #include "net/TCPSocket.h"
@ -625,8 +626,9 @@ bool SecureSocket::verifyCertFingerprint(const QString &FingerprintDatabasePath)
if (!sha256.isValid()) if (!sha256.isValid())
return false; return false;
// Gui Must Parse this line, DO NOT CHANGE const auto fingerprint = deskflow::formatSSLFingerprint(sha256.data, false);
LOG_IPC("peer fingerprint: %s", qPrintable(deskflow::formatSSLFingerprint(sha256.data, false))); LOG_DEBUG("peer fingerprint: %s", qPrintable(fingerprint));
ipcSendToClient("peerFingerprint", fingerprint);
QFile file(FingerprintDatabasePath); QFile file(FingerprintDatabasePath);

View file

@ -9,6 +9,7 @@
#include <sstream> #include <sstream>
#include <base/Log.h> #include <base/Log.h>
#include <deskflow/ipc/CoreIpc.h>
#include <openssl/err.h> #include <openssl/err.h>
#include <openssl/ssl.h> #include <openssl/ssl.h>
@ -105,13 +106,12 @@ void SslLogger::logSecureConnectInfo(const SSL *ssl)
std::istream_iterator<std::string>{iss}, std::istream_iterator<std::string>{} std::istream_iterator<std::string>{iss}, std::istream_iterator<std::string>{}
}; };
if (parts.size() > 2) { if (parts.size() > 2) {
// log the section containing the protocol version LOG_DEBUG("network encryption protocol: %s", parts[1].c_str());
LOG_INFO("network encryption protocol: %s", parts[1].c_str()); ipcSendToClient("secureSocket", parts[1].c_str());
} else { } else {
// log the error in spliting then display the whole description rather
// then nothing
LOG_ERR("could not split cipher for protocol"); 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 { } else {
LOG_ERR("could not get secure socket cipher"); LOG_ERR("could not get secure socket cipher");

View file

@ -185,7 +185,7 @@ bool ClientProxy1_0::parseMessage(const uint8_t *code)
void ClientProxy1_0::handleDisconnect() void ClientProxy1_0::handleDisconnect()
{ {
LOG_IPC("client \"%s\" has disconnected", getName().c_str()); LOG_DEBUG("client \"%s\" has disconnected", getName().c_str());
disconnect(); disconnect();
} }
@ -198,7 +198,7 @@ void ClientProxy1_0::handleWriteError()
void ClientProxy1_0::handleFlatline() void ClientProxy1_0::handleFlatline()
{ {
// didn't get a heartbeat fast enough. assume client is dead. // 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(); disconnect();
} }

View file

@ -11,6 +11,7 @@
#include "base/IEventQueue.h" #include "base/IEventQueue.h"
#include "base/Log.h" #include "base/Log.h"
#include "deskflow/AppUtil.h" #include "deskflow/AppUtil.h"
#include "deskflow/ipc/CoreIpc.h"
#include "deskflow/DeskflowException.h" #include "deskflow/DeskflowException.h"
#include "deskflow/IPlatformScreen.h" #include "deskflow/IPlatformScreen.h"
#include "deskflow/OptionTypes.h" #include "deskflow/OptionTypes.h"
@ -233,7 +234,7 @@ void Server::adoptClient(BaseClientProxy *client)
// name must be in our configuration // name must be in our configuration
if (!m_config->isScreen(client->getName())) { 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); closeClient(client, kMsgEUnknown);
return; return;
} }
@ -245,7 +246,9 @@ void Server::adoptClient(BaseClientProxy *client)
closeClient(client, kMsgEBusy); closeClient(client, kMsgEBusy);
return; 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 // send configuration options to client
sendOptions(client); sendOptions(client);
@ -291,6 +294,18 @@ void Server::getClients(std::vector<std::string> &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 Server::getName(const BaseClientProxy *client) const
{ {
std::string name = m_config->getCanonicalName(client->getName()); std::string name = m_config->getCanonicalName(client->getName());
@ -1285,6 +1300,11 @@ void Server::handleClientDisconnected(BaseClientProxy *client)
removeActiveClient(client); removeActiveClient(client);
removeOldClient(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; delete client;
} }

View file

@ -197,6 +197,7 @@ public:
Set the \c list to the names of the currently connected clients. Set the \c list to the names of the currently connected clients.
*/ */
void getClients(std::vector<std::string> &list) const; void getClients(std::vector<std::string> &list) const;
void sendConnectedClientsIpc() const;
//@} //@}