From d99fadd1f16aa2f8507c27aa33c03298e471e53d Mon Sep 17 00:00:00 2001 From: Martchus Date: Fri, 19 Apr 2024 00:04:48 +0200 Subject: [PATCH] Allow stopping the systemd service automatically on metered connections This is the same as the feature already present in the launcher and connection settings. Not sure how much sense it makes for systemd services as they run rather independently from the UI but maybe it is useful. --- syncthingconnector/syncthingconnection.cpp | 3 +- syncthingconnector/syncthingconnection.h | 6 +- syncthingconnector/syncthingservice.cpp | 71 +++++++++++++++++++ syncthingconnector/syncthingservice.h | 26 +++++++ syncthingconnector/utils.cpp | 36 ++++++++++ syncthingconnector/utils.h | 8 +++ syncthingwidgets/misc/syncthinglauncher.cpp | 19 +---- syncthingwidgets/settings/settings.cpp | 3 + syncthingwidgets/settings/settings.h | 1 + syncthingwidgets/settings/settingsdialog.cpp | 8 +++ .../settings/systemdoptionpage.ui | 7 ++ 11 files changed, 165 insertions(+), 23 deletions(-) diff --git a/syncthingconnector/syncthingconnection.cpp b/syncthingconnector/syncthingconnection.cpp index d8a5bb4..701132e 100644 --- a/syncthingconnector/syncthingconnection.cpp +++ b/syncthingconnector/syncthingconnection.cpp @@ -267,8 +267,7 @@ void SyncthingConnection::setPausingOnMeteredConnection(bool pausingOnMeteredCon // initialize handling of metered connections #ifdef SYNCTHINGCONNECTION_SUPPORT_METERED if (!m_handlingMeteredConnectionInitialized) { - QNetworkInformation::loadBackendByFeatures(QNetworkInformation::Feature::Metered); - if (const auto *const networkInformation = QNetworkInformation::instance()) { + if (const auto *const networkInformation = loadNetworkInformationBackendForMetered()) { QObject::connect(networkInformation, &QNetworkInformation::isMeteredChanged, this, &SyncthingConnection::handleMeteredConnection); } } diff --git a/syncthingconnector/syncthingconnection.h b/syncthingconnector/syncthingconnection.h index f5aec99..1fb6ef6 100644 --- a/syncthingconnector/syncthingconnection.h +++ b/syncthingconnector/syncthingconnection.h @@ -5,6 +5,7 @@ #include "./syncthingconnectionstatus.h" #include "./syncthingdev.h" #include "./syncthingdir.h" +#include "./utils.h" #include @@ -17,11 +18,6 @@ #include #include -#if (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)) -#include -#define SYNCTHINGCONNECTION_SUPPORT_METERED -#endif - #include #include #include diff --git a/syncthingconnector/syncthingservice.cpp b/syncthingconnector/syncthingservice.cpp index 4a54b8c..a32709f 100644 --- a/syncthingconnector/syncthingservice.cpp +++ b/syncthingconnector/syncthingservice.cpp @@ -1,6 +1,7 @@ #ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD #include "./syncthingservice.h" +#include "./utils.h" #include "loginmanagerinterface.h" #include "managerinterface.h" @@ -16,6 +17,10 @@ #include #include +#ifdef SYNCTHINGCONNECTION_SUPPORT_METERED +#include +#endif + #include using namespace std; @@ -77,7 +82,9 @@ SyncthingService::SyncthingService(SystemdScope scope, QObject *parent) , m_currentSystemdInterface(nullptr) , m_scope(scope) , m_manuallyStopped(false) + , m_stoppedMetered(false) , m_unitAvailable(false) + , m_stopOnMeteredConnection(false) { setupFreedesktopLoginInterface(); @@ -105,6 +112,14 @@ SyncthingService::SyncthingService(SystemdScope scope, QObject *parent) QStringList()); }); #endif + + // initialize handling of metered connections +#ifdef SYNCTHINGCONNECTION_SUPPORT_METERED + if (const auto *const networkInformation = loadNetworkInformationBackendForMetered()) { + connect(networkInformation, &QNetworkInformation::isMeteredChanged, this, [this](bool isMetered) { setNetworkConnectionMetered(isMetered); }); + setNetworkConnectionMetered(networkInformation->isMetered()); + } +#endif } /*! @@ -370,6 +385,7 @@ void SyncthingService::setRunning(bool running) { #ifndef LIB_SYNCTHING_CONNECTOR_SERVICE_MOCKED m_manuallyStopped = !running; + m_stoppedMetered = false; if (!m_currentSystemdInterface) { setupSystemdInterface(); } @@ -496,6 +512,7 @@ void SyncthingService::handlePropertiesChanged( if (wasRunningBefore != currentlyRunning) { if (currentlyRunning) { m_manuallyStopped = false; + m_stoppedMetered = false; } emit runningChanged(currentlyRunning); } @@ -587,6 +604,17 @@ bool SyncthingService::handlePropertyChanged( return false; } +/*! + * \brief Internal helper to stop the service when the network connection becomes metered. + */ +void SyncthingService::stopDueToMeteredConnection() +{ + if (isRunning()) { + setRunning(false); + } + m_stoppedMetered = true; +} + /*! * \brief Sets the current unit data. */ @@ -612,6 +640,17 @@ void SyncthingService::setUnit(const QDBusObjectPath &objectPath) if (m_unit->isValid()) { m_activeSince = dateTimeFromSystemdTimeStamp(m_unit->activeEnterTimestamp()); setProperties(true, m_unit->activeState(), m_unit->subState(), m_unit->unitFileState(), m_unit->description()); + // handle metered network connection: if the connection is metered and we care about it, then … + if (isStoppingOnMeteredConnection() && isNetworkConnectionMetered().value_or(false)) { + if (isRunning()) { + // stop an already running service immediately + stopDueToMeteredConnection(); + } else { + // consider an already stopped service as stopped due to a metered connection; so we will start it as soon as the connection + // is no longer metered + m_stoppedMetered = true; + } + } } else { // fallback to querying unit file state makeAsyncCall(m_currentSystemdInterface->GetUnitFileState(m_unitName), &SyncthingService::handleGetUnitFileState); @@ -661,6 +700,38 @@ void SyncthingService::setProperties( } } +/*! + * \brief Sets whether the current network connection is metered and stops/starts Syncthing accordingly as needed. + * \remarks + * - This is detected and monitored automatically. A manually set value will be overridden again on the next change. + * - One may set this manually for testing purposes or in case the automatic detection is not supported (then + * isNetworkConnectionMetered() returns a std::optional without value). + */ +void SyncthingService::setNetworkConnectionMetered(std::optional metered) +{ + if (metered != m_metered) { + m_metered = metered; + if (m_stopOnMeteredConnection) { + if (metered.value_or(false)) { + stopDueToMeteredConnection(); + } else if (!metered.value_or(true) && m_stoppedMetered) { + start(); + } + } + emit networkConnectionMeteredChanged(metered); + } +} + +/*! + * \brief Sets whether Syncthing should automatically be stopped as long as the network connection is metered. + */ +void SyncthingService::setStoppingOnMeteredConnection(bool stopOnMeteredConnection) +{ + if ((stopOnMeteredConnection != m_stopOnMeteredConnection) && (m_stopOnMeteredConnection = stopOnMeteredConnection) && m_metered) { + stopDueToMeteredConnection(); + } +} + } // namespace Data #endif diff --git a/syncthingconnector/syncthingservice.h b/syncthingconnector/syncthingservice.h index 9f74e76..5c6f426 100644 --- a/syncthingconnector/syncthingservice.h +++ b/syncthingconnector/syncthingservice.h @@ -8,6 +8,7 @@ #include #include +#include #include QT_FORWARD_DECLARE_CLASS(QDBusServiceWatcher) @@ -52,6 +53,9 @@ class LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingService : public QObject { Q_PROPERTY(bool manuallyStopped READ isManuallyStopped) Q_PROPERTY(SystemdScope scope READ scope WRITE setScope NOTIFY scopeChanged) Q_PROPERTY(bool userScope READ isUserScope NOTIFY scopeChanged) + Q_PROPERTY(std::optional networkConnectionMetered READ isNetworkConnectionMetered WRITE setNetworkConnectionMetered NOTIFY + networkConnectionMeteredChanged) + Q_PROPERTY(bool stoppingOnMeteredConnection READ isStoppingOnMeteredConnection WRITE setStoppingOnMeteredConnection) public: explicit SyncthingService(SystemdScope scope = SystemdScope::User, QObject *parent = nullptr); @@ -77,6 +81,10 @@ public: void setScope(SystemdScope scope); void setScopeAndUnitName(SystemdScope scope, const QString &unitName); bool isUserScope() const; + std::optional isNetworkConnectionMetered() const; + void setNetworkConnectionMetered(std::optional metered); + bool isStoppingOnMeteredConnection() const; + void setStoppingOnMeteredConnection(bool stopOnMeteredConnection); static SyncthingService *mainInstance(); static void setMainInstance(SyncthingService *mainInstance); @@ -104,6 +112,7 @@ Q_SIGNALS: void enabledChanged(bool enable); void errorOccurred(const QString &context, const QString &name, const QString &message); void scopeChanged(Data::SystemdScope scope); + void networkConnectionMeteredChanged(std::optional isMetered); private Q_SLOTS: void handleUnitAdded(const QString &unitName, const QDBusObjectPath &unitPath); @@ -132,6 +141,7 @@ private: const QVariantMap &changedProperties, const QStringList &invalidatedProperties); bool handlePropertyChanged(CppUtilities::DateTime &variable, const QString &propertyName, const QVariantMap &changedProperties, const QStringList &invalidatedProperties); + void stopDueToMeteredConnection(); static OrgFreedesktopSystemd1ManagerInterface *s_systemdUserInterface; static OrgFreedesktopSystemd1ManagerInterface *s_systemdSystemInterface; @@ -153,7 +163,10 @@ private: std::unordered_set m_pendingCalls; SystemdScope m_scope; bool m_manuallyStopped; + bool m_stoppedMetered; bool m_unitAvailable; + bool m_stopOnMeteredConnection; + std::optional m_metered; }; /*! @@ -334,6 +347,19 @@ inline void SyncthingService::disable() setEnabled(false); } +/// \brief Returns whether the current network connection is metered. +/// \remarks Returns an std::optional without value if it is unknown whether the network connection is metered. +inline std::optional SyncthingService::isNetworkConnectionMetered() const +{ + return m_metered; +} + +/// \brief Returns whether Syncthing should automatically be stopped as long as the network connection is metered. +inline bool SyncthingService::isStoppingOnMeteredConnection() const +{ + return m_stopOnMeteredConnection; +} + /*! * \brief Returns the SyncthingService instance which has previously been assigned via SyncthingService::setMainInstance(). */ diff --git a/syncthingconnector/utils.cpp b/syncthingconnector/utils.cpp index 021dad5..9e130a3 100644 --- a/syncthingconnector/utils.cpp +++ b/syncthingconnector/utils.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -16,6 +17,12 @@ #include #include +#ifdef SYNCTHINGCONNECTION_SUPPORT_METERED +#include +#endif + +#include + using namespace CppUtilities; namespace Data { @@ -251,4 +258,33 @@ QString substituteTilde(const QString &path, const QString &tilde, const QString return path; } +#ifdef SYNCTHINGCONNECTION_SUPPORT_METERED +/*! + * \brief Loads the QNetworkInformation backend for determining whether the connection is metered. + */ +const QNetworkInformation *loadNetworkInformationBackendForMetered() +{ + static const auto *const backend = []() -> const QNetworkInformation * { + QNetworkInformation::loadBackendByFeatures(QNetworkInformation::Feature::Metered); + if (const auto *const networkInformation = QNetworkInformation::instance(); + networkInformation && networkInformation->supports(QNetworkInformation::Feature::Metered)) { + return networkInformation; + } + + std::cerr << EscapeCodes::Phrases::Error + << "Unable to load network information backend to monitor metered connections, available backends:" << EscapeCodes::Phrases::End; + const auto availableBackends = QNetworkInformation::availableBackends(); + if (availableBackends.isEmpty()) { + std::cerr << "none\n"; + } else { + for (const auto &backendName : availableBackends) { + std::cerr << " - " << backendName.toStdString() << '\n'; + } + } + return nullptr; + }(); + return backend; +} +#endif + } // namespace Data diff --git a/syncthingconnector/utils.h b/syncthingconnector/utils.h index 67c03e6..038b52a 100644 --- a/syncthingconnector/utils.h +++ b/syncthingconnector/utils.h @@ -14,6 +14,11 @@ QT_FORWARD_DECLARE_CLASS(QJsonObject) QT_FORWARD_DECLARE_CLASS(QHostAddress) +QT_FORWARD_DECLARE_CLASS(QNetworkInformation) + +#if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)) +#define SYNCTHINGCONNECTION_SUPPORT_METERED +#endif namespace CppUtilities { class DateTime; @@ -39,6 +44,9 @@ LIB_SYNCTHING_CONNECTOR_EXPORT bool isLocal(const QString &hostName, const QHost LIB_SYNCTHING_CONNECTOR_EXPORT bool setDirectoriesPaused(QJsonObject &syncthingConfig, const QStringList &dirIds, bool paused); LIB_SYNCTHING_CONNECTOR_EXPORT bool setDevicesPaused(QJsonObject &syncthingConfig, const QStringList &dirs, bool paused); LIB_SYNCTHING_CONNECTOR_EXPORT QString substituteTilde(const QString &path, const QString &tilde, const QString &pathSeparator); +#ifdef SYNCTHINGCONNECTION_SUPPORT_METERED +LIB_SYNCTHING_CONNECTOR_EXPORT const QNetworkInformation *loadNetworkInformationBackendForMetered(); +#endif /*! * \brief Returns whether the host specified by the given \a url is the local machine. diff --git a/syncthingwidgets/misc/syncthinglauncher.cpp b/syncthingwidgets/misc/syncthinglauncher.cpp index a4c13de..bc98e27 100644 --- a/syncthingwidgets/misc/syncthinglauncher.cpp +++ b/syncthingwidgets/misc/syncthinglauncher.cpp @@ -1,6 +1,7 @@ #include "./syncthinglauncher.h" #include +#include #include "../settings/settings.h" @@ -8,9 +9,8 @@ #include -#if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)) +#ifdef SYNCTHINGCONNECTION_SUPPORT_METERED #include -#define SYNCTHINGCONNECTION_SUPPORT_METERED #endif #include @@ -63,22 +63,9 @@ SyncthingLauncher::SyncthingLauncher(QObject *parent) // initialize handling of metered connections #ifdef SYNCTHINGCONNECTION_SUPPORT_METERED - QNetworkInformation::loadBackendByFeatures(QNetworkInformation::Feature::Metered); - if (const auto *const networkInformation = QNetworkInformation::instance(); - networkInformation && networkInformation->supports(QNetworkInformation::Feature::Metered)) { + if (const auto *const networkInformation = loadNetworkInformationBackendForMetered()) { connect(networkInformation, &QNetworkInformation::isMeteredChanged, this, [this](bool isMetered) { setNetworkConnectionMetered(isMetered); }); setNetworkConnectionMetered(networkInformation->isMetered()); - } else { - std::cerr << EscapeCodes::Phrases::Error - << "Unable to load network information backend to monitor metered connections, available backends:" << EscapeCodes::Phrases::End; - const auto availableBackends = QNetworkInformation::availableBackends(); - if (availableBackends.isEmpty()) { - std::cerr << "none\n"; - } else { - for (const auto &backend : availableBackends) { - std::cerr << " - " << backend.toStdString() << '\n'; - } - } } #endif } diff --git a/syncthingwidgets/settings/settings.cpp b/syncthingwidgets/settings/settings.cpp index 4d6c7e2..9a4c581 100644 --- a/syncthingwidgets/settings/settings.cpp +++ b/syncthingwidgets/settings/settings.cpp @@ -412,6 +412,7 @@ bool restore() systemd.systemUnit = settings.value(QStringLiteral("systemUnit"), systemd.systemUnit).toBool(); systemd.showButton = settings.value(QStringLiteral("showButton"), systemd.showButton).toBool(); systemd.considerForReconnect = settings.value(QStringLiteral("considerForReconnect"), systemd.considerForReconnect).toBool(); + systemd.stopOnMeteredConnection = settings.value(QStringLiteral("stopServiceOnMetered"), systemd.stopOnMeteredConnection).toBool(); #endif settings.endGroup(); @@ -540,6 +541,7 @@ bool save() settings.setValue(QStringLiteral("systemUnit"), systemd.systemUnit); settings.setValue(QStringLiteral("showButton"), systemd.showButton); settings.setValue(QStringLiteral("considerForReconnect"), systemd.considerForReconnect); + settings.setValue(QStringLiteral("stopServiceOnMetered"), systemd.stopOnMeteredConnection); #endif settings.endGroup(); @@ -606,6 +608,7 @@ void Settings::apply(SyncthingNotifier ¬ifier) const */ void Systemd::setupService(SyncthingService &service) const { + service.setStoppingOnMeteredConnection(stopOnMeteredConnection); service.setScopeAndUnitName(systemUnit ? SystemdScope::System : SystemdScope::User, syncthingUnit); } diff --git a/syncthingwidgets/settings/settings.h b/syncthingwidgets/settings/settings.h index 30970d3..4a6604c 100644 --- a/syncthingwidgets/settings/settings.h +++ b/syncthingwidgets/settings/settings.h @@ -127,6 +127,7 @@ struct SYNCTHINGWIDGETS_EXPORT Systemd { bool systemUnit = false; bool showButton = false; bool considerForReconnect = false; + bool stopOnMeteredConnection = false; struct SYNCTHINGWIDGETS_EXPORT ServiceStatus { bool relevant = false; diff --git a/syncthingwidgets/settings/settingsdialog.cpp b/syncthingwidgets/settings/settingsdialog.cpp index d5c481f..9d5d7e3 100644 --- a/syncthingwidgets/settings/settingsdialog.cpp +++ b/syncthingwidgets/settings/settingsdialog.cpp @@ -1426,6 +1426,7 @@ QWidget *SystemdOptionPage::setupWidget() QIcon::fromTheme(QStringLiteral("view-refresh"), QIcon(QStringLiteral(":/icons/hicolor/scalable/actions/view-refresh.svg")))); ui()->syncthingUnitLineEdit->addCustomAction(refreshAction); if (!m_service) { + ui()->stopOnMeteredCheckBox->setHidden(true); return widget; } QObject::connect(refreshAction, &QAction::triggered, m_service, &SyncthingService::reloadAllUnitFiles); @@ -1434,6 +1435,8 @@ QWidget *SystemdOptionPage::setupWidget() QObject::connect(ui()->stopPushButton, &QPushButton::clicked, m_service, &SyncthingService::stop); QObject::connect(ui()->enablePushButton, &QPushButton::clicked, m_service, &SyncthingService::enable); QObject::connect(ui()->disablePushButton, &QPushButton::clicked, m_service, &SyncthingService::disable); + QObject::connect(ui()->stopOnMeteredCheckBox, &QCheckBox::checkStateChanged, m_service, + [s = m_service](Qt::CheckState checkState) { s->setStoppingOnMeteredConnection(checkState == Qt::Checked); }); m_unitChangedConn = QObject::connect(ui()->systemUnitCheckBox, &QCheckBox::clicked, m_service, bind(&SystemdOptionPage::handleSystemUnitChanged, this)); m_descChangedConn @@ -1445,6 +1448,9 @@ QWidget *SystemdOptionPage::setupWidget() if (const auto *optionPageWidget = qobject_cast(widget)) { QObject::connect(optionPageWidget, &OptionPageWidget::paletteChanged, std::bind(&SystemdOptionPage::updateColors, this)); } + configureMeteredCheckbox(ui()->stopOnMeteredCheckBox, m_service->isNetworkConnectionMetered()); + QObject::connect(m_service, &SyncthingService::networkConnectionMeteredChanged, + std::bind(&configureMeteredCheckbox, ui()->stopOnMeteredCheckBox, std::placeholders::_1)); return widget; } @@ -1457,6 +1463,7 @@ bool SystemdOptionPage::apply() systemdSettings.systemUnit = ui()->systemUnitCheckBox->isChecked(); systemdSettings.showButton = ui()->showButtonCheckBox->isChecked(); systemdSettings.considerForReconnect = ui()->considerForReconnectCheckBox->isChecked(); + systemdSettings.stopOnMeteredConnection = ui()->stopOnMeteredCheckBox->isChecked(); auto result = true; if (systemdSettings.showButton && launcherSettings.showButton) { errors().append(QCoreApplication::translate("QtGui::SystemdOptionPage", @@ -1480,6 +1487,7 @@ void SystemdOptionPage::reset() ui()->systemUnitCheckBox->setChecked(settings.systemUnit); ui()->showButtonCheckBox->setChecked(settings.showButton); ui()->considerForReconnectCheckBox->setChecked(settings.considerForReconnect); + ui()->stopOnMeteredCheckBox->setChecked(settings.stopOnMeteredConnection); if (!m_service) { return; } diff --git a/syncthingwidgets/settings/systemdoptionpage.ui b/syncthingwidgets/settings/systemdoptionpage.ui index df6cb11..4db76e8 100644 --- a/syncthingwidgets/settings/systemdoptionpage.ui +++ b/syncthingwidgets/settings/systemdoptionpage.ui @@ -32,6 +32,13 @@ + + + + Stop automatically when network connection is metered + + +