From 4c6315b4509b5247f69d3f60a7980932a1b942fc Mon Sep 17 00:00:00 2001 From: Martchus Date: Thu, 15 Jul 2021 02:38:26 +0200 Subject: [PATCH] Terminate Syncthing gracefully via REST-API on non-UNIX platforms (2) A follow up to 0faacaa7c8c8 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. --- connector/syncthingprocess.cpp | 5 ++- widgets/CMakeLists.txt | 4 +- widgets/misc/syncthingkiller.cpp | 8 ++-- widgets/misc/syncthingkiller.h | 19 ++++++++-- widgets/misc/syncthinglauncher.cpp | 24 +++++++++++- widgets/misc/syncthinglauncher.h | 31 ++++++++++++++++ widgets/settings/settings.cpp | 57 +++++++++++++++++++++++++---- widgets/settings/settings.h | 8 +++- widgets/settings/settingsdialog.cpp | 2 +- 9 files changed, 137 insertions(+), 21 deletions(-) diff --git a/connector/syncthingprocess.cpp b/connector/syncthingprocess.cpp index e239f4d..9f4cb8c 100644 --- a/connector/syncthingprocess.cpp +++ b/connector/syncthingprocess.cpp @@ -4,6 +4,9 @@ #include +// uncomment to enforce stopSyncthing() via REST-API (for testing) +//#define LIB_SYNCTHING_CONNECTOR_ENFORCE_STOP_VIA_API + #ifdef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS #include @@ -224,7 +227,7 @@ void SyncthingProcess::stopSyncthing(SyncthingConnection *currentConnection) { m_manuallyStopped = true; m_killTimer.start(); -#ifdef PLATFORM_UNIX +#if defined(PLATFORM_UNIX) && !defined(LIB_SYNCTHING_CONNECTOR_ENFORCE_STOP_VIA_API) Q_UNUSED(currentConnection) #else if (currentConnection && !currentConnection->syncthingUrl().isEmpty() && !currentConnection->apiKey().isEmpty() && currentConnection->isLocal()) { diff --git a/widgets/CMakeLists.txt b/widgets/CMakeLists.txt index bf208a1..8a6fe8e 100644 --- a/widgets/CMakeLists.txt +++ b/widgets/CMakeLists.txt @@ -74,8 +74,8 @@ set(REQUIRED_ICONS go-up) # find c++utilities -find_package(c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.0.0 REQUIRED) -use_cpp_utilities() +find_package(c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.11.0 REQUIRED) +use_cpp_utilities(VISIBILITY PUBLIC) # find qtutilities find_package(qtutilities${CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES} 6.3.0 REQUIRED) diff --git a/widgets/misc/syncthingkiller.cpp b/widgets/misc/syncthingkiller.cpp index 3effa3a..7d5fa34 100644 --- a/widgets/misc/syncthingkiller.cpp +++ b/widgets/misc/syncthingkiller.cpp @@ -13,18 +13,18 @@ using namespace CppUtilities; namespace QtGui { -SyncthingKiller::SyncthingKiller(std::vector &&processes) +SyncthingKiller::SyncthingKiller(std::vector &&processes) : m_processes(processes) { - for (auto *process : m_processes) { - process->stopSyncthing(); + for (const auto [process, connection] : m_processes) { + process->stopSyncthing(connection); connect(process, &SyncthingProcess::confirmKill, this, &SyncthingKiller::confirmKill); } } void SyncthingKiller::waitForFinished() { - for (auto *process : m_processes) { + for (const auto [process, connection] : m_processes) { if (!process->isRunning()) { continue; } diff --git a/widgets/misc/syncthingkiller.h b/widgets/misc/syncthingkiller.h index f7e94ae..21a6dd6 100644 --- a/widgets/misc/syncthingkiller.h +++ b/widgets/misc/syncthingkiller.h @@ -8,15 +8,28 @@ #include namespace Data { +class SyncthingConnection; class SyncthingProcess; -} +} // namespace Data 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 { Q_OBJECT public: - SyncthingKiller(std::vector &&processes); + explicit SyncthingKiller(std::vector &&processes); Q_SIGNALS: void ignored(); @@ -28,7 +41,7 @@ private Q_SLOTS: void confirmKill() const; private: - std::vector m_processes; + std::vector m_processes; }; } // namespace QtGui diff --git a/widgets/misc/syncthinglauncher.cpp b/widgets/misc/syncthinglauncher.cpp index 36895a0..6e6da65 100644 --- a/widgets/misc/syncthinglauncher.cpp +++ b/widgets/misc/syncthinglauncher.cpp @@ -7,6 +7,7 @@ #include #include #include +#include using namespace std; using namespace std::placeholders; @@ -30,6 +31,8 @@ SyncthingLauncher *SyncthingLauncher::s_mainInstance = nullptr; */ SyncthingLauncher::SyncthingLauncher(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 , m_libsyncthingLogLevel(LibSyncthing::LogLevel::Info) #endif @@ -94,7 +97,7 @@ void SyncthingLauncher::launch(const QString &program, const QStringList &argume if (isRunning() || m_stopFuture.isRunning()) { return; } - m_manuallyStopped = false; + resetState(); // start external process if (!program.isEmpty()) { @@ -148,7 +151,7 @@ void SyncthingLauncher::launch(const LibSyncthing::RuntimeOptions &runtimeOption if (isRunning() || m_stopFuture.isRunning()) { return; } - m_manuallyStopped = false; + resetState(); m_startFuture = QtConcurrent::run(std::bind(&SyncthingLauncher::runLibSyncthing, this, runtimeOptions)); } #endif @@ -207,6 +210,16 @@ void SyncthingLauncher::handleProcessFinished(int exitCode, QProcess::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 static const char *const logLevelStrings[] = { "[DEBUG] ", @@ -234,6 +247,7 @@ void SyncthingLauncher::handleLoggingCallback(LibSyncthing::LogLevel level, cons void SyncthingLauncher::handleOutputAvailable(QByteArray &&data) { + m_guiListeningUrlSearch(data.data(), static_cast(data.size())); if (isEmittingOutput()) { emit outputAvailable(data); } 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 void SyncthingLauncher::runLibSyncthing(const LibSyncthing::RuntimeOptions &runtimeOptions) { diff --git a/widgets/misc/syncthinglauncher.h b/widgets/misc/syncthinglauncher.h index e3e4b70..313c5be 100644 --- a/widgets/misc/syncthinglauncher.h +++ b/widgets/misc/syncthinglauncher.h @@ -9,8 +9,11 @@ #include +#include + #include #include +#include namespace Settings { struct Launcher; @@ -26,6 +29,8 @@ class SYNCTHINGWIDGETS_EXPORT SyncthingLauncher : public QObject { Q_PROPERTY(CppUtilities::DateTime activeSince READ activeSince) Q_PROPERTY(bool manuallyStopped READ isManuallyStopped) Q_PROPERTY(bool emittingOutput READ isEmittingOutput WRITE setEmittingOutput) + Q_PROPERTY(QUrl guiUrl READ guiUrl WRITE guiUrlChanged) + Q_PROPERTY(SyncthingProcess *process READ process) public: explicit SyncthingLauncher(QObject *parent = nullptr); @@ -37,6 +42,9 @@ public: bool isEmittingOutput() const; void setEmittingOutput(bool emittingOutput); QString errorString() const; + QUrl guiUrl() const; + SyncthingProcess *process(); + const SyncthingProcess *process() const; #ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING LibSyncthing::LogLevel libSyncthingLogLevel() const; void setLibSyncthingLogLevel(LibSyncthing::LogLevel logLevel); @@ -55,6 +63,7 @@ Q_SIGNALS: void outputAvailable(const QByteArray &data); void exited(int exitCode, QProcess::ExitStatus exitStatus); void errorOccurred(QProcess::ProcessError error); + void guiUrlChanged(const QUrl &newUrl); public Q_SLOTS: void launch(const QString &program, const QStringList &arguments); @@ -75,15 +84,19 @@ private Q_SLOTS: #endif private: + void resetState(); #ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING void handleLoggingCallback(LibSyncthing::LogLevel, const char *message, std::size_t messageSize); #endif void handleOutputAvailable(QByteArray &&data); + void handleGuiListeningUrlFound(CppUtilities::BufferSearch &bufferSearch, std::string &&searchResult); SyncthingProcess m_process; + QUrl m_guiListeningUrl; QFuture m_startFuture; QFuture m_stopFuture; QByteArray m_outputBuffer; + CppUtilities::BufferSearch m_guiListeningUrlSearch; CppUtilities::DateTime m_futureStarted; #ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING LibSyncthing::LogLevel m_libsyncthingLogLevel; @@ -138,6 +151,24 @@ inline QString SyncthingLauncher::errorString() const 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 /// \brief Returns the log level used for libsyncthing. inline LibSyncthing::LogLevel SyncthingLauncher::libSyncthingLogLevel() const diff --git a/widgets/settings/settings.cpp b/widgets/settings/settings.cpp index 5b48fe1..67bce9d 100644 --- a/widgets/settings/settings.cpp +++ b/widgets/settings/settings.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD #include #endif @@ -20,6 +21,8 @@ #include #endif +#include + #include #include #include @@ -29,11 +32,13 @@ #include #include +#include #include #include using namespace std; using namespace Data; +using namespace CppUtilities::EscapeCodes; using namespace QtUtilities; namespace Settings { @@ -64,15 +69,53 @@ SyncthingProcess &Launcher::toolProcess(const QString &tool) return toolProcesses[tool]; } -std::vector Launcher::allProcesses() +static bool isLocalAndMatchesPort(const Data::SyncthingConnectionSettings &settings, int port) { - vector processes; - processes.reserve(1 + toolProcesses.size()); - if (auto *const syncthingProcess = SyncthingProcess::mainInstance()) { - processes.push_back(syncthingProcess); + if (settings.syncthingUrl.isEmpty() || settings.apiKey.isEmpty()) { + return false; } - for (auto &process : toolProcesses) { - processes.push_back(&process.second); + const auto url = QUrl(settings.syncthingUrl); + 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 Launcher::allProcesses() +{ + auto processes = std::vector(); + 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; } diff --git a/widgets/settings/settings.h b/widgets/settings/settings.h index 1c0a733..067172a 100644 --- a/widgets/settings/settings.h +++ b/widgets/settings/settings.h @@ -29,11 +29,16 @@ class QtSettings; namespace Data { class SyncthingProcess; +class SyncthingLauncher; class SyncthingNotifier; class SyncthingConnection; class SyncthingService; } // namespace Data +namespace QtGui { +struct ProcessWithConnection; +} + namespace Settings { struct SYNCTHINGWIDGETS_EXPORT Connection { @@ -94,7 +99,8 @@ struct SYNCTHINGWIDGETS_EXPORT Launcher { #endif static Data::SyncthingProcess &toolProcess(const QString &tool); - static std::vector allProcesses(); + static Data::SyncthingConnection *connectionForLauncher(Data::SyncthingLauncher *launcher); + static std::vector allProcesses(); void autostart() const; static void terminate(); struct SYNCTHINGWIDGETS_EXPORT LauncherStatus { diff --git a/widgets/settings/settingsdialog.cpp b/widgets/settings/settingsdialog.cpp index 2c7b687..777ddd5 100644 --- a/widgets/settings/settingsdialog.cpp +++ b/widgets/settings/settingsdialog.cpp @@ -1188,7 +1188,7 @@ void LauncherOptionPage::stop() m_process->stopSyncthing(); } if (m_launcher) { - m_launcher->terminate(); + m_launcher->terminate(Launcher::connectionForLauncher(m_launcher)); } } }