feat: Tail daemon log file instead of using IPC log outputter

This commit is contained in:
Nick Bolton
2025-02-10 12:58:23 +00:00
parent 5980fb741b
commit 5733541b2a
11 changed files with 202 additions and 39 deletions

View File

@ -27,6 +27,8 @@
#include <QCoreApplication>
#include <QThread>
using namespace deskflow::core;
int main(int argc, char **argv)
{
#if SYSAPI_WIN32
@ -51,13 +53,11 @@ int main(int argc, char **argv)
using enum DaemonApp::InitResult;
case StartDaemon: {
using namespace deskflow::core;
QCoreApplication app(argc, argv);
// Thread must be heap-allocated for deferred deletion on thread exit.
// Avoid setting Qt ownership to prevent premature deletion (thread may run longer than Qt loop).
auto *pDaemonThread = new QThread(); // NOSONAR
auto *pDaemonThread = new QThread(); // NOSONAR - Qt memory
pDaemon->moveToThread(pDaemonThread);
QObject::connect(pDaemonThread, &QThread::started, pDaemon, &DaemonApp::run);
@ -66,7 +66,7 @@ int main(int argc, char **argv)
QObject::connect(pDaemonThread, &QThread::finished, QCoreApplication::instance(), &QCoreApplication::quit);
// The daemon app is on it's own thread which doesn't have a Qt event loop, so we need to use direct connection.
auto *ipcServer = new ipc::DaemonIpcServer(&app); // NOSONAR
auto *ipcServer = new ipc::DaemonIpcServer(&app, QString::fromStdString(pDaemon->logFilename())); // NOSONAR
QObject::connect(
ipcServer, &ipc::DaemonIpcServer::logLevelChanged, pDaemon, &DaemonApp::saveLogLevel, //
Qt::DirectConnection

View File

@ -57,6 +57,9 @@ public:
void applyWatchdogCommand() const;
void clearWatchdogCommand();
// Getters
std::string logFilename();
static DaemonApp &instance()
{
static DaemonApp instance; // NOSONAR - Meyers' Singleton
@ -69,7 +72,6 @@ private:
void daemonize();
void handleError(const char *message);
std::string logFilename();
void handleIpcMessage(const Event &e, void *);
#if SYSAPI_WIN32

View File

@ -17,9 +17,10 @@ namespace deskflow::core::ipc {
const auto kAckMessage = "ok\n";
const auto kErrorMessage = "error\n";
DaemonIpcServer::DaemonIpcServer(QObject *parent)
: QObject(parent), //
m_server{new QLocalServer(this)} // NOSONAR
DaemonIpcServer::DaemonIpcServer(QObject *parent, const QString &logFilename)
: QObject(parent),
m_logFilename(logFilename),
m_server{new QLocalServer(this)} // NOSONAR - Qt memory
{
// Daemon runs as system, but GUI runs as regular user, so we need to allow world access.
m_server->setSocketOptions(QLocalServer::WorldAccessOption);
@ -108,7 +109,7 @@ void DaemonIpcServer::processMessage(QLocalSocket *clientSocket, const QString &
}
const auto &command = parts[0];
if (command == "hello") { // NOSONAR
if (command == "hello") { // NOSONAR - if-init is confusing here
clientSocket->write("hello\n");
} else if (command == "noop") {
clientSocket->write(kAckMessage);
@ -128,7 +129,7 @@ void DaemonIpcServer::processMessage(QLocalSocket *clientSocket, const QString &
clientSocket->write(kAckMessage);
} else if (command == "logPath") {
LOG_DEBUG("ipc server got log path request");
// TODO: send log path
clientSocket->write("logPath=" + m_logFilename.toUtf8());
} else {
LOG_WARN("ipc server got unknown message: %s", message.toUtf8().constData());
}

View File

@ -19,7 +19,7 @@ class DaemonIpcServer : public QObject
Q_OBJECT
public:
explicit DaemonIpcServer(QObject *parent);
explicit DaemonIpcServer(QObject *parent, const QString &logFilename);
~DaemonIpcServer() override;
signals:
@ -42,6 +42,7 @@ private slots:
void handleErrorOccurred();
private:
const QString m_logFilename;
QLocalServer *m_server;
QSet<QLocalSocket *> m_clients;
};

View File

@ -25,6 +25,8 @@ add_library(${target} STATIC
dotenv.cpp
dotenv.h
env_vars.h
FileTail.cpp
FileTail.h
Logger.cpp
Logger.h
messages.cpp

45
src/lib/gui/FileTail.cpp Normal file
View File

@ -0,0 +1,45 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Symless Ltd.
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "FileTail.h"
#include <QDebug>
#include <QFile>
#include <QFileSystemWatcher>
#include <QObject>
#include <QTextStream>
namespace deskflow::gui {
FileTail::FileTail(const QString &filePath, QObject *parent)
: QObject(parent),
m_file(filePath),
m_watcher(new QFileSystemWatcher(this))
{
if (!m_file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qCritical() << "failed to open file for tail:" << filePath;
return;
}
qDebug() << "starting file tail:" << filePath;
m_watcher->addPath(filePath);
m_lastPos = m_file.size();
connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &FileTail::handleFileChanged);
}
void FileTail::handleFileChanged(const QString &)
{
m_file.seek(m_lastPos);
QTextStream stream(&m_file);
while (!stream.atEnd()) {
QString line = stream.readLine();
Q_EMIT newLine(line);
}
m_lastPos = m_file.pos();
}
} // namespace deskflow::gui

35
src/lib/gui/FileTail.h Normal file
View File

@ -0,0 +1,35 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2025 Symless Ltd.
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include <QFile>
#include <QObject>
class QFileSystemWatcher;
namespace deskflow::gui {
class FileTail : public QObject
{
Q_OBJECT
public:
FileTail(const QString &filePath, QObject *parent = nullptr);
signals:
void newLine(const QString &line);
private slots:
void handleFileChanged(const QString &);
private:
QFile m_file;
QFileSystemWatcher *m_watcher = nullptr;
qint64 m_lastPos;
};
} // namespace deskflow::gui

View File

@ -1,6 +1,6 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2024 Symless Ltd.
* SPDX-FileCopyrightText: (C) 2024 - 2025 Symless Ltd.
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
@ -158,6 +158,15 @@ CoreProcess::CoreProcess(const IAppConfig &appConfig, const IServerConfig &serve
m_pDeps(deps),
m_daemonIpcClient{new ipc::DaemonIpcClient(this)}
{
if (m_appConfig.processMode() == ProcessMode::kService) {
const auto logPath = requestDaemonLogPath();
if (!logPath.isEmpty()) {
qInfo() << "daemon log path:" << logPath;
m_daemonFileTail = new FileTail(logPath, this);
connect(m_daemonFileTail, &FileTail::newLine, this, &CoreProcess::handleLogLines);
}
}
connect(&m_pDeps->process(), &QProcessProxy::finished, this, &CoreProcess::onProcessFinished);
connect(
@ -743,4 +752,21 @@ QString CoreProcess::correctedAddress() const
return wrapIpv6(m_address);
}
QString CoreProcess::requestDaemonLogPath()
{
qDebug() << "requesting daemon log path";
const auto logPath = m_daemonIpcClient->requestLogPath();
if (logPath.isEmpty()) {
qCritical() << "failed to get daemon log path";
return QString();
}
if (QFileInfo logFile(logPath); !logFile.exists() || !logFile.isFile()) {
qWarning() << "daemon log path file does not exist:" << logPath;
return QString();
}
return logPath;
}
} // namespace deskflow::gui

View File

@ -1,22 +1,25 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2024 Symless Ltd.
* SPDX-FileCopyrightText: (C) 2024 - 2025 Symless Ltd.
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include "gui/FileTail.h"
#include "gui/config/IAppConfig.h"
#include "gui/config/IServerConfig.h"
#include "gui/ipc/QIpcClient.h"
#include "gui/proxy/QProcessProxy.h"
#include <memory>
#include <QFileSystemWatcher>
#include <QMutex>
#include <QObject>
#include <QString>
#include <QStringList>
#include <QTimer>
#include <memory>
namespace deskflow::gui {
@ -158,6 +161,7 @@ private:
void handleLogLines(const QString &text);
QString correctedInterface() const;
QString correctedAddress() const;
QString requestDaemonLogPath();
#ifdef Q_OS_MAC
void checkOSXNotification(const QString &line);
@ -176,6 +180,7 @@ private:
QTimer m_retryTimer;
int m_connections = 0;
deskflow::gui::ipc::DaemonIpcClient *m_daemonIpcClient = nullptr;
FileTail *m_daemonFileTail = nullptr;
};
} // namespace deskflow::gui

View File

@ -18,8 +18,8 @@ namespace deskflow::gui::ipc {
const auto kTimeout = 1000;
DaemonIpcClient::DaemonIpcClient(QObject *parent)
: QObject(parent), //
m_socket{new QLocalSocket(this)} // NOSONAR
: QObject(parent),
m_socket{new QLocalSocket(this)} // NOSONAR - Qt memory
{
}
@ -58,15 +58,59 @@ void DaemonIpcClient::handleErrorOccurred()
m_connected = false;
}
bool DaemonIpcClient::sendMessage(const QString &message, const QString &expectAck, const bool expectConnected)
{
if (expectConnected && !m_connected) {
qWarning() << "cannot send command, ipc not connected";
return false;
}
QByteArray messageData = message.toUtf8() + "\n";
m_socket->write(messageData);
if (!m_socket->waitForBytesWritten(kTimeout)) {
qWarning() << "ipc failed to write command";
return false;
}
if (!expectAck.isEmpty()) {
qDebug() << "ipc waiting for ack: " << expectAck;
if (!m_socket->waitForReadyRead(kTimeout)) {
qWarning() << "ipc failed to read response";
return false;
}
QByteArray response = m_socket->readAll();
if (response.isEmpty()) {
qWarning() << "ipc got empty response";
return false;
}
QString responseData = QString::fromUtf8(response);
if (responseData.isEmpty()) {
qWarning() << "ipc failed to convert response to string";
return false;
}
if (responseData != expectAck + "\n") {
qWarning() << "ipc got unexpected response: " << responseData;
return false;
}
}
qDebug() << "ipc sent message: " << messageData;
return true;
}
bool DaemonIpcClient::keepAlive()
{
if (!isConnected() && !connectToServer()) {
qWarning() << "ipc client keep alive failed to connect";
qWarning() << "ipc keep alive failed to connect";
return false;
}
if (!sendMessage("noop")) {
qWarning() << "ipc client keep alive ping failed";
qWarning() << "ipc keep alive ping failed";
m_connected = false;
return false;
}
@ -104,44 +148,45 @@ bool DaemonIpcClient::sendStopProcess()
return sendMessage("stop");
}
bool DaemonIpcClient::sendMessage(const QString &message, const QString &expectAck, const bool expectConnected)
QString DaemonIpcClient::requestLogPath()
{
if (expectConnected && !m_connected) {
qWarning() << "cannot send command, ipc not connected";
return false;
}
if (!keepAlive())
return QString();
QByteArray messageData = message.toUtf8() + "\n";
m_socket->write(messageData);
if (!m_socket->waitForBytesWritten(kTimeout)) {
qWarning() << "ipc client failed to write command";
return false;
if (!sendMessage("logPath", QString())) {
return QString();
}
if (!m_socket->waitForReadyRead(kTimeout)) {
qWarning() << "ipc client failed to read response";
return false;
qWarning() << "ipc failed to read log path response";
return QString();
}
QByteArray response = m_socket->readAll();
if (response.isEmpty()) {
qWarning() << "ipc client got empty response";
return false;
qWarning() << "ipc got empty log path response";
return QString();
}
QString responseData = QString::fromUtf8(response);
if (responseData.isEmpty()) {
qWarning() << "ipc client failed to convert response to string";
return false;
qWarning() << "ipc failed to convert log path response to string";
return QString();
}
if (responseData != expectAck + "\n") {
qWarning() << "ipc client got unexpected response: " << responseData;
return false;
// Trimming removes newline from end of message.
QStringList parts = responseData.trimmed().split("=");
if (parts.size() != 2) {
qWarning() << "ipc got invalid log path response: " << responseData;
return QString();
}
qDebug() << "ipc client sent message: " << messageData;
return true;
if (parts[0] != "logPath") {
qWarning() << "ipc got unexpected log path response: " << responseData;
return QString();
}
return parts[1];
}
} // namespace deskflow::gui::ipc

View File

@ -24,6 +24,7 @@ public:
bool sendLogLevel(const QString &logLevel);
bool sendStartProcess(const QString &command, ElevateMode elevateMode);
bool sendStopProcess();
QString requestLogPath();
bool isConnected() const
{