feat: Tail daemon log file instead of using IPC log outputter
This commit is contained in:
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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
45
src/lib/gui/FileTail.cpp
Normal 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
35
src/lib/gui/FileTail.h
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -24,6 +24,7 @@ public:
|
||||
bool sendLogLevel(const QString &logLevel);
|
||||
bool sendStartProcess(const QString &command, ElevateMode elevateMode);
|
||||
bool sendStopProcess();
|
||||
QString requestLogPath();
|
||||
|
||||
bool isConnected() const
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user