refactor: Update IPC server to handle log level and elevate mode changes

This commit is contained in:
Nick Bolton 2025-02-07 17:03:26 +00:00
parent 53038760de
commit 7c672b06d8
7 changed files with 162 additions and 88 deletions

View file

@ -9,6 +9,7 @@
#include "base/Log.h" #include "base/Log.h"
#include "common/constants.h" #include "common/constants.h"
#include "deskflow/DaemonApp.h" #include "deskflow/DaemonApp.h"
#include "deskflow/ipc/DaemonIpcServer.h"
#include "arch/Arch.h" #include "arch/Arch.h"
#include "base/EventQueue.h" #include "base/EventQueue.h"
@ -44,28 +45,58 @@ int main(int argc, char **argv)
QCoreApplication app(argc, argv); QCoreApplication app(argc, argv);
const auto pDaemon = &DaemonApp::instance(); const auto pDaemon = &DaemonApp::instance();
QObject::connect(pDaemon, &DaemonApp::mainLoopFinished, &app, &QCoreApplication::quit); const auto initResult = pDaemon->init(&events, argc, argv);
QObject::connect(pDaemon, &DaemonApp::fatalErrorOccurred, &app, &QCoreApplication::quit);
QObject::connect(pDaemon, &DaemonApp::serviceInstalled, &app, &QCoreApplication::quit);
QObject::connect(pDaemon, &DaemonApp::serviceUninstalled, &app, &QCoreApplication::quit);
pDaemon->init(&events, argc, argv);
// Thread must be on the heap, as the thread needs to be deleted only when the loop finishes, switch (initResult) {
// and not when we go out of scope. using enum DaemonApp::InitResult;
// Deliberately do not set ownership of the thread to the daemon, as we want to delete the thread
// when the loop finishes, and not when the daemon is deleted.
QThread *thread = new QThread(); // NOSONAR
pDaemon->moveToThread(thread);
QObject::connect(thread, &QThread::started, pDaemon, &DaemonApp::run);
QObject::connect(thread, &QThread::finished, pDaemon, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
return QCoreApplication::exec(); case StartDaemon: {
using namespace deskflow::core;
QCoreApplication app(argc, argv);
auto *pDaemonThread = new QThread(); // NOSONAR
pDaemon->moveToThread(pDaemonThread);
QObject::connect(pDaemonThread, &QThread::started, pDaemon, &DaemonApp::run);
QObject::connect(pDaemonThread, &QThread::finished, pDaemon, &QObject::deleteLater);
QObject::connect(pDaemonThread, &QThread::finished, pDaemonThread, &QThread::deleteLater);
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
QObject::connect(
ipcServer, &ipc::DaemonIpcServer::logLevelChanged, pDaemon, &DaemonApp::saveLogLevel, //
Qt::DirectConnection
);
QObject::connect(
ipcServer, &ipc::DaemonIpcServer::elevateModeChanged, pDaemon, &DaemonApp::setElevate, //
Qt::DirectConnection
);
QObject::connect(
ipcServer, &ipc::DaemonIpcServer::commandChanged, pDaemon, &DaemonApp::setCommand, //
Qt::DirectConnection
);
QObject::connect(
ipcServer, &ipc::DaemonIpcServer::restartRequested, pDaemon, &DaemonApp::restartCoreProcess, //
Qt::DirectConnection
);
pDaemonThread->start();
return QCoreApplication::exec();
}
case FatalError:
return kExitFailed;
default:
return kExitSuccess;
}
} }
#if SYSAPI_WIN32 #if SYSAPI_WIN32
// Win32 subsystem entry point (simply forwards to main).
// We need this because using regular main under the Win32 subsystem results in empty args.
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{ {
return main(__argc, __argv); return main(__argc, __argv);

View file

@ -1,7 +1,7 @@
/* /*
* Deskflow -- mouse and keyboard sharing utility * Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2012 - 2016 Symless Ltd. * SPDX-FileCopyrightText: (C) 2010 - 2018, 2024 - 2025 Symless Ltd.
* SPDX-FileCopyrightText: (C) 2002 Chris Schoeneman * SPDX-FileCopyrightText: (C) 2002 - 2007 Chris Schoeneman
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/ */
@ -22,8 +22,8 @@
// make assert available since we use it a lot // make assert available since we use it a lot
#include <assert.h> #include <assert.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h> // IWYU pragma: keep
#include <string.h> #include <string.h> // IWYU pragma: keep
// defined in Carbon // defined in Carbon
#if !defined(__MACTYPES__) #if !defined(__MACTYPES__)
@ -35,10 +35,9 @@
enum enum
{ {
kExitSuccess = 0, // successful completion kExitSuccess = 0, // successful completion
kExitFailed = 1, // general failure kExitFailed = 1, // general failure
kExitTerminated = 2, // killed by signal kExitTerminated = 2, // killed by signal
kExitArgs = 3, // bad arguments kExitArgs = 3, // bad arguments
kExitConfig = 4, // cannot read configuration kExitConfig = 4, // cannot read configuration
kExitSubscription = 5 // subscription error
}; };

View file

@ -1,8 +1,7 @@
/* /*
* Deskflow -- mouse and keyboard sharing utility * Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2024 Chris Rizzitello <sithlord48@gmail.com> * SPDX-FileCopyrightText: (C) 2024 Chris Rizzitello <sithlord48@gmail.com>
* SPDX-FileCopyrightText: (C) 2012 - 2025 Symless Ltd. * SPDX-FileCopyrightText: (C) 2025 Symless Ltd.
* SPDX-FileCopyrightText: (C) 2002 Chris Schoeneman
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/ */

View file

@ -1,6 +1,6 @@
/* /*
* Deskflow -- mouse and keyboard sharing utility * Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2012 Symless Ltd. * SPDX-FileCopyrightText: (C) 2012 - 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
*/ */
@ -19,7 +19,6 @@
#include "deskflow/ArgParser.h" #include "deskflow/ArgParser.h"
#include "deskflow/ClientArgs.h" #include "deskflow/ClientArgs.h"
#include "deskflow/ServerArgs.h" #include "deskflow/ServerArgs.h"
#include "ipc/DaemonIpcServer.h"
#include "ipc/IpcClientProxy.h" #include "ipc/IpcClientProxy.h"
#include "ipc/IpcLogOutputter.h" #include "ipc/IpcLogOutputter.h"
#include "ipc/IpcMessage.h" #include "ipc/IpcMessage.h"
@ -37,16 +36,10 @@
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#include <Windows.h> #include <Windows.h>
#elif SYSAPI_UNIX
#include <iostream>
#endif #endif
#include <QCoreApplication> #include <filesystem>
#include <QObject> #include <iostream>
#include <QThread>
#include <memory> #include <memory>
#include <sstream> #include <sstream>
#include <string> #include <string>
@ -99,13 +92,13 @@ int winMainLoopStatic(int, const char **)
} }
#endif #endif
DaemonApp::DaemonApp() : m_ipcServer{new ipc::DaemonIpcServer(this)} void showHelp(int argc, char **argv) // NOSONAR - CLI args
{ {
connect(m_ipcServer, &ipc::DaemonIpcServer::elevateModeChanged, this, &DaemonApp::handleElevateModeChanged); const auto binName = argc > 0 ? std::filesystem::path(argv[0]).filename().string() : "deskflow-core";
connect(m_ipcServer, &ipc::DaemonIpcServer::commandChanged, this, &DaemonApp::handleCommandChanged); std::cout << "Usage: " << binName << " daemon [-f|--foreground] [--install] [--uninstall]" << std::endl;
connect(m_ipcServer, &ipc::DaemonIpcServer::restartRequested, this, &DaemonApp::handleRestartRequested);
} }
DaemonApp::DaemonApp() = default;
DaemonApp::~DaemonApp() = default; DaemonApp::~DaemonApp() = default;
void DaemonApp::run() void DaemonApp::run()
@ -125,34 +118,62 @@ void DaemonApp::run()
ARCH->daemonize(kAppName, unixMainLoopStatic); ARCH->daemonize(kAppName, unixMainLoopStatic);
#endif #endif
} }
Q_EMIT mainLoopFinished();
} }
void DaemonApp::handleElevateModeChanged(int mode) void DaemonApp::saveLogLevel(const QString &logLevel) const
{ {
LOG_DEBUG("elevate mode changed: %d", mode); LOG_DEBUG("log level changed: %s", logLevel.toUtf8().constData());
m_elevateMode = mode; CLOG->setFilter(logLevel.toUtf8().constData());
try {
// saves setting for next time the daemon starts.
ARCH->setting("LogLevel", logLevel.toStdString());
} catch (XArch &e) {
LOG((CLOG_ERR "failed to save log level setting: %s", e.what()));
}
} }
void DaemonApp::handleCommandChanged(const QString &command) void DaemonApp::setElevate(bool elevate)
{
LOG((CLOG_DEBUG "elevate value changed: %s", elevate ? "yes" : "no"));
m_elevate = elevate;
try {
// saves setting for next time the daemon starts.
ARCH->setting("Elevate", std::string(elevate ? "1" : "0"));
} catch (XArch &e) {
LOG((CLOG_ERR "failed to save elevate setting: %s", e.what()));
}
}
void DaemonApp::setCommand(const QString &command)
{ {
LOG_DEBUG("service command updated"); LOG_DEBUG("service command updated");
m_command = command.toStdString(); m_command = command.toStdString();
try {
// saves setting for next time the daemon starts.
ARCH->setting("Command", command.toStdString());
} catch (XArch &e) {
LOG((CLOG_ERR "failed to save command setting: %s", e.what()));
}
} }
void DaemonApp::handleRestartRequested() void DaemonApp::restartCoreProcess()
{ {
LOG_DEBUG("service restart requested"); LOG_DEBUG("service restart requested");
#if SYSAPI_WIN32 #if SYSAPI_WIN32
m_watchdog->setCommand(m_command, m_elevateMode); m_watchdog->setCommand(m_command, m_elevate);
#else #else
LOG_ERR("restart not implemented on this platform"); LOG_ERR("restart not implemented on this platform");
#endif #endif
} }
void DaemonApp::init(IEventQueue *events, int argc, char **argv) // NOSONAR DaemonApp::InitResult DaemonApp::init(IEventQueue *events, int argc, char **argv) // NOSONAR - CLI args
{ {
using enum InitResult;
if (events == nullptr) { if (events == nullptr) {
throw XDeskflow("event queue not set"); throw XDeskflow("event queue not set");
} }
@ -168,26 +189,30 @@ void DaemonApp::init(IEventQueue *events, int argc, char **argv) // NOSONAR
for (int i = 1; i < argc; ++i) { for (int i = 1; i < argc; ++i) {
string arg(argv[i]); string arg(argv[i]);
if (arg == "-f") { if (arg == "-h" || arg == "--help") {
showHelp(argc, argv);
return ShowHelp;
} else if (arg == "-f" || arg == "--foreground") {
m_foreground = true; m_foreground = true;
} }
#if SYSAPI_WIN32 #if SYSAPI_WIN32
else if (arg == "--install" || arg == "/install") { else if (arg == "--install" || arg == "/install") {
LOG((CLOG_NOTE "installing windows daemon")); LOG((CLOG_NOTE "installing windows daemon"));
ARCH->installDaemon(); ARCH->installDaemon();
Q_EMIT serviceInstalled(); return Installed;
} else if (arg == "--uninstall" || arg == "/uninstall") { } else if (arg == "--uninstall" || arg == "/uninstall") {
LOG((CLOG_NOTE "uninstalling windows daemon")); LOG((CLOG_NOTE "uninstalling windows daemon"));
isUninstalling = true; isUninstalling = true;
ARCH->uninstallDaemon(); ARCH->uninstallDaemon();
Q_EMIT serviceUninstalled(); return Uninstalled;
} }
#endif #endif
else { else {
stringstream ss; stringstream ss;
ss << "Unrecognized argument: " << arg; ss << "Unrecognized argument: " << arg;
foregroundError(ss.str().c_str()); handleError(ss.str().c_str());
Q_EMIT fatalErrorOccurred(); showHelp(argc, argv);
return ArgsError;
} }
} }
@ -198,6 +223,9 @@ void DaemonApp::init(IEventQueue *events, int argc, char **argv) // NOSONAR
CLOG->insert(new MSWindowsDebugOutputter()); // NOSONAR - Adopted by `Log` CLOG->insert(new MSWindowsDebugOutputter()); // NOSONAR - Adopted by `Log`
#endif #endif
} }
return StartDaemon;
} catch (XArch &e) { } catch (XArch &e) {
std::string message = e.what(); std::string message = e.what();
if (isUninstalling && (message.find("The service has not been started") != std::string::npos)) { if (isUninstalling && (message.find("The service has not been started") != std::string::npos)) {
@ -207,16 +235,15 @@ void DaemonApp::init(IEventQueue *events, int argc, char **argv) // NOSONAR
// horribly wrong, but it's just the service manager reporting a false // horribly wrong, but it's just the service manager reporting a false
// positive (the service has actually shut down in most cases). // positive (the service has actually shut down in most cases).
} else { } else {
foregroundError(message.c_str()); handleError(message.c_str());
} }
Q_EMIT fatalErrorOccurred();
} catch (std::exception &e) { } catch (std::exception &e) {
foregroundError(e.what()); handleError(e.what());
Q_EMIT fatalErrorOccurred();
} catch (...) { } catch (...) {
foregroundError("Unrecognized error."); handleError("Unrecognized error.");
Q_EMIT fatalErrorOccurred();
} }
return FatalError;
} }
void DaemonApp::mainLoop(bool logToFile, bool foreground) void DaemonApp::mainLoop(bool logToFile, bool foreground)
@ -268,12 +295,14 @@ void DaemonApp::mainLoop(bool logToFile, bool foreground)
} }
} }
void DaemonApp::foregroundError(const char *message) void DaemonApp::handleError(const char *message)
{ {
// Always print error to stdout in case run as CLI program.
LOG_ERR("%s", message);
#if SYSAPI_WIN32 #if SYSAPI_WIN32
MessageBoxA(NULL, message, "Deskflow Service", MB_OK | MB_ICONERROR); // Show a message box for when run from MSI in Win32 subsystem.
#elif SYSAPI_UNIX MessageBoxA(nullptr, message, "Deskflow daemon error", MB_OK | MB_ICONERROR);
cerr << message << endl;
#endif #endif
} }

