feat: Restart process from daemon through new Qt IPC impl

This commit is contained in:
Nick Bolton 2025-02-06 13:31:18 +00:00
parent 0d2d9f385d
commit c1d7474700
10 changed files with 212 additions and 61 deletions

View file

@ -59,6 +59,7 @@
"Priddy", "Priddy",
"psutil", "psutil",
"pyproject", "pyproject",
"qobject",
"qputenv", "qputenv",
"readf", "readf",
"Regen", "Regen",

View file

@ -169,8 +169,6 @@ MainWindow::MainWindow(ConfigScopes &configScopes, AppConfig &appConfig)
regenerateLocalFingerprints(); regenerateLocalFingerprints();
} }
} }
m_daemonIpcClient->connect();
} }
MainWindow::~MainWindow() MainWindow::~MainWindow()

View file

@ -101,6 +101,10 @@ DaemonApp::DaemonApp(IEventQueue *events, int argc, char **argv)
m_events(events), m_events(events),
m_ipcServer{new ipc::DaemonIpcServer(this)} m_ipcServer{new ipc::DaemonIpcServer(this)}
{ {
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);
s_instance = this; s_instance = this;
} }
@ -109,6 +113,28 @@ DaemonApp::~DaemonApp()
s_instance = nullptr; s_instance = nullptr;
} }
void DaemonApp::handleElevateModeChanged(int mode)
{
LOG_DEBUG("elevate mode changed: %d", mode);
m_elevateMode = mode;
}
void DaemonApp::handleCommandChanged(const QString &command)
{
LOG_DEBUG("service command updated");
m_command = command.toStdString();
}
void DaemonApp::handleRestartRequested()
{
LOG_DEBUG("service restart requested");
#if SYSAPI_WIN32
m_watchdog->setCommand(m_command, m_elevateMode);
#else
LOG_ERR("restart not implemented on this platform");
#endif
}
void DaemonApp::startAsync() void DaemonApp::startAsync()
{ {
#if SYSAPI_WIN32 #if SYSAPI_WIN32
@ -205,7 +231,7 @@ void DaemonApp::mainLoop(bool logToFile, bool foreground)
std::string command = ARCH->setting("Command"); std::string command = ARCH->setting("Command");
bool elevate = ARCH->setting("Elevate") == "1"; bool elevate = ARCH->setting("Elevate") == "1";
if (command != "") { if (command != "") {
LOG((CLOG_INFO "using last known command: %s", command.c_str())); LOG_DEBUG("using last known command: %s", command.c_str());
m_watchdog->setCommand(command, elevate); m_watchdog->setCommand(command, elevate);
} }
@ -217,8 +243,6 @@ void DaemonApp::mainLoop(bool logToFile, bool foreground)
m_watchdog->stop(); m_watchdog->stop();
#endif #endif
m_events->removeHandler(m_events->forIpcServer().messageReceived(), m_ipcServer.get());
CLOG->remove(m_ipcLogOutputter.get()); CLOG->remove(m_ipcLogOutputter.get());
DAEMON_RUNNING(false); DAEMON_RUNNING(false);

View file

@ -13,6 +13,8 @@
#include <QCoreApplication> #include <QCoreApplication>
#include "common/common.h"
class Event; class Event;
class IEventQueue; class IEventQueue;
class IpcLogOutputter; class IpcLogOutputter;
@ -44,6 +46,11 @@ private:
std::string logFilename(); std::string logFilename();
void handleIpcMessage(const Event &, void *); void handleIpcMessage(const Event &, void *);
private slots:
void handleElevateModeChanged(int mode);
void handleCommandChanged(const QString &command);
void handleRestartRequested();
public: public:
static DaemonApp *s_instance; static DaemonApp *s_instance;
@ -55,5 +62,7 @@ private:
std::unique_ptr<IpcLogOutputter> m_ipcLogOutputter; std::unique_ptr<IpcLogOutputter> m_ipcLogOutputter;
IEventQueue *m_events = nullptr; IEventQueue *m_events = nullptr;
FileLogOutputter *m_fileLogOutputter = nullptr; FileLogOutputter *m_fileLogOutputter = nullptr;
std::unique_ptr<deskflow::core::ipc::DaemonIpcServer> m_ipcServer; deskflow::core::ipc::DaemonIpcServer *m_ipcServer = nullptr;
std::string m_command = "";
int m_elevateMode = 0;
}; };

View file

