diff --git a/src/libs/solutions/tasking/CMakeLists.txt b/src/libs/solutions/tasking/CMakeLists.txt index ae51b12a7bd..717163e1685 100644 --- a/src/libs/solutions/tasking/CMakeLists.txt +++ b/src/libs/solutions/tasking/CMakeLists.txt @@ -6,6 +6,7 @@ add_qtc_library(Tasking OBJECT barrier.cpp barrier.h concurrentcall.h networkquery.cpp networkquery.h + qprocesstask.cpp qprocesstask.h tasking_global.h tasktree.cpp tasktree.h EXPLICIT_MOC diff --git a/src/libs/solutions/tasking/qprocesstask.cpp b/src/libs/solutions/tasking/qprocesstask.cpp new file mode 100644 index 00000000000..7d63e658478 --- /dev/null +++ b/src/libs/solutions/tasking/qprocesstask.cpp @@ -0,0 +1,265 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "qprocesstask.h" + +#include +#include +#include +#include +#include +#include + +namespace Tasking { + +class ProcessReaperPrivate; + +class ProcessReaper final +{ +public: + static void reap(QProcess *process, int timeoutMs = 500); + ProcessReaper(); + ~ProcessReaper(); + +private: + static ProcessReaper *instance(); + + QThread m_thread; + ProcessReaperPrivate *m_private; +}; + +static const int s_timeoutThreshold = 10000; // 10 seconds + +static QString execWithArguments(QProcess *process) +{ + QStringList commandLine; + commandLine.append(process->program()); + commandLine.append(process->arguments()); + return commandLine.join(QChar::Space); +} + +struct ReaperSetup +{ + QProcess *m_process = nullptr; + int m_timeoutMs; +}; + +class Reaper : public QObject +{ + Q_OBJECT + +public: + Reaper(const ReaperSetup &reaperSetup) : m_reaperSetup(reaperSetup) {} + + void reap() + { + m_timer.start(); + connect(m_reaperSetup.m_process, &QProcess::finished, this, &Reaper::handleFinished); + if (emitFinished()) + return; + terminate(); + } + +signals: + void finished(); + +private: + void terminate() + { + m_reaperSetup.m_process->terminate(); + QTimer::singleShot(m_reaperSetup.m_timeoutMs, this, &Reaper::handleTerminateTimeout); + } + + void kill() { m_reaperSetup.m_process->kill(); } + + bool emitFinished() + { + if (m_reaperSetup.m_process->state() != QProcess::NotRunning) + return false; + + if (!m_finished) { + const int timeout = m_timer.elapsed(); + if (timeout > s_timeoutThreshold) { + qWarning() << "Finished parallel reaping of" << execWithArguments(m_reaperSetup.m_process) + << "in" << (timeout / 1000.0) << "seconds."; + } + + m_finished = true; + emit finished(); + } + return true; + } + + void handleFinished() + { + if (emitFinished()) + return; + qWarning() << "Finished process still running..."; + // In case the process is still running - wait until it has finished + QTimer::singleShot(m_reaperSetup.m_timeoutMs, this, &Reaper::handleFinished); + } + + void handleTerminateTimeout() + { + if (emitFinished()) + return; + kill(); + } + + bool m_finished = false; + QElapsedTimer m_timer; + const ReaperSetup m_reaperSetup; +}; + +class ProcessReaperPrivate : public QObject +{ + Q_OBJECT + +public: + // Called from non-reaper's thread + void scheduleReap(const ReaperSetup &reaperSetup) + { + if (QThread::currentThread() == thread()) + qWarning() << "Can't schedule reap from the reaper internal thread."; + + QMutexLocker locker(&m_mutex); + m_reaperSetupList.append(reaperSetup); + QMetaObject::invokeMethod(this, &ProcessReaperPrivate::flush); + } + + // Called from non-reaper's thread + void waitForFinished() + { + if (QThread::currentThread() == thread()) + qWarning() << "Can't wait for finished from the reaper internal thread."; + + QMetaObject::invokeMethod(this, &ProcessReaperPrivate::flush, + Qt::BlockingQueuedConnection); + QMutexLocker locker(&m_mutex); + if (m_reaperList.isEmpty()) + return; + + m_waitCondition.wait(&m_mutex); + } + +private: + // All the private methods are called from the reaper's thread + QList takeReaperSetupList() + { + QMutexLocker locker(&m_mutex); + return std::exchange(m_reaperSetupList, {}); + } + + void flush() + { + while (true) { + const QList reaperSetupList = takeReaperSetupList(); + if (reaperSetupList.isEmpty()) + return; + for (const ReaperSetup &reaperSetup : reaperSetupList) + reap(reaperSetup); + } + } + + void reap(const ReaperSetup &reaperSetup) + { + Reaper *reaper = new Reaper(reaperSetup); + connect(reaper, &Reaper::finished, this, [this, reaper, process = reaperSetup.m_process] { + QMutexLocker locker(&m_mutex); + const bool isRemoved = m_reaperList.removeOne(reaper); + if (!isRemoved) + qWarning() << "Reaper list doesn't contain the finished process."; + + delete reaper; + delete process; + if (m_reaperList.isEmpty()) + m_waitCondition.wakeOne(); + }, Qt::QueuedConnection); + + { + QMutexLocker locker(&m_mutex); + m_reaperList.append(reaper); + } + + reaper->reap(); + } + + QMutex m_mutex; + QWaitCondition m_waitCondition; + QList m_reaperSetupList; + QList m_reaperList; +}; + +static ProcessReaper *s_instance = nullptr; +static QMutex s_instanceMutex; + +// Call me with s_instanceMutex locked. +ProcessReaper *ProcessReaper::instance() +{ + if (!s_instance) + s_instance = new ProcessReaper; + return s_instance; +} + +ProcessReaper::ProcessReaper() + : m_private(new ProcessReaperPrivate) +{ + m_private->moveToThread(&m_thread); + QObject::connect(&m_thread, &QThread::finished, m_private, &QObject::deleteLater); + m_thread.start(); + m_thread.moveToThread(qApp->thread()); +} + +ProcessReaper::~ProcessReaper() +{ + if (QThread::currentThread() != qApp->thread()) + qWarning() << "Destructing process reaper from non-main thread."; + + instance()->m_private->waitForFinished(); + m_thread.quit(); + m_thread.wait(); +} + +void ProcessReaper::reap(QProcess *process, int timeoutMs) +{ + if (!process) + return; + + if (QThread::currentThread() != process->thread()) { + qWarning() << "Can't reap process from non-process's thread."; + return; + } + + process->disconnect(); + if (process->state() == QProcess::NotRunning) { + delete process; + return; + } + + // Neither can move object with a parent into a different thread + // nor reaping the process with a parent makes any sense. + process->setParent(nullptr); + + QMutexLocker locker(&s_instanceMutex); + ProcessReaperPrivate *priv = instance()->m_private; + + process->moveToThread(priv->thread()); + ReaperSetup reaperSetup {process, timeoutMs}; + priv->scheduleReap(reaperSetup); +} + +void QProcessDeleter::deleteAll() +{ + QMutexLocker locker(&s_instanceMutex); + delete s_instance; + s_instance = nullptr; +} + +void QProcessDeleter::operator()(QProcess *process) +{ + ProcessReaper::reap(process); +} + +} // namespace Tasking + +#include "qprocesstask.moc" diff --git a/src/libs/solutions/tasking/qprocesstask.h b/src/libs/solutions/tasking/qprocesstask.h new file mode 100644 index 00000000000..5cec4890e6f --- /dev/null +++ b/src/libs/solutions/tasking/qprocesstask.h @@ -0,0 +1,43 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "tasking_global.h" + +#include "tasktree.h" + +#include + +namespace Tasking { + +class TASKING_EXPORT QProcessDeleter +{ +public: + // Blocking, should be called after all QProcessAdapter instances are deleted. + static void deleteAll(); + void operator()(QProcess *process); +}; + +class TASKING_EXPORT QProcessAdapter : public TaskAdapter +{ +private: + void start() { + connect(task(), &QProcess::finished, this, [this] { + const bool success = task()->exitStatus() == QProcess::NormalExit + && task()->error() == QProcess::UnknownError + && task()->exitCode() == 0; + emit done(success); + }); + connect(task(), &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) { + if (error != QProcess::FailedToStart) + return; + emit done(false); + }); + task()->start(); + } +}; + +using QProcessTask = CustomTask; + +} // namespace Tasking diff --git a/src/libs/solutions/tasking/tasking.qbs b/src/libs/solutions/tasking/tasking.qbs index fba42b10b1d..7840812faab 100644 --- a/src/libs/solutions/tasking/tasking.qbs +++ b/src/libs/solutions/tasking/tasking.qbs @@ -11,6 +11,8 @@ QtcLibrary { "concurrentcall.h", "networkquery.cpp", "networkquery.h", + "qprocesstask.cpp", + "qprocesstask.h", "tasking_global.h", "tasktree.cpp", "tasktree.h",