Compare commits

...

2 Commits

Author SHA1 Message Date
Martchus 4c6315b450 Terminate Syncthing gracefully via REST-API on non-UNIX platforms (2)
A follow up to 0faacaa7c8 to cover the stop button within the launcher
and terminating Syncthing on shutdown/exit. To find the relevant connection
the connection settings are searched for a local URL where the port matches
the port from the Syncthing process log.
2021-07-15 02:38:26 +02:00
Martchus 69f466be66 Fix crash when systemd unit status changes when settings page has already been destroyed 2021-07-15 02:21:18 +02:00
11 changed files with 154 additions and 26 deletions

View File

@ -11,7 +11,7 @@ set(META_APP_CATEGORIES "Network;FileTransfer")
set(META_GUI_OPTIONAL false) set(META_GUI_OPTIONAL false)
set(META_VERSION_MAJOR 1) set(META_VERSION_MAJOR 1)
set(META_VERSION_MINOR 1) set(META_VERSION_MINOR 1)
set(META_VERSION_PATCH 8) set(META_VERSION_PATCH 9)
set(META_VERSION_EXACT_SONAME ON) set(META_VERSION_EXACT_SONAME ON)
set(META_ADD_DEFAULT_CPP_UNIT_TEST_APPLICATION ON) set(META_ADD_DEFAULT_CPP_UNIT_TEST_APPLICATION ON)

View File

