diff --git a/src/apps/deskflow-daemon/deskflow-daemon.cpp b/src/apps/deskflow-daemon/deskflow-daemon.cpp index 862fe01dc..a840cb053 100644 --- a/src/apps/deskflow-daemon/deskflow-daemon.cpp +++ b/src/apps/deskflow-daemon/deskflow-daemon.cpp @@ -27,6 +27,8 @@ #include #include +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 diff --git a/src/lib/deskflow/DaemonApp.h b/src/lib/deskflow/DaemonApp.h index fd25bb7e5..f64687418 100644 --- a/src/lib/deskflow/DaemonApp.h +++ b/src/lib/deskflow/DaemonApp.h @@ -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 diff --git a/src/lib/deskflow/ipc/DaemonIpcServer.cpp b/src/lib/deskflow/ipc/DaemonIpcServer.cpp index 26814e484..effaa13ed 100644 --- a/src/lib/deskflow/ipc/DaemonIpcServer.cpp +++ b/src/lib/deskflow/ipc/DaemonIpcServer.cpp @@ -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()); } diff --git a/src/lib/deskflow/ipc/DaemonIpcServer.h b/src/lib/deskflow/ipc/DaemonIpcServer.h index 7a35452f6..78b65cb88 100644 --- a/src/lib/deskflow/ipc/DaemonIpcServer.h +++ b/src/lib/deskflow/ipc/DaemonIpcServer.h @@ -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 m_clients; }; diff --git a/src/lib/gui/CMakeLists.txt b/src/lib/gui/CMakeLists.txt index c848eec8b..f9e6a649e 100644 --- a/src/lib/gui/CMakeLists.txt +++ b/src/lib/gui/CMakeLists.txt @@ -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 diff --git a/src/lib/gui/FileTail.cpp b/src/lib/gui/FileTail.cpp new file mode 100644 index 000000000..68d1e6d17 --- /dev/null +++ b/src/lib/gui/FileTail.cpp @@ -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 +#include +#include +#include +#include + +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 diff --git a/src/lib/gui/FileTail.h b/src/lib/gui/FileTail.h new file mode 100644 index 000000000..7cd51fc8e --- /dev/null +++ b/src/lib/gui/FileTail.h @@ -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 +#include + +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 diff --git a/src/lib/gui/core/CoreProcess.cpp b/src/lib/gui/core/CoreProcess.cpp index 3d49f20a6..7ff009c5d 100644 --- a/src/lib/gui/core/CoreProcess.cpp +++ b/src/lib/gui/core/CoreProcess.cpp @@ -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 diff --git a/src/lib/gui/core/CoreProcess.h b/src/lib/gui/core/CoreProcess.h index 769a6f86a..6372eb9ba 100644 --- a/src/lib/gui/core/CoreProcess.h +++ b/src/lib/gui/core/CoreProcess.h @@ -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 + +#include #include #include #include #include #include -#include 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 diff --git a/src/lib/gui/ipc/DaemonIpcClient.cpp b/src/lib/gui/ipc/DaemonIpcClient.cpp index 28f280181..921ad602c 100644 --- a/src/lib/gui/ipc/DaemonIpcClient.cpp +++ b/src/lib/gui/ipc/DaemonIpcClient.cpp @@ -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 diff --git a/src/lib/gui/ipc/DaemonIpcClient.h b/src/lib/gui/ipc/DaemonIpcClient.h index f872effd3..33a915b33 100644 --- a/src/lib/gui/ipc/DaemonIpcClient.h +++ b/src/lib/gui/ipc/DaemonIpcClient.h @@ -24,6 +24,7 @@ public: bool sendLogLevel(const QString &logLevel); bool sendStartProcess(const QString &command, ElevateMode elevateMode); bool sendStopProcess(); + QString requestLogPath(); bool isConnected() const {