diff --git a/.env.example b/.env.example index 52f9be028..2122779da 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,12 @@ # Version checker URL to use (useful for testing) # SYNERGY_VERSION_URL="http://localhost:8787?version=v1" +# Enable debug logging in the GUI (on by default in debug mode) +# SYNERGY_GUI_DEBUG=true + +# Enable verbose logging in the GUI (always off by default) +# SYNERGY_GUI_VERBOSE=true + # # Build # diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4547b4b8d..489ba3b25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,8 +26,8 @@ env: SYNERGY_VERSION: ${{ github.event.inputs.version || github.event.release.tag_name }} SYNERGY_PRODUCT_NAME: ${{ vars.SYNERGY_PRODUCT_NAME }} SYNERGY_PACKAGE_PREFIX: ${{ vars.SYNERGY_PACKAGE_PREFIX }} - SYNERGY_ENABLE_LICENSING: ${{ vars.SYNERGY_ENABLE_LICENSING }} - SYNERGY_SHOW_DEV_THANKS: ${{ vars.SYNERGY_SHOW_DEV_THANKS }} + SYNERGY_LICENSED_PRODUCT: ${{ vars.SYNERGY_LICENSED_PRODUCT }} + SYNERGY_ENABLE_ACTIVATION: ${{ vars.SYNERGY_ENABLE_ACTIVATION }} PACKAGE_BUILD: ${{ !github.event.pull_request.draft }} PACKAGE_UPLOAD: ${{ !github.event.pull_request.draft && github.event_name != 'schedule' }} UPLOAD_TO_GITHUB: ${{ github.event_name == 'pull_request' && !github.event.pull_request.draft }} diff --git a/.vscode/launch.json b/.vscode/launch.json index f0639c554..b9425187d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ { "name": "gui windows", "type": "cppvsdbg", - "cwd": "${workspaceRoot}/build/bin", + "cwd": "${workspaceRoot}", "request": "launch", "program": "${workspaceFolder}/build/bin/synergy", "internalConsoleOptions": "openOnSessionStart", @@ -60,7 +60,7 @@ "preLaunchTask": "build" }, { - "name": "daemon windows attach", + "name": "windows attach", "type": "cppvsdbg", "request": "attach", "processId": "${command:pickProcess}" diff --git a/ChangeLog b/ChangeLog index 956a3488e..7ac451884 100644 --- a/ChangeLog +++ b/ChangeLog @@ -68,6 +68,7 @@ Enhancements: - #7422 Handle empty value for computer name setting - #7423 Refactor local/global save/load and show dev thanks message - #7424 Add Alma Linux and Rocky Linux runners +- #7425 Refactored core process management and IPC client # 1.14.6 diff --git a/cmake/Definitions.cmake b/cmake/Definitions.cmake index abdb659ef..5265a4b26 100644 --- a/cmake/Definitions.cmake +++ b/cmake/Definitions.cmake @@ -45,14 +45,20 @@ macro(configure_definitions) add_definitions(-DGIT_SHA_SHORT="${GIT_SHA_SHORT}") endif() - if(ENABLE_LICENSING) - message(STATUS "Licensing enabled") - add_definitions(-DSYNERGY_ENABLE_LICENSING=1) + if(NOT "$ENV{SYNERGY_PRODUCT_NAME}" STREQUAL "") + set(PRODUCT_NAME $ENV{SYNERGY_PRODUCT_NAME}) + endif() + + if(LICENSED_PRODUCT) + message(STATUS "Licensed product") + add_definitions(-DSYNERGY_LICENSED_PRODUCT=1) + + if(ENABLE_ACTIVATION) + message(STATUS "Activation enabled") + add_definitions(-DSYNERGY_ENABLE_ACTIVATION=1) + endif() else() set(PRODUCT_NAME "Synergy 1 Community Edition") - if(NOT "$ENV{SYNERGY_PRODUCT_NAME}" STREQUAL "") - set(PRODUCT_NAME $ENV{SYNERGY_PRODUCT_NAME}) - endif() endif() if(PRODUCT_NAME) @@ -110,10 +116,13 @@ macro(configure_options) # coverage is off by default because it's GCC only and a developer preference. set(DEFAULT_ENABLE_COVERAGE OFF) - # licensing is off by default to make life easier for contributors. - set(DEFAULT_ENABLE_LICENSING OFF) + # licensed product is off by default to show links to github, etc. + set(DEFAULT_LICENSED_PRODUCT OFF) - # by default, show the dev thanks message. + # activation is off by default to make life easier for contributors. + set(DEFAULT_ENABLE_ACTIVATION OFF) + + # by default, show the dev thanks message, guides contributions, etc. set(DEFAULT_SHOW_DEV_THANKS ON) if("$ENV{SYNERGY_BUILD_MINIMAL}" STREQUAL "true") @@ -129,8 +138,12 @@ macro(configure_options) set(DEFAULT_BUILD_UNIFIED ON) endif() - if("$ENV{SYNERGY_ENABLE_LICENSING}" STREQUAL "true") - set(DEFAULT_ENABLE_LICENSING ON) + if("$ENV{SYNERGY_ENABLE_ACTIVATION}" STREQUAL "true") + set(DEFAULT_ENABLE_ACTIVATION ON) + endif() + + if("$ENV{SYNERGY_LICENSED_PRODUCT}" STREQUAL "true") + set(DEFAULT_LICENSED_PRODUCT ON) endif() if("$ENV{SYNERGY_ENABLE_COVERAGE}" STREQUAL "true") @@ -145,7 +158,8 @@ macro(configure_options) option(BUILD_INSTALLER "Build installer" ${DEFAULT_BUILD_INSTALLER}) option(BUILD_TESTS "Build tests" ${DEFAULT_BUILD_TESTS}) option(BUILD_UNIFIED "Build unified binary" ${DEFAULT_BUILD_UNIFIED}) - option(ENABLE_LICENSING "Enable licensing" ${DEFAULT_ENABLE_LICENSING}) + option(ENABLE_ACTIVATION "Enable activation" ${DEFAULT_ENABLE_ACTIVATION}) + option(LICENSED_PRODUCT "Show licensing info" ${DEFAULT_LICENSED_PRODUCT}) option(ENABLE_COVERAGE "Enable test coverage" ${DEFAULT_ENABLE_COVERAGE}) option(SHOW_DEV_THANKS "Show dev thanks message" ${DEFAULT_SHOW_DEV_THANKS}) diff --git a/cspell.json b/cspell.json index 1477c86f9..a9b1eaa99 100644 --- a/cspell.json +++ b/cspell.json @@ -4,23 +4,46 @@ "dictionaryDefinitions": [], "dictionaries": [], "words": [ - "Schoeneman", "aqtinstall", + "Axelson", + "Breen", "codesign", "codesigning", "contribs", + "Daun", "distros", "dmgbuild", "dotenv", + "Evenson", + "Feder", "gdrive", + "Hadzhylov", + "Hetu", "keychain", "Keychains", + "Kutytska", + "Lanz", "LLDB", + "Lysytsia", "macdeployqt", "msvc", + "noquote", "notarytool", + "Oleksandr", + "Olena", "outputdir", + "Petroules", + "Poschta", + "Priddy", "runas", + "Sbârnea", + "Schoeneman", + "Serhii", + "Sorin", + "synergyc", + "synergyd", + "synergys", + "Volker", "winget" ], "ignoreWords": [], diff --git a/scripts/install_deps.py b/scripts/install_deps.py index 8333dd723..b4595bbec 100755 --- a/scripts/install_deps.py +++ b/scripts/install_deps.py @@ -133,9 +133,12 @@ class Dependencies: print("Running dependencies prerequisites command") check = True - if distro_like and "fedora" in distro_like: - print("Fedora-like detected, ignoring return code") - # On Fedora, dnf update returns code 100 when updates are available. + if distro == "fedora" or (distro_like and "fedora" in distro_like): + print( + "Fedora-like detected, " + "ignoring return code on dependencies prerequisites command" + ) + # On Fedora-like, dnf update returns code 100 when updates are available. check = False linux.run_command(command_pre, check) diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 68f1bf64c..9bffab945 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -29,11 +29,6 @@ file( src/widgets/*) file(GLOB GUI_UI_FILES src/*.ui) -if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") - file(GLOB GUI_MAC_SOURCE_FILES src/*.mm) - list(APPEND GUI_SOURCE_FILES ${GUI_MAC_SOURCE_FILES}) -endif() - if(WIN32) set(GUI_RC_FILES res/win/Synergy.rc ${CMAKE_BINARY_DIR}/src/version.rc) endif() diff --git a/src/gui/src/AboutDialog.cpp b/src/gui/src/AboutDialog.cpp index 9b28eaea2..c523ffde1 100644 --- a/src/gui/src/AboutDialog.cpp +++ b/src/gui/src/AboutDialog.cpp @@ -21,7 +21,7 @@ #include #if defined(Q_OS_MAC) -#include "OSXHelpers.h" +#include "gui/OSXHelpers.h" #endif #include "gui/version.h" @@ -29,8 +29,11 @@ AboutDialog::AboutDialog(MainWindow *parent) : QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint), Ui::AboutDialogBase() { + setupUi(this); + this->setFixedSize(this->size()); + m_pLabelSynergyVersion->setText(synergy::gui::version()); QString buildDateString = QString::fromLocal8Bit(__DATE__).simplified(); @@ -59,21 +62,47 @@ void AboutDialog::updateLogo() const { } QString AboutDialog::getImportantDevelopers() const { - return QString( - // The ultimate creator - "Chris Schoeneman, " + QStringList awesomePeople; - // Precursor developers - "Richard Lee, Adam Feder, " + // Chris is the ultimate creator, and the one who started it all in 2001. + awesomePeople << "Chris Schoeneman"; - // Contributors - "Nick Bolton, Volker Lanz, Ryan Breen, Guido Poschta, " - "Bertrand Landry Hetu, Tom Chadwick, Brent Priddy, Jason Axelson, " - "Jake Petroules, Sorin Sbârnea, " + // Richard and Adam developed CosmoSynergy, the 90's predecessor project. + awesomePeople << "Richard Lee" << "Adam Feder"; - // Symless employees - "Kyle Bloom, Daun Chung, Serhii Hadzhylov, " - "Oleksandr Lysytsia, Olena Kutytska."); + // Nick continued the legacy in 2009 started by Chris. + awesomePeople << "Nick Bolton"; + + // Volker wrote the first version of the GUI (QSynergy) in 2008. + awesomePeople << "Volker Lanz"; + + // Re-ignited the project in 2008 and rebuilt the community. + awesomePeople << "Sorin Sbârnea"; + + // Contributors of bug fixes in the early days. + awesomePeople << "Ryan Breen" + << "Guido Poschta" + << "Bertrand Landry Hetu" + << "Tom Chadwick" + << "Brent Priddy" + << "Jason Axelson" + << "Jake Petroules"; + + // Symless employees (in order of joining). + awesomePeople << "Kyle Bloom" + << "Daun Chung" + << "Serhii Hadzhylov" + << "Oleksandr Lysytsia" + << "Olena Kutytska" + << "Owen Phillips" + << "Daniel Evenson"; + + for (auto &person : awesomePeople) { + // prevent names from breaking on the space when wrapped + person = person.replace(" ", QString::fromUtf8(" ")); + } + + return awesomePeople.join(", ") + "."; } QString AboutDialog::getCopyright() const { diff --git a/src/gui/src/AboutDialogBase.ui b/src/gui/src/AboutDialogBase.ui index de6b09e07..0a52a02ad 100644 --- a/src/gui/src/AboutDialogBase.ui +++ b/src/gui/src/AboutDialogBase.ui @@ -12,28 +12,10 @@ 0 0 - 450 - 495 + 600 + 400 - - - 0 - 0 - - - - - 450 - 495 - - - - - 450 - 495 - - About Synergy @@ -90,7 +72,6 @@ - 75 true @@ -161,6 +142,9 @@ Developers + + Qt::RichText + true diff --git a/src/gui/src/ActivationDialog.cpp b/src/gui/src/ActivationDialog.cpp index 24c3a409a..05ef96bfc 100644 --- a/src/gui/src/ActivationDialog.cpp +++ b/src/gui/src/ActivationDialog.cpp @@ -19,10 +19,10 @@ #include "CancelActivationDialog.h" #include "MainWindow.h" -#include "gui/AppConfig.h" -#include "gui/LicenseHandler.h" +#include "gui/config/AppConfig.h" #include "gui/constants.h" -#include "gui/license_notices.h" +#include "gui/license/LicenseHandler.h" +#include "gui/license/license_notices.h" #include "gui/styles.h" #include "license/ProductEdition.h" #include "license/parse_serial_key.h" diff --git a/src/gui/src/ActivationDialog.h b/src/gui/src/ActivationDialog.h index ec3f3d31b..b44a0fcc9 100644 --- a/src/gui/src/ActivationDialog.h +++ b/src/gui/src/ActivationDialog.h @@ -17,7 +17,7 @@ #pragma once -#include "gui/LicenseHandler.h" +#include "gui/license/LicenseHandler.h" #include @@ -42,8 +42,8 @@ public: }; public slots: - void reject(); - void accept(); + void reject() override; + void accept() override; protected: void refreshSerialKey(); diff --git a/src/gui/src/MainWindow.cpp b/src/gui/src/MainWindow.cpp index 021e9c13c..0f5640808 100644 --- a/src/gui/src/MainWindow.cpp +++ b/src/gui/src/MainWindow.cpp @@ -22,18 +22,21 @@ #include "ActivationDialog.h" #include "ServerConfigDialog.h" #include "SettingsDialog.h" -#include "gui/ConfigScopes.h" -#include "gui/LicenseHandler.h" -#include "gui/TlsFingerprint.h" +#include "gui/Logger.h" +#include "gui/TrayIcon.h" #include "gui/VersionChecker.h" +#include "gui/config/ConfigScopes.h" #include "gui/constants.h" -#include "gui/license_notices.h" +#include "gui/core/CoreProcess.h" +#include "gui/license/LicenseHandler.h" +#include "gui/license/license_notices.h" #include "gui/messages.h" #include "gui/styles.h" +#include "gui/tls/TlsFingerprint.h" #include "license/License.h" #if defined(Q_OS_MAC) -#include "OSXHelpers.h" +#include "gui/OSXHelpers.h" #endif #include @@ -45,11 +48,10 @@ #include #include #include +#include #include #include #include -#include -#include #if defined(Q_OS_MAC) #include @@ -58,8 +60,9 @@ using namespace synergy::gui; using namespace synergy::license; -static const int kRetryDelay = 1000; -static const int kDebugLogLevel = 1; +using CoreMode = CoreProcess::Mode; +using CoreConnectionState = CoreProcess::ConnectionState; +using CoreProcessState = CoreProcess::ProcessState; #if defined(Q_OS_MAC) @@ -90,8 +93,9 @@ MainWindow::MainWindow(ConfigScopes &configScopes, AppConfig &appConfig) : m_ConfigScopes(configScopes), m_AppConfig(appConfig), m_ServerConfig(appConfig, *this), - m_ServerConnection(*this), - m_ClientConnection(*this), + m_CoreProcess(appConfig, m_ServerConfig), + m_ServerConnection(*this, appConfig, m_ServerConfig), + m_ClientConnection(*this, appConfig), m_TlsUtility(appConfig, m_LicenseHandler.license()), m_WindowSaveTimer(this) { @@ -104,20 +108,13 @@ MainWindow::MainWindow(ConfigScopes &configScopes, AppConfig &appConfig) } MainWindow::~MainWindow() { - try { - if (m_AppConfig.processMode() == ProcessMode::kDesktop) { - m_ExpectedRunningState = RuningState::Stopped; - stopDesktop(); - } - } catch (const std::exception &e) { - qFatal("failed to stop core on main window close: %s", e.what()); - } - try { saveWindow(); } catch (const std::exception &e) { qFatal("failed to save window on main window close: %s", e.what()); } + + m_CoreProcess.cleanup(); } void MainWindow::restoreWindow() { @@ -153,6 +150,10 @@ void MainWindow::saveWindow() { } void MainWindow::setupControls() { + if (!kEnableActivation) { + updateWindowTitle(); + } + createMenuBar(); secureSocket(false); @@ -169,7 +170,7 @@ void MainWindow::setupControls() { m_AppConfig.setLastVersion(SYNERGY_VERSION); } - if (kLicensingEnabled) { + if (kEnableActivation) { m_pActivate->setVisible(true); } @@ -188,7 +189,11 @@ void MainWindow::setupControls() { // remember: using queued connection allows the render loop to run before // executing the slot. the default is to instantly call the slot when the // signal is emitted from the thread that owns the receiver's object. -void MainWindow::connectSlots() const { +void MainWindow::connectSlots() { + + connect( + &Logger::instance(), &Logger::newLine, this, // + [this](const QString &line) { handleLogLine(line); }); connect(this, &MainWindow::created, this, &MainWindow::onCreated); @@ -212,6 +217,30 @@ void MainWindow::connectSlots() const { &m_AppConfig, &AppConfig::invertConnectionChanged, this, &MainWindow::onAppConfigInvertConnection); + connect( + &m_CoreProcess, &CoreProcess::starting, this, + &MainWindow::onCoreProcessStarting, Qt::DirectConnection); + + connect( + &m_CoreProcess, &CoreProcess::error, this, + &MainWindow::onCoreProcessError); + + connect( + &m_CoreProcess, &CoreProcess::logLine, // + [this](const QString &line) { handleLogLine(line); }); + + connect( + &m_CoreProcess, &CoreProcess::processStateChanged, this, + &MainWindow::onCoreProcessStateChanged); + + connect( + &m_CoreProcess, &CoreProcess::connectionStateChanged, this, + &MainWindow::onCoreConnectionStateChanged); + + connect( + &m_CoreProcess, &CoreProcess::secureSocket, this, + &MainWindow::onCoreProcessSecureSocket); + connect( &m_LicenseHandler, &LicenseHandler::serialKeyChanged, this, &MainWindow::onLicenseHandlerSerialKeyChanged); @@ -220,18 +249,6 @@ void MainWindow::connectSlots() const { &m_LicenseHandler, &LicenseHandler::invalidLicense, this, &MainWindow::onLicenseHandlerInvalidLicense); - connect( - &m_IpcClient, &QIpcClient::readLogLine, this, - &MainWindow::onIpcClientReadLogLine); - - connect( - &m_IpcClient, &QIpcClient::errorMessage, this, - &MainWindow::onIpcClientErrorMessage); - - connect( - &m_IpcClient, &QIpcClient::infoMessage, this, - &MainWindow::onIpcClientInfoMessage); - connect(m_pActionMinimize, &QAction::triggered, this, &MainWindow::hide); connect(m_pActionRestore, &QAction::triggered, this, &MainWindow::showNormal); @@ -253,26 +270,27 @@ void MainWindow::connectSlots() const { connect( &m_WindowSaveTimer, &QTimer::timeout, this, &MainWindow::onWindowSaveTimerTimeout); + + connect( + &m_TrayIcon, &TrayIcon::activated, this, + &MainWindow::onTrayIconActivated); + + connect( + &m_ServerConnection, &ServerConnection::configureClient, this, + &MainWindow::onServerConnectionConfigureClient); } void MainWindow::onAppAboutToQuit() { m_ConfigScopes.save(); } void MainWindow::onCreated() { -#if defined(Q_OS_WIN) - - // TODO: only connect permenantly to ipc when switching to service mode. - // if switching from service to desktop, connect only to stop the service - // and don't retry. - m_IpcClient.connectToHost(); - -#endif + setIcon(CoreConnectionState::Disconnected); m_ConfigScopes.signalReady(); applyCloseToTray(); - if (kLicensingEnabled && !m_AppConfig.serialKey().isEmpty()) { + if (kEnableActivation && !m_AppConfig.serialKey().isEmpty()) { m_LicenseHandler.changeSerialKey(m_AppConfig.serialKey()); } @@ -282,7 +300,7 @@ void MainWindow::onCreated() { } void MainWindow::onShown() { - if (kLicensingEnabled) { + if (kEnableActivation) { const auto &license = m_LicenseHandler.license(); if (!m_AppConfig.activationHasRun() || !license.isValid() || license.isExpired()) { @@ -292,7 +310,7 @@ void MainWindow::onShown() { } void MainWindow::onLicenseHandlerSerialKeyChanged(const QString &serialKey) { - setWindowTitle(m_LicenseHandler.productName()); + updateWindowTitle(); showLicenseNotice(); if (m_AppConfig.serialKey() != serialKey) { @@ -302,7 +320,7 @@ void MainWindow::onLicenseHandlerSerialKeyChanged(const QString &serialKey) { } void MainWindow::onLicenseHandlerInvalidLicense() { - stopCore(); + m_CoreProcess.stop(); showActivationDialog(); } @@ -311,23 +329,12 @@ void MainWindow::onConfigScopesSaving() { m_ServerConfig.commit(); } void MainWindow::onAppConfigTlsChanged() { updateLocalFingerprint(); if (m_TlsUtility.isAvailableAndEnabled()) { - m_TlsUtility.generateCertificate(true); + m_TlsUtility.generateCertificate(); } } -void MainWindow::onIpcClientReadLogLine(const QString &text) { - processCoreLogLine(text); -} +void MainWindow::onTrayIconActivated(QSystemTrayIcon::ActivationReason reason) { -void MainWindow::onIpcClientErrorMessage(const QString &text) { - appendLogError(text); -} - -void MainWindow::onIpcClientInfoMessage(const QString &text) { - appendLogInfo(text); -} - -void MainWindow::onTrayIconCreate(QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::DoubleClick) { if (isVisible()) { hide(); @@ -338,23 +345,6 @@ void MainWindow::onTrayIconCreate(QSystemTrayIcon::ActivationReason reason) { } } -void MainWindow::onCoreProcessReadyReadStandardOutput() { - if (m_pCoreProcess) { - QString text(m_pCoreProcess->readAllStandardOutput()); - for (QString line : text.split(QRegularExpression("\r|\n|\r\n"))) { - if (!line.isEmpty()) { - processCoreLogLine(line); - } - } - } -} - -void MainWindow::onCoreProcessReadyReadStandardError() { - if (m_pCoreProcess) { - processCoreLogLine(m_pCoreProcess->readAllStandardError()); - } -} - void MainWindow::onVersionCheckerUpdateFound(const QString &version) { const auto link = QString(kLinkDownload).arg(kUrlDownload, kColorWhite); const auto text = @@ -366,43 +356,31 @@ void MainWindow::onVersionCheckerUpdateFound(const QString &version) { void MainWindow::onActionStartCoreTriggered() { m_ClientConnection.setCheckConnection(true); - startCore(); + m_CoreProcess.start(); } -void MainWindow::onCoreProcessRetryStart() { - // This function is only called after a failed start - // Only start synergy if the current state is pending retry - if (m_CoreState == CoreState::PendingRetry) { - startCore(); - } +void MainWindow::onActionStopCoreTriggered() { + qDebug("stopping core process"); + m_CoreProcess.stop(); } -void MainWindow::onActionStopCoreTriggered() { stopCore(); } - void MainWindow::onAppConfigScreenNameChanged() { updateScreenName(); } void MainWindow::onAppConfigInvertConnection() { applyConfig(); } -void MainWindow::onCoreProcessFinished(int exitCode, QProcess::ExitStatus) { - if (exitCode == 0) { - appendLogInfo("process exited normally"); - } else { - appendLogError(QString("process exited with error code: %1").arg(exitCode)); - } - - if (m_ExpectedRunningState == RuningState::Started) { - - if (m_CoreState != CoreState::PendingRetry) { - QTimer::singleShot( - kRetryDelay, this, &MainWindow::onCoreProcessRetryStart); - appendLogInfo("detected process not running, auto restarting"); - } else { - appendLogInfo("detected process not running, already auto restarting"); - } - - setCoreState(CoreState::PendingRetry); - } else { - setCoreState(CoreState::Disconnected); +void MainWindow::onCoreProcessError(CoreProcess::Error error) { + if (error == CoreProcess::Error::AddressMissing) { + QMessageBox::warning( + this, QString("Address missing"), + QString( + "Please enter the hostname or IP address of the other computer.")); + } else if (error == CoreProcess::Error::StartFailed) { + show(); + QMessageBox::warning( + this, QString("Core cannot be started"), + "The Core executable could not be successfully started, " + "although it does exist. " + "Please check if you have sufficient permissions to run this program."); } } @@ -438,8 +416,8 @@ void MainWindow::on_m_pActionSettings_triggered() { applyConfig(); applyCloseToTray(); - if (isCoreActive()) { - restartCore(); + if (m_CoreProcess.isStarted()) { + m_CoreProcess.restart(); } } } @@ -458,9 +436,17 @@ void MainWindow::on_m_pLineEditClientIp_returnPressed() { m_pButtonConnectToClient->click(); } +void MainWindow::on_m_pLineEditHostname_textChanged(const QString &text) { + m_CoreProcess.setAddress(text); +} + +void MainWindow::on_m_pLineEditClientIp_textChanged(const QString &text) { + m_CoreProcess.setAddress(text); +} + void MainWindow::on_m_pButtonApply_clicked() { m_ClientConnection.setCheckConnection(true); - restartCore(); + m_CoreProcess.restart(); } void MainWindow::on_m_pLabelComputerName_linkActivated(const QString &) { @@ -492,6 +478,13 @@ void MainWindow::on_m_pButtonConnectToClient_clicked() { void MainWindow::onWindowSaveTimerTimeout() { saveWindow(); } +void MainWindow::onServerConnectionConfigureClient(const QString &clientName) { + ServerConfigDialog dialog(this, m_ServerConfig, m_AppConfig); + if (dialog.addClient(clientName) && dialog.exec() == QDialog::Accepted) { + m_CoreProcess.restart(); + } +} + ////////////////////////////////////////////////////////////////////////////// // End slots ////////////////////////////////////////////////////////////////////////////// @@ -514,16 +507,11 @@ void MainWindow::moveEvent(QMoveEvent *event) { void MainWindow::open() { - std::array trayMenu = { + std::vector trayMenu = { m_pActionStartCore, m_pActionStopCore, nullptr, m_pActionMinimize, m_pActionRestore, nullptr, m_pActionQuit}; - // TODO: this seems like a hack that needs investigating... - m_TrayIcon.create(trayMenu, [this](QObject const *o, const char *s) { - connect( - o, s, this, SLOT(onTrayIconCreate(QSystemTrayIcon::ActivationReason))); - setIcon(CoreState::Disconnected); - }); + m_TrayIcon.create(trayMenu); if (m_AppConfig.autoHide()) { hide(); @@ -533,15 +521,25 @@ void MainWindow::open() { m_VersionChecker.checkLatest(); - // only start if user has previously started. this stops the gui from - // auto hiding before the user has configured synergy (which of course - // confuses first time users, who think synergy has crashed). - if (m_AppConfig.startedBefore() && - m_AppConfig.processMode() == ProcessMode::kDesktop) { - startCore(); + if (m_AppConfig.startedBefore()) { + m_CoreProcess.start(); } } +void MainWindow::onCoreProcessStarting() { + + if (kEnableActivation) { + const auto &license = m_LicenseHandler.license(); + if (license.isExpired() && showActivationDialog() == QDialog::Rejected) { + qDebug("license expired, cancelling core start"); + m_CoreProcess.stop(); + return; + } + } + + saveSettings(); +} + void MainWindow::setStatus(const QString &status) { m_pStatusLabel->setText(status); } @@ -598,7 +596,7 @@ void MainWindow::saveSettings() { m_ConfigScopes.save(); } -void MainWindow::setIcon(CoreState state) const { +void MainWindow::setIcon(CoreConnectionState state) { QIcon icon; auto index = static_cast(state); @@ -620,69 +618,38 @@ void MainWindow::setIcon(CoreState state) const { icon.addFile(kDefaultIconFiles[index]); #endif - m_TrayIcon.set(icon); + m_TrayIcon.setIcon(icon); } -void MainWindow::appendLogInfo(const QString &text) { - qInfo("%s", text.toStdString().c_str()); +void MainWindow::handleLogLine(const QString &line) { + const int kScrollBottomThreshold = 2; - processCoreLogLine(getTimeStamp() + " INFO: " + text); -} + QScrollBar *verticalScroll = m_pLogOutput->verticalScrollBar(); + int currentScroll = verticalScroll->value(); + int maxScroll = verticalScroll->maximum(); + const auto scrollAtBottom = + qAbs(currentScroll - maxScroll) <= kScrollBottomThreshold; -void MainWindow::appendLogDebug(const QString &text) { - qDebug("%s", text.toStdString().c_str()); + m_pLogOutput->appendPlainText(line); - if (m_AppConfig.logLevel() >= kDebugLogLevel) { - processCoreLogLine(getTimeStamp() + " DEBUG: " + text); + if (scrollAtBottom) { + verticalScroll->setValue(verticalScroll->maximum()); + m_pLogOutput->horizontalScrollBar()->setValue(0); } -} -void MainWindow::appendLogError(const QString &text) { - qCritical("%s", text.toStdString().c_str()); - processCoreLogLine(getTimeStamp() + " ERROR: " + text); -} - -void MainWindow::processCoreLogLine(const QString &text) { - foreach (QString line, text.split(QRegularExpression("\r|\n|\r\n"))) { - if (line.isEmpty()) { - continue; - } - - // only start if there is no active service running - if (line.contains("service status: idle") && m_AppConfig.startedBefore()) { - startCore(); - } - - // HACK: macOS 10.13.4+ spamming error lines in logs making them - // impossible to read and debug; giving users a red herring. - if (line.contains("calling TIS/TSM in non-main thread environment")) { - continue; - } - - m_pLogOutput->appendPlainText(line); - updateFromLogLine(line); - } + updateFromLogLine(line); } void MainWindow::updateFromLogLine(const QString &line) { - // TODO: This shouldn't be updating from log needs a better way of doing this checkConnected(line); checkFingerprint(line); - checkSecureSocket(line); - // subprocess (synergys, synergyc) is not allowed to show notifications - // process the log from it and show notificatino from synergy instead -#ifdef Q_OS_MAC - checkOSXNotification(line); -#endif - - if (kLicensingEnabled) { + if (kEnableActivation) { checkLicense(line); } } void MainWindow::checkConnected(const QString &line) { - // TODO: implement ipc connection state messages to replace this hack. if (m_pRadioGroupServer->isChecked()) { m_ServerConnection.update(line); m_pLabelServerState->updateServerState(line); @@ -690,24 +657,6 @@ void MainWindow::checkConnected(const QString &line) { m_ClientConnection.update(line); m_pLabelClientState->updateClientState(line); } - - if (line.contains("connected to server") || line.contains("has connected")) { - setCoreState(CoreState::Connected); - - if (isVisible()) { - showFirstRunMessage(); - showDevThanksMessage(); - } - - } else if (line.contains("started server")) { - setCoreState(CoreState::Listening); - } else if ( - line.contains("disconnected from server") || - line.contains("process exited")) { - setCoreState(CoreState::Disconnected); - } else if (line.contains("connecting to")) { - setCoreState(CoreState::Connecting); - } } void MainWindow::checkLicense(const QString &line) { @@ -731,7 +680,7 @@ void MainWindow::checkFingerprint(const QString &line) { static bool messageBoxAlreadyShown = false; if (!messageBoxAlreadyShown) { - onActionStopCoreTriggered(); + m_CoreProcess.stop(); messageBoxAlreadyShown = true; QMessageBox::StandardButton fingerprintReply = QMessageBox::information( @@ -749,56 +698,20 @@ void MainWindow::checkFingerprint(const QString &line) { QMessageBox::Yes | QMessageBox::No); if (fingerprintReply == QMessageBox::Yes) { - // restart core process after trusting fingerprint. + // start core process again after trusting fingerprint. TlsFingerprint::trustedServers().trust(fingerprint); - startCore(); + m_CoreProcess.start(); } messageBoxAlreadyShown = false; } } -bool MainWindow::checkSecureSocket(const QString &line) { - static const QString tlsCheckString = "network encryption protocol: "; - const auto index = line.indexOf(tlsCheckString, 0, Qt::CaseInsensitive); - if (index == -1) { - return false; - } - - secureSocket(true); - m_SecureSocketVersion = line.mid(index + tlsCheckString.size()); - return true; -} - -#ifdef Q_OS_MAC -void MainWindow::checkOSXNotification(const QString &line) { - static const QString OSXNotificationSubstring = "OSX Notification: "; - if (line.contains(OSXNotificationSubstring) && line.contains('|')) { - int delimterPosition = line.indexOf('|'); - int notificationStartPosition = line.indexOf(OSXNotificationSubstring); - QString title = line.mid( - notificationStartPosition + OSXNotificationSubstring.length(), - delimterPosition - notificationStartPosition - - OSXNotificationSubstring.length()); - QString body = - line.mid(delimterPosition + 1, line.length() - delimterPosition); - if (!showOSXNotification(title, body)) { - appendLogInfo("OSX notification was not shown"); - } - } -} -#endif - QString MainWindow::getTimeStamp() const { QDateTime current = QDateTime::currentDateTime(); return '[' + current.toString(Qt::ISODate) + ']'; } -void MainWindow::restartCore() { - stopCore(); - startCore(); -} - void MainWindow::showEvent(QShowEvent *event) { QMainWindow::showEvent(event); emit shown(); @@ -827,7 +740,7 @@ void MainWindow::showFirstRunMessage() { m_AppConfig.setStartedBefore(true); m_ConfigScopes.save(); - const auto isServer = coreMode() == CoreMode::Server; + const auto isServer = m_CoreProcess.mode() == CoreMode::Server; messages::showFirstRunMessage( this, m_AppConfig.closeToTray(), m_AppConfig.enableService(), isServer); } @@ -838,8 +751,8 @@ void MainWindow::showDevThanksMessage() { return; } - if (kLicensingEnabled) { - qDebug("licensing enabled, skipping dev thanks message"); + if (kEnableActivation) { + qDebug("activation enabled, skipping dev thanks message"); return; } @@ -849,344 +762,75 @@ void MainWindow::showDevThanksMessage() { messages::showDevThanks(this, kProductName); } -void MainWindow::startCore() { - appendLogInfo(QString("starting core %1 process").arg(coreModeString())); - - saveSettings(); - -#ifdef Q_OS_MAC - requestOSXNotificationPermission(); -#endif - - if (kLicensingEnabled) { - const auto &license = m_LicenseHandler.license(); - if (license.isExpired() && showActivationDialog() == QDialog::Rejected) { - appendLogDebug("starting core process"); - return; - } - } - - m_ExpectedRunningState = RuningState::Started; - setCoreState(CoreState::Connecting); - - QString app; - QStringList args; - - args << "-f" - << "--no-tray" - << "--debug" << m_AppConfig.logLevelText(); - - args << "--name" << m_AppConfig.screenName(); - - ProcessMode mode = m_AppConfig.processMode(); - - if (mode == ProcessMode::kDesktop) { - m_pCoreProcess = std::make_unique(this); - } else { - // tell client/server to talk to daemon through ipc. - args << "--ipc"; - -#if defined(Q_OS_WIN) - // tell the client/server to shut down when a ms windows desk - // is switched; this is because we may need to elevate or not - // based on which desk the user is in (login always needs - // elevation, where as default desk does not). - // Note that this is only enabled when synergy is set to elevate - // 'as needed' (e.g. on a UAC dialog popup) in order to prevent - // unnecessary restarts when synergy was started elevated or - // when it is not allowed to elevate. In these cases restarting - // the server is fruitless. - if (m_AppConfig.elevateMode() == ElevateAsNeeded) { - args << "--stop-on-desk-switch"; - } -#endif - } - -#ifndef Q_OS_LINUX - - if (m_ServerConfig.enableDragAndDrop()) { - args << "--enable-drag-drop"; - } - -#endif - -#if defined(Q_OS_WIN) - if (m_AppConfig.tlsEnabled()) { - args << "--enable-crypto"; - args << "--tls-cert" << m_AppConfig.tlsCertPath(); - } - - try { - // on windows, the profile directory changes depending on the user that - // launched the process (e.g. when launched with elevation). setting the - // profile dir on launch ensures it uses the same profile dir is used - // no matter how its relaunched. - args << "--profile-dir" << getProfileRootForArg(); - } catch (const std::exception &e) { - qDebug() << e.what(); - qFatal("failed to get profile dir, skipping arg"); - } - -#else - if (m_AppConfig.tlsEnabled()) { - args << "--enable-crypto"; - args << "--tls-cert" << m_AppConfig.tlsCertPath(); - } -#endif - - if (m_AppConfig.preventSleep()) { - args << "--prevent-sleep"; - } - - // put a space between last log output and new instance. - if (!m_pLogOutput->toPlainText().isEmpty()) - onIpcClientReadLogLine(""); - - if ((coreMode() == CoreMode::Client && !clientArgs(args, app)) || - (coreMode() == CoreMode::Server && !serverArgs(args, app))) { - onActionStopCoreTriggered(); - return; - } - - if (mode == ProcessMode::kDesktop) { - connect( - m_pCoreProcess.get(), &QProcess::finished, this, - &MainWindow::onCoreProcessFinished); - connect( - m_pCoreProcess.get(), &QProcess::readyReadStandardOutput, this, - &MainWindow::onCoreProcessReadyReadStandardOutput); - connect( - m_pCoreProcess.get(), &QProcess::readyReadStandardError, this, - &MainWindow::onCoreProcessReadyReadStandardError); - } - - if (m_AppConfig.logLevel() >= kDebugLogLevel) { - appendLogInfo(QString("command: %1 %2").arg(app, args.join(" "))); - } - - appendLogInfo("log level: " + m_AppConfig.logLevelText()); - - if (m_AppConfig.logToFile()) - appendLogInfo("log file: " + m_AppConfig.logFilename()); - - if (mode == ProcessMode::kDesktop) { - m_pCoreProcess->start(app, args); - if (!m_pCoreProcess->waitForStarted()) { - show(); - QMessageBox::warning( - this, QString("Program can not be started"), - QString( - QString( - "The executable