@ -42,28 +42,93 @@ void DaemonIpcServer::handleNewConnection()
} }
LOG_DEBUG("ipc server got new connection"); LOG_DEBUG("ipc server got new connection");
m_clients.insert(clientSocket);
connect(clientSocket, &QLocalSocket::readyRead, this, [clientSocket]() { connect(clientSocket, &QLocalSocket::readyRead, this, &DaemonIpcServer::handleReadyRead);
LOG_DEBUG("ipc server ready to read data"); connect(clientSocket, &QLocalSocket::disconnected, this, &DaemonIpcServer::handleDisconnected);
connect(clientSocket, &QLocalSocket::errorOccurred, this, &DaemonIpcServer::handleErrorOccurred);
}
QByteArray data = clientSocket->readAll(); void DaemonIpcServer::handleReadyRead()
if (data.isEmpty()) { {
LOG_WARN("ipc server got empty message"); const auto clientSocket = qobject_cast<QLocalSocket *>(sender());
LOG_DEBUG("ipc server ready to read data");
QByteArray data = clientSocket->readAll();
if (data.isEmpty()) {
LOG_WARN("ipc server got empty message");
return;
}
// we don't handle incomplete messages yet; each socket read must have delimiters.
if (!data.contains('\n')) {
LOG_WARN("ipc server got incomplete message: %s", data.constData());
return;
}
// each message is delimited by a newline to keep the protocol super simple.
while (data.contains('\n')) {
int index = data.indexOf('\n');
QByteArray messageData = data.left(index);
data.remove(0, index + 1);
QString message = QString::fromUtf8(messageData);
processMessage(clientSocket, message);
}
}
void DaemonIpcServer::handleDisconnected()
{
const auto clientSocket = qobject_cast<QLocalSocket *>(sender());
LOG_DEBUG("ipc server client disconnected");
m_clients.remove(clientSocket);
clientSocket->deleteLater();
}
void DaemonIpcServer::handleErrorOccurred()
{
const auto clientSocket = qobject_cast<QLocalSocket *>(sender());
LOG_ERR("ipc server client error: %s", clientSocket->errorString().toUtf8().constData());
m_clients.remove(clientSocket);
clientSocket->deleteLater();
}
void DaemonIpcServer::processMessage(QLocalSocket *clientSocket, const QString &message)
{
const auto kAckMessage = "ok\n";
const auto kErrorMessage = "error\n";
LOG_DEBUG("ipc server got message: %s", message.toUtf8().constData());
if (message == "hello") {
clientSocket->write("hello\n");
} 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);
clientSocket->write(kErrorMessage);
return; return;
} else {
LOG_DEBUG("ipc server got new elevate mode: %d", elevateMode);
Q_SIGNAL elevateModeChanged(elevateMode);
clientSocket->write(kAckMessage);
} }
} else if (message.startsWith("command=")) {
QString dataStr = QString::fromUtf8(data); const auto command = message.split('=')[1];
if (dataStr.isEmpty()) { if (command.isEmpty()) {
LOG_ERR("ipc server failed to convert message to string"); LOG_ERR("ipc server got empty command");
return; clientSocket->write(kErrorMessage);
} else {
LOG_DEBUG("ipc server got new command: %s", command.toUtf8().constData());
Q_SIGNAL commandChanged(command);
clientSocket->write(kAckMessage);
} }
} else if (message == "restart") {
if (dataStr == "hello") { LOG_DEBUG("ipc server got restart message");
LOG_DEBUG("ipc server got message: %s", data.constData()); Q_SIGNAL restartRequested();
clientSocket->write("hello"); clientSocket->write(kAckMessage);
clientSocket->flush(); } else {
} LOG_WARN("ipc server got unknown message: %s", message.toUtf8().constData());
}); }
clientSocket->flush();
} }
} // namespace deskflow::core::ipc } // namespace deskflow::core::ipc

View file

@ -7,8 +7,10 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QSet>
class QLocalServer; class QLocalServer;
class QLocalSocket;
namespace deskflow::core::ipc { namespace deskflow::core::ipc {
@ -20,11 +22,23 @@ public:
DaemonIpcServer(QObject *parent); DaemonIpcServer(QObject *parent);
~DaemonIpcServer(); ~DaemonIpcServer();
signals:
void elevateModeChanged(int mode);
void commandChanged(const QString &command);
void restartRequested();
private:
void processMessage(QLocalSocket *clientSocket, const QString &message);
private slots: private slots:
void handleNewConnection(); void handleNewConnection();
void handleReadyRead();
void handleDisconnected();
void handleErrorOccurred();
private: private:
QLocalServer *m_server; QLocalServer *m_server;
QSet<QLocalSocket *> m_clients;
}; };
} // namespace deskflow::core::ipc } // namespace deskflow::core::ipc

View file

