From c97bf0394ae127e01a30b478c99b1b79e2b8cd55 Mon Sep 17 00:00:00 2001 From: LFRon Date: Fri, 4 Sep 2026 18:02:26 +0800 Subject: [PATCH] fix: avoid truncating large clipboard payloads on non-blocking pipes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compositor hands out O_NONBLOCK pipe ends for selection transfers and paces large transfers (X11 INCR) by pausing reads, so a naive QFile::write() stopped at the first EAGAIN after only ~64 KiB. Large payloads such as screenshots reached XWayland clients truncated: images showed partially decoded content with garbled bands and failed to send, while Wayland clients using blocking offer pipes were unaffected. - Add writeDataToPipe() which resumes after short writes, waits for writability on EAGAIN via poll(), suppresses SIGPIPE around each write and bounds the whole write with a 30s deadline so a stalled reader can never wedge the write thread pool. - Evaluate the requested payload on the owning thread and pass the implicitly-shared QByteArray to the worker; check write results, log failures and guard against a null m_mimeData by closing the fd. - Serve stored raw bytes for exact image MIME requests instead of re-encoding through QImageWriter, which changed the payload size and cost a full decode+encode round trip per request; keep the re-encoding fallback for entries without stored bytes (history replay). 修复向非阻塞选区管道回传大数据被截断的问题。 合成器为选区传输下发的管道两端为 O_NONBLOCK,并依靠暂停读取对大传输(X11 INCR)节流;原先一次朴素的 QFile::write 在首次 EAGAIN 时即停止,仅送出约 64KiB。截图等大 payload 到达 XWayland 客户端时被截断,表现为图片只有部分 解码内容并出现花屏条带、发送失败;使用阻塞 offer 管道的 Wayland 客户端 不受影响。 - 新增 writeDataToPipe():短写后继续写,EAGAIN 时用 poll() 等待可写,每次 write 周围屏蔽 SIGPIPE,并以 30 秒超时兜底,读取方卡死不会永久占用写 线程池。 - 在所属线程求值请求数据,将隐式共享的 QByteArray 交给工作线程;检查写入 结果并记录日志;对空的 m_mimeData 判空并关闭 fd,避免空指针解引用。 - 精确匹配的图片 MIME 请求优先返回已存储的原始字节,不再经 QImageWriter 重编码(避免 payload 尺寸变化和每请求一次完整解码+编码的开销);无存储 字节的条目(历史回放)保留原有重编码回退。 Log: avoid truncating large clipboard payloads on non-blocking pipes --- .../wlrdatacontrolclipboardinterface.cpp | 144 ++++++++++++++++-- 1 file changed, 134 insertions(+), 10 deletions(-) diff --git a/dde-clipboard-daemon/wlrintegration/wlrdatacontrolclipboardinterface.cpp b/dde-clipboard-daemon/wlrintegration/wlrdatacontrolclipboardinterface.cpp index a1365d8c..badba401 100644 --- a/dde-clipboard-daemon/wlrintegration/wlrdatacontrolclipboardinterface.cpp +++ b/dde-clipboard-daemon/wlrintegration/wlrdatacontrolclipboardinterface.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include using namespace Qt::StringLiterals; @@ -294,6 +295,18 @@ static QByteArray getByteArray(QMimeData *mimeData, const QString &mimeType) } else if (mimeData->hasImage() && (mimeType == QLatin1String("application/x-qt-image") || mimeType.startsWith(QLatin1String("image/")))) { + // Prefer the payload stored for the exact MIME type: it is the + // original bytes captured from the source application. Re-encoding + // through QImage changes the file (different encoder settings, so a + // screenshot PNG grows or shrinks) and costs a full decode+encode + // round trip. Formats without a stored payload (reborn history + // entries only carry an image, or requests for image formats the + // source did not offer) fall through to the QImageWriter path below. + if (mimeType != QLatin1String("application/x-qt-image")) { + const QByteArray stored = mimeData->data(mimeType); + if (!stored.isEmpty()) + return stored; + } const QVariant imageData = mimeData->imageData(); QImage image = qvariant_cast(imageData); if (image.isNull()) { @@ -328,6 +341,99 @@ static QByteArray getByteArray(QMimeData *mimeData, const QString &mimeType) return content; } +// Writes every byte of \a data to the pipe \a fd and returns whether the +// whole payload was delivered. +// +// The compositor hands out O_NONBLOCK pipe ends for selection transfers (see +// wlroots xwayland/selection/outgoing.c, both pipe fds are set non-blocking), +// and the peer paces large transfers: for INCR-sized payloads the XWM pauses +// reading until the X11 requestor consumed the previous chunk. A naive +// write() therefore stops at the first EAGAIN with only the pipe capacity +// (~64 KiB) written and silently truncates large payloads such as screenshots. +// +// So keep writing after partial writes, wait for writability on EAGAIN, +// suppress SIGPIPE (a peer closing the pipe is a normal abort path, not a +// crash) and bound the whole write with a deadline so a stalled reader can +// never wedge a worker thread permanently. +static bool writeDataToPipe(int fd, const QByteArray &data, int writeTimeoutMs = 30 * 1000) +{ + const auto writeChunk = [fd](const char *pos, size_t count) -> ssize_t { + // Linux has no MSG_NOSIGNAL for pipes; block SIGPIPE around the + // write instead. pthread_sigmask is used because this runs on a + // worker thread of a thread pool. + sigset_t blocked, previous; + sigemptyset(&blocked); + sigaddset(&blocked, SIGPIPE); + pthread_sigmask(SIG_BLOCK, &blocked, &previous); + const ssize_t written = ::write(fd, pos, count); + const int savedErrno = errno; + pthread_sigmask(SIG_SETMASK, &previous, nullptr); + errno = savedErrno; + return written; + }; + + QElapsedTimer timer; + timer.start(); + + qsizetype written = 0; + const qsizetype total = data.size(); + while (written < total) { + if (timer.elapsed() >= writeTimeoutMs) { + qWarning() << "Wayland clipboard pipe write timed out, wrote" + << written << "of" << total << "bytes"; + return false; + } + + const ssize_t chunk = writeChunk(data.constData() + written, + size_t(total - written)); + if (chunk > 0) { + written += chunk; + continue; + } + + if (chunk == 0) { + // Defensive: a pipe write does not return 0 for a positive count, + // but bail out instead of spinning forever if it ever does. + qWarning() << "Wayland clipboard pipe write returned 0, wrote" + << written << "of" << total << "bytes"; + return false; + } + + if (errno == EINTR) + continue; + + if (errno != EAGAIN && errno != EWOULDBLOCK) { + qWarning() << "Wayland clipboard pipe write failed:" << strerror(errno) + << ", wrote" << written << "of" << total << "bytes"; + return false; + } + + // Pipe buffer is full: the compositor paces the transfer, wait until + // it drains the pipe again. Never pass a negative timeout to poll(): + // that would block indefinitely. + pollfd pfd; + pfd.fd = fd; + pfd.events = POLLOUT; + pfd.revents = 0; + const int remain = writeTimeoutMs - int(timer.elapsed()); + const int ready = ::poll(&pfd, 1, qBound(0, remain, 1000)); + if (ready < 0) { + if (errno == EINTR) + continue; + qWarning() << "Failed to poll Wayland clipboard pipe:" << strerror(errno); + return false; + } + if (ready > 0 && (pfd.revents & (POLLERR | POLLHUP | POLLNVAL))) { + // Read end closed: the requestor aborted the transfer. + qWarning() << "Wayland clipboard pipe peer is gone, wrote" + << written << "of" << total << "bytes"; + return false; + } + } + + return true; +} + WlrDataControlClipboardInterface::WlrDataControlClipboardInterface(QObject *parent) : QObject{parent} { @@ -597,20 +703,38 @@ void WlrDataControlClipboardInterface::onSourceSend(QString mimeType, int fd) // This should be put into a thread because daemon itself will also reply on reading // the clipboard (design burden) qInfo() << "Wayland clipboard owner received data request, MIME type:" << mimeType; - auto _ = QtConcurrent::run(&m_writeThreadPool, [](QByteArray data, int fd){ + + if (!m_mimeData) { + // Defensive: a send event cannot arrive without a source, and a + // source cannot exist before m_mimeData was set. Close the fd instead + // of dereferencing a null QMimeData if that ever changes. + qWarning() << "No MIME data to serve for Wayland clipboard send request, MIME type:" + << mimeType; + ::close(fd); + return; + } + + // Evaluate the payload on the owning (GUI) thread: QMimeData is not + // thread-safe and m_mimeData may be replaced by new clipboard content at + // any time. The returned QByteArray is implicitly shared with atomic + // reference counting, so handing it to the worker thread is safe. + const QByteArray data = getByteArray(m_mimeData.get(), mimeType); + + auto _ = QtConcurrent::run(&m_writeThreadPool, [data, mimeType](int fd) { FdGuard fdGuard(fd); - QFile fdFile; - if (!fdFile.open(fd, QFile::WriteOnly)) { - qWarning() << "Cannot open pipe; error:" << fdFile.errorString(); + if (data.isEmpty()) { + qWarning() << "No data available for Wayland clipboard send request, MIME type:" + << mimeType; return; } - if (!fdFile.isWritable()) { - qWarning() << "Pipe file is not writable."; - return; + // The fd handed out by the compositor may be non-blocking; writing + // must tolerate EAGAIN and short writes, otherwise large payloads + // (e.g. screenshots) get truncated after the first pipe buffer. + if (!writeDataToPipe(fd, data)) { + qWarning() << "Writing Wayland clipboard data failed, MIME type:" + << mimeType << "bytes:" << data.size(); } - - fdFile.write(data); - }, getByteArray(m_mimeData.get(), mimeType), fd); + }, fd); } void WlrDataControlClipboardInterface::onSourceCancelled()