[client] wayland: reuse remote clipboard text
Some checks failed
build / client (Debug, map[cc:clang cxx:clang++], libdecor) (push) Has been cancelled
build / client (Debug, map[cc:clang cxx:clang++], xdg-shell) (push) Has been cancelled
build / client (Debug, map[cc:gcc cxx:g++], libdecor) (push) Has been cancelled
build / client (Debug, map[cc:gcc cxx:g++], xdg-shell) (push) Has been cancelled
build / client (Release, map[cc:clang cxx:clang++], libdecor) (push) Has been cancelled
build / client (Release, map[cc:clang cxx:clang++], xdg-shell) (push) Has been cancelled
build / client (Release, map[cc:gcc cxx:g++], libdecor) (push) Has been cancelled
build / client (Release, map[cc:gcc cxx:g++], xdg-shell) (push) Has been cancelled
build / module (push) Has been cancelled
build / host-linux (push) Has been cancelled
build / host-windows-cross (push) Has been cancelled
build / host-windows-native (push) Has been cancelled
build / idd (push) Has been cancelled
build / obs (clang) (push) Has been cancelled
build / obs (gcc) (push) Has been cancelled
build / docs (push) Has been cancelled

This commit is contained in:
Geoffrey McRae
2026-08-25 23:05:33 +10:00
parent b888be0ec3
commit 54ea580e35
4 changed files with 421 additions and 105 deletions

View File