View file

@ -1,6 +1,6 @@
/* /*
* Deskflow -- mouse and keyboard sharing utility * Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2012 Symless Ltd. * SPDX-FileCopyrightText: (C) 2012 - 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
*/ */
@ -37,9 +37,23 @@ class DaemonApp : public QObject
Q_OBJECT Q_OBJECT
public: public:
void init(IEventQueue *events, int argc, char **argv); enum class InitResult
{
Installed,
Uninstalled,
StartDaemon,
ShowHelp,
ArgsError,
FatalError,
};
InitResult init(IEventQueue *events, int argc, char **argv);
void run(); void run();
void mainLoop(bool logToFile, bool foreground = false); void mainLoop(bool logToFile, bool foreground = false);
void restartCoreProcess();
void saveLogLevel(const QString &logLevel) const;
void setElevate(bool elevate);
void setCommand(const QString &command);
static DaemonApp &instance() static DaemonApp &instance()
{ {
@ -47,25 +61,14 @@ public:
return instance; return instance;
} }
signals:
void mainLoopFinished();
void fatalErrorOccurred();
void serviceInstalled();
void serviceUninstalled();
private: private:
explicit DaemonApp(); explicit DaemonApp();
~DaemonApp() override; ~DaemonApp() override;
void daemonize(); void daemonize();
void foregroundError(const char *message); void handleError(const char *message);
std::string logFilename(); std::string logFilename();
void handleIpcMessage(const Event &, void *); void handleIpcMessage(const Event &e, void *);
private slots:
void handleElevateModeChanged(int mode);
void handleCommandChanged(const QString &command);
void handleRestartRequested();
#if SYSAPI_WIN32 #if SYSAPI_WIN32
std::unique_ptr<MSWindowsWatchdog> m_watchdog; std::unique_ptr<MSWindowsWatchdog> m_watchdog;
@ -77,6 +80,6 @@ private:
FileLogOutputter *m_fileLogOutputter = nullptr; FileLogOutputter *m_fileLogOutputter = nullptr;
deskflow::core::ipc::DaemonIpcServer *m_ipcServer = nullptr; deskflow::core::ipc::DaemonIpcServer *m_ipcServer = nullptr;
std::string m_command = ""; std::string m_command = "";
int m_elevateMode = 0; bool m_elevate = false;
bool m_foreground = false; bool m_foreground = false;
}; };

