feat(socket): add unix socket listener

This commit is contained in:
outfoxxed 2024-03-02 05:05:45 -08:00
parent bb5bc0547a
commit 83a0ec6fc6
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
10 changed files with 479 additions and 0 deletions

View file

@ -5,11 +5,16 @@ set(QT_MIN_VERSION "6.6.0")
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(TESTS "Build tests" OFF)
option(SOCKETS "Enable unix socket support" ON)
option(WAYLAND "Enable wayland support" ON)
option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON)
option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON)
message(STATUS "Quickshell configuration")
message(STATUS " Build tests: ${TESTS}")
message(STATUS " Sockets: ${SOCKETS}")
message(STATUS " Wayland: ${WAYLAND}")
if (WAYLAND)
message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}")
@ -31,6 +36,16 @@ endif()
set(QT_DEPS Qt6::Gui Qt6::Qml Qt6::Quick Qt6::QuickControls2)
set(QT_FPDEPS Gui Qml Quick QuickControls2)
if (TESTS)
enable_testing()
list(APPEND QT_FPDEPS Test)
endif()
if (SOCKETS)
list(APPEND QT_DEPS Qt6::Network)
list(APPEND QT_FPDEPS Network)
endif()
if (WAYLAND)
list(APPEND QT_DEPS Qt6::WaylandClient Qt6::WaylandClientPrivate)
list(APPEND QT_FPDEPS WaylandClient)

View file

@ -25,3 +25,6 @@ clean:
run *ARGS='': build
{{builddir}}/src/core/quickshell {{ARGS}}
test *ARGS='': build
ctest --test-dir build --output-on-failure {{ARGS}}

View file

@ -15,8 +15,17 @@ qt_add_executable(quickshell
windowinterface.cpp
floatingwindow.cpp
panelinterface.cpp
datastream.cpp
)
qt_add_qml_module(quickshell URI Quickshell)
if (SOCKETS)
target_sources(quickshell PRIVATE socket.cpp)
endif()
target_link_libraries(quickshell PRIVATE ${QT_DEPS})
if (TESTS)
add_subdirectory(test)
endif()

108
src/core/datastream.cpp Normal file
View file

@ -0,0 +1,108 @@
#include "datastream.hpp"
#include <algorithm>
#include <utility>
#include <qlocalsocket.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qtypes.h>
DataStreamParser* DataStream::reader() const { return this->mReader; }
void DataStream::setReader(DataStreamParser* reader) {
if (reader == this->mReader) return;
if (this->mReader != nullptr) {
QObject::disconnect(this->mReader, nullptr, this, nullptr);
}
this->mReader = reader;
if (reader != nullptr) {
QObject::connect(reader, &QObject::destroyed, this, &DataStream::onReaderDestroyed);
}
emit this->readerChanged();
if (reader != nullptr && !this->buffer.isEmpty()) {
reader->parseBytes(this->buffer, this->buffer);
}
}
void DataStream::onReaderDestroyed() {
this->mReader = nullptr;
emit this->readerChanged();
}
void DataStream::onBytesAvailable() {
auto buf = this->ioDevice()->readAll();
this->mReader->parseBytes(buf, this->buffer);
}
void SplitParser::parseBytes(QByteArray& incoming, QByteArray& buffer) {
if (this->mSplitMarker.isEmpty()) {
if (!buffer.isEmpty()) {
emit this->read(QString(buffer));
buffer.clear();
}
emit this->read(QString(incoming));
return;
}
// make sure we dont miss any delimiters in the buffer if the delimiter changes
if (this->mSplitMarkerChanged) {
this->mSplitMarkerChanged = false;
this->parseBytes(buffer, buffer);
}
auto marker = this->mSplitMarker.toUtf8();
auto mlen = marker.length();
auto blen = buffer.size();
auto ilen = incoming.size();
qsizetype start = &incoming == &buffer ? 0 : -blen;
for (auto readi = -std::min(blen, mlen - 1); readi <= ilen - mlen; readi++) {
for (auto marki = 0; marki < mlen; marki++) {
qint8 byte; // NOLINT
if (readi + marki < 0) byte = buffer[blen + readi + marki];
else byte = incoming[readi + marki];
if (byte != marker[marki]) goto fail;
}
{
QByteArray slice;
if (start < 0) slice = buffer.sliced(0, std::min(blen, blen + readi));
if (readi > 0) {
auto sstart = std::max(static_cast<qsizetype>(0), start);
slice.append(incoming.sliced(sstart, readi - sstart));
}
readi += mlen;
start = readi;
emit this->read(QString(slice));
}
fail:;
}
if (start < 0) {
buffer.append(incoming);
} else {
// Will break the init case if inlined. Must be before clear.
auto slice = incoming.sliced(start);
buffer.clear();
buffer.insert(0, slice);
}
}
QString SplitParser::splitMarker() const { return this->mSplitMarker; }
void SplitParser::setSplitMarker(QString marker) {
if (marker == this->mSplitMarker) return;
this->mSplitMarker = std::move(marker);
this->mSplitMarkerChanged = true;
emit this->splitMarkerChanged();
}