@@ -31,6 +31,7 @@
#include "core/clipboard.h" #include "core/clipboard.h"
#include "core/clipboard_files.h" #include "core/clipboard_files.h"
#include "common/countedbuffer.h"
#include "common/debug.h" #include "common/debug.h"
#include "common/KVMFRClipboard.h" #include "common/KVMFRClipboard.h"
@@ -1053,6 +1054,26 @@ void waylandCBRequestCancel(LG_ClipboardRequest request,
clipboardReadRetire(data); clipboardReadRetire(data);
} }
struct WCBPending
{
int fd;
struct WCBPending * next;
};
struct WCBTextCache
{
LG_Lock lock;
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 struct ClipboardWrite
{ {
LG_Lock lock; LG_Lock lock;
@@ -1070,11 +1091,60 @@ struct ClipboardWrite
bool pollRegistered; bool pollRegistered;
bool streamActive; bool streamActive;
bool requesting; bool requesting;
bool cacheTerminal;
struct WCBTransfer * cacheTransfer;
uint8_t * cache;
size_t cacheSize;
size_t cacheCapacity;
uint8_t buffer[KVMFR_CLIPBOARD_REPRESENTATION_BYTES]; 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) static void clipboardWriteDestroy(struct ClipboardWrite * data)
{ {
free(data->cache);
if (data->cacheTransfer)
fileTransferRelease(data->cacheTransfer);
LG_LOCK_FREE(data->lock); LG_LOCK_FREE(data->lock);
free(data); free(data);
} }
@@ -1102,7 +1172,6 @@ static void clipboardWriteRetireStream(struct ClipboardWrite * data)
static LG_ClipboardResult clipboardWriteBegin(void * opaque, static LG_ClipboardResult clipboardWriteBegin(void * opaque,
LG_ClipboardData type, uint64_t sizeHint) LG_ClipboardData type, uint64_t sizeHint)
{ {
(void)sizeHint;
struct ClipboardWrite * data = opaque; struct ClipboardWrite * data = opaque;
LG_LOCK(data->lock); LG_LOCK(data->lock);
if (data->begun || data->ended || !data->streamActive || if (data->begun || data->ended || !data->streamActive ||
@@ -1111,6 +1180,13 @@ static LG_ClipboardResult clipboardWriteBegin(void * opaque,
LG_UNLOCK(data->lock); LG_UNLOCK(data->lock);
return LG_CLIPBOARD_RESULT_FAILED; 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; data->begun = true;
LG_UNLOCK(data->lock); LG_UNLOCK(data->lock);
@@ -1139,6 +1215,17 @@ static LG_ClipboardResult clipboardWriteChunk(void * opaque,
LG_UNLOCK(data->lock); LG_UNLOCK(data->lock);
return LG_CLIPBOARD_RESULT_FAILED; 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) if (data->discard)
{ {
data->offset += size; data->offset += size;
@@ -1168,6 +1255,8 @@ static LG_ClipboardResult clipboardWriteEnd(void * opaque,
uint64_t finalSize) uint64_t finalSize)
{ {
struct ClipboardWrite * data = opaque; struct ClipboardWrite * data = opaque;
CountedBuffer * cache = NULL;
struct WCBTransfer * transfer = NULL;
LG_LOCK(data->lock); LG_LOCK(data->lock);
if (!data->begun || data->ended || !data->streamActive) if (!data->begun || data->ended || !data->streamActive)
{ {
@@ -1185,9 +1274,28 @@ static LG_ClipboardResult clipboardWriteEnd(void * opaque,
LG_UNLOCK(data->lock); LG_UNLOCK(data->lock);
return LG_CLIPBOARD_RESULT_FAILED; 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; data->ended = true;
LG_UNLOCK(data->lock); LG_UNLOCK(data->lock);
if (transfer)
{
clipboardTextComplete(transfer, cache);
countedBufferRelease(&cache);
}
clipboardWriteRetireStream(data); clipboardWriteRetireStream(data);
return LG_CLIPBOARD_RESULT_ACCEPTED; return LG_CLIPBOARD_RESULT_ACCEPTED;
} }
@@ -1197,6 +1305,7 @@ static void clipboardWriteCancel(void * opaque,
{ {
(void)reason; (void)reason;
struct ClipboardWrite * data = opaque; struct ClipboardWrite * data = opaque;
clipboardWriteCancelCache(data);
clipboardWriteRetireStream(data); clipboardWriteRetireStream(data);
} }
@@ -1219,10 +1328,34 @@ struct ClipboardFileWrite
bool complete; 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) static void fileTransferDestroy(struct WCBTransfer * transfer)
{ {
if (transfer->filePresentation) if (transfer->filePresentation)
clipboardFiles_remotePresentationRelease(transfer->filePresentation); clipboardFiles_remotePresentationRelease(transfer->filePresentation);
clipboardTextDestroy(transfer->textCache);
free(transfer->fileUri); free(transfer->fileUri);
free(transfer->fileGnome); free(transfer->fileGnome);
free(transfer->fileKde); free(transfer->fileKde);
@@ -1317,6 +1450,107 @@ static bool clipboardFileWriteStart(int fd, const void * data, size_t size,
return true; 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, static bool fileTransferPayload(const struct WCBTransfer * transfer,
const char * mime, const char ** data, size_t * size) const char * mime, const char ** data, size_t * size)
{ {
@@ -1453,19 +1687,45 @@ static void dataSourceHandleSend(void * data, struct wl_data_source * source,
close(fd); close(fd);
return; 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)); switch (clipboardTextSend(transfer, fd))
if (!data) {
case CLIPBOARD_TEXT_SEND_HANDLED:
return;
case CLIPBOARD_TEXT_SEND_FAILED:
goto error;
case CLIPBOARD_TEXT_SEND_STREAM:
cacheText = true;
break;
}
}
struct ClipboardWrite * output = calloc(1, sizeof(*output));
if (!output)
{ {
DEBUG_ERROR("Out of memory trying to allocate ClipboardWrite"); DEBUG_ERROR("Out of memory trying to allocate ClipboardWrite");
if (cacheText)
clipboardTextCancel(transfer);
goto error; goto error;
} }
data->fd = fd; output->fd = fd;
data->request = LG_CLIPBOARD_REQUEST_INVALID; output->request = LG_CLIPBOARD_REQUEST_INVALID;
data->type = transfer->type; output->type = transfer->type;
LG_LOCK_INIT(data->lock); LG_LOCK_INIT(output->lock);
if (cacheText)
{
output->cacheTransfer = transfer;
fileTransferRetain(transfer);
}
const int flags = fcntl(fd, F_GETFL); const int flags = fcntl(fd, F_GETFL);
const int fdFlags = fcntl(fd, F_GETFD); const int fdFlags = fcntl(fd, F_GETFD);
if (flags < 0 || fdFlags < 0 || if (flags < 0 || fdFlags < 0 ||
@@ -1474,50 +1734,60 @@ static void dataSourceHandleSend(void * data, struct wl_data_source * source,
{ {
DEBUG_ERROR("Failed to make clipboard pipe nonblocking: %s", DEBUG_ERROR("Failed to make clipboard pipe nonblocking: %s",
strerror(errno)); strerror(errno));
clipboardWriteDestroy(data); clipboardWriteCancelCache(output);
clipboardWriteDestroy(output);
goto error; goto error;
} }
data->pollOwned = true;
data->pollRegistered = true; output->pollOwned = true;
data->streamActive = true; output->pollRegistered = true;
output->streamActive = true;
if (waylandPollRegisterWithCleanup(fd, if (waylandPollRegisterWithCleanup(fd,
clipboardWriteCallback, data, clipboardWriteCleanup, 0)) clipboardWriteCallback, output, clipboardWriteCleanup, 0))
{ {
LG_LOCK(data->lock); LG_LOCK(output->lock);
data->requesting = true; output->requesting = true;
LG_UNLOCK(data->lock); LG_UNLOCK(output->lock);
const bool result = clipboard_requestStream(transfer->type, const bool result = clipboard_requestStream(transfer->type,
&clipboardWriteStream, data, &data->request); &clipboardWriteStream, output, &output->request);
int retireFd = -1; int retireFd = -1;
bool destroy = false; bool destroy = false;
LG_LOCK(data->lock); bool cancelCache = false;
data->requesting = false; LG_LOCK(output->lock);
if (!result || data->request == LG_CLIPBOARD_REQUEST_INVALID || output->requesting = false;
!data->streamActive) if (!result || output->request == LG_CLIPBOARD_REQUEST_INVALID ||
!output->streamActive)
{ {
data->streamActive = false; output->streamActive = false;
if (data->pollRegistered) if (output->cacheTransfer && !output->cacheTerminal)
{ {
data->pollRegistered = false; output->cacheTerminal = true;
retireFd = data->fd; cancelCache = true;
} }
destroy = !data->pollOwned; if (output->pollRegistered)
{
output->pollRegistered = false;
retireFd = output->fd;
} }
LG_UNLOCK(data->lock); destroy = !output->pollOwned;
}
LG_UNLOCK(output->lock);
if (cancelCache)
clipboardTextCancel(output->cacheTransfer);
if (retireFd >= 0) if (retireFd >= 0)
waylandPollUnregister(retireFd); waylandPollUnregister(retireFd);
else if (destroy) else if (destroy)
clipboardWriteDestroy(data); clipboardWriteDestroy(output);
return; return;
} }
data->pollOwned = false; output->pollOwned = false;
data->pollRegistered = false; output->pollRegistered = false;
data->streamActive = false; output->streamActive = false;
clipboardWriteDestroy(data); clipboardWriteCancelCache(output);
} clipboardWriteDestroy(output);
error: error:
close(fd); close(fd);
@@ -1560,8 +1830,18 @@ static bool waylandCBPublish(LG_ClipboardData type)
atomic_init(&transfer->references, 1); atomic_init(&transfer->references, 1);
transfer->mimetypes = cbTypeToMimetypes(type); transfer->mimetypes = cbTypeToMimetypes(type);
transfer->textCache = type == LG_CLIPBOARD_DATA_TEXT ?
calloc(1, sizeof(*transfer->textCache)) : NULL;
transfer->type = type; transfer->type = type;
transfer->next = NULL; 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) if (type == LG_CLIPBOARD_DATA_FILES)
transfer->filePresentation = transfer->filePresentation =
clipboardFiles_remotePresentationAcquire(); clipboardFiles_remotePresentationAcquire();

View File

@@ -286,11 +286,14 @@ struct WaylandDSState
LG_DSEventSource eventSource; LG_DSEventSource eventSource;
}; };
struct WCBTextCache;
struct WCBTransfer struct WCBTransfer
{ {
atomic_uint references; atomic_uint references;
LG_ClipboardData type; LG_ClipboardData type;
const char ** mimetypes; const char ** mimetypes;
struct WCBTextCache * textCache;
struct wl_data_source * source; struct wl_data_source * source;
struct WCBTransfer * next; struct WCBTransfer * next;
uint64_t filePresentation; uint64_t filePresentation;

View File

@@ -1311,11 +1311,10 @@ static void testSource(void)
struct Proxy * source = &proto.source[0]; struct Proxy * source = &proto.source[0];
CHECK(proto.selection == source); CHECK(proto.selection == source);
CHECK(proto.serial == 77); CHECK(proto.serial == 77);
CHECK(source->mimeN == 6); CHECK(source->mimeN == 3);
CHECK(strcmp(source->mime[0], "text/plain") == 0); CHECK(strcmp(source->mime[0], "text/plain;charset=utf-8") == 0);
CHECK(strcmp(source->mime[1], "text/plain;charset=utf-8") == 0); CHECK(strcmp(source->mime[1], "text/plain") == 0);
CHECK(strcmp(source->mime[4], "UTF8_STRING") == 0); CHECK(strcmp(source->mime[2], wlCb.lgMimetype) == 0);
CHECK(strcmp(source->mime[5], wlCb.lgMimetype) == 0);
int bad[2]; int bad[2];
CHECK(pipe(bad) == 0); CHECK(pipe(bad) == 0);
@@ -1338,6 +1337,14 @@ static void testSource(void)
CHECK(rec.requestStream); CHECK(rec.requestStream);
CHECK(rec.requestOpaque); CHECK(rec.requestOpaque);
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 first [] = "source ";
const uint8_t second[] = "data"; const uint8_t second[] = "data";
CHECK(rec.requestStream->begin(rec.requestOpaque, CHECK(rec.requestStream->begin(rec.requestOpaque,
@@ -1361,19 +1368,39 @@ static void testSource(void)
CHECK(rec.requestStream->end(rec.requestOpaque, CHECK(rec.requestStream->end(rec.requestOpaque,
sizeof(first) + sizeof(second) - 2) == LG_CLIPBOARD_RESULT_ACCEPTED); sizeof(first) + sizeof(second) - 2) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(!rec.poll[0].active); 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, sourceListener(source)->cancelled(source->data,
(struct wl_data_source *)source); (struct wl_data_source *)source);
CHECK(source->dead); CHECK(source->dead);
CHECK(proto.sourceDestroyN == 1); CHECK(proto.sourceDestroyN == 1);
CHECK(rec.pollCleanupN == 1); CHECK(rec.pollCleanupN == 3);
uint8_t actual[sizeof(first) + sizeof(second)] = {};
const size_t expected = sizeof(first) + sizeof(second) - 2; const size_t expected = sizeof(first) + sizeof(second) - 2;
CHECK(read(good[0], actual, sizeof(actual)) == (ssize_t)expected); 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(memcmp(actual, "source data", expected) == 0);
CHECK(read(good[0], &value, 1) == 0); CHECK(read(reads[i], &value, 1) == 0);
CHECK(close(good[0]) == 0); CHECK(close(reads[i]) == 0);
checkClosed(good[1]); }
finish(); finish();
} }

View File

@@ -62,6 +62,8 @@
#define A_GNOME 106UL #define A_GNOME 106UL
#define A_MATE 107UL #define A_MATE 107UL
#define A_KDE 108UL #define A_KDE 108UL
#define A_TEXT_PLAIN 109UL
#define A_TEXT_UTF8 110UL
struct WindowLog struct WindowLog
{ {
@@ -212,6 +214,10 @@ Atom XInternAtom(Display * display, const char * name, Bool onlyIfExists)
CHECK(!onlyIfExists); CHECK(!onlyIfExists);
if (!strcmp(name, "UTF8_STRING")) if (!strcmp(name, "UTF8_STRING"))
return A_TEXT; 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")) if (!strcmp(name, "image/png"))
return A_PNG; return A_PNG;
if (!strcmp(name, "image/bmp")) if (!strcmp(name, "image/bmp"))