/* This file is part of the KDE libraries SPDX-FileCopyrightText: 2000 Stephan Kulow SPDX-FileCopyrightText: 2000 David Faure SPDX-FileCopyrightText: 2007 Thiago Macieira SPDX-FileCopyrightText: 2024 Harald Sitter SPDX-License-Identifier: LGPL-2.0-or-later */ #include "connectionbackend_p.h" #include #include #include #include #include #include #include #include #include #include #include "kiocoreconnectiondebug.h" using namespace KIO; ConnectionBackend::ConnectionBackend(QObject *parent) : QObject(parent) , state(Idle) , socket(nullptr) , signalEmitted(false) { localServer = nullptr; } ConnectionBackend::~ConnectionBackend() { } void ConnectionBackend::setSuspended(bool enable) { if (state != Connected) { return; } Q_ASSERT(socket); Q_ASSERT(!localServer); // !tcpServer as well if (enable) { // qCDebug(KIO_CORE) << socket << "suspending"; socket->setReadBufferSize(1); } else { // qCDebug(KIO_CORE) << socket << "resuming"; // Calling setReadBufferSize from a readyRead slot leads to a bug in Qt, fixed in 13c246ee119 socket->setReadBufferSize(StandardBufferSize); if (socket->bytesAvailable() >= HeaderSize) { // there are bytes available QMetaObject::invokeMethod(this, &ConnectionBackend::socketReadyRead, Qt::QueuedConnection); } // We read all bytes here, but we don't use readAll() because we need // to read at least one byte (even if there isn't any) so that the // socket notifier is re-enabled QByteArray data = socket->read(socket->bytesAvailable() + 1); for (int i = data.size(); --i >= 0;) { socket->ungetChar(data[i]); } // Workaround Qt5 bug, readyRead isn't always emitted here... QMetaObject::invokeMethod(this, &ConnectionBackend::socketReadyRead, Qt::QueuedConnection); } } bool ConnectionBackend::connectToRemote(const QUrl &url) { Q_ASSERT(state == Idle); Q_ASSERT(!socket); Q_ASSERT(!localServer); // !tcpServer as well QLocalSocket *sock = new QLocalSocket(this); QString path = url.path(); sock->connectToServer(path); socket = sock; connect(socket, &QIODevice::readyRead, this, &ConnectionBackend::socketReadyRead); connect(socket, &QLocalSocket::disconnected, this, &ConnectionBackend::socketDisconnected); state = Connected; return true; } void ConnectionBackend::socketDisconnected() { state = Idle; Q_EMIT disconnected(); } ConnectionBackend::ConnectionResult ConnectionBackend::listenForRemote() { Q_ASSERT(state == Idle); Q_ASSERT(!socket); Q_ASSERT(!localServer); // !tcpServer as well const QString prefix = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation); static QBasicAtomicInt s_socketCounter = Q_BASIC_ATOMIC_INITIALIZER(1); QString appName = QCoreApplication::instance()->applicationName(); appName.replace(QLatin1Char('/'), QLatin1Char('_')); // #357499 QTemporaryFile socketfile(prefix + QLatin1Char('/') + appName + QStringLiteral("XXXXXX.%1.kioworker.socket").arg(s_socketCounter.fetchAndAddAcquire(1))); if (!socketfile.open()) { return {false, i18n("Unable to create KIO worker: %1", QString::fromUtf8(strerror(errno)))}; } QString sockname = socketfile.fileName(); address.clear(); address.setScheme(QStringLiteral("local")); address.setPath(sockname); socketfile.setAutoRemove(false); socketfile.remove(); // can't bind if there is such a file localServer = new QLocalServer(this); if (!localServer->listen(sockname)) { errorString = localServer->errorString(); delete localServer; localServer = nullptr; return {false, errorString}; } connect(localServer, &QLocalServer::newConnection, this, &ConnectionBackend::newConnection); state = Listening; return {true, QString()}; } bool ConnectionBackend::waitForIncomingTask(int ms) { Q_ASSERT(state == Connected); Q_ASSERT(socket); if (socket->state() != QLocalSocket::LocalSocketState::ConnectedState) { state = Idle; return false; // socket has probably closed, what do we do? } signalEmitted = false; if (socket->bytesAvailable()) { socketReadyRead(); } if (signalEmitted) { return true; // there was enough data in the socket } // not enough data in the socket, so wait for more QElapsedTimer timer; timer.start(); while (socket->state() == QLocalSocket::LocalSocketState::ConnectedState && !signalEmitted && (ms == -1 || timer.elapsed() < ms)) { if (!socket->waitForReadyRead(ms == -1 ? -1 : ms - timer.elapsed())) { break; } } if (signalEmitted) { return true; } if (socket->state() != QLocalSocket::LocalSocketState::ConnectedState) { state = Idle; } return false; } bool ConnectionBackend::sendCommand(int cmd, const QByteArray &data) const { Q_ASSERT(state == Connected); Q_ASSERT(socket); char buffer[HeaderSize + 2]; sprintf(buffer, "%6zx_%2x_", static_cast(data.size()), cmd); socket->write(buffer, HeaderSize); socket->write(data); // qCDebug(KIO_CORE) << this << "Sending command" << hex << cmd << "of" // << data.size() << "bytes (" << socket->bytesToWrite() // << "bytes left to write )"; // blocking mode: while (socket->bytesToWrite() > 0 && socket->state() == QLocalSocket::LocalSocketState::ConnectedState) { socket->waitForBytesWritten(-1); } if (socket->state() != QLocalSocket::LocalSocketState::ConnectedState) { qCWarning(KIO_CORE_CONNECTION) << "Socket not connected" << socket->error(); } return socket->state() == QLocalSocket::LocalSocketState::ConnectedState; } ConnectionBackend *ConnectionBackend::nextPendingConnection() { Q_ASSERT(state == Listening); Q_ASSERT(localServer); Q_ASSERT(!socket); qCDebug(KIO_CORE_CONNECTION) << "Got a new connection"; QLocalSocket *newSocket = localServer->nextPendingConnection(); if (!newSocket) { qCDebug(KIO_CORE_CONNECTION) << "... nevermind"; return nullptr; // there was no connection... } ConnectionBackend *result = new ConnectionBackend(); result->state = Connected; result->socket = newSocket; newSocket->setParent(result); connect(newSocket, &QIODevice::readyRead, result, &ConnectionBackend::socketReadyRead); connect(newSocket, &QLocalSocket::disconnected, result, &ConnectionBackend::socketDisconnected); return result; } void ConnectionBackend::socketReadyRead() { bool shouldReadAnother; do { if (!socket) // might happen if the invokeMethods were delivered after we disconnected { return; } qCDebug(KIO_CORE_CONNECTION) << this << "Got" << socket->bytesAvailable() << "bytes"; if (!pendingTask.has_value()) { // We have to read the header char buffer[HeaderSize]; if (socket->bytesAvailable() < HeaderSize) { return; // wait for more data } socket->read(buffer, sizeof buffer); buffer[6] = 0; buffer[9] = 0; char *p = buffer; while (*p == ' ') { p++; } auto len = strtol(p, nullptr, 16); p = buffer + 7; while (*p == ' ') { p++; } auto cmd = strtol(p, nullptr, 16); pendingTask = Task{.cmd = static_cast(cmd), .len = len}; qCDebug(KIO_CORE_CONNECTION) << this << "Beginning of command" << pendingTask->cmd << "of size" << pendingTask->len; } QPointer that = this; const auto toRead = std::min(socket->bytesAvailable(), pendingTask->len - pendingTask->data.size()); qCDebug(KIO_CORE_CONNECTION) << socket << "Want to read" << toRead << "bytes; appending to already existing bytes" << pendingTask->data.size(); pendingTask->data += socket->read(toRead); if (pendingTask->data.size() == pendingTask->len) { // read all data of this task -> emit it and reset signalEmitted = true; qCDebug(KIO_CORE_CONNECTION) << "emitting task" << pendingTask->cmd << pendingTask->data.size(); Q_EMIT commandReceived(pendingTask.value()); pendingTask = {}; } // If we're dead, better don't try anything. if (that.isNull()) { return; } // Do we have enough for an another read? if (!pendingTask.has_value()) { shouldReadAnother = socket->bytesAvailable() >= HeaderSize; } else { // NOTE: if we don't have data pending we may still have a pendingTask that gets resumed when we get more data! shouldReadAnother = socket->bytesAvailable(); } } while (shouldReadAnother); } #include "moc_connectionbackend_p.cpp"