From cda12804562d64bbdb493bb835c75ab18a2a7cf1 Mon Sep 17 00:00:00 2001 From: Matthijs Tijink Date: Sun, 20 Jan 2019 12:23:50 +0100 Subject: [PATCH] Add connection multiplexer --- core/backends/bluetooth/CMakeLists.txt | 3 + .../bluetooth/Multiplexing protocol.md | 6 +- .../bluetooth/connectionmultiplexer.cpp | 401 ++++++++++++++++++ .../bluetooth/connectionmultiplexer.h | 149 +++++++ core/backends/bluetooth/multiplexchannel.cpp | 103 +++++ core/backends/bluetooth/multiplexchannel.h | 71 ++++ .../bluetooth/multiplexchannelstate.cpp | 23 + .../bluetooth/multiplexchannelstate.h | 101 +++++ 8 files changed, 854 insertions(+), 3 deletions(-) create mode 100644 core/backends/bluetooth/connectionmultiplexer.cpp create mode 100644 core/backends/bluetooth/connectionmultiplexer.h create mode 100644 core/backends/bluetooth/multiplexchannel.cpp create mode 100644 core/backends/bluetooth/multiplexchannel.h create mode 100644 core/backends/bluetooth/multiplexchannelstate.cpp create mode 100644 core/backends/bluetooth/multiplexchannelstate.h diff --git a/core/backends/bluetooth/CMakeLists.txt b/core/backends/bluetooth/CMakeLists.txt index 9933f3323..9f4b7ca1e 100644 --- a/core/backends/bluetooth/CMakeLists.txt +++ b/core/backends/bluetooth/CMakeLists.txt @@ -2,6 +2,9 @@ set(backends_kdeconnect_SRCS ${backends_kdeconnect_SRCS} + backends/bluetooth/multiplexchannel.cpp + backends/bluetooth/multiplexchannelstate.cpp + backends/bluetooth/connectionmultiplexer.cpp backends/bluetooth/bluetoothlinkprovider.cpp backends/bluetooth/bluetoothdevicelink.cpp backends/bluetooth/bluetoothpairinghandler.cpp diff --git a/core/backends/bluetooth/Multiplexing protocol.md b/core/backends/bluetooth/Multiplexing protocol.md index 554eb8a65..19b941a94 100644 --- a/core/backends/bluetooth/Multiplexing protocol.md +++ b/core/backends/bluetooth/Multiplexing protocol.md @@ -43,11 +43,11 @@ Where the message type can be one of the following. This message should be the first message send, and never at a later time. Its format is as follows: ``` -| MESSAGE_PROTOCOL_VERSION header | Lowest version supported | Highest version supported | -| 19 bytes (UUID ignored) | 2 bytes (Big-Endian) | 2 bytes (Big-Endian) | +| MESSAGE_PROTOCOL_VERSION header | Lowest version supported | Highest version supported | Other data | +| 19 bytes (UUID ignored) | 2 bytes (Big-Endian) | 2 bytes (Big-Endian) | Remaining data bytes | ``` -This message should be the first message to send. Use the maximum version supported by both endpoints (if any), or otherwise close the connection. +This message should be the first message to send. Use the maximum version supported by both endpoints (if any), or otherwise close the connection. The other data field is not used (and should be empty for protocol version 1), but it implies that message lengths of more than 4 need to be supported for future compatability. Currently, no client will send this message with a version other than 1, but you *must* accept and check it, for forward compatibility. diff --git a/core/backends/bluetooth/connectionmultiplexer.cpp b/core/backends/bluetooth/connectionmultiplexer.cpp new file mode 100644 index 000000000..fc532cf6d --- /dev/null +++ b/core/backends/bluetooth/connectionmultiplexer.cpp @@ -0,0 +1,401 @@ +/** + * Copyright 2019 Matthijs Tijink + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "connectionmultiplexer.h" + +#include +#include +#include +#include +#include "multiplexchannel.h" +#include "core_debug.h" +#include "multiplexchannelstate.h" + +/** + * The default channel uuid. This channel is opened implicitely (without communication). + */ +constexpr const char DEFAULT_CHANNEL_UUID[] = "a0d0aaf4-1072-4d81-aa35-902a954b1266"; + +//Message type constants +constexpr char MESSAGE_PROTOCOL_VERSION = 0; +constexpr char MESSAGE_OPEN_CHANNEL = 1; +constexpr char MESSAGE_CLOSE_CHANNEL = 2; +constexpr char MESSAGE_READ = 3; +constexpr char MESSAGE_WRITE = 4; + +ConnectionMultiplexer::ConnectionMultiplexer(QBluetoothSocket *socket, QObject *parent) : QObject(parent), mSocket{socket}, receivedProtocolVersion{false} { + connect(mSocket, &QIODevice::readyRead, this, &ConnectionMultiplexer::readyRead); + connect(mSocket, &QIODevice::aboutToClose, this, &ConnectionMultiplexer::disconnected); + connect(mSocket, &QBluetoothSocket::disconnected, this, &ConnectionMultiplexer::disconnected); + connect(mSocket, &QIODevice::bytesWritten, this, &ConnectionMultiplexer::bytesWritten); + + //Send the protocol version + QByteArray message(23, (char) 0); + message[0] = MESSAGE_PROTOCOL_VERSION; + qToBigEndian(4, &message.data()[1]); + //Leave UUID empty + //Only support version 1 (lowest supported = highest supported = 1) + qToBigEndian(1, &message.data()[19]); + qToBigEndian(1, &message.data()[21]); + to_write_bytes.append(message); + + //Send the protocol version message (queued) + QMetaObject::invokeMethod(this, &ConnectionMultiplexer::bytesWritten, Qt::QueuedConnection); + + //Always open the default channel + addChannel(QBluetoothUuid{QString{DEFAULT_CHANNEL_UUID}}); + + //Immediately check if we can read stuff ("readyRead" may not be called in that case) + if (mSocket->bytesAvailable()) { + //But invoke it queued + QMetaObject::invokeMethod(this, &ConnectionMultiplexer::readyRead, Qt::QueuedConnection); + } +} + +ConnectionMultiplexer::~ConnectionMultiplexer() { + //Always make sure we close the connection + close(); +} + +void ConnectionMultiplexer::readyRead() { + //Continue parsing messages until we need more data for another message + while (tryParseMessage()) {} +} + +void ConnectionMultiplexer::disconnected() { + //In case we get disconnected, remove all channels + for (auto &&channel : channels) { + disconnect(channel.data(), nullptr, this, nullptr); + channel->disconnected(); + } + channels.clear(); + for (auto channel : unrequested_channels) { + delete channel; + } + unrequested_channels.clear(); +} + +void ConnectionMultiplexer::close() { + //In case we want to close the connection, remove all channels + for (auto &&channel : channels) { + disconnect(channel.data(), nullptr, this, nullptr); + channel->disconnected(); + } + channels.clear(); + for (auto channel : unrequested_channels) { + delete channel; + } + unrequested_channels.clear(); + + mSocket->close(); +} + +bool ConnectionMultiplexer::isOpen() const { + return mSocket->isOpen(); +} + +bool ConnectionMultiplexer::tryParseMessage() { + mSocket->startTransaction(); + + //The message header is 19 bytes long + QByteArray header = mSocket->read(19); + if (header.size() != 19) { + mSocket->rollbackTransaction(); + return false; + } + + /** + * Parse the header: + * - message type (1 byte) + * - message length (2 bytes, Big-Endian), excludes header size + * - channel uuid (16 bytes, Big-Endian) + */ + char message_type = header[0]; + uint16_t message_length = qFromBigEndian(&header.data()[1]); + quint128 message_uuid_raw; + for (int i = 0; i < 16; ++i) message_uuid_raw.data[i] = header[3 + i]; + QBluetoothUuid message_uuid = QBluetoothUuid(message_uuid_raw); + + //Check if we have the full message including its data + QByteArray data = mSocket->read(message_length); + if (data.size() != message_length) { + mSocket->rollbackTransaction(); + return false; + } + + Q_ASSERT(receivedProtocolVersion || message_type == MESSAGE_PROTOCOL_VERSION); + + //Parse the different message types + if (message_type == MESSAGE_OPEN_CHANNEL) { + //The other endpoint requested us to open a channel + Q_ASSERT(message_length == 0); + + addChannel(message_uuid); + } else if (message_type == MESSAGE_READ) { + //The other endpoint has read some data and requests more data + Q_ASSERT(message_length == 2); + //Read the number of bytes requested (2 bytes, Big-Endian) + uint16_t additional_read = qFromBigEndian(data.data()); + Q_ASSERT(additional_read > 0); + + //Check if we haven't closed the channel in the meanwhile + // (note: different from the user's endpoint of a closed channel, since we might have outstanding buffers) + auto iter = channels.find(message_uuid); + if (iter != channels.end() && (*iter)->connected) { + auto channel = *iter; + + //We have "additional_read" more bytes we can safely write in this channel + channel->freeWriteAmount += additional_read; + mSocket->commitTransaction(); + //We might still have some data in the write buffer + Q_EMIT channel->writeAvailable(); + return true; + } + } else if (message_type == MESSAGE_WRITE) { + //The other endpoint has written data into a channel (because we requested it) + Q_ASSERT(message_length > 0); + + //Check if we haven't closed the channel in the meanwhile + // (note: different from the user's endpoint of a closed channel, since we might have outstanding buffers) + auto iter = channels.find(message_uuid); + if (iter != channels.end() && (*iter)->connected) { + auto channel = *iter; + + Q_ASSERT(channel->requestedReadAmount >= message_length); + + //We received some data, so update the buffer and the amount of outstanding read requests + channel->requestedReadAmount -= message_length; + channel->read_buffer.append(std::move(data)); + + mSocket->commitTransaction(); + //Indicate that the channel can read some bytes + Q_EMIT channel->readyRead(); + return true; + } + } else if (message_type == MESSAGE_CLOSE_CHANNEL) { + //The other endpoint wants to close a channel + Q_ASSERT(message_length == 0); + + //Check if we haven't closed the channel in the meanwhile + // (note: different from the user's endpoint of a closed channel, since we might have outstanding buffers) + auto iter = channels.find(message_uuid); + if (iter != channels.end()) { + auto channel = *iter; + + //We don't want signals anymore, since the channel is closed + disconnect(channel.data(), nullptr, this, nullptr); + removeChannel(message_uuid); + } + } else if (message_type == MESSAGE_PROTOCOL_VERSION) { + //Checks for protocol compatibility + Q_ASSERT(message_length >= 4); + //Read the lowest & highest version supported (each 2 bytes, Big-Endian) + uint16_t lowest_version = qFromBigEndian(&data.data()[0]); + uint16_t highest_version = qFromBigEndian(&data.data()[2]); + + Q_ASSERT(lowest_version == 1); + Q_ASSERT(highest_version >= 1); + receivedProtocolVersion = true; + } else { + //Other message types are not supported + Q_ASSERT(false); + } + + mSocket->commitTransaction(); + return true; +} + +QBluetoothUuid ConnectionMultiplexer::newChannel() { + //Create a random uuid + QBluetoothUuid new_id(QUuid::createUuid()); + + //Open the channel on the other endpoint + QByteArray message(3, (char) 0); + message[0] = MESSAGE_OPEN_CHANNEL; + qToBigEndian(0, &message.data()[1]); + + quint128 new_id_raw = new_id.toUInt128(); + message.append((const char*) new_id_raw.data, 16); + to_write_bytes.append(message); + + //Add the channel ourselves + addChannel(new_id); + //Write the data + bytesWritten(); + return new_id; +} + +void ConnectionMultiplexer::addChannel(QBluetoothUuid new_id) { + MultiplexChannelState *channelState = new MultiplexChannelState(); + //Connect all channels queued, so that we have opportunities to combine read/write requests + + Q_ASSERT(unrequested_channels.size() <= 20); + + //Note that none of the channels knows its own uuid, so we have to add it ourselves + connect(channelState, &MultiplexChannelState::readAvailable, this, [new_id,this] () { + channelCanRead(new_id); + }, Qt::QueuedConnection); + connect(channelState, &MultiplexChannelState::writeAvailable, this, [new_id,this] () { + channelCanWrite(new_id); + }, Qt::QueuedConnection); + connect(channelState, &MultiplexChannelState::requestClose, this, [new_id,this] () { + closeChannel(new_id); + }, Qt::QueuedConnection); + auto channelStatePtr = QSharedPointer{channelState}; + channels[new_id] = channelStatePtr; + unrequested_channels[new_id] = new MultiplexChannel{channelStatePtr}; + //Immediately ask for data in this channel + Q_EMIT channelStatePtr->readAvailable(); +} + +std::unique_ptr ConnectionMultiplexer::getChannel(QBluetoothUuid channelId) { + auto iter = unrequested_channels.find(channelId); + if (iter == unrequested_channels.end()) { + return nullptr; + } else if (!(*iter)->isOpen()) { + //Delete the channel + delete *iter; + unrequested_channels.erase(iter); + //Don't return closed channels + return nullptr; + } else { + auto channel = *iter; + unrequested_channels.erase(iter); + return std::unique_ptr{channel}; + } +} + +std::unique_ptr ConnectionMultiplexer::getDefaultChannel() { + return getChannel(QBluetoothUuid{QString{DEFAULT_CHANNEL_UUID}}); +} + +void ConnectionMultiplexer::bytesWritten() { + if (to_write_bytes.size() > 0) { + //If we have stuff to write, try to write it + auto num_written = mSocket->write(to_write_bytes); + if (num_written <= 0) { + //On error: disconnected will be called later + //On buffer full: will be retried later + return; + } else if (num_written == to_write_bytes.size()) { + to_write_bytes.clear(); + } else { + to_write_bytes.remove(0, num_written); + return; + } + } +} + +void ConnectionMultiplexer::channelCanRead(QBluetoothUuid channelId) { + auto iter = channels.find(channelId); + if (iter == channels.end()) return; + auto channel = *iter; + + //Check if we can request more data to read without overflowing the buffer + if (channel->read_buffer.size() + channel->requestedReadAmount < channel->BUFFER_SIZE) { + //Request the exact amount to fill up the buffer + auto read_amount = channel->BUFFER_SIZE - channel->requestedReadAmount - channel->read_buffer.size(); + channel->requestedReadAmount += read_amount; + + //Send a MESSAGE_READ request for more data + QByteArray message(3, (char) 0); + message[0] = MESSAGE_READ; + qToBigEndian(2, &message.data()[1]); + quint128 id_raw = channelId.toUInt128(); + message.append((const char*) id_raw.data, 16); + message.append(2, 0); + qToBigEndian(read_amount, &message.data()[19]); + to_write_bytes.append(message); + //Try to send it immediately + bytesWritten(); + } +} + +void ConnectionMultiplexer::channelCanWrite(QBluetoothUuid channelId) { + auto iter = channels.find(channelId); + if (iter == channels.end()) return; + auto channel = *iter; + + //Check if we can freely send data and we actually have some data + if (channel->write_buffer.size() > 0 && channel->freeWriteAmount > 0) { + //Figure out how much we can send now + auto amount = qMin((int) channel->write_buffer.size(), channel->freeWriteAmount); + QByteArray data = channel->write_buffer.left(amount); + channel->write_buffer.remove(0, amount); + channel->freeWriteAmount -= amount; + + //Send the data + QByteArray message(3, (char) 0); + message[0] = MESSAGE_WRITE; + qToBigEndian(amount, &message.data()[1]); + + quint128 id_raw = channelId.toUInt128(); + message.append((const char*) id_raw.data, 16); + message.append(data); + to_write_bytes.append(message); + //Try to send it immediately + bytesWritten(); + //Let the channel's users know that some data has been written + Q_EMIT channel->bytesWritten(amount); + + //If the user previously asked to close the channel and we finally managed to write the buffer, actually close it + if (channel->write_buffer.isEmpty() && channel->close_after_write) { + closeChannel(channelId); + } + } +} + +void ConnectionMultiplexer::closeChannel(QBluetoothUuid channelId) { + auto iter = channels.find(channelId); + if (iter == channels.end()) return; + auto channel = *iter; + + //If the user wants to close a channel, then the user won't be reading from it anymore + channel->read_buffer.clear(); + channel->close_after_write = true; + + //If there's still stuff to write, don't close it just yet + if (!channel->write_buffer.isEmpty()) return; + channels.erase(iter); + channel->connected = false; + + //Send the actual close channel message + QByteArray message(3, (char) 0); + message[0] = MESSAGE_CLOSE_CHANNEL; + qToBigEndian(0, &message.data()[1]); + + quint128 id_raw = channelId.toUInt128(); + message.append((const char*) id_raw.data, 16); + to_write_bytes.append(message); + //Try to send it immediately + bytesWritten(); +} + +void ConnectionMultiplexer::removeChannel(QBluetoothUuid channelId) { + auto iter = channels.find(channelId); + if (iter == channels.end()) return; + auto channel = *iter; + + //Remove the channel from the channel list + channels.erase(iter); + channel->connected = false; + + Q_EMIT channel->disconnected(); +} diff --git a/core/backends/bluetooth/connectionmultiplexer.h b/core/backends/bluetooth/connectionmultiplexer.h new file mode 100644 index 000000000..e750d0489 --- /dev/null +++ b/core/backends/bluetooth/connectionmultiplexer.h @@ -0,0 +1,149 @@ +/** + * Copyright 2019 Matthijs Tijink + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CONNECTIONMULTIPLEXER_H +#define CONNECTIONMULTIPLEXER_H + +#include +#include +#include +#include +#include + +class QBluetoothUuid; +class MultiplexChannel; +class MultiplexChannelState; +class QBluetoothSocket; + +/** + * An utility class to split a single (bluetooth) connection to multiple independent channels. + * By default (and without needing any communication with the other endpoint), a single default channel is open. + * + * Destroying/closing this object will automatically close all channels. + */ +class ConnectionMultiplexer : public QObject { + Q_OBJECT +public: + ConnectionMultiplexer(QBluetoothSocket *socket, QObject *parent = nullptr); + ~ConnectionMultiplexer(); + + /** + * Open a new channel within this connection. + * + * @return The uuid to refer to this channel. + * @see getChannel() + */ + QBluetoothUuid newChannel(); + /** + * Get the channel device for the specified channel uuid. + * If the channel does not exist, this will return a null pointer. + * + * A channel is guaranteed to exist until the first call to get it. + * @param channelId The channel uuid + * @return A shared pointer to the channel object + * @see getDefaultChannel() + */ + std::unique_ptr getChannel(QBluetoothUuid channelId); + /** + * Get the default channel. + * + * @see getChannel() + */ + std::unique_ptr getDefaultChannel(); + + /** + * Close all channels and the underlying connection. + */ + void close(); + + /** + * Check if the underlying connection is still open. + * @return True if the connection is open + * @see close() + */ + bool isOpen() const; + +private: + /** + * The underlying connection + */ + QBluetoothSocket *mSocket; + /** + * The buffer of to-be-written bytes + */ + QByteArray to_write_bytes; + /** + * The channels not requested by the user yet + */ + QHash unrequested_channels; + /** + * All channels currently open + */ + QHash> channels; + /** + * True once the other side has sent its protocol version + */ + bool receivedProtocolVersion; + + /** + * Slot for connection reading + */ + void readyRead(); + /** + * Slot for disconnection + */ + void disconnected(); + /** + * Slot for progress in writing data/new data available to be written + */ + void bytesWritten(); + /** + * Tries to parse a single connection message. + * + * @return True if a message was parsed succesfully. + */ + bool tryParseMessage(); + /** + * Add a new channel. Assumes that the communication about this channel is done + * (i.e. the other endpoint also knows this channel exists). + * + * @param new_id The channel uuid + */ + void addChannel(QBluetoothUuid new_id); + + /** + * Slot for closing a channel + */ + void closeChannel(QBluetoothUuid channelId); + /** + * Slot for writing a channel's data to the other endpoint + */ + void channelCanWrite(QBluetoothUuid channelId); + /** + * Slot for indicating that a channel can receive more data + */ + void channelCanRead(QBluetoothUuid channelId); + /** + * Slot for removing a channel from tracking + */ + void removeChannel(QBluetoothUuid channelId); +}; + +#endif diff --git a/core/backends/bluetooth/multiplexchannel.cpp b/core/backends/bluetooth/multiplexchannel.cpp new file mode 100644 index 000000000..88446500f --- /dev/null +++ b/core/backends/bluetooth/multiplexchannel.cpp @@ -0,0 +1,103 @@ +/** + * Copyright 2019 Matthijs Tijink + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "multiplexchannel.h" +#include "multiplexchannelstate.h" +#include "core_debug.h" + +MultiplexChannel::MultiplexChannel(QSharedPointer state) : state{state} { + QIODevice::open(QIODevice::ReadWrite); + + connect(this, &QIODevice::aboutToClose, state.data(), &MultiplexChannelState::requestClose); + connect(state.data(), &MultiplexChannelState::readyRead, this, &QIODevice::readyRead); + connect(state.data(), &MultiplexChannelState::bytesWritten, this, &QIODevice::bytesWritten); + connect(state.data(), &MultiplexChannelState::disconnected, this, &MultiplexChannel::disconnect); +} + +MultiplexChannel::~MultiplexChannel() {} + +bool MultiplexChannel::atEnd() const { + return !isOpen() || (!state->connected && state->read_buffer.isEmpty()); +} + +void MultiplexChannel::disconnect() { + state->connected = false; + setOpenMode(QIODevice::ReadOnly); + Q_EMIT state->readyRead(); + Q_EMIT state->requestClose(); + if (state->read_buffer.isEmpty()) { + close(); + } +} + +qint64 MultiplexChannel::bytesAvailable() const { + return state->read_buffer.size() + QIODevice::bytesAvailable(); +} + +qint64 MultiplexChannel::bytesToWrite() const { + return state->write_buffer.size() + QIODevice::bytesToWrite(); +} + +qint64 MultiplexChannel::readData(char* data, qint64 maxlen) { + if (maxlen <= state->read_buffer.size()) { + for (int i = 0; i < maxlen; ++i) { + data[i] = state->read_buffer[i]; + } + state->read_buffer.remove(0, maxlen); + Q_EMIT state->readAvailable(); + if (!state->connected && state->read_buffer.isEmpty()) { + close(); + } + return maxlen; + } else if (state->read_buffer.size() > 0) { + auto num_to_read = state->read_buffer.size(); + for (int i = 0; i < num_to_read; ++i) { + data[i] = state->read_buffer[i]; + } + state->read_buffer.remove(0, num_to_read); + Q_EMIT state->readAvailable(); + if (!state->connected && state->read_buffer.isEmpty()) { + close(); + } + return num_to_read; + } else if (isOpen() && state->connected) { + if (state->requestedReadAmount < BUFFER_SIZE) { + Q_EMIT state->readAvailable(); + } + return 0; + } else { + close(); + return -1; + } +} + +qint64 MultiplexChannel::writeData(const char* data, qint64 len) { + state->write_buffer.append(data, len); + Q_EMIT state->writeAvailable(); + return len; +} + +bool MultiplexChannel::canReadLine() const { + return isReadable() && (QIODevice::canReadLine() || state->read_buffer.contains('\n')); +} + +bool MultiplexChannel::isSequential() const { + return true; +} diff --git a/core/backends/bluetooth/multiplexchannel.h b/core/backends/bluetooth/multiplexchannel.h new file mode 100644 index 000000000..3cf16e8ef --- /dev/null +++ b/core/backends/bluetooth/multiplexchannel.h @@ -0,0 +1,71 @@ +/** + * Copyright 2019 Matthijs Tijink + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MULTIPLEXCHANNEL_H +#define MULTIPLEXCHANNEL_H + +#include +#include + +class ConnectionMultiplexer; +class MultiplexChannelState; + +/** + * Represents a single channel in a multiplexed connection + * + * @see ConnectionMultiplexer + * @see ConnectionMultiplexer::getChannel + */ +class MultiplexChannel : public QIODevice { + Q_OBJECT + +private: + /** + * You cannot construct a MultiplexChannel yourself, use the ConnectionMultiplexer + */ + MultiplexChannel(QSharedPointer state); +public: + ~MultiplexChannel(); + + constexpr static int BUFFER_SIZE = 4096; + + bool canReadLine() const override; + bool atEnd() const override; + qint64 bytesAvailable() const override; + qint64 bytesToWrite() const override; + + bool isSequential() const override; + +protected: + qint64 readData(char * data, qint64 maxlen) override; + qint64 writeData(const char * data, qint64 len) override; + +private: + QSharedPointer state; + + /** + * Disconnects the channel + */ + void disconnect(); + + friend class ConnectionMultiplexer; +}; + +#endif diff --git a/core/backends/bluetooth/multiplexchannelstate.cpp b/core/backends/bluetooth/multiplexchannelstate.cpp new file mode 100644 index 000000000..06dc09cb6 --- /dev/null +++ b/core/backends/bluetooth/multiplexchannelstate.cpp @@ -0,0 +1,23 @@ +/** + * Copyright 2019 Matthijs Tijink + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "multiplexchannelstate.h" + +MultiplexChannelState::MultiplexChannelState() : requestedReadAmount{0}, freeWriteAmount{0}, connected{true}, close_after_write{false} {} diff --git a/core/backends/bluetooth/multiplexchannelstate.h b/core/backends/bluetooth/multiplexchannelstate.h new file mode 100644 index 000000000..76cf8b684 --- /dev/null +++ b/core/backends/bluetooth/multiplexchannelstate.h @@ -0,0 +1,101 @@ +/** + * Copyright 2019 Matthijs Tijink + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MULTIPLEXCHANNELSTATE_H +#define MULTIPLEXCHANNELSTATE_H + +#include +#include + +class ConnectionMultiplexer; +class MultiplexChannel; + +/** + * Represents a single channel in a multiplexed connection + * + * @internal + * @see ConnectionMultiplexer + */ +class MultiplexChannelState : public QObject { + Q_OBJECT + +private: + MultiplexChannelState(); +public: + ~MultiplexChannelState() = default; + + constexpr static int BUFFER_SIZE = 4096; + +private: + /** + * The read buffer (already read from underlying connection but not read by the user of the channel) + */ + QByteArray read_buffer; + /** + * The write buffer (already written by the user of the channel, but not to the underlying connection yet) + */ + QByteArray write_buffer; + /** + * The amount of bytes requested to the other endpoint + */ + int requestedReadAmount; + /** + * The amount of bytes the other endpoint requested + */ + int freeWriteAmount; + /** + * True if the channel is still connected in the underlying connection + */ + bool connected; + /** + * True if the channel needs to be closed after writing the buffer + */ + bool close_after_write; + friend class ConnectionMultiplexer; + friend class MultiplexChannel; + +Q_SIGNALS: + /** + * Emitted if there are new bytes available to be written + */ + void writeAvailable(); + /** + * Emitted if the channel has buffer room for more bytes to be read + */ + void readAvailable(); + /** + * Emitted if the channel bytes ready for reading + */ + void readyRead(); + /** + * Emitted if some bytes of the channel have been written + */ + void bytesWritten(qint64 amount); + /** + * Emitted if the user requested to close the channel + */ + void requestClose(); + /** + * Only fired on disconnections + */ + void disconnected(); +}; + +#endif