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

source code of qtbase/src/assets/downloader/tasking/qprocesstask.cpp