90
src/core/datastream.hpp Normal file
View file

@ -0,0 +1,90 @@
#pragma once
#include <qbytearray.h>
#include <qlocalsocket.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qvariant.h>
class DataStreamParser;
///! Data source that can be streamed into a parser.
/// See also: [DataStreamParser](../datastreamparser)
class DataStream: public QObject {
Q_OBJECT;
/// The parser to stream data from this source into.
/// If the parser is null no data will be read.
Q_PROPERTY(DataStreamParser* parser READ reader WRITE setReader NOTIFY readerChanged);
QML_ELEMENT;
QML_UNCREATABLE("base class");
public:
explicit DataStream(QObject* parent = nullptr): QObject(parent) {}
[[nodiscard]] DataStreamParser* reader() const;
void setReader(DataStreamParser* reader);
signals:
void readerChanged();
public slots:
void onBytesAvailable();
protected:
[[nodiscard]] virtual QIODevice* ioDevice() const = 0;
private slots:
void onReaderDestroyed();
private:
DataStreamParser* mReader = nullptr;
QByteArray buffer;
};
///! Parser for streamed input data.
/// See also: [DataStream](../datastream), [SplitParser](../splitparser)
class DataStreamParser: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_UNCREATABLE("base class");
public:
explicit DataStreamParser(QObject* parent = nullptr): QObject(parent) {}
// the buffer will be sent in both slots if there is data remaining from a previous parser
virtual void parseBytes(QByteArray& incoming, QByteArray& buffer) = 0;
signals:
/// Emitted when data is read from the stream.
void read(QString data);
};
///! Parser for delimited data streams.
/// Parser for delimited data streams. [read()] is emitted once per delimited chunk of the stream.
///
/// [read()]: ../datastreamparser#sig.read
class SplitParser: public DataStreamParser {
Q_OBJECT;
/// The delimiter for parsed data. May be multiple characters. Defaults to `\n`.
///
/// If the delimiter is empty read lengths may be arbitrary (whatever is returned by the
/// underlying read call.)
Q_PROPERTY(QString splitMarker READ splitMarker WRITE setSplitMarker NOTIFY splitMarkerChanged);
QML_ELEMENT;
public:
explicit SplitParser(QObject* parent = nullptr): DataStreamParser(parent) {}
void parseBytes(QByteArray& incoming, QByteArray& buffer) override;
[[nodiscard]] QString splitMarker() const;
void setSplitMarker(QString marker);
signals:
void splitMarkerChanged();
private:
QString mSplitMarker = "\n";
bool mSplitMarkerChanged = false;
};

View file

@ -12,5 +12,7 @@ headers = [
"windowinterface.hpp",
"panelinterface.hpp",
"floatingwindow.hpp",
"datastream.hpp",
"socket.hpp",
]
-----