View file

@ -100,15 +100,27 @@ void DaemonIpcServer::processMessage(QLocalSocket *clientSocket, const QString &
LOG_DEBUG("ipc server got message: %s", message.toUtf8().constData()); LOG_DEBUG("ipc server got message: %s", message.toUtf8().constData());
if (message == "hello") { if (message == "hello") {
clientSocket->write("hello\n"); clientSocket->write("hello\n");
} else if (message == "noop") {
clientSocket->write(kAckMessage);
} else if (message.startsWith("logLevel=")) {
const auto logLevel = message.split('=')[1];
if (logLevel.isEmpty()) {
LOG_ERR("ipc server got empty log level");
clientSocket->write(kErrorMessage);
} else {
LOG_DEBUG("ipc server got new log level: %s", logLevel.toUtf8().constData());
Q_EMIT logLevelChanged(logLevel);
clientSocket->write(kAckMessage);
}
} else if (message.startsWith("elevate=")) { } else if (message.startsWith("elevate=")) {
const auto elevateMode = message.split('=')[1].toInt(); const auto elevateMode = message.split('=')[1];
if (elevateMode < 0 || elevateMode > 2) { if (elevateMode != "yes" && elevateMode != "no") {
LOG_ERR("ipc server got invalid elevate mode: %d", elevateMode); LOG_ERR("ipc server got invalid elevate value: %s", elevateMode.toUtf8().constData());
clientSocket->write(kErrorMessage); clientSocket->write(kErrorMessage);
return; return;
} else { } else {
LOG_DEBUG("ipc server got new elevate mode: %d", elevateMode); LOG_DEBUG("ipc server got new elevate value: %s", elevateMode.toUtf8().constData());
Q_EMIT elevateModeChanged(elevateMode); Q_EMIT elevateModeChanged(elevateMode == "yes");
clientSocket->write(kAckMessage); clientSocket->write(kAckMessage);
} }
} else if (message.startsWith("command=")) { } else if (message.startsWith("command=")) {

View file

@ -23,7 +23,8 @@ public:
~DaemonIpcServer(); ~DaemonIpcServer();
signals: signals:
void elevateModeChanged(int mode); void logLevelChanged(const QString &logLevel);
void elevateModeChanged(bool elevate);
void commandChanged(const QString &command); void commandChanged(const QString &command);
void restartRequested(); void restartRequested();