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 | |
25 | using namespace KIO; |
26 | |
27 | ConnectionBackend::ConnectionBackend(QObject *parent) |
28 | : QObject(parent) |
29 | , state(Idle) |
30 | , socket(nullptr) |
31 | , signalEmitted(false) |
32 | { |
33 | localServer = nullptr; |
34 | } |
35 | |
36 | ConnectionBackend::~ConnectionBackend() |
37 | { |
38 | } |
39 | |
40 | void 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 | |
72 | bool 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 | |
89 | void ConnectionBackend::socketDisconnected() |
90 | { |
91 | state = Idle; |
92 | Q_EMIT disconnected(); |
93 | } |
94 | |
95 | ConnectionBackend::ConnectionResult 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 | return {.success: false, i18n("Unable to create KIO worker: %1" , QString::fromUtf8(strerror(errno)))}; |
108 | } |
109 | |
110 | QString sockname = socketfile.fileName(); |
111 | address.clear(); |
112 | address.setScheme(QStringLiteral("local" )); |
113 | address.setPath(path: sockname); |
114 | socketfile.setAutoRemove(false); |
115 | socketfile.remove(); // can't bind if there is such a file |
116 | |
117 | localServer = new QLocalServer(this); |
118 | if (!localServer->listen(name: sockname)) { |
119 | errorString = localServer->errorString(); |
120 | delete localServer; |
121 | localServer = nullptr; |
122 | return {.success: false, .error: errorString}; |
123 | } |
124 | |
125 | connect(sender: localServer, signal: &QLocalServer::newConnection, context: this, slot: &ConnectionBackend::newConnection); |
126 | |
127 | state = Listening; |
128 | return {.success: true, .error: QString()}; |
129 | } |
130 | |
131 | bool ConnectionBackend::waitForIncomingTask(int ms) |
132 | { |
133 | Q_ASSERT(state == Connected); |
134 | Q_ASSERT(socket); |
135 | if (socket->state() != QLocalSocket::LocalSocketState::ConnectedState) { |
136 | state = Idle; |
137 | return false; // socket has probably closed, what do we do? |
138 | } |
139 | |
140 | signalEmitted = false; |
141 | if (socket->bytesAvailable()) { |
142 | socketReadyRead(); |
143 | } |
144 | if (signalEmitted) { |
145 | return true; // there was enough data in the socket |
146 | } |
147 | |
148 | // not enough data in the socket, so wait for more |
149 | QElapsedTimer timer; |
150 | timer.start(); |
151 | |
152 | while (socket->state() == QLocalSocket::LocalSocketState::ConnectedState && !signalEmitted && (ms == -1 || timer.elapsed() < ms)) { |
153 | if (!socket->waitForReadyRead(msecs: ms == -1 ? -1 : ms - timer.elapsed())) { |
154 | break; |
155 | } |
156 | } |
157 | |
158 | if (signalEmitted) { |
159 | return true; |
160 | } |
161 | if (socket->state() != QLocalSocket::LocalSocketState::ConnectedState) { |
162 | state = Idle; |
163 | } |
164 | return false; |
165 | } |
166 | |
167 | bool ConnectionBackend::sendCommand(int cmd, const QByteArray &data) const |
168 | { |
169 | Q_ASSERT(state == Connected); |
170 | Q_ASSERT(socket); |
171 | |
172 | char buffer[HeaderSize + 2]; |
173 | sprintf(s: buffer, format: "%6zx_%2x_" , static_cast<size_t>(data.size()), cmd); |
174 | socket->write(data: buffer, len: HeaderSize); |
175 | socket->write(data); |
176 | |
177 | // qCDebug(KIO_CORE) << this << "Sending command" << hex << cmd << "of" |
178 | // << data.size() << "bytes (" << socket->bytesToWrite() |
179 | // << "bytes left to write )"; |
180 | |
181 | // blocking mode: |
182 | while (socket->bytesToWrite() > 0 && socket->state() == QLocalSocket::LocalSocketState::ConnectedState) { |
183 | socket->waitForBytesWritten(msecs: -1); |
184 | } |
185 | |
186 | if (socket->state() != QLocalSocket::LocalSocketState::ConnectedState) { |
187 | qCWarning(KIO_CORE_CONNECTION) << "Socket not connected" << socket->error(); |
188 | } |
189 | |
190 | return socket->state() == QLocalSocket::LocalSocketState::ConnectedState; |
191 | } |
192 | |
193 | ConnectionBackend *ConnectionBackend::nextPendingConnection() |
194 | { |
195 | Q_ASSERT(state == Listening); |
196 | Q_ASSERT(localServer); |
197 | Q_ASSERT(!socket); |
198 | |
199 | qCDebug(KIO_CORE_CONNECTION) << "Got a new connection" ; |
200 | |
201 | QLocalSocket *newSocket = localServer->nextPendingConnection(); |
202 | |
203 | if (!newSocket) { |
204 | qCDebug(KIO_CORE_CONNECTION) << "... nevermind" ; |
205 | return nullptr; // there was no connection... |
206 | } |
207 | |
208 | ConnectionBackend *result = new ConnectionBackend(); |
209 | result->state = Connected; |
210 | result->socket = newSocket; |
211 | newSocket->setParent(result); |
212 | connect(sender: newSocket, signal: &QIODevice::readyRead, context: result, slot: &ConnectionBackend::socketReadyRead); |
213 | connect(sender: newSocket, signal: &QLocalSocket::disconnected, context: result, slot: &ConnectionBackend::socketDisconnected); |
214 | |
215 | return result; |
216 | } |
217 | |
218 | void ConnectionBackend::socketReadyRead() |
219 | { |
220 | bool shouldReadAnother; |
221 | do { |
222 | if (!socket) |
223 | // might happen if the invokeMethods were delivered after we disconnected |
224 | { |
225 | return; |
226 | } |
227 | |
228 | qCDebug(KIO_CORE_CONNECTION) << this << "Got" << socket->bytesAvailable() << "bytes" ; |
229 | if (!pendingTask.has_value()) { |
230 | // We have to read the header |
231 | char buffer[HeaderSize]; |
232 | |
233 | if (socket->bytesAvailable() < HeaderSize) { |
234 | return; // wait for more data |
235 | } |
236 | |
237 | socket->read(data: buffer, maxlen: sizeof buffer); |
238 | buffer[6] = 0; |
239 | buffer[9] = 0; |
240 | |
241 | char *p = buffer; |
242 | while (*p == ' ') { |
243 | p++; |
244 | } |
245 | auto len = strtol(nptr: p, endptr: nullptr, base: 16); |
246 | |
247 | p = buffer + 7; |
248 | while (*p == ' ') { |
249 | p++; |
250 | } |
251 | auto cmd = strtol(nptr: p, endptr: nullptr, base: 16); |
252 | |
253 | pendingTask = Task{.cmd = static_cast<int>(cmd), .len = len}; |
254 | |
255 | qCDebug(KIO_CORE_CONNECTION) << this << "Beginning of command" << pendingTask->cmd << "of size" << pendingTask->len; |
256 | } |
257 | |
258 | QPointer<ConnectionBackend> that = this; |
259 | |
260 | const auto toRead = std::min<off_t>(a: socket->bytesAvailable(), b: pendingTask->len - pendingTask->data.size()); |
261 | qCDebug(KIO_CORE_CONNECTION) << socket << "Want to read" << toRead << "bytes; appending to already existing bytes" << pendingTask->data.size(); |
262 | pendingTask->data += socket->read(maxlen: toRead); |
263 | |
264 | if (pendingTask->data.size() == pendingTask->len) { // read all data of this task -> emit it and reset |
265 | signalEmitted = true; |
266 | qCDebug(KIO_CORE_CONNECTION) << "emitting task" << pendingTask->cmd << pendingTask->data.size(); |
267 | Q_EMIT commandReceived(task: pendingTask.value()); |
268 | |
269 | pendingTask = {}; |
270 | } |
271 | |
272 | // If we're dead, better don't try anything. |
273 | if (that.isNull()) { |
274 | return; |
275 | } |
276 | |
277 | // Do we have enough for an another read? |
278 | if (!pendingTask.has_value()) { |
279 | shouldReadAnother = socket->bytesAvailable() >= HeaderSize; |
280 | } else { // NOTE: if we don't have data pending we may still have a pendingTask that gets resumed when we get more data! |
281 | shouldReadAnother = socket->bytesAvailable(); |
282 | } |
283 | } while (shouldReadAnother); |
284 | } |
285 | |
286 | #include "moc_connectionbackend_p.cpp" |
287 | |