Compare commits

...

2 commits

Author SHA1 Message Date
outfoxxed
bd62179277
all: retry incomplete socket reads
Fixes greetd and hyprland ipc sockets reads being incomplete and
breaking said integrations on slow machines.
2026-03-10 00:54:45 -07:00
-k
cf1a2aeb2d
wayland/toplevel: clear activeToplevel on deactivation 2026-03-09 19:37:15 -07:00
14 changed files with 227 additions and 104 deletions

View file

@ -50,7 +50,9 @@ set shell id.
- Fixed ClippingRectangle related crashes. - Fixed ClippingRectangle related crashes.
- Fixed crashes when monitors are unplugged. - Fixed crashes when monitors are unplugged.
- Fixed crashes when default pipewire devices are lost. - Fixed crashes when default pipewire devices are lost.
- Fixed ToplevelManager not clearing activeToplevel on deactivation.
- Desktop action order is now preserved. - Desktop action order is now preserved.
- Fixed partial socket reads in greetd and hyprland on slow machines.
## Packaging Changes ## Packaging Changes

View file

@ -40,6 +40,7 @@ qt_add_library(quickshell-core STATIC
scriptmodel.cpp scriptmodel.cpp
colorquantizer.cpp colorquantizer.cpp
toolsupport.cpp toolsupport.cpp
streamreader.cpp
) )
qt_add_qml_module(quickshell-core qt_add_qml_module(quickshell-core

98
src/core/streamreader.cpp Normal file
View file

@ -0,0 +1,98 @@
#include "streamreader.hpp"
#include <cstring>
#include <qbytearray.h>
#include <qiodevice.h>
#include <qtypes.h>
void StreamReader::setDevice(QIODevice* device) {
this->reset();
this->device = device;
}
void StreamReader::startTransaction() {
this->cursor = 0;
this->failed = false;
}
bool StreamReader::fill() {
auto available = this->device->bytesAvailable();
if (available <= 0) return false;
auto oldSize = this->buffer.size();
this->buffer.resize(oldSize + available);
auto bytesRead = this->device->read(this->buffer.data() + oldSize, available); // NOLINT
if (bytesRead <= 0) {
this->buffer.resize(oldSize);
return false;
}
this->buffer.resize(oldSize + bytesRead);
return true;
}
QByteArray StreamReader::readBytes(qsizetype count) {
if (this->failed) return {};
auto needed = this->cursor + count;
while (this->buffer.size() < needed) {
if (!this->fill()) {
this->failed = true;
return {};
}
}
auto result = this->buffer.mid(this->cursor, count);
this->cursor += count;
return result;
}
QByteArray StreamReader::readUntil(char terminator) {
if (this->failed) return {};
auto searchFrom = this->cursor;
auto idx = this->buffer.indexOf(terminator, searchFrom);
while (idx == -1) {
searchFrom = this->buffer.size();
if (!this->fill()) {
this->failed = true;
return {};
}
idx = this->buffer.indexOf(terminator, searchFrom);
}
auto length = idx - this->cursor + 1;
auto result = this->buffer.mid(this->cursor, length);
this->cursor += length;
return result;
}
void StreamReader::readInto(char* ptr, qsizetype count) {
auto data = this->readBytes(count);
if (!data.isEmpty()) memcpy(ptr, data.data(), count);
}
qint32 StreamReader::readI32() {
qint32 value = 0;
this->readInto(reinterpret_cast<char*>(&value), sizeof(qint32));
return value;
}
bool StreamReader::commitTransaction() {
if (this->failed) {
this->cursor = 0;
return false;
}
this->buffer.remove(0, this->cursor);
this->cursor = 0;
return true;
}
void StreamReader::reset() {
this->buffer.clear();
this->cursor = 0;
}

26
src/core/streamreader.hpp Normal file
View file

@ -0,0 +1,26 @@
#pragma once
#include <qbytearray.h>
#include <qiodevice.h>
#include <qtypes.h>
class StreamReader {
public:
void setDevice(QIODevice* device);
void startTransaction();
QByteArray readBytes(qsizetype count);
QByteArray readUntil(char terminator);
void readInto(char* ptr, qsizetype count);
qint32 readI32();
bool commitTransaction();
void reset();
private:
bool fill();
QIODevice* device = nullptr;
QByteArray buffer;
qsizetype cursor = 0;
bool failed = false;
};

View file

@ -12,7 +12,7 @@ qt_add_qml_module(quickshell-service-greetd
install_qml_module(quickshell-service-greetd) install_qml_module(quickshell-service-greetd)
# can't be Qt::Qml because generation.hpp pulls in gui types # can't be Qt::Qml because generation.hpp pulls in gui types
target_link_libraries(quickshell-service-greetd PRIVATE Qt::Quick) target_link_libraries(quickshell-service-greetd PRIVATE Qt::Quick quickshell-core)
qs_module_pch(quickshell-service-greetd) qs_module_pch(quickshell-service-greetd)

View file

@ -145,6 +145,7 @@ void GreetdConnection::setInactive() {
QString GreetdConnection::user() const { return this->mUser; } QString GreetdConnection::user() const { return this->mUser; }
void GreetdConnection::onSocketConnected() { void GreetdConnection::onSocketConnected() {
this->reader.setDevice(&this->socket);
qCDebug(logGreetd) << "Connected to greetd socket."; qCDebug(logGreetd) << "Connected to greetd socket.";
if (this->mTargetActive) { if (this->mTargetActive) {
@ -160,11 +161,12 @@ void GreetdConnection::onSocketError(QLocalSocket::LocalSocketError error) {
} }
void GreetdConnection::onSocketReady() { void GreetdConnection::onSocketReady() {
qint32 length = 0; while (true) {
this->reader.startTransaction();
auto length = this->reader.readI32();
auto text = this->reader.readBytes(length);
if (!this->reader.commitTransaction()) return;
this->socket.read(reinterpret_cast<char*>(&length), sizeof(qint32));
auto text = this->socket.read(length);
auto json = QJsonDocument::fromJson(text).object(); auto json = QJsonDocument::fromJson(text).object();
auto type = json.value("type").toString(); auto type = json.value("type").toString();
@ -232,11 +234,12 @@ void GreetdConnection::onSocketReady() {
} }
} else goto unexpected; } else goto unexpected;
return; continue;
unexpected: unexpected:
qCCritical(logGreetd) << "Received unexpected greetd response" << text; qCCritical(logGreetd) << "Received unexpected greetd response" << text;
this->setActive(false); this->setActive(false);
} }
}
void GreetdConnection::sendRequest(const QJsonObject& json) { void GreetdConnection::sendRequest(const QJsonObject& json) {
auto text = QJsonDocument(json).toJson(QJsonDocument::Compact); auto text = QJsonDocument(json).toJson(QJsonDocument::Compact);

View file

@ -8,6 +8,8 @@
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h> #include <qtypes.h>
#include "../../core/streamreader.hpp"
///! State of the Greetd connection. ///! State of the Greetd connection.
/// See @@Greetd.state. /// See @@Greetd.state.
class GreetdState: public QObject { class GreetdState: public QObject {
@ -74,4 +76,5 @@ private:
bool mResponseRequired = false; bool mResponseRequired = false;
QString mUser; QString mUser;
QLocalSocket socket; QLocalSocket socket;
StreamReader reader;
}; };

View file

@ -15,7 +15,7 @@ qs_add_module_deps_light(quickshell-hyprland-ipc Quickshell)
install_qml_module(quickshell-hyprland-ipc) install_qml_module(quickshell-hyprland-ipc)
target_link_libraries(quickshell-hyprland-ipc PRIVATE Qt::Quick) target_link_libraries(quickshell-hyprland-ipc PRIVATE Qt::Quick quickshell-core)
if (WAYLAND_TOPLEVEL_MANAGEMENT) if (WAYLAND_TOPLEVEL_MANAGEMENT)
target_sources(quickshell-hyprland-ipc PRIVATE target_sources(quickshell-hyprland-ipc PRIVATE

View file

@ -93,6 +93,7 @@ void HyprlandIpc::eventSocketError(QLocalSocket::LocalSocketError error) const {
void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) { void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) {
if (state == QLocalSocket::ConnectedState) { if (state == QLocalSocket::ConnectedState) {
this->eventReader.setDevice(&this->eventSocket);
qCInfo(logHyprlandIpc) << "Hyprland event socket connected."; qCInfo(logHyprlandIpc) << "Hyprland event socket connected.";
emit this->connected(); emit this->connected();
} else if (state == QLocalSocket::UnconnectedState && this->valid) { } else if (state == QLocalSocket::UnconnectedState && this->valid) {
@ -104,11 +105,11 @@ void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state)
void HyprlandIpc::eventSocketReady() { void HyprlandIpc::eventSocketReady() {
while (true) { while (true) {
auto rawEvent = this->eventSocket.readLine(); this->eventReader.startTransaction();
if (rawEvent.isEmpty()) break; auto rawEvent = this->eventReader.readUntil('\n');
if (!this->eventReader.commitTransaction()) return;
// remove trailing \n rawEvent.chop(1); // remove trailing \n
rawEvent.truncate(rawEvent.length() - 1);
auto splitIdx = rawEvent.indexOf(">>"); auto splitIdx = rawEvent.indexOf(">>");
auto event = QByteArrayView(rawEvent.data(), splitIdx); auto event = QByteArrayView(rawEvent.data(), splitIdx);
auto data = QByteArrayView( auto data = QByteArrayView(

View file

@ -14,6 +14,7 @@
#include "../../../core/model.hpp" #include "../../../core/model.hpp"
#include "../../../core/qmlscreen.hpp" #include "../../../core/qmlscreen.hpp"
#include "../../../core/streamreader.hpp"
#include "../../../wayland/toplevel_management/handle.hpp" #include "../../../wayland/toplevel_management/handle.hpp"
namespace qs::hyprland::ipc { namespace qs::hyprland::ipc {
@ -139,6 +140,7 @@ private:
static bool compareWorkspaces(HyprlandWorkspace* a, HyprlandWorkspace* b); static bool compareWorkspaces(HyprlandWorkspace* a, HyprlandWorkspace* b);
QLocalSocket eventSocket; QLocalSocket eventSocket;
StreamReader eventReader;
QString mRequestSocketPath; QString mRequestSocketPath;
QString mEventSocketPath; QString mEventSocketPath;
bool valid = false; bool valid = false;

View file

@ -161,7 +161,11 @@ void ToplevelManager::onToplevelReady(impl::ToplevelHandle* handle) {
void ToplevelManager::onToplevelActiveChanged() { void ToplevelManager::onToplevelActiveChanged() {
auto* toplevel = qobject_cast<Toplevel*>(this->sender()); auto* toplevel = qobject_cast<Toplevel*>(this->sender());
if (toplevel->activated()) this->setActiveToplevel(toplevel); if (toplevel->activated()) {
this->setActiveToplevel(toplevel);
} else if (toplevel == this->mActiveToplevel) {
this->setActiveToplevel(nullptr);
}
} }
void ToplevelManager::onToplevelClosed() { void ToplevelManager::onToplevelClosed() {

View file

@ -17,7 +17,7 @@ qs_add_module_deps_light(quickshell-i3-ipc Quickshell)
install_qml_module(quickshell-i3-ipc) install_qml_module(quickshell-i3-ipc)
target_link_libraries(quickshell-i3-ipc PRIVATE Qt::Quick) target_link_libraries(quickshell-i3-ipc PRIVATE Qt::Quick quickshell-core)
qs_module_pch(quickshell-i3-ipc SET large) qs_module_pch(quickshell-i3-ipc SET large)

View file

@ -7,7 +7,6 @@
#include <qbytearray.h> #include <qbytearray.h>
#include <qbytearrayview.h> #include <qbytearrayview.h>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qdatastream.h>
#include <qjsonarray.h> #include <qjsonarray.h>
#include <qjsondocument.h> #include <qjsondocument.h>
#include <qjsonobject.h> #include <qjsonobject.h>
@ -15,9 +14,7 @@
#include <qlocalsocket.h> #include <qlocalsocket.h>
#include <qlogging.h> #include <qlogging.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h> #include <qobject.h>
#include <qsysinfo.h>
#include <qtenvironmentvariables.h> #include <qtenvironmentvariables.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h> #include <qtypes.h>
@ -89,9 +86,6 @@ I3Ipc::I3Ipc(const QList<QString>& events): mEvents(events) {
QObject::connect(&this->liveEventSocket, &QLocalSocket::readyRead, this, &I3Ipc::eventSocketReady); QObject::connect(&this->liveEventSocket, &QLocalSocket::readyRead, this, &I3Ipc::eventSocketReady);
QObject::connect(&this->liveEventSocket, &QLocalSocket::connected, this, &I3Ipc::subscribe); QObject::connect(&this->liveEventSocket, &QLocalSocket::connected, this, &I3Ipc::subscribe);
// clang-format on // clang-format on
this->liveEventSocketDs.setDevice(&this->liveEventSocket);
this->liveEventSocketDs.setByteOrder(static_cast<QDataStream::ByteOrder>(QSysInfo::ByteOrder));
} }
void I3Ipc::makeRequest(const QByteArray& request) { void I3Ipc::makeRequest(const QByteArray& request) {
@ -145,34 +139,21 @@ void I3Ipc::reconnectIPC() {
} }
QVector<Event> I3Ipc::parseResponse() { QVector<Event> I3Ipc::parseResponse() {
QVector<std::tuple<EventCode, QJsonDocument>> events; QVector<Event> events;
const int magicLen = 6;
while (!this->liveEventSocketDs.atEnd()) { while (true) {
this->liveEventSocketDs.startTransaction(); this->eventReader.startTransaction();
this->liveEventSocketDs.startTransaction(); auto magic = this->eventReader.readBytes(6);
auto size = this->eventReader.readI32();
auto type = this->eventReader.readI32();
auto payload = this->eventReader.readBytes(size);
if (!this->eventReader.commitTransaction()) return events;
std::array<char, 6> buffer = {}; if (magic.size() < 6 || strncmp(magic.data(), MAGIC.data(), 6) != 0) {
qint32 size = 0;
qint32 type = EventCode::Unknown;
this->liveEventSocketDs.readRawData(buffer.data(), magicLen);
this->liveEventSocketDs >> size;
this->liveEventSocketDs >> type;
if (!this->liveEventSocketDs.commitTransaction()) break;
QByteArray payload(size, Qt::Uninitialized);
this->liveEventSocketDs.readRawData(payload.data(), size);
if (!this->liveEventSocketDs.commitTransaction()) break;
if (strncmp(buffer.data(), MAGIC.data(), 6) != 0) {
qCWarning(logI3Ipc) << "No magic sequence found in string."; qCWarning(logI3Ipc) << "No magic sequence found in string.";
this->reconnectIPC(); this->reconnectIPC();
break; break;
}; }
if (I3IpcEvent::intToEvent(type) == EventCode::Unknown) { if (I3IpcEvent::intToEvent(type) == EventCode::Unknown) {
qCWarning(logI3Ipc) << "Received unknown event"; qCWarning(logI3Ipc) << "Received unknown event";
@ -204,6 +185,7 @@ void I3Ipc::eventSocketError(QLocalSocket::LocalSocketError error) const {
void I3Ipc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) { void I3Ipc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) {
if (state == QLocalSocket::ConnectedState) { if (state == QLocalSocket::ConnectedState) {
this->eventReader.setDevice(&this->liveEventSocket);
qCInfo(logI3Ipc) << "I3 event socket connected."; qCInfo(logI3Ipc) << "I3 event socket connected.";
emit this->connected(); emit this->connected();
} else if (state == QLocalSocket::UnconnectedState && this->valid) { } else if (state == QLocalSocket::UnconnectedState && this->valid) {

View file

@ -1,7 +1,6 @@
#pragma once #pragma once
#include <qbytearrayview.h> #include <qbytearrayview.h>
#include <qdatastream.h>
#include <qjsondocument.h> #include <qjsondocument.h>
#include <qlocalsocket.h> #include <qlocalsocket.h>
#include <qobject.h> #include <qobject.h>
@ -9,6 +8,8 @@
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h> #include <qtypes.h>
#include "../../../core/streamreader.hpp"
namespace qs::i3::ipc { namespace qs::i3::ipc {
constexpr std::string MAGIC = "i3-ipc"; constexpr std::string MAGIC = "i3-ipc";
@ -92,7 +93,7 @@ protected:
QVector<std::tuple<EventCode, QJsonDocument>> parseResponse(); QVector<std::tuple<EventCode, QJsonDocument>> parseResponse();
QLocalSocket liveEventSocket; QLocalSocket liveEventSocket;
QDataStream liveEventSocketDs; StreamReader eventReader;
QString mSocketPath; QString mSocketPath;
bool valid = false; bool valid = false;