77
src/core/socket.cpp Normal file
View file

@ -0,0 +1,77 @@
#include "socket.hpp"
#include <utility>
#include <qlocalsocket.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include "datastream.hpp"
void Socket::setSocket(QLocalSocket* socket) {
if (this->socket != nullptr) this->socket->deleteLater();
this->socket = socket;
socket->setParent(this);
if (socket != nullptr) {
// clang-format off
QObject::connect(this->socket, &QLocalSocket::connected, this, &Socket::onSocketConnected);
QObject::connect(this->socket, &QLocalSocket::disconnected, this, &Socket::onSocketDisconnected);
QObject::connect(this->socket, &QLocalSocket::errorOccurred, this, &Socket::error);
QObject::connect(this->socket, &QLocalSocket::readyRead, this, &DataStream::onBytesAvailable);
// clang-format on
if (socket->isOpen()) this->onSocketConnected();
}
}
QString Socket::path() const { return this->mPath; }
void Socket::setPath(QString path) {
if ((this->connected && !this->disconnecting) || path == this->mPath) return;
this->mPath = std::move(path);
emit this->pathChanged();
if (this->targetConnected && this->socket == nullptr) this->connectPathSocket();
}
void Socket::onSocketConnected() {
this->connected = true;
this->targetConnected = false;
this->disconnecting = false;
emit this->connectionStateChanged();
}
void Socket::onSocketDisconnected() {
this->connected = false;
this->disconnecting = false;
this->socket->deleteLater();
this->socket = nullptr;
emit this->connectionStateChanged();
if (this->targetConnected) this->connectPathSocket();
}
bool Socket::isConnected() const { return this->connected; }
void Socket::setConnected(bool connected) {
this->targetConnected = connected;
if (!connected) {
if (this->socket != nullptr && !this->disconnecting) {
this->disconnecting = true;
this->socket->disconnectFromServer();
}
} else if (this->socket == nullptr) this->connectPathSocket();
}
QIODevice* Socket::ioDevice() const { return this->socket; }
void Socket::connectPathSocket() {
if (!this->mPath.isEmpty()) {
auto* socket = new QLocalSocket();
socket->setServerName(this->mPath);
this->setSocket(socket);
this->socket->connectToServer(QIODevice::ReadWrite);
}
}

58
src/core/socket.hpp Normal file
View file

@ -0,0 +1,58 @@
#pragma once
#include <qlocalsocket.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include "datastream.hpp"
///! Unix socket listener.
class Socket: public DataStream {
Q_OBJECT;
/// Returns if the socket is currently connected.
///
/// Writing to this property will set the target connection state and will not
/// update the property immediately. Setting the property to false will begin disconnecting
/// the socket, and setting it to true will begin connecting the socket if path is not empty.
Q_PROPERTY(bool connected READ isConnected WRITE setConnected NOTIFY connectionStateChanged);
/// The path to connect this socket to when `connected` is set to true.
///
/// Changing this property will have no effect while the connection is active.
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged);
QML_ELEMENT;
public:
explicit Socket(QObject* parent = nullptr): DataStream(parent) {}
// takes ownership
void setSocket(QLocalSocket* socket);
[[nodiscard]] bool isConnected() const;
void setConnected(bool connected);
[[nodiscard]] QString path() const;
void setPath(QString path);
signals:
/// This signal is sent whenever a socket error is encountered.
void error(QLocalSocket::LocalSocketError error);
void connectionStateChanged();
void pathChanged();
protected:
[[nodiscard]] QIODevice* ioDevice() const override;
private slots:
void onSocketConnected();
void onSocketDisconnected();
private:
void connectPathSocket();
QLocalSocket* socket = nullptr;
bool connected = false;
bool disconnecting = false;
bool targetConnected = false;
QString mPath;
};

View file