@ -9,9 +9,9 @@
#include "common/constants.h" #include "common/constants.h"
#include "gui/config/IAppConfig.h" #include "gui/config/IAppConfig.h"
#include "gui/core/CoreTool.h" #include "gui/core/CoreTool.h"
#include "gui/ipc/DaemonIpcClient.h"
#include "gui/paths.h" #include "gui/paths.h"
#include "tls/TlsUtility.h" #include "tls/TlsUtility.h"
#include <qlogging.h>
#if defined(Q_OS_MAC) #if defined(Q_OS_MAC)
#include "OSXHelpers.h" #include "OSXHelpers.h"
@ -25,7 +25,6 @@
#include <QRegularExpression> #include <QRegularExpression>
#include <QStandardPaths> #include <QStandardPaths>
#include <QTimer> #include <QTimer>
#include <QtGlobal>
namespace deskflow::gui { namespace deskflow::gui {
@ -156,19 +155,9 @@ QString CoreProcess::Deps::getProfileRoot() const
CoreProcess::CoreProcess(const IAppConfig &appConfig, const IServerConfig &serverConfig, std::shared_ptr<Deps> deps) CoreProcess::CoreProcess(const IAppConfig &appConfig, const IServerConfig &serverConfig, std::shared_ptr<Deps> deps)
: m_appConfig(appConfig), : m_appConfig(appConfig),
m_serverConfig(serverConfig), m_serverConfig(serverConfig),
m_pDeps(deps) m_pDeps(deps),
m_daemonIpcClient{new ipc::DaemonIpcClient(this)}
{ {
connect(
&m_pDeps->ipcClient(), &QIpcClient::read, this, //
&CoreProcess::onIpcClientRead
);
connect(
&m_pDeps->ipcClient(), &QIpcClient::serviceReady, this, //
&CoreProcess::onIpcClientServiceReady
);
connect(&m_pDeps->process(), &QProcessProxy::finished, this, &CoreProcess::onProcessFinished); connect(&m_pDeps->process(), &QProcessProxy::finished, this, &CoreProcess::onProcessFinished);
connect( connect(
@ -186,6 +175,8 @@ CoreProcess::CoreProcess(const IAppConfig &appConfig, const IServerConfig &serve
qDebug("retry cancelled, process state is not retry pending"); qDebug("retry cancelled, process state is not retry pending");
} }
}); });
m_daemonIpcClient->connectToServer();
} }
void CoreProcess::onIpcClientServiceReady() void CoreProcess::onIpcClientServiceReady()
@ -205,12 +196,6 @@ void CoreProcess::onIpcClientServiceReady()
void CoreProcess::onIpcClientError(const QString &text) const void CoreProcess::onIpcClientError(const QString &text) const
{ {
qCritical().noquote() << text; qCritical().noquote() << text;
if (m_appConfig.processMode() != ProcessMode::kService) {
// if not meant to be in service mode and there is an ipc connection error,
// then abandon the ipc client connection.
m_pDeps->ipcClient().disconnectFromHost();
}
} }
void CoreProcess::onIpcClientRead(const QString &text) void CoreProcess::onIpcClientRead(const QString &text)
@ -290,17 +275,15 @@ void CoreProcess::startProcessFromDaemon(const QString &app, const QStringList &
qFatal("core process must be in starting state"); qFatal("core process must be in starting state");
} }
if (!m_pDeps->ipcClient().isConnected()) { if (!m_daemonIpcClient->isConnected()) {
// when service state changes, start will be called again. qFatal("cannot start process, daemon ipc not connected");
qDebug("cannot start process, ipc not connected, connecting instead");
m_pDeps->ipcClient().connectToHost();
return; return;
} }
QString commandQuoted = makeQuotedArgs(app, args); QString commandQuoted = makeQuotedArgs(app, args);
qInfo("running command: %s", qPrintable(commandQuoted)); qInfo("running command: %s", qPrintable(commandQuoted));
m_pDeps->ipcClient().sendCommand(commandQuoted, m_appConfig.elevateMode()); m_daemonIpcClient->sendCommand(commandQuoted, m_appConfig.elevateMode());
setProcessState(Started); setProcessState(Started);
} }

View file

