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 | |
14 | QT_BEGIN_NAMESPACE |
15 | |
16 | #if QT_CONFIG(process) |
17 | |
18 | namespace Tasking { |
19 | |
20 | class ProcessReaperPrivate; |
21 | |
22 | class ProcessReaper final |
23 | { |
24 | public: |
25 | static void reap(QProcess *process, int timeoutMs = 500); |
26 | ProcessReaper(); |
27 | ~ProcessReaper(); |
28 | |
29 | private: |
30 | static ProcessReaper *instance(); |
31 | |
32 | QThread m_thread; |
33 | ProcessReaperPrivate *m_private; |
34 | }; |
35 | |
36 | static const int s_timeoutThreshold = 10000; // 10 seconds |
37 | |
38 | static 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 | |
46 | struct ReaperSetup |
47 | { |
48 | QProcess *m_process = nullptr; |
49 | int m_timeoutMs; |
50 | }; |
51 | |
52 | class Reaper : public QObject |
53 | { |
54 | Q_OBJECT |
55 | |
56 | public: |
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 | |
68 | Q_SIGNALS: |
69 | void finished(); |
70 | |
71 | private: |
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 | |
119 | class ProcessReaperPrivate : public QObject |
120 | { |
121 | Q_OBJECT |
122 | |
123 | public: |
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 | |
150 | private: |
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 | |
198 | static ProcessReaper *s_instance = nullptr; |
199 | static QMutex s_instanceMutex; |
200 | |
201 | // Call me with s_instanceMutex locked. |
202 | ProcessReaper *ProcessReaper::instance() |
203 | { |
204 | if (!s_instance) |
205 | s_instance = new ProcessReaper; |
206 | return s_instance; |
207 | } |
208 | |
209 | ProcessReaper::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 | |
218 | ProcessReaper::~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 | |
228 | void 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 | |
256 | void QProcessDeleter::deleteAll() |
257 | { |
258 | QMutexLocker locker(&s_instanceMutex); |
259 | delete s_instance; |
260 | s_instance = nullptr; |
261 | } |
262 | |
263 | void QProcessDeleter::operator()(QProcess *process) |
264 | { |
265 | ProcessReaper::reap(process); |
266 | } |
267 | |
268 | } // namespace Tasking |
269 | |
270 | #endif // QT_CONFIG(process) |
271 | |
272 | QT_END_NAMESPACE |
273 | |
274 | #if QT_CONFIG(process) |
275 | |
276 | #include "qprocesstask.moc" |
277 | |
278 | #endif // QT_CONFIG(process) |
279 | |
280 | |