@ -0,0 +1,7 @@
function (qs_test name)
add_executable(${name} ${ARGN})
target_link_libraries(${name} PRIVATE ${QT_DEPS} Qt6::Test)
add_test(NAME ${name} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" COMMAND $<TARGET_FILE:${name}>)
endfunction()
qs_test(datastream datastream.cpp ../datastream.cpp)

View file

@ -0,0 +1,110 @@
#include "../datastream.hpp"
#include <qbytearray.h>
#include <qlist.h>
#include <qlogging.h>
#include <qobject.h>
#include <qsignalspy.h>
#include <qtest.h>
#include <qtestcase.h>
#include <qtmetamacros.h>
class TestSplitParser: public QObject {
Q_OBJECT;
private slots:
void splits_data() { // NOLINT
QTest::addColumn<QString>("mark");
QTest::addColumn<QString>("buffer"); // max that can go in the buffer
QTest::addColumn<QString>("incoming"); // data that has to be tested on the end in one go
QTest::addColumn<QList<QString>>("results");
QTest::addColumn<QString>("remainder");
// NOLINTBEGIN
// clang-format off
QTest::addRow("simple") << "-"
<< "foo" << "-"
<< QList<QString>("foo") << "";
QTest::addRow("multiple") << "-"
<< "foo" << "-bar-baz-"
<< QList<QString>({ "foo", "bar", "baz" }) << "";
QTest::addRow("incomplete") << "-"
<< "foo" << "-bar-baz"
<< QList<QString>({ "foo", "bar" }) << "baz";
QTest::addRow("longsplit") << "12345"
<< "foo1234" << "5bar12345"
<< QList<QString>({ "foo", "bar" }) << "";
QTest::addRow("longsplit-incomplete") << "123"
<< "foo12" << "3bar123baz"
<< QList<QString>({ "foo", "bar" }) << "baz";
// clang-format on
// NOLINTEND
}
void splits() { // NOLINT
// NOLINTBEGIN
QFETCH(QString, mark);
QFETCH(QString, buffer);
QFETCH(QString, incoming);
QFETCH(QList<QString>, results);
QFETCH(QString, remainder);
// NOLINTEND
auto bufferArray = buffer.toUtf8();
auto incomingArray = incoming.toUtf8();
for (auto i = 0; i <= bufferArray.length(); i++) {
auto buffer = bufferArray.sliced(0, i);
auto incoming = bufferArray.sliced(i);
incoming.append(incomingArray);
qInfo() << "BUFFER" << QString(buffer);
qInfo() << "INCOMING" << QString(incoming);
auto parser = SplitParser();
auto spy = QSignalSpy(&parser, &DataStreamParser::read);
parser.setSplitMarker(mark);
parser.parseBytes(incoming, buffer);
auto actualResults = QList<QString>();
for (auto& read: spy) {
actualResults.push_back(read[0].toString());
}
qInfo() << "EXPECTED RESULTS" << results;
qInfo() << "ACTUAL RESULTS" << actualResults;
qInfo() << "EXPECTED REMAINDER" << remainder;
qInfo() << "ACTUAL REMAINDER" << remainder;
QCOMPARE(actualResults, results);
QCOMPARE(buffer, remainder);
}
}
void initBuffer() { // NOLINT
auto parser = SplitParser();
auto spy = QSignalSpy(&parser, &DataStreamParser::read);
auto buf = QString("foo-bar-baz").toUtf8();
auto expected = QList<QString>({"foo", "bar"});
parser.setSplitMarker("-");
parser.parseBytes(buf, buf);
auto actualResults = QList<QString>();
for (auto& read: spy) {
actualResults.push_back(read[0].toString());
}
qInfo() << "EXPECTED RESULTS" << expected;
qInfo() << "ACTUAL RESULTS" << actualResults;
QCOMPARE(actualResults, expected);
QCOMPARE(buf, "baz");
}
};
QTEST_MAIN(TestSplitParser)
#include "datastream.moc"