diff --git a/src/plumb/ninep.cpp b/src/plumb/ninep.cpp index f718fd5..abbbcda 100644 --- a/src/plumb/ninep.cpp +++ b/src/plumb/ninep.cpp @@ -129,10 +129,16 @@ void NineP::close() bool NineP::readExactly(char *buf, int n) { + // Bounded per-chunk wait: a well-formed plumb message arrives whole, so the + // bytes are already buffered after the first-byte wait. The timeout only + // guards against a truncated/stalled message, in which case we fail rather + // than block forever — critical so the reader thread stays promptly + // stoppable and is never force-deleted while blocked here. + static constexpr int ReadChunkTimeoutMs = 2000; int got = 0; while (got < n) { if (m_sock->bytesAvailable() == 0) { - if (!m_sock->waitForReadyRead(-1)) { + if (!m_sock->waitForReadyRead(ReadChunkTimeoutMs)) { m_error = QStringLiteral("socket read failed: %1").arg(m_sock->errorString()); return false; } diff --git a/src/plumb/plumber.cpp b/src/plumb/plumber.cpp index b366dc3..c843d43 100644 --- a/src/plumb/plumber.cpp +++ b/src/plumb/plumber.cpp @@ -160,12 +160,21 @@ Plumber::Plumber(QObject *parent) Plumber::~Plumber() { - if (m_reader) { - m_reader->stop(); - m_reader->wait(2000); - delete m_reader; - m_reader = nullptr; + if (!m_reader) { + return; } + m_reader->stop(); + // Never delete a still-running QThread: that frees its QString/QByteArray + // members out from under run(), crashing on the reader thread + // (QArrayData::deallocate). Give it a brief grace to exit cooperatively; + // if it has not (e.g. still inside a blocking connect/read), hand ownership + // to the thread itself via deleteLater so it frees only after run() returns. + if (m_reader->wait(3000)) { + delete m_reader; + } else { + connect(m_reader, &QThread::finished, m_reader, &QObject::deleteLater); + } + m_reader = nullptr; } bool Plumber::send(const QString &data, const QString &wdir)