1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org>
4 SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
5 SPDX-FileCopyrightText: 2007 Thiago Macieira <thiago@kde.org>
6 SPDX-FileCopyrightText: 2024 Harald Sitter <sitter@kde.org>
7
8 SPDX-License-Identifier: LGPL-2.0-or-later
9*/
10
11#include "connectionbackend_p.h"
12#include <KLocalizedString>
13#include <QCoreApplication>
14#include <QElapsedTimer>
15#include <QFile>
16#include <QLocalServer>
17#include <QLocalSocket>
18#include <QPointer>
19#include <QStandardPaths>
20#include <QTemporaryFile>
21#include <cerrno>
22
23#include "kiocoreconnectiondebug.h"
24
25using namespace KIO;
26
27ConnectionBackend::ConnectionBackend(QObject *parent)
28 : QObject(parent)
29 , state(Idle)
30 , socket(nullptr)
31 , signalEmitted(false)
32{
33 localServer = nullptr;
34}
35
36ConnectionBackend::~ConnectionBackend()
37{
38}
39
40void ConnectionBackend::setSuspended(bool enable)
41{
42 if (state != Connected) {
43 return;
44 }
45 Q_ASSERT(socket);
46 Q_ASSERT(!localServer); // !tcpServer as well
47
48 if (enable) {
49 // qCDebug(KIO_CORE) << socket << "suspending";
50 socket->setReadBufferSize(1);
51 } else {
52 // qCDebug(KIO_CORE) << socket << "resuming";
53 // Calling setReadBufferSize from a readyRead slot leads to a bug in Qt, fixed in 13c246ee119
54 socket->setReadBufferSize(StandardBufferSize);
55 if (socket->bytesAvailable() >= HeaderSize) {
56 // there are bytes available
57 QMetaObject::invokeMethod(object: this, function: &ConnectionBackend::socketReadyRead, type: Qt::QueuedConnection);
58 }
59
60 // We read all bytes here, but we don't use readAll() because we need
61 // to read at least one byte (even if there isn't any) so that the
62 // socket notifier is re-enabled
63 QByteArray data = socket->read(maxlen: socket->bytesAvailable() + 1);
64 for (int i = data.size(); --i >= 0;) {
65 socket->ungetChar(c: data[i]);
66 }
67 // Workaround Qt5 bug, readyRead isn't always emitted here...
68 QMetaObject::invokeMethod(object: this, function: &ConnectionBackend::socketReadyRead, type: Qt::QueuedConnection);
69 }
70}
71
72bool ConnectionBackend::connectToRemote(const QUrl &url)
73{
74 Q_ASSERT(state == Idle);
75 Q_ASSERT(!socket);
76 Q_ASSERT(!localServer); // !tcpServer as well
77
78 QLocalSocket *sock = new QLocalSocket(this);
79 QString path = url.path();
80 sock->connectToServer(name: path);
81 socket = sock;
82
83 connect(sender: socket, signal: &QIODevice::readyRead, context: this, slot: &ConnectionBackend::socketReadyRead);
84 connect(sender: socket, signal: &QLocalSocket::disconnected, context: this, slot: &ConnectionBackend::socketDisconnected);
85 state = Connected;
86 return true;
87}
88
89void ConnectionBackend::socketDisconnected()
90{
91 state = Idle;
92 Q_EMIT disconnected();
93}
94
95bool ConnectionBackend::listenForRemote()
96{
97 Q_ASSERT(state == Idle);
98 Q_ASSERT(!socket);
99 Q_ASSERT(!localServer); // !tcpServer as well
100
101 const QString prefix = QStandardPaths::writableLocation(type: QStandardPaths::RuntimeLocation);
102 static QBasicAtomicInt s_socketCounter = Q_BASIC_ATOMIC_INITIALIZER(1);
103 QString appName = QCoreApplication::instance()->applicationName();
104 appName.replace(before: QLatin1Char('/'), after: QLatin1Char('_')); // #357499
105 QTemporaryFile socketfile(prefix + QLatin1Char('/') + appName + QStringLiteral("XXXXXX.%1.kioworker.socket").arg(a: s_socketCounter.fetchAndAddAcquire(valueToAdd: 1)));
106 if (!socketfile.open()) {
107 errorString = i18n("Unable to create KIO worker: %1", QString::fromUtf8(strerror(errno)));
108 return false;
109 }
110
111 QString sockname = socketfile.fileName();
112 address.clear();
113 address.setScheme(QStringLiteral("local"));
114 address.setPath(path: sockname);
115 socketfile.setAutoRemove(false);
116 socketfile.remove(); // can't bind if there is such a file
117
118 localServer = new QLocalServer(this);
119 if (!localServer->listen(name: sockname)) {
120 errorString = localServer->errorString();
121 delete localServer;
122 localServer = nullptr;
123 return false;
124 }
125
126 connect(sender: localServer, signal: &QLocalServer::newConnection, context: this, slot: &ConnectionBackend::newConnection);
127
128 state = Listening;
129 return true;
130}
131
132bool ConnectionBackend::waitForIncomingTask(int ms)
133{
134 Q_ASSERT(state == Connected);
135 Q_ASSERT(socket);
136 if (socket->state() != QLocalSocket::LocalSocketState::ConnectedState) {
137 state = Idle;
138 return false; // socket has probably closed, what do we do?
139 }
140
141 signalEmitted = false;
142 if (socket->bytesAvailable()) {
143 socketReadyRead();
144 }
145 if (signalEmitted) {
146 return true; // there was enough data in the socket
147 }
148
149 // not enough data in the socket, so wait for more
150 QElapsedTimer timer;
151 timer.start();
152
153 while (socket->state() == QLocalSocket::LocalSocketState::ConnectedState && !signalEmitted && (ms == -1 || timer.elapsed() < ms)) {
154 if (!socket->waitForReadyRead(msecs: ms == -1 ? -1 : ms - timer.elapsed())) {
155 break;
156 }
157 }
158
159 if (signalEmitted) {
160 return true;
161 }
162 if (socket->state() != QLocalSocket::LocalSocketState::ConnectedState) {
163 state = Idle;
164 }
165 return false;
166}
167
168bool ConnectionBackend::sendCommand(int cmd, const QByteArray &data) const
169{
170 Q_ASSERT(state == Connected);
171 Q_ASSERT(socket);
172
173 char buffer[HeaderSize + 2];
174 sprintf(s: buffer, format: "%6zx_%2x_", static_cast<size_t>(data.size()), cmd);
175 socket->write(data: buffer, len: HeaderSize);
176 socket->write(data);
177
178 // qCDebug(KIO_CORE) << this << "Sending command" << hex << cmd << "of"
179 // << data.size() << "bytes (" << socket->bytesToWrite()
180 // << "bytes left to write )";
181
182 // blocking mode:
183 while (socket->bytesToWrite() > 0 && socket->state() == QLocalSocket::LocalSocketState::ConnectedState) {
184 socket->waitForBytesWritten(msecs: -1);
185 }
186
187 return socket->state() == QLocalSocket::LocalSocketState::ConnectedState;
188}
189
190ConnectionBackend *ConnectionBackend::nextPendingConnection()
191{
192 Q_ASSERT(state == Listening);
193 Q_ASSERT(localServer);
194 Q_ASSERT(!socket);
195
196 qCDebug(KIO_CORE_CONNECTION) << "Got a new connection";
197
198 QLocalSocket *newSocket = localServer->nextPendingConnection();
199
200 if (!newSocket) {
201 qCDebug(KIO_CORE_CONNECTION) << "... nevermind";
202 return nullptr; // there was no connection...
203 }
204
205 ConnectionBackend *result = new ConnectionBackend();
206 result->state = Connected;
207 result->socket = newSocket;
208 newSocket->setParent(result);
209 connect(sender: newSocket, signal: &QIODevice::readyRead, context: result, slot: &ConnectionBackend::socketReadyRead);
210 connect(sender: newSocket, signal: &QLocalSocket::disconnected, context: result, slot: &ConnectionBackend::socketDisconnected);
211
212 return result;
213}
214
215void ConnectionBackend::socketReadyRead()
216{
217 bool shouldReadAnother;
218 do {
219 if (!socket)
220 // might happen if the invokeMethods were delivered after we disconnected
221 {
222 return;
223 }
224
225 qCDebug(KIO_CORE_CONNECTION) << this << "Got" << socket->bytesAvailable() << "bytes";
226 if (!pendingTask.has_value()) {
227 // We have to read the header
228 char buffer[HeaderSize];
229
230 if (socket->bytesAvailable() < HeaderSize) {
231 return; // wait for more data
232 }
233
234 socket->read(data: buffer, maxlen: sizeof buffer);
235 buffer[6] = 0;
236 buffer[9] = 0;
237
238 char *p = buffer;
239 while (*p == ' ') {
240 p++;
241 }
242 auto len = strtol(nptr: p, endptr: nullptr, base: 16);
243
244 p = buffer + 7;
245 while (*p == ' ') {
246 p++;
247 }
248 auto cmd = strtol(nptr: p, endptr: nullptr, base: 16);
249
250 pendingTask = Task{.cmd = static_cast<int>(cmd), .len = len};
251
252 qCDebug(KIO_CORE_CONNECTION) << this << "Beginning of command" << pendingTask->cmd << "of size" << pendingTask->len;
253 }
254
255 QPointer<ConnectionBackend> that = this;
256
257 const auto toRead = std::min<off_t>(a: socket->bytesAvailable(), b: pendingTask->len - pendingTask->data.size());
258 qCDebug(KIO_CORE_CONNECTION) << socket << "Want to read" << toRead << "bytes; appending to already existing bytes" << pendingTask->data.size();
259 pendingTask->data += socket->read(maxlen: toRead);
260
261 if (pendingTask->data.size() == pendingTask->len) { // read all data of this task -> emit it and reset
262 signalEmitted = true;
263 qCDebug(KIO_CORE_CONNECTION) << "emitting task" << pendingTask->cmd << pendingTask->data.size();
264 Q_EMIT commandReceived(task: pendingTask.value());
265
266 pendingTask = {};
267 }
268
269 // If we're dead, better don't try anything.
270 if (that.isNull()) {
271 return;
272 }
273
274 // Do we have enough for an another read?
275 if (!pendingTask.has_value()) {
276 shouldReadAnother = socket->bytesAvailable() >= HeaderSize;
277 } else { // NOTE: if we don't have data pending we may still have a pendingTask that gets resumed when we get more data!
278 shouldReadAnother = socket->bytesAvailable();
279 }
280 } while (shouldReadAnother);
281}
282
283#include "moc_connectionbackend_p.cpp"
284

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