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
// 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__))

View file

@ -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

View file

@ -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
)

View file

@ -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);
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());
}

View file

@ -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) {

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 {
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)

View file

@ -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;
};

View file

@ -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';

View file

@ -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<QLocalSocket *> m_clients;
QString m_serverName;
QStringList m_pendingMessages;
};
} // 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::clientsChanged, this, &MainWindow::serverClientsChanged);
connect(&m_coreProcess, &CoreProcess::connectedClientsChanged, this, &MainWindow::serverClientsChanged);
connect(
&m_serverConnection, &ServerConnection::requestNewClientPrompt, this, &MainWindow::handleNewClientPromptRequest
);

View file

@ -23,6 +23,7 @@
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QMetaEnum>
#include <QMutexLocker>
#include <QRegularExpression>
@ -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<Settings::ProcessMode>();
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);
};
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
@ -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);
};
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)
@ -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<ProcessMode> 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::ConnectionType>(Qt::SingleShotConnection | Qt::QueuedConnection)
);
if (processMode == ProcessMode::Desktop) {
startForegroundProcess(args);
} else if (processMode == ProcessMode::Service) {
@ -373,21 +416,6 @@ void CoreProcess::start(std::optional<ProcessMode> 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<ConnectionState>();
bool ok = false;
const auto state = static_cast<ConnectionState>(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

View file

@ -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<bool, QString> 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<ProcessMode> 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;

View file

@ -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

View file

@ -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

View file

@ -9,93 +9,46 @@
#include "common/Constants.h"
#include <QDebug>
#include <QLocalSocket>
#include <QMutexLocker>
#include <QObject>
#include <QString>
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;
const auto elevateStr = elevate ? QStringLiteral("yes") : QStringLiteral("no");
sendMessage("elevate=" + elevateStr);
sendMessage("command=" + command);
sendMessage("start");
}
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())
return QString();
if (!sendMessage("logPath", QString())) {
return QString();
sendMessage("clearSettings");
}
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

View file

@ -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

View file

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

View file

@ -6,7 +6,6 @@
#pragma once
#include <QMutex>
#include <QObject>
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

View file

@ -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);

View file

@ -9,6 +9,7 @@
#include <sstream>
#include <base/Log.h>
#include <deskflow/ipc/CoreIpc.h>
#include <openssl/err.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>{}
};
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");

View file

@ -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();
}

View file

@ -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<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 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;
}

View file

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