%1

could not be successfully " - "started, although it does exist. Please check if you have " - "sufficient permissions to run this program.") - .arg(app))); - return; - } - } else if (mode == ProcessMode::kService) { - QString command(app + " " + args.join(" ")); - m_IpcClient.sendCommand(command, m_AppConfig.elevateMode()); - } +void MainWindow::onCoreProcessSecureSocket(bool enabled) { + secureSocket(enabled); } -bool MainWindow::clientArgs(QStringList &args, QString &app) { - app = appPath(m_AppConfig.coreClientName()); +void MainWindow::updateStatus() { + const auto connection = m_CoreProcess.connectionState(); + const auto process = m_CoreProcess.processState(); - if (!QFile::exists(app)) { - show(); - QMessageBox::warning( - this, QString("Synergy client not found"), - QString("The executable for the synergy client does not exist.")); - return false; - } + switch (process) { + using enum CoreProcessState; - if (m_AppConfig.logToFile()) { - m_AppConfig.persistLogDir(); - args << "--log" << m_AppConfig.logFilename(); - } + case Starting: + setIcon(CoreConnectionState::Disconnected); + setStatus("Synergy is starting..."); + break; - if (m_AppConfig.languageSync()) { - args << "--sync-language"; - } + case Stopping: + setIcon(CoreConnectionState::Disconnected); + setStatus("Synergy is stopping..."); + break; - if (m_AppConfig.invertScrollDirection()) { - args << "--invert-scroll"; - } + case Stopped: + setIcon(CoreConnectionState::Disconnected); + setStatus("Synergy is not running"); + break; - if (m_AppConfig.invertConnection()) { - args << "--host"; - args << ":" + QString::number(m_AppConfig.port()); - } else { - if (m_pLineEditHostname->text().isEmpty()) { - show(); - QMessageBox::warning( - this, QString("IP/hostname is empty"), - QString("Please enter a server hostname or IP address.")); - return false; - } + case Started: { + setIcon(connection); - QString hostName = m_pLineEditHostname->text(); - // if interface is IPv6 - ensure that ip is in square brackets - if (hostName.count(':') > 1) { - if (hostName[0] != '[') { - hostName.insert(0, '['); + switch (connection) { + using enum CoreConnectionState; + + case Listening: { + if (m_CoreProcess.mode() == CoreMode::Server) { + setStatus("Synergy is waiting for clients"); } - if (hostName[hostName.size() - 1] != ']') { - hostName.push_back(']'); - } - } - args << hostName + ":" + QString::number(m_AppConfig.port()); - } - - return true; -} - -QString MainWindow::configFilename() { - QString configFullPath; - if (m_AppConfig.useExternalConfig()) { - configFullPath = m_AppConfig.configFile(); - } else { - QStringList errors; - for (auto path : - {QStandardPaths::AppDataLocation, QStandardPaths::AppConfigLocation}) { - auto configDirPath = QStandardPaths::writableLocation(path); - if (!QDir().mkpath(configDirPath)) { - errors.push_back(QString("Failed to create config folder \"%1\"") - .arg(configDirPath)); - continue; - } - - QFile configFile(configDirPath + "/LastConfig.cfg"); - if (!configFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - errors.push_back( - QString("File:\"%1\" Error:%2") - .arg(configFile.fileName(), configFile.errorString())); - continue; - } - - m_ServerConfig.save(configFile); - configFile.close(); - configFullPath = configFile.fileName(); break; } - if (configFullPath.isEmpty()) { - QMessageBox::critical( - this, QString("Cannot write configuration file"), errors.join('\n')); + case Connecting: + setStatus("Synergy is connecting..."); + break; + + case Connected: { + if (m_SecureSocket) { + setStatus(QString("Synergy is connected (with %1)") + .arg(m_CoreProcess.secureSocketVersion())); + } else { + setStatus("Synergy is connected (without TLS encryption)"); + } + break; } - } - return configFullPath; -} - -QString MainWindow::address() const { - QString i = m_AppConfig.networkInterface(); - // if interface is IPv6 - ensure that ip is in square brackets - if (i.count(':') > 1) { - if (i[0] != '[') { - i.insert(0, '['); - } - if (i[i.size() - 1] != ']') { - i.push_back(']'); + case Disconnected: + setStatus("Synergy is disconnected"); + break; } + } break; } - return (!i.isEmpty() ? i : "") + ":" + QString::number(m_AppConfig.port()); } -QString MainWindow::appPath(const QString &name) const { - QDir dir(QCoreApplication::applicationDirPath()); - return dir.filePath(name); -} +void MainWindow::onCoreProcessStateChanged(CoreProcessState state) { + qDebug("core process state changed: %d", static_cast(state)); -bool MainWindow::serverArgs(QStringList &args, QString &app) { - app = appPath(m_AppConfig.coreServerName()); + updateStatus(); - if (!QFile::exists(app)) { - QMessageBox::warning( - this, QString("Synergy server not found"), - QString("The executable for the synergy server does not exist.")); - return false; - } - - if (m_AppConfig.invertConnection() && m_pLineEditClientIp->text().isEmpty()) { - QMessageBox::warning( - this, QString("Client IP address or name is empty"), - QString("Please fill in a client IP address or name.")); - return false; - } - - if (m_AppConfig.logToFile()) { - m_AppConfig.persistLogDir(); - - args << "--log" << m_AppConfig.logFilename(); - } - - QString configFilename = this->configFilename(); - if (configFilename.isEmpty()) { - return false; - } - - args << "-c" << configFilename << "--address" << address(); - appendLogInfo("config file: " + configFilename); - - if (kLicensingEnabled && !m_AppConfig.serialKey().isEmpty()) { - args << "--serial-key" << m_AppConfig.serialKey(); - } - - return true; -} - -void MainWindow::stopCore() { - appendLogDebug("stopping core process"); - - m_ExpectedRunningState = RuningState::Stopped; - - if (m_AppConfig.processMode() == ProcessMode::kService) { - stopService(); - } else if (m_AppConfig.processMode() == ProcessMode::kDesktop) { - stopDesktop(); - } - - setCoreState(CoreState::Disconnected); - - // reset so that new connects cause auto-hide. - m_AlreadyHidden = false; -} - -void MainWindow::stopService() { - // send empty command to stop service from laucning anything. - m_IpcClient.sendCommand("", m_AppConfig.elevateMode()); -} - -void MainWindow::stopDesktop() { - QMutexLocker locker(&m_StopDesktopMutex); - if (!m_pCoreProcess) { - return; - } - - appendLogInfo("stopping synergy desktop process"); - - if (m_pCoreProcess->isOpen()) { - m_pCoreProcess->close(); - } - - m_pCoreProcess->reset(); -} - -void MainWindow::setCoreState(CoreState state) { - // always assume connection is not secure when connection changes - // to anything except connected. the only way the padlock shows is - // when the correct TLS version string is detected. - if (state != CoreState::Connected) { - secureSocket(false); - } - - if (m_CoreState == state) - return; - - if ((state == CoreState::Connected) || (state == CoreState::Connecting) || - (state == CoreState::Listening) || (state == CoreState::PendingRetry)) { + if (state == CoreProcessState::Started || + state == CoreProcessState::Starting) { disconnect( m_pButtonToggleStart, &QPushButton::clicked, m_pActionStartCore, &QAction::trigger); @@ -1200,7 +844,7 @@ void MainWindow::setCoreState(CoreState state) { m_pActionStartCore->setEnabled(false); m_pActionStopCore->setEnabled(true); - } else if (state == CoreState::Disconnected) { + } else { disconnect( m_pButtonToggleStart, &QPushButton::clicked, m_pActionStopCore, &QAction::trigger); @@ -1214,40 +858,22 @@ void MainWindow::setCoreState(CoreState state) { m_pActionStartCore->setEnabled(true); m_pActionStopCore->setEnabled(false); } +} - switch (state) { - using enum CoreState; +void MainWindow::onCoreConnectionStateChanged(CoreConnectionState state) { + qDebug("core connection state changed: %d", static_cast(state)); - case Listening: { - if (coreMode() == CoreMode::Server) { - setStatus("Synergy is waiting for clients"); - } + updateStatus(); - break; + // always assume connection is not secure when connection changes + // to anything except connected. the only way the padlock shows is + // when the correct TLS version string is detected. + if (state != CoreConnectionState::Connected) { + secureSocket(false); + } else if (isVisible()) { + showFirstRunMessage(); + showDevThanksMessage(); } - case Connected: { - if (m_SecureSocket) { - setStatus( - QString("Synergy is connected (with %1)").arg(m_SecureSocketVersion)); - } else { - setStatus("Synergy is running (without TLS encryption)"); - } - break; - } - case Connecting: - setStatus("Synergy is starting..."); - break; - case PendingRetry: - setStatus("There was an error, retrying..."); - break; - case Disconnected: - setStatus("Synergy is not running"); - break; - } - - setIcon(state); - - m_CoreState = state; } void MainWindow::setVisible(bool visible) { @@ -1269,34 +895,6 @@ void MainWindow::setVisible(bool visible) { #endif } -MainWindow::CoreMode MainWindow::coreMode() const { - using enum CoreMode; - - auto serverChecked = m_pRadioGroupServer->isChecked(); - auto clientChecked = m_pRadioGroupClient->isChecked(); - - if (serverChecked) { - return Server; - } else if (clientChecked) { - return Client; - } else { - return None; - } -} - -QString MainWindow::coreModeString() const { - using enum CoreMode; - - switch (coreMode()) { - case Server: - return "server"; - case Client: - return "client"; - default: - qFatal("invalid core mode"); - } -} - QString MainWindow::getIPAddresses() const { QStringList result; bool hinted = false; @@ -1356,12 +954,13 @@ void MainWindow::updateLocalFingerprint() { } QString MainWindow::productName() const { - if (kLicensingEnabled) { + if (kEnableActivation) { return m_LicenseHandler.productName(); } else if (!kProductName.isEmpty()) { return kProductName; } else { qFatal("product name not set"); + return ""; } } @@ -1370,7 +969,8 @@ void MainWindow::updateWindowTitle() { setWindowTitle(productName()); } void MainWindow::autoAddScreen(const QString name) { if (m_ActivationDialogRunning) { - // add this screen to the pending list if the activation dialog is running. + // add this screen to the pending list if the activation dialog is + // running. m_PendingClientNames.append(name); return; } @@ -1392,18 +992,11 @@ void MainWindow::autoAddScreen(const QString name) { } } -bool MainWindow::isCoreActive() const { - using enum CoreState; - - auto state = m_CoreState; - return (state == Connected) || (state == Connecting) || (state == Listening); -} - void MainWindow::showConfigureServer(const QString &message) { ServerConfigDialog dialog(this, serverConfig(), m_AppConfig); dialog.message(message); - if ((dialog.exec() == QDialog::Accepted) && isCoreActive()) { - restartCore(); + if ((dialog.exec() == QDialog::Accepted) && m_CoreProcess.isStarted()) { + m_CoreProcess.restart(); } } @@ -1433,20 +1026,6 @@ int MainWindow::showActivationDialog() { return result; } -QString MainWindow::getProfileRootForArg() const { - CoreInterface coreInterface; - QString dir = coreInterface.getProfileDir(); - - // HACK: strip our app name since we're returning the root dir. -#if defined(Q_OS_WIN) - dir.replace("\\Synergy", ""); -#else - dir.replace("/.synergy", ""); -#endif - - return dir; -} - void MainWindow::secureSocket(bool secureSocket) { m_SecureSocket = secureSocket; if (secureSocket) { @@ -1475,6 +1054,7 @@ void MainWindow::enableServer(bool enable) { if (enable) { m_pButtonToggleStart->setEnabled(true); m_pActionStartCore->setEnabled(true); + m_CoreProcess.setMode(CoreProcess::Mode::Server); } } @@ -1488,5 +1068,6 @@ void MainWindow::enableClient(bool enable) { if (enable) { m_pButtonToggleStart->setEnabled(true); m_pActionStartCore->setEnabled(true); + m_CoreProcess.setMode(CoreProcess::Mode::Client); } } diff --git a/src/gui/src/MainWindow.h b/src/gui/src/MainWindow.h index f995e7020..de9edd72d 100644 --- a/src/gui/src/MainWindow.h +++ b/src/gui/src/MainWindow.h @@ -24,21 +24,19 @@ #include #include #include -#include - -#include "ui_MainWindowBase.h" #include "ActivationDialog.h" -#include "ClientConnection.h" #include "ServerConfig.h" -#include "ServerConnection.h" -#include "TrayIcon.h" #include "global/Ipc.h" -#include "gui/AppConfig.h" -#include "gui/ConfigScopes.h" -#include "gui/QIpcClient.h" -#include "gui/TlsUtility.h" +#include "gui/TrayIcon.h" #include "gui/VersionChecker.h" +#include "gui/config/AppConfig.h" +#include "gui/config/ConfigScopes.h" +#include "gui/core/ClientConnection.h" +#include "gui/core/CoreProcess.h" +#include "gui/core/ServerConnection.h" +#include "gui/tls/TlsUtility.h" +#include "ui_MainWindowBase.h" class QAction; class QMenu; @@ -53,35 +51,22 @@ class QRadioButton; class QMessageBox; class QAbstractButton; -class LogDialog; class QSynergyApplication; class SetupWizard; -class DataDownloader; -class CommandProcess; -class TlsCertificate; class MainWindow : public QMainWindow, public Ui::MainWindowBase { + using CoreMode = synergy::gui::CoreProcess::Mode; + using CoreProcess = synergy::gui::CoreProcess; + Q_OBJECT friend class QSynergyApplication; friend class SetupWizard; friend class ActivationDialog; friend class SettingsDialog; - friend class ServerConnection; - friend class ClientConnection; public: - enum class CoreState { - Disconnected, - Connecting, - Connected, - Listening, - PendingRetry - }; - - enum class CoreMode { None, Client, Server }; enum class LogLevel { Error, Info }; - enum class RuningState { Started, Stopped }; public: explicit MainWindow( @@ -89,17 +74,12 @@ public: ~MainWindow() override; void setVisible(bool visible) override; - CoreMode coreMode() const; - QString coreModeString() const; + CoreMode coreMode() const { return m_CoreProcess.mode(); } QString address() const; - QString appPath(const QString &name) const; void open(); ServerConfig &serverConfig() { return m_ServerConfig; } void autoAddScreen(const QString name); int showActivationDialog(); - void appendLogInfo(const QString &text); - void appendLogDebug(const QString &text); - void appendLogError(const QString &text); signals: void created(); @@ -115,19 +95,19 @@ private slots: void onAppConfigTlsChanged(); void onAppConfigScreenNameChanged(); void onAppConfigInvertConnection(); + void onCoreProcessStarting(); + void onCoreProcessError(CoreProcess::Error error); + void onCoreConnectionStateChanged(CoreProcess::ConnectionState state); + void onCoreProcessStateChanged(CoreProcess::ProcessState state); + void onCoreProcessSecureSocket(bool enabled); void onLicenseHandlerSerialKeyChanged(const QString &serialKey); void onLicenseHandlerInvalidLicense(); - void onIpcClientReadLogLine(const QString &text); - void onIpcClientErrorMessage(const QString &text); - void onIpcClientInfoMessage(const QString &text); - void onCoreProcessFinished(int exitCode, QProcess::ExitStatus); void onVersionCheckerUpdateFound(const QString &version); - void onTrayIconCreate(QSystemTrayIcon::ActivationReason reason); + void onTrayIconActivated(QSystemTrayIcon::ActivationReason reason); void onActionStartCoreTriggered(); void onActionStopCoreTriggered(); - void onCoreProcessReadyReadStandardOutput(); - void onCoreProcessReadyReadStandardError(); void onWindowSaveTimerTimeout(); + void onServerConnectionConfigureClient(const QString &clientName); // autoconnect slots void on_m_pButtonApply_clicked(); @@ -145,6 +125,8 @@ private slots: void on_m_pActivate_triggered(); void on_m_pLineEditHostname_returnPressed(); void on_m_pLineEditClientIp_returnPressed(); + void on_m_pLineEditHostname_textChanged(const QString &text); + void on_m_pLineEditClientIp_textChanged(const QString &text); private: AppConfig &appConfig() { return m_AppConfig; } @@ -154,44 +136,32 @@ private: void createTrayIcon(); void applyConfig(); void applyCloseToTray() const; - void setIcon(CoreState state) const; - void setCoreState(CoreState state); + void setIcon(CoreProcess::ConnectionState state); bool checkForApp(int which, QString &app); - bool clientArgs(QStringList &args, QString &app); - bool serverArgs(QStringList &args, QString &app); void setStatus(const QString &status); void sendIpcMessage(IpcMessageType type, const char *buffer, bool showErrors); void updateFromLogLine(const QString &line); QString getIPAddresses() const; - void stopService(); - void stopDesktop(); void enableServer(bool enable); void enableClient(bool enable); - QString getProfileRootForArg() const; void checkConnected(const QString &line); void checkFingerprint(const QString &line); - bool checkSecureSocket(const QString &line); void checkLicense(const QString &line); QString getTimeStamp() const; - void restartCore(); void showEvent(QShowEvent *) override; void closeEvent(QCloseEvent *event) override; void secureSocket(bool secureSocket); void windowStateChanged(); - void connectSlots() const; + void connectSlots(); void updateWindowTitle(); - void processCoreLogLine(const QString &line); - void startCore(); - void onCoreProcessRetryStart(); + void handleLogLine(const QString &line); void updateLocalFingerprint(); void updateScreenName(); void saveSettings(); QString configFilename(); - bool isCoreActive() const; void showConfigureServer(const QString &message); void showConfigureServer() { showConfigureServer(""); } void showLicenseNotice(); - void stopCore(); void restoreWindow(); void saveWindow(); void setupControls(); @@ -200,37 +170,28 @@ private: void showFirstRunMessage(); void showDevThanksMessage(); QString productName() const; - -#ifdef Q_OS_MAC - void checkOSXNotification(const QString &line); -#endif + void updateStatus(); VersionChecker m_VersionChecker; - QIpcClient m_IpcClient; - TrayIcon m_TrayIcon; - QMutex m_StopDesktopMutex; + synergy::gui::TrayIcon m_TrayIcon; bool m_ActivationDialogRunning = false; QStringList m_PendingClientNames; - RuningState m_ExpectedRunningState = RuningState::Stopped; - std::unique_ptr m_pCoreProcess; QMenuBar *m_pMenuBar = nullptr; QMenu *m_pMenuFile = nullptr; QMenu *m_pMenuEdit = nullptr; QMenu *m_pMenuWindow = nullptr; QMenu *m_pMenuHelp = nullptr; QAbstractButton *m_pCancelButton = nullptr; - CoreState m_CoreState = CoreState::Disconnected; - bool m_AlreadyHidden = false; bool m_SecureSocket = false; - QString m_SecureSocketVersion = ""; bool m_SaveWindow = false; LicenseHandler m_LicenseHandler; synergy::gui::ConfigScopes &m_ConfigScopes; AppConfig &m_AppConfig; ServerConfig m_ServerConfig; - ServerConnection m_ServerConnection; - ClientConnection m_ClientConnection; + synergy::gui::CoreProcess m_CoreProcess; + synergy::gui::ServerConnection m_ServerConnection; + synergy::gui::ClientConnection m_ClientConnection; synergy::gui::TlsUtility m_TlsUtility; QTimer m_WindowSaveTimer; }; diff --git a/src/gui/src/MainWindowBase.ui b/src/gui/src/MainWindowBase.ui index 0f61c718a..25f86e96f 100644 --- a/src/gui/src/MainWindowBase.ui +++ b/src/gui/src/MainWindowBase.ui @@ -25,16 +25,13 @@ Synergy - - + + - 0 - - - 20 + 10 - + @@ -67,22 +64,6 @@ - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 10 - - - - @@ -94,7 +75,7 @@ - + 0 @@ -102,6 +83,21 @@ + + 20 + + + 0 + + + 0 + + + 0 + + + 0 + @@ -116,6 +112,9 @@ + + 0 + @@ -281,6 +280,9 @@ + + 0 + @@ -388,41 +390,15 @@ - - - Logs - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 5 - - - - - - + 0 0 - - QFrame::StyledPanel - - - QFrame::Raised + + Logs @@ -469,16 +445,13 @@ - + 6 QLayout::SetDefaultConstraint - - 16 - @@ -599,9 +572,9 @@ - - false - + + false + &Start diff --git a/src/gui/src/QSynergyApplication.cpp b/src/gui/src/QSynergyApplication.cpp index 4b693471e..20f671d55 100644 --- a/src/gui/src/QSynergyApplication.cpp +++ b/src/gui/src/QSynergyApplication.cpp @@ -24,4 +24,8 @@ #include QSynergyApplication::QSynergyApplication(int &argc, char **argv) - : QApplication(argc, argv) {} + : QApplication(argc, argv) { + + // causes dark mode to be used on some OS (e.g. Windows) + setStyle("fusion"); +} diff --git a/src/gui/src/ScreenSettingsDialog.cpp b/src/gui/src/ScreenSettingsDialog.cpp index 0f5cde0a6..bc0807485 100644 --- a/src/gui/src/ScreenSettingsDialog.cpp +++ b/src/gui/src/ScreenSettingsDialog.cpp @@ -27,6 +27,8 @@ #include #include +using namespace synergy::gui; + ScreenSettingsDialog::ScreenSettingsDialog( QWidget *parent, Screen *pScreen, const ScreenList *pScreens) : QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint), diff --git a/src/gui/src/ServerConfig.cpp b/src/gui/src/ServerConfig.cpp index 188ea1bbf..f41b779e4 100644 --- a/src/gui/src/ServerConfig.cpp +++ b/src/gui/src/ServerConfig.cpp @@ -340,8 +340,7 @@ int ServerConfig::autoAddScreen(const QString name) { } if (findScreenName(name, targetIndex)) { - m_pMainWindow->appendLogDebug( - QString("ignoring screen already in config: %1").arg(name)); + qDebug("ignoring screen already in config: %s", qPrintable(name)); return kAutoAddScreenIgnore; } @@ -428,7 +427,7 @@ bool ServerConfig::isFull() const { return isFull; } -bool ServerConfig::isScreenExists(const QString &screenName) const { +bool ServerConfig::screenExists(const QString &screenName) const { bool isExists = false; for (const auto &screen : screens()) { diff --git a/src/gui/src/ServerConfig.h b/src/gui/src/ServerConfig.h index bb8dd1ca3..f4764c567 100644 --- a/src/gui/src/ServerConfig.h +++ b/src/gui/src/ServerConfig.h @@ -21,6 +21,7 @@ #include "Hotkey.h" #include "ScreenConfig.h" #include "ScreenList.h" +#include "gui/config/IServerConfig.h" #include @@ -35,9 +36,8 @@ class ServerConfigDialog; class MainWindow; class AppConfig; -class ServerConfig : public ScreenConfig { +class ServerConfig : public ScreenConfig, public synergy::gui::IServerConfig { friend class ServerConfigDialog; - friend class ServerConnection; friend QTextStream & operator<<(QTextStream &outStream, const ServerConfig &config); @@ -45,7 +45,7 @@ public: ServerConfig( AppConfig &appConfig, MainWindow &mainWindow, int columns = kDefaultColumns, int rows = kDefaultRows); - ~ServerConfig() = default; + ~ServerConfig() override = default; bool operator==(const ServerConfig &sc) const; @@ -64,23 +64,23 @@ public: int switchCornerSize() const { return m_SwitchCornerSize; } const QList &switchCorners() const { return m_SwitchCorners; } const HotkeyList &hotkeys() const { return m_Hotkeys; } - bool enableDragAndDrop() const { return m_EnableDragAndDrop; } + bool enableDragAndDrop() const override { return m_EnableDragAndDrop; } bool disableLockToScreen() const { return m_DisableLockToScreen; } bool clipboardSharing() const { return m_ClipboardSharing; } size_t clipboardSharingSize() const { return m_ClipboardSharingSize; } static size_t defaultClipboardSharingSize(); void commit(); - bool save(const QString &fileName) const; - void save(QFile &file) const; + bool save(const QString &fileName) const override; + void save(QFile &file) const override; int numScreens() const; int autoAddScreen(const QString name); const QString &getServerName() const; void updateServerName(); const QString &configFile() const; bool useExternalConfig() const; - bool isFull() const; - bool isScreenExists(const QString &screenName) const; + bool isFull() const override; + bool screenExists(const QString &screenName) const override; void addClient(const QString &clientName); QString getClientAddress() const; void setClientAddress(const QString &address); diff --git a/src/gui/src/ServerConfigDialog.h b/src/gui/src/ServerConfigDialog.h index 2d3d94d57..7b859574d 100644 --- a/src/gui/src/ServerConfigDialog.h +++ b/src/gui/src/ServerConfigDialog.h @@ -22,7 +22,7 @@ #include "ScreenSetupModel.h" #include "ServerConfig.h" -#include "gui/AppConfig.h" +#include "gui/config/AppConfig.h" #include "ui_ServerConfigDialogBase.h" #include @@ -36,9 +36,9 @@ public: bool addClient(const QString &clientName); public slots: - void accept(); + void accept() override; void reject() override; - void showEvent(QShowEvent *event); + void showEvent(QShowEvent *event) override; void message(const QString &message) { m_Message = message; } protected slots: diff --git a/src/gui/src/ServerConfigDialogBase.ui b/src/gui/src/ServerConfigDialogBase.ui index e8eb23513..bc3d4cf57 100644 --- a/src/gui/src/ServerConfigDialogBase.ui +++ b/src/gui/src/ServerConfigDialogBase.ui @@ -76,7 +76,7 @@ - + Qt::Horizontal @@ -117,7 +117,7 @@ - + Qt::Horizontal @@ -342,7 +342,7 @@ 9 - + 0 @@ -482,7 +482,7 @@ - + Qt::Vertical @@ -644,7 +644,7 @@ - + @@ -1105,7 +1105,7 @@ Enabling this setting will disable the server config GUI. - + Qt::Vertical diff --git a/src/gui/src/ServerConnection.cpp b/src/gui/src/ServerConnection.cpp deleted file mode 100644 index 54342e623..000000000 --- a/src/gui/src/ServerConnection.cpp +++ /dev/null @@ -1,78 +0,0 @@ -/* - * synergy -- mouse and keyboard sharing utility - * Copyright (C) 2012-2021 Symless Ltd. - * Copyright (C) 2008 Volker Lanz (vl@fidra.de) - * - * This package is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * found in the file LICENSE that should have accompanied this file. - * - * This package is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "ServerConnection.h" - -#include "MainWindow.h" -#include "ServerConfigDialog.h" -#include "ServerMessage.h" -#include "gui/AppConfig.h" - -#include - -ServerConnection::ServerConnection(MainWindow &parent) : m_parent(parent) {} - -void ServerConnection::update(const QString &line) { - ServerMessage message(line); - - if (!m_parent.appConfig().useExternalConfig() && - message.isNewClientMessage() && - !m_ignoredClients.contains(message.getClientName())) { - addClient(message.getClientName()); - } -} - -bool ServerConnection::checkMainWindow() { - bool result = m_parent.isActiveWindow(); - - if (m_parent.isMinimized() || m_parent.isHidden()) { - m_parent.showNormal(); - m_parent.activateWindow(); - result = true; - } - - return result; -} - -void ServerConnection::addClient(const QString &clientName) { - if (!m_parent.serverConfig().isFull() && - !m_parent.serverConfig().isScreenExists(clientName) && - checkMainWindow()) { - QMessageBox message(&m_parent); - message.addButton(QObject::tr("Ignore"), QMessageBox::RejectRole); - message.addButton( - QObject::tr("Accept and configure"), QMessageBox::AcceptRole); - message.setText( - QObject::tr("%1 client has made a connection request").arg(clientName)); - - if (message.exec() == QMessageBox::Accepted) { - configureClient(clientName); - } else { - m_ignoredClients.append(clientName); - } - } -} - -void ServerConnection::configureClient(const QString &clientName) { - ServerConfigDialog dialog( - &m_parent, m_parent.serverConfig(), m_parent.appConfig()); - - if (dialog.addClient(clientName) && dialog.exec() == QDialog::Accepted) { - m_parent.restartCore(); - } -} diff --git a/src/gui/src/SettingsDialog.cpp b/src/gui/src/SettingsDialog.cpp index 19e8ed889..637031c86 100644 --- a/src/gui/src/SettingsDialog.cpp +++ b/src/gui/src/SettingsDialog.cpp @@ -20,9 +20,10 @@ #include "MainWindow.h" #include "UpgradeDialog.h" -#include "gui/AppConfig.h" -#include "gui/TlsCertificate.h" +#include "gui/config/AppConfig.h" #include "gui/constants.h" +#include "gui/tls/TlsCertificate.h" +#include "gui/tls/TlsUtility.h" #include "validators/ScreenNameValidator.h" #include "validators/ValidationError.h" @@ -31,8 +32,10 @@ #include #include #include +#include using namespace synergy::license; +using namespace synergy::gui; const char *const kProProductName = "Synergy 1 Pro"; @@ -82,7 +85,7 @@ void SettingsDialog::accept() { appConfig().setAutoHide(m_pCheckBoxAutoHide->isChecked()); appConfig().setPreventSleep(m_pCheckBoxPreventSleep->isChecked()); appConfig().setTlsCertPath(m_pLineEditCertificatePath->text()); - appConfig().setTlsKeyLength(m_pComboBoxKeyLength->currentText()); + appConfig().setTlsKeyLength(m_pComboBoxKeyLength->currentText().toInt()); appConfig().setTlsEnabled(m_pCheckBoxEnableCrypto->isChecked()); appConfig().setLanguageSync(m_pCheckBoxLanguageSync->isChecked()); appConfig().setInvertScrollDirection(m_pCheckBoxScrollDirection->isChecked()); @@ -136,8 +139,9 @@ void SettingsDialog::updateTlsControls() { if (QFile(appConfig().tlsCertPath()).exists()) { updateKeyLengthOnFile(appConfig().tlsCertPath()); } else { + const auto keyLengthText = QString::number(appConfig().tlsKeyLength()); m_pComboBoxKeyLength->setCurrentIndex( - m_pComboBoxKeyLength->findText(appConfig().tlsKeyLength())); + m_pComboBoxKeyLength->findText(keyLengthText)); } m_pCheckBoxEnableCrypto->setChecked(m_appConfig.tlsEnabled()); @@ -189,7 +193,7 @@ void SettingsDialog::on_m_pButtonBrowseLog_clicked() { void SettingsDialog::on_m_pCheckBoxEnableCrypto_clicked(bool) { updateTlsControlsEnabled(); - if (kLicensingEnabled && !m_tlsUtility.isAvailable()) { + if (kEnableActivation && !m_tlsUtility.isAvailable()) { auto edition = m_license.productEdition(); if (edition == Edition::kBasic) { UpgradeDialog upgradeDialog(this); @@ -215,9 +219,11 @@ void SettingsDialog::on_m_pPushButtonBrowseCert_clicked() { if (!fileName.isEmpty()) { m_pLineEditCertificatePath->setText(fileName); - // If the tls file exists test its key length and update - if (QFile(appConfig().tlsCertPath()).exists()) { + + if (QFile(fileName).exists()) { updateKeyLengthOnFile(fileName); + } else { + qDebug("no tls certificate file at: %s", qUtf8Printable(fileName)); } } updateTlsRegenerateButton(); @@ -228,9 +234,8 @@ void SettingsDialog::on_m_pComboBoxKeyLength_currentIndexChanged(int index) { } void SettingsDialog::updateTlsRegenerateButton() { - // Disable the Regenerate cert button if the key length is different to saved - auto keyChanged = - appConfig().tlsKeyLength() != m_pComboBoxKeyLength->currentText(); + const auto keyLength = m_pComboBoxKeyLength->currentText().toInt(); + auto keyChanged = appConfig().tlsKeyLength() != keyLength; auto pathChanged = appConfig().tlsCertPath() != m_pLineEditCertificatePath->text(); // NOR the above bools, if any have changed regen should be disabled as it @@ -241,15 +246,22 @@ void SettingsDialog::updateTlsRegenerateButton() { } void SettingsDialog::on_m_pPushButtonRegenCert_clicked() { - m_tlsUtility.generateCertificate(true); + if (m_tlsUtility.generateCertificate()) { + QMessageBox::information( + this, tr("TLS Certificate Regenerated"), + tr("TLS certificate regenerated successfully.")); + } } void SettingsDialog::updateKeyLengthOnFile(const QString &path) { TlsCertificate ssl; + if (!QFile(path).exists()) { + qFatal("tls certificate file not found: %s", qUtf8Printable(path)); + } + auto length = ssl.getCertKeyLength(path); - auto index = m_pComboBoxKeyLength->findText(length); + auto index = m_pComboBoxKeyLength->findText(QString::number(length)); m_pComboBoxKeyLength->setCurrentIndex(index); - // Also update what is in the appconfig to match the file itself appConfig().setTlsKeyLength(length); } diff --git a/src/gui/src/SettingsDialog.h b/src/gui/src/SettingsDialog.h index 9613b4da9..aa0e060aa 100644 --- a/src/gui/src/SettingsDialog.h +++ b/src/gui/src/SettingsDialog.h @@ -20,8 +20,8 @@ #include "ui_SettingsDialogBase.h" -#include "gui/CoreInterface.h" -#include "gui/TlsUtility.h" +#include "gui/core/CoreTool.h" +#include "gui/tls/TlsUtility.h" #include "license/License.h" #include "validators/ValidationError.h" @@ -68,7 +68,7 @@ protected: private: MainWindow *m_pMainWindow; AppConfig &m_appConfig; - CoreInterface m_coreInterface; + [[no_unique_address]] CoreTool m_coreTool; const synergy::license::License &m_license; synergy::gui::TlsUtility m_tlsUtility; validators::ValidationError *m_pScreenNameError; diff --git a/src/gui/src/SetupWizard.cpp b/src/gui/src/SetupWizard.cpp index d871c8a93..405bc9c24 100644 --- a/src/gui/src/SetupWizard.cpp +++ b/src/gui/src/SetupWizard.cpp @@ -21,6 +21,8 @@ #include "validators/ScreenNameValidator.h" #include "validators/ValidationError.h" +using namespace synergy::gui; + SetupWizard::SetupWizard(AppConfig &appConfig) : m_appConfig(appConfig) { setupUi(this); diff --git a/src/gui/src/SetupWizard.h b/src/gui/src/SetupWizard.h index 01487e2e0..4d5de9636 100644 --- a/src/gui/src/SetupWizard.h +++ b/src/gui/src/SetupWizard.h @@ -19,7 +19,7 @@ #include "ui_SetupWizardBase.h" -#include "gui/AppConfig.h" +#include "gui/config/AppConfig.h" #include #include diff --git a/src/gui/src/TrayIcon.cpp b/src/gui/src/TrayIcon.cpp deleted file mode 100644 index 5cb57fd1d..000000000 --- a/src/gui/src/TrayIcon.cpp +++ /dev/null @@ -1,35 +0,0 @@ -/* - * synergy -- mouse and keyboard sharing utility - * Copyright (C) 2021 Symless Ltd. - * - * This package is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * found in the file LICENSE that should have accompanied this file. - * - * This package is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "TrayIcon.h" - -void TrayIcon::tryCreate() const { - QSystemTrayIcon trayIcon; // by creating a new tray icon, we actually make the - // DBus implementation refresh the connection (DBus) - trayIcon.hide(); // we ony hide it in order for the compiler to not - // optimise-out the object (make some use of it) - if (QSystemTrayIcon::isSystemTrayAvailable()) { // this ends up calling the - // underlying DBus connection - // (on DBus) - m_pTrayIcon->show(); - m_connector( - m_pTrayIcon.get(), - SIGNAL(activated(QSystemTrayIcon::ActivationReason))); - } else { - QTimer::singleShot(2500, this, &TrayIcon::tryCreate); - } -} diff --git a/src/gui/src/TrayIcon.h b/src/gui/src/TrayIcon.h deleted file mode 100644 index a8bdb0140..000000000 --- a/src/gui/src/TrayIcon.h +++ /dev/null @@ -1,73 +0,0 @@ -/* - * synergy -- mouse and keyboard sharing utility - * Copyright (C) 2021 Symless Ltd. - * - * This package is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * found in the file LICENSE that should have accompanied this file. - * - * This package is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include -#include -#include -#include -#include - -class TrayIcon : public QObject { - Q_OBJECT -public: - using TConnector = std::function; - - TrayIcon() { - m_set = [this](const QIcon &icon) { - m_init = [this, icon]() { this->set(icon); }; - }; - } - - template - void - create(TActionContainer const &actionContainer, TConnector const &connector) { - m_connector = connector; - m_pTrayIconMenu = std::make_unique(); - - for (auto action : actionContainer) { - if (action) { - m_pTrayIconMenu->addAction(action); - } else { - m_pTrayIconMenu->addSeparator(); - } - } - - m_pTrayIcon = std::make_unique(); - m_pTrayIcon->setContextMenu(m_pTrayIconMenu.get()); - m_pTrayIcon->setToolTip("Synergy"); - m_set = [this](const QIcon &icon) { m_pTrayIcon->setIcon(icon); }; - - tryCreate(); - if (m_init) { - m_init(); - m_init = std::function(); - } - } - - void tryCreate() const; - void set(const QIcon &icon) const { m_set(icon); } - -private: - std::unique_ptr m_pTrayIcon{}; - std::unique_ptr m_pTrayIconMenu{}; - TConnector m_connector; - std::function m_init; - std::function m_set; -}; diff --git a/src/gui/src/main.cpp b/src/gui/src/main.cpp index c2952d168..4c7493820 100644 --- a/src/gui/src/main.cpp +++ b/src/gui/src/main.cpp @@ -20,8 +20,9 @@ #include "QSynergyApplication.h" #include "SetupWizard.h" #include "SetupWizardBlocker.h" -#include "gui/AppConfig.h" -#include "gui/ConfigScopes.h" +#include "gui/Logger.h" +#include "gui/config/AppConfig.h" +#include "gui/config/ConfigScopes.h" #include "gui/constants.h" #include "gui/dotenv.h" #include "gui/messages.h" @@ -54,7 +55,7 @@ bool checkMacAssistiveDevices(); int main(int argc, char *argv[]) { -#ifdef Q_OS_DARWIN +#if defined(Q_OS_MAC) /* Workaround for QTBUG-40332 - "High ping when QNetworkAccessManager is * instantiated" */ ::setenv("QT_BEARER_POLL_TIMEOUT", "-1", 1); @@ -67,9 +68,10 @@ int main(int argc, char *argv[]) { QSynergyApplication app(argc, argv); qInstallMessageHandler(synergy::gui::messages::messageHandler); - qInfo("Synergy v%s", synergy::gui::version().toUtf8().constData()); - dotenv(".env"); + Logger::instance().loadEnvVars(); + + qInfo("Synergy v%s", synergy::gui::version().toUtf8().constData()); #if defined(Q_OS_MAC) diff --git a/src/gui/src/validators/LineEditValidator.cpp b/src/gui/src/validators/LineEditValidator.cpp index 7bf9bec5c..e52b46cbd 100644 --- a/src/gui/src/validators/LineEditValidator.cpp +++ b/src/gui/src/validators/LineEditValidator.cpp @@ -18,7 +18,10 @@ #include "LineEditValidator.h" #include "gui/styles.h" -#include + +#include + +using namespace synergy::gui; namespace validators { diff --git a/src/gui/src/validators/ValidationError.cpp b/src/gui/src/validators/ValidationError.cpp index 6ff872000..5381989b0 100644 --- a/src/gui/src/validators/ValidationError.cpp +++ b/src/gui/src/validators/ValidationError.cpp @@ -19,6 +19,8 @@ #include "gui/styles.h" +using namespace synergy::gui; + namespace validators { void clear(QLabel *label) { diff --git a/src/gui/src/validators/ValidationError.h b/src/gui/src/validators/ValidationError.h index a1f257e9f..324461e7a 100644 --- a/src/gui/src/validators/ValidationError.h +++ b/src/gui/src/validators/ValidationError.h @@ -18,8 +18,8 @@ #pragma once #include +#include #include -#include namespace validators { diff --git a/src/gui/src/widgets/ServerStateLabel.cpp b/src/gui/src/widgets/ServerStateLabel.cpp index c867d1a5e..97a13d777 100644 --- a/src/gui/src/widgets/ServerStateLabel.cpp +++ b/src/gui/src/widgets/ServerStateLabel.cpp @@ -18,7 +18,9 @@ #include "ServerStateLabel.h" -#include "ServerMessage.h" +#include "gui/core/ServerMessage.h" + +using namespace synergy::gui; namespace synergy_widgets { @@ -38,7 +40,14 @@ void ServerStateLabel::updateServerState(const QString &line) { if (m_clients.isEmpty()) { setText(tr("No clients connected")); } else { - setText(tr("Connected clients: %1").arg(m_clients.join(", "))); + // unfortunately, we can't rely on the clients list because we don't always + // catch the connect/disconnect messages. so clients tend to get stuck in + // the list even though they're offline. + // in order to properly show a list of clients, we would need the core to + // print a list of connected clients on every connect/disconnect event, + // which could be a bit noisy in the logs (perhaps an ipc message would be + // needed). + setText(tr("Client(s) are connected")); } } diff --git a/src/lib/gui/CMakeLists.txt b/src/lib/gui/CMakeLists.txt index 67ff3679d..6952269a0 100644 --- a/src/lib/gui/CMakeLists.txt +++ b/src/lib/gui/CMakeLists.txt @@ -23,6 +23,11 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) file(GLOB_RECURSE sources *.cpp) file(GLOB_RECURSE headers *.h) +if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") + file(GLOB mac_sources *.mm) + list(APPEND sources ${mac_sources}) +endif() + if(ADD_HEADERS_TO_SOURCES) list(APPEND sources ${headers}) endif() diff --git a/src/lib/gui/ElevateMode.h b/src/lib/gui/ElevateMode.h deleted file mode 100644 index d2099912e..000000000 --- a/src/lib/gui/ElevateMode.h +++ /dev/null @@ -1,35 +0,0 @@ -/* - * synergy -- mouse and keyboard sharing utility - * Copyright (C) 2016 Symless - * - * This package is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * found in the file LICENSE that should have accompanied this file. - * - * This package is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -// The elevate mode tristate determines two behaviours on Windows. -// The first, switch-on-desk-switch (SodS), passed through synergyd as a -// command line argument to synergy core, determines if the server restarts -// when switching Windows desktops (e.g. when Windows UAC dialog pops up). -// The second, passed as a boolean flag to Synergyd over the IPC inside -// IpcMessageType::CommandMessage, determines whether Synergy should be started -// with elevated privileges. -// -// The matrix for these two behaviours is as follows: -// SodS Elevate -// ___________________________ -// ElevateAsNeeded | true | false -// ElevateAlways | false | true -// ElevateNever | false | false -// -enum ElevateMode { ElevateAsNeeded = 0, ElevateAlways = 1, ElevateNever = 2 }; diff --git a/src/lib/gui/Logger.cpp b/src/lib/gui/Logger.cpp new file mode 100644 index 000000000..b90764c3d --- /dev/null +++ b/src/lib/gui/Logger.cpp @@ -0,0 +1,100 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Logger.h" + +#include "string_utils.h" + +#include +#include +#include +#include + +namespace synergy::gui { + +Logger Logger::s_instance; + +QString printLine( + FILE *out, const QString &type, const QString &message, + const QString &fileLine = "") { + auto datetime = QDateTime::currentDateTime().toString("yyyy-MM-ddTHH:mm:ss"); + auto logLine = QString("[%1] %2: %3").arg(datetime).arg(type).arg(message); + + if (!fileLine.isEmpty()) { + logLine += "\n\t" + fileLine; + } + + auto logLineUtf = logLine.toUtf8(); + auto logLine_c = logLineUtf.constData(); + fprintf(out, "%s\n", logLine_c); + fflush(out); + return logLine; +} + +void Logger::loadEnvVars() { + const auto debugEnvVar = qEnvironmentVariable("SYNERGY_GUI_DEBUG"); + if (!debugEnvVar.isEmpty()) { + m_debug = strToTrue(debugEnvVar); + } + + const auto verboseEnvVar = qEnvironmentVariable("SYNERGY_GUI_VERBOSE"); + if (!verboseEnvVar.isEmpty()) { + m_verbose = strToTrue(verboseEnvVar); + } +} + +void Logger::logVerbose(const QString &message) const { + if (m_verbose) { + printLine(stdout, "VERBOSE", message); + } +} + +void Logger::handleMessage( + QtMsgType type, const QMessageLogContext &context, const QString &message) { + + QString typeString; + auto out = stdout; + switch (type) { + case QtDebugMsg: + typeString = "DEBUG"; + if (!m_debug) { + return; + } + break; + case QtInfoMsg: + typeString = "INFO"; + break; + case QtWarningMsg: + typeString = "WARNING"; + out = stderr; + break; + case QtCriticalMsg: + typeString = "CRITICAL"; + out = stderr; + break; + case QtFatalMsg: + typeString = "FATAL"; + out = stderr; + break; + } + + const auto fileLine = QString("%1:%2").arg(context.file).arg(context.line); + const auto logLine = printLine(out, typeString, message, fileLine); + emit newLine(logLine); +} + +} // namespace synergy::gui diff --git a/src/lib/gui/Logger.h b/src/lib/gui/Logger.h new file mode 100644 index 000000000..156980e6a --- /dev/null +++ b/src/lib/gui/Logger.h @@ -0,0 +1,55 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#ifdef NDEBUG +const bool kDebug = false; +#else +const bool kDebug = true; +#endif + +namespace synergy::gui { + +class Logger : public QObject { + Q_OBJECT + +public: + static Logger &instance() { return s_instance; } + + void loadEnvVars(); + void handleMessage( + QtMsgType type, const QMessageLogContext &context, + const QString &message); + void logVerbose(const QString &message) const; + +signals: + void newLine(const QString &line); + +private: + static Logger s_instance; + bool m_debug = kDebug; + bool m_verbose = false; +}; + +inline void logVerbose(const QString &message) { + Logger::instance().logVerbose(message); +} + +} // namespace synergy::gui diff --git a/src/gui/src/OSXHelpers.h b/src/lib/gui/OSXHelpers.h similarity index 90% rename from src/gui/src/OSXHelpers.h rename to src/lib/gui/OSXHelpers.h index 1911e89a0..2670494ce 100644 --- a/src/gui/src/OSXHelpers.h +++ b/src/lib/gui/OSXHelpers.h @@ -1,6 +1,6 @@ /* * synergy -- mouse and keyboard sharing utility - * Copyright (C) 2015 Synergy Si Ltd. + * Copyright (C) 2015 Symless Ltd. * * This package is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -15,9 +15,7 @@ * along with this program. If not, see . */ -#if !defined(OSXHELPERS__H) - -#define OSXHELPERS__H +#pragma once #include @@ -28,5 +26,3 @@ bool isOSXDevelopmentBuild(); bool showOSXNotification(const QString &title, const QString &body); bool isOSXInterfaceStyleDark(); IconsTheme getOSXIconsTheme(); - -#endif diff --git a/src/gui/src/OSXHelpers.mm b/src/lib/gui/OSXHelpers.mm similarity index 99% rename from src/gui/src/OSXHelpers.mm rename to src/lib/gui/OSXHelpers.mm index 0c98afc16..30a758bb0 100644 --- a/src/gui/src/OSXHelpers.mm +++ b/src/lib/gui/OSXHelpers.mm @@ -1,6 +1,6 @@ /* * synergy -- mouse and keyboard sharing utility - * Copyright (C) 2015 Synergy Si Ltd. + * Copyright (C) 2015 Symless Ltd. * * This package is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License diff --git a/src/lib/gui/QIpcClient.cpp b/src/lib/gui/QIpcClient.cpp deleted file mode 100644 index ed2ddc02d..000000000 --- a/src/lib/gui/QIpcClient.cpp +++ /dev/null @@ -1,155 +0,0 @@ -/* - * synergy -- mouse and keyboard sharing utility - * Copyright (C) 2012 Symless Ltd. - * - * This package is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * found in the file LICENSE that should have accompanied this file. - * - * This package is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "QIpcClient.h" - -#include "IpcReader.h" -#include "global/Ipc.h" - -#include -#include -#include - -QIpcClient::QIpcClient(const StreamProvider &streamProvider) - : m_ReaderStarted(false), - m_Enabled(false), - m_StreamProvider(streamProvider) { - - m_Socket = new QTcpSocket(this); - - if (!m_StreamProvider) { - m_StreamProvider = [this]() { - return std::make_shared(m_Socket); - }; - } - - connect(m_Socket, SIGNAL(connected()), this, SLOT(connected())); - connect( - m_Socket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)), this, - SLOT(error(QAbstractSocket::SocketError))); - - m_Reader = new IpcReader(m_Socket); - connect( - m_Reader, SIGNAL(readLogLine(const QString &)), this, - SLOT(handleReadLogLine(const QString &))); -} - -QIpcClient::~QIpcClient() { - delete m_Reader; - delete m_Socket; -} - -void QIpcClient::connected() { - - sendHello(); - emit infoMessage("connection established"); -} - -void QIpcClient::connectToHost() { - m_Enabled = true; - - emit infoMessage("connecting to service..."); - const auto port = static_cast(kIpcPort); - m_Socket->connectToHost(QHostAddress(QHostAddress::LocalHost), port); - - if (!m_ReaderStarted) { - m_Reader->start(); - m_ReaderStarted = true; - } -} - -void QIpcClient::disconnectFromHost() { - emit infoMessage("service disconnect"); - m_Reader->stop(); - m_Socket->close(); -} - -void QIpcClient::error(QAbstractSocket::SocketError error) { - QString text; - switch (error) { - case 0: - text = "connection refused"; - break; - case 1: - text = "remote host closed"; - break; - default: - text = QString("code=%1").arg(error); - break; - } - - emit errorMessage(QString("ipc connection error, %1").arg(text)); - - QTimer::singleShot(1000, this, SLOT(retryConnect())); -} - -void QIpcClient::retryConnect() { - if (m_Enabled) { - connectToHost(); - } -} - -void QIpcClient::sendHello() { - auto stream = m_StreamProvider(); - stream->writeRawData(kIpcMsgHello, 4); - - char typeBuf[1]; - typeBuf[0] = static_cast(IpcClientType::GUI); - stream->writeRawData(typeBuf, 1); -} - -void QIpcClient::sendCommand( - const QString &command, ElevateMode const elevate) { - auto stream = m_StreamProvider(); - stream->writeRawData(kIpcMsgCommand, 4); - - std::string stdStringCommand = command.toStdString(); - const char *charCommand = stdStringCommand.c_str(); - auto length = static_cast(stdStringCommand.length()); - - char lenBuf[4]; - intToBytes(length, lenBuf, 4); - stream->writeRawData(lenBuf, 4); - stream->writeRawData(charCommand, length); - - char elevateBuf[1]; - // Refer to enum ElevateMode documentation for why this flag is mapped this - // way - elevateBuf[0] = (elevate == ElevateAlways) ? 1 : 0; - stream->writeRawData(elevateBuf, 1); -} - -void QIpcClient::handleReadLogLine(const QString &text) { - emit readLogLine(text); -} - -// TODO: qt must have a built in way of converting int to bytes. -void QIpcClient::intToBytes(int value, char *buffer, int size) { - if (size == 1) { - buffer[0] = value & 0xff; - } else if (size == 2) { - buffer[0] = (value >> 8) & 0xff; - buffer[1] = value & 0xff; - } else if (size == 4) { - buffer[0] = (value >> 24) & 0xff; - buffer[1] = (value >> 16) & 0xff; - buffer[2] = (value >> 8) & 0xff; - buffer[3] = value & 0xff; - } else { - // TODO: other sizes, if needed. - } -} diff --git a/src/lib/gui/TlsCertificate.h b/src/lib/gui/TlsCertificate.h deleted file mode 100644 index 15de65b70..000000000 --- a/src/lib/gui/TlsCertificate.h +++ /dev/null @@ -1,58 +0,0 @@ -/* - * synergy -- mouse and keyboard sharing utility - * Copyright (C) 2015 Symless Ltd. - * - * This package is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * found in the file LICENSE that should have accompanied this file. - * - * This package is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include "gui/CoreInterface.h" - -#include - -class TlsCertificate : public QObject { - Q_OBJECT - -public: - explicit TlsCertificate(QObject *parent = nullptr); - -public slots: - /// @brief Generates a TLS cert and private key - /// @param [in] QString path The path of the file to be generated - /// @param [in] QString keyLength The size of the private key. default: 2048 - /// @param [in] bool Should the file be created regardless of if the file - /// already exists - void generateCertificate( - const QString &path = QString(), const QString &keyLength = "2048", - bool forceGen = false); - - /// @brief Get the key length of a TLS private key - /// @param [in] QString path The path of the file to checked - /// @return QString The key legnth as a string - QString getCertKeyLength(const QString &path); - -signals: - void error(QString e); - void info(QString i); - void generateFinished(); - -private: - bool runTool(const QStringList &args); - void generateFingerprint(const QString &certificateFilename); - -private: - QString m_ProfileDir; - QString m_ToolOutput; - CoreInterface m_CoreInterface; -}; diff --git a/src/lib/gui/TrayIcon.cpp b/src/lib/gui/TrayIcon.cpp new file mode 100644 index 000000000..99faedc3f --- /dev/null +++ b/src/lib/gui/TrayIcon.cpp @@ -0,0 +1,75 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2021 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "TrayIcon.h" + +#include "constants.h" + +namespace synergy::gui { + +const auto kShowRetryInterval = 1000; + +void TrayIcon::setIcon(const QIcon &icon) { + m_icon = icon; + if (m_pTrayIcon && !icon.isNull()) { + m_pTrayIcon->setIcon(icon); + } +} + +void TrayIcon::showRetryLoop() { + // HACK: apparently this is needed to create a dbus connection, and the hide + // is needed to make use of the object so the dbus connection doesn't get + // optimized away by the compiler. + // TODO: we should verify that this hack actually works. + QSystemTrayIcon trayIcon; + trayIcon.hide(); + + if (QSystemTrayIcon::isSystemTrayAvailable()) { + m_pTrayIcon->show(); + } else { + // on some platforms, it's not always possible to create the tray when the + // app starts, so keep trying until it is possible. + qDebug("system tray not ready yet, retrying in %d ms", kShowRetryInterval); + QTimer::singleShot(kShowRetryInterval, this, &TrayIcon::showRetryLoop); + } +} + +void TrayIcon::create(std::vector const &actions) { + m_pTrayIconMenu = std::make_unique(); + + for (auto action : actions) { + if (action) { + m_pTrayIconMenu->addAction(action); + } else { + m_pTrayIconMenu->addSeparator(); + } + } + + m_pTrayIcon = std::make_unique(); + setIcon(m_icon); + + connect( + m_pTrayIcon.get(), &QSystemTrayIcon::activated, this, + &TrayIcon::activated); + + m_pTrayIcon->setContextMenu(m_pTrayIconMenu.get()); + m_pTrayIcon->setToolTip(kAppName); + + showRetryLoop(); +} + +} // namespace synergy::gui diff --git a/src/lib/gui/TrayIcon.h b/src/lib/gui/TrayIcon.h new file mode 100644 index 000000000..f9d8350cd --- /dev/null +++ b/src/lib/gui/TrayIcon.h @@ -0,0 +1,50 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2021 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace synergy::gui { + +class TrayIcon : public QObject { + Q_OBJECT +public: + explicit TrayIcon() = default; + + void create(std::vector const &actions); + void setIcon(const QIcon &icon); + +signals: + void activated(QSystemTrayIcon::ActivationReason reason); + +private: + void showRetryLoop(); + + std::unique_ptr m_pTrayIcon; + std::unique_ptr m_pTrayIconMenu; + std::function m_init; + std::function m_setIcon; + QIcon m_icon; +}; + +} // namespace synergy::gui diff --git a/src/lib/gui/byte_utils.h b/src/lib/gui/byte_utils.h new file mode 100644 index 000000000..9d9034da3 --- /dev/null +++ b/src/lib/gui/byte_utils.h @@ -0,0 +1,42 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +namespace synergy::gui { + +inline int bytesToInt(const char *buffer, size_t size) { + QByteArray byteArray(buffer, static_cast(size)); + QDataStream stream(byteArray); + int result; + stream >> result; + return result; +} + +inline QByteArray intToBytes(int value) { + QByteArray bytes; + QDataStream stream(&bytes, QIODevice::WriteOnly); + stream << value; + return bytes; +} + +} // namespace synergy::gui diff --git a/src/lib/gui/AppConfig.cpp b/src/lib/gui/config/AppConfig.cpp similarity index 88% rename from src/lib/gui/AppConfig.cpp rename to src/lib/gui/config/AppConfig.cpp index 0f3095b6f..cede9cce0 100644 --- a/src/lib/gui/AppConfig.cpp +++ b/src/lib/gui/config/AppConfig.cpp @@ -28,13 +28,15 @@ #include #include -using ConfigScopes = synergy::gui::ConfigScopes; -using IConfigScopes = synergy::gui::IConfigScopes; +using namespace synergy::gui; // this should be incremented each time the wizard is changed, // which will force it to re-run for existing installations. const int kWizardVersion = 8; +static const char *const kLogLevelNames[] = { + "INFO", "DEBUG", "DEBUG1", "DEBUG2"}; + #if defined(Q_OS_WIN) const char AppConfig::m_CoreServerName[] = "synergys.exe"; const char AppConfig::m_CoreClientName[] = "synergyc.exe"; @@ -94,11 +96,9 @@ const char *const AppConfig::m_SettingsName[] = { "showCloseReminder", }; -static const char *logLevelNames[] = {"INFO", "DEBUG", "DEBUG1", "DEBUG2"}; - AppConfig::AppConfig( synergy::gui::IConfigScopes &scopes, std::shared_ptr deps) - : m_scopes(scopes), + : m_Scopes(scopes), m_pDeps(deps), m_ScreenName(deps->hostname()) { determineScope(); @@ -120,8 +120,8 @@ void AppConfig::recallFromAllScopes() { m_WizardLastRun = findInAllScopes(kWizardLastRun, m_WizardLastRun).toInt(); m_LoadFromSystemScope = findInAllScopes(kLoadSystemSettings, m_LoadFromSystemScope).toBool(); - m_licenseNextCheck = - findInAllScopes(kLicenseNextCheck, m_licenseNextCheck).toULongLong(); + m_LicenseNextCheck = + findInAllScopes(kLicenseNextCheck, m_LicenseNextCheck).toULongLong(); } void AppConfig::recallFromCurrentScope() { @@ -165,8 +165,7 @@ void AppConfig::recallFromCurrentScope() { m_CloseToTray = getFromCurrentScope(kCloseToTray, m_CloseToTray).toBool(); m_TlsEnabled = getFromCurrentScope(kTlsEnabled, m_TlsEnabled).toBool(); m_TlsCertPath = getFromCurrentScope(kTlsCertPath, m_TlsCertPath).toString(); - m_TlsKeyLength = - getFromCurrentScope(kTlsKeyLength, m_TlsKeyLength).toString(); + m_TlsKeyLength = getFromCurrentScope(kTlsKeyLength, m_TlsKeyLength).toInt(); m_MainWindowPosition = getFromCurrentScope( kMainWindowPosition, [](const QVariant &v) { return v.toPoint(); }); m_MainWindowSize = getFromCurrentScope( @@ -197,13 +196,13 @@ void AppConfig::recallScreenName() { void AppConfig::commit() { using enum Setting; - qDebug("comitting app config"); + qDebug("committing app config"); saveToAllScopes(kWizardLastRun, m_WizardLastRun); saveToAllScopes(kLoadSystemSettings, m_LoadFromSystemScope); saveToAllScopes(kClientGroupChecked, m_ClientGroupChecked); saveToAllScopes(kServerGroupChecked, m_ServerGroupChecked); - saveToAllScopes(kLicenseNextCheck, m_licenseNextCheck); + saveToAllScopes(kLicenseNextCheck, m_LicenseNextCheck); if (isActiveScopeWritable()) { setInCurrentScope(kScreenName, m_ScreenName); @@ -214,7 +213,8 @@ void AppConfig::commit() { setInCurrentScope(kLogFilename, m_LogFilename); setInCurrentScope(kStartedBefore, m_StartedBefore); setInCurrentScope(kElevateMode, static_cast(m_ElevateMode)); - setInCurrentScope(kElevateModeLegacy, m_ElevateMode == ElevateAlways); + setInCurrentScope( + kElevateModeLegacy, m_ElevateMode == ElevateMode::kAlways); setInCurrentScope(kTlsEnabled, m_TlsEnabled); setInCurrentScope(kAutoHide, m_AutoHide); setInCurrentScope(kSerialKey, m_SerialKey); @@ -249,11 +249,11 @@ void AppConfig::determineScope() { // first, try to determine if the system scope should be used according to the // user scope... - if (m_scopes.scopeContains( + if (m_Scopes.scopeContains( settingName(Setting::kLoadSystemSettings), ConfigScopes::Scope::User)) { auto loadFromSystemScope = - m_scopes + m_Scopes .getFromScope( settingName(Setting::kLoadSystemSettings), m_LoadFromSystemScope, ConfigScopes::Scope::User) @@ -265,7 +265,7 @@ void AppConfig::determineScope() { // ...failing that, check the system scope instead to see if an arbitrary // required setting is present. if it is, then we can assume that the system // scope should be used. - else if (m_scopes.scopeContains( + else if (m_Scopes.scopeContains( settingName(Setting::kScreenName), ConfigScopes::Scope::System)) { setLoadFromSystemScope(true); @@ -275,7 +275,7 @@ void AppConfig::determineScope() { void AppConfig::recallSerialKey() { using enum Setting; - if (!m_scopes.scopeContains(settingName(kLoadSystemSettings))) { + if (!m_Scopes.scopeContains(settingName(kLoadSystemSettings))) { qDebug("no serial key in current scope, skipping"); return; } @@ -294,7 +294,7 @@ void AppConfig::recallSerialKey() { void AppConfig::recallElevateMode() { using enum Setting; - if (!m_scopes.scopeContains(settingName(kElevateMode))) { + if (!m_Scopes.scopeContains(settingName(kElevateMode))) { qDebug("elevate mode not set yet, skipping"); return; } @@ -322,24 +322,24 @@ QString AppConfig::settingName(Setting name) { } template void AppConfig::setInCurrentScope(Setting name, T value) { - m_scopes.setInScope(settingName(name), value); + m_Scopes.setInScope(settingName(name), value); } template void AppConfig::saveToAllScopes(Setting name, T value) { - m_scopes.setInScope(settingName(name), value, ConfigScopes::Scope::User); - m_scopes.setInScope(settingName(name), value, ConfigScopes::Scope::System); + m_Scopes.setInScope(settingName(name), value, ConfigScopes::Scope::User); + m_Scopes.setInScope(settingName(name), value, ConfigScopes::Scope::System); } QVariant AppConfig::getFromCurrentScope( Setting name, const QVariant &defaultValue) const { - return m_scopes.getFromScope(settingName(name), defaultValue); + return m_Scopes.getFromScope(settingName(name), defaultValue); } template std::optional AppConfig::getFromCurrentScope( Setting name, std::function toType) const { - if (m_scopes.scopeContains(settingName(name))) { - return toType(m_scopes.getFromScope(settingName(name))); + if (m_Scopes.scopeContains(settingName(name))) { + return toType(m_Scopes.getFromScope(settingName(name))); } else { return std::nullopt; } @@ -348,7 +348,7 @@ std::optional AppConfig::getFromCurrentScope( template void AppConfig::setInCurrentScope(Setting name, const std::optional &value) { if (value.has_value()) { - m_scopes.setInScope(settingName(name), value.value()); + m_Scopes.setInScope(settingName(name), value.value()); } } @@ -359,14 +359,14 @@ AppConfig::findInAllScopes(Setting name, const QVariant &defaultValue) const { QVariant result(defaultValue); QString setting(settingName(name)); - if (m_scopes.scopeContains(setting)) { - result = m_scopes.getFromScope(setting, defaultValue); - } else if (m_scopes.activeScope() == System) { - if (m_scopes.scopeContains(setting, User)) { - result = m_scopes.getFromScope(setting, defaultValue, User); + if (m_Scopes.scopeContains(setting)) { + result = m_Scopes.getFromScope(setting, defaultValue); + } else if (m_Scopes.activeScope() == System) { + if (m_Scopes.scopeContains(setting, User)) { + result = m_Scopes.getFromScope(setting, defaultValue, User); } - } else if (m_scopes.scopeContains(setting, System)) { - result = m_scopes.getFromScope(setting, defaultValue, System); + } else if (m_Scopes.scopeContains(setting, System)) { + result = m_Scopes.getFromScope(setting, defaultValue, System); } return result; @@ -388,18 +388,18 @@ void AppConfig::loadScope(ConfigScopes::Scope scope) { qFatal("invalid scope"); } - if (m_scopes.activeScope() == scope) { + if (m_Scopes.activeScope() == scope) { qDebug("already in required scope, skipping"); return; } - m_scopes.setActiveScope(scope); + m_Scopes.setActiveScope(scope); // only signal ready if there is at least one setting in the required scope. // this prevents the current settings from being set back to default. - if (m_scopes.scopeContains( - settingName(Setting::kScreenName), m_scopes.activeScope())) { - m_scopes.signalReady(); + if (m_Scopes.scopeContains( + settingName(Setting::kScreenName), m_Scopes.activeScope())) { + m_Scopes.signalReady(); } else { qDebug("no screen name in scope, skipping"); } @@ -421,11 +421,11 @@ void AppConfig::setLoadFromSystemScope(bool value) { } bool AppConfig::isActiveScopeWritable() const { - return m_scopes.isActiveScopeWritable(); + return m_Scopes.isActiveScopeWritable(); } bool AppConfig::isActiveScopeSystem() const { - return m_scopes.activeScope() == ConfigScopes::Scope::System; + return m_Scopes.activeScope() == ConfigScopes::Scope::System; } QString AppConfig::logDir() const { @@ -446,7 +446,7 @@ void AppConfig::persistLogDir() const { // Begin getters /////////////////////////////////////////////////////////////////////////////// -IConfigScopes &AppConfig::scopes() { return m_scopes; } +IConfigScopes &AppConfig::scopes() { return m_Scopes; } bool AppConfig::activationHasRun() const { return m_ActivationHasRun; } @@ -464,7 +464,7 @@ bool AppConfig::logToFile() const { return m_LogToFile; } const QString &AppConfig::logFilename() const { return m_LogFilename; } -QString AppConfig::logLevelText() const { return logLevelNames[logLevel()]; } +QString AppConfig::logLevelText() const { return kLogLevelNames[logLevel()]; } ProcessMode AppConfig::processMode() const { return m_EnableService ? ProcessMode::kService : ProcessMode::kDesktop; @@ -493,7 +493,7 @@ bool AppConfig::invertScrollDirection() const { } unsigned long long AppConfig::licenseNextCheck() const { - return m_licenseNextCheck; + return m_LicenseNextCheck; } bool AppConfig::languageSync() const { return m_LanguageSync; } @@ -504,7 +504,7 @@ bool AppConfig::invertConnection() const { return m_InvertConnection; } QString AppConfig::tlsCertPath() const { return m_TlsCertPath; } -QString AppConfig::tlsKeyLength() const { return m_TlsKeyLength; } +int AppConfig::tlsKeyLength() const { return m_TlsKeyLength; } bool AppConfig::enableService() const { return m_EnableService; } @@ -555,18 +555,28 @@ void AppConfig::setTlsEnabled(bool value) { } void AppConfig::setTlsCertPath(const QString &value) { - m_TlsChanged = m_TlsCertPath != value; + if (m_TlsCertPath != value) { + // deliberately only set the changed flag if there was a change. + // it's important not to set this flag to false here. + m_TlsChanged = true; + } m_TlsCertPath = value; } -void AppConfig::setTlsKeyLength(const QString &value) { - m_TlsChanged = m_TlsKeyLength != value; +void AppConfig::setTlsKeyLength(int value) { + if (m_TlsKeyLength != value) { + // deliberately only set the changed flag if there was a change. + // it's important not to set this flag to false here. + m_TlsChanged = true; + } m_TlsKeyLength = value; } + void AppConfig::setSerialKey(const QString &serialKey) { m_SerialKey = serialKey; saveToAllScopes(Setting::kSerialKey, m_SerialKey); } + void AppConfig::setServerGroupChecked(bool newValue) { m_ServerGroupChecked = newValue; } @@ -618,7 +628,7 @@ void AppConfig::setElevateMode(ElevateMode em) { m_ElevateMode = em; } void AppConfig::setAutoHide(bool b) { m_AutoHide = b; } void AppConfig::setLicenseNextCheck(unsigned long long time) { - m_licenseNextCheck = time; + m_LicenseNextCheck = time; } void AppConfig::setInvertScrollDirection(bool newValue) { diff --git a/src/lib/gui/AppConfig.h b/src/lib/gui/config/AppConfig.h similarity index 83% rename from src/lib/gui/AppConfig.h rename to src/lib/gui/config/AppConfig.h index be0e62cf1..0e5b158ea 100644 --- a/src/lib/gui/AppConfig.h +++ b/src/lib/gui/config/AppConfig.h @@ -18,10 +18,10 @@ #pragma once -#include "ConfigScopes.h" -#include "CoreInterface.h" #include "ElevateMode.h" #include "IAppConfig.h" +#include "IConfigScopes.h" +#include "gui/core/CoreTool.h" #include #include @@ -32,10 +32,11 @@ #include #include -enum class ProcessMode { kService, kDesktop }; +namespace synergy::gui { -const ElevateMode kDefaultElevateMode = ElevateAsNeeded; +const ElevateMode kDefaultElevateMode = ElevateMode::kAutomatic; const QString kDefaultLogFile = "synergy.log"; +const int kDefaultTlsKeyLength = 2048; #if defined(Q_OS_WIN) const ProcessMode kDefaultProcessMode = ProcessMode::kService; @@ -49,6 +50,8 @@ const bool kDefaultShowDevThanks = true; const bool kDefaultShowDevThanks = false; #endif // SYNERGY_SHOW_DEV_THANKS +} // namespace synergy::gui + /** * @brief Simply reads and writes app settings. * @@ -58,6 +61,9 @@ const bool kDefaultShowDevThanks = false; * becoming a god object. */ class AppConfig : public QObject, public synergy::gui::IAppConfig { + using ProcessMode = synergy::gui::ProcessMode; + using IConfigScopes = synergy::gui::IConfigScopes; + Q_OBJECT private: @@ -110,20 +116,19 @@ private: public: struct Deps { virtual ~Deps() = default; - virtual QString profileDir() const { - return m_coreInterface.getProfileDir(); - } + virtual QString profileDir() const { return m_coreTool.getProfileDir(); } virtual QString hostname() const { return QHostInfo::localHostName(); } private: - [[no_unique_address]] CoreInterface m_coreInterface; + [[no_unique_address]] CoreTool m_coreTool; }; explicit AppConfig( - synergy::gui::IConfigScopes &scopes, + IConfigScopes &scopes, std::shared_ptr deps = std::make_shared()); - synergy::gui::IConfigScopes &scopes(); + IConfigScopes &scopes(); + void determineScope(); /** * @brief Commits the current settings to the active scope. @@ -131,50 +136,47 @@ public: */ void commit(); - void determineScope(); - /** * Getters */ - void setActivationHasRun(bool value); + ProcessMode processMode() const override; + ElevateMode elevateMode() const override; + bool tlsEnabled() const override; + QString tlsCertPath() const override; + int tlsKeyLength() const override; + QString logLevelText() const override; + const QString &screenName() const override; + bool logToFile() const override; + bool preventSleep() const override; + const QString &logFilename() const override; + QString coreServerName() const override; + QString coreClientName() const override; + bool invertConnection() const override; + void persistLogDir() const override; + QString serialKey() const override; + bool languageSync() const override; + bool invertScrollDirection() const override; + int port() const override; + bool useExternalConfig() const override; + const QString &configFile() const override; + const QString &networkInterface() const override; bool isActiveScopeWritable() const; bool isActiveScopeSystem() const; - const QString &screenName() const; - int port() const; - const QString &networkInterface() const; int logLevel() const; - bool logToFile() const; - const QString &logFilename() const; - QString logLevelText() const; - ProcessMode processMode() const; bool wizardShouldRun() const; bool startedBefore() const; - QString coreServerName() const; - QString coreClientName() const; QString logDir() const; - void persistLogDir() const; - ElevateMode elevateMode() const; bool autoHide() const; - bool invertScrollDirection() const; unsigned long long licenseNextCheck() const; - bool languageSync() const; - bool preventSleep() const; - bool invertConnection() const; bool serverGroupChecked() const; - bool useExternalConfig() const; - const QString &configFile() const; bool useInternalConfig() const; bool clientGroupChecked() const; QString serverHostname() const; QString lastVersion() const; bool enableService() const; bool closeToTray() const; - QString serialKey() const; bool activationHasRun() const; - bool tlsEnabled() const override; - QString tlsCertPath() const override; - QString tlsKeyLength() const override; std::optional mainWindowSize() const; std::optional mainWindowPosition() const; bool showDevThanks() const; @@ -184,6 +186,7 @@ public: * Setters */ + void setActivationHasRun(bool value); void setScreenName(const QString &s); void setPort(int i); void setNetworkInterface(const QString &s); @@ -211,7 +214,7 @@ public: void setEnableService(bool enabled); void setCloseToTray(bool minimize); void setTlsCertPath(const QString &path); - void setTlsKeyLength(const QString &length); + void setTlsKeyLength(int length); void setInvertConnection(bool value); void setMainWindowSize(const QSize &size); void setMainWindowPosition(const QPoint &position); @@ -274,30 +277,30 @@ private: /// @brief This method loads config from specified scope /// @param [in] scope which should be loaded. - void loadScope(synergy::gui::ConfigScopes::Scope scope); + void loadScope(IConfigScopes::Scope scope); /** * @brief Gets a TLS certificate path based on the user's profile dir. */ QString defaultTlsCertPath() const; - synergy::gui::IConfigScopes &m_scopes; + synergy::gui::IConfigScopes &m_Scopes; std::shared_ptr m_pDeps; QString m_ScreenName; int m_Port = 24800; QString m_Interface = ""; int m_LogLevel = 0; bool m_LogToFile = false; - QString m_LogFilename = logDir() + kDefaultLogFile; + QString m_LogFilename = logDir() + synergy::gui::kDefaultLogFile; int m_WizardLastRun = 0; bool m_StartedBefore = false; - ElevateMode m_ElevateMode = kDefaultElevateMode; + ElevateMode m_ElevateMode = synergy::gui::kDefaultElevateMode; QString m_ActivateEmail = ""; bool m_TlsEnabled = true; bool m_AutoHide = false; QString m_SerialKey = ""; QString m_LastVersion = ""; - unsigned long long m_licenseNextCheck = 0; + unsigned long long m_LicenseNextCheck = 0; bool m_ActivationHasRun = false; bool m_InvertScrollDirection = false; bool m_LanguageSync = true; @@ -309,13 +312,14 @@ private: bool m_UseInternalConfig = false; bool m_ClientGroupChecked = false; QString m_ServerHostname = ""; - bool m_EnableService = kDefaultProcessMode == ProcessMode::kService; + bool m_EnableService = + synergy::gui::kDefaultProcessMode == ProcessMode::kService; bool m_CloseToTray = true; QString m_TlsCertPath = defaultTlsCertPath(); - QString m_TlsKeyLength = "2048"; + int m_TlsKeyLength = synergy::gui::kDefaultTlsKeyLength; std::optional m_MainWindowSize; std::optional m_MainWindowPosition; - bool m_ShowDevThanks = kDefaultShowDevThanks; + bool m_ShowDevThanks = synergy::gui::kDefaultShowDevThanks; bool m_LoadFromSystemScope = false; bool m_ShowCloseReminder = true; diff --git a/src/lib/gui/ConfigScopes.cpp b/src/lib/gui/config/ConfigScopes.cpp similarity index 97% rename from src/lib/gui/ConfigScopes.cpp rename to src/lib/gui/config/ConfigScopes.cpp index 0eef3cce4..121d5ea9b 100644 --- a/src/lib/gui/ConfigScopes.cpp +++ b/src/lib/gui/config/ConfigScopes.cpp @@ -18,20 +18,23 @@ #include "ConfigScopes.h" #include +#include #include -#include #include const auto kSystemConfigFilename = "SystemConfig.ini"; + +#if defined(Q_OS_UNIX) const auto kUnixSystemConfigPath = "/usr/local/etc/symless/"; +#endif namespace synergy::gui { QString getSystemSettingPath() { const QString settingFilename(kSystemConfigFilename); #if defined(Q_OS_WIN) - return QCoreApplication::applicationDirPath() + "\\"; -#elif defined(Q_OS_DARWIN) + return QCoreApplication::applicationDirPath() + QDir::separator(); +#elif defined(Q_OS_MAC) // it would be nice to use /Library dir, but qt has no elevate system. return kUnixSystemConfigPath + settingFilename; #elif defined(Q_OS_LINUX) diff --git a/src/lib/gui/ConfigScopes.h b/src/lib/gui/config/ConfigScopes.h similarity index 97% rename from src/lib/gui/ConfigScopes.h rename to src/lib/gui/config/ConfigScopes.h index e5110f1e8..00c5db925 100644 --- a/src/lib/gui/ConfigScopes.h +++ b/src/lib/gui/config/ConfigScopes.h @@ -32,7 +32,7 @@ class ConfigScopes : public QObject, public IConfigScopes { public: explicit ConfigScopes(); - virtual ~ConfigScopes() = default; + ~ConfigScopes() override = default; void signalReady() override; void save() override; diff --git a/src/lib/gui/config/ElevateMode.h b/src/lib/gui/config/ElevateMode.h new file mode 100644 index 000000000..49dbe82ed --- /dev/null +++ b/src/lib/gui/config/ElevateMode.h @@ -0,0 +1,38 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2016 Symless + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +/** + * @brief The elevate mode tristate determines two behaviors on Windows. + * + * The first, switch-on-desk-switch (SodS), passed through synergyd as a + * command line argument to synergy core, determines if the server restarts + * when switching Windows desktops (e.g. when Windows UAC dialog pops up). + * The second, passed as a boolean flag to Synergyd over the IPC inside + * IpcMessageType::CommandMessage, determines whether Synergy should be started + * with elevated privileges. + * + * The matrix for these two behaviors is as follows: + * + * | SodS | Elevate | + * |-----------|------------| + * kAutomatic | true | false | + * kAlways | false | true | + * kNever | false | false | + */ +enum class ElevateMode { kAutomatic = 0, kAlways = 1, kNever = 2 }; diff --git a/src/lib/gui/config/IAppConfig.h b/src/lib/gui/config/IAppConfig.h new file mode 100644 index 000000000..bd958da20 --- /dev/null +++ b/src/lib/gui/config/IAppConfig.h @@ -0,0 +1,54 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "ElevateMode.h" + +#include + +namespace synergy::gui { + +enum class ProcessMode { kService, kDesktop }; + +class IAppConfig { +public: + virtual ~IAppConfig() = default; + virtual QString tlsCertPath() const = 0; + virtual int tlsKeyLength() const = 0; + virtual bool tlsEnabled() const = 0; + virtual ProcessMode processMode() const = 0; + virtual ElevateMode elevateMode() const = 0; + virtual QString logLevelText() const = 0; + virtual const QString &screenName() const = 0; + virtual bool preventSleep() const = 0; + virtual bool logToFile() const = 0; + virtual const QString &logFilename() const = 0; + virtual QString coreServerName() const = 0; + virtual QString coreClientName() const = 0; + virtual bool invertConnection() const = 0; + virtual void persistLogDir() const = 0; + virtual QString serialKey() const = 0; + virtual bool languageSync() const = 0; + virtual bool invertScrollDirection() const = 0; + virtual int port() const = 0; + virtual bool useExternalConfig() const = 0; + virtual const QString &configFile() const = 0; + virtual const QString &networkInterface() const = 0; +}; + +} // namespace synergy::gui diff --git a/src/lib/gui/IConfigScopes.h b/src/lib/gui/config/IConfigScopes.h similarity index 100% rename from src/lib/gui/IConfigScopes.h rename to src/lib/gui/config/IConfigScopes.h diff --git a/src/lib/gui/config/IServerConfig.h b/src/lib/gui/config/IServerConfig.h new file mode 100644 index 000000000..313dda7f9 --- /dev/null +++ b/src/lib/gui/config/IServerConfig.h @@ -0,0 +1,35 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +namespace synergy::gui { + +class IServerConfig { +public: + virtual ~IServerConfig() = default; + virtual bool isFull() const = 0; + virtual bool screenExists(const QString &screenName) const = 0; + virtual bool save(const QString &fileName) const = 0; + virtual void save(QFile &file) const = 0; + virtual bool enableDragAndDrop() const = 0; +}; + +} // namespace synergy::gui diff --git a/src/lib/gui/constants.h b/src/lib/gui/constants.h index 827001437..eb11ceea1 100644 --- a/src/lib/gui/constants.h +++ b/src/lib/gui/constants.h @@ -1,6 +1,6 @@ /* * synergy -- mouse and keyboard sharing utility - * Copyright (C) 2014 Symless Ltd. + * Copyright (C) 2024 Symless Ltd. * * This package is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -19,23 +19,36 @@ #include +namespace synergy::gui { + const auto kAppName = "Synergy"; // TODO: change to `com.symless`. we'll need to gracefully import old settings, // since qt uses this on some platforms when saving settings. const auto kAppDomain = "https://symless.com"; +const int kDebugLogLevel = 1; + #ifdef SYNERGY_PRODUCT_NAME const QString kProductName = SYNERGY_PRODUCT_NAME; #else const QString kProductName; #endif -#ifdef SYNERGY_ENABLE_LICENSING -const bool kLicensingEnabled = true; +#ifdef SYNERGY_LICENSED_PRODUCT +const bool kLicensedProduct = true; #else -const bool kLicensingEnabled = false; -#endif // SYNERGY_ENABLE_LICENSING +const bool kLicensedProduct = false; +#endif + +#ifdef SYNERGY_ENABLE_ACTIVATION +#ifndef SYNERGY_LICENSED_PRODUCT +#error "activation requires licensed product" +#endif +const bool kEnableActivation = true; +#else +const bool kEnableActivation = false; +#endif // SYNERGY_ENABLE_ACTIVATION const auto kLinkBuy = R"(Buy now)"; const auto kLinkRenew = R"(Renew now)"; @@ -43,7 +56,7 @@ const auto kLinkDownload = R"(Download now)"; const auto kUrlSourceQuery = "source=gui"; const auto kUrlWebsite = "https://symless.com"; -const auto kUrlContribute = "https://github.com/symless/synergy-core"; +const auto kUrlGitHub = "https://github.com/symless/synergy-core"; const auto kUrlGnomeTrayFix = "https://extensions.gnome.org/extension/2890/tray-icons-reloaded/"; const auto kUrlProduct = QString("%1/synergy").arg(kUrlWebsite); @@ -54,3 +67,6 @@ const auto kUrlContact = const auto kUrlHelp = QString("%1/help?%2").arg(kUrlProduct, kUrlSourceQuery); const auto kUrlDownload = QString("%1/download?%2").arg(kUrlProduct, kUrlSourceQuery); +const auto kUrlBugReport = QString("%1/issues").arg(kUrlGitHub); + +} // namespace synergy::gui diff --git a/src/gui/src/ClientConnection.cpp b/src/lib/gui/core/ClientConnection.cpp similarity index 88% rename from src/gui/src/ClientConnection.cpp rename to src/lib/gui/core/ClientConnection.cpp index 2197caa94..43fb09d5b 100644 --- a/src/gui/src/ClientConnection.cpp +++ b/src/lib/gui/core/ClientConnection.cpp @@ -1,7 +1,6 @@ /* * synergy -- mouse and keyboard sharing utility - * Copyright (C) 2012-2021 Symless Ltd. - * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * Copyright (C) 2021 Symless Ltd. * * This package is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -18,12 +17,14 @@ #include "ClientConnection.h" -#include "MainWindow.h" - #include #include -ClientConnection::ClientConnection(MainWindow &parent) : m_parent(parent) {} +namespace synergy::gui { + +ClientConnection::ClientConnection(QWidget &parent, AppConfig &appConfig) + : m_parent(parent), + m_appConfig(appConfig) {} void ClientConnection::update(const QString &line) { if (m_checkConnection && checkMainWindow()) { @@ -59,13 +60,13 @@ QString ClientConnection::getMessage(const QString &line) const { message = QObject::tr("Connection failed.\nYou can’t name 2 computers the same."); } else { - QHostAddress address(m_parent.appConfig().serverHostname()); + QHostAddress address(m_appConfig.serverHostname()); if (address.isNull()) { message = QObject::tr( "We can’t connect to the server \"%1\" try to connect using the " "server IP address and check your firewall settings.") - .arg(m_parent.appConfig().serverHostname()); + .arg(m_appConfig.serverHostname()); } } @@ -82,3 +83,5 @@ void ClientConnection::showMessage(const QString &message) const { void ClientConnection::setCheckConnection(bool checkConnection) { m_checkConnection = checkConnection; } + +} // namespace synergy::gui diff --git a/src/gui/src/ClientConnection.h b/src/lib/gui/core/ClientConnection.h similarity index 78% rename from src/gui/src/ClientConnection.h rename to src/lib/gui/core/ClientConnection.h index 6e5803953..5c64ce6f3 100644 --- a/src/gui/src/ClientConnection.h +++ b/src/lib/gui/core/ClientConnection.h @@ -1,7 +1,6 @@ /* * synergy -- mouse and keyboard sharing utility - * Copyright (C) 2012-2021 Symless Ltd. - * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * Copyright (C) 2021 Symless Ltd. * * This package is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -16,19 +15,18 @@ * along with this program. If not, see . */ -#ifndef CLIENTCONNECTION_H -#define CLIENTCONNECTION_H +#pragma once + +#include "gui/config/AppConfig.h" #include +#include -class MainWindow; +namespace synergy::gui { class ClientConnection { - MainWindow &m_parent; - bool m_checkConnection = false; - public: - explicit ClientConnection(MainWindow &parent); + explicit ClientConnection(QWidget &parent, AppConfig &appConfig); void update(const QString &line); void setCheckConnection(bool checkConnection); @@ -36,6 +34,10 @@ private: QString getMessage(const QString &line) const; bool checkMainWindow(); void showMessage(const QString &message) const; + + QWidget &m_parent; + AppConfig &m_appConfig; + bool m_checkConnection = false; }; -#endif // CLIENTCONNECTION_H +} // namespace synergy::gui diff --git a/src/lib/gui/CommandProcess.cpp b/src/lib/gui/core/CommandProcess.cpp similarity index 94% rename from src/lib/gui/CommandProcess.cpp rename to src/lib/gui/core/CommandProcess.cpp index c4fde3c57..83828eab7 100644 --- a/src/lib/gui/CommandProcess.cpp +++ b/src/lib/gui/core/CommandProcess.cpp @@ -39,8 +39,7 @@ QString CommandProcess::run() { } if (process.waitForFinished()) { - output = - QString::fromLocal8Bit(process.readAllStandardOutput().trimmed()); + output = process.readAllStandardOutput().trimmed(); error = process.readAllStandardError().trimmed(); } } diff --git a/src/lib/gui/CommandProcess.h b/src/lib/gui/core/CommandProcess.h similarity index 100% rename from src/lib/gui/CommandProcess.h rename to src/lib/gui/core/CommandProcess.h diff --git a/src/lib/gui/core/CoreProcess.cpp b/src/lib/gui/core/CoreProcess.cpp new file mode 100644 index 000000000..029587055 --- /dev/null +++ b/src/lib/gui/core/CoreProcess.cpp @@ -0,0 +1,696 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "CoreProcess.h" + +#include "constants.h" +#include "gui/config/IAppConfig.h" +#include "gui/core/CoreTool.h" +#include +#include + +#if defined(Q_OS_MAC) +#include "OSXHelpers.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace synergy::gui { + +const int kRetryDelay = 1000; +const auto kLastConfigFilename = "LastConfig.cfg"; +const auto kLineSplitRegex = QRegularExpression("\r|\n|\r\n"); + +// +// free functions +// + +QString processModeToString(ProcessMode mode) { + using enum ProcessMode; + + switch (mode) { + case kDesktop: + return "desktop"; + case kService: + return "service"; + default: + qFatal("invalid process mode"); + return ""; + } +} + +/** + * @brief Wraps options that contain spaces in quotes + * + * Useful to handle things like paths with spaces (e.g. "C:\Program Files"). + * + * Can also be used to create a representation of a command that can be pasted + * into a terminal. + */ +QString makeQuotedArgs(const QString &app, const QStringList &args) { + QStringList command; + command << app; + command << args; + + QStringList quoted; + for (const auto &arg : command) { + if (arg.contains(' ')) { + quoted << QString("\"%1\"").arg(arg); + } else { + quoted << arg; + } + } + + return quoted.join(" "); +} + +/** + * @brief If IPv6, ensures the IP is surround in square brackets. + */ +QString wrapIpv6(const QString &address) { + if (!address.contains(':') || address.isEmpty()) { + return address; + } + + QString wrapped = address; + + if (address[0] != '[') { + wrapped.insert(0, '['); + } + + if (address[address.size() - 1] != ']') { + wrapped.push_back(']'); + } + + return address; +} + +// +// CoreProcess::Deps +// + +QString CoreProcess::Deps::appPath(const QString &name) const { + QDir dir(QCoreApplication::applicationDirPath()); + return dir.filePath(name); +} + +bool CoreProcess::Deps::fileExists(const QString &path) const { + return QFile::exists(path); +} + +QString CoreProcess::Deps::getProfileRoot() const { + CoreTool coreTool; + QDir appDir = coreTool.getProfileDir(); + + // the core expects the profile root dir, not the app-specific profile dir. + if (!appDir.cdUp()) { + qFatal("failed to cd up to profile root dir"); + } + + return appDir.absolutePath(); +} + +// +// CoreProcess +// + +CoreProcess::CoreProcess( + IAppConfig &appConfig, IServerConfig &serverConfig, + std::shared_ptr deps) + : m_appConfig(appConfig), + m_serverConfig(serverConfig), + m_pDeps(deps) { + + 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::readyReadStandardOutput, this, + &CoreProcess::onProcessReadyReadStandardOutput); + + connect( + &m_pDeps->process(), &QProcessProxy::readyReadStandardError, this, + &CoreProcess::onProcessReadyReadStandardError); +} + +void CoreProcess::onIpcClientServiceReady() { + if (m_processState == ProcessState::Starting) { + qDebug("service ready, continuing core process start"); + start(); + } else if (m_processState == ProcessState::Stopping) { + qDebug("service ready, continuing core process stop"); + stop(); + } else { + qCritical("service ready, but process state is not starting or stopping"); + } +} + +void CoreProcess::onIpcClientError(const QString &text) const { + 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) { handleLogLines(text); } + +void CoreProcess::onProcessReadyReadStandardOutput() { + if (m_pDeps->process()) { + handleLogLines(m_pDeps->process().readAllStandardOutput()); + } +} + +void CoreProcess::onProcessReadyReadStandardError() { + if (m_pDeps->process()) { + handleLogLines(m_pDeps->process().readAllStandardError()); + } +} + +void CoreProcess::onProcessFinished(int exitCode, QProcess::ExitStatus) { + const auto wasStarted = m_processState == ProcessState::Started; + + setProcessState(ProcessState::Stopped); + setConnectionState(ConnectionState::Disconnected); + + if (exitCode == 0) { + qDebug("desktop process exited normally"); + } else { + qWarning("desktop process exited with error code: %d", exitCode); + } + + if (wasStarted) { + qDebug("desktop process was running, retrying in %d ms", kRetryDelay); + QTimer::singleShot(kRetryDelay, [this] { start(); }); + } +} + +void CoreProcess::startDesktop(const QString &app, const QStringList &args) { + using enum ProcessState; + + if (m_processState != Starting) { + qFatal("core process must be in starting state"); + } + + // only make quoted args for printing the command for convenience; so that the + // core command can be easily copy/pasted to the terminal for testing. + const auto quoted = makeQuotedArgs(app, args); + qInfo("running command: %s", qPrintable(quoted)); + + m_pDeps->process().start(app, args); + + if (m_pDeps->process().waitForStarted()) { + setProcessState(Started); + } else { + setProcessState(Stopped); + emit error(Error::StartFailed); + } +} + +void CoreProcess::startService(const QString &app, const QStringList &args) { + using enum ProcessState; + + if (m_processState != Starting) { + qFatal("core process must be in starting state"); + } + + if (!m_pDeps->ipcClient().isConnected()) { + // when service state changes, start will be called again. + qDebug("cannot start process, ipc not connected, connecting instead"); + m_pDeps->ipcClient().connectToHost(); + return; + } + + QString commandQuoted = makeQuotedArgs(app, args); + + qInfo("running command: %s", qPrintable(commandQuoted)); + m_pDeps->ipcClient().sendCommand(commandQuoted, m_appConfig.elevateMode()); + setProcessState(Started); +} + +void CoreProcess::stopDesktop() const { + if (m_processState != ProcessState::Stopping) { + qFatal("core process must be in stopping state"); + } + + if (!m_pDeps->process()) { + qFatal("process not set, cannot stop"); + } + + qInfo("stopping core desktop process"); + + if (m_pDeps->process().state() == QProcess::ProcessState::Running) { + qDebug("process is running, closing"); + m_pDeps->process().close(); + } else { + qDebug("process is not running, skipping terminate"); + } +} + +void CoreProcess::stopService() { + if (m_processState != ProcessState::Stopping) { + qFatal("core process must be in stopping state"); + } + + if (!m_pDeps->ipcClient().isConnected()) { + qDebug("cannot stop process, ipc not connected"); + return; + } + + m_pDeps->ipcClient().sendCommand("", m_appConfig.elevateMode()); + setProcessState(ProcessState::Stopped); +} + +void CoreProcess::handleLogLines(const QString &text) { + for (const auto &line : text.split(kLineSplitRegex)) { + if (line.isEmpty()) { + continue; + } + +#if defined(Q_OS_MAC) + // HACK: macOS 10.13.4+ spamming error lines in logs making them + // impossible to read and debug; giving users a red herring. + if (line.contains("calling TIS/TSM in non-main thread environment")) { + continue; + } +#endif + + checkLogLine(line); + emit logLine(line); + } +} + +void CoreProcess::start(std::optional processModeOption) { + QMutexLocker locker(&m_processMutex); + + const auto processMode = + processModeOption.value_or(m_appConfig.processMode()); + + qInfo( + "starting core %s process (%s mode)", qPrintable(modeString()), + qPrintable(processModeToString(processMode))); + + if (m_processState == ProcessState::Started) { + qCritical("core process already started"); + return; + } + + // allow external listeners to abort the start process (e.g. licensing issue). + setProcessState(ProcessState::Starting); + emit starting(); + if (m_processState == ProcessState::Stopped) { + qDebug("core process start was cancelled by listener"); + return; + } + +#ifdef Q_OS_MAC + requestOSXNotificationPermission(); +#endif + + setConnectionState(ConnectionState::Connecting); + + QString app; + QStringList args; + + args << "-f" + << "--no-tray" + << "--debug" << m_appConfig.logLevelText(); + + args << "--name" << m_appConfig.screenName(); + + if (processMode == ProcessMode::kDesktop) { + m_pDeps->process().create(); + } else { + // tell client/server to talk to daemon through ipc. + args << "--ipc"; + +#if defined(Q_OS_WIN) + // tell the client/server to shut down when a ms windows desk + // is switched; this is because we may need to elevate or not + // based on which desk the user is in (login always needs + // elevation, where as default desk does not). + // Note that this is only enabled when synergy is set to elevate + // 'as needed' (e.g. on a UAC dialog popup) in order to prevent + // unnecessary restarts when synergy was started elevated or + // when it is not allowed to elevate. In these cases restarting + // the server is fruitless. + if (m_appConfig.elevateMode() == ElevateMode::kAutomatic) { + args << "--stop-on-desk-switch"; + } +#endif + } + +#ifndef Q_OS_LINUX + + if (m_serverConfig.enableDragAndDrop()) { + args << "--enable-drag-drop"; + } + +#endif + + if (m_appConfig.tlsEnabled()) { + args << "--enable-crypto"; + args << "--tls-cert" << m_appConfig.tlsCertPath(); + } + +#if defined(Q_OS_WIN) + // on windows, the profile directory changes depending on the user that + // launched the process (e.g. when launched with elevation). setting the + // profile dir on launch ensures it uses the same profile dir is used + // no matter how its relaunched. + args << "--profile-dir" << m_pDeps->getProfileRoot(); +#endif + + if (m_appConfig.preventSleep()) { + args << "--prevent-sleep"; + } + + if ((mode() == Mode::Client && !clientArgs(args, app)) || + (mode() == Mode::Server && !serverArgs(args, app))) { + qDebug("failed to get args for core process, aborting start"); + return; + } + + qDebug("log level: %s", qPrintable(m_appConfig.logLevelText())); + + if (m_appConfig.logToFile()) + qInfo("log file: %s", qPrintable(m_appConfig.logFilename())); + + if (processMode == ProcessMode::kDesktop) { + startDesktop(app, args); + } else if (processMode == ProcessMode::kService) { + startService(app, args); + } + + m_lastProcessMode = processMode; +} + +void CoreProcess::stop(std::optional processModeOption) { + QMutexLocker locker(&m_processMutex); + + const auto processMode = + processModeOption.value_or(m_appConfig.processMode()); + + qInfo( + "stopping core process (%s mode)", + qPrintable(processModeToString(processMode))); + + if (m_processState == ProcessState::Starting) { + qDebug("core process is starting, cancelling"); + setProcessState(ProcessState::Stopped); + } else if (m_processState != ProcessState::Stopped) { + setProcessState(ProcessState::Stopping); + + if (processMode == ProcessMode::kService) { + stopService(); + } else if (processMode == ProcessMode::kDesktop) { + stopDesktop(); + } + + } else { + qWarning("core process already stopped"); + } + + setConnectionState(ConnectionState::Disconnected); +} + +void CoreProcess::restart() { + using enum ProcessMode; + + qDebug("restarting core process"); + + const auto processMode = m_appConfig.processMode(); + + if (m_lastProcessMode != processMode) { + if (processMode == kDesktop) { + qDebug("process mode changed to desktop, stopping service process"); + stop(kService); + } else if (processMode == kService) { + qDebug("process mode changed to service, stopping desktop process"); + stop(kDesktop); + } else { + qFatal("invalid process mode"); + } + } else { + // in service mode: though there is technically no need to stop the service + // before restarting it, it does make for cleaner process state tracking, + // especially if something goes wrong with starting the service. + stop(); + } + + start(); +} + +void CoreProcess::cleanup() { + qInfo("cleaning up core process"); + + const auto isDesktop = m_appConfig.processMode() == ProcessMode::kDesktop; + const auto isRunning = m_processState == ProcessState::Started; + if (isDesktop && isRunning) { + stop(); + } + + m_pDeps->ipcClient().disconnectFromHost(); +} + +bool CoreProcess::serverArgs(QStringList &args, QString &app) { + app = m_pDeps->appPath(m_appConfig.coreServerName()); + + if (!m_pDeps->fileExists(app)) { + qFatal("core server binary does not exist"); + return false; + } + + if (m_appConfig.logToFile()) { + m_appConfig.persistLogDir(); + + args << "--log" << m_appConfig.logFilename(); + } + + QString configFilename = persistConfig(); + if (configFilename.isEmpty()) { + qFatal("config file name empty for server args"); + return false; + } + + if (m_appConfig.invertConnection()) { + qDebug("inverting server connection"); + + if (correctedAddress().isEmpty()) { + emit error(Error::AddressMissing); + qDebug("address is missing for server args"); + return false; + } + } + + // the address arg is dual purpose; when in listening mode, it's the address + // that the server listens on. when tcp sockets are inverted, it connects to + // that address. this is a bit confusing, and there should be probably be + // different args for different purposes. + args << "--address" << correctedInterface(); + + args << "-c" << configFilename; + qInfo("config file: %s", qPrintable(configFilename)); + + if (kEnableActivation && !m_appConfig.serialKey().isEmpty()) { + args << "--serial-key" << m_appConfig.serialKey(); + } + + return true; +} + +bool CoreProcess::clientArgs(QStringList &args, QString &app) { + app = m_pDeps->appPath(m_appConfig.coreClientName()); + + if (!m_pDeps->fileExists(app)) { + qFatal("core client binary does not exist"); + return false; + } + + if (m_appConfig.logToFile()) { + m_appConfig.persistLogDir(); + args << "--log" << m_appConfig.logFilename(); + } + + if (m_appConfig.languageSync()) { + args << "--sync-language"; + } + + if (m_appConfig.invertScrollDirection()) { + args << "--invert-scroll"; + } + + if (m_appConfig.invertConnection()) { + qDebug("inverting client connection"); + args << "--host"; + args << ":" + QString::number(m_appConfig.port()); + } else { + + if (correctedAddress().isEmpty()) { + emit error(Error::AddressMissing); + qDebug("address is missing for client args"); + return false; + } + + args << correctedAddress() + ":" + QString::number(m_appConfig.port()); + } + + return true; +} + +QString CoreProcess::persistConfig() const { + QString configFullPath; + if (m_appConfig.useExternalConfig()) { + return m_appConfig.configFile(); + } + + for (auto path : + {QStandardPaths::AppDataLocation, QStandardPaths::AppConfigLocation}) { + auto configDirPath = QStandardPaths::writableLocation(path); + if (!QDir().mkpath(configDirPath)) { + qWarning("failed to create config folder: %s", qPrintable(configDirPath)); + continue; + } + + QFile configFile(configDirPath + "/" + kLastConfigFilename); + if (!configFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qWarning( + "failed to open config file: %s", qPrintable(configFile.fileName())); + continue; + } + + m_serverConfig.save(configFile); + configFile.close(); + return configFile.fileName(); + } + + qFatal("failed to persist config file"); + return ""; +} + +QString CoreProcess::modeString() const { + using enum Mode; + + switch (m_mode) { + case Server: + return "server"; + case Client: + return "client"; + default: + qFatal("invalid core mode"); + return ""; + } +} + +void CoreProcess::setConnectionState(ConnectionState state) { + if (m_connectionState == state) { + return; + } + + m_connectionState = state; + emit connectionStateChanged(state); +} + +void CoreProcess::setProcessState(ProcessState state) { + if (m_processState == state) { + return; + } + + m_processState = state; + emit processStateChanged(state); +} + +void CoreProcess::checkLogLine(const QString &line) { + using enum ConnectionState; + + if (line.contains("connected to server") || line.contains("has connected")) { + setConnectionState(Connected); + + } else if (line.contains("started server")) { + setConnectionState(Listening); + } else if ( + line.contains("disconnected from server") || + line.contains("process exited")) { + setConnectionState(Disconnected); + } else if (line.contains("connecting to")) { + setConnectionState(Connecting); + } + + checkSecureSocket(line); + + // subprocess (synergys, synergyc) is not allowed to show notifications + // process the log from it and show notification from synergy instead +#ifdef Q_OS_MAC + checkOSXNotification(line); +#endif +} + +bool CoreProcess::checkSecureSocket(const QString &line) { + static const QString tlsCheckString = "network encryption protocol: "; + const auto index = line.indexOf(tlsCheckString, 0, Qt::CaseInsensitive); + if (index == -1) { + return false; + } + + emit secureSocket(true); + m_secureSocketVersion = line.mid(index + tlsCheckString.size()); + return true; +} + +#ifdef Q_OS_MAC +void CoreProcess::checkOSXNotification(const QString &line) { + static const QString needle = "OSX Notification: "; + if (line.contains(needle) && line.contains('|')) { + int delimiterPosition = line.indexOf('|'); + int start = line.indexOf(needle); + QString title = line.mid( + start + needle.length(), delimiterPosition - start - needle.length()); + QString body = + line.mid(delimiterPosition + 1, line.length() - delimiterPosition); + if (!showOSXNotification(title, body)) { + qDebug("osx notification was not shown"); + } + } +} +#endif + +QString CoreProcess::correctedInterface() const { + QString interface = wrapIpv6(m_appConfig.networkInterface()); + return interface + ":" + QString::number(m_appConfig.port()); +} + +QString CoreProcess::correctedAddress() const { return wrapIpv6(m_address); } + +} // namespace synergy::gui diff --git a/src/lib/gui/core/CoreProcess.h b/src/lib/gui/core/CoreProcess.h new file mode 100644 index 000000000..197f4a2f8 --- /dev/null +++ b/src/lib/gui/core/CoreProcess.h @@ -0,0 +1,130 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#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 + +namespace synergy::gui { + +class CoreProcess : public QObject { + using IServerConfig = synergy::gui::IServerConfig; + using QProcessProxy = synergy::gui::proxy::QProcessProxy; + using IQIpcClient = synergy::gui::ipc::IQIpcClient; + + Q_OBJECT + +public: + struct Deps { + virtual ~Deps() = default; + virtual QProcessProxy &process() { return m_process; } + virtual IQIpcClient &ipcClient() { return m_ipcClient; } + virtual QString appPath(const QString &name) const; + virtual bool fileExists(const QString &path) const; + virtual QString getProfileRoot() const; + + private: + QProcessProxy m_process; + QIpcClient m_ipcClient; + }; + + enum class Mode { None, Client, Server }; + enum class Error { AddressMissing, StartFailed }; + enum class ProcessState { Starting, Started, Stopping, Stopped }; + enum class ConnectionState { Disconnected, Connecting, Connected, Listening }; + + explicit CoreProcess( + IAppConfig &appConfig, IServerConfig &serverConfig, + std::shared_ptr deps = std::make_shared()); + + void extracted(QString &app, QStringList &args); + void start(std::optional processMode = std::nullopt); + void stop(std::optional processMode = std::nullopt); + void restart(); + void cleanup(); + + // getters + Mode mode() const { return m_mode; } + QString secureSocketVersion() const { return m_secureSocketVersion; } + bool isStarted() const { return m_processState == ProcessState::Started; } + ProcessState processState() const { return m_processState; } + ConnectionState connectionState() const { return m_connectionState; } + + // setters + void setAddress(const QString &address) { m_address = address.trimmed(); } + void setMode(Mode mode) { m_mode = mode; } + +signals: + void starting(); + void error(Error error); + void logLine(const QString &line); + void connectionStateChanged(ConnectionState state); + void processStateChanged(ProcessState state); + void secureSocket(bool enabled); + +private slots: + void onIpcClientServiceReady(); + void onIpcClientRead(const QString &text); + void onIpcClientError(const QString &text) const; + void onProcessFinished(int exitCode, QProcess::ExitStatus); + void onProcessReadyReadStandardOutput(); + void onProcessReadyReadStandardError(); + +private: + void startDesktop(const QString &app, const QStringList &args); + void startService(const QString &app, const QStringList &args); + void stopDesktop() const; + void stopService(); + bool serverArgs(QStringList &args, QString &app); + bool clientArgs(QStringList &args, QString &app); + QString persistConfig() const; + QString modeString() const; + QString processModeString() const; + void setConnectionState(ConnectionState state); + void setProcessState(ProcessState state); + void checkLogLine(const QString &line); + bool checkSecureSocket(const QString &line); + void handleLogLines(const QString &text); + QString correctedInterface() const; + QString correctedAddress() const; + +#ifdef Q_OS_MAC + void checkOSXNotification(const QString &line); +#endif + + IAppConfig &m_appConfig; + IServerConfig &m_serverConfig; + std::shared_ptr m_pDeps; + QString m_address; + ProcessState m_processState = ProcessState::Stopped; + ConnectionState m_connectionState = ConnectionState::Disconnected; + Mode m_mode = Mode::None; + QMutex m_processMutex; + QString m_secureSocketVersion = ""; + std::optional m_lastProcessMode = std::nullopt; +}; + +} // namespace synergy::gui diff --git a/src/lib/gui/CoreInterface.cpp b/src/lib/gui/core/CoreTool.cpp similarity index 83% rename from src/lib/gui/CoreInterface.cpp rename to src/lib/gui/core/CoreTool.cpp index b5370d12a..846ddd5f2 100644 --- a/src/lib/gui/CoreInterface.cpp +++ b/src/lib/gui/core/CoreTool.cpp @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -#include "gui/CoreInterface.h" +#include "gui/core/CoreTool.h" #include "CommandProcess.h" @@ -32,28 +32,27 @@ static const char kSerialKeyFilename[] = "Synergy.subkey"; static const char kSerialKeyFilename[] = ".synergy.subkey"; #endif -QString CoreInterface::getProfileDir() const { +QString CoreTool::getProfileDir() const { QStringList args("--get-profile-dir"); return QDir::cleanPath(run(args)); } -QString CoreInterface::getInstalledDir() const { +QString CoreTool::getInstalledDir() const { QStringList args("--get-installed-dir"); return QDir::cleanPath(run(args)); } -QString CoreInterface::getArch() const { +QString CoreTool::getArch() const { QStringList args("--get-arch"); return run(args); } -QString CoreInterface::getSerialKeyFilePath() const { +QString CoreTool::getSerialKeyFilePath() const { auto filename = getProfileDir() + QDir::separator() + kSerialKeyFilename; return QDir::cleanPath(filename); } -QString -CoreInterface::run(const QStringList &args, const QString &input) const { +QString CoreTool::run(const QStringList &args, const QString &input) const { QString program(QCoreApplication::applicationDirPath() + "/" + kCoreBinary); CommandProcess commandProcess(program, args, input); diff --git a/src/lib/gui/CoreInterface.h b/src/lib/gui/core/CoreTool.h similarity index 97% rename from src/lib/gui/CoreInterface.h rename to src/lib/gui/core/CoreTool.h index 3d1b62bd7..d23bc215e 100644 --- a/src/lib/gui/CoreInterface.h +++ b/src/lib/gui/core/CoreTool.h @@ -19,7 +19,7 @@ #include -class CoreInterface { +class CoreTool { public: QString getProfileDir() const; QString getInstalledDir() const; diff --git a/src/lib/gui/core/ServerConnection.cpp b/src/lib/gui/core/ServerConnection.cpp new file mode 100644 index 000000000..6832d8150 --- /dev/null +++ b/src/lib/gui/core/ServerConnection.cpp @@ -0,0 +1,90 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2021 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ServerConnection.h" + +#include "ServerMessage.h" +#include "gui/config/AppConfig.h" + +#include + +namespace synergy::gui { + +ServerConnection::ServerConnection( + QWidget &parent, AppConfig &appConfig, IServerConfig &serverConfig) + : m_parent(parent), + m_appConfig(appConfig), + m_serverConfig(serverConfig) {} + +void ServerConnection::update(const QString &line) { + ServerMessage message(line); + + if (!m_appConfig.useExternalConfig() && message.isNewClientMessage() && + !m_ignoredClients.contains(message.getClientName())) { + addClient(message.getClientName()); + } +} + +// TOOD: merge duplicated code between client and server connection +bool ServerConnection::checkMainWindow() { + bool result = m_parent.isActiveWindow(); + + if (m_parent.isMinimized() || m_parent.isHidden()) { + m_parent.showNormal(); + m_parent.activateWindow(); + result = true; + } + + return result; +} + +void ServerConnection::addClient(const QString &clientName) { + if (m_serverConfig.isFull()) { + qDebug( + "server config full, skipping add client prompt for: %s", + qPrintable(clientName)); + return; + } + + if (m_serverConfig.screenExists(clientName)) { + qDebug( + "client already added, skipping add client prompt for: %s", + qPrintable(clientName)); + return; + } + + if (!checkMainWindow()) { + qDebug( + "main window not active, skipping add client prompt for: %s", + qPrintable(clientName)); + return; + } + + QMessageBox message(&m_parent); + message.addButton(QObject::tr("Ignore"), QMessageBox::RejectRole); + message.addButton(QObject::tr("Add client"), QMessageBox::AcceptRole); + message.setText( + QObject::tr("Client with name '%1' wants to connect").arg(clientName)); + + if (message.exec() == QMessageBox::Accepted) { + emit configureClient(clientName); + } else { + m_ignoredClients.append(clientName); + } +} + +} // namespace synergy::gui diff --git a/src/gui/src/ServerConnection.h b/src/lib/gui/core/ServerConnection.h similarity index 65% rename from src/gui/src/ServerConnection.h rename to src/lib/gui/core/ServerConnection.h index 954b7bf1c..312acab5e 100644 --- a/src/gui/src/ServerConnection.h +++ b/src/lib/gui/core/ServerConnection.h @@ -1,7 +1,6 @@ /* * synergy -- mouse and keyboard sharing utility - * Copyright (C) 2012-2021 Symless Ltd. - * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * Copyright (C) 2021 Symless Ltd. * * This package is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -16,26 +15,36 @@ * along with this program. If not, see . */ -#ifndef SERVERCONNECTION_H -#define SERVERCONNECTION_H +#pragma once #include #include -class MainWindow; +#include "gui/config/AppConfig.h" +#include "gui/config/IServerConfig.h" -class ServerConnection { - MainWindow &m_parent; - QStringList m_ignoredClients; +namespace synergy::gui { + +class ServerConnection : public QObject { + Q_OBJECT + using IServerConfig = synergy::gui::IServerConfig; public: - explicit ServerConnection(MainWindow &parent); + explicit ServerConnection( + QWidget &parent, AppConfig &appConfig, IServerConfig &serverConfig); void update(const QString &line); +signals: + void configureClient(const QString &clientName); + private: void addClient(const QString &clientName); - void configureClient(const QString &clientName); bool checkMainWindow(); + + QWidget &m_parent; + AppConfig &m_appConfig; + IServerConfig &m_serverConfig; + QStringList m_ignoredClients; }; -#endif // SERVERCONNECTION_H +} // namespace synergy::gui diff --git a/src/gui/src/ServerMessage.cpp b/src/lib/gui/core/ServerMessage.cpp similarity index 94% rename from src/gui/src/ServerMessage.cpp rename to src/lib/gui/core/ServerMessage.cpp index 4b58af082..c8124534f 100644 --- a/src/gui/src/ServerMessage.cpp +++ b/src/lib/gui/core/ServerMessage.cpp @@ -1,7 +1,6 @@ /* * synergy -- mouse and keyboard sharing utility - * Copyright (C) 2012-2021 Symless Ltd. - * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * Copyright (C) 2021 Symless Ltd. * * This package is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -15,8 +14,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + #include "ServerMessage.h" +namespace synergy::gui { + ServerMessage::ServerMessage(const QString &message) : m_message(message), m_clienName(parseClientName(message)) {} @@ -50,3 +52,5 @@ QString ServerMessage::parseClientName(const QString &line) const { return clientName; } + +} // namespace synergy::gui diff --git a/src/gui/src/ServerMessage.h b/src/lib/gui/core/ServerMessage.h similarity index 86% rename from src/gui/src/ServerMessage.h rename to src/lib/gui/core/ServerMessage.h index e162dad1d..557179161 100644 --- a/src/gui/src/ServerMessage.h +++ b/src/lib/gui/core/ServerMessage.h @@ -1,7 +1,6 @@ /* * synergy -- mouse and keyboard sharing utility - * Copyright (C) 2012-2021 Symless Ltd. - * Copyright (C) 2008 Volker Lanz (vl@fidra.de) + * Copyright (C) 2021 Symless Ltd. * * This package is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -16,11 +15,12 @@ * along with this program. If not, see . */ -#ifndef SERVERMESSAGE_H -#define SERVERMESSAGE_H +#pragma once #include +namespace synergy::gui { + class ServerMessage { QString m_message; QString m_clienName; @@ -39,4 +39,4 @@ private: QString parseClientName(const QString &line) const; }; -#endif // SERVERMESSAGE_H +} // namespace synergy::gui diff --git a/src/lib/gui/IAppConfig.h b/src/lib/gui/ipc/IQIpcClient.cpp similarity index 71% rename from src/lib/gui/IAppConfig.h rename to src/lib/gui/ipc/IQIpcClient.cpp index 8e6e9fee1..26f4aa38b 100644 --- a/src/lib/gui/IAppConfig.h +++ b/src/lib/gui/ipc/IQIpcClient.cpp @@ -15,18 +15,4 @@ * along with this program. If not, see . */ -#pragma once - -#include - -namespace synergy::gui { - -class IAppConfig { -public: - virtual ~IAppConfig() = default; - virtual QString tlsCertPath() const = 0; - virtual QString tlsKeyLength() const = 0; - virtual bool tlsEnabled() const = 0; -}; - -} // namespace synergy::gui +// empty .cpp for qt moc diff --git a/src/lib/gui/ipc/IQIpcClient.h b/src/lib/gui/ipc/IQIpcClient.h new file mode 100644 index 000000000..e2bed875e --- /dev/null +++ b/src/lib/gui/ipc/IQIpcClient.h @@ -0,0 +1,43 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +#include "gui/config/ElevateMode.h" + +namespace synergy::gui::ipc { + +class IQIpcClient : public QObject { + Q_OBJECT +public: + ~IQIpcClient() override = default; + virtual void sendHello() const = 0; + virtual void + sendCommand(const QString &command, ElevateMode elevate) const = 0; + virtual void connectToHost() = 0; + virtual void disconnectFromHost() = 0; + virtual bool isConnected() const = 0; + +signals: + void read(const QString &text); + void serviceReady(); +}; + +} // namespace synergy::gui::ipc diff --git a/src/lib/gui/IpcReader.cpp b/src/lib/gui/ipc/IpcReader.cpp similarity index 50% rename from src/lib/gui/IpcReader.cpp rename to src/lib/gui/ipc/IpcReader.cpp index 7848df17b..fa42a7497 100644 --- a/src/lib/gui/IpcReader.cpp +++ b/src/lib/gui/ipc/IpcReader.cpp @@ -16,97 +16,88 @@ */ #include "IpcReader.h" + +#include "Logger.h" +#include "byte_utils.h" #include "global/Ipc.h" #include #include #include -#include + +using namespace synergy::gui; IpcReader::IpcReader(QTcpSocket *socket) : m_Socket(socket) {} -IpcReader::~IpcReader() {} - -void IpcReader::start() { - connect(m_Socket, SIGNAL(readyRead()), this, SLOT(read())); +void IpcReader::start() const { + connect( + m_Socket, &QTcpSocket::readyRead, this, &IpcReader::onSocketReadyRead); } -void IpcReader::stop() { - disconnect(m_Socket, SIGNAL(readyRead()), this, SLOT(read())); +void IpcReader::stop() const { + disconnect( + m_Socket, &QTcpSocket::readyRead, this, &IpcReader::onSocketReadyRead); } -void IpcReader::read() { +void IpcReader::onSocketReadyRead() { QMutexLocker locker(&m_Mutex); - std::cout << "ready read" << std::endl; + logVerbose("ready read"); while (m_Socket->bytesAvailable()) { - std::cout << "bytes available" << std::endl; + logVerbose("bytes available"); char codeBuf[5]; readStream(codeBuf, 4); codeBuf[4] = 0; - std::cout << "ipc read: " << codeBuf << std::endl; + logVerbose(QString("ipc read: %1").arg(codeBuf)); if (memcmp(codeBuf, kIpcMsgLogLine, 4) == 0) { - std::cout << "reading log line" << std::endl; + logVerbose("reading log line"); char lenBuf[4]; readStream(lenBuf, 4); int len = bytesToInt(lenBuf, 4); - char *data = new char[len]; - readStream(data, len); - QString line = QString::fromUtf8(data, len); - delete[] data; + std::vector dataBuf(len); + readStream(dataBuf.data(), len); + QString text = QString::fromUtf8(dataBuf.data(), len); - readLogLine(line); + emit read(text); + } else if (memcmp(codeBuf, kIpcMsgHelloBack, 4) == 0) { + logVerbose("reading hello back"); + emit helloBack(); } else { - std::cerr << "aborting, message invalid" << std::endl; + qCritical("aborting ipc read, message invalid"); return; } } - std::cout << "read done" << std::endl; + logVerbose("read done"); } bool IpcReader::readStream(char *buffer, int length) { - std::cout << "reading stream" << std::endl; + logVerbose("reading stream"); int read = 0; while (read < length) { int ask = length - read; if (m_Socket->bytesAvailable() < ask) { - std::cout << "buffer too short, waiting" << std::endl; + logVerbose("buffer too short, waiting"); m_Socket->waitForReadyRead(-1); } - int got = m_Socket->read(buffer, ask); + auto got = m_Socket->read(buffer, ask); read += got; - std::cout << "> ask=" << ask << " got=" << got << " read=" << read - << std::endl; + logVerbose(QString("ask=%1 got=%2 read=%3").arg(ask).arg(got).arg(read)); if (got == -1) { - std::cout << "socket ended, aborting" << std::endl; + logVerbose("socket ended, aborting"); return false; } else if (length - read > 0) { - std::cout << "more remains, seek to " << got << std::endl; + logVerbose(QString("more remains, seek to %1").arg(got)); buffer += got; } } return true; } - -int IpcReader::bytesToInt(const char *buffer, int size) { - if (size == 1) { - return (unsigned char)buffer[0]; - } else if (size == 2) { - return (((unsigned char)buffer[0]) << 8) + (unsigned char)buffer[1]; - } else if (size == 4) { - return (((unsigned char)buffer[0]) << 24) + - (((unsigned char)buffer[1]) << 16) + - (((unsigned char)buffer[2]) << 8) + (unsigned char)buffer[3]; - } else { - return 0; - } -} diff --git a/src/lib/gui/IpcReader.h b/src/lib/gui/ipc/IpcReader.h similarity index 82% rename from src/lib/gui/IpcReader.h rename to src/lib/gui/ipc/IpcReader.h index 1d8a7c366..dd68725d9 100644 --- a/src/lib/gui/IpcReader.h +++ b/src/lib/gui/ipc/IpcReader.h @@ -26,20 +26,20 @@ class IpcReader : public QObject { Q_OBJECT; public: - IpcReader(QTcpSocket *socket); - virtual ~IpcReader(); - void start(); - void stop(); + explicit IpcReader(QTcpSocket *socket); + ~IpcReader() override = default; + void start() const; + void stop() const; signals: - void readLogLine(const QString &text); + void read(const QString &text); + void helloBack(); private: bool readStream(char *buffer, int length); - int bytesToInt(const char *buffer, int size); private slots: - void read(); + void onSocketReadyRead(); private: QTcpSocket *m_Socket; diff --git a/src/lib/gui/QDataStreamProxy.h b/src/lib/gui/ipc/QDataStreamProxy.h similarity index 94% rename from src/lib/gui/QDataStreamProxy.h rename to src/lib/gui/ipc/QDataStreamProxy.h index 5398db4e9..d6af903a5 100644 --- a/src/lib/gui/QDataStreamProxy.h +++ b/src/lib/gui/ipc/QDataStreamProxy.h @@ -28,7 +28,7 @@ public: } virtual ~QDataStreamProxy() = default; - virtual qint64 writeRawData(const char *data, qint64 len) { + virtual qint64 writeRawData(const char *data, int len) { assert(m_Stream); return m_Stream->writeRawData(data, len); } diff --git a/src/lib/gui/ipc/QIpcClient.cpp b/src/lib/gui/ipc/QIpcClient.cpp new file mode 100644 index 000000000..0fc3c905e --- /dev/null +++ b/src/lib/gui/ipc/QIpcClient.cpp @@ -0,0 +1,161 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2012 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "QIpcClient.h" + +#include "IpcReader.h" +#include "byte_utils.h" +#include "global/Ipc.h" + +#include +#include +#include +#include + +const auto kRetryInterval = 1000; +const auto kConnectTimeout = 5000; + +using namespace synergy::gui; + +QIpcClient::QIpcClient(const StreamProvider &streamProvider) + : m_streamProvider(streamProvider) { + + m_pSocket = std::make_unique(); + + if (!m_streamProvider) { + m_streamProvider = [this]() { + return std::make_shared(m_pSocket.get()); + }; + } + + connect( + m_pSocket.get(), &QTcpSocket::connected, this, + &QIpcClient::onSocketConnected); + connect( + m_pSocket.get(), &QTcpSocket::errorOccurred, this, + &QIpcClient::onSocketError); + + m_pReader = std::make_unique(m_pSocket.get()); + connect( + m_pReader.get(), &IpcReader::read, this, // + &QIpcClient::onIpcReaderRead); + connect( + m_pReader.get(), &IpcReader::helloBack, this, + &QIpcClient::onIpcReaderHelloBack); +} + +void QIpcClient::onSocketConnected() const { sendHello(); } + +void QIpcClient::connectToHost() { + m_isConnecting = true; + qInfo("connecting to background service..."); + const auto port = static_cast(kIpcPort); + m_pSocket->connectToHost(QHostAddress(QHostAddress::LocalHost), port); + + if (!m_readerStarted) { + m_pReader->start(); + m_readerStarted = true; + } + + QTimer::singleShot(kConnectTimeout, this, [this]() { + if (!m_isConnected) { + qCritical("ipc connection timeout"); + } + }); +} + +void QIpcClient::disconnectFromHost() { + m_isConnecting = false; + qInfo("disconnected from background service"); + m_pReader->stop(); + m_pSocket->close(); + m_isConnected = false; +} + +void QIpcClient::onSocketError(QAbstractSocket::SocketError socketError) const { + QString text; + switch (socketError) { + case 0: + text = "connection refused"; + break; + case 1: + text = "remote host closed"; + break; + default: + text = QString("code=%1").arg(socketError); + break; + } + + qWarning("ipc connection error, %s", qUtf8Printable(text)); + + QTimer::singleShot(kRetryInterval, this, &QIpcClient::onRetryConnect); +} + +void QIpcClient::onRetryConnect() { + if (m_isConnected) { + qDebug("ipc already connected, skipping retry"); + return; + } else if (!m_isConnecting) { + qDebug("ipc not connecting, skipping retry"); + return; + } + + qInfo("retrying connection to background service..."); + connectToHost(); +} + +void QIpcClient::sendHello() const { + qDebug("sending ipc hello message"); + auto stream = m_streamProvider(); + stream->writeRawData(kIpcMsgHello, 4); + + char typeBuf[1]; + typeBuf[0] = static_cast(IpcClientType::GUI); + stream->writeRawData(typeBuf, 1); +} + +void QIpcClient::sendCommand( + const QString &command, ElevateMode const elevate) const { + qDebug("sending ipc command: %s", qUtf8Printable(command)); + + auto stream = m_streamProvider(); + stream->writeRawData(kIpcMsgCommand, 4); + + std::string stdStringCommand = command.toStdString(); + const char *charCommand = stdStringCommand.c_str(); + auto length = static_cast(stdStringCommand.length()); + + QByteArray lenBuf = intToBytes(length); + if (lenBuf.size() != 4) { + qFatal("unexpected int buffer size: %lld", lenBuf.size()); + } + stream->writeRawData(lenBuf, 4); + stream->writeRawData(charCommand, length); + + char elevateBuf[1]; + // see enum ElevateMode documentation for why this flag is mapped this way + elevateBuf[0] = (elevate == ElevateMode::kAlways) ? 1 : 0; + stream->writeRawData(elevateBuf, 1); +} + +void QIpcClient::onIpcReaderHelloBack() { + qDebug("ipc hello back received"); + m_isConnected = true; + serviceReady(); +} + +void QIpcClient::onIpcReaderRead(const QString &text) { emit read(text); } diff --git a/src/lib/gui/QIpcClient.h b/src/lib/gui/ipc/QIpcClient.h similarity index 55% rename from src/lib/gui/QIpcClient.h rename to src/lib/gui/ipc/QIpcClient.h index 84195c959..cfd0f8c3a 100644 --- a/src/lib/gui/QIpcClient.h +++ b/src/lib/gui/ipc/QIpcClient.h @@ -21,46 +21,41 @@ #include #include #include +#include -#include "ElevateMode.h" +#include "IpcReader.h" #include "QDataStreamProxy.h" +#include "gui/config/ElevateMode.h" +#include "gui/ipc/IQIpcClient.h" class IpcReader; -class QIpcClient : public QObject { +class QIpcClient : public synergy::gui::ipc::IQIpcClient { Q_OBJECT public: using StreamProvider = std::function()>; explicit QIpcClient(const StreamProvider &streamProvider = nullptr); - ~QIpcClient() override; - void sendHello(); - void sendCommand(const QString &command, ElevateMode elevate); - void connectToHost(); - void disconnectFromHost(); - -public slots: - void retryConnect(); - -private: - void intToBytes(int value, char *buffer, int size); + void sendHello() const override; + void sendCommand(const QString &command, ElevateMode elevate) const override; + void connectToHost() override; + void disconnectFromHost() override; + bool isConnected() const override { return m_isConnected; } private slots: - void connected(); - void error(QAbstractSocket::SocketError error); - void handleReadLogLine(const QString &text); - -signals: - void readLogLine(const QString &text); - void infoMessage(const QString &text); - void errorMessage(const QString &text); + void onRetryConnect(); + void onSocketConnected() const; + void onIpcReaderHelloBack(); + void onSocketError(QAbstractSocket::SocketError error) const; + void onIpcReaderRead(const QString &text); private: - QTcpSocket *m_Socket; - IpcReader *m_Reader; - bool m_ReaderStarted; - bool m_Enabled; - StreamProvider m_StreamProvider; + std::unique_ptr m_pSocket; + std::unique_ptr m_pReader; + bool m_readerStarted = false; + StreamProvider m_streamProvider; + bool m_isConnected = false; + bool m_isConnecting = false; }; diff --git a/src/lib/gui/LicenseHandler.cpp b/src/lib/gui/license/LicenseHandler.cpp similarity index 100% rename from src/lib/gui/LicenseHandler.cpp rename to src/lib/gui/license/LicenseHandler.cpp diff --git a/src/lib/gui/LicenseHandler.h b/src/lib/gui/license/LicenseHandler.h similarity index 96% rename from src/lib/gui/LicenseHandler.h rename to src/lib/gui/license/LicenseHandler.h index e6ed2ec28..7c07433c1 100644 --- a/src/lib/gui/LicenseHandler.h +++ b/src/lib/gui/license/LicenseHandler.h @@ -52,6 +52,6 @@ signals: void invalidLicense() const; private: - bool m_enabled = kLicensingEnabled; + bool m_enabled = synergy::gui::kEnableActivation; License m_license = License::invalid(); }; diff --git a/src/lib/gui/license_notices.cpp b/src/lib/gui/license/license_notices.cpp similarity index 99% rename from src/lib/gui/license_notices.cpp rename to src/lib/gui/license/license_notices.cpp index 2aab5f540..c3b127f4f 100644 --- a/src/lib/gui/license_notices.cpp +++ b/src/lib/gui/license/license_notices.cpp @@ -35,6 +35,7 @@ QString licenseNotice(const License &license) { return subscriptionLicenseNotice(license); } else { qFatal("license notice only for time limited licenses"); + return ""; } } diff --git a/src/lib/gui/license_notices.h b/src/lib/gui/license/license_notices.h similarity index 100% rename from src/lib/gui/license_notices.h rename to src/lib/gui/license/license_notices.h diff --git a/src/lib/gui/messages.cpp b/src/lib/gui/messages.cpp index c057a0dfd..0afe54487 100644 --- a/src/lib/gui/messages.cpp +++ b/src/lib/gui/messages.cpp @@ -17,75 +17,100 @@ #include "messages.h" +#include "Logger.h" #include "constants.h" #include "styles.h" +#include #include +#include #include #include +#include namespace synergy::gui::messages { +struct Errors { + static std::unique_ptr s_criticalMessage; + static QStringList s_ignoredErrors; +}; + +std::unique_ptr Errors::s_criticalMessage; +QStringList Errors::s_ignoredErrors; + +void showErrorDialog( + const QString &message, const QMessageLogContext &context, QtMsgType type) { + auto filename = QFileInfo(context.file).fileName(); + auto contextString = QString("%1:%2").arg(filename).arg(context.line); + + auto title = type == QtFatalMsg ? "Fatal error" : "Critical error"; + QString text; + if (type == QtFatalMsg) { + text = "

Sorry, a fatal error has occurred and the application must " + "now exit.

"; + } else { + text = "

Sorry, a critical error has occurred.

"; + } + + if (kLicensedProduct) { + text += QString(R"(

Please contact us)" + " and copy/paste the following error:

") + .arg(kUrlContact, kColorSecondary); + } else { + text += + QString(R"(

Please report a bug)" + " and copy/paste the following error:

") + .arg(kUrlBugReport, kColorSecondary); + } + + text += QString("
%3\n\n%4
").arg(message, contextString); + + if (type == QtFatalMsg) { + // create a blocking message box for fatal errors, as we want to wait + // until the dialog is dismissed before aborting the app. + QMessageBox::critical(nullptr, title, text); + } else if (!Errors::s_ignoredErrors.contains(message)) { + // prevent message boxes piling up by deleting the last one if it exists. + // if none exists yet, then nothing will happen. + Errors::s_criticalMessage.reset(); + + // as we don't abort for critical messages, create a new non-blocking + // message box. this is so that we don't block the message handler; if we + // did, we would prevent new messages from being logged properly. + // the memory will stay allocated until the app exits, which is acceptable. + Errors::s_criticalMessage = std::make_unique( + QMessageBox::Critical, title, text, + QMessageBox::Ok | QMessageBox::Ignore); + Errors::s_criticalMessage->open(); + + QAction::connect( + Errors::s_criticalMessage.get(), &QMessageBox::finished, // + [message](int result) { + if (result == QMessageBox::Ignore) { + Errors::s_ignoredErrors.append(message); + } + }); + } +} + void messageHandler( QtMsgType type, const QMessageLogContext &context, const QString &message) { - auto datetime = QDateTime::currentDateTime().toString("yyyy-MM-ddTHH:mm:ss"); - const auto filename = QString(context.file).split("/").last(); - auto function = context.function ? context.function : ""; + Logger::instance().handleMessage(type, context, message); - QString typeString; - auto out = stdout; - switch (type) { - case QtDebugMsg: - typeString = "DEBUG"; - break; - case QtInfoMsg: - typeString = "INFO"; - break; - case QtWarningMsg: - typeString = "WARNING"; - out = stderr; - break; - case QtCriticalMsg: - typeString = "CRITICAL"; - out = stderr; - break; - case QtFatalMsg: - typeString = "FATAL"; - out = stderr; - break; + if (type == QtFatalMsg || type == QtCriticalMsg) { + showErrorDialog(message, context, type); } - auto logLine = QString("[%1] %2: %3\n\t%4:%5, %6") - .arg(datetime) - .arg(typeString) - .arg(message) - .arg(filename) - .arg(context.line) - .arg(function); - - auto logLineUtf = logLine.toUtf8(); - auto logLine_c = logLineUtf.constData(); - fprintf(out, "%s\n", logLine_c); - if (type == QtFatalMsg) { - auto contextString = - QString("%1:%2\n%3").arg(filename).arg(context.line).arg(function); - - QMessageBox::critical( - nullptr, "Fatal error", - QString("

Sorry, a fatal error has occurred " - "and the application must now exit.

" - "

Please " - R"(contact us)" - " and copy/paste the following error:

" - "
%3\n\n%4
") - .arg(kUrlContact, kColorSecondary, message, contextString)); - - // developers: if you hit this line in your debugger, traverse the stack to - // find the cause of the fatal error. - // important: crash the app on fatal error to prevent the app being used in - // a broken state. + // developers: if you hit this line in your debugger, traverse the stack + // to find the cause of the fatal error. important: crash the app on fatal + // error to prevent the app being used in a broken state. + // + // hint: if you don't want to crash, but still want to show an error + // message, use `qCritical()` instead of `qFatal()`. you should use + // fatal errors when the app is in an unrecoverable state; i.e. it cannot + // function correctly in it's current state and must be restarted. abort(); } } @@ -143,16 +168,16 @@ void showDevThanks(QWidget *parent, const QString &productName) { QMessageBox::information( parent, "Thank you!", - QString( - "

Thanks for using %1.

" - "

If you enjoy using this app, you can support the developers by " - R"(purchasing a license)" - " or " - R"(contributing code.)" - "

" - "

This message will only appear once.

") + QString("

Thanks for using %1.

" + "

If you enjoy using this app, you can support the
" + "developers by " + R"(purchasing a license)" + " or " + R"(contributing code.)" + "

" + "

This message will only appear once.

") .arg( - productName, kUrlPurchase, kColorSecondary, kUrlContribute, + productName, kUrlPurchase, kColorSecondary, kUrlGitHub, kColorSecondary)); } diff --git a/src/lib/gui/proxy/QProcessProxy.cpp b/src/lib/gui/proxy/QProcessProxy.cpp new file mode 100644 index 000000000..339e6055d --- /dev/null +++ b/src/lib/gui/proxy/QProcessProxy.cpp @@ -0,0 +1,63 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "QProcessProxy.h" + +namespace synergy::gui::proxy { + +void QProcessProxy::create() { + m_pProcess = std::make_unique(); + + connect( + m_pProcess.get(), &QProcess::finished, this, + [this](int exitCode, QProcess::ExitStatus exitStatus) { + emit finished(exitCode, exitStatus); + }); + + connect( + m_pProcess.get(), &QProcess::readyReadStandardOutput, // + this, [this]() { emit readyReadStandardOutput(); }); + + connect( + m_pProcess.get(), &QProcess::readyReadStandardError, // + this, [this]() { 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 synergy::gui::proxy diff --git a/src/lib/gui/proxy/QProcessProxy.h b/src/lib/gui/proxy/QProcessProxy.h new file mode 100644 index 000000000..cb556b25b --- /dev/null +++ b/src/lib/gui/proxy/QProcessProxy.h @@ -0,0 +1,48 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +namespace synergy::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(); + +signals: + void finished(int exitCode, QProcess::ExitStatus exitStatus); + void readyReadStandardOutput(); + void readyReadStandardError(); + +private: + std::unique_ptr m_pProcess; +}; + +} // namespace synergy::gui::proxy diff --git a/src/lib/gui/string_utils.h b/src/lib/gui/string_utils.h new file mode 100644 index 000000000..1a9ca96f4 --- /dev/null +++ b/src/lib/gui/string_utils.h @@ -0,0 +1,27 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Symless + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +/** + * @brief Useful for environment variables that have string boolean values. + */ +inline bool strToTrue(const QString &str) { + return str.toLower() == "true" || str == "1"; +} diff --git a/src/lib/gui/styles.h b/src/lib/gui/styles.h index 5e63d457d..7bd5e9339 100644 --- a/src/lib/gui/styles.h +++ b/src/lib/gui/styles.h @@ -19,6 +19,8 @@ #include +namespace synergy::gui { + const auto kColorWhite = "#ffffff"; const auto kColorPrimary = "#ff7c00"; const auto kColorSecondary = "#4285f4"; @@ -47,3 +49,5 @@ const auto kStyleNoticeLabel = // QString("padding: 3px 5px; border-radius: 3px;" "background-color: %1; color: %2") .arg(kColorNotice, kColorWhite); + +} // namespace synergy::gui diff --git a/src/lib/gui/TlsCertificate.cpp b/src/lib/gui/tls/TlsCertificate.cpp similarity index 57% rename from src/lib/gui/TlsCertificate.cpp rename to src/lib/gui/tls/TlsCertificate.cpp index ac67f7282..a9267c471 100644 --- a/src/lib/gui/TlsCertificate.cpp +++ b/src/lib/gui/tls/TlsCertificate.cpp @@ -22,13 +22,10 @@ #include #include #include +#include -// RSA Bit length (e.g. 1024/2048/4096) static const char *const kCertificateKeyLength = "rsa:"; - -// fingerprint hashing algorithm static const char *const kCertificateHashAlgorithm = "-sha256"; - static const char *const kCertificateLifetime = "365"; static const char *const kCertificateSubjectInfo = "/CN=Synergy"; static const char *const kCertificateFilename = "Synergy.pem"; @@ -42,9 +39,10 @@ static const char *const kConfigFile = "synergy.conf"; static const char *const kUnixOpenSslCommand = "openssl"; #endif -namespace synergy::gui { #if defined(Q_OS_WIN) +namespace synergy::gui { + QString openSslWindowsDir() { auto appDir = QDir(QCoreApplication::applicationDirPath()); @@ -83,16 +81,16 @@ QString openSslWindowsBinary() { return path; } -#endif - } // namespace synergy::gui using namespace synergy::gui; +#endif + TlsCertificate::TlsCertificate(QObject *parent) : QObject(parent) { - m_ProfileDir = m_CoreInterface.getProfileDir(); - if (m_ProfileDir.isEmpty()) { - emit error(tr("Failed to get profile directory.")); + m_profileDir = m_coreTool.getProfileDir(); + if (m_profileDir.isEmpty()) { + qCritical("empty profile directory result"); } } @@ -108,92 +106,118 @@ bool TlsCertificate::runTool(const QStringList &args) { #if defined(Q_OS_WIN) auto openSslDir = QDir(openSslWindowsDir()); auto config = QDir::cleanPath(openSslDir.filePath(kConfigFile)); + if (!QFile::exists(config)) { + qDebug("openssl config file not found: %s", qUtf8Printable(config)); + + // if the expected production file location doesn't exist, try the dev path. + config = QDir::cleanPath(QString("res/openssl/%1").arg(kConfigFile)); + + // if it still isn't there, then there's something seriously wrong. + if (!QFile::exists(config)) { + qFatal() << "openssl config file not found: " << config; + } + } + environment << QString("OPENSSL_CONF=%1").arg(config); #endif + qDebug( + "running: %s %s", qUtf8Printable(program), + qUtf8Printable(args.join(" "))); + QProcess process; + + for (const auto &envVar : environment) { + qDebug("setting env var %s", qUtf8Printable(envVar)); + } + process.setEnvironment(environment); + process.start(program, args); bool success = process.waitForStarted(); - QString standardError; + QString stderrOutput; if (success && process.waitForFinished()) { - m_ToolOutput = process.readAllStandardOutput().trimmed(); - standardError = process.readAllStandardError().trimmed(); + m_toolStdout = process.readAllStandardOutput().trimmed(); + stderrOutput = process.readAllStandardError().trimmed(); } if (int code = process.exitCode(); !success || code != 0) { - emit error(QString("SSL tool failed: %1\n\nCode: %2\nError: %3") - .arg(program) - .arg(process.exitCode()) - .arg(standardError.isEmpty() ? "Unknown" : standardError)); + qDebug( + "openssl failed with code %d: %s", code, qUtf8Printable(stderrOutput)); + + qCritical( + "failed to generate TLS certificate:\n\n%s", + qUtf8Printable(stderrOutput)); return false; } return true; } -void TlsCertificate::generateCertificate( - const QString &path, const QString &keyLength, bool forceGen) { +bool TlsCertificate::generateCertificate(const QString &path, int keyLength) { QString sslDirPath = - QString("%1%2%3").arg(m_ProfileDir).arg(QDir::separator()).arg(kSslDir); + QString("%1%2%3").arg(m_profileDir).arg(QDir::separator()).arg(kSslDir); - QString filename = QString("%1%2%3") - .arg(sslDirPath) - .arg(QDir::separator()) - .arg(kCertificateFilename); + QString defaultPath = QString("%1%2%3") + .arg(sslDirPath) + .arg(QDir::separator()) + .arg(kCertificateFilename); - QString keySize = kCertificateKeyLength + keyLength; + QString keySize = kCertificateKeyLength + QString::number(keyLength); - const QString pathToUse = path.isEmpty() ? filename : path; + const QString pathToUse = + QDir::cleanPath(path.isEmpty() ? defaultPath : path); - if (QFile file(pathToUse); !file.exists() || forceGen) { - QStringList arguments; + qDebug("generating tls certificate: %s", qUtf8Printable(pathToUse)); - // self signed certificate - arguments.append("req"); - arguments.append("-x509"); - arguments.append("-nodes"); + QStringList arguments; - // valide duration - arguments.append("-days"); - arguments.append(kCertificateLifetime); + // self signed certificate + arguments.append("req"); + arguments.append("-x509"); + arguments.append("-nodes"); - // subject information - arguments.append("-subj"); + // valide duration + arguments.append("-days"); + arguments.append(kCertificateLifetime); - QString subInfo(kCertificateSubjectInfo); - arguments.append(subInfo); + // subject information + arguments.append("-subj"); - // private key - arguments.append("-newkey"); - arguments.append(keySize); + QString subInfo(kCertificateSubjectInfo); + arguments.append(subInfo); - if (QDir sslDir(sslDirPath); !sslDir.exists()) { - sslDir.mkpath("."); - } + // private key + arguments.append("-newkey"); + arguments.append(keySize); - // key output filename - arguments.append("-keyout"); - arguments.append(pathToUse); - - // certificate output filename - arguments.append("-out"); - arguments.append(pathToUse); - - if (!runTool(arguments)) { - return; - } - - generateFingerprint(pathToUse); - emit info(tr("SSL certificate generated.")); + if (QDir sslDir(sslDirPath); !sslDir.exists()) { + sslDir.mkpath("."); } - emit generateFinished(); + // key output filename + arguments.append("-keyout"); + arguments.append(pathToUse); + + // certificate output filename + arguments.append("-out"); + arguments.append(pathToUse); + + if (runTool(arguments)) { + qDebug("tls certificate generated"); + + return generateFingerprint(pathToUse); + } else { + qCritical("failed to generate tls certificate"); + return false; + } } -void TlsCertificate::generateFingerprint(const QString &certificateFilename) { +bool TlsCertificate::generateFingerprint(const QString &certificateFilename) { + qDebug("generating tls fingerprint"); + QStringList arguments; arguments.append("x509"); arguments.append("-fingerprint"); @@ -203,23 +227,26 @@ void TlsCertificate::generateFingerprint(const QString &certificateFilename) { arguments.append(certificateFilename); if (!runTool(arguments)) { - return; + qCritical("failed to generate tls fingerprint"); + return false; } // find the fingerprint from the tool output - auto i = m_ToolOutput.indexOf("="); + auto i = m_toolStdout.indexOf("="); if (i != -1) { i++; - QString fingerprint = m_ToolOutput.mid(i, m_ToolOutput.size() - i); + QString fingerprint = m_toolStdout.mid(i, m_toolStdout.size() - i); TlsFingerprint::local().trust(fingerprint, false); - emit info(tr("SSL fingerprint generated.")); + qDebug("tls fingerprint generated"); + return true; } else { - emit error(tr("Failed to find SSL fingerprint.")); + qCritical("failed to find tls fingerprint in tls tool output"); + return false; } } -QString TlsCertificate::getCertKeyLength(const QString &path) { +int TlsCertificate::getCertKeyLength(const QString &path) { QStringList arguments; arguments.append("rsa"); @@ -229,17 +256,19 @@ QString TlsCertificate::getCertKeyLength(const QString &path) { arguments.append("-noout"); if (!runTool(arguments)) { - return QString(); + qFatal("failed to get key length from certificate"); + return 0; } + const QString searchStart("Private-Key: ("); const QString searchEnd(" bit"); // Get the line that contains the key length from the output - const auto indexStart = m_ToolOutput.indexOf(searchStart); - const auto indexEnd = m_ToolOutput.indexOf(searchEnd, indexStart); + const auto indexStart = m_toolStdout.indexOf(searchStart); + const auto indexEnd = m_toolStdout.indexOf(searchEnd, indexStart); const auto start = indexStart + searchStart.length(); const auto end = indexEnd - (indexStart + searchStart.length()); - auto keyLength = m_ToolOutput.mid(start, end); + auto keyLength = m_toolStdout.mid(start, end); - return keyLength; + return keyLength.toInt(); } diff --git a/src/lib/gui/tls/TlsCertificate.h b/src/lib/gui/tls/TlsCertificate.h new file mode 100644 index 000000000..dbb5a858e --- /dev/null +++ b/src/lib/gui/tls/TlsCertificate.h @@ -0,0 +1,41 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2015 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "gui/core/CoreTool.h" + +#include + +class TlsCertificate : public QObject { + Q_OBJECT + +public: + explicit TlsCertificate(QObject *parent = nullptr); + + bool generateCertificate(const QString &path, int keyLength); + int getCertKeyLength(const QString &path); + +private: + bool runTool(const QStringList &args); + bool generateFingerprint(const QString &certificateFilename); + +private: + QString m_profileDir; + QString m_toolStdout; + [[no_unique_address]] CoreTool m_coreTool; +}; diff --git a/src/lib/gui/TlsFingerprint.cpp b/src/lib/gui/tls/TlsFingerprint.cpp similarity index 88% rename from src/lib/gui/TlsFingerprint.cpp rename to src/lib/gui/tls/TlsFingerprint.cpp index 1d19ea3a4..19ce8c603 100644 --- a/src/lib/gui/TlsFingerprint.cpp +++ b/src/lib/gui/tls/TlsFingerprint.cpp @@ -17,7 +17,7 @@ #include "TlsFingerprint.h" -#include "gui/CoreInterface.h" +#include "gui/core/CoreTool.h" #include #include @@ -27,11 +27,10 @@ static const char kLocalFilename[] = "Local.txt"; static const char kTrustedServersFilename[] = "TrustedServers.txt"; static const char kTrustedClientsFilename[] = "TrustedClients.txt"; -TlsFingerprint::TlsFingerprint(const QString &filename) { - m_Filename = filename; -} +TlsFingerprint::TlsFingerprint(const QString &filename) + : m_Filename(filename) {} -void TlsFingerprint::trust(const QString &fingerprintText, bool append) { +void TlsFingerprint::trust(const QString &fingerprintText, bool append) const { TlsFingerprint::persistDirectory(); QIODevice::OpenMode openMode; @@ -59,7 +58,7 @@ bool TlsFingerprint::fileExists() const { return file.exists(); } -bool TlsFingerprint::isTrusted(const QString &fingerprintText) { +bool TlsFingerprint::isTrusted(const QString &fingerprintText) const { QStringList list = readList(); foreach (QString trusted, list) { if (trusted == fingerprintText) { @@ -69,7 +68,7 @@ bool TlsFingerprint::isTrusted(const QString &fingerprintText) { return false; } -QStringList TlsFingerprint::readList(const int readTo) { +QStringList TlsFingerprint::readList(const int readTo) const { QStringList list; QString dirName = TlsFingerprint::directoryPath(); @@ -93,7 +92,7 @@ QStringList TlsFingerprint::readList(const int readTo) { return list; } -QString TlsFingerprint::readFirst() { +QString TlsFingerprint::readFirst() const { QStringList list = readList(1); return list.at(0); } @@ -111,8 +110,8 @@ void TlsFingerprint::persistDirectory() { } QString TlsFingerprint::directoryPath() { - CoreInterface coreInterface; - QString profileDir = coreInterface.getProfileDir(); + CoreTool coreTool; + QString profileDir = coreTool.getProfileDir(); return QString("%1/%2").arg(profileDir).arg(kDirName); } diff --git a/src/lib/gui/TlsFingerprint.h b/src/lib/gui/tls/TlsFingerprint.h similarity index 85% rename from src/lib/gui/TlsFingerprint.h rename to src/lib/gui/tls/TlsFingerprint.h index a4f1a4275..7123b2fc4 100644 --- a/src/lib/gui/TlsFingerprint.h +++ b/src/lib/gui/tls/TlsFingerprint.h @@ -21,15 +21,7 @@ class TlsFingerprint { private: - TlsFingerprint(const QString &filename); - -public: - void trust(const QString &fingerprintText, bool append = true); - bool isTrusted(const QString &fingerprintText); - QStringList readList(const int readTo = -1); - QString readFirst(); - QString filePath() const; - bool fileExists() const; + explicit TlsFingerprint(const QString &filename); public: static TlsFingerprint local(); @@ -40,6 +32,13 @@ public: static bool localFingerprintExists(); static void persistDirectory(); + void trust(const QString &fingerprintText, bool append = true) const; + bool isTrusted(const QString &fingerprintText) const; + QStringList readList(const int readTo = -1) const; + QString readFirst() const; + QString filePath() const; + bool fileExists() const; + private: QString m_Filename; }; diff --git a/src/lib/gui/TlsUtility.cpp b/src/lib/gui/tls/TlsUtility.cpp similarity index 72% rename from src/lib/gui/TlsUtility.cpp rename to src/lib/gui/tls/TlsUtility.cpp index fe17207e2..a30526dfc 100644 --- a/src/lib/gui/TlsUtility.cpp +++ b/src/lib/gui/tls/TlsUtility.cpp @@ -20,10 +20,17 @@ #include "TlsCertificate.h" #include "constants.h" +#include + namespace synergy::gui { +TlsUtility::TlsUtility( + const IAppConfig &appConfig, const license::License &license) + : m_appConfig(appConfig), + m_license(license) {} + bool TlsUtility::isAvailable() const { - return !kLicensingEnabled || m_license.isTlsAvailable(); + return !kEnableActivation || m_license.isTlsAvailable(); } bool TlsUtility::isAvailableAndEnabled() const { @@ -31,20 +38,20 @@ bool TlsUtility::isAvailableAndEnabled() const { return isAvailable() && config.tlsEnabled(); } -void TlsUtility::generateCertificate(bool replace) const { +bool TlsUtility::generateCertificate() { qDebug("generating tls certificate, " "all clients must trust the new fingerprint"); if (!isAvailableAndEnabled()) { - qFatal("unable to generate tls certificate, " - "tls is either not available or not enabled"); + qCritical("unable to generate tls certificate, " + "tls is either not available or not enabled"); + return false; } auto path = m_appConfig.tlsCertPath(); auto length = m_appConfig.tlsKeyLength(); - TlsCertificate cert; - cert.generateCertificate(path, length, replace); + return m_certificate.generateCertificate(path, length); } } // namespace synergy::gui diff --git a/src/lib/gui/TlsUtility.h b/src/lib/gui/tls/TlsUtility.h similarity index 85% rename from src/lib/gui/TlsUtility.h rename to src/lib/gui/tls/TlsUtility.h index aa6d6e83e..4c0ecb819 100644 --- a/src/lib/gui/TlsUtility.h +++ b/src/lib/gui/tls/TlsUtility.h @@ -17,18 +17,23 @@ #pragma once -#include "IAppConfig.h" +#include "gui/config/IAppConfig.h" +#include "TlsCertificate.h" #include "license/License.h" +#include + namespace synergy::gui { -class TlsUtility { +class TlsUtility : public QObject { + Q_OBJECT + public: explicit TlsUtility( - const IAppConfig &appConfig, const license::License &license) - : m_appConfig(appConfig), - m_license(license) {} + const IAppConfig &appConfig, const license::License &license); + + bool generateCertificate(); /** * @brief Combines the availability and the enabled status of TLS. @@ -44,14 +49,10 @@ public: */ bool isAvailable() const; - /** - * @param replace Replace certificate on disk (default: false). - */ - void generateCertificate(bool replace = false) const; - private: const IAppConfig &m_appConfig; const license::License &m_license; + TlsCertificate m_certificate; }; } // namespace synergy::gui diff --git a/src/lib/synergy/Screen.cpp b/src/lib/synergy/Screen.cpp index af9869350..027306d32 100644 --- a/src/lib/synergy/Screen.cpp +++ b/src/lib/synergy/Screen.cpp @@ -56,7 +56,11 @@ Screen::~Screen() { disable(); } assert(!m_enabled); + + // TODO: why assert this? it appears to be false when an elevated dialog + // appears on windows and the process is killed. assert(m_entered == m_isPrimary); + delete m_screen; LOG((CLOG_DEBUG "closed display")); } diff --git a/src/test/integtests/gui/CommandProcessTests.cpp b/src/test/integtests/gui/core/CommandProcessTests.cpp similarity index 73% rename from src/test/integtests/gui/CommandProcessTests.cpp rename to src/test/integtests/gui/core/CommandProcessTests.cpp index edfe95346..4640f8080 100644 --- a/src/test/integtests/gui/CommandProcessTests.cpp +++ b/src/test/integtests/gui/core/CommandProcessTests.cpp @@ -15,14 +15,23 @@ * along with this program. If not, see . */ -#include "gui/CommandProcess.h" +#include "gui/core/CommandProcess.h" #include TEST(CommandProcessTests, run_commandSucceeds_returnsOutput) { + + // it seems that on windows, you can sometimes just call echo (this worked + // with windows 2022), but on windows 10 you can't call echo directly. +#if defined(Q_OS_WIN) + const QString command = "cmd"; + const QStringList arguments = {"/C", "echo Hello, World!"}; + const QString input = ""; +#elif defined(Q_OS_UNIX) const QString command = "echo"; const QStringList arguments = {"Hello, World!"}; const QString input = ""; +#endif // Q_OS_WIN CommandProcess commandProcess(command, arguments, input); diff --git a/src/test/integtests/gui/CoreInterfaceTests.cpp b/src/test/integtests/gui/core/CoreToolTests.cpp similarity index 57% rename from src/test/integtests/gui/CoreInterfaceTests.cpp rename to src/test/integtests/gui/core/CoreToolTests.cpp index 55a62792b..90e98b356 100644 --- a/src/test/integtests/gui/CoreInterfaceTests.cpp +++ b/src/test/integtests/gui/core/CoreToolTests.cpp @@ -15,41 +15,41 @@ * along with this program. If not, see . */ -#include "gui/CoreInterface.h" +#include "gui/core/CoreTool.h" #include "shared/gui/QtCoreTest.h" #include -class CoreInterfaceTests : public QtCoreTest {}; +class CoreToolTests : public QtCoreTest {}; -TEST_F(CoreInterfaceTests, getProfileDir_noMock_returnsNonEmpty) { - CoreInterface coreInterface; +TEST_F(CoreToolTests, getProfileDir_noMock_returnsNonEmpty) { + CoreTool coreTool; - QString profileDir = coreInterface.getProfileDir(); + QString profileDir = coreTool.getProfileDir(); EXPECT_FALSE(profileDir.isEmpty()); } -TEST_F(CoreInterfaceTests, getInstalledDir_noMock_returnsNonEmpty) { - CoreInterface coreInterface; +TEST_F(CoreToolTests, getInstalledDir_noMock_returnsNonEmpty) { + CoreTool coreTool; - QString installedDir = coreInterface.getInstalledDir(); + QString installedDir = coreTool.getInstalledDir(); EXPECT_FALSE(installedDir.isEmpty()); } -TEST_F(CoreInterfaceTests, getArch_noMock_returnsNonEmpty) { - CoreInterface coreInterface; +TEST_F(CoreToolTests, getArch_noMock_returnsNonEmpty) { + CoreTool coreTool; - QString arch = coreInterface.getArch(); + QString arch = coreTool.getArch(); EXPECT_FALSE(arch.isEmpty()); } -TEST_F(CoreInterfaceTests, getSerialKeyFilePath_noMock_returnsNonEmpty) { - CoreInterface coreInterface; +TEST_F(CoreToolTests, getSerialKeyFilePath_noMock_returnsNonEmpty) { + CoreTool coreTool; - QString serialKeyFilePath = coreInterface.getSerialKeyFilePath(); + QString serialKeyFilePath = coreTool.getSerialKeyFilePath(); EXPECT_FALSE(serialKeyFilePath.isEmpty()); } diff --git a/src/test/shared/TestEventQueue.cpp b/src/test/shared/TestEventQueue.cpp index 2915d2323..bba5940a7 100644 --- a/src/test/shared/TestEventQueue.cpp +++ b/src/test/shared/TestEventQueue.cpp @@ -33,7 +33,7 @@ void TestEventQueue::initQuitTimeout(double timeout) { void TestEventQueue::cleanupQuitTimeout() { removeHandler(Event::kTimer, m_pQuitTimeoutTimer); - delete m_pQuitTimeoutTimer; + deleteTimer(m_pQuitTimeoutTimer); m_pQuitTimeoutTimer = nullptr; } diff --git a/src/test/unittests/gui/byte_utils_tests.cpp b/src/test/unittests/gui/byte_utils_tests.cpp new file mode 100644 index 000000000..1e97dc137 --- /dev/null +++ b/src/test/unittests/gui/byte_utils_tests.cpp @@ -0,0 +1,40 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "gui/byte_utils.h" + +#include + +using namespace synergy::gui; + +TEST(byte_utils_tests, bytesToInt_size4) { + char buffer[4] = {0x01, 0x02, 0x03, 0x04}; + + const auto i = bytesToInt(buffer, 4); + + EXPECT_EQ(i, 0x01020304); +} + +TEST(byte_utils_tests, intToBytes_size4) { + QByteArray bytes = intToBytes(0x01020304); + + EXPECT_EQ(bytes.size(), 4); + EXPECT_EQ(bytes[0], 0x01); + EXPECT_EQ(bytes[1], 0x02); + EXPECT_EQ(bytes[2], 0x03); + EXPECT_EQ(bytes[3], 0x04); +} diff --git a/src/test/unittests/gui/AppConfigTests.cpp b/src/test/unittests/gui/config/AppConfigTests.cpp similarity index 98% rename from src/test/unittests/gui/AppConfigTests.cpp rename to src/test/unittests/gui/config/AppConfigTests.cpp index d185f75f7..ddad213e0 100644 --- a/src/test/unittests/gui/AppConfigTests.cpp +++ b/src/test/unittests/gui/config/AppConfigTests.cpp @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -#include "gui/AppConfig.h" +#include "gui/config/AppConfig.h" #include "gmock/gmock.h" #include diff --git a/src/test/unittests/gui/core/CoreProcessTests.cpp b/src/test/unittests/gui/core/CoreProcessTests.cpp new file mode 100644 index 000000000..73ab3aa91 --- /dev/null +++ b/src/test/unittests/gui/core/CoreProcessTests.cpp @@ -0,0 +1,231 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Symless Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "gui/config/IAppConfig.h" +#include "gui/core/CoreProcess.h" +#include "gui/ipc/IQIpcClient.h" +#include "gui/proxy/QProcessProxy.h" + +#include "gmock/gmock.h" +#include +#include + +using namespace synergy::gui; +using ::testing::_; +using ::testing::NiceMock; +using ::testing::Return; +using ::testing::ReturnRef; + +class MockAppConfig : public IAppConfig { +public: + MockAppConfig() { + ON_CALL(*this, screenName()).WillByDefault(ReturnRef(m_stubName)); + ON_CALL(*this, networkInterface()) + .WillByDefault(ReturnRef(m_stubInterface)); + ON_CALL(*this, logLevelText()).WillByDefault(Return("stub log level")); + } + + MOCK_METHOD(QString, tlsCertPath, (), (const, override)); + MOCK_METHOD(int, tlsKeyLength, (), (const, override)); + MOCK_METHOD(bool, tlsEnabled, (), (const, override)); + MOCK_METHOD(ProcessMode, processMode, (), (const, override)); + MOCK_METHOD(ElevateMode, elevateMode, (), (const, override)); + MOCK_METHOD(QString, logLevelText, (), (const, override)); + MOCK_METHOD(const QString &, screenName, (), (const, override)); + MOCK_METHOD(bool, preventSleep, (), (const, override)); + MOCK_METHOD(bool, logToFile, (), (const, override)); + MOCK_METHOD(const QString &, logFilename, (), (const, override)); + MOCK_METHOD(QString, coreServerName, (), (const, override)); + MOCK_METHOD(QString, coreClientName, (), (const, override)); + MOCK_METHOD(bool, invertConnection, (), (const, override)); + MOCK_METHOD(void, persistLogDir, (), (const, override)); + MOCK_METHOD(QString, serialKey, (), (const, override)); + MOCK_METHOD(bool, languageSync, (), (const, override)); + MOCK_METHOD(bool, invertScrollDirection, (), (const, override)); + MOCK_METHOD(int, port, (), (const, override)); + MOCK_METHOD(bool, useExternalConfig, (), (const, override)); + MOCK_METHOD(const QString &, configFile, (), (const, override)); + MOCK_METHOD(const QString &, networkInterface, (), (const, override)); + +private: + const QString m_stubName = "stub name"; + const QString m_stubInterface = "stub interface"; + const QString m_stubAddress = "stub address"; +}; + +class MockServerConfig : public IServerConfig { +public: + MOCK_METHOD(bool, isFull, (), (const, override)); + MOCK_METHOD( + bool, screenExists, (const QString &screenName), (const, override)); + MOCK_METHOD(bool, save, (const QString &fileName), (const, override)); + MOCK_METHOD(void, save, (QFile & file), (const, override)); + MOCK_METHOD(bool, enableDragAndDrop, (), (const, override)); +}; + +class MockQProcessProxy : public proxy::QProcessProxy { +public: + operator bool() const override { return toBool(); } + + MockQProcessProxy() { + 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 MockQIpcClient : public ipc::IQIpcClient { +public: + MockQIpcClient() { + ON_CALL(*this, isConnected()).WillByDefault(Return(true)); + } + + MOCK_METHOD(void, sendHello, (), (const, override)); + MOCK_METHOD( + void, sendCommand, (const QString &command, ElevateMode elevate), + (const, override)); + MOCK_METHOD(void, connectToHost, (), (override)); + MOCK_METHOD(void, disconnectFromHost, (), (override)); + MOCK_METHOD(bool, isConnected, (), (const, override)); +}; + +class MockDeps : public CoreProcess::Deps { +public: + MockDeps() { + ON_CALL(*this, process()).WillByDefault(ReturnRef(m_process)); + ON_CALL(*this, ipcClient()).WillByDefault(ReturnRef(m_ipcClient)); + ON_CALL(*this, appPath(_)).WillByDefault(Return("stub app path")); + ON_CALL(*this, fileExists(_)).WillByDefault(Return(true)); + ON_CALL(*this, getProfileRoot()).WillByDefault(Return("stub profile")); + } + + MOCK_METHOD(proxy::QProcessProxy &, process, (), (override)); + MOCK_METHOD(ipc::IQIpcClient &, ipcClient, (), (override)); + MOCK_METHOD(QString, appPath, (const QString &name), (const, override)); + MOCK_METHOD(bool, fileExists, (const QString &path), (const, override)); + MOCK_METHOD(QString, getProfileRoot, (), (const, override)); + + NiceMock m_process; + NiceMock m_ipcClient; +}; + +class CoreProcessTests : public ::testing::Test { +public: + CoreProcessTests() : m_coreProcess(m_appConfig, m_serverConfig, m_pDeps) {} + + NiceMock m_appConfig; + NiceMock m_serverConfig; + std::shared_ptr> m_pDeps = + std::make_shared>(); + CoreProcess m_coreProcess; +}; + +TEST_F(CoreProcessTests, start_serverDesktop_callsProcessStart) { + m_coreProcess.setMode(CoreProcess::Mode::Server); + + EXPECT_CALL(m_pDeps->m_process, start(_, _)).Times(1); + + m_coreProcess.start(ProcessMode::kDesktop); +} + +TEST_F(CoreProcessTests, start_serverService_callsSendCommand) { + m_coreProcess.setMode(CoreProcess::Mode::Server); + + EXPECT_CALL(m_pDeps->m_ipcClient, sendCommand(_, _)).Times(1); + + m_coreProcess.start(ProcessMode::kService); +} + +TEST_F(CoreProcessTests, start_clientDesktop_callsProcessStart) { + m_coreProcess.setMode(CoreProcess::Mode::Client); + m_coreProcess.setAddress("stub address"); + + EXPECT_CALL(m_pDeps->m_process, start(_, _)).Times(1); + + m_coreProcess.start(ProcessMode::kDesktop); +} + +TEST_F(CoreProcessTests, start_clientService_callsSendCommand) { + m_coreProcess.setMode(CoreProcess::Mode::Client); + m_coreProcess.setAddress("stub address"); + + EXPECT_CALL(m_pDeps->m_ipcClient, sendCommand(_, _)).Times(1); + + m_coreProcess.start(ProcessMode::kService); +} + +TEST_F(CoreProcessTests, stop_serverDesktop_callsProcessClose) { + m_coreProcess.setMode(CoreProcess::Mode::Server); + m_coreProcess.start(); + + EXPECT_CALL(m_pDeps->m_process, close()).Times(1); + + m_coreProcess.stop(ProcessMode::kDesktop); +} + +TEST_F(CoreProcessTests, stop_serverService_callsSendCommand) { + m_coreProcess.setMode(CoreProcess::Mode::Server); + m_coreProcess.start(); + + EXPECT_CALL(m_pDeps->m_ipcClient, sendCommand(_, _)).Times(1); + + m_coreProcess.stop(ProcessMode::kService); +} + +TEST_F(CoreProcessTests, stop_clientDesktop_callsProcessClose) { + m_coreProcess.setMode(CoreProcess::Mode::Client); + m_coreProcess.setAddress("stub address"); + m_coreProcess.start(); + + EXPECT_CALL(m_pDeps->m_process, close()).Times(1); + + m_coreProcess.stop(ProcessMode::kDesktop); +} + +TEST_F(CoreProcessTests, stop_clientService_callsSendCommand) { + m_coreProcess.setMode(CoreProcess::Mode::Client); + m_coreProcess.setAddress("stub address"); + m_coreProcess.start(); + + EXPECT_CALL(m_pDeps->m_ipcClient, sendCommand(_, _)).Times(1); + + m_coreProcess.stop(ProcessMode::kService); +} + +TEST_F(CoreProcessTests, restart_serverDesktop_callsProcessStart) { + ON_CALL(m_appConfig, processMode()) + .WillByDefault(Return(ProcessMode::kDesktop)); + m_coreProcess.setMode(CoreProcess::Mode::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(); +} diff --git a/src/test/unittests/gui/QIpcClientTests.cpp b/src/test/unittests/gui/ipc/QIpcClientTests.cpp similarity index 88% rename from src/test/unittests/gui/QIpcClientTests.cpp rename to src/test/unittests/gui/ipc/QIpcClientTests.cpp index 0dccf9e34..8a66d879a 100644 --- a/src/test/unittests/gui/QIpcClientTests.cpp +++ b/src/test/unittests/gui/ipc/QIpcClientTests.cpp @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -#include "gui/QIpcClient.h" +#include "gui/ipc/QIpcClient.h" #include #include @@ -25,7 +25,7 @@ using testing::StrEq; class MockStream : public QDataStreamProxy { public: - MOCK_METHOD(qint64, writeRawData, (const char *, qint64), (override)); + MOCK_METHOD(qint64, writeRawData, (const char *, int), (override)); }; TEST(QIpcClientTests, sendCommand_anyCommand_commandSent) { @@ -38,5 +38,5 @@ TEST(QIpcClientTests, sendCommand_anyCommand_commandSent) { EXPECT_CALL(*mockStream, writeRawData(StrEq("test"), 4)).Times(1); QIpcClient ipcClient(streamProvider); - ipcClient.sendCommand("test", ElevateMode::ElevateAsNeeded); + ipcClient.sendCommand("test", ElevateMode::kAutomatic); } diff --git a/src/test/unittests/gui/LicenseHandlerTests.cpp b/src/test/unittests/gui/license/LicenseHandlerTests.cpp similarity index 97% rename from src/test/unittests/gui/LicenseHandlerTests.cpp rename to src/test/unittests/gui/license/LicenseHandlerTests.cpp index c926c01b6..f2ac5a0a8 100644 --- a/src/test/unittests/gui/LicenseHandlerTests.cpp +++ b/src/test/unittests/gui/license/LicenseHandlerTests.cpp @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -#include "gui/LicenseHandler.h" +#include "gui/license/LicenseHandler.h" #include #include diff --git a/src/test/unittests/gui/license_notices_tests.cpp b/src/test/unittests/gui/license/license_notices_tests.cpp similarity index 99% rename from src/test/unittests/gui/license_notices_tests.cpp rename to src/test/unittests/gui/license/license_notices_tests.cpp index 0b701990d..25242c7d7 100644 --- a/src/test/unittests/gui/license_notices_tests.cpp +++ b/src/test/unittests/gui/license/license_notices_tests.cpp @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -#include "gui/license_notices.h" +#include "gui/license/license_notices.h" #include #include