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 | bool 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 | |
132 | bool 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 | |
168 | bool 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 | |
190 | ConnectionBackend *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 | |
215 | void 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 | |