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
41using namespace KIO;
42
43static 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
50static constexpr int s_workerConnectionTimeoutMax = 10;
51#else
52static constexpr int s_workerConnectionTimeoutMax = 3600;
53#endif
54
55void 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
64void 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
101Worker::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
117Worker::~Worker()
118{
119 // qDebug() << "destructing worker object pid =" << m_pid;
120 delete m_workerConnServer;
121}
122
123QString Worker::protocol() const
124{
125 return m_protocol;
126}
127
128void Worker::setProtocol(const QString &protocol)
129{
130 m_protocol = protocol;
131}
132
133QString Worker::workerProtocol() const
134{
135 return m_workerProtocol;
136}
137
138QString Worker::host() const
139{
140 return m_host;
141}
142
143quint16 Worker::port() const
144{
145 return m_port;
146}
147
148QString Worker::user() const
149{
150 return m_user;
151}
152
153QString Worker::passwd() const
154{
155 return m_passwd;
156}
157
158void Worker::setIdle()
159{
160 m_idleSince.start();
161}
162
163void Worker::ref()
164{
165 m_refCount++;
166}
167
168void 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
190void Worker::aboutToDelete()
191{
192 m_connection->disconnect(receiver: this);
193 this->disconnect();
194}
195
196void Worker::setWorkerThread(WorkerThread *thread)
197{
198 m_workerThread = thread;
199}
200
201int Worker::idleTime() const
202{
203 if (!m_idleSince.isValid()) {
204 return 0;
205 }
206 return m_idleSince.elapsed() / 1000;
207}
208
209void Worker::setPID(qint64 pid)
210{
211 m_pid = pid;
212}
213
214qint64 Worker::worker_pid() const
215{
216 return m_pid;
217}
218
219void Worker::setJob(KIO::SimpleJob *job)
220{
221 if (!m_sslMetaData.isEmpty()) {
222 Q_EMIT metaData(m_sslMetaData);
223 }
224 m_job = job;
225}
226
227KIO::SimpleJob *Worker::job() const
228{
229 return m_job;
230}
231
232bool Worker::isAlive() const
233{
234 return !m_dead;
235}
236
237void Worker::suspend()
238{
239 m_connection->suspend();
240}
241
242void Worker::resume()
243{
244 m_connection->resume();
245}
246
247bool Worker::suspended()
248{
249 return m_connection->suspended();
250}
251
252void Worker::send(int cmd, const QByteArray &arr)
253{
254 m_connection->send(cmd, arr);
255}
256
257void 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
280void 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
294void 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
308void Worker::resetHost()
309{
310 m_sslMetaData.clear();
311 m_host = QStringLiteral("<reset>");
312}
313
314void 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
323Worker *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

source code of kio/src/core/worker.cpp