diff --git a/src/apps/deskflow-daemon/deskflow-daemon.cpp b/src/apps/deskflow-daemon/deskflow-daemon.cpp index a393b4d61..33297b581 100644 --- a/src/apps/deskflow-daemon/deskflow-daemon.cpp +++ b/src/apps/deskflow-daemon/deskflow-daemon.cpp @@ -9,6 +9,7 @@ #include "base/Log.h" #include "common/constants.h" #include "deskflow/DaemonApp.h" +#include "deskflow/ipc/DaemonIpcServer.h" #include "arch/Arch.h" #include "base/EventQueue.h" @@ -44,28 +45,58 @@ int main(int argc, char **argv) QCoreApplication app(argc, argv); const auto pDaemon = &DaemonApp::instance(); - QObject::connect(pDaemon, &DaemonApp::mainLoopFinished, &app, &QCoreApplication::quit); - 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); + const auto initResult = pDaemon->init(&events, argc, argv); - // Thread must be on the heap, as the thread needs to be deleted only when the loop finishes, - // and not when we go out of scope. - // 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(); + switch (initResult) { + using enum DaemonApp::InitResult; - 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 +// 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) { return main(__argc, __argv); diff --git a/src/lib/common/common.h b/src/lib/common/common.h index c234bdb18..ba127398d 100644 --- a/src/lib/common/common.h +++ b/src/lib/common/common.h @@ -1,7 +1,7 @@ /* * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2012 - 2016 Symless Ltd. - * SPDX-FileCopyrightText: (C) 2002 Chris Schoeneman + * SPDX-FileCopyrightText: (C) 2010 - 2018, 2024 - 2025 Symless Ltd. + * SPDX-FileCopyrightText: (C) 2002 - 2007 Chris Schoeneman * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception */ @@ -22,8 +22,8 @@ // make assert available since we use it a lot #include #include -#include -#include +#include // IWYU pragma: keep +#include // IWYU pragma: keep // defined in Carbon #if !defined(__MACTYPES__) @@ -35,10 +35,9 @@ enum { - kExitSuccess = 0, // successful completion - kExitFailed = 1, // general failure - kExitTerminated = 2, // killed by signal - kExitArgs = 3, // bad arguments - kExitConfig = 4, // cannot read configuration - kExitSubscription = 5 // subscription error + kExitSuccess = 0, // successful completion + kExitFailed = 1, // general failure + kExitTerminated = 2, // killed by signal + kExitArgs = 3, // bad arguments + kExitConfig = 4, // cannot read configuration }; diff --git a/src/lib/common/constants.h.in b/src/lib/common/constants.h.in index 0a72bd358..743ef789d 100644 --- a/src/lib/common/constants.h.in +++ b/src/lib/common/constants.h.in @@ -1,8 +1,7 @@ /* * Deskflow -- mouse and keyboard sharing utility * SPDX-FileCopyrightText: (C) 2024 Chris Rizzitello - * SPDX-FileCopyrightText: (C) 2012 - 2025 Symless Ltd. - * SPDX-FileCopyrightText: (C) 2002 Chris Schoeneman + * SPDX-FileCopyrightText: (C) 2025 Symless Ltd. * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception */ diff --git a/src/lib/deskflow/DaemonApp.cpp b/src/lib/deskflow/DaemonApp.cpp index 45245baa0..9fbf37168 100644 --- a/src/lib/deskflow/DaemonApp.cpp +++ b/src/lib/deskflow/DaemonApp.cpp @@ -1,6 +1,6 @@ /* * 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 */ @@ -19,7 +19,6 @@ #include "deskflow/ArgParser.h" #include "deskflow/ClientArgs.h" #include "deskflow/ServerArgs.h" -#include "ipc/DaemonIpcServer.h" #include "ipc/IpcClientProxy.h" #include "ipc/IpcLogOutputter.h" #include "ipc/IpcMessage.h" @@ -37,16 +36,10 @@ #define WIN32_LEAN_AND_MEAN #include -#elif SYSAPI_UNIX - -#include - #endif -#include -#include -#include - +#include +#include #include #include #include @@ -99,13 +92,13 @@ int winMainLoopStatic(int, const char **) } #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); - connect(m_ipcServer, &ipc::DaemonIpcServer::commandChanged, this, &DaemonApp::handleCommandChanged); - connect(m_ipcServer, &ipc::DaemonIpcServer::restartRequested, this, &DaemonApp::handleRestartRequested); + const auto binName = argc > 0 ? std::filesystem::path(argv[0]).filename().string() : "deskflow-core"; + std::cout << "Usage: " << binName << " daemon [-f|--foreground] [--install] [--uninstall]" << std::endl; } +DaemonApp::DaemonApp() = default; DaemonApp::~DaemonApp() = default; void DaemonApp::run() @@ -125,34 +118,62 @@ void DaemonApp::run() ARCH->daemonize(kAppName, unixMainLoopStatic); #endif } - - Q_EMIT mainLoopFinished(); } -void DaemonApp::handleElevateModeChanged(int mode) +void DaemonApp::saveLogLevel(const QString &logLevel) const { - LOG_DEBUG("elevate mode changed: %d", mode); - m_elevateMode = mode; + LOG_DEBUG("log level changed: %s", logLevel.toUtf8().constData()); + 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"); 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"); + #if SYSAPI_WIN32 - m_watchdog->setCommand(m_command, m_elevateMode); + m_watchdog->setCommand(m_command, m_elevate); #else LOG_ERR("restart not implemented on this platform"); #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) { 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) { 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; } #if SYSAPI_WIN32 else if (arg == "--install" || arg == "/install") { LOG((CLOG_NOTE "installing windows daemon")); ARCH->installDaemon(); - Q_EMIT serviceInstalled(); + return Installed; } else if (arg == "--uninstall" || arg == "/uninstall") { LOG((CLOG_NOTE "uninstalling windows daemon")); isUninstalling = true; ARCH->uninstallDaemon(); - Q_EMIT serviceUninstalled(); + return Uninstalled; } #endif else { stringstream ss; ss << "Unrecognized argument: " << arg; - foregroundError(ss.str().c_str()); - Q_EMIT fatalErrorOccurred(); + handleError(ss.str().c_str()); + 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` #endif } + + return StartDaemon; + } catch (XArch &e) { std::string message = e.what(); 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 // positive (the service has actually shut down in most cases). } else { - foregroundError(message.c_str()); + handleError(message.c_str()); } - Q_EMIT fatalErrorOccurred(); } catch (std::exception &e) { - foregroundError(e.what()); - Q_EMIT fatalErrorOccurred(); + handleError(e.what()); } catch (...) { - foregroundError("Unrecognized error."); - Q_EMIT fatalErrorOccurred(); + handleError("Unrecognized error."); } + + return FatalError; } 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 - MessageBoxA(NULL, message, "Deskflow Service", MB_OK | MB_ICONERROR); -#elif SYSAPI_UNIX - cerr << message << endl; + // Show a message box for when run from MSI in Win32 subsystem. + MessageBoxA(nullptr, message, "Deskflow daemon error", MB_OK | MB_ICONERROR); #endif } diff --git a/src/lib/deskflow/DaemonApp.h b/src/lib/deskflow/DaemonApp.h index e24a417ba..5296c623e 100644 --- a/src/lib/deskflow/DaemonApp.h +++ b/src/lib/deskflow/DaemonApp.h @@ -1,6 +1,6 @@ /* * 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 */ @@ -37,9 +37,23 @@ class DaemonApp : public QObject Q_OBJECT 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 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() { @@ -47,25 +61,14 @@ public: return instance; } -signals: - void mainLoopFinished(); - void fatalErrorOccurred(); - void serviceInstalled(); - void serviceUninstalled(); - private: explicit DaemonApp(); ~DaemonApp() override; void daemonize(); - void foregroundError(const char *message); + void handleError(const char *message); std::string logFilename(); - void handleIpcMessage(const Event &, void *); - -private slots: - void handleElevateModeChanged(int mode); - void handleCommandChanged(const QString &command); - void handleRestartRequested(); + void handleIpcMessage(const Event &e, void *); #if SYSAPI_WIN32 std::unique_ptr m_watchdog; @@ -77,6 +80,6 @@ private: FileLogOutputter *m_fileLogOutputter = nullptr; deskflow::core::ipc::DaemonIpcServer *m_ipcServer = nullptr; std::string m_command = ""; - int m_elevateMode = 0; + bool m_elevate = false; bool m_foreground = false; }; diff --git a/src/lib/deskflow/ipc/DaemonIpcServer.cpp b/src/lib/deskflow/ipc/DaemonIpcServer.cpp index 4f8e25f20..e1f19c93d 100644 --- a/src/lib/deskflow/ipc/DaemonIpcServer.cpp +++ b/src/lib/deskflow/ipc/DaemonIpcServer.cpp @@ -100,15 +100,27 @@ void DaemonIpcServer::processMessage(QLocalSocket *clientSocket, const QString & LOG_DEBUG("ipc server got message: %s", message.toUtf8().constData()); if (message == "hello") { 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=")) { - const auto elevateMode = message.split('=')[1].toInt(); - if (elevateMode < 0 || elevateMode > 2) { - LOG_ERR("ipc server got invalid elevate mode: %d", elevateMode); + const auto elevateMode = message.split('=')[1]; + if (elevateMode != "yes" && elevateMode != "no") { + LOG_ERR("ipc server got invalid elevate value: %s", elevateMode.toUtf8().constData()); clientSocket->write(kErrorMessage); return; } else { - LOG_DEBUG("ipc server got new elevate mode: %d", elevateMode); - Q_EMIT elevateModeChanged(elevateMode); + LOG_DEBUG("ipc server got new elevate value: %s", elevateMode.toUtf8().constData()); + Q_EMIT elevateModeChanged(elevateMode == "yes"); clientSocket->write(kAckMessage); } } else if (message.startsWith("command=")) { diff --git a/src/lib/deskflow/ipc/DaemonIpcServer.h b/src/lib/deskflow/ipc/DaemonIpcServer.h index 7de92053a..68b54aae2 100644 --- a/src/lib/deskflow/ipc/DaemonIpcServer.h +++ b/src/lib/deskflow/ipc/DaemonIpcServer.h @@ -23,7 +23,8 @@ public: ~DaemonIpcServer(); signals: - void elevateModeChanged(int mode); + void logLevelChanged(const QString &logLevel); + void elevateModeChanged(bool elevate); void commandChanged(const QString &command); void restartRequested();