@ -20,6 +20,10 @@
namespace deskflow::gui { namespace deskflow::gui {
namespace ipc {
class DaemonIpcClient;
}
class CoreProcess : public QObject class CoreProcess : public QObject
{ {
using IServerConfig = deskflow::gui::IServerConfig; using IServerConfig = deskflow::gui::IServerConfig;
@ -170,6 +174,7 @@ private:
std::optional<ProcessMode> m_lastProcessMode = std::nullopt; std::optional<ProcessMode> m_lastProcessMode = std::nullopt;
QTimer m_retryTimer; QTimer m_retryTimer;
int m_connections = 0; int m_connections = 0;
deskflow::gui::ipc::DaemonIpcClient *m_daemonIpcClient = nullptr;
}; };
} // namespace deskflow::gui } // namespace deskflow::gui

View file

@ -16,7 +16,7 @@ namespace deskflow::gui::ipc {
const auto kTimeout = 1000; const auto kTimeout = 1000;
DaemonIpcClient::DaemonIpcClient(QObject *parent) : QObject(parent), socket{new QLocalSocket(this)} DaemonIpcClient::DaemonIpcClient(QObject *parent) : QObject(parent), m_socket{new QLocalSocket(this)}
{ {
} }
@ -24,26 +24,62 @@ DaemonIpcClient::~DaemonIpcClient()
{ {
} }
void DaemonIpcClient::connect() void DaemonIpcClient::connectToServer()
{ {
socket->connectToServer(kDaemonIpcName); m_socket->connectToServer(kDaemonIpcName);
if (!socket->waitForConnected(kTimeout)) { if (!m_socket->waitForConnected(kTimeout)) {
qCritical() << "ipc client failed to connect to server:" << kDaemonIpcName; qCritical() << "ipc client failed to connect to server:" << kDaemonIpcName;
return; return;
} }
socket->write("hello"); sendMessage("hello", "hello", false);
if (!socket->waitForBytesWritten(kTimeout)) {
qCritical() << "ipc client failed to write message"; connect(m_socket, &QLocalSocket::disconnected, this, &DaemonIpcClient::handleDisconnected);
connect(m_socket, &QLocalSocket::errorOccurred, this, &DaemonIpcClient::handleErrorOccurred);
m_connected = true;
qInfo() << "ipc client connected to server:" << kDaemonIpcName;
}
void DaemonIpcClient::handleDisconnected()
{
qInfo() << "ipc client disconnected from server";
m_connected = false;
}
void DaemonIpcClient::handleErrorOccurred()
{
qCritical() << "ipc client error:" << m_socket->errorString();
m_connected = false;
}
void DaemonIpcClient::sendCommand(const QString &command, ElevateMode elevateMode)
{
sendMessage("elevate=" + QString::number(static_cast<int>(elevateMode)));
sendMessage("command=" + command);
sendMessage("restart");
}
void DaemonIpcClient::sendMessage(const QString &message, const QString &expectAck, const bool expectConnected)
{
if (expectConnected && !m_connected) {
qCritical() << "cannot send command, ipc not connected";
return; return;
} }
if (!socket->waitForReadyRead(kTimeout)) { QByteArray messageData = message.toUtf8() + "\n";
m_socket->write(messageData);
if (!m_socket->waitForBytesWritten(kTimeout)) {
qCritical() << "ipc client failed to write command";
return;
}
if (!m_socket->waitForReadyRead(kTimeout)) {
qCritical() << "ipc client failed to read response"; qCritical() << "ipc client failed to read response";
return; return;
} }
QByteArray response = socket->readAll(); QByteArray response = m_socket->readAll();
if (response.isEmpty()) { if (response.isEmpty()) {
qCritical() << "ipc client got empty response"; qCritical() << "ipc client got empty response";
return; return;
@ -55,12 +91,12 @@ void DaemonIpcClient::connect()
return; return;
} }
if (responseData != "hello") { if (responseData != expectAck + "\n") {
qCritical() << "ipc client got unexpected response: " << responseData; qCritical() << "ipc client got unexpected response: " << responseData;
return; return;
} }
qInfo() << "ipc client connected to server:" << kDaemonIpcName; qInfo() << "ipc client sent message: " << messageData;
} }
} // namespace deskflow::gui::ipc } // namespace deskflow::gui::ipc

View file

@ -8,6 +8,8 @@
#include <QObject> #include <QObject>
#include "gui/config/ElevateMode.h"
class QLocalSocket; class QLocalSocket;
namespace deskflow::gui::ipc { namespace deskflow::gui::ipc {
@ -19,10 +21,24 @@ class DaemonIpcClient : public QObject
public: public:
DaemonIpcClient(QObject *parent = nullptr); DaemonIpcClient(QObject *parent = nullptr);
~DaemonIpcClient(); ~DaemonIpcClient();
void connect(); void connectToServer();
void sendCommand(const QString &command, ElevateMode elevateMode);
bool isConnected() const
{
return m_connected;
}
private slots:
void handleDisconnected();
void handleErrorOccurred();
private: private:
QLocalSocket *socket; void sendMessage(const QString &message, const QString &expectAck = "ok", const bool expectConnected = true);
private:
QLocalSocket *m_socket;
bool m_connected{false};
}; };
} // namespace deskflow::gui::ipc } // namespace deskflow::gui::ipc