1 | /* |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 2000 Waldo Bastian <bastian@kde.org> |
4 | SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org> |
5 | SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org> |
6 | |
7 | SPDX-License-Identifier: LGPL-2.0-only |
8 | */ |
9 | |
10 | #include "worker_p.h" |
11 | |
12 | #include <qplatformdefs.h> |
13 | #include <stdio.h> |
14 | |
15 | #include <QCoreApplication> |
16 | #include <QDataStream> |
17 | #include <QDir> |
18 | #include <QFile> |
19 | #include <QLibraryInfo> |
20 | #include <QPluginLoader> |
21 | #include <QProcess> |
22 | #include <QStandardPaths> |
23 | #include <QTimer> |
24 | |
25 | #include <KLibexec> |
26 | #include <KLocalizedString> |
27 | |
28 | #include "commands_p.h" |
29 | #include "connection_p.h" |
30 | #include "connectionserver.h" |
31 | #include "dataprotocol_p.h" |
32 | #include "kioglobal_p.h" |
33 | #include <config-kiocore.h> // KDE_INSTALL_FULL_LIBEXECDIR_KF |
34 | #include <kprotocolinfo.h> |
35 | |
36 | #include "kiocoredebug.h" |
37 | #include "workerbase.h" |
38 | #include "workerfactory.h" |
39 | #include "workerthread_p.h" |
40 | |
41 | using namespace KIO; |
42 | |
43 | static constexpr int s_workerConnectionTimeoutMin = 2; |
44 | |
45 | // Without debug info we consider it an error if the worker doesn't connect |
46 | // within 10 seconds. |
47 | // With debug info we give the worker an hour so that developers have a chance |
48 | // to debug their worker. |
49 | #ifdef NDEBUG |
50 | static constexpr int s_workerConnectionTimeoutMax = 10; |
51 | #else |
52 | static constexpr int s_workerConnectionTimeoutMax = 3600; |
53 | #endif |
54 | |
55 | void Worker::accept() |
56 | { |
57 | m_workerConnServer->setNextPendingConnection(m_connection); |
58 | m_workerConnServer->deleteLater(); |
59 | m_workerConnServer = nullptr; |
60 | |
61 | connect(sender: m_connection, signal: &Connection::readyRead, context: this, slot: &Worker::gotInput); |
62 | } |
63 | |
64 | void Worker::timeout() |
65 | { |
66 | if (m_dead) { // already dead? then workerDied was emitted and we are done |
67 | return; |
68 | } |
69 | if (m_connection->isConnected()) { |
70 | return; |
71 | } |
72 | |
73 | /*qDebug() << "worker failed to connect to application pid=" << m_pid |
74 | << " protocol=" << m_protocol;*/ |
75 | if (m_pid && KIOPrivate::isProcessAlive(pid: m_pid)) { |
76 | int delta_t = m_contact_started.elapsed() / 1000; |
77 | // qDebug() << "worker is slow... pid=" << m_pid << " t=" << delta_t; |
78 | if (delta_t < s_workerConnectionTimeoutMax) { |
79 | QTimer::singleShot(interval: 1000 * s_workerConnectionTimeoutMin, receiver: this, slot: &Worker::timeout); |
80 | return; |
81 | } |
82 | } |
83 | // qDebug() << "Houston, we lost our worker, pid=" << m_pid; |
84 | m_connection->close(); |
85 | m_dead = true; |
86 | QString arg = m_protocol; |
87 | if (!m_host.isEmpty()) { |
88 | arg += QLatin1String("://" ) + m_host; |
89 | } |
90 | // qDebug() << "worker failed to connect pid =" << m_pid << arg; |
91 | |
92 | ref(); |
93 | // Tell the job about the problem. |
94 | Q_EMIT error(ERR_WORKER_DIED, arg); |
95 | // Tell the scheduler about the problem. |
96 | Q_EMIT workerDied(worker: this); |
97 | // After the above signal we're dead!! |
98 | deref(); |
99 | } |
100 | |
101 | Worker::Worker(const QString &protocol, QObject *parent) |
102 | : WorkerInterface(parent) |
103 | , m_protocol(protocol) |
104 | , m_workerProtocol(protocol) |
105 | , m_workerConnServer(new KIO::ConnectionServer) |
106 | { |
107 | m_contact_started.start(); |
108 | m_workerConnServer->setParent(this); |
109 | m_workerConnServer->listenForRemote(); |
110 | if (!m_workerConnServer->isListening()) { |
111 | qCWarning(KIO_CORE) << "KIO Connection server not listening, could not connect" ; |
112 | } |
113 | m_connection = new Connection(Connection::Type::Application, this); |
114 | connect(sender: m_workerConnServer, signal: &ConnectionServer::newConnection, context: this, slot: &Worker::accept); |
115 | } |
116 | |
117 | Worker::~Worker() |
118 | { |
119 | // qDebug() << "destructing worker object pid =" << m_pid; |
120 | delete m_workerConnServer; |
121 | } |
122 | |
123 | QString Worker::protocol() const |
124 | { |
125 | return m_protocol; |
126 | } |
127 | |
128 | void Worker::setProtocol(const QString &protocol) |
129 | { |
130 | m_protocol = protocol; |
131 | } |
132 | |
133 | QString Worker::workerProtocol() const |
134 | { |
135 | return m_workerProtocol; |
136 | } |
137 | |
138 | QString Worker::host() const |
139 | { |
140 | return m_host; |
141 | } |
142 | |
143 | quint16 Worker::port() const |
144 | { |
145 | return m_port; |
146 | } |
147 | |
148 | QString Worker::user() const |
149 | { |
150 | return m_user; |
151 | } |
152 | |
153 | QString Worker::passwd() const |
154 | { |
155 | return m_passwd; |
156 | } |
157 | |
158 | void Worker::setIdle() |
159 | { |
160 | m_idleSince.start(); |
161 | } |
162 | |
163 | void Worker::ref() |
164 | { |
165 | m_refCount++; |
166 | } |
167 | |
168 | void Worker::deref() |
169 | { |
170 | m_refCount--; |
171 | if (!m_refCount) { |
172 | aboutToDelete(); |
173 | if (m_workerThread) { |
174 | // When on a thread, delete in a thread to prevent deadlocks between the main thread and the worker thread. |
175 | // This most notably can happen when the worker thread uses QDBus, because traffic will generally be routed |
176 | // through the main loop. |
177 | // Generally speaking we'd want to avoid waiting in the main thread anyway, the worker stopping isn't really |
178 | // useful for anything but delaying deletion. |
179 | // https://bugs.kde.org/show_bug.cgi?id=468673 |
180 | WorkerThread *workerThread = nullptr; |
181 | std::swap(a&: workerThread, b&: m_workerThread); |
182 | workerThread->setParent(nullptr); |
183 | connect(sender: workerThread, signal: &QThread::finished, context: workerThread, slot: &QThread::deleteLater); |
184 | workerThread->quit(); |
185 | } |
186 | delete this; // yes it reads funny, but it's too late for a deleteLater() here, no event loop anymore |
187 | } |
188 | } |
189 | |
190 | void Worker::aboutToDelete() |
191 | { |
192 | m_connection->disconnect(receiver: this); |
193 | this->disconnect(); |
194 | } |
195 | |
196 | void Worker::setWorkerThread(WorkerThread *thread) |
197 | { |
198 | m_workerThread = thread; |
199 | } |
200 | |
201 | int Worker::idleTime() const |
202 | { |
203 | if (!m_idleSince.isValid()) { |
204 | return 0; |
205 | } |
206 | return m_idleSince.elapsed() / 1000; |
207 | } |
208 | |
209 | void Worker::setPID(qint64 pid) |
210 | { |
211 | m_pid = pid; |
212 | } |
213 | |
214 | qint64 Worker::worker_pid() const |
215 | { |
216 | return m_pid; |
217 | } |
218 | |
219 | void Worker::setJob(KIO::SimpleJob *job) |
220 | { |
221 | if (!m_sslMetaData.isEmpty()) { |
222 | Q_EMIT metaData(m_sslMetaData); |
223 | } |
224 | m_job = job; |
225 | } |
226 | |
227 | KIO::SimpleJob *Worker::job() const |
228 | { |
229 | return m_job; |
230 | } |
231 | |
232 | bool Worker::isAlive() const |
233 | { |
234 | return !m_dead; |
235 | } |
236 | |
237 | void Worker::suspend() |
238 | { |
239 | m_connection->suspend(); |
240 | } |
241 | |
242 | void Worker::resume() |
243 | { |
244 | m_connection->resume(); |
245 | } |
246 | |
247 | bool Worker::suspended() |
248 | { |
249 | return m_connection->suspended(); |
250 | } |
251 | |
252 | void Worker::send(int cmd, const QByteArray &arr) |
253 | { |
254 | m_connection->send(cmd, arr); |
255 | } |
256 | |
257 | void Worker::gotInput() |
258 | { |
259 | if (m_dead) { // already dead? then workerDied was emitted and we are done |
260 | return; |
261 | } |
262 | ref(); |
263 | if (!dispatch()) { |
264 | m_connection->close(); |
265 | m_dead = true; |
266 | QString arg = m_protocol; |
267 | if (!m_host.isEmpty()) { |
268 | arg += QLatin1String("://" ) + m_host; |
269 | } |
270 | // qDebug() << "worker died pid =" << m_pid << arg; |
271 | // Tell the job about the problem. |
272 | Q_EMIT error(ERR_WORKER_DIED, arg); |
273 | // Tell the scheduler about the problem. |
274 | Q_EMIT workerDied(worker: this); |
275 | } |
276 | deref(); |
277 | // Here we might be dead!! |
278 | } |
279 | |
280 | void Worker::kill() |
281 | { |
282 | m_dead = true; // OO can be such simple. |
283 | if (m_pid) { |
284 | qCDebug(KIO_CORE) << "killing worker process pid" << m_pid << "(" << m_protocol + QLatin1String("://" ) + m_host << ")" ; |
285 | KIOPrivate::sendTerminateSignal(pid: m_pid); |
286 | m_pid = 0; |
287 | } else if (m_workerThread) { |
288 | qCDebug(KIO_CORE) << "aborting worker thread for " << m_protocol + QLatin1String("://" ) + m_host; |
289 | m_workerThread->abort(); |
290 | } |
291 | deref(); |
292 | } |
293 | |
294 | void Worker::setHost(const QString &host, quint16 port, const QString &user, const QString &passwd) |
295 | { |
296 | m_host = host; |
297 | m_port = port; |
298 | m_user = user; |
299 | m_passwd = passwd; |
300 | m_sslMetaData.clear(); |
301 | |
302 | QByteArray data; |
303 | QDataStream stream(&data, QIODevice::WriteOnly); |
304 | stream << m_host << m_port << m_user << m_passwd; |
305 | m_connection->send(cmd: CMD_HOST, arr: data); |
306 | } |
307 | |
308 | void Worker::resetHost() |
309 | { |
310 | m_sslMetaData.clear(); |
311 | m_host = QStringLiteral("<reset>" ); |
312 | } |
313 | |
314 | void Worker::setConfig(const MetaData &config) |
315 | { |
316 | QByteArray data; |
317 | QDataStream stream(&data, QIODevice::WriteOnly); |
318 | stream << config; |
319 | m_connection->send(cmd: CMD_CONFIG, arr: data); |
320 | } |
321 | |
322 | // TODO KF6: return std::unique_ptr |
323 | Worker *Worker::createWorker(const QString &protocol, const QUrl &url, int &error, QString &error_text) |
324 | { |
325 | Q_UNUSED(url) |
326 | // qDebug() << "createWorker" << protocol << "for" << url; |
327 | // Firstly take into account all special workers |
328 | if (protocol == QLatin1String("data" )) { |
329 | return new DataProtocol(); |
330 | } |
331 | |
332 | const QString _name = KProtocolInfo::exec(protocol); |
333 | if (_name.isEmpty()) { |
334 | error_text = i18n("Unknown protocol '%1'." , protocol); |
335 | error = KIO::ERR_CANNOT_CREATE_WORKER; |
336 | return nullptr; |
337 | } |
338 | |
339 | // find the KIO worker using QPluginLoader; kioworker would do this |
340 | // anyway, but if it doesn't exist, we want to be able to return |
341 | // a useful error message immediately |
342 | QPluginLoader loader(_name); |
343 | const QString lib_path = loader.fileName(); |
344 | if (lib_path.isEmpty()) { |
345 | error_text = i18n("Can not find a KIO worker for protocol '%1'." , protocol); |
346 | error = KIO::ERR_CANNOT_CREATE_WORKER; |
347 | return nullptr; |
348 | } |
349 | |
350 | auto *worker = new Worker(protocol); |
351 | const QUrl workerAddress = worker->m_workerConnServer->address(); |
352 | if (workerAddress.isEmpty()) { |
353 | error_text = i18n("Can not create a socket for launching a KIO worker for protocol '%1'." , protocol); |
354 | error = KIO::ERR_CANNOT_CREATE_WORKER; |
355 | delete worker; |
356 | return nullptr; |
357 | } |
358 | |
359 | // Threads are enabled by default, set KIO_ENABLE_WORKER_THREADS=0 to disable them |
360 | const auto useThreads = []() { |
361 | return qgetenv(varName: "KIO_ENABLE_WORKER_THREADS" ) != "0" ; |
362 | }; |
363 | static bool bUseThreads = useThreads(); |
364 | |
365 | // Threads have performance benefits, but degrade robustness |
366 | // (a worker crashing kills the app). So let's only enable the feature for kio_file, for now. |
367 | if (protocol == QLatin1String("admin" ) || (bUseThreads && protocol == QLatin1String("file" ))) { |
368 | auto *factory = qobject_cast<WorkerFactory *>(object: loader.instance()); |
369 | if (factory) { |
370 | auto *thread = new WorkerThread(worker, factory, workerAddress.toString().toLocal8Bit()); |
371 | thread->start(); |
372 | worker->setWorkerThread(thread); |
373 | return worker; |
374 | } else { |
375 | qCWarning(KIO_CORE) << lib_path << "doesn't implement WorkerFactory?" ; |
376 | } |
377 | } |
378 | |
379 | const QStringList args = QStringList{lib_path, protocol, QString(), workerAddress.toString()}; |
380 | // qDebug() << "kioworker" << ", " << lib_path << ", " << protocol << ", " << QString() << ", " << workerAddress; |
381 | |
382 | // search paths |
383 | QStringList searchPaths = KLibexec::kdeFrameworksPaths(QStringLiteral("libexec/kf6" )); |
384 | searchPaths.append(t: QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR_KF)); // look at our installation location |
385 | QString kioworkerExecutable = QStandardPaths::findExecutable(QStringLiteral("kioworker" ), paths: searchPaths); |
386 | if (kioworkerExecutable.isEmpty()) { |
387 | // Fallback to PATH. On win32 we install to bin/ which tests outside |
388 | // KIO cannot not find at the time ctest is run because it |
389 | // isn't the same as applicationDirPath(). |
390 | kioworkerExecutable = QStandardPaths::findExecutable(QStringLiteral("kioworker" )); |
391 | } |
392 | if (kioworkerExecutable.isEmpty()) { |
393 | error_text = i18n("Can not find 'kioworker' executable at '%1'" , searchPaths.join(QLatin1String(", " ))); |
394 | error = KIO::ERR_CANNOT_CREATE_WORKER; |
395 | delete worker; |
396 | return nullptr; |
397 | } |
398 | |
399 | qint64 pid = 0; |
400 | QProcess process; |
401 | process.setProgram(kioworkerExecutable); |
402 | process.setArguments(args); |
403 | #ifdef Q_OS_UNIX |
404 | process.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors); |
405 | #endif |
406 | process.startDetached(pid: &pid); |
407 | worker->setPID(pid); |
408 | |
409 | return worker; |
410 | } |
411 | |
412 | #include "moc_worker_p.cpp" |
413 | |