forked from qt-creator/qt-creator
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:
@@ -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
|
||||
|
265
src/libs/solutions/tasking/qprocesstask.cpp
Normal file
265
src/libs/solutions/tasking/qprocesstask.cpp
Normal 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"
|
43
src/libs/solutions/tasking/qprocesstask.h
Normal file
43
src/libs/solutions/tasking/qprocesstask.h
Normal 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
|
@@ -11,6 +11,8 @@ QtcLibrary {
|
||||
"concurrentcall.h",
|
||||
"networkquery.cpp",
|
||||
"networkquery.h",
|
||||
"qprocesstask.cpp",
|
||||
"qprocesstask.h",
|
||||
"tasking_global.h",
|
||||
"tasktree.cpp",
|
||||
"tasktree.h",
|
||||
|
Reference in New Issue
Block a user