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.
This commit is contained in:
Martchus 2024-04-19 00:04:48 +02:00
parent 353a380464
commit d99fadd1f1
11 changed files with 165 additions and 23 deletions

View File

@ -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);
}
}

View File

@ -5,6 +5,7 @@
#include "./syncthingconnectionstatus.h"
#include "./syncthingdev.h"
#include "./syncthingdir.h"
#include "./utils.h"
#include <c++utilities/misc/flagenumclass.h>
@ -17,11 +18,6 @@
#include <QSslError>
#include <QTimer>
#if (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0))
#include <QNetworkInformation>
#define SYNCTHINGCONNECTION_SUPPORT_METERED
#endif
#include <cstdint>
#include <functional>
#include <limits>

View File

@ -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 <QDBusPendingReply>
#include <QDBusServiceWatcher>
#ifdef SYNCTHINGCONNECTION_SUPPORT_METERED
#include <QNetworkInformation>
#endif
#include <functional>
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<bool> without value).
*/
void SyncthingService::setNetworkConnectionMetered(std::optional<bool> 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

View File

@ -8,6 +8,7 @@
#include <QObject>
#include <QVariantMap>
#include <optional>
#include <unordered_set>
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<bool> 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<bool> isNetworkConnectionMetered() const;
void setNetworkConnectionMetered(std::optional<bool> 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<bool> 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<QDBusPendingCallWatcher *> m_pendingCalls;
SystemdScope m_scope;
bool m_manuallyStopped;
bool m_stoppedMetered;
bool m_unitAvailable;
bool m_stopOnMeteredConnection;
std::optional<bool> 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<bool> without value if it is unknown whether the network connection is metered.
inline std::optional<bool> 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().
*/

View File

@ -5,6 +5,7 @@
#include <c++utilities/chrono/datetime.h>
#include <c++utilities/conversion/stringconversion.h>
#include <c++utilities/io/ansiescapecodes.h>
#include <QCoreApplication>
#include <QHostAddress>
@ -16,6 +17,12 @@
#include <QStringBuilder>
#include <QUrl>
#ifdef SYNCTHINGCONNECTION_SUPPORT_METERED
#include <QNetworkInformation>
#endif
#include <iostream>
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

View File

@ -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.

View File

@ -1,6 +1,7 @@
#include "./syncthinglauncher.h"
#include <syncthingconnector/syncthingconnection.h>
#include <syncthingconnector/utils.h>
#include "../settings/settings.h"
@ -8,9 +9,8 @@
#include <QtConcurrentRun>
#if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0))
#ifdef SYNCTHINGCONNECTION_SUPPORT_METERED
#include <QNetworkInformation>
#define SYNCTHINGCONNECTION_SUPPORT_METERED
#endif
#include <algorithm>
@ -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
}

View File

@ -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 &notifier) const
*/
void Systemd::setupService(SyncthingService &service) const
{
service.setStoppingOnMeteredConnection(stopOnMeteredConnection);
service.setScopeAndUnitName(systemUnit ? SystemdScope::System : SystemdScope::User, syncthingUnit);
}

View File

@ -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;

View File

@ -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<OptionPageWidget *>(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;
}

View File

@ -32,6 +32,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="stopOnMeteredCheckBox">
<property name="text">
<string>Stop automatically when network connection is metered</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">