Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qprocesstask.cpp
Go to the documentation of this file.
1// Copyright (C) 2024 Jarek Kobus
2// Copyright (C) 2024 The Qt Company Ltd.
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4
5#include "qprocesstask.h"
6
7#include <QtCore/QCoreApplication>
8#include <QtCore/QDebug>
9#include <QtCore/QElapsedTimer>
10#include <QtCore/QMutex>
11#include <QtCore/QThread>
12#include <QtCore/QTimer>
13#include <QtCore/QWaitCondition>
14
15QT_BEGIN_NAMESPACE
16
17#if QT_CONFIG(process)
18
19namespace Tasking {
20
21class ProcessReaperPrivate;
22
23class ProcessReaper final
24{
25public:
26 static void reap(QProcess *process, int timeoutMs = 500);
27 ProcessReaper();
28 ~ProcessReaper();
29
30private:
31 static ProcessReaper *instance();
32
33 QThread m_thread;
34 ProcessReaperPrivate *m_private;
35};
36
37static const int s_timeoutThreshold = 10000; // 10 seconds
38
39static QString execWithArguments(QProcess *process)
40{
41 QStringList commandLine;
42 commandLine.append(process->program());
43 commandLine.append(process->arguments());
44 return commandLine.join(QChar::Space);
45}
46
47struct ReaperSetup
48{
49 QProcess *m_process = nullptr;
50 int m_timeoutMs;
51};
52
53class Reaper : public QObject
54{
55 Q_OBJECT
56
57public:
58 Reaper(const ReaperSetup &reaperSetup) : m_reaperSetup(reaperSetup) {}
59
60 void reap()
61 {
62 m_timer.start();
63 connect(m_reaperSetup.m_process, &QProcess::finished, this, &Reaper::handleFinished);
64 if (emitFinished())
65 return;
66 terminate();
67 }
68
69Q_SIGNALS:
70 void finished();
71
72private:
73 void terminate()
74 {
75 m_reaperSetup.m_process->terminate();
76 QTimer::singleShot(m_reaperSetup.m_timeoutMs, this, &Reaper::handleTerminateTimeout);
77 }
78
79 void kill() { m_reaperSetup.m_process->kill(); }
80
81 bool emitFinished()
82 {
83 if (m_reaperSetup.m_process->state() != QProcess::NotRunning)
84 return false;
85
86 if (!m_finished) {
87 const int timeout = m_timer.elapsed();
88 if (timeout > s_timeoutThreshold) {
89 qWarning() << "Finished parallel reaping of" << execWithArguments(m_reaperSetup.m_process)
90 << "in" << (timeout / 1000.0) << "seconds.";
91 }
92
93 m_finished = true;
94 emit finished();
95 }
96 return true;
97 }
98
99 void handleFinished()
100 {
101 if (emitFinished())
102 return;
103 qWarning() << "Finished process still running...";
104 // In case the process is still running - wait until it has finished
105 QTimer::singleShot(m_reaperSetup.m_timeoutMs, this, &Reaper::handleFinished);
106 }
107
108 void handleTerminateTimeout()
109 {
110 if (emitFinished())
111 return;
112 kill();
113 }
114
115 bool m_finished = false;
116 QElapsedTimer m_timer;
117 const ReaperSetup m_reaperSetup;
118};
119
120class ProcessReaperPrivate : public QObject
121{
122 Q_OBJECT
123
124public:
125 // Called from non-reaper's thread
126 void scheduleReap(const ReaperSetup &reaperSetup)
127 {
128 if (QThread::currentThread() == thread())
129 qWarning() << "Can't schedule reap from the reaper internal thread.";
130
131 QMutexLocker locker(&m_mutex);
132 m_reaperSetupList.append(reaperSetup);
133 QMetaObject::invokeMethod(this, &ProcessReaperPrivate::flush);
134 }
135
136 // Called from non-reaper's thread
137 void waitForFinished()
138 {
139 if (QThread::currentThread() == thread())
140 qWarning() << "Can't wait for finished from the reaper internal thread.";
141
142 QMetaObject::invokeMethod(this, &ProcessReaperPrivate::flush,
143 Qt::BlockingQueuedConnection);
144 QMutexLocker locker(&m_mutex);
145 if (m_reaperList.isEmpty())
146 return;
147
148 m_waitCondition.wait(&m_mutex);
149 }
150
151private:
152 // All the private methods are called from the reaper's thread
153 QList<ReaperSetup> takeReaperSetupList()
154 {
155 QMutexLocker locker(&m_mutex);
156 return std::exchange(m_reaperSetupList, {});
157 }
158
159 void flush()
160 {
161 while (true) {
162 const QList<ReaperSetup> reaperSetupList = takeReaperSetupList();
163 if (reaperSetupList.isEmpty())
164 return;
165 for (const ReaperSetup &reaperSetup : reaperSetupList)
166 reap(reaperSetup);
167 }
168 }
169
170 void reap(const ReaperSetup &reaperSetup)
171 {
172 Reaper *reaper = new Reaper(reaperSetup);
173 connect(reaper, &Reaper::finished, this, [this, reaper, process = reaperSetup.m_process] {
174 QMutexLocker locker(&m_mutex);
175 const bool isRemoved = m_reaperList.removeOne(reaper);
176 if (!isRemoved)
177 qWarning() << "Reaper list doesn't contain the finished process.";
178
179 delete reaper;
180 delete process;
181 if (m_reaperList.isEmpty())
182 m_waitCondition.wakeOne();
183 }, Qt::QueuedConnection);
184
185 {
186 QMutexLocker locker(&m_mutex);
187 m_reaperList.append(reaper);
188 }
189
190 reaper->reap();
191 }
192
193 QMutex m_mutex;
194 QWaitCondition m_waitCondition;
195 QList<ReaperSetup> m_reaperSetupList;
196 QList<Reaper *> m_reaperList;
197};
198
199static ProcessReaper *s_instance = nullptr;
200static QMutex s_instanceMutex;
201
202// Call me with s_instanceMutex locked.
203ProcessReaper *ProcessReaper::instance()
204{
205 if (!s_instance)
206 s_instance = new ProcessReaper;
207 return s_instance;
208}
209
210ProcessReaper::ProcessReaper()
211 : m_private(new ProcessReaperPrivate)
212{
213 m_private->moveToThread(&m_thread);
214 QObject::connect(&m_thread, &QThread::finished, m_private, &QObject::deleteLater);
215 m_thread.start();
216 m_thread.moveToThread(qApp->thread());
217}
218
219ProcessReaper::~ProcessReaper()
220{
221 if (!QThread::isMainThread())
222 qWarning() << "Destructing process reaper from non-main thread.";
223
224 instance()->m_private->waitForFinished();
225 m_thread.quit();
226 m_thread.wait();
227}
228
229void ProcessReaper::reap(QProcess *process, int timeoutMs)
230{
231 if (!process)
232 return;
233
234 if (QThread::currentThread() != process->thread()) {
235 qWarning() << "Can't reap process from non-process's thread.";
236 return;
237 }
238
239 process->disconnect();
240 if (process->state() == QProcess::NotRunning) {
241 delete process;
242 return;
243 }
244
245 // Neither can move object with a parent into a different thread
246 // nor reaping the process with a parent makes any sense.
247 process->setParent(nullptr);
248
249 QMutexLocker locker(&s_instanceMutex);
250 ProcessReaperPrivate *priv = instance()->m_private;
251
252 process->moveToThread(priv->thread());
253 ReaperSetup reaperSetup {process, timeoutMs};
254 priv->scheduleReap(reaperSetup);
255}
256
257void QProcessDeleter::deleteAll()
258{
259 QMutexLocker locker(&s_instanceMutex);
260 delete s_instance;
261 s_instance = nullptr;
262}
263
264void QProcessDeleter::operator()(QProcess *process)
265{
266 ProcessReaper::reap(process);
267}
268
269} // namespace Tasking
270
271#endif // QT_CONFIG(process)
272
273QT_END_NAMESPACE
274
275#if QT_CONFIG(process)
276
277#include "qprocesstask.moc"
278
279#endif // QT_CONFIG(process)