TaskTree: Introduce QProcessTask with internal reaper

In order to make it possible to use QProcess in TaskTree
outside of QtCreator.

Change-Id: Icff4113a7799297c5941ee68ee1cc874806c3816
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: hjk <hjk@qt.io>
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
This commit is contained in:
Jarek Kobus
2023-10-26 17:44:37 +02:00
parent 6d50724937
commit 65341d7e5f
4 changed files with 311 additions and 0 deletions

View File

@@ -6,6 +6,7 @@ add_qtc_library(Tasking OBJECT
barrier.cpp barrier.h barrier.cpp barrier.h
concurrentcall.h concurrentcall.h
networkquery.cpp networkquery.h networkquery.cpp networkquery.h
qprocesstask.cpp qprocesstask.h
tasking_global.h tasking_global.h
tasktree.cpp tasktree.h tasktree.cpp tasktree.h
EXPLICIT_MOC EXPLICIT_MOC

View File

@@ -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 <QCoreApplication>
#include <QDebug>
#include <QMutex>
#include <QThread>
#include <QTimer>
#include <QWaitCondition>
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<ReaperSetup> takeReaperSetupList()
{
QMutexLocker locker(&m_mutex);
return std::exchange(m_reaperSetupList, {});
}
void flush()
{
while (true) {
const QList<ReaperSetup> 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<ReaperSetup> m_reaperSetupList;
QList<Reaper *> 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"

View File

@@ -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 <QProcess>
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<QProcess, QProcessDeleter>
{
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<QProcessAdapter>;
} // namespace Tasking

View File

@@ -11,6 +11,8 @@ QtcLibrary {
"concurrentcall.h", "concurrentcall.h",
"networkquery.cpp", "networkquery.cpp",
"networkquery.h", "networkquery.h",
"qprocesstask.cpp",
"qprocesstask.h",
"tasking_global.h", "tasking_global.h",
"tasktree.cpp", "tasktree.cpp",
"tasktree.h", "tasktree.h",