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 + + +