diff --git a/connector/CMakeLists.txt b/connector/CMakeLists.txt index 07fe79c..5d36d7b 100644 --- a/connector/CMakeLists.txt +++ b/connector/CMakeLists.txt @@ -48,6 +48,34 @@ use_cpp_utilities(VISIBILITY PUBLIC) find_package(qtutilities${CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES} 6.3.0 REQUIRED) use_qt_utilities(ONLY_HEADERS VISIBILITY PUBLIC) +# find boost libraries +option(USE_BOOST_PROCESS "enables Boost.Process for improved behavior of the launcher" ON) +if (USE_BOOST_PROCESS) + option(BOOST_STATIC_LINKAGE "${STATIC_LINKAGE}" "link statically against Boost (instead of dynamically)") + set(Boost_USE_MULTITHREADED ON) + if (BOOST_STATIC_LINKAGE) + set(Boost_USE_STATIC_LIBS ON) + endif () + # add Boost::boost target which represents include directory for header-only deps add Boost::filesystem when building for + # Windows as it is needed by Boost.Process there + set(BOOST_ARGS REQUIRED) + if (WIN32) + list(APPEND BOOST_ARGS COMPONENTS filesystem) + endif () + use_package(TARGET_NAME Boost::boost PACKAGE_NAME Boost PACKAGE_ARGS "${BOOST_ARGS}") + if (WIN32) + use_package(TARGET_NAME Boost::filesystem PACKAGE_NAME Boost PACKAGE_ARGS "${BOOST_ARGS}") + endif () + list(APPEND META_PUBLIC_COMPILE_DEFINITIONS ${META_PROJECT_VARNAME_UPPER}_BOOST_PROCESS) + if (MINGW) + # workaround https://github.com/boostorg/process/issues/96 + set_property( + SOURCE syncthingprocess.cpp + APPEND + PROPERTY COMPILE_DEFINITIONS BOOST_USE_WINDOWS_H WIN32_LEAN_AND_MEAN) + endif () +endif () + # link also explicitely against the following Qt modules list(APPEND ADDITIONAL_QT_MODULES Network) set(META_PUBLIC_QT_MODULES Core ${ADDITIONAL_QT_MODULES}) diff --git a/connector/syncthingnotifier.cpp b/connector/syncthingnotifier.cpp index a90e274..ce60fbe 100644 --- a/connector/syncthingnotifier.cpp +++ b/connector/syncthingnotifier.cpp @@ -42,7 +42,7 @@ SyncthingNotifier::SyncthingNotifier(const SyncthingConnection &connection, QObj connect(&connection, &SyncthingConnection::newDevAvailable, this, &SyncthingNotifier::handleNewDevEvent); connect(&connection, &SyncthingConnection::newDirAvailable, this, &SyncthingNotifier::handleNewDirEvent); if (m_process) { - connect(m_process, &QProcess::errorOccurred, this, &SyncthingNotifier::handleSyncthingProcessError); + connect(m_process, &SyncthingProcess::errorOccurred, this, &SyncthingNotifier::handleSyncthingProcessError); } } @@ -101,16 +101,17 @@ void SyncthingNotifier::handleSyncthingProcessError(QProcess::ProcessError proce return; } + const auto error = m_process->errorString(); switch (processError) { case QProcess::FailedToStart: - emit syncthingProcessError( - tr("Failed to start Syncthing"), tr("Maybe the configured binary path is wrong or the binary is not marked as executable.")); + emit syncthingProcessError(tr("Failed to start Syncthing"), + error.isEmpty() ? tr("Maybe the configured binary path is wrong or the binary is not marked as executable.") : error); break; case QProcess::Crashed: - emit syncthingProcessError(tr("Syncthing crashed with exit code %1").arg(m_process->exitCode()), QString()); + emit syncthingProcessError(tr("Syncthing crashed"), error); break; default: - emit syncthingProcessError(tr("Syncthing launcher error occurred"), m_process->errorString()); + emit syncthingProcessError(tr("Syncthing launcher error occurred"), error); } } diff --git a/connector/syncthingprocess.cpp b/connector/syncthingprocess.cpp index 5f1deb1..80e2ad0 100644 --- a/connector/syncthingprocess.cpp +++ b/connector/syncthingprocess.cpp @@ -2,10 +2,69 @@ #include +#ifdef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#endif + using namespace CppUtilities; namespace Data { +/// \cond +#ifdef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS +struct SyncthingProcessInternalData : std::enable_shared_from_this { + static constexpr std::size_t bufferCapacity = 0x1000; + static_assert(SyncthingProcessInternalData::bufferCapacity <= std::numeric_limits::max()); + + explicit SyncthingProcessInternalData(boost::asio::io_context &ioc); + struct Lock { + explicit Lock(const std::weak_ptr &weak); + operator bool() const; + std::shared_ptr process; + std::unique_lock lock; + }; + + std::mutex mutex; + QString program; + QStringList arguments; + boost::process::group group; + boost::process::child child; + boost::process::async_pipe pipe; + char buffer[bufferCapacity]; + std::atomic_size_t bytesBuffered = 0; + std::size_t bytesRead = 0; + QProcess::ProcessState state = QProcess::NotRunning; +}; + +struct SyncthingProcessIOHandler { + explicit SyncthingProcessIOHandler(); + ~SyncthingProcessIOHandler(); + + boost::asio::io_context ioc; + boost::asio::executor_work_guard guard; + std::thread t; +}; +#endif +/// \endcond + SyncthingProcess *SyncthingProcess::s_mainInstance = nullptr; /*! @@ -20,18 +79,35 @@ SyncthingProcess *SyncthingProcess::s_mainInstance = nullptr; * \brief Constructs a new Syncthing process. */ SyncthingProcess::SyncthingProcess(QObject *parent) - : QProcess(parent) + : SyncthingProcessBase(parent) , m_manuallyStopped(true) { m_killTimer.setInterval(3000); m_killTimer.setSingleShot(true); +#ifndef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS setProcessChannelMode(QProcess::MergedChannels); +#endif connect(this, &SyncthingProcess::started, this, &SyncthingProcess::handleStarted); connect(this, static_cast(&SyncthingProcess::finished), this, &SyncthingProcess::handleFinished); connect(&m_killTimer, &QTimer::timeout, this, &SyncthingProcess::confirmKill); } +/*! + * \brief Destroys the process. + */ +SyncthingProcess::~SyncthingProcess() +{ +#ifdef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS + // block until all callbacks have been processed + if (!m_process) { + return; + } + const auto lock = std::lock_guard(m_process->mutex); + m_process.reset(); +#endif +} + /*! * \brief Splits the given arguments similar to how a shell would split it. So whitespaces are considered seperators unless quotes are used. */ @@ -129,7 +205,7 @@ void SyncthingProcess::startSyncthing(const QString &program, const QStringList } m_manuallyStopped = false; m_killTimer.stop(); - start(program, arguments, QProcess::ReadOnly); + start(program, arguments, QIODevice::ReadOnly); } /*! @@ -137,9 +213,6 @@ void SyncthingProcess::startSyncthing(const QString &program, const QStringList */ void SyncthingProcess::stopSyncthing() { - if (!isRunning()) { - return; - } m_manuallyStopped = true; m_killTimer.start(); terminate(); @@ -150,9 +223,6 @@ void SyncthingProcess::stopSyncthing() */ void SyncthingProcess::killSyncthing() { - if (!isRunning()) { - return; - } m_manuallyStopped = true; m_killTimer.stop(); kill(); @@ -183,4 +253,342 @@ void SyncthingProcess::killToRestart() } } +/// \cond +/// \remarks The functions below are for using Boost.Process (instead of QProcess) to be able to +/// terminate the process better by using a group. +#ifdef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS +SyncthingProcessIOHandler::SyncthingProcessIOHandler() + : ioc() + , guard(boost::asio::make_work_guard(ioc)) + , t([this] { ioc.run(); }) +{ +} + +SyncthingProcessIOHandler::~SyncthingProcessIOHandler() +{ + ioc.stop(); + guard.reset(); + t.join(); +} + +Data::SyncthingProcessInternalData::SyncthingProcessInternalData(boost::asio::io_context &ioc) + : pipe(ioc) +{ +} + +inline Data::SyncthingProcessInternalData::Lock::Lock(const std::weak_ptr &weak) + : process(weak.lock()) + , lock(process ? decltype(lock)(process->mutex, std::try_to_lock) : decltype(lock)()) +{ +} + +inline Data::SyncthingProcessInternalData::Lock::operator bool() const +{ + return process && lock; +} + +void SyncthingProcess::handleError(QProcess::ProcessError error, const QString &errorMessage, bool closed) +{ + setErrorString(errorMessage); + errorOccurred(error); + if (closed) { + setOpenMode(QIODevice::NotOpen); + } +} + +QProcess::ProcessState SyncthingProcess::state() const +{ + return !m_process ? QProcess::NotRunning : m_process->state; +} + +void SyncthingProcess::start(const QString &program, const QStringList &arguments, QIODevice::OpenMode openMode) +{ + // ensure the dev is only opened in read-only mode because writing is not implemented here + if (openMode != QIODevice::ReadOnly) { + setErrorString(QStringLiteral("only read-only openmode supported")); + std::cerr << EscapeCodes::Phrases::Error << "Unable to launch process: only read-only openmode supported" << EscapeCodes::Phrases::End; + emit errorOccurred(QProcess::FailedToStart); + return; + } + + // handle current/previous process + if (m_process) { + const auto lock = std::lock_guard(m_process->mutex); + if (m_process->state != QProcess::NotRunning) { + setErrorString(QStringLiteral("process is still running")); + std::cerr << EscapeCodes::Phrases::Error << "Unable to launch process: previous process still running" << EscapeCodes::Phrases::End; + emit errorOccurred(QProcess::FailedToStart); + return; + } + m_process.reset(); + } + + // setup new handler/process + if (!m_handler) { + m_handler = std::make_unique(); + } + m_process = std::make_shared(m_handler->ioc); + emit stateChanged(m_process->state = QProcess::Starting); + + // convert args + auto args = std::vector(); + args.reserve(1 + static_cast(arguments.size())); + args.emplace_back(program.toStdString()); + for (const auto &arg : arguments) { + args.emplace_back(arg.toStdString()); + } + m_process->program = program; + m_process->arguments = arguments; + + // start the process within a new process group (or job object under Windows) + try { + m_process->child = boost::process::child( + m_handler->ioc, m_process->group, args, (boost::process::std_out & boost::process::std_err) > m_process->pipe, + boost::process::extend::on_success = + [this, maybeProcess = m_process->weak_from_this()](auto &executor) { + std::cerr << EscapeCodes::Phrases::Info << "Launched process, PID: " + << executor +#ifdef PLATFORM_WINDOWS + .proc_info.dwProcessId +#else + .pid +#endif + << EscapeCodes::Phrases::End; + if (const auto lock = SyncthingProcessInternalData::Lock(maybeProcess)) { + emit stateChanged(m_process->state = QProcess::Running); + emit started(); + } + }, + boost::process::on_exit = + [this, maybeProcess = m_process->weak_from_this()](int rc, const std::error_code &ec) { + const auto lock = SyncthingProcessInternalData::Lock(maybeProcess); + if (!lock) { + return; + } + handleLeftoverProcesses(); + emit stateChanged(m_process->state = QProcess::NotRunning); + emit finished(rc, rc != 0 ? QProcess::CrashExit : QProcess::NormalExit); + if (ec) { + const auto msg = ec.message(); + std::cerr << EscapeCodes::Phrases::Error << "Launched process " << m_process->child.native_handle() + << " exited with error: " << msg << EscapeCodes::Phrases::End; + QMetaObject::invokeMethod(this, "handleError", Qt::QueuedConnection, Q_ARG(QProcess::ProcessError, QProcess::Crashed), + Q_ARG(QString, QString::fromStdString(msg)), Q_ARG(bool, false)); + } + }, + boost::process::extend::on_error = + [this, maybeProcess = m_process->weak_from_this()](auto &, const std::error_code &ec) { + const auto lock = SyncthingProcessInternalData::Lock(maybeProcess); + if (!lock) { + return; + } + handleLeftoverProcesses(); + const auto started = m_process->state == QProcess::Running; + if (m_process->state != QProcess::NotRunning) { + emit stateChanged(m_process->state = QProcess::NotRunning); + } + if (started) { + emit finished(0, QProcess::CrashExit); + } + const auto error = ec == std::errc::timed_out || ec == std::errc::stream_timeout ? QProcess::Timedout : QProcess::Crashed; + const auto msg = ec.message(); + std::cerr << EscapeCodes::Phrases::Error << "Unable to launch process: " << msg << EscapeCodes::Phrases::End; + QMetaObject::invokeMethod(this, "handleError", Qt::QueuedConnection, Q_ARG(QProcess::ProcessError, error), + Q_ARG(QString, QString::fromStdString(msg)), Q_ARG(bool, false)); + }); + } catch (const boost::process::process_error &e) { + std::cerr << EscapeCodes::Phrases::Error << "Unable to launch process: " << e.what() << EscapeCodes::Phrases::End; + emit stateChanged(m_process->state = QProcess::NotRunning); + handleError(QProcess::FailedToStart, QString::fromUtf8(e.what()), false); + return; + } + + // start reading the process' output + open(QIODevice::ReadOnly); + bufferOutput(); +} + +void SyncthingProcess::terminate() +{ + if (!m_process) { + return; + } +#ifdef PLATFORM_UNIX + auto lock = std::unique_lock(m_process->mutex); + if (!m_process->group.valid()) { + return; + } + const auto groupId = m_process->group.native_handle(); + lock.unlock(); + if (::killpg(groupId, SIGTERM) == -1) { + if (const auto ec = boost::process::detail::get_last_error(); ec != std::errc::no_such_process) { + const auto msg = ec.message(); + std::cerr << EscapeCodes::Phrases::Error << "Unable to kill process group " << groupId << ": " << msg << EscapeCodes::Phrases::End; + setErrorString(QString::fromStdString(msg)); + errorOccurred(QProcess::UnknownError); + } + } +#else + // there seems no way to stop the process group gracefully under Windows so just kill it + // note: Posting a WM_CLOSE message like QProcess would attempt doesn't work for Syncthing. + kill(); +#endif +} + +void SyncthingProcess::kill() +{ + if (!m_process) { + return; + } + auto ec = std::error_code(); + auto lock = std::unique_lock(m_process->mutex); + if (!m_process->group.valid()) { + return; + } + const auto groupId = m_process->group.native_handle(); + m_process->group.terminate(ec); + lock.unlock(); + if (ec && ec != std::errc::no_such_process) { + const auto msg = ec.message(); + std::cerr << EscapeCodes::Phrases::Error << "Unable to kill process group " << groupId << ": " << msg << EscapeCodes::Phrases::End; + setErrorString(QString::fromStdString(msg)); + errorOccurred(QProcess::UnknownError); + } + // note: No need to emit finished() signal here, the on_exit handler will fire + // also in case of a forceful termination. +} + +void SyncthingProcess::bufferOutput() +{ + m_process->pipe.async_read_some(boost::asio::buffer(m_process->buffer, m_process->bufferCapacity), + [this, maybeProcess = m_process->weak_from_this()](const boost::system::error_code &ec, auto bytesRead) { + const auto lock = SyncthingProcessInternalData::Lock(maybeProcess); + if (!lock) { + return; + } + m_process->bytesBuffered = bytesRead; + if (ec == boost::asio::error::eof +#ifdef PLATFORM_WINDOWS // looks like we're getting broken pipe (and not just eof) under Windows when stopping the process + || ec == boost::asio::error::broken_pipe +#endif + ) { + m_process->pipe.async_close(); + setOpenMode(QIODevice::NotOpen); + } else if (ec) { + const auto msg = ec.message(); + std::cerr << EscapeCodes::Phrases::Error << "Unable to read output of process " << m_process->child.native_handle() << ": " << msg + << EscapeCodes::Phrases::End; + QMetaObject::invokeMethod(this, "handleError", Qt::QueuedConnection, Q_ARG(QProcess::ProcessError, QProcess::ReadError), + Q_ARG(QString, QString::fromStdString(msg)), Q_ARG(bool, true)); + } + if (!ec || bytesRead) { + emit readyRead(); + } + }); +} + +void SyncthingProcess::handleLeftoverProcesses() +{ + if (!m_process->group.valid()) { + return; + } + auto ec = std::error_code(); + m_process->group.terminate(ec); + if (ec && ec != std::errc::no_such_process) { + std::cerr << EscapeCodes::Phrases::Error << "Unable to kill leftover processes in group " << m_process->group.native_handle() << ": " + << ec.message() << EscapeCodes::Phrases::End; + } + m_process->group.wait(ec); + if (ec && ec != std::errc::no_such_process) { + std::cerr << EscapeCodes::Phrases::Error << "Unable to wait for leftover processes in group " << m_process->group.native_handle() << ": " + << ec.message() << EscapeCodes::Phrases::End; + } +} + +qint64 SyncthingProcess::bytesAvailable() const +{ + return (m_process ? static_cast(m_process->bytesBuffered) : 0) + QIODevice::bytesAvailable(); +} + +void SyncthingProcess::close() +{ + emit aboutToClose(); + if (m_process) { + const auto lock = std::lock_guard(m_process->mutex); + m_process->pipe.async_close(); + kill(); + } + setOpenMode(QIODevice::NotOpen); +} + +int SyncthingProcess::exitCode() const +{ + return m_process ? m_process->child.exit_code() : 0; +} + +bool SyncthingProcess::waitForFinished(int msecs) +{ + if (!m_process) { + return false; + } + auto ec = std::error_code(); + if (msecs < 0) { + m_process->group.wait(ec); + } else { + m_process->group.wait_for(std::chrono::milliseconds(msecs), ec); + } + return !ec || ec == std::errc::no_such_process || ec == std::errc::no_child_process; +} + +qint64 SyncthingProcess::processId() const +{ + return m_process ? m_process->child.id() : -1; +} + +QString SyncthingProcess::program() const +{ + return m_process ? m_process->program : QString(); +} + +QStringList SyncthingProcess::arguments() const +{ + return m_process ? m_process->arguments : QStringList(); +} + +qint64 SyncthingProcess::readData(char *data, qint64 maxSize) +{ + if (!m_process) { + return -1; + } + if (maxSize < 1) { + return 0; + } + if (!m_process->bytesBuffered) { + bufferOutput(); + return 0; + } + + const auto bytesAvailable = m_process->bytesBuffered - m_process->bytesRead; + if (bytesAvailable > static_cast(maxSize)) { + std::memcpy(data, m_process->buffer + m_process->bytesRead, static_cast(maxSize)); + m_process->bytesRead += static_cast(maxSize); + return maxSize; + } else { + std::memcpy(data, m_process->buffer + m_process->bytesRead, bytesAvailable); + m_process->bytesBuffered = 0; + m_process->bytesRead = 0; + bufferOutput(); + return static_cast(bytesAvailable); + } +} + +qint64 SyncthingProcess::writeData(const char *data, qint64 len) +{ + Q_UNUSED(data) + Q_UNUSED(len) + return -1; +} +#endif +/// \endcond + } // namespace Data diff --git a/connector/syncthingprocess.h b/connector/syncthingprocess.h index 584c811..73c0f6b 100644 --- a/connector/syncthingprocess.h +++ b/connector/syncthingprocess.h @@ -9,9 +9,21 @@ #include #include +#ifdef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS +#include +#endif + namespace Data { -class LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingProcess : public QProcess { +#ifdef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS +struct SyncthingProcessInternalData; +struct SyncthingProcessIOHandler; +using SyncthingProcessBase = QIODevice; +#else +using SyncthingProcessBase = QProcess; +#endif + +class LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingProcess : public SyncthingProcessBase { Q_OBJECT Q_PROPERTY(bool running READ isRunning) Q_PROPERTY(CppUtilities::DateTime activeSince READ activeSince) @@ -19,6 +31,7 @@ class LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingProcess : public QProcess { public: explicit SyncthingProcess(QObject *parent = nullptr); + ~SyncthingProcess() override; bool isRunning() const; CppUtilities::DateTime activeSince() const; bool isActiveFor(unsigned int atLeastSeconds) const; @@ -26,26 +39,62 @@ public: static SyncthingProcess *mainInstance(); static void setMainInstance(SyncthingProcess *mainInstance); static QStringList splitArguments(const QString &arguments); - -Q_SIGNALS: - void confirmKill(); +#ifdef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS + QProcess::ProcessState state() const; + void start(const QString &program, const QStringList &arguments, QIODevice::OpenMode openMode = QIODevice::ReadOnly); + qint64 bytesAvailable() const override; + void close() override; + int exitCode() const; + bool waitForFinished(int msecs = 30000); + qint64 processId() const; + QString program() const; + QStringList arguments() const; +#endif public Q_SLOTS: void restartSyncthing(const QString &program, const QStringList &arguments); void startSyncthing(const QString &program, const QStringList &arguments); void stopSyncthing(); void killSyncthing(); +#ifdef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS + void terminate(); + void kill(); +#endif + +Q_SIGNALS: +#ifdef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS + void started(); + void finished(int exitCode, QProcess::ExitStatus exitStatus); + void errorOccurred(QProcess::ProcessError error); + void stateChanged(QProcess::ProcessState newState); +#endif + void confirmKill(); + +#ifdef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS +protected: + qint64 readData(char *data, qint64 maxSize) override; + qint64 writeData(const char *data, qint64 len) override; +#endif private Q_SLOTS: void handleStarted(); void handleFinished(int exitCode, QProcess::ExitStatus exitStatus); void killToRestart(); +#ifdef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS + void handleError(QProcess::ProcessError error, const QString &errorMessage, bool closed); + void bufferOutput(); + void handleLeftoverProcesses(); +#endif private: QString m_program; QStringList m_arguments; CppUtilities::DateTime m_activeSince; QTimer m_killTimer; +#ifdef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS + std::shared_ptr m_process; + std::unique_ptr m_handler; +#endif bool m_manuallyStopped; static SyncthingProcess *s_mainInstance; }; diff --git a/scripts/dummy.sh b/scripts/dummy.sh index aefaa06..3fc061b 100755 --- a/scripts/dummy.sh +++ b/scripts/dummy.sh @@ -7,7 +7,9 @@ function handle_int { } trap "handle_int" SIGINT SIGTERM +i=0 while [[ true ]]; do - echo $RANDOM + echo "$i : $RANDOM" + i=$((i + 1)) sleep 1 done diff --git a/testhelper/syncthingtestinstance.cpp b/testhelper/syncthingtestinstance.cpp index c0297ac..9d47809 100644 --- a/testhelper/syncthingtestinstance.cpp +++ b/testhelper/syncthingtestinstance.cpp @@ -94,13 +94,14 @@ void SyncthingTestInstance::start() */ void SyncthingTestInstance::stop() { - if (m_syncthingProcess.state() == QProcess::Running) { + if (m_syncthingProcess.isRunning()) { cerr << "\n - Waiting for Syncthing to terminate ..." << endl; m_syncthingProcess.terminate(); m_syncthingProcess.waitForFinished(); } if (m_syncthingProcess.isOpen()) { cerr << "\n - Syncthing terminated with exit code " << m_syncthingProcess.exitCode() << ".\n"; + /* const auto stdOut(m_syncthingProcess.readAllStandardOutput()); if (!stdOut.isEmpty()) { cerr << "\n - Syncthing stdout during the testrun:\n" << stdOut.data(); @@ -114,6 +115,7 @@ void SyncthingTestInstance::stop() cerr << "\n - Syncthing exited: " << stdOut.count("INFO: Syncthing exited: exit status") << " times"; cerr << "\n - Syncthing panicked: " << stdOut.count("WARNING: Panic detected") << " times"; } + */ } } @@ -126,7 +128,7 @@ void SyncthingTestInstance::setInterleavedOutputEnabled(bool interleavedOutputEn return; } m_interleavedOutput = interleavedOutputEnabled; - m_syncthingProcess.setProcessChannelMode(interleavedOutputEnabled ? QProcess::ForwardedChannels : QProcess::SeparateChannels); + //m_syncthingProcess.setProcessChannelMode(interleavedOutputEnabled ? QProcess::ForwardedChannels : QProcess::SeparateChannels); } /*! diff --git a/testhelper/syncthingtestinstance.h b/testhelper/syncthingtestinstance.h index 66c47ea..095371e 100644 --- a/testhelper/syncthingtestinstance.h +++ b/testhelper/syncthingtestinstance.h @@ -25,7 +25,7 @@ public: const QString &apiKey() const; const QString &syncthingPort() const; QCoreApplication &application(); - QProcess &syncthingProcess(); + Data::SyncthingProcess &syncthingProcess(); public Q_SLOTS: void start(); @@ -57,7 +57,7 @@ inline QCoreApplication &SyncthingTestInstance::application() return m_app; } -inline QProcess &SyncthingTestInstance::syncthingProcess() +inline Data::SyncthingProcess &SyncthingTestInstance::syncthingProcess() { return m_syncthingProcess; } diff --git a/testhelper/tests/manualtesting.cpp b/testhelper/tests/manualtesting.cpp index 7609f66..cf8e2e9 100644 --- a/testhelper/tests/manualtesting.cpp +++ b/testhelper/tests/manualtesting.cpp @@ -19,8 +19,9 @@ int main(int argc, char **argv) SyncthingTestInstance testInstance; auto &syncthingProcess(testInstance.syncthingProcess()); - syncthingProcess.setProcessChannelMode(QProcess::ForwardedChannels); - QObject::connect(&syncthingProcess, static_cast(&QProcess::finished), &QCoreApplication::exit); + //syncthingProcess.setProcessChannelMode(QProcess::ForwardedChannels); + QObject::connect(&syncthingProcess, static_cast(&Data::SyncthingProcess::finished), + &QCoreApplication::exit); testInstance.start(); const int res = testInstance.application().exec(); diff --git a/widgets/misc/syncthingkiller.cpp b/widgets/misc/syncthingkiller.cpp index 5fb39e5..3effa3a 100644 --- a/widgets/misc/syncthingkiller.cpp +++ b/widgets/misc/syncthingkiller.cpp @@ -59,11 +59,11 @@ void SyncthingKiller::confirmKill() const msgBox->addButton(tr("Keep running"), QMessageBox::RejectRole); msgBox->addButton(tr("Kill process"), QMessageBox::AcceptRole); connect(process, -#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) || QT_DEPRECATED_SINCE(5, 13) +#if !defined(LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS) && (QT_VERSION < QT_VERSION_CHECK(5, 13, 0) || QT_DEPRECATED_SINCE(5, 13)) static_cast( #endif &SyncthingProcess::finished -#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) || QT_DEPRECATED_SINCE(5, 13) +#if !defined(LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS) && (QT_VERSION < QT_VERSION_CHECK(5, 13, 0) || QT_DEPRECATED_SINCE(5, 13)) ) #endif , diff --git a/widgets/misc/syncthinglauncher.cpp b/widgets/misc/syncthinglauncher.cpp index 72dea54..c64f706 100644 --- a/widgets/misc/syncthinglauncher.cpp +++ b/widgets/misc/syncthinglauncher.cpp @@ -39,8 +39,8 @@ SyncthingLauncher::SyncthingLauncher(QObject *parent) connect(&m_process, &SyncthingProcess::readyRead, this, &SyncthingLauncher::handleProcessReadyRead); connect(&m_process, static_cast(&SyncthingProcess::finished), this, &SyncthingLauncher::handleProcessFinished); - connect(&m_process, &SyncthingProcess::stateChanged, this, &SyncthingLauncher::handleProcessStateChanged); - connect(&m_process, &SyncthingProcess::errorOccurred, this, &SyncthingLauncher::errorOccurred); + connect(&m_process, &SyncthingProcess::stateChanged, this, &SyncthingLauncher::handleProcessStateChanged, Qt::QueuedConnection); + connect(&m_process, &SyncthingProcess::errorOccurred, this, &SyncthingLauncher::errorOccurred, Qt::QueuedConnection); connect(&m_process, &SyncthingProcess::confirmKill, this, &SyncthingLauncher::confirmKill); } diff --git a/widgets/misc/syncthinglauncher.h b/widgets/misc/syncthinglauncher.h index 3e14439..6e915cb 100644 --- a/widgets/misc/syncthinglauncher.h +++ b/widgets/misc/syncthinglauncher.h @@ -34,6 +34,7 @@ public: bool isManuallyStopped() const; bool isEmittingOutput() const; void setEmittingOutput(bool emittingOutput); + QString errorString() const; #ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING LibSyncthing::LogLevel libSyncthingLogLevel() const; void setLibSyncthingLogLevel(LibSyncthing::LogLevel logLevel); @@ -129,6 +130,12 @@ inline bool SyncthingLauncher::isEmittingOutput() const return m_emittingOutput; } +/// \brief Returns the last error message. +inline QString SyncthingLauncher::errorString() const +{ + return m_process.errorString(); +} + #ifdef SYNCTHINGWIDGETS_USE_LIBSYNCTHING /// \brief Returns the log level used for libsyncthing. inline LibSyncthing::LogLevel SyncthingLauncher::libSyncthingLogLevel() const diff --git a/widgets/settings/settings.h b/widgets/settings/settings.h index 697d307..1c0a733 100644 --- a/widgets/settings/settings.h +++ b/widgets/settings/settings.h @@ -80,7 +80,7 @@ struct SYNCTHINGWIDGETS_EXPORT Launcher { #else QStringLiteral("syncthing"); #endif - QString syncthingArgs = QStringLiteral("-no-browser -no-restart -logflags=3"); + QString syncthingArgs = QStringLiteral("-no-browser -no-console -no-restart -logflags=3"); QHash tools; bool considerForReconnect = false; bool showButton = false; diff --git a/widgets/settings/settingsdialog.cpp b/widgets/settings/settingsdialog.cpp index fbf93e7..f5e89b2 100644 --- a/widgets/settings/settingsdialog.cpp +++ b/widgets/settings/settingsdialog.cpp @@ -1094,25 +1094,32 @@ void LauncherOptionPage::handleSyncthingError(QProcess::ProcessError error) cursor.movePosition(QTextCursor::End); cursor.insertBlock(); - QString errorString; - switch (error) { - case QProcess::FailedToStart: - errorString = tr("failed to start (e.g. executable does not exist or not permission error)"); - break; - case QProcess::Crashed: - errorString = tr("process crashed"); - break; - case QProcess::Timedout: - errorString = tr("timeout error"); - break; - case QProcess::ReadError: - errorString = tr("read error"); - break; - case QProcess::WriteError: - errorString = tr("write error"); - break; - default: - errorString = tr("unknown process error"); + auto errorString = QString(); + if (m_launcher) { + errorString = m_launcher->errorString(); + } else if (m_process) { + errorString = m_process->errorString(); + } + if (errorString.isEmpty()) { + switch (error) { + case QProcess::FailedToStart: + errorString = tr("failed to start (e.g. executable does not exist or not permission error)"); + break; + case QProcess::Crashed: + errorString = tr("process crashed"); + break; + case QProcess::Timedout: + errorString = tr("timeout error"); + break; + case QProcess::ReadError: + errorString = tr("read error"); + break; + case QProcess::WriteError: + errorString = tr("write error"); + break; + default: + errorString = tr("unknown process error"); + } } cursor.insertText(tr("An error occurred when running %1: %2").arg(m_tool.isEmpty() ? QStringLiteral("Syncthing") : m_tool, errorString)); cursor.insertBlock();