From 54ea580e35d8f3c96ab6177ff284d1d047796bf7 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Tue, 25 Aug 2026 23:05:33 +1000 Subject: [PATCH] [client] wayland: reuse remote clipboard text --- client/displayservers/Wayland/clipboard.c | 438 ++++++++++++++++++---- client/displayservers/Wayland/wayland.h | 3 + client/tests/wayland_clipboard_test.c | 53 ++- client/tests/x11_clipboard_test.c | 32 +- 4 files changed, 421 insertions(+), 105 deletions(-) diff --git a/client/displayservers/Wayland/clipboard.c b/client/displayservers/Wayland/clipboard.c index 7559acda..4ef16bd0 100644 --- a/client/displayservers/Wayland/clipboard.c +++ b/client/displayservers/Wayland/clipboard.c @@ -31,6 +31,7 @@ #include "core/clipboard.h" #include "core/clipboard_files.h" +#include "common/countedbuffer.h" #include "common/debug.h" #include "common/KVMFRClipboard.h" @@ -1053,28 +1054,97 @@ void waylandCBRequestCancel(LG_ClipboardRequest request, clipboardReadRetire(data); } -struct ClipboardWrite +struct WCBPending +{ + int fd; + struct WCBPending * next; +}; + +struct WCBTextCache { LG_Lock lock; - int fd; - LG_ClipboardRequest request; - LG_ClipboardData type; - uint64_t offset; - size_t pos; - size_t pending; - bool begun; - bool ended; - bool blocked; - bool discard; - bool pollOwned; - bool pollRegistered; - bool streamActive; - bool requesting; - uint8_t buffer[KVMFR_CLIPBOARD_REPRESENTATION_BYTES]; + CountedBuffer * data; + struct WCBPending * pending; + bool request; }; +static void fileTransferRetain(struct WCBTransfer * transfer); +static void fileTransferRelease(struct WCBTransfer * transfer); +static void clipboardTextComplete(struct WCBTransfer * transfer, + CountedBuffer * buffer); +static void clipboardTextCancel(struct WCBTransfer * transfer); + +struct ClipboardWrite +{ + LG_Lock lock; + int fd; + LG_ClipboardRequest request; + LG_ClipboardData type; + uint64_t offset; + size_t pos; + size_t pending; + bool begun; + bool ended; + bool blocked; + bool discard; + bool pollOwned; + bool pollRegistered; + bool streamActive; + bool requesting; + bool cacheTerminal; + struct WCBTransfer * cacheTransfer; + uint8_t * cache; + size_t cacheSize; + size_t cacheCapacity; + uint8_t buffer[KVMFR_CLIPBOARD_REPRESENTATION_BYTES]; +}; + +static bool clipboardWriteCacheGrow(struct ClipboardWrite * data, + size_t wanted) +{ + if (wanted <= data->cacheCapacity) + return true; + + size_t capacity = data->cacheCapacity ? data->cacheCapacity : 4096U; + while (capacity < wanted) + { + if (capacity > SIZE_MAX / 2U) + { + capacity = wanted; + break; + } + capacity *= 2U; + } + + uint8_t * cache = realloc(data->cache, capacity); + if (!cache) + return false; + + data->cache = cache; + data->cacheCapacity = capacity; + return true; +} + +static void clipboardWriteCancelCache(struct ClipboardWrite * data) +{ + struct WCBTransfer * transfer = NULL; + LG_LOCK(data->lock); + if (data->cacheTransfer && !data->cacheTerminal) + { + data->cacheTerminal = true; + transfer = data->cacheTransfer; + } + LG_UNLOCK(data->lock); + + if (transfer) + clipboardTextCancel(transfer); +} + static void clipboardWriteDestroy(struct ClipboardWrite * data) { + free(data->cache); + if (data->cacheTransfer) + fileTransferRelease(data->cacheTransfer); LG_LOCK_FREE(data->lock); free(data); } @@ -1102,7 +1172,6 @@ static void clipboardWriteRetireStream(struct ClipboardWrite * data) static LG_ClipboardResult clipboardWriteBegin(void * opaque, LG_ClipboardData type, uint64_t sizeHint) { - (void)sizeHint; struct ClipboardWrite * data = opaque; LG_LOCK(data->lock); if (data->begun || data->ended || !data->streamActive || @@ -1111,6 +1180,13 @@ static LG_ClipboardResult clipboardWriteBegin(void * opaque, LG_UNLOCK(data->lock); return LG_CLIPBOARD_RESULT_FAILED; } + if (data->cacheTransfer && sizeHint != LG_CLIPBOARD_SIZE_UNKNOWN && + (sizeHint > SIZE_MAX || + !clipboardWriteCacheGrow(data, (size_t)sizeHint))) + { + LG_UNLOCK(data->lock); + return LG_CLIPBOARD_RESULT_FAILED; + } data->begun = true; LG_UNLOCK(data->lock); @@ -1139,6 +1215,17 @@ static LG_ClipboardResult clipboardWriteChunk(void * opaque, LG_UNLOCK(data->lock); return LG_CLIPBOARD_RESULT_FAILED; } + if (data->cacheTransfer) + { + if (offset != data->cacheSize || size > SIZE_MAX - data->cacheSize || + !clipboardWriteCacheGrow(data, data->cacheSize + size)) + { + LG_UNLOCK(data->lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + memcpy(data->cache + data->cacheSize, buffer, size); + data->cacheSize += size; + } if (data->discard) { data->offset += size; @@ -1167,7 +1254,9 @@ static LG_ClipboardResult clipboardWriteChunk(void * opaque, static LG_ClipboardResult clipboardWriteEnd(void * opaque, uint64_t finalSize) { - struct ClipboardWrite * data = opaque; + struct ClipboardWrite * data = opaque; + CountedBuffer * cache = NULL; + struct WCBTransfer * transfer = NULL; LG_LOCK(data->lock); if (!data->begun || data->ended || !data->streamActive) { @@ -1185,9 +1274,28 @@ static LG_ClipboardResult clipboardWriteEnd(void * opaque, LG_UNLOCK(data->lock); return LG_CLIPBOARD_RESULT_FAILED; } + if (data->cacheTransfer) + { + if (finalSize != data->cacheSize || + data->cacheSize > SIZE_MAX - sizeof(*cache) || + !(cache = countedBufferNew(data->cacheSize))) + { + LG_UNLOCK(data->lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + if (data->cacheSize) + memcpy(cache->data, data->cache, data->cacheSize); + data->cacheTerminal = true; + transfer = data->cacheTransfer; + } data->ended = true; LG_UNLOCK(data->lock); + if (transfer) + { + clipboardTextComplete(transfer, cache); + countedBufferRelease(&cache); + } clipboardWriteRetireStream(data); return LG_CLIPBOARD_RESULT_ACCEPTED; } @@ -1197,6 +1305,7 @@ static void clipboardWriteCancel(void * opaque, { (void)reason; struct ClipboardWrite * data = opaque; + clipboardWriteCancelCache(data); clipboardWriteRetireStream(data); } @@ -1219,10 +1328,34 @@ struct ClipboardFileWrite bool complete; }; +static void clipboardPendingClose(struct WCBPending * pending) +{ + while (pending) + { + struct WCBPending * next = pending->next; + close(pending->fd); + free(pending); + pending = next; + } +} + +static void clipboardTextDestroy(struct WCBTextCache * cache) +{ + if (!cache) + return; + + if (cache->data) + countedBufferRelease(&cache->data); + clipboardPendingClose(cache->pending); + LG_LOCK_FREE(cache->lock); + free(cache); +} + static void fileTransferDestroy(struct WCBTransfer * transfer) { if (transfer->filePresentation) clipboardFiles_remotePresentationRelease(transfer->filePresentation); + clipboardTextDestroy(transfer->textCache); free(transfer->fileUri); free(transfer->fileGnome); free(transfer->fileKde); @@ -1317,6 +1450,107 @@ static bool clipboardFileWriteStart(int fd, const void * data, size_t size, return true; } +/* Wayland consumers may request one selection more than once. Retain the + * first text transfer and hold overlapping reads until that cache is ready. */ +static void clipboardTextComplete(struct WCBTransfer * transfer, + CountedBuffer * buffer) +{ + struct WCBTextCache * cache = transfer->textCache; + struct WCBPending * pending = NULL; + CountedBuffer * cached = NULL; + + LG_LOCK(cache->lock); + if (cache->request) + { + if (!cache->data) + { + countedBufferAddRef(buffer); + cache->data = buffer; + } + cache->request = false; + pending = cache->pending; + cache->pending = NULL; + cached = cache->data; + } + LG_UNLOCK(cache->lock); + + while (pending) + { + struct WCBPending * next = pending->next; + if (!cached || + !clipboardFileWriteStart(pending->fd, cached->data, + cached->size, transfer, false)) + close(pending->fd); + free(pending); + pending = next; + } +} + +static void clipboardTextCancel(struct WCBTransfer * transfer) +{ + struct WCBTextCache * cache = transfer->textCache; + struct WCBPending * pending = NULL; + + LG_LOCK(cache->lock); + if (cache->request) + { + cache->request = false; + pending = cache->pending; + cache->pending = NULL; + } + LG_UNLOCK(cache->lock); + clipboardPendingClose(pending); +} + +enum ClipboardTextSendResult +{ + CLIPBOARD_TEXT_SEND_FAILED, + CLIPBOARD_TEXT_SEND_HANDLED, + CLIPBOARD_TEXT_SEND_STREAM, +}; + +static enum ClipboardTextSendResult clipboardTextSend( + struct WCBTransfer * transfer, int fd) +{ + struct WCBPending * pending = malloc(sizeof(*pending)); + if (!pending) + { + DEBUG_ERROR("Out of memory queuing clipboard request"); + return CLIPBOARD_TEXT_SEND_FAILED; + } + pending->fd = fd; + pending->next = NULL; + + struct WCBTextCache * cache = transfer->textCache; + CountedBuffer * buffer = NULL; + bool stream = false; + LG_LOCK(cache->lock); + if (cache->data) + buffer = cache->data; + else if (cache->request) + { + pending->next = cache->pending; + cache->pending = pending; + pending = NULL; + } + else + { + cache->request = true; + stream = true; + } + LG_UNLOCK(cache->lock); + + free(pending); + if (stream) + return CLIPBOARD_TEXT_SEND_STREAM; + if (!buffer) + return CLIPBOARD_TEXT_SEND_HANDLED; + if (clipboardFileWriteStart(fd, buffer->data, + buffer->size, transfer, false)) + return CLIPBOARD_TEXT_SEND_HANDLED; + return CLIPBOARD_TEXT_SEND_FAILED; +} + static bool fileTransferPayload(const struct WCBTransfer * transfer, const char * mime, const char ** data, size_t * size) { @@ -1453,72 +1687,108 @@ static void dataSourceHandleSend(void * data, struct wl_data_source * source, close(fd); return; } - if (containsMimetype(transfer->mimetypes, mimetype)) + if (!containsMimetype(transfer->mimetypes, mimetype)) + goto error; + + bool cacheText = false; + if (transfer->textCache) { - struct ClipboardWrite * data = calloc(1, sizeof(*data)); - if (!data) + switch (clipboardTextSend(transfer, fd)) { - DEBUG_ERROR("Out of memory trying to allocate ClipboardWrite"); - goto error; + case CLIPBOARD_TEXT_SEND_HANDLED: + return; + + case CLIPBOARD_TEXT_SEND_FAILED: + goto error; + + case CLIPBOARD_TEXT_SEND_STREAM: + cacheText = true; + break; } - - data->fd = fd; - data->request = LG_CLIPBOARD_REQUEST_INVALID; - data->type = transfer->type; - LG_LOCK_INIT(data->lock); - const int flags = fcntl(fd, F_GETFL); - const int fdFlags = fcntl(fd, F_GETFD); - if (flags < 0 || fdFlags < 0 || - fcntl(fd, F_SETFD, fdFlags | FD_CLOEXEC) < 0 || - fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) - { - DEBUG_ERROR("Failed to make clipboard pipe nonblocking: %s", - strerror(errno)); - clipboardWriteDestroy(data); - goto error; - } - data->pollOwned = true; - data->pollRegistered = true; - data->streamActive = true; - if (waylandPollRegisterWithCleanup(fd, - clipboardWriteCallback, data, clipboardWriteCleanup, 0)) - { - LG_LOCK(data->lock); - data->requesting = true; - LG_UNLOCK(data->lock); - const bool result = clipboard_requestStream(transfer->type, - &clipboardWriteStream, data, &data->request); - - int retireFd = -1; - bool destroy = false; - LG_LOCK(data->lock); - data->requesting = false; - if (!result || data->request == LG_CLIPBOARD_REQUEST_INVALID || - !data->streamActive) - { - data->streamActive = false; - if (data->pollRegistered) - { - data->pollRegistered = false; - retireFd = data->fd; - } - destroy = !data->pollOwned; - } - LG_UNLOCK(data->lock); - - if (retireFd >= 0) - waylandPollUnregister(retireFd); - else if (destroy) - clipboardWriteDestroy(data); - return; - } - - data->pollOwned = false; - data->pollRegistered = false; - data->streamActive = false; - clipboardWriteDestroy(data); } + struct ClipboardWrite * output = calloc(1, sizeof(*output)); + if (!output) + { + DEBUG_ERROR("Out of memory trying to allocate ClipboardWrite"); + if (cacheText) + clipboardTextCancel(transfer); + goto error; + } + + output->fd = fd; + output->request = LG_CLIPBOARD_REQUEST_INVALID; + output->type = transfer->type; + LG_LOCK_INIT(output->lock); + if (cacheText) + { + output->cacheTransfer = transfer; + fileTransferRetain(transfer); + } + + const int flags = fcntl(fd, F_GETFL); + const int fdFlags = fcntl(fd, F_GETFD); + if (flags < 0 || fdFlags < 0 || + fcntl(fd, F_SETFD, fdFlags | FD_CLOEXEC) < 0 || + fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) + { + DEBUG_ERROR("Failed to make clipboard pipe nonblocking: %s", + strerror(errno)); + clipboardWriteCancelCache(output); + clipboardWriteDestroy(output); + goto error; + } + + output->pollOwned = true; + output->pollRegistered = true; + output->streamActive = true; + if (waylandPollRegisterWithCleanup(fd, + clipboardWriteCallback, output, clipboardWriteCleanup, 0)) + { + LG_LOCK(output->lock); + output->requesting = true; + LG_UNLOCK(output->lock); + const bool result = clipboard_requestStream(transfer->type, + &clipboardWriteStream, output, &output->request); + + int retireFd = -1; + bool destroy = false; + bool cancelCache = false; + LG_LOCK(output->lock); + output->requesting = false; + if (!result || output->request == LG_CLIPBOARD_REQUEST_INVALID || + !output->streamActive) + { + output->streamActive = false; + if (output->cacheTransfer && !output->cacheTerminal) + { + output->cacheTerminal = true; + cancelCache = true; + } + if (output->pollRegistered) + { + output->pollRegistered = false; + retireFd = output->fd; + } + destroy = !output->pollOwned; + } + LG_UNLOCK(output->lock); + + if (cancelCache) + clipboardTextCancel(output->cacheTransfer); + if (retireFd >= 0) + waylandPollUnregister(retireFd); + else if (destroy) + clipboardWriteDestroy(output); + return; + } + + output->pollOwned = false; + output->pollRegistered = false; + output->streamActive = false; + clipboardWriteCancelCache(output); + clipboardWriteDestroy(output); + error: close(fd); } @@ -1560,8 +1830,18 @@ static bool waylandCBPublish(LG_ClipboardData type) atomic_init(&transfer->references, 1); transfer->mimetypes = cbTypeToMimetypes(type); + transfer->textCache = type == LG_CLIPBOARD_DATA_TEXT ? + calloc(1, sizeof(*transfer->textCache)) : NULL; transfer->type = type; transfer->next = NULL; + if (type == LG_CLIPBOARD_DATA_TEXT && !transfer->textCache) + { + DEBUG_ERROR("Out of memory when allocating WCBTextCache"); + fileTransferRelease(transfer); + return false; + } + if (transfer->textCache) + LG_LOCK_INIT(transfer->textCache->lock); if (type == LG_CLIPBOARD_DATA_FILES) transfer->filePresentation = clipboardFiles_remotePresentationAcquire(); diff --git a/client/displayservers/Wayland/wayland.h b/client/displayservers/Wayland/wayland.h index e7187163..ff6b47ea 100644 --- a/client/displayservers/Wayland/wayland.h +++ b/client/displayservers/Wayland/wayland.h @@ -286,11 +286,14 @@ struct WaylandDSState LG_DSEventSource eventSource; }; +struct WCBTextCache; + struct WCBTransfer { atomic_uint references; LG_ClipboardData type; const char ** mimetypes; + struct WCBTextCache * textCache; struct wl_data_source * source; struct WCBTransfer * next; uint64_t filePresentation; diff --git a/client/tests/wayland_clipboard_test.c b/client/tests/wayland_clipboard_test.c index 83aad25a..4f6142b2 100644 --- a/client/tests/wayland_clipboard_test.c +++ b/client/tests/wayland_clipboard_test.c @@ -1311,11 +1311,10 @@ static void testSource(void) struct Proxy * source = &proto.source[0]; CHECK(proto.selection == source); CHECK(proto.serial == 77); - CHECK(source->mimeN == 6); - CHECK(strcmp(source->mime[0], "text/plain") == 0); - CHECK(strcmp(source->mime[1], "text/plain;charset=utf-8") == 0); - CHECK(strcmp(source->mime[4], "UTF8_STRING") == 0); - CHECK(strcmp(source->mime[5], wlCb.lgMimetype) == 0); + CHECK(source->mimeN == 3); + CHECK(strcmp(source->mime[0], "text/plain;charset=utf-8") == 0); + CHECK(strcmp(source->mime[1], "text/plain") == 0); + CHECK(strcmp(source->mime[2], wlCb.lgMimetype) == 0); int bad[2]; CHECK(pipe(bad) == 0); @@ -1338,7 +1337,15 @@ static void testSource(void) CHECK(rec.requestStream); CHECK(rec.requestOpaque); - const uint8_t first[] = "source "; + int queued[2]; + CHECK(pipe(queued) == 0); + sourceListener(source)->send(source->data, + (struct wl_data_source *)source, + "text/plain;charset=utf-8", queued[1]); + CHECK(rec.pollN == 1); + CHECK(rec.requestN == 1); + + const uint8_t first [] = "source "; const uint8_t second[] = "data"; CHECK(rec.requestStream->begin(rec.requestOpaque, LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_SIZE_UNKNOWN) == @@ -1361,19 +1368,39 @@ static void testSource(void) CHECK(rec.requestStream->end(rec.requestOpaque, sizeof(first) + sizeof(second) - 2) == LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(!rec.poll[0].active); + CHECK(rec.pollN == 2); + CHECK(rec.poll[1].fd == queued[1]); + CHECK(rec.poll[1].events == EPOLLOUT); + pollFire(1, EPOLLOUT); + checkClosed(good[1]); + checkClosed(queued[1]); + + int cached[2]; + CHECK(pipe(cached) == 0); + sourceListener(source)->send(source->data, + (struct wl_data_source *)source, "text/plain", cached[1]); + CHECK(rec.pollN == 3); + CHECK(rec.poll[2].fd == cached[1]); + CHECK(rec.poll[2].events == EPOLLOUT); + CHECK(rec.requestN == 1); + pollFire(2, EPOLLOUT); + checkClosed(cached[1]); sourceListener(source)->cancelled(source->data, (struct wl_data_source *)source); CHECK(source->dead); CHECK(proto.sourceDestroyN == 1); - CHECK(rec.pollCleanupN == 1); - uint8_t actual[sizeof(first) + sizeof(second)] = {}; + CHECK(rec.pollCleanupN == 3); const size_t expected = sizeof(first) + sizeof(second) - 2; - CHECK(read(good[0], actual, sizeof(actual)) == (ssize_t)expected); - CHECK(memcmp(actual, "source data", expected) == 0); - CHECK(read(good[0], &value, 1) == 0); - CHECK(close(good[0]) == 0); - checkClosed(good[1]); + const int reads[] = { good[0], queued[0], cached[0] }; + for (size_t i = 0; i < ARRAY_LENGTH(reads); ++i) + { + uint8_t actual[sizeof(first) + sizeof(second)] = {}; + CHECK(read(reads[i], actual, sizeof(actual)) == (ssize_t)expected); + CHECK(memcmp(actual, "source data", expected) == 0); + CHECK(read(reads[i], &value, 1) == 0); + CHECK(close(reads[i]) == 0); + } finish(); } diff --git a/client/tests/x11_clipboard_test.c b/client/tests/x11_clipboard_test.c index 7172c866..61c38c6b 100644 --- a/client/tests/x11_clipboard_test.c +++ b/client/tests/x11_clipboard_test.c @@ -49,19 +49,21 @@ #define MAX_TRANSFER (128U * 1024U) #define MAX_WINDOW 16U -#define A_CLIPBOARD 10UL -#define A_TARGETS 11UL -#define A_SEL_DATA 12UL -#define A_INCR 13UL -#define A_TEXT 100UL -#define A_PNG 101UL -#define A_BMP 102UL -#define A_TIFF 103UL -#define A_JPEG 104UL -#define A_URI 105UL -#define A_GNOME 106UL -#define A_MATE 107UL -#define A_KDE 108UL +#define A_CLIPBOARD 10UL +#define A_TARGETS 11UL +#define A_SEL_DATA 12UL +#define A_INCR 13UL +#define A_TEXT 100UL +#define A_PNG 101UL +#define A_BMP 102UL +#define A_TIFF 103UL +#define A_JPEG 104UL +#define A_URI 105UL +#define A_GNOME 106UL +#define A_MATE 107UL +#define A_KDE 108UL +#define A_TEXT_PLAIN 109UL +#define A_TEXT_UTF8 110UL struct WindowLog { @@ -212,6 +214,10 @@ Atom XInternAtom(Display * display, const char * name, Bool onlyIfExists) CHECK(!onlyIfExists); if (!strcmp(name, "UTF8_STRING")) return A_TEXT; + if (!strcmp(name, "text/plain")) + return A_TEXT_PLAIN; + if (!strcmp(name, "text/plain;charset=utf-8")) + return A_TEXT_UTF8; if (!strcmp(name, "image/png")) return A_PNG; if (!strcmp(name, "image/bmp"))