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 <QCoreApplication>
#include <QThread> #include <QThread>
using namespace deskflow::core;
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
#if SYSAPI_WIN32 #if SYSAPI_WIN32
@ -51,13 +53,11 @@ int main(int argc, char **argv)
using enum DaemonApp::InitResult; using enum DaemonApp::InitResult;
case StartDaemon: { case StartDaemon: {
using namespace deskflow::core;
QCoreApplication app(argc, argv); QCoreApplication app(argc, argv);
// Thread must be heap-allocated for deferred deletion on thread exit. // 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). // 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); pDaemon->moveToThread(pDaemonThread);
QObject::connect(pDaemonThread, &QThread::started, pDaemon, &DaemonApp::run); 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); 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. // 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( QObject::connect(
ipcServer, &ipc::DaemonIpcServer::logLevelChanged, pDaemon, &DaemonApp::saveLogLevel, // ipcServer, &ipc::DaemonIpcServer::logLevelChanged, pDaemon, &DaemonApp::saveLogLevel, //
Qt::DirectConnection Qt::DirectConnection

View file

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

View file

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

View file

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

View file

@ -25,6 +25,8 @@ add_library(${target} STATIC
dotenv.cpp dotenv.cpp
dotenv.h dotenv.h
env_vars.h env_vars.h
FileTail.cpp
FileTail.h
Logger.cpp Logger.cpp
Logger.h Logger.h
messages.cpp 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 * 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 * 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_pDeps(deps),
m_daemonIpcClient{new ipc::DaemonIpcClient(this)} 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(&m_pDeps->process(), &QProcessProxy::finished, this, &CoreProcess::onProcessFinished);
connect( connect(
@ -743,4 +752,21 @@ QString CoreProcess::correctedAddress() const
return wrapIpv6(m_address); 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 } // namespace deskflow::gui

View file

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

View file

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

View file

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