@ -4,6 +4,9 @@
#include <QTimer> #include <QTimer>
// uncomment to enforce stopSyncthing() via REST-API (for testing)
//#define LIB_SYNCTHING_CONNECTOR_ENFORCE_STOP_VIA_API
#ifdef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS #ifdef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS
#include <c++utilities/io/ansiescapecodes.h> #include <c++utilities/io/ansiescapecodes.h>
@ -224,7 +227,7 @@ void SyncthingProcess::stopSyncthing(SyncthingConnection *currentConnection)
{ {
m_manuallyStopped = true; m_manuallyStopped = true;
m_killTimer.start(); m_killTimer.start();
#ifdef PLATFORM_UNIX #if defined(PLATFORM_UNIX) && !defined(LIB_SYNCTHING_CONNECTOR_ENFORCE_STOP_VIA_API)
Q_UNUSED(currentConnection) Q_UNUSED(currentConnection)
#else #else
if (currentConnection && !currentConnection->syncthingUrl().isEmpty() && !currentConnection->apiKey().isEmpty() && currentConnection->isLocal()) { if (currentConnection && !currentConnection->syncthingUrl().isEmpty() && !currentConnection->apiKey().isEmpty() && currentConnection->isLocal()) {

View File

@ -74,8 +74,8 @@ set(REQUIRED_ICONS
go-up) go-up)
# find c++utilities # find c++utilities
find_package(c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.0.0 REQUIRED) find_package(c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.11.0 REQUIRED)
use_cpp_utilities() use_cpp_utilities(VISIBILITY PUBLIC)
# find qtutilities # find qtutilities
find_package(qtutilities${CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES} 6.3.0 REQUIRED) find_package(qtutilities${CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES} 6.3.0 REQUIRED)

View File

@ -13,18 +13,18 @@ using namespace CppUtilities;
namespace QtGui { namespace QtGui {
SyncthingKiller::SyncthingKiller(std::vector<SyncthingProcess *> &&processes) SyncthingKiller::SyncthingKiller(std::vector<ProcessWithConnection> &&processes)
: m_processes(processes) : m_processes(processes)
{ {
for (auto *process : m_processes) { for (const auto [process, connection] : m_processes) {
process->stopSyncthing(); process->stopSyncthing(connection);
connect(process, &SyncthingProcess::confirmKill, this, &SyncthingKiller::confirmKill); connect(process, &SyncthingProcess::confirmKill, this, &SyncthingKiller::confirmKill);
} }
} }
void SyncthingKiller::waitForFinished() void SyncthingKiller::waitForFinished()
{ {
for (auto *process : m_processes) { for (const auto [process, connection] : m_processes) {
if (!process->isRunning()) { if (!process->isRunning()) {
continue; continue;
} }

View File

@ -8,15 +8,28 @@
#include <vector> #include <vector>
namespace Data { namespace Data {
class SyncthingConnection;
class SyncthingProcess; class SyncthingProcess;
} } // namespace Data
namespace QtGui { namespace QtGui {
struct ProcessWithConnection {
explicit ProcessWithConnection(Data::SyncthingProcess *process, Data::SyncthingConnection *connection = nullptr);
Data::SyncthingProcess *const process;
Data::SyncthingConnection *const connection;
};
inline ProcessWithConnection::ProcessWithConnection(Data::SyncthingProcess *process, Data::SyncthingConnection *connection)
: process(process)
, connection(connection)
{
}
class SYNCTHINGWIDGETS_EXPORT SyncthingKiller : public QObject { class SYNCTHINGWIDGETS_EXPORT SyncthingKiller : public QObject {
Q_OBJECT Q_OBJECT
public: public:
SyncthingKiller(std::vector<Data::SyncthingProcess *> &&processes); explicit SyncthingKiller(std::vector<ProcessWithConnection> &&processes);
Q_SIGNALS: Q_SIGNALS:
void ignored(); void ignored();
@ -28,7 +41,7 @@ private Q_SLOTS:
void confirmKill() const; void confirmKill() const;
private: private:
std::vector<Data::SyncthingProcess *> m_processes; std::vector<ProcessWithConnection> m_processes;
}; };
} // namespace QtGui } // namespace QtGui

View File

@ -7,6 +7,7 @@
#include <algorithm> #include <algorithm>
#include <functional> #include <functional>
#include <limits> #include <limits>
#include <string_view>
using namespace std; using namespace std;
using namespace std::placeholders; using namespace std::placeholders;
@ -30,6 +31,8 @@ SyncthingLauncher *SyncthingLauncher::s_mainInstance = nullptr;
*/ */
SyncthingLauncher::SyncthingLauncher(QObject *parent) SyncthingLauncher::SyncthingLauncher(QObject *parent)
: QObject(parent) : QObject(parent)
, m_guiListeningUrlSearch("Access the GUI via the following URL: ", "\n\r", std::string_view(),
std::bind(&SyncthingLauncher::handleGuiListeningUrlFound, this, std::placeholders::_1, std::placeholders::_2))
#ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING #ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING
, m_libsyncthingLogLevel(LibSyncthing::LogLevel::Info) , m_libsyncthingLogLevel(LibSyncthing::LogLevel::Info)
#endif #endif
@ -94,7 +97,7 @@ void SyncthingLauncher::launch(const QString &program, const QStringList &argume
if (isRunning() || m_stopFuture.isRunning()) { if (isRunning() || m_stopFuture.isRunning()) {
return; return;
} }
m_manuallyStopped = false; resetState();
// start external process // start external process
if (!program.isEmpty()) { if (!program.isEmpty()) {
@ -148,7 +151,7 @@ void SyncthingLauncher::launch(const LibSyncthing::RuntimeOptions &runtimeOption
if (isRunning() || m_stopFuture.isRunning()) { if (isRunning() || m_stopFuture.isRunning()) {
return; return;
} }
m_manuallyStopped = false; resetState();
m_startFuture = QtConcurrent::run(std::bind(&SyncthingLauncher::runLibSyncthing, this, runtimeOptions)); m_startFuture = QtConcurrent::run(std::bind(&SyncthingLauncher::runLibSyncthing, this, runtimeOptions));
} }
#endif #endif
@ -207,6 +210,16 @@ void SyncthingLauncher::handleProcessFinished(int exitCode, QProcess::ExitStatus
emit exited(exitCode, exitStatus); emit exited(exitCode, exitStatus);
} }
void SyncthingLauncher::resetState()
{
m_manuallyStopped = false;
m_guiListeningUrlSearch.reset();
if (!m_guiListeningUrl.isEmpty()) {
m_guiListeningUrl.clear();
emit guiUrlChanged(m_guiListeningUrl);
}
}
#ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING #ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING
static const char *const logLevelStrings[] = { static const char *const logLevelStrings[] = {
"[DEBUG] ", "[DEBUG] ",
@ -234,6 +247,7 @@ void SyncthingLauncher::handleLoggingCallback(LibSyncthing::LogLevel level, cons
void SyncthingLauncher::handleOutputAvailable(QByteArray &&data) void SyncthingLauncher::handleOutputAvailable(QByteArray &&data)
{ {
m_guiListeningUrlSearch(data.data(), static_cast<std::size_t>(data.size()));
if (isEmittingOutput()) { if (isEmittingOutput()) {
emit outputAvailable(data); emit outputAvailable(data);
} else { } else {
@ -241,6 +255,12 @@ void SyncthingLauncher::handleOutputAvailable(QByteArray &&data)
} }
} }
void SyncthingLauncher::handleGuiListeningUrlFound(CppUtilities::BufferSearch &, std::string &&searchResult)
{
m_guiListeningUrl.setUrl(QString::fromStdString(searchResult));
emit guiUrlChanged(m_guiListeningUrl);
}
#ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING #ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING
void SyncthingLauncher::runLibSyncthing(const LibSyncthing::RuntimeOptions &runtimeOptions) void SyncthingLauncher::runLibSyncthing(const LibSyncthing::RuntimeOptions &runtimeOptions)
{ {

View File

@ -9,8 +9,11 @@
#include <syncthingconnector/syncthingprocess.h> #include <syncthingconnector/syncthingprocess.h>
#include <c++utilities/io/buffersearch.h>
#include <QByteArray> #include <QByteArray>
#include <QFuture> #include <QFuture>
#include <QUrl>
namespace Settings { namespace Settings {
struct Launcher; struct Launcher;
@ -26,6 +29,8 @@ class SYNCTHINGWIDGETS_EXPORT SyncthingLauncher : public QObject {
Q_PROPERTY(CppUtilities::DateTime activeSince READ activeSince) Q_PROPERTY(CppUtilities::DateTime activeSince READ activeSince)
Q_PROPERTY(bool manuallyStopped READ isManuallyStopped) Q_PROPERTY(bool manuallyStopped READ isManuallyStopped)
Q_PROPERTY(bool emittingOutput READ isEmittingOutput WRITE setEmittingOutput) Q_PROPERTY(bool emittingOutput READ isEmittingOutput WRITE setEmittingOutput)
Q_PROPERTY(QUrl guiUrl READ guiUrl WRITE guiUrlChanged)
Q_PROPERTY(SyncthingProcess *process READ process)
public: public:
explicit SyncthingLauncher(QObject *parent = nullptr); explicit SyncthingLauncher(QObject *parent = nullptr);
@ -37,6 +42,9 @@ public:
bool isEmittingOutput() const; bool isEmittingOutput() const;
void setEmittingOutput(bool emittingOutput); void setEmittingOutput(bool emittingOutput);
QString errorString() const; QString errorString() const;
QUrl guiUrl() const;
SyncthingProcess *process();
const SyncthingProcess *process() const;
#ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING #ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING
LibSyncthing::LogLevel libSyncthingLogLevel() const; LibSyncthing::LogLevel libSyncthingLogLevel() const;
void setLibSyncthingLogLevel(LibSyncthing::LogLevel logLevel); void setLibSyncthingLogLevel(LibSyncthing::LogLevel logLevel);
@ -55,6 +63,7 @@ Q_SIGNALS:
void outputAvailable(const QByteArray &data); void outputAvailable(const QByteArray &data);
void exited(int exitCode, QProcess::ExitStatus exitStatus); void exited(int exitCode, QProcess::ExitStatus exitStatus);
void errorOccurred(QProcess::ProcessError error); void errorOccurred(QProcess::ProcessError error);
void guiUrlChanged(const QUrl &newUrl);
public Q_SLOTS: public Q_SLOTS:
void launch(const QString &program, const QStringList &arguments); void launch(const QString &program, const QStringList &arguments);
@ -75,15 +84,19 @@ private Q_SLOTS:
#endif #endif
private: private:
void resetState();
#ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING #ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING
void handleLoggingCallback(LibSyncthing::LogLevel, const char *message, std::size_t messageSize); void handleLoggingCallback(LibSyncthing::LogLevel, const char *message, std::size_t messageSize);
#endif #endif
void handleOutputAvailable(QByteArray &&data); void handleOutputAvailable(QByteArray &&data);
void handleGuiListeningUrlFound(CppUtilities::BufferSearch &bufferSearch, std::string &&searchResult);
SyncthingProcess m_process; SyncthingProcess m_process;
QUrl m_guiListeningUrl;
QFuture<void> m_startFuture; QFuture<void> m_startFuture;
QFuture<void> m_stopFuture; QFuture<void> m_stopFuture;
QByteArray m_outputBuffer; QByteArray m_outputBuffer;
CppUtilities::BufferSearch m_guiListeningUrlSearch;
CppUtilities::DateTime m_futureStarted; CppUtilities::DateTime m_futureStarted;
#ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING #ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING
LibSyncthing::LogLevel m_libsyncthingLogLevel; LibSyncthing::LogLevel m_libsyncthingLogLevel;
@ -138,6 +151,24 @@ inline QString SyncthingLauncher::errorString() const
return m_process.errorString(); return m_process.errorString();
} }
/// \brief Returns the GUI listening URL determined from Syncthing's log.
inline QUrl SyncthingLauncher::guiUrl() const
{
return m_guiListeningUrl;
}
/// \brief Returns the underlying SyncthingProcess.
inline SyncthingProcess *SyncthingLauncher::process()
{
return &m_process;
}
/// \brief Returns the underlying SyncthingProcess.
inline const SyncthingProcess *SyncthingLauncher::process() const
{
return &m_process;
}
#ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING #ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING
/// \brief Returns the log level used for libsyncthing. /// \brief Returns the log level used for libsyncthing.
inline LibSyncthing::LogLevel SyncthingLauncher::libSyncthingLogLevel() const inline LibSyncthing::LogLevel SyncthingLauncher::libSyncthingLogLevel() const

View File

@ -8,6 +8,7 @@
#include <syncthingconnector/syncthingconnectionsettings.h> #include <syncthingconnector/syncthingconnectionsettings.h>
#include <syncthingconnector/syncthingnotifier.h> #include <syncthingconnector/syncthingnotifier.h>
#include <syncthingconnector/syncthingprocess.h> #include <syncthingconnector/syncthingprocess.h>
#include <syncthingconnector/utils.h>
#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD #ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD
#include <syncthingconnector/syncthingservice.h> #include <syncthingconnector/syncthingservice.h>
#endif #endif
@ -20,6 +21,8 @@
#include <qtutilities/misc/dbusnotification.h> #include <qtutilities/misc/dbusnotification.h>
#endif #endif
#include <c++utilities/io/ansiescapecodes.h>
#include <QApplication> #include <QApplication>
#include <QCursor> #include <QCursor>
#include <QFile> #include <QFile>
@ -29,11 +32,13 @@
#include <QSslError> #include <QSslError>
#include <QStringBuilder> #include <QStringBuilder>
#include <iostream>
#include <type_traits> #include <type_traits>
#include <unordered_map> #include <unordered_map>
using namespace std; using namespace std;
using namespace Data; using namespace Data;
using namespace CppUtilities::EscapeCodes;
using namespace QtUtilities; using namespace QtUtilities;
namespace Settings { namespace Settings {
@ -64,15 +69,53 @@ SyncthingProcess &Launcher::toolProcess(const QString &tool)
return toolProcesses[tool]; return toolProcesses[tool];
} }
std::vector<SyncthingProcess *> Launcher::allProcesses() static bool isLocalAndMatchesPort(const Data::SyncthingConnectionSettings &settings, int port)
{ {
vector<SyncthingProcess *> processes; if (settings.syncthingUrl.isEmpty() || settings.apiKey.isEmpty()) {
processes.reserve(1 + toolProcesses.size()); return false;
if (auto *const syncthingProcess = SyncthingProcess::mainInstance()) {
processes.push_back(syncthingProcess);
} }
for (auto &process : toolProcesses) { const auto url = QUrl(settings.syncthingUrl);
processes.push_back(&process.second); return ::Data::isLocal(url) && port == url.port(url.scheme() == QLatin1String("https") ? 443 : 80);
}
Data::SyncthingConnection *Launcher::connectionForLauncher(Data::SyncthingLauncher *launcher)
{
const auto port = launcher->guiUrl().port(-1);
if (port < 0) {
return nullptr;
}
auto &connectionSettings = values().connection;
auto *relevantSetting = isLocalAndMatchesPort(connectionSettings.primary, port) ? &connectionSettings.primary : nullptr;
if (!relevantSetting) {
for (auto &secondarySetting : connectionSettings.secondary) {
if (isLocalAndMatchesPort(secondarySetting, port)) {
relevantSetting = &secondarySetting;
continue;
}
}
}
if (!relevantSetting) {
return nullptr;
}
auto *const connection = new SyncthingConnection();
connection->setParent(launcher);
connection->applySettings(*relevantSetting);
std::cerr << Phrases::Info << "Considering configured connection \"" << relevantSetting->label.toStdString()
<< "\" (URL: " << relevantSetting->syncthingUrl.toStdString() << ") to terminate Syncthing" << Phrases::End;
return connection;
}
std::vector<QtGui::ProcessWithConnection> Launcher::allProcesses()
{
auto processes = std::vector<QtGui::ProcessWithConnection>();
processes.reserve(1 + toolProcesses.size());
if (auto *const launcher = SyncthingLauncher::mainInstance()) {
processes.emplace_back(launcher->process(), connectionForLauncher(launcher));
} else if (auto *const process = SyncthingProcess::mainInstance()) {
processes.emplace_back(process);
}
for (auto &[tool, process] : toolProcesses) {
processes.emplace_back(&process);
} }
return processes; return processes;
} }

View File

@ -29,11 +29,16 @@ class QtSettings;
namespace Data { namespace Data {
class SyncthingProcess; class SyncthingProcess;
class SyncthingLauncher;
class SyncthingNotifier; class SyncthingNotifier;
class SyncthingConnection; class SyncthingConnection;
class SyncthingService; class SyncthingService;
} // namespace Data } // namespace Data
namespace QtGui {
struct ProcessWithConnection;
}
namespace Settings { namespace Settings {
struct SYNCTHINGWIDGETS_EXPORT Connection { struct SYNCTHINGWIDGETS_EXPORT Connection {
@ -94,7 +99,8 @@ struct SYNCTHINGWIDGETS_EXPORT Launcher {
#endif #endif
static Data::SyncthingProcess &toolProcess(const QString &tool); static Data::SyncthingProcess &toolProcess(const QString &tool);
static std::vector<Data::SyncthingProcess *> allProcesses(); static Data::SyncthingConnection *connectionForLauncher(Data::SyncthingLauncher *launcher);
static std::vector<QtGui::ProcessWithConnection> allProcesses();
void autostart() const; void autostart() const;
static void terminate(); static void terminate();
struct SYNCTHINGWIDGETS_EXPORT LauncherStatus { struct SYNCTHINGWIDGETS_EXPORT LauncherStatus {

View File

@ -1188,7 +1188,7 @@ void LauncherOptionPage::stop()
m_process->stopSyncthing(); m_process->stopSyncthing();
} }
if (m_launcher) { if (m_launcher) {
m_launcher->terminate(); m_launcher->terminate(Launcher::connectionForLauncher(m_launcher));
} }
} }
} }
@ -1209,6 +1209,10 @@ SystemdOptionPage::SystemdOptionPage(QWidget *parentWidget)
SystemdOptionPage::~SystemdOptionPage() SystemdOptionPage::~SystemdOptionPage()
{ {
QObject::disconnect(m_unitChangedConn);
QObject::disconnect(m_descChangedConn);
QObject::disconnect(m_statusChangedConn);
QObject::disconnect(m_enabledChangedConn);
} }
QWidget *SystemdOptionPage::setupWidget() QWidget *SystemdOptionPage::setupWidget()
@ -1218,14 +1222,18 @@ QWidget *SystemdOptionPage::setupWidget()
return widget; return widget;
} }
QObject::connect(ui()->syncthingUnitLineEdit, &QLineEdit::textChanged, m_service, &SyncthingService::setUnitName); QObject::connect(ui()->syncthingUnitLineEdit, &QLineEdit::textChanged, m_service, &SyncthingService::setUnitName);
QObject::connect(ui()->systemUnitCheckBox, &QCheckBox::clicked, m_service, bind(&SystemdOptionPage::handleSystemUnitChanged, this));
QObject::connect(ui()->startPushButton, &QPushButton::clicked, m_service, &SyncthingService::start); QObject::connect(ui()->startPushButton, &QPushButton::clicked, m_service, &SyncthingService::start);
QObject::connect(ui()->stopPushButton, &QPushButton::clicked, m_service, &SyncthingService::stop); QObject::connect(ui()->stopPushButton, &QPushButton::clicked, m_service, &SyncthingService::stop);
QObject::connect(ui()->enablePushButton, &QPushButton::clicked, m_service, &SyncthingService::enable); QObject::connect(ui()->enablePushButton, &QPushButton::clicked, m_service, &SyncthingService::enable);
QObject::connect(ui()->disablePushButton, &QPushButton::clicked, m_service, &SyncthingService::disable); QObject::connect(ui()->disablePushButton, &QPushButton::clicked, m_service, &SyncthingService::disable);
QObject::connect(m_service, &SyncthingService::descriptionChanged, bind(&SystemdOptionPage::handleDescriptionChanged, this, _1)); m_unitChangedConn
QObject::connect(m_service, &SyncthingService::stateChanged, bind(&SystemdOptionPage::handleStatusChanged, this, _1, _2, _3)); = QObject::connect(ui()->systemUnitCheckBox, &QCheckBox::clicked, m_service, bind(&SystemdOptionPage::handleSystemUnitChanged, this));
QObject::connect(m_service, &SyncthingService::unitFileStateChanged, bind(&SystemdOptionPage::handleEnabledChanged, this, _1)); m_descChangedConn
= QObject::connect(m_service, &SyncthingService::descriptionChanged, bind(&SystemdOptionPage::handleDescriptionChanged, this, _1));
m_statusChangedConn
= QObject::connect(m_service, &SyncthingService::stateChanged, bind(&SystemdOptionPage::handleStatusChanged, this, _1, _2, _3));
m_enabledChangedConn
= QObject::connect(m_service, &SyncthingService::unitFileStateChanged, bind(&SystemdOptionPage::handleEnabledChanged, this, _1));
return widget; return widget;
} }

View File

@ -148,6 +148,10 @@ void handleDescriptionChanged(const QString &description);
void handleStatusChanged(const QString &activeState, const QString &subState, CppUtilities::DateTime activeSince); void handleStatusChanged(const QString &activeState, const QString &subState, CppUtilities::DateTime activeSince);
void handleEnabledChanged(const QString &unitFileState); void handleEnabledChanged(const QString &unitFileState);
Data::SyncthingService *const m_service; Data::SyncthingService *const m_service;
QMetaObject::Connection m_unitChangedConn;
QMetaObject::Connection m_descChangedConn;
QMetaObject::Connection m_statusChangedConn;
QMetaObject::Connection m_enabledChangedConn;
END_DECLARE_OPTION_PAGE END_DECLARE_OPTION_PAGE
#endif #endif