Martchus
fe3d7891e9
It looks like `libstdc++` and `libc++` only define this `enum class` member when the `ETIME` macro is defined. To prevent build errors when this macro is not defined it makes sense to check for this error only conditionally.
805 lines
28 KiB
C++
805 lines
28 KiB
C++
#include "./syncthingprocess.h"
|
|
#include "./syncthingconnection.h"
|
|
|
|
#include <QEventLoop>
|
|
#include <QStringBuilder>
|
|
#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>
|
|
|
|
#include <boost/asio/executor_work_guard.hpp>
|
|
#include <boost/asio/io_context.hpp>
|
|
#include <boost/filesystem/path.hpp>
|
|
#include <boost/process/async.hpp>
|
|
#include <boost/process/async_pipe.hpp>
|
|
#include <boost/process/child.hpp>
|
|
#include <boost/process/extend.hpp>
|
|
#include <boost/process/group.hpp>
|
|
#include <boost/process/io.hpp>
|
|
#include <boost/process/locale.hpp>
|
|
#include <boost/process/search_path.hpp>
|
|
#ifdef PLATFORM_WINDOWS
|
|
#include <boost/process/windows.hpp>
|
|
#endif
|
|
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <csignal>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <mutex>
|
|
#include <system_error>
|
|
#include <thread>
|
|
#endif
|
|
|
|
using namespace CppUtilities;
|
|
|
|
namespace Data {
|
|
|
|
#ifdef LIB_SYNCTHING_CONNECTOR_BOOST_PROCESS
|
|
#ifdef PLATFORM_WINDOWS
|
|
#define LIB_SYNCTHING_CONNECTOR_PLATFORM_ARGS boost::process::windows::create_no_window,
|
|
#ifdef Q_CC_MSVC
|
|
// fix crashes using MSVC; for some reason using tStdWString() leads to crashes with Qt 6.5.0 and MSVC 2022
|
|
#define LIB_SYNCTHING_CONNECTOR_STRING_CONVERSION(s) std::wstring(reinterpret_cast<const wchar_t *>(s.utf16()), s.size())
|
|
#else
|
|
#define LIB_SYNCTHING_CONNECTOR_STRING_CONVERSION(s) s.toStdWString()
|
|
#endif
|
|
#else
|
|
#define LIB_SYNCTHING_CONNECTOR_PLATFORM_ARGS
|
|
#define LIB_SYNCTHING_CONNECTOR_STRING_CONVERSION(s) s.toLocal8Bit().toStdString()
|
|
#endif
|
|
|
|
/// \brief Holds data related to the process execution via Boost.Process.
|
|
/// \remarks A new one is created for each process to be started.
|
|
struct SyncthingProcessInternalData : std::enable_shared_from_this<SyncthingProcessInternalData> {
|
|
static constexpr std::size_t bufferCapacity = 0x1000;
|
|
static_assert(SyncthingProcessInternalData::bufferCapacity <= std::numeric_limits<qint64>::max());
|
|
|
|
explicit SyncthingProcessInternalData(boost::asio::io_context &ioc);
|
|
struct Lock {
|
|
explicit Lock(const std::weak_ptr<SyncthingProcessInternalData> &weak);
|
|
operator bool() const;
|
|
std::shared_ptr<SyncthingProcessInternalData> process;
|
|
std::unique_lock<std::mutex> lock;
|
|
};
|
|
|
|
std::mutex mutex;
|
|
QString program;
|
|
QStringList arguments;
|
|
boost::process::group group;
|
|
boost::process::child child;
|
|
boost::process::async_pipe pipe;
|
|
std::mutex readMutex;
|
|
std::condition_variable readCondVar;
|
|
char buffer[bufferCapacity];
|
|
std::atomic_size_t bytesBuffered = 0;
|
|
std::size_t bytesRead = 0;
|
|
QProcess::ProcessState state = QProcess::NotRunning;
|
|
};
|
|
|
|
/// \brief Holds the IO context and thread handles for the process execution via Boost.Process.
|
|
/// \remarks A new one is created for each SyncthingProcess instance.
|
|
struct SyncthingProcessIOHandler {
|
|
explicit SyncthingProcessIOHandler();
|
|
~SyncthingProcessIOHandler();
|
|
|
|
boost::asio::io_context ioc;
|
|
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> guard;
|
|
std::thread t;
|
|
};
|
|
#endif
|
|
|
|
SyncthingProcess *SyncthingProcess::s_mainInstance = nullptr;
|
|
|
|
/*!
|
|
* \class SyncthingProcess
|
|
* \brief The SyncthingProcess class starts a Syncthing instance or additional tools as an external process.
|
|
*
|
|
* This class is actually not Syncthing-specific. It is just an extension of QProcess for some use-cases within
|
|
* Syncthing Tray.
|
|
*/
|
|
|
|
/*!
|
|
* \brief Constructs a new Syncthing process.
|
|
*/
|
|
SyncthingProcess::SyncthingProcess(QObject *parent)
|
|
: SyncthingProcessBase(parent)
|
|
, m_manuallyStopped(false)
|
|
{
|
|
m_killTimer.setInterval(3000);
|
|
m_killTimer.setSingleShot(true);
|
|
setProcessChannelMode(QProcess::MergedChannels);
|
|
connect(this, &SyncthingProcess::started, this, &SyncthingProcess::handleStarted);
|
|
connect(this, static_cast<void (SyncthingProcess::*)(int exitCode, QProcess::ExitStatus exitStatus)>(&SyncthingProcess::finished), this,
|
|
&SyncthingProcess::handleFinished, Qt::QueuedConnection);
|
|
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<std::mutex>(m_process->mutex);
|
|
m_process.reset();
|
|
#endif
|
|
}
|
|
|
|
/*!
|
|
* \brief Splits the given arguments similar to how a shell would split it. So whitespaces are considered separators unless quotes are used.
|
|
*/
|
|
QStringList SyncthingProcess::splitArguments(const QString &arguments)
|
|
{
|
|
enum { Any, Quote, Slash, Space } lastInput = Any;
|
|
bool inQuotes = false;
|
|
QStringList result;
|
|
QString currentArg;
|
|
for (const auto c : arguments) {
|
|
switch (c.unicode()) {
|
|
case '\"':
|
|
case '\'':
|
|
switch (lastInput) {
|
|
case Slash:
|
|
currentArg += c;
|
|
lastInput = Any;
|
|
break;
|
|
default:
|
|
inQuotes = !inQuotes;
|
|
lastInput = Quote;
|
|
}
|
|
break;
|
|
case '\\':
|
|
switch (lastInput) {
|
|
case Slash:
|
|
currentArg += c;
|
|
lastInput = Any;
|
|
break;
|
|
default:
|
|
lastInput = Slash;
|
|
}
|
|
break;
|
|
case ' ':
|
|
switch (lastInput) {
|
|
case Slash:
|
|
if (!currentArg.isEmpty()) {
|
|
currentArg += c;
|
|
}
|
|
lastInput = Any;
|
|
break;
|
|
case Space:
|
|
if (inQuotes) {
|
|
currentArg += c;
|
|
lastInput = Any;
|
|
}
|
|
break;
|
|
default:
|
|
if (inQuotes) {
|
|
currentArg += c;
|
|
lastInput = Any;
|
|
} else {
|
|
if (!currentArg.isEmpty()) {
|
|
result << currentArg;
|
|
currentArg.clear();
|
|
}
|
|
lastInput = Space;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
currentArg += c;
|
|
lastInput = Any;
|
|
}
|
|
}
|
|
if (!currentArg.isEmpty()) {
|
|
result << currentArg;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/*!
|
|
* \brief Emits errorOccurred() as if the specified \a error had occurred and sets the specified \a errorString.
|
|
* \remarks May be called if a precondition for starting the process (that is checked outside of the SyncthingProcess
|
|
* class itself) has failed (instead of starting the process).
|
|
*/
|
|
void SyncthingProcess::reportError(QProcess::ProcessError error, const QString &errorString)
|
|
{
|
|
setErrorString(errorString);
|
|
emit errorOccurred(error);
|
|
}
|
|
|
|
/*!
|
|
* \brief Stops the currently running process gracefully. If it has been stopped, starts the specified \a program with the specified \a arguments.
|
|
* \sa See stopSyncthing() for details about \a currentConnection.
|
|
*/
|
|
void SyncthingProcess::restartSyncthing(const QString &program, const QStringList &arguments, SyncthingConnection *currentConnection)
|
|
{
|
|
if (!isRunning()) {
|
|
startSyncthing(program, arguments);
|
|
return;
|
|
}
|
|
m_program = program;
|
|
m_arguments = arguments;
|
|
stopSyncthing(currentConnection);
|
|
}
|
|
|
|
/*!
|
|
* \brief Starts the specified \a program with the specified \a arguments.
|
|
*/
|
|
void SyncthingProcess::startSyncthing(const QString &program, const QStringList &arguments)
|
|
{
|
|
if (isRunning()) {
|
|
return;
|
|
}
|
|
m_manuallyStopped = false;
|
|
m_killTimer.stop();
|
|
start(program, arguments, QIODevice::ReadOnly);
|
|
}
|
|
|
|
/*!
|
|
* \brief Stops the currently running process gracefully. If it doesn't stop after 3 seconds, attempts to kill the process.
|
|
* \remarks
|
|
* If graceful stopping is not possible (under Windows) the process is attempted to be stopped via the REST-API using \a currentConnection.
|
|
* This is however only considered, if \a currentConnection is configured and a local connection.
|
|
*/
|
|
void SyncthingProcess::stopSyncthing(SyncthingConnection *currentConnection)
|
|
{
|
|
m_manuallyStopped = true;
|
|
m_killTimer.start();
|
|
#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()) {
|
|
currentConnection->shutdown();
|
|
return;
|
|
}
|
|
#endif
|
|
terminate();
|
|
}
|
|
|
|
/*!
|
|
* \brief Kills the currently running process.
|
|
*/
|
|
void SyncthingProcess::killSyncthing()
|
|
{
|
|
m_manuallyStopped = true;
|
|
m_killTimer.stop();
|
|
kill();
|
|
}
|
|
|
|
/*!
|
|
* \brief Keeps track of when the process has been started.
|
|
*/
|
|
void SyncthingProcess::handleStarted()
|
|
{
|
|
m_activeSince = DateTime::gmtNow();
|
|
}
|
|
|
|
/*!
|
|
* \brief Resets the state after the process has finished; restart it again if requested.
|
|
*/
|
|
void SyncthingProcess::handleFinished(int exitCode, QProcess::ExitStatus exitStatus)
|
|
{
|
|
Q_UNUSED(exitCode)
|
|
Q_UNUSED(exitStatus)
|
|
m_activeSince = DateTime();
|
|
m_killTimer.stop();
|
|
if (!m_program.isEmpty()) {
|
|
startSyncthing(m_program, m_arguments);
|
|
m_program.clear();
|
|
m_arguments.clear();
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief Kills the process if it is supposed to be restarted.
|
|
*/
|
|
void SyncthingProcess::killToRestart()
|
|
{
|
|
if (!m_program.isEmpty()) {
|
|
kill();
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
}
|
|
|
|
Data::SyncthingProcessInternalData::Lock::Lock(const std::weak_ptr<SyncthingProcessInternalData> &weak)
|
|
: process(weak.lock())
|
|
, lock(process ? decltype(lock)(process->mutex) : decltype(lock)())
|
|
{
|
|
}
|
|
|
|
Data::SyncthingProcessInternalData::Lock::operator bool() const
|
|
{
|
|
return process && lock;
|
|
}
|
|
|
|
/*!
|
|
* \brief Internally handles an error.
|
|
*/
|
|
void SyncthingProcess::handleError(int error, const QString &errorMessage, bool closed)
|
|
{
|
|
setErrorString(errorMessage);
|
|
emit errorOccurred(static_cast<QProcess::ProcessError>(error));
|
|
if (closed) {
|
|
setOpenMode(QIODevice::NotOpen);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns the process' state.
|
|
* \remarks The process is only considered QProcess::NotRunning if all sub processes it has created have been
|
|
* terminated as well.
|
|
*/
|
|
QProcess::ProcessState SyncthingProcess::state() const
|
|
{
|
|
return !m_process ? QProcess::NotRunning : m_process->state;
|
|
}
|
|
|
|
/*!
|
|
* \brief Starts a new process within a new process group (or job object under Windows) with the specified parameters.
|
|
* \remarks
|
|
* - The specified \a openMode must be QIODevice::ReadOnly.
|
|
* - Only one process can be started at a time. Starting another process while the previous one or any child
|
|
* process of it is still running is an error.
|
|
* - In case of an error the error strinng is set and errorOccurred() is emitted.
|
|
* - If \a program is a relative path, it is tried to be located in PATH.
|
|
*/
|
|
void SyncthingProcess::start(const QString &program, const QStringList &arguments, QIODevice::OpenMode openMode)
|
|
{
|
|
start(QStringList{ program }, arguments, openMode);
|
|
}
|
|
|
|
/*!
|
|
* \brief Starts a new process within a new process group (or job object under Windows) with the specified parameters.
|
|
*
|
|
* This function is generally the same as the overload that just takes a single program (see its remarks for details).
|
|
* The difference is that it allows one to specify fallback \a programs which will be tried to be located in PATH in
|
|
* case the first program cannot be located in PATH.
|
|
*
|
|
* \remarks
|
|
* If an absolute path is given, no lookup via PATH will happen. Instead, the absolute path is used as-is and further
|
|
* fallbacks are not considered. This is even the case when the absolute path does not exist.
|
|
*
|
|
*/
|
|
void SyncthingProcess::start(const QStringList &programs, const QStringList &arguments, OpenMode openMode)
|
|
{
|
|
// get Boost.Process' code converter to provoke and handle a possible error when it is setting up its default locale via e.g. `std::locale("")`
|
|
try {
|
|
boost::process::codecvt();
|
|
} catch (const std::runtime_error &e) {
|
|
setErrorString(QStringLiteral("unable to initialize codecvt/locale: ") % QString::fromLocal8Bit(e.what())
|
|
% QStringLiteral("\nBe sure your locale setup is correct, see https://wiki.archlinux.org/title/Locale for details."));
|
|
std::cerr << EscapeCodes::Phrases::Error << "Unable to initialize locale for launching process" << EscapeCodes::Phrases::End;
|
|
std::cerr << EscapeCodes::Phrases::SubError << "Unable to initialize boost::process::codecvt/std::locale: " << e.what()
|
|
<< EscapeCodes::Phrases::End;
|
|
std::cerr << EscapeCodes::Phrases::SubMessage
|
|
<< "Be sure the locale is setup correctly in your environment (see https://wiki.archlinux.org/title/Locale)"
|
|
<< EscapeCodes::Phrases::End;
|
|
emit errorOccurred(QProcess::FailedToStart);
|
|
return;
|
|
}
|
|
|
|
// 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<std::mutex>(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<SyncthingProcessIOHandler>();
|
|
}
|
|
m_process = std::make_shared<SyncthingProcessInternalData>(m_handler->ioc);
|
|
emit stateChanged(m_process->state = QProcess::Starting);
|
|
|
|
// define handler
|
|
auto successHandler = 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();
|
|
}
|
|
};
|
|
auto exitHandler = 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::Info << "Launched process " << m_process->child.native_handle() << " exited with error: " << msg
|
|
<< EscapeCodes::Phrases::End;
|
|
QMetaObject::invokeMethod(this, "handleError", Qt::QueuedConnection, Q_ARG(int, QProcess::Crashed),
|
|
Q_ARG(QString, QString::fromStdString(msg)), Q_ARG(bool, false));
|
|
}
|
|
};
|
|
auto errorHandler = 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
|
|
#ifdef ETIME
|
|
|| ec == std::errc::stream_timeout
|
|
#endif
|
|
? 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(int, error), Q_ARG(QString, QString::fromStdString(msg)), Q_ARG(bool, false));
|
|
};
|
|
|
|
// convert args
|
|
m_process->arguments = arguments;
|
|
auto args = std::vector<decltype(LIB_SYNCTHING_CONNECTOR_STRING_CONVERSION(arguments.front()))>();
|
|
for (const auto &arg : arguments) {
|
|
args.emplace_back(LIB_SYNCTHING_CONNECTOR_STRING_CONVERSION(arg));
|
|
}
|
|
|
|
// locate program
|
|
auto hasProgram = false;
|
|
auto path = boost::filesystem::path();
|
|
for (const auto &program : programs) {
|
|
m_process->program = program;
|
|
path = boost::filesystem::path(LIB_SYNCTHING_CONNECTOR_STRING_CONVERSION(program));
|
|
if (path.empty()) {
|
|
continue;
|
|
}
|
|
hasProgram = true;
|
|
if (path.is_relative()) {
|
|
path = boost::process::search_path(path);
|
|
}
|
|
if (!path.empty()) {
|
|
break;
|
|
}
|
|
}
|
|
if (!hasProgram) {
|
|
std::cerr << EscapeCodes::Phrases::Error << "Unable to launch process: no executable specified" << EscapeCodes::Phrases::End;
|
|
emit stateChanged(m_process->state = QProcess::NotRunning);
|
|
handleError(QProcess::FailedToStart, QStringLiteral("no executable specified"), false);
|
|
return;
|
|
}
|
|
|
|
// start the process within a new process group (or job object under Windows)
|
|
try {
|
|
if (path.empty()) {
|
|
throw boost::process::process_error(
|
|
std::make_error_code(std::errc::no_such_file_or_directory), "unable to find the specified executable in the search paths");
|
|
}
|
|
switch (m_mode) {
|
|
case QProcess::MergedChannels:
|
|
m_process->child = boost::process::child(LIB_SYNCTHING_CONNECTOR_PLATFORM_ARGS m_handler->ioc, m_process->group, path, args,
|
|
(boost::process::std_out & boost::process::std_err) > m_process->pipe, std::move(successHandler), std::move(exitHandler),
|
|
std::move(errorHandler));
|
|
break;
|
|
case QProcess::ForwardedChannels:
|
|
m_process->child = boost::process::child(LIB_SYNCTHING_CONNECTOR_PLATFORM_ARGS m_handler->ioc, m_process->group, path, args,
|
|
std::move(successHandler), std::move(exitHandler), std::move(errorHandler));
|
|
break;
|
|
default:
|
|
std::cerr << EscapeCodes::Phrases::Error << "Unable to launch process: specified channel mode not supported" << EscapeCodes::Phrases::End;
|
|
emit stateChanged(m_process->state = QProcess::NotRunning);
|
|
handleError(QProcess::FailedToStart, QStringLiteral("specified channel mode not supported"), false);
|
|
return;
|
|
}
|
|
} 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();
|
|
}
|
|
|
|
/*!
|
|
* \brief Terminates the launched process and all child processes gracefully if possible.
|
|
* \remarks Sends SIGTERM to the process group under POSIX and invokes TerminateJobObject() under Windows.
|
|
*/
|
|
void SyncthingProcess::terminate()
|
|
{
|
|
if (!m_process) {
|
|
return;
|
|
}
|
|
#ifdef PLATFORM_UNIX
|
|
auto lock = std::unique_lock<std::mutex>(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));
|
|
emit 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
|
|
}
|
|
|
|
/*!
|
|
* \brief Terminates the launched process and all child processes forcefully.
|
|
* \remarks Sends SIGKILL to the process group under POSIX and invokes TerminateJobObject() under Windows.
|
|
*/
|
|
void SyncthingProcess::kill()
|
|
{
|
|
if (!m_process) {
|
|
return;
|
|
}
|
|
auto ec = std::error_code();
|
|
auto lock = std::unique_lock<std::mutex>(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));
|
|
emit errorOccurred(QProcess::UnknownError);
|
|
}
|
|
// note: No need to emit finished() signal here, the on_exit handler will fire
|
|
// also in case of a forceful termination.
|
|
}
|
|
|
|
/*!
|
|
* \brief Reads data from the pipe into the internal buffer.
|
|
*/
|
|
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(int, QProcess::ReadError),
|
|
Q_ARG(QString, QString::fromStdString(msg)), Q_ARG(bool, true));
|
|
}
|
|
if (!ec || bytesRead) {
|
|
emit readyRead();
|
|
m_process->readCondVar.notify_all();
|
|
}
|
|
});
|
|
}
|
|
|
|
/*!
|
|
* \brief Terminates all processes in the group forcefully and waits until they're gone.
|
|
*/
|
|
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;
|
|
}
|
|
if (!m_process->group.valid()) {
|
|
return;
|
|
}
|
|
m_process->group.wait(ec); // wait until group has terminated: Is this ever required?
|
|
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<qint64>(m_process->bytesBuffered) : 0) + QIODevice::bytesAvailable();
|
|
}
|
|
|
|
void SyncthingProcess::close()
|
|
{
|
|
aboutToClose();
|
|
if (m_process) {
|
|
const auto lock = std::lock_guard<std::mutex>(m_process->mutex);
|
|
m_process->pipe.async_close();
|
|
kill();
|
|
}
|
|
setOpenMode(QIODevice::NotOpen);
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns the exit code of the last process which has been started.
|
|
* \remarks Only valid if the process has already terminated.
|
|
*/
|
|
int SyncthingProcess::exitCode() const
|
|
{
|
|
return m_process ? m_process->child.exit_code() : 0;
|
|
}
|
|
|
|
/*!
|
|
* \brief Waits until the current process and all its child processes have been terminated.
|
|
* \param msecs Specifies the number of milliseconds to wait at most or a negative integer to wait infinitely.
|
|
*/
|
|
bool SyncthingProcess::waitForFinished(int msecs)
|
|
{
|
|
if (!m_process || !m_process->group.valid()) {
|
|
return false;
|
|
}
|
|
auto ec = std::error_code();
|
|
if (msecs < 0) {
|
|
m_process->group.wait(ec);
|
|
} else {
|
|
// disable warning about deprecated/unreliable function `wait_for`; it is good enough for now
|
|
#ifdef __GNUC__
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
|
#endif
|
|
m_process->group.wait_for(std::chrono::milliseconds(msecs), ec);
|
|
#ifdef __GNUC__
|
|
#pragma GCC diagnostic pop
|
|
#endif
|
|
}
|
|
return !ec || ec == std::errc::no_such_process || ec == std::errc::no_child_process;
|
|
}
|
|
|
|
bool SyncthingProcess::waitForReadyRead(int msecs)
|
|
{
|
|
if (!m_process) {
|
|
return false;
|
|
}
|
|
if (m_process->bytesBuffered) {
|
|
return true;
|
|
}
|
|
QEventLoop().processEvents(
|
|
QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers); // ensure a possibly pending bufferOutput() invocation is processed
|
|
auto lock = std::unique_lock<std::mutex>(m_process->readMutex);
|
|
if (msecs < 0) {
|
|
m_process->readCondVar.wait(lock);
|
|
} else {
|
|
m_process->readCondVar.wait_for(lock, std::chrono::milliseconds(msecs));
|
|
}
|
|
return m_process->bytesBuffered;
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns the process ID of the last process which has been started.
|
|
*/
|
|
qint64 SyncthingProcess::processId() const
|
|
{
|
|
return m_process ? static_cast<qint64>(m_process->child.id()) : static_cast<qint64>(-1);
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns the path/executable of the last process which has been started.
|
|
*/
|
|
QString SyncthingProcess::program() const
|
|
{
|
|
return m_process ? m_process->program : QString();
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns the arguments the last process has been started with.
|
|
*/
|
|
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) {
|
|
return 0; // do *not* invoke bufferOutput() here; an async read operation is already pending
|
|
}
|
|
|
|
const auto bytesAvailable = m_process->bytesBuffered - m_process->bytesRead;
|
|
if (bytesAvailable > static_cast<std::size_t>(maxSize)) {
|
|
std::memcpy(data, m_process->buffer + m_process->bytesRead, static_cast<std::size_t>(maxSize));
|
|
m_process->bytesRead += static_cast<std::size_t>(maxSize);
|
|
return maxSize;
|
|
} else {
|
|
std::memcpy(data, m_process->buffer + m_process->bytesRead, bytesAvailable);
|
|
m_process->bytesBuffered = 0;
|
|
m_process->bytesRead = 0;
|
|
QMetaObject::invokeMethod(this, "bufferOutput", Qt::QueuedConnection);
|
|
return static_cast<qint64>(bytesAvailable);
|
|
}
|
|
}
|
|
|
|
qint64 SyncthingProcess::writeData(const char *data, qint64 len)
|
|
{
|
|
Q_UNUSED(data)
|
|
Q_UNUSED(len)
|
|
return -1;
|
|
}
|
|
#endif
|
|
|
|
} // namespace Data
|