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.
This commit is contained in:
Martchus 2021-07-15 02:38:26 +02:00
parent 69f466be66
commit 4c6315b450
9 changed files with 137 additions and 21 deletions

View File

@ -4,6 +4,9 @@
#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
#include <c++utilities/io/ansiescapecodes.h>
@ -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()) {

View File

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

View File

@ -13,18 +13,18 @@ using namespace CppUtilities;
namespace QtGui {
SyncthingKiller::SyncthingKiller(std::vector<SyncthingProcess *> &&processes)
SyncthingKiller::SyncthingKiller(std::vector<ProcessWithConnection> &&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;
}

View File

@ -8,15 +8,28 @@
#include <vector>
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<Data::SyncthingProcess *> &&processes);
explicit SyncthingKiller(std::vector<ProcessWithConnection> &&processes);
Q_SIGNALS:
void ignored();
@ -28,7 +41,7 @@ private Q_SLOTS:
void confirmKill() const;
private:
std::vector<Data::SyncthingProcess *> m_processes;
std::vector<ProcessWithConnection> m_processes;
};
} // namespace QtGui

View File

@ -7,6 +7,7 @@
#include <algorithm>
#include <functional>
#include <limits>
#include <string_view>
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<std::size_t>(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)
{

View File

@ -9,8 +9,11 @@
#include <syncthingconnector/syncthingprocess.h>
#include <c++utilities/io/buffersearch.h>
#include <QByteArray>
#include <QFuture>
#include <QUrl>
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<void> m_startFuture;
QFuture<void> 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

View File

@ -8,6 +8,7 @@
#include <syncthingconnector/syncthingconnectionsettings.h>
#include <syncthingconnector/syncthingnotifier.h>
#include <syncthingconnector/syncthingprocess.h>
#include <syncthingconnector/utils.h>
#ifdef LIB_SYNCTHING_CONNECTOR_SUPPORT_SYSTEMD
#include <syncthingconnector/syncthingservice.h>
#endif
@ -20,6 +21,8 @@
#include <qtutilities/misc/dbusnotification.h>
#endif
#include <c++utilities/io/ansiescapecodes.h>
#include <QApplication>
#include <QCursor>
#include <QFile>
@ -29,11 +32,13 @@
#include <QSslError>
#include <QStringBuilder>
#include <iostream>
#include <type_traits>
#include <unordered_map>
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<SyncthingProcess *> Launcher::allProcesses()
static bool isLocalAndMatchesPort(const Data::SyncthingConnectionSettings &settings, int port)
{
vector<SyncthingProcess *> 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<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;
}

View File

@ -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<Data::SyncthingProcess *> allProcesses();
static Data::SyncthingConnection *connectionForLauncher(Data::SyncthingLauncher *launcher);
static std::vector<QtGui::ProcessWithConnection> allProcesses();
void autostart() const;
static void terminate();
struct SYNCTHINGWIDGETS_EXPORT LauncherStatus {

View File

@ -1188,7 +1188,7 @@ void LauncherOptionPage::stop()
m_process->stopSyncthing();
}
if (m_launcher) {
m_launcher->terminate();
m_launcher->terminate(Launcher::connectionForLauncher(m_launcher));
}
}
}