refactor: Remove QProcessProxy and simplify CoreProcess

Making a proxy class for QProcess to make GTest work is an excellent example of over-abstraction making simple things hard to do. I just wanted to call a function on QProcess and had to jump through hoops to do it.

Bye bye QProcessProxy! We can do much better with QTest.
This commit is contained in:
Nick Bolton
2025-08-22 10:33:05 +01:00
parent 78b83c5639
commit 4a16931c5d
7 changed files with 31 additions and 300 deletions

View File

@ -88,8 +88,6 @@ add_library(${target} STATIC
dialogs/SettingsDialog.ui
ipc/DaemonIpcClient.cpp
ipc/DaemonIpcClient.h
proxy/QProcessProxy.cpp
proxy/QProcessProxy.h
tls/TlsCertificate.cpp
tls/TlsCertificate.h
tls/TlsUtility.cpp

View File

@ -20,6 +20,7 @@
#include <QFile>
#include <QMessageBox>
#include <QMutexLocker>
#include <QProcess>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QTimer>
@ -116,28 +117,18 @@ QString wrapIpv6(const QString &address)
return address;
}
//
// CoreProcess::Deps
//
QString CoreProcess::Deps::appPath(const QString &name) const
QString getAppFilePath(const QString &name)
{
QDir dir(QCoreApplication::applicationDirPath());
return dir.filePath(name);
}
bool CoreProcess::Deps::fileExists(const QString &path) const
{
return QFile::exists(path);
}
//
// CoreProcess
//
CoreProcess::CoreProcess(const IServerConfig &serverConfig, std::shared_ptr<Deps> deps)
CoreProcess::CoreProcess(const IServerConfig &serverConfig)
: m_serverConfig(serverConfig),
m_pDeps(deps),
m_daemonIpcClient{new ipc::DaemonIpcClient(this)}
{
connect(m_daemonIpcClient, &ipc::DaemonIpcClient::connected, this, &CoreProcess::daemonIpcClientConnected);
@ -145,16 +136,6 @@ CoreProcess::CoreProcess(const IServerConfig &serverConfig, std::shared_ptr<Deps
m_daemonIpcClient, &ipc::DaemonIpcClient::connectionFailed, this, &CoreProcess::daemonIpcClientConnectionFailed
);
connect(&m_pDeps->process(), &QProcessProxy::finished, this, &CoreProcess::onProcessFinished);
connect(
&m_pDeps->process(), &QProcessProxy::readyReadStandardOutput, this, &CoreProcess::onProcessReadyReadStandardOutput
);
connect(
&m_pDeps->process(), &QProcessProxy::readyReadStandardError, this, &CoreProcess::onProcessReadyReadStandardError
);
connect(&m_retryTimer, &QTimer::timeout, this, [this] {
if (m_processState == ProcessState::RetryPending) {
start();
@ -166,15 +147,15 @@ CoreProcess::CoreProcess(const IServerConfig &serverConfig, std::shared_ptr<Deps
void CoreProcess::onProcessReadyReadStandardOutput()
{
if (m_pDeps->process()) {
handleLogLines(m_pDeps->process().readAllStandardOutput());
if (m_pProcess) {
handleLogLines(m_pProcess->readAllStandardOutput());
}
}
void CoreProcess::onProcessReadyReadStandardError()
{
if (m_pDeps->process()) {
handleLogLines(m_pDeps->process().readAllStandardError());
if (m_pProcess) {
handleLogLines(m_pProcess->readAllStandardError());
}
}
@ -250,9 +231,9 @@ void CoreProcess::startForegroundProcess(const QString &app, const QStringList &
const auto quoted = makeQuotedArgs(app, args);
qInfo("running command: %s", qPrintable(quoted));
m_pDeps->process().start(app, args);
m_pProcess->start(app, args);
if (m_pDeps->process().waitForStarted()) {
if (m_pProcess->waitForStarted()) {
setProcessState(Started);
} else {
setProcessState(Stopped);
@ -284,15 +265,15 @@ void CoreProcess::stopForegroundProcess() const
qFatal("core process must be in stopping state");
}
if (!m_pDeps->process()) {
if (!m_pProcess) {
qFatal("process not set, cannot stop");
}
qInfo("stopping core desktop process");
if (m_pDeps->process().state() == QProcess::ProcessState::Running) {
if (m_pProcess->state() == QProcess::ProcessState::Running) {
qDebug("process is running, closing");
m_pDeps->process().close();
m_pProcess->close();
} else {
qDebug("process is not running, skipping terminate");
}
@ -365,7 +346,16 @@ void CoreProcess::start(std::optional<ProcessMode> processModeOption)
setConnectionState(ConnectionState::Connecting);
if (processMode == ProcessMode::Desktop) {
m_pDeps->process().create();
m_pProcess = new QProcess(this);
connect(m_pProcess, &QProcess::finished, this, &CoreProcess::onProcessFinished, Qt::UniqueConnection);
connect(
m_pProcess, &QProcess::readyReadStandardOutput, this, &CoreProcess::onProcessReadyReadStandardOutput,
Qt::UniqueConnection
);
connect(
m_pProcess, &QProcess::readyReadStandardError, this, &CoreProcess::onProcessReadyReadStandardError,
Qt::UniqueConnection
);
}
QString app;
@ -489,9 +479,9 @@ bool CoreProcess::addGenericArgs(QStringList &args, const ProcessMode processMod
bool CoreProcess::addServerArgs(QStringList &args, QString &app)
{
app = m_pDeps->appPath(Settings::value(Settings::Server::Binary).toString());
app = getAppFilePath(Settings::value(Settings::Server::Binary).toString());
if (!m_pDeps->fileExists(app)) {
if (!QFile::exists(app)) {
qFatal("core server binary does not exist");
return false;
}
@ -535,9 +525,9 @@ bool CoreProcess::addServerArgs(QStringList &args, QString &app)
bool CoreProcess::addClientArgs(QStringList &args, QString &app)
{
app = m_pDeps->appPath(Settings::value(Settings::Client::Binary).toString());
app = getAppFilePath(Settings::value(Settings::Client::Binary).toString());
if (!m_pDeps->fileExists(app)) {
if (!QFile::exists(app)) {
qFatal("core client binary does not exist");
return false;
}

View File

@ -9,13 +9,11 @@
#include "common/Settings.h"
#include "gui/FileTail.h"
#include "gui/config/IServerConfig.h"
#include "gui/proxy/QProcessProxy.h"
#include <memory>
#include <QFileSystemWatcher>
#include <QMutex>
#include <QObject>
#include <QProcess>
#include <QString>
#include <QStringList>
#include <QTimer>
@ -30,25 +28,10 @@ class CoreProcess : public QObject
{
using ProcessMode = Settings::ProcessMode;
using IServerConfig = deskflow::gui::IServerConfig;
using QProcessProxy = deskflow::gui::proxy::QProcessProxy;
Q_OBJECT
public:
struct Deps
{
virtual ~Deps() = default;
virtual QProcessProxy &process()
{
return m_process;
}
virtual QString appPath(const QString &name) const;
virtual bool fileExists(const QString &path) const;
private:
QProcessProxy m_process;
};
enum class Error
{
AddressMissing,
@ -70,7 +53,7 @@ public:
Listening
};
explicit CoreProcess(const IServerConfig &serverConfig, std::shared_ptr<Deps> deps = std::make_shared<Deps>());
explicit CoreProcess(const IServerConfig &serverConfig);
void extracted(QString &app, QStringList &args);
void start(std::optional<ProcessMode> processMode = std::nullopt);
@ -154,18 +137,19 @@ private:
#endif
const IServerConfig &m_serverConfig;
std::shared_ptr<Deps> m_pDeps;
QString m_address;
ProcessState m_processState = ProcessState::Stopped;
ConnectionState m_connectionState = ConnectionState::Disconnected;
Settings::CoreMode m_mode = Settings::CoreMode::None;
QMutex m_processMutex;
QString m_secureSocketVersion = "";
QString m_secureSocketVersion;
std::optional<ProcessMode> m_lastProcessMode = std::nullopt;
QTimer m_retryTimer;
int m_connections = 0;
deskflow::gui::ipc::DaemonIpcClient *m_daemonIpcClient = nullptr;
FileTail *m_daemonFileTail = nullptr;
QProcess *m_pProcess = nullptr;
QString m_appPath;
};
} // namespace deskflow::gui

View File

@ -1,65 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2024 Symless Ltd.
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "QProcessProxy.h"
namespace deskflow::gui::proxy {
void QProcessProxy::create()
{
m_pProcess = std::make_unique<QProcess>();
connect(m_pProcess.get(), &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
Q_EMIT finished(exitCode, exitStatus);
});
connect(
m_pProcess.get(), &QProcess::readyReadStandardOutput, //
this, [this]() { Q_EMIT readyReadStandardOutput(); }
);
connect(
m_pProcess.get(), &QProcess::readyReadStandardError, //
this, [this]() { Q_EMIT readyReadStandardError(); }
);
}
QProcessProxy::operator bool() const
{
return m_pProcess.get();
}
void QProcessProxy::start(const QString &program, const QStringList &arguments)
{
m_pProcess->start(program, arguments);
}
bool QProcessProxy::waitForStarted()
{
return m_pProcess->waitForStarted();
}
QProcess::ProcessState QProcessProxy::state() const
{
return m_pProcess->state();
}
void QProcessProxy::close()
{
m_pProcess->close();
}
QString QProcessProxy::readAllStandardOutput()
{
return m_pProcess->readAllStandardOutput();
}
QString QProcessProxy::readAllStandardError()
{
return m_pProcess->readAllStandardError();
}
} // namespace deskflow::gui::proxy

View File

@ -1,38 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2024 Symless Ltd.
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#pragma once
#include <QObject>
#include <QProcess>
namespace deskflow::gui::proxy {
class QProcessProxy : public QObject
{
Q_OBJECT
public:
~QProcessProxy() override = default;
explicit virtual operator bool() const;
virtual void create();
virtual void start(const QString &program, const QStringList &arguments = {});
virtual bool waitForStarted();
virtual QProcess::ProcessState state() const;
virtual void close();
virtual QString readAllStandardOutput();
virtual QString readAllStandardError();
Q_SIGNALS:
void finished(int exitCode, QProcess::ExitStatus exitStatus);
void readyReadStandardOutput();
void readyReadStandardError();
private:
std::unique_ptr<QProcess> m_pProcess;
};
} // namespace deskflow::gui::proxy

View File

@ -62,11 +62,6 @@ macro(set_sources)
list(APPEND sources ${PROJECT_SOURCE_DIR}/src/apps/res/deskflow.qrc)
# Hack this test is broken on CI ARM64
if(WIN32 AND BUILD_ARCHITECTURE MATCHES "arm64")
list(REMOVE_ITEM sources ${test_base_dir}/legacytests/gui/core/CoreProcessTests.cpp)
endif()
replace_platform_sources()
replace_arch_sources()

View File

@ -1,133 +0,0 @@
/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2024 Symless Ltd.
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
*/
#include "common/Settings.h"
#include "gui/core/CoreProcess.h"
#include "gui/proxy/QProcessProxy.h"
#include "unittests/legacytests/shared/gui/mocks/ServerConfigMock.h"
#include "gmock/gmock.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
using namespace deskflow::gui;
using namespace testing;
namespace {
class QProcessProxyMock : public proxy::QProcessProxy
{
public:
operator bool() const override
{
return toBool();
}
QProcessProxyMock()
{
ON_CALL(*this, toBool()).WillByDefault(Return(true));
ON_CALL(*this, state()).WillByDefault(Return(QProcess::ProcessState::Running));
ON_CALL(*this, waitForStarted()).WillByDefault(Return(true));
}
MOCK_METHOD(bool, toBool, (), (const));
MOCK_METHOD(void, create, (), (override));
MOCK_METHOD(void, start, (const QString &program, const QStringList &arguments), (override));
MOCK_METHOD(bool, waitForStarted, (), (override));
MOCK_METHOD(QProcess::ProcessState, state, (), (const, override));
MOCK_METHOD(void, close, (), (override));
MOCK_METHOD(QString, readAllStandardOutput, (), (override));
MOCK_METHOD(QString, readAllStandardError, (), (override));
};
class DepsMock : public CoreProcess::Deps
{
public:
DepsMock()
{
ON_CALL(*this, process()).WillByDefault(ReturnRef(m_process));
ON_CALL(*this, appPath(_)).WillByDefault(Return("stub app path"));
ON_CALL(*this, fileExists(_)).WillByDefault(Return(true));
}
MOCK_METHOD(proxy::QProcessProxy &, process, (), (override));
MOCK_METHOD(QString, appPath, (const QString &name), (const, override));
MOCK_METHOD(bool, fileExists, (const QString &path), (const, override));
NiceMock<QProcessProxyMock> m_process;
};
class CoreProcessTests : public Test
{
public:
CoreProcessTests() : m_coreProcess(m_serverConfig, m_pDeps)
{
Settings::setValue(Settings::Server::ExternalConfig, true);
Settings::setValue(Settings::Server::ExternalConfigFile, m_configFile);
Settings::setValue(Settings::Core::ProcessMode, Settings::ProcessMode::Desktop);
}
NiceMock<ServerConfigMock> m_serverConfig;
std::shared_ptr<NiceMock<DepsMock>> m_pDeps = std::make_shared<NiceMock<DepsMock>>();
CoreProcess m_coreProcess;
private:
const QString m_configFile = "tmp/deskflow-server.conf";
};
} // namespace
TEST_F(CoreProcessTests, start_serverDesktop_callsProcessStart)
{
m_coreProcess.setMode(Settings::CoreMode::Server);
EXPECT_CALL(m_pDeps->m_process, start(_, _)).Times(1);
m_coreProcess.start(Settings::ProcessMode::Desktop);
}
TEST_F(CoreProcessTests, start_clientDesktop_callsProcessStart)
{
m_coreProcess.setMode(Settings::CoreMode::Client);
m_coreProcess.setAddress("stub address");
EXPECT_CALL(m_pDeps->m_process, start(_, _)).Times(1);
m_coreProcess.start(Settings::ProcessMode::Desktop);
}
TEST_F(CoreProcessTests, stop_serverDesktop_callsProcessClose)
{
m_coreProcess.setMode(Settings::CoreMode::Server);
m_coreProcess.start();
EXPECT_CALL(m_pDeps->m_process, close()).Times(1);
m_coreProcess.stop(Settings::ProcessMode::Desktop);
}
TEST_F(CoreProcessTests, stop_clientDesktop_callsProcessClose)
{
m_coreProcess.setMode(Settings::CoreMode::Client);
m_coreProcess.setAddress("stub address");
m_coreProcess.start();
EXPECT_CALL(m_pDeps->m_process, close()).Times(1);
m_coreProcess.stop(Settings::ProcessMode::Desktop);
}
TEST_F(CoreProcessTests, restart_serverDesktop_callsProcessStart)
{
Settings::setValue(Settings::Core::ProcessMode, Settings::ProcessMode::Desktop);
m_coreProcess.setMode(Settings::CoreMode::Server);
m_coreProcess.start();
EXPECT_CALL(m_pDeps->m_process, close()).Times(1);
EXPECT_CALL(m_pDeps->m_process, start(_, _)).Times(1);
m_coreProcess.restart();
}