From be2f79a412be6128e0eb807fb6d7b1dc904cc47d Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Fri, 14 Aug 2026 06:27:12 +1000 Subject: [PATCH] [client] clipboard: add direct LGMP synchronization --- client/displayservers/Wayland/clipboard.c | 709 +++++++-- client/displayservers/Wayland/poll.c | 55 +- client/displayservers/Wayland/wayland.c | 10 +- client/displayservers/Wayland/wayland.h | 34 +- client/displayservers/X11/clipboard.c | 787 ++++++++-- client/displayservers/X11/clipboard.h | 3 + client/displayservers/X11/x11.c | 10 +- client/include/app.h | 8 - client/include/interface/transport.h | 1 + client/src/app.c | 30 - client/src/clipboard.c | 15 +- client/src/clipboard.h | 3 + client/tests/CMakeLists.txt | 49 + client/tests/clipboard_test.c | 59 + client/tests/lgmp_clipboard_test.c | 1078 +++++++++++++ client/tests/wayland_clipboard_test.c | 522 ++++++- client/tests/x11_clipboard_test.c | 449 +++++- client/transports/LGMP/CMakeLists.txt | 1 + client/transports/LGMP/clipboard.c | 1679 +++++++++++++++++++++ client/transports/LGMP/clipboard.h | 41 + client/transports/LGMP/lgmp.c | 35 + 21 files changed, 5228 insertions(+), 350 deletions(-) create mode 100644 client/tests/lgmp_clipboard_test.c create mode 100644 client/transports/LGMP/clipboard.c create mode 100644 client/transports/LGMP/clipboard.h diff --git a/client/displayservers/Wayland/clipboard.c b/client/displayservers/Wayland/clipboard.c index a2d892fd..5db0d410 100644 --- a/client/displayservers/Wayland/clipboard.c +++ b/client/displayservers/Wayland/clipboard.c @@ -29,8 +29,9 @@ #include #include -#include "app.h" +#include "../../src/clipboard.h" #include "common/debug.h" +#include "common/KVMFRClipboard.h" struct DataOffer { bool isSelfCopy; @@ -221,12 +222,18 @@ static bool clipboardReadClaim(struct ClipboardRead * data) return current; } +static void clipboardReadRelease(struct ClipboardRead * data) +{ + if (atomic_fetch_sub_explicit( + &data->references, 1, memory_order_acq_rel) == 1) + free(data); +} + static void clipboardReadCleanup(void * opaque) { struct ClipboardRead * data = opaque; close(data->fd); - free(data->buf); - free(data); + clipboardReadRelease(data); } // Destination client handlers. @@ -302,6 +309,9 @@ static void dataDeviceHandleSelection(void * opaque, { if (!offer) { + LG_LOCK(wlCb.lock); + wlCb.selectionSource = NULL; + LG_UNLOCK(wlCb.lock); waylandCBInvalidate(); return; } @@ -309,6 +319,12 @@ static void dataDeviceHandleSelection(void * opaque, struct DataOffer * extra = wl_data_offer_get_user_data(offer); if (!hasAnyMimetype(extra->mimetypes) || extra->isSelfCopy) { + if (!extra->isSelfCopy) + { + LG_LOCK(wlCb.lock); + wlCb.selectionSource = NULL; + LG_UNLOCK(wlCb.lock); + } wl_data_offer_set_user_data(offer, NULL); for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) free(extra->mimetypes[i]); @@ -318,7 +334,7 @@ static void dataDeviceHandleSelection(void * opaque, return; } - int idx = 0; + size_t idx = 0; enum LG_ClipboardData types[LG_CLIPBOARD_DATA_NONE]; for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) if (extra->mimetypes[i]) @@ -329,6 +345,7 @@ static void dataDeviceHandleSelection(void * opaque, struct ClipboardRead * read = clipboardReadTakeCurrentNL(); struct wl_data_offer * oldOffer = wlCb.offer; memcpy(oldMimetypes, wlCb.mimetypes, sizeof(oldMimetypes)); + wlCb.selectionSource = NULL; wlCb.offer = offer; memcpy(wlCb.mimetypes, extra->mimetypes, sizeof(wlCb.mimetypes)); LG_UNLOCK(wlCb.lock); @@ -341,14 +358,14 @@ static void dataDeviceHandleSelection(void * opaque, { const LG_ClipboardRequest request = read->request; clipboardReadRetire(read); - app_clipboardAbort(request); + lgClipboard_abort(request); } if (oldOffer && oldOffer != offer) wl_data_offer_destroy(oldOffer); for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) free(oldMimetypes[i]); - app_clipboardNotifyTypes(types, idx); + lgClipboard_notifyTypes(types, idx); } static void dataDeviceHandleEnter(void * data, struct wl_data_device * device, @@ -425,54 +442,208 @@ bool waylandCBInit(void) return true; } +enum ClipboardReadOperation +{ + CLIPBOARD_READ_BEGIN, + CLIPBOARD_READ_CHUNK, + CLIPBOARD_READ_END, +}; + +enum ClipboardReadCallResult +{ + CLIPBOARD_READ_CALL_STALE, + CLIPBOARD_READ_CALL_ACCEPTED, + CLIPBOARD_READ_CALL_BLOCKED, + CLIPBOARD_READ_CALL_RETRY, + CLIPBOARD_READ_CALL_FAILED, +}; + +/* The provider's ready callback can run after its operation has returned but + * before this thread records BLOCKED. Keep the call in progress until the + * result is committed under wlCb.lock so that edge is retained. */ +static enum ClipboardReadCallResult clipboardReadCall( + struct ClipboardRead * data, enum ClipboardReadOperation operation) +{ + LG_ClipboardRequest request; + LG_ClipboardData type; + uint64_t offset; + size_t pending; + + LG_LOCK(wlCb.lock); + if (wlCb.currentRead != data || data->blocked || data->calling) + { + LG_UNLOCK(wlCb.lock); + return CLIPBOARD_READ_CALL_STALE; + } + + data->calling = true; + data->readyPending = false; + request = data->request; + type = data->type; + offset = data->offset; + pending = data->pending; + LG_UNLOCK(wlCb.lock); + + LG_ClipboardResult result; + switch (operation) + { + case CLIPBOARD_READ_BEGIN: + result = lgClipboard_dataBegin( + request, type, LG_CLIPBOARD_SIZE_UNKNOWN); + break; + + case CLIPBOARD_READ_CHUNK: + result = lgClipboard_dataChunk( + request, offset, data->buffer, pending); + break; + + case CLIPBOARD_READ_END: + result = lgClipboard_dataEnd(request, offset); + break; + + default: + result = LG_CLIPBOARD_RESULT_FAILED; + break; + } + + LG_LOCK(wlCb.lock); + if (wlCb.currentRead != data) + { + LG_UNLOCK(wlCb.lock); + return CLIPBOARD_READ_CALL_STALE; + } + + data->calling = false; + if (result == LG_CLIPBOARD_RESULT_BLOCKED) + { + if (data->readyPending) + { + data->readyPending = false; + LG_UNLOCK(wlCb.lock); + return CLIPBOARD_READ_CALL_RETRY; + } + + data->blocked = true; + waylandPollUpdate(data->fd, 0); + LG_UNLOCK(wlCb.lock); + return CLIPBOARD_READ_CALL_BLOCKED; + } + + data->readyPending = false; + if (result != LG_CLIPBOARD_RESULT_ACCEPTED) + { + LG_UNLOCK(wlCb.lock); + return CLIPBOARD_READ_CALL_FAILED; + } + + switch (operation) + { + case CLIPBOARD_READ_BEGIN: + data->begun = true; + break; + + case CLIPBOARD_READ_CHUNK: + data->offset += data->pending; + data->pending = 0; + break; + + case CLIPBOARD_READ_END: + break; + } + LG_UNLOCK(wlCb.lock); + return CLIPBOARD_READ_CALL_ACCEPTED; +} + static void clipboardReadCallback(uint32_t events, void * opaque) { struct ClipboardRead * data = opaque; LG_LOCK(wlCb.lock); const bool active = wlCb.currentRead == data; + const bool busy = active && (data->blocked || data->calling); LG_UNLOCK(wlCb.lock); - if (!active) + if (!active || busy) return; if (events & EPOLLERR) { if (clipboardReadClaim(data)) - app_clipboardAbort(data->request); + lgClipboard_abort(data->request); return; } - ssize_t result = read(data->fd, - data->buf + data->numRead, data->size - data->numRead); - if (result < 0) + bool readAttempted = false; + for (;;) { - DEBUG_ERROR("Failed to read from clipboard: %s", strerror(errno)); - if (clipboardReadClaim(data)) - app_clipboardAbort(data->request); - return; - } + enum ClipboardReadOperation operation; + bool haveOperation = true; + int readError = 0; - if (result == 0) - { - if (clipboardReadClaim(data)) - app_clipboardData( - data->request, data->type, data->buf, data->numRead); - return; - } - - data->numRead += result; - if (data->numRead >= data->size) - { - data->size *= 2; - void * nbuf = realloc(data->buf, data->size); - if (!nbuf) + LG_LOCK(wlCb.lock); + if (wlCb.currentRead != data || data->blocked || data->calling) { - DEBUG_ERROR("Failed to realloc clipboard buffer: %s", strerror(errno)); - if (clipboardReadClaim(data)) - app_clipboardAbort(data->request); + LG_UNLOCK(wlCb.lock); return; } - data->buf = nbuf; + if (!data->begun) + operation = CLIPBOARD_READ_BEGIN; + else if (data->pending) + operation = CLIPBOARD_READ_CHUNK; + else if (data->eof) + operation = CLIPBOARD_READ_END; + else if (!readAttempted) + { + const ssize_t result = read( + data->fd, data->buffer, sizeof(data->buffer)); + readAttempted = true; + if (result < 0) + { + readError = errno; + haveOperation = false; + } + else if (result == 0) + { + data->eof = true; + operation = CLIPBOARD_READ_END; + } + else + { + data->pending = (size_t)result; + operation = CLIPBOARD_READ_CHUNK; + } + } + else + haveOperation = false; + LG_UNLOCK(wlCb.lock); + + if (readError) + { + if (readError == EAGAIN || readError == EWOULDBLOCK) + return; + DEBUG_ERROR("Failed to read from clipboard: %s", strerror(readError)); + if (clipboardReadClaim(data)) + lgClipboard_abort(data->request); + return; + } + if (!haveOperation) + return; + + const enum ClipboardReadCallResult result = + clipboardReadCall(data, operation); + if (result == CLIPBOARD_READ_CALL_RETRY || + result == CLIPBOARD_READ_CALL_ACCEPTED) + { + if (operation == CLIPBOARD_READ_END && + result == CLIPBOARD_READ_CALL_ACCEPTED) + { + clipboardReadClaim(data); + return; + } + continue; + } + if (result == CLIPBOARD_READ_CALL_FAILED) + clipboardReadClaim(data); + return; } } @@ -491,10 +662,10 @@ void waylandCBInvalidate(void) { const LG_ClipboardRequest request = read->request; clipboardReadRetire(read); - app_clipboardAbort(request); + lgClipboard_abort(request); } - app_clipboardRelease(); + lgClipboard_release(); if (offer) wl_data_offer_destroy(offer); @@ -512,7 +683,21 @@ void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type) if (pipe(fds) < 0) { DEBUG_ERROR("Failed to get a clipboard pipe: %s", strerror(errno)); - app_clipboardAbort(request); + lgClipboard_abort(request); + return; + } + const int readFlags = fcntl(fds[0], F_GETFL); + const int readFdFlags = fcntl(fds[0], F_GETFD); + const int writeFdFlags = fcntl(fds[1], F_GETFD); + if (readFlags < 0 || readFdFlags < 0 || writeFdFlags < 0 || + fcntl(fds[0], F_SETFD, readFdFlags | FD_CLOEXEC) < 0 || + fcntl(fds[1], F_SETFD, writeFdFlags | FD_CLOEXEC) < 0 || + fcntl(fds[0], F_SETFL, readFlags | O_NONBLOCK) < 0) + { + DEBUG_ERROR("Failed to configure clipboard pipe: %s", strerror(errno)); + close(fds[0]); + close(fds[1]); + lgClipboard_abort(request); return; } @@ -522,26 +707,17 @@ void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type) DEBUG_ERROR("Failed to allocate memory to read clipboard"); close(fds[0]); close(fds[1]); - app_clipboardAbort(request); + lgClipboard_abort(request); return; } - data->fd = fds[0]; - data->size = 4096; - data->numRead = 0; - data->buf = malloc(data->size); - data->request = request; - data->type = type; - - if (!data->buf) + *data = (struct ClipboardRead) { - DEBUG_ERROR("Failed to allocate memory to receive clipboard data"); - close(data->fd); - close(fds[1]); - free(data); - app_clipboardAbort(request); - return; - } + .fd = fds[0], + .request = request, + .type = type, + }; + atomic_init(&data->references, 1); LG_LOCK(wlCb.lock); struct ClipboardRead * old = clipboardReadTakeCurrentNL(); @@ -564,7 +740,7 @@ void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type) { const LG_ClipboardRequest oldRequest = old->request; clipboardReadRetire(old); - app_clipboardAbort(oldRequest); + lgClipboard_abort(oldRequest); } if (!registered) { @@ -572,46 +748,302 @@ void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type) DEBUG_ERROR("Failed to register clipboard read into epoll: %s", strerror(errno)); clipboardReadCleanup(data); - app_clipboardAbort(request); + lgClipboard_abort(request); return; } } +void waylandCBRequestReady(LG_ClipboardRequest request) +{ + struct ClipboardRead * failed = NULL; + struct ClipboardRead * retry = NULL; + LG_LOCK(wlCb.lock); + struct ClipboardRead * data = wlCb.currentRead; + if (data && data->request == request && data->calling) + data->readyPending = true; + else if (data && data->request == request && data->blocked) + { + data->blocked = false; + if (!waylandPollUpdate(data->fd, EPOLLIN)) + failed = clipboardReadTakeCurrentNL(); + else + { + atomic_fetch_add_explicit( + &data->references, 1, memory_order_relaxed); + retry = data; + } + } + LG_UNLOCK(wlCb.lock); + + if (retry) + { + /* A blocked operation may already own bytes in data->buffer. Retry it + * directly: an empty pipe with an open writer has no EPOLLIN state with + * which to wake the pending operation. The extra reference covers + * cancellation while this non-poll dispatch is in progress. */ + clipboardReadCallback(0, retry); + clipboardReadRelease(retry); + } + + if (failed) + { + const LG_ClipboardRequest id = failed->request; + clipboardReadRetire(failed); + lgClipboard_abort(id); + } +} + +void waylandCBRequestCancel(LG_ClipboardRequest request, + LG_ClipboardCancelReason reason) +{ + (void)reason; + LG_LOCK(wlCb.lock); + struct ClipboardRead * data = wlCb.currentRead; + if (!data || data->request != request) + data = NULL; + else + clipboardReadTakeCurrentNL(); + LG_UNLOCK(wlCb.lock); + clipboardReadRetire(data); +} + struct ClipboardWrite { - int fd; - size_t pos; - struct CountedBuffer * buffer; + 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_DATA_BYTES]; +}; + +static void clipboardWriteDestroy(struct ClipboardWrite * data) +{ + LG_LOCK_FREE(data->lock); + free(data); +} + +static void clipboardWriteRetireStream(struct ClipboardWrite * data) +{ + int fd = -1; + bool destroy; + LG_LOCK(data->lock); + data->streamActive = false; + if (data->pollRegistered) + { + data->pollRegistered = false; + fd = data->fd; + } + destroy = !data->pollOwned && !data->requesting; + LG_UNLOCK(data->lock); + + if (fd >= 0) + waylandPollUnregister(fd); + else if (destroy) + clipboardWriteDestroy(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 || + type != data->type) + { + LG_UNLOCK(data->lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + data->begun = true; + LG_UNLOCK(data->lock); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult clipboardWriteChunk(void * opaque, + uint64_t offset, const void * buffer, size_t size) +{ + struct ClipboardWrite * data = opaque; + LG_LOCK(data->lock); + if (!data->begun || data->ended || !data->streamActive || + !buffer || !size) + { + LG_UNLOCK(data->lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + if (data->pending) + { + data->blocked = true; + LG_UNLOCK(data->lock); + return LG_CLIPBOARD_RESULT_BLOCKED; + } + if (offset != data->offset) + { + LG_UNLOCK(data->lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + if (data->discard) + { + data->offset += size; + LG_UNLOCK(data->lock); + return LG_CLIPBOARD_RESULT_ACCEPTED; + } + if (size > sizeof(data->buffer)) + { + LG_UNLOCK(data->lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + memcpy(data->buffer, buffer, size); + data->pending = size; + data->pos = 0; + if (!waylandPollUpdate(data->fd, EPOLLOUT)) + { + data->discard = true; + data->pending = 0; + data->offset += size; + } + LG_UNLOCK(data->lock); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult clipboardWriteEnd(void * opaque, + uint64_t finalSize) +{ + struct ClipboardWrite * data = opaque; + LG_LOCK(data->lock); + if (!data->begun || data->ended || !data->streamActive) + { + LG_UNLOCK(data->lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + if (data->pending) + { + data->blocked = true; + LG_UNLOCK(data->lock); + return LG_CLIPBOARD_RESULT_BLOCKED; + } + if (finalSize != data->offset) + { + LG_UNLOCK(data->lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + data->ended = true; + LG_UNLOCK(data->lock); + clipboardWriteRetireStream(data); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static void clipboardWriteCancel(void * opaque, + LG_ClipboardCancelReason reason) +{ + (void)reason; + struct ClipboardWrite * data = opaque; + clipboardWriteRetireStream(data); +} + +static const LG_ClipboardStreamOps clipboardWriteStream = +{ + .begin = clipboardWriteBegin, + .chunk = clipboardWriteChunk, + .end = clipboardWriteEnd, + .cancel = clipboardWriteCancel, }; static void clipboardWriteCleanup(void * opaque) { struct ClipboardWrite * data = opaque; + LG_LOCK(data->lock); + data->pollOwned = false; + data->pollRegistered = false; close(data->fd); - countedBufferRelease(&data->buffer); - free(data); + data->fd = -1; + const bool destroy = !data->streamActive && !data->requesting; + LG_UNLOCK(data->lock); + if (destroy) + clipboardWriteDestroy(data); } static void clipboardWriteCallback(uint32_t events, void * opaque) { struct ClipboardWrite * data = opaque; - if (events & EPOLLERR) + if (events & (EPOLLERR | EPOLLHUP)) goto error; - ssize_t written = write(data->fd, data->buffer->data + data->pos, data->buffer->size - data->pos); + LG_LOCK(data->lock); + if (!data->pending) + { + waylandPollUpdate(data->fd, 0); + LG_UNLOCK(data->lock); + return; + } + + const ssize_t written = write(data->fd, data->buffer + data->pos, + data->pending - data->pos); if (written < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + { + LG_UNLOCK(data->lock); + return; + } if (errno != EPIPE) DEBUG_ERROR("Failed to write clipboard data: %s", strerror(errno)); + LG_UNLOCK(data->lock); goto error; } data->pos += written; - if (data->pos < data->buffer->size) + if (data->pos < data->pending) + { + LG_UNLOCK(data->lock); return; + } + + data->offset += data->pending; + data->pending = 0; + data->pos = 0; + waylandPollUpdate(data->fd, 0); + const bool ready = data->blocked; + data->blocked = false; + const LG_ClipboardRequest request = data->request; + LG_UNLOCK(data->lock); + if (ready) + lgClipboard_requestReady(request); + return; error: - waylandPollUnregister(data->fd); + LG_LOCK(data->lock); + data->discard = true; + data->offset += data->pending; + data->pending = 0; + data->pos = 0; + int fd = -1; + if (data->pollRegistered) + { + data->pollRegistered = false; + fd = data->fd; + } + const bool errorReady = data->blocked; + data->blocked = false; + const LG_ClipboardRequest errorRequest = data->request; + LG_UNLOCK(data->lock); + if (fd >= 0) + waylandPollUnregister(fd); + if (errorReady) + lgClipboard_requestReady(errorRequest); } static void dataSourceHandleTarget(void * data, struct wl_data_source * source, @@ -627,23 +1059,68 @@ static void dataSourceHandleSend(void * data, struct wl_data_source * source, struct WCBTransfer * transfer = (struct WCBTransfer *) data; if (containsMimetype(transfer->mimetypes, mimetype)) { - struct ClipboardWrite * data = malloc(sizeof(*data)); + struct ClipboardWrite * data = calloc(1, sizeof(*data)); if (!data) { DEBUG_ERROR("Out of memory trying to allocate ClipboardWrite"); goto error; } - data->fd = fd; - data->pos = 0; - data->buffer = transfer->data; - countedBufferAddRef(transfer->data); + 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, EPOLLOUT)) - return; + clipboardWriteCallback, data, clipboardWriteCleanup, 0)) + { + LG_LOCK(data->lock); + data->requesting = true; + LG_UNLOCK(data->lock); + const bool result = lgClipboard_requestStream(transfer->type, + &clipboardWriteStream, data, &data->request); - countedBufferRelease(&data->buffer); - free(data); + 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); } error: @@ -654,7 +1131,18 @@ static void dataSourceHandleCancelled(void * data, struct wl_data_source * source) { struct WCBTransfer * transfer = (struct WCBTransfer *) data; - countedBufferRelease(&transfer->data); + DEBUG_ASSERT(transfer->source == source); + + LG_LOCK(wlCb.lock); + struct WCBTransfer ** link = &wlCb.sources; + while (*link && *link != transfer) + link = &(*link)->next; + if (*link) + *link = transfer->next; + if (wlCb.selectionSource == source) + wlCb.selectionSource = NULL; + LG_UNLOCK(wlCb.lock); + free(transfer); wl_data_source_destroy(source); } @@ -665,12 +1153,8 @@ static const struct wl_data_source_listener dataSourceListener = { .cancelled = dataSourceHandleCancelled, }; -static void waylandCBReplyFn(void * opaque, LG_ClipboardData type, - const uint8_t * data, uint32_t size) +static void waylandCBPublish(LG_ClipboardData type) { - if (type == LG_CLIPBOARD_DATA_NONE) - return; - struct WCBTransfer * transfer = malloc(sizeof(*transfer)); if (!transfer) { @@ -679,35 +1163,61 @@ static void waylandCBReplyFn(void * opaque, LG_ClipboardData type, } transfer->mimetypes = cbTypeToMimetypes(type); - transfer->data = countedBufferNew(size); - if (!transfer->data) - { - DEBUG_ERROR("Out of memory when allocating clipboard buffer"); - free(transfer); - return; - } - if (size) - memcpy(transfer->data->data, data, size); + transfer->type = type; + transfer->next = NULL; struct wl_data_source * source = wl_data_device_manager_create_data_source(wlWm.dataDeviceManager); - wl_data_source_add_listener(source, &dataSourceListener, transfer); + if (!source) + { + DEBUG_ERROR("Failed to create clipboard data source"); + free(transfer); + return; + } + transfer->source = source; + if (wl_data_source_add_listener(source, &dataSourceListener, transfer) < 0) + { + DEBUG_ERROR("Failed to listen to clipboard data source"); + wl_data_source_destroy(source); + free(transfer); + return; + } for (const char ** mimetype = transfer->mimetypes; *mimetype; mimetype++) wl_data_source_offer(source, *mimetype); wl_data_source_offer(source, wlCb.lgMimetype); - wl_data_device_set_selection(wlCb.dataDevice, source, - wlWm.keyboardEnterSerial); + LG_LOCK(wlCb.lock); + if (wlCb.dataDevice) + { + transfer->next = wlCb.sources; + wlCb.sources = transfer; + wlCb.selectionSource = source; + wl_data_device_set_selection(wlCb.dataDevice, source, + wlWm.keyboardEnterSerial); + } + else + { + wl_data_source_destroy(source); + free(transfer); + } + LG_UNLOCK(wlCb.lock); } void waylandCBNotice(LG_ClipboardData type) { - if (!app_clipboardRequest(type, waylandCBReplyFn, NULL)) - DEBUG_ERROR("Failed to request remote clipboard data"); + waylandCBPublish(type); } void waylandCBRelease(void) { + LG_LOCK(wlCb.lock); + if (wlCb.dataDevice && wlCb.selectionSource) + { + wlCb.selectionSource = NULL; + wl_data_device_set_selection(wlCb.dataDevice, NULL, + wlWm.keyboardEnterSerial); + } + LG_UNLOCK(wlCb.lock); } void waylandCBFree(void) @@ -718,9 +1228,15 @@ void waylandCBFree(void) struct wl_data_offer * offer = wlCb.offer; struct wl_data_offer * dndOffer = wlCb.dndOffer; struct wl_data_device * dataDevice = wlCb.dataDevice; + struct WCBTransfer * sources = wlCb.sources; + if (dataDevice && wlCb.selectionSource) + wl_data_device_set_selection(dataDevice, NULL, + wlWm.keyboardEnterSerial); wlCb.offer = NULL; wlCb.dndOffer = NULL; wlCb.dataDevice = NULL; + wlCb.selectionSource = NULL; + wlCb.sources = NULL; memcpy(mimetypes, wlCb.mimetypes, sizeof(mimetypes)); memset(wlCb.mimetypes, 0, sizeof(wlCb.mimetypes)); LG_UNLOCK(wlCb.lock); @@ -732,6 +1248,13 @@ void waylandCBFree(void) wl_data_offer_destroy(dndOffer); if (dataDevice) wl_data_device_release(dataDevice); + while (sources) + { + struct WCBTransfer * next = sources->next; + wl_data_source_destroy(sources->source); + free(sources); + sources = next; + } for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) free(mimetypes[i]); diff --git a/client/displayservers/Wayland/poll.c b/client/displayservers/Wayland/poll.c index 51380e4e..7107548f 100644 --- a/client/displayservers/Wayland/poll.c +++ b/client/displayservers/Wayland/poll.c @@ -133,16 +133,18 @@ bool waylandPollRegisterWithCleanup(int fd, WaylandPollCallback callback, return false; node->fd = fd; + node->events = events; node->callback = callback; node->cleanup = cleanup; node->opaque = opaque; atomic_init(&node->removed, false); LG_LOCK(wlWm.pollLock); - if (epoll_ctl(wlWm.epollFd, EPOLL_CTL_ADD, fd, &(struct epoll_event) { - .events = events, - .data = (epoll_data_t) { .ptr = node }, - }) < 0) + if (events && epoll_ctl(wlWm.epollFd, EPOLL_CTL_ADD, fd, + &(struct epoll_event) { + .events = events, + .data = (epoll_data_t) { .ptr = node }, + }) < 0) { LG_UNLOCK(wlWm.pollLock); free(node); @@ -184,7 +186,8 @@ bool waylandPollUnregister(int fd) return false; } - if (epoll_ctl(wlWm.epollFd, EPOLL_CTL_DEL, fd, NULL) < 0) + if (node->events && + epoll_ctl(wlWm.epollFd, EPOLL_CTL_DEL, fd, NULL) < 0) { LG_UNLOCK(wlWm.pollLock); DEBUG_ERROR("Failed to unregister from epoll: %s", strerror(errno)); @@ -199,3 +202,45 @@ bool waylandPollUnregister(int fd) }); return true; } + +bool waylandPollUpdate(int fd, uint32_t events) +{ + bool result = false; + LG_LOCK(wlWm.pollLock); + struct WaylandPoll * node; + wl_list_for_each(node, &wlWm.poll, link) + { + if (node->fd != fd) + continue; + + int operation; + if (!node->events && events) + operation = EPOLL_CTL_ADD; + else if (node->events && !events) + operation = EPOLL_CTL_DEL; + else if (events) + operation = EPOLL_CTL_MOD; + else + { + result = true; + break; + } + + struct epoll_event event = + { + .events = events, + .data = (epoll_data_t) { .ptr = node }, + }; + if (epoll_ctl(wlWm.epollFd, operation, fd, + operation == EPOLL_CTL_DEL ? NULL : &event) < 0) + DEBUG_ERROR("Failed to update epoll events: %s", strerror(errno)); + else + { + node->events = events; + result = true; + } + break; + } + LG_UNLOCK(wlWm.pollLock); + return result; +} diff --git a/client/displayservers/Wayland/wayland.c b/client/displayservers/Wayland/wayland.c index 16dbe7ac..b947feb4 100644 --- a/client/displayservers/Wayland/wayland.c +++ b/client/displayservers/Wayland/wayland.c @@ -355,8 +355,10 @@ struct LG_DisplayServerOps LGDS_Wayland = .setHDRImageDesc = waylandHDRCallback, - .cbInit = waylandCBInit, - .cbNotice = waylandCBNotice, - .cbRelease = waylandCBRelease, - .cbRequest = waylandCBRequest + .cbInit = waylandCBInit, + .cbNotice = waylandCBNotice, + .cbRelease = waylandCBRelease, + .cbRequest = waylandCBRequest, + .cbRequestReady = waylandCBRequestReady, + .cbRequestCancel = waylandCBRequestCancel, }; diff --git a/client/displayservers/Wayland/wayland.h b/client/displayservers/Wayland/wayland.h index fdb9e6a0..477c4481 100644 --- a/client/displayservers/Wayland/wayland.h +++ b/client/displayservers/Wayland/wayland.h @@ -35,8 +35,8 @@ #include "app.h" #include "egl_dynprocs.h" +#include "common/KVMFRClipboard.h" #include "common/locking.h" -#include "common/countedbuffer.h" #include "common/ringbuffer.h" #include "interface/displayserver.h" #include "interface/desktop.h" @@ -65,6 +65,7 @@ typedef void (*WaylandPollCleanup)(void * opaque); struct WaylandPoll { int fd; + uint32_t events; atomic_bool removed; WaylandPollCallback callback; WaylandPollCleanup cleanup; @@ -286,18 +287,26 @@ struct WaylandDSState struct WCBTransfer { - struct CountedBuffer * data; - const char ** mimetypes; + LG_ClipboardData type; + const char ** mimetypes; + struct wl_data_source * source; + struct WCBTransfer * next; }; struct ClipboardRead { - int fd; - size_t size; - size_t numRead; - uint8_t * buf; - LG_ClipboardRequest request; - LG_ClipboardData type; + atomic_uint references; + int fd; + LG_ClipboardRequest request; + LG_ClipboardData type; + uint64_t offset; + size_t pending; + bool begun; + bool blocked; + bool calling; + bool readyPending; + bool eof; + uint8_t buffer[KVMFR_CLIPBOARD_DATA_BYTES]; }; struct WCBState @@ -309,6 +318,9 @@ struct WCBState struct wl_data_offer * offer; struct wl_data_offer * dndOffer; + struct wl_data_source * selectionSource; + struct WCBTransfer * sources; + LG_Lock lock; struct ClipboardRead * currentRead; }; @@ -325,6 +337,9 @@ void waylandActivationRequestActivation(void); bool waylandCBInit(void); void waylandCBFree(void); void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type); +void waylandCBRequestReady(LG_ClipboardRequest request); +void waylandCBRequestCancel(LG_ClipboardRequest request, + LG_ClipboardCancelReason reason); void waylandCBNotice(LG_ClipboardData type); void waylandCBRelease(void); void waylandCBInvalidate(void); @@ -417,6 +432,7 @@ bool waylandPollRegister(int fd, WaylandPollCallback callback, void * opaque, ui bool waylandPollRegisterWithCleanup(int fd, WaylandPollCallback callback, void * opaque, WaylandPollCleanup cleanup, uint32_t events); bool waylandPollUnregister(int fd); +bool waylandPollUpdate(int fd, uint32_t events); // presentation module struct WaylandPresentationFrame; diff --git a/client/displayservers/X11/clipboard.c b/client/displayservers/X11/clipboard.c index d66c103b..f8413ff9 100644 --- a/client/displayservers/X11/clipboard.c +++ b/client/displayservers/X11/clipboard.c @@ -22,15 +22,16 @@ #include "x11.h" #include "atoms.h" +#include #include -#include #include #include -#include "app.h" +#include "../../src/clipboard.h" #include "common/array.h" #include "common/debug.h" +#include "common/KVMFRClipboard.h" #include "common/locking.h" struct X11ClipboardRead @@ -39,9 +40,33 @@ struct X11ClipboardRead LG_ClipboardRequest request; LG_ClipboardData type; bool incremental; - uint8_t * buffer; - size_t size; - size_t capacity; + bool begun; + bool blocked; + bool calling; + bool readyPending; + bool endPending; + bool propertyPending; + bool pendingMore; + uint64_t sizeHint; + uint64_t offset; + long propertyOffset; + unsigned long pendingUnits; + uint8_t * pending; + size_t pendingSize; +}; + +struct X11ClipboardWrite +{ + struct X11ClipboardWrite * next; + XEvent event; + LG_ClipboardRequest request; + LG_ClipboardData type; + uint64_t offset; + bool begun; + bool blocked; + bool ready; + bool endPending; + bool creating; }; struct X11ClipboardState @@ -53,7 +78,8 @@ struct X11ClipboardState LG_ClipboardData type; bool haveRequest; - struct X11ClipboardRead read; + struct X11ClipboardRead read; + struct X11ClipboardWrite * writes; }; static const char * atomTypes[] = @@ -73,7 +99,10 @@ static void x11CBSelectionClear(const XSelectionClearEvent e); static void x11CBSelectionIncr(const XPropertyEvent e); static void x11CBSelectionNotify(const XSelectionEvent e); static void x11CBXFixesSelectionNotify(const XFixesSelectionNotifyEvent e); -static LG_ClipboardRequest cancelReadNL(void); +static LG_ClipboardRequest cancelReadNL(bool abort); +static bool writeLinkedNL(const struct X11ClipboardWrite * write); +static void writeRemoveNL(struct X11ClipboardWrite * write); +static bool advanceReadPropertyNL(unsigned long units); static void clearTargetsNL(void); bool x11CBEventThread(const XEvent * xe) @@ -93,14 +122,39 @@ bool x11CBEventThread(const XEvent * xe) return true; case PropertyNotify: - if (xe->xproperty.state != PropertyNewValue) - break; - - if (xe->xproperty.atom == x11atoms.SEL_DATA) + if (xe->xproperty.state == PropertyNewValue && + xe->xproperty.atom == x11atoms.SEL_DATA) { x11CBSelectionIncr(xe->xproperty); return true; } + if (xe->xproperty.state == PropertyDelete) + { + LG_ClipboardRequest request = LG_CLIPBOARD_REQUEST_INVALID; + LG_LOCK(x11cb.lock); + struct X11ClipboardWrite * write; + for (write = x11cb.writes; write; write = write->next) + if (write->request != LG_CLIPBOARD_REQUEST_INVALID && + xe->xproperty.window == write->event.xselection.requestor && + xe->xproperty.atom == write->event.xselection.property) + break; + if (write) + { + write->ready = true; + if (write->blocked) + { + write->blocked = false; + request = write->request; + } + } + LG_UNLOCK(x11cb.lock); + if (write) + { + if (request != LG_CLIPBOARD_REQUEST_INVALID) + lgClipboard_requestReady(request); + return true; + } + } break; default: @@ -120,6 +174,8 @@ bool x11CBInit(void) { LG_LOCK_INIT(x11cb.lock); x11cb.aCurSelection = BadValue; + x11cb.read.request = LG_CLIPBOARD_REQUEST_INVALID; + x11cb.writes = NULL; for(int i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) { x11cb.aTypes[i] = XInternAtom(x11.display, atomTypes[i], False); @@ -143,29 +199,152 @@ bool x11CBInit(void) return true; } -static void x11CBReplyFn(void * opaque, LG_ClipboardData type, - const uint8_t * data, uint32_t size) +static void x11CBWriteSend(XEvent * event) { - XEvent *s = (XEvent *)opaque; - - if (type == LG_CLIPBOARD_DATA_NONE) - s->xselection.property = None; - else - XChangeProperty( - x11.display , - s->xselection.requestor, - s->xselection.property , - s->xselection.target , - 8, - PropModeReplace, - data, - size); - - XSendEvent(x11.display, s->xselection.requestor, 0, 0, s); + XSendEvent(x11.display, event->xselection.requestor, 0, 0, event); XFlush(x11.display); - free(s); } +static LG_ClipboardResult x11CBWriteBegin(void * opaque, + LG_ClipboardData type, uint64_t sizeHint) +{ + struct X11ClipboardWrite * write = opaque; + LG_LOCK(x11cb.lock); + if (!writeLinkedNL(write) || + write->request == LG_CLIPBOARD_REQUEST_INVALID || + write->begun || type != write->type) + { + LG_UNLOCK(x11cb.lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + const unsigned long hint = sizeHint == LG_CLIPBOARD_SIZE_UNKNOWN ? 0 : + (sizeHint > ULONG_MAX ? ULONG_MAX : (unsigned long)sizeHint); + XChangeProperty(x11.display, write->event.xselection.requestor, + write->event.xselection.property, x11atoms.INCR, 32, + PropModeReplace, (const unsigned char *)&hint, 1); + XSelectInput(x11.display, write->event.xselection.requestor, + PropertyChangeMask); + write->begun = true; + write->ready = false; + XEvent event = write->event; + LG_UNLOCK(x11cb.lock); + x11CBWriteSend(&event); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult x11CBWriteChunk(void * opaque, + uint64_t offset, const void * data, size_t size) +{ + struct X11ClipboardWrite * write = opaque; + LG_LOCK(x11cb.lock); + if (!writeLinkedNL(write) || + write->request == LG_CLIPBOARD_REQUEST_INVALID || !write->begun || + write->endPending || !data || !size) + { + LG_UNLOCK(x11cb.lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + if (!write->ready) + { + write->blocked = true; + LG_UNLOCK(x11cb.lock); + return LG_CLIPBOARD_RESULT_BLOCKED; + } + if (offset != write->offset || size > KVMFR_CLIPBOARD_DATA_BYTES) + { + LG_UNLOCK(x11cb.lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + XChangeProperty(x11.display, write->event.xselection.requestor, + write->event.xselection.property, write->event.xselection.target, + 8, PropModeReplace, data, (int)size); + write->ready = false; + write->blocked = false; + write->offset += size; + LG_UNLOCK(x11cb.lock); + XFlush(x11.display); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult x11CBWriteEnd(void * opaque, uint64_t finalSize) +{ + struct X11ClipboardWrite * write = opaque; + LG_LOCK(x11cb.lock); + if (!writeLinkedNL(write) || + write->request == LG_CLIPBOARD_REQUEST_INVALID || !write->begun) + { + LG_UNLOCK(x11cb.lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + if (!write->ready) + { + write->blocked = true; + write->endPending = true; + LG_UNLOCK(x11cb.lock); + return LG_CLIPBOARD_RESULT_BLOCKED; + } + if (finalSize != write->offset) + { + LG_UNLOCK(x11cb.lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + + const XEvent event = write->event; + const bool destroy = !write->creating; + writeRemoveNL(write); + LG_UNLOCK(x11cb.lock); + XChangeProperty(x11.display, event.xselection.requestor, + event.xselection.property, event.xselection.target, 8, + PropModeReplace, NULL, 0); + XFlush(x11.display); + if (destroy) + free(write); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static void x11CBWriteCancel(void * opaque, + LG_ClipboardCancelReason reason) +{ + struct X11ClipboardWrite * write = opaque; + (void)reason; + LG_LOCK(x11cb.lock); + if (!writeLinkedNL(write)) + { + LG_UNLOCK(x11cb.lock); + return; + } + const XEvent event = write->event; + const bool begun = write->begun; + const bool destroy = !write->creating; + writeRemoveNL(write); + LG_UNLOCK(x11cb.lock); + if (begun) + { + XChangeProperty(x11.display, event.xselection.requestor, + event.xselection.property, event.xselection.target, 8, + PropModeReplace, NULL, 0); + XFlush(x11.display); + } + else + { + XEvent reply = event; + reply.xselection.property = None; + x11CBWriteSend(&reply); + } + if (destroy) + free(write); +} + +static const LG_ClipboardStreamOps x11CBWriteStream = +{ + .begin = x11CBWriteBegin, + .chunk = x11CBWriteChunk, + .end = x11CBWriteEnd, + .cancel = x11CBWriteCancel, +}; + static void x11CBSelectionRequest(const XSelectionRequestEvent e) { XEvent * s = malloc(sizeof(*s)); @@ -215,9 +394,64 @@ static void x11CBSelectionRequest(const XSelectionRequestEvent e) for(int i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) if (x11cb.aTypes[i] == e.target && requestType == i) { - // request the data - if (app_clipboardRequest(requestType, x11CBReplyFn, s)) + struct X11ClipboardWrite * write = calloc(1, sizeof(*write)); + if (!write) + { + DEBUG_ERROR("out of memory"); + goto nodata; + } + write->event = *s; + write->request = LG_CLIPBOARD_REQUEST_INVALID; + write->type = requestType; + write->creating = true; + + LG_LOCK(x11cb.lock); + bool duplicate = false; + for (struct X11ClipboardWrite * current = x11cb.writes; + current; current = current->next) + { + duplicate = current->event.xselection.requestor == e.requestor && + current->event.xselection.property == e.property; + if (duplicate) + break; + } + if (!duplicate) + { + write->next = x11cb.writes; + x11cb.writes = write; + } + LG_UNLOCK(x11cb.lock); + if (duplicate) + { + free(write); + goto nodata; + } + + const bool result = lgClipboard_requestStream(requestType, + &x11CBWriteStream, write, &write->request); + LG_LOCK(x11cb.lock); + const bool linked = writeLinkedNL(write); + if (linked) + { + write->creating = false; + if (!result) + writeRemoveNL(write); + } + else + write->creating = false; + LG_UNLOCK(x11cb.lock); + + if (result && linked) + { + free(s); return; + } + free(write); + if (!linked) + { + free(s); + return; + } goto nodata; } @@ -240,18 +474,51 @@ static void x11CBSelectionClear(const XSelectionClearEvent e) static struct X11ClipboardRead clearReadNL(void) { const struct X11ClipboardRead read = x11cb.read; - x11cb.read = (struct X11ClipboardRead) { 0 }; + x11cb.read = (struct X11ClipboardRead) + { + .request = LG_CLIPBOARD_REQUEST_INVALID, + }; if (read.window) XDestroyWindow(x11.display, read.window); return read; } /* x11cb.lock must be held. */ -static LG_ClipboardRequest cancelReadNL(void) +static LG_ClipboardRequest cancelReadNL(bool abort) { const struct X11ClipboardRead read = clearReadNL(); - free(read.buffer); - return read.request; + free(read.pending); + return abort ? read.request : LG_CLIPBOARD_REQUEST_INVALID; +} + +/* x11cb.lock must be held. */ +static bool writeLinkedNL(const struct X11ClipboardWrite * write) +{ + for (const struct X11ClipboardWrite * current = x11cb.writes; + current; current = current->next) + if (current == write) + return true; + return false; +} + +/* x11cb.lock must be held and write must be linked. */ +static void writeRemoveNL(struct X11ClipboardWrite * write) +{ + struct X11ClipboardWrite ** current = &x11cb.writes; + while (*current != write) + current = &(*current)->next; + *current = write->next; + write->next = NULL; +} + +/* x11cb.lock must be held. */ +static bool advanceReadPropertyNL(unsigned long units) +{ + if (units > LONG_MAX || + x11cb.read.propertyOffset > LONG_MAX - (long)units) + return false; + x11cb.read.propertyOffset += (long)units; + return true; } /* x11cb.lock must be held. */ @@ -264,6 +531,75 @@ static void clearTargetsNL(void) x11cb.targetsWindow = 0; } +enum X11ClipboardReadOperation +{ + X11_CLIPBOARD_READ_BEGIN, + X11_CLIPBOARD_READ_CHUNK, + X11_CLIPBOARD_READ_END, +}; + +enum X11ClipboardReadCallResult +{ + X11_CLIPBOARD_READ_CALL_STALE, + X11_CLIPBOARD_READ_CALL_ACCEPTED, + X11_CLIPBOARD_READ_CALL_BLOCKED, + X11_CLIPBOARD_READ_CALL_RETRY, + X11_CLIPBOARD_READ_CALL_FAILED, +}; + +/* x11cb.lock must be held on entry and is held again on return. Retain a ready + * edge delivered between the provider operation and committing BLOCKED. */ +static enum X11ClipboardReadCallResult x11CBReadCallNL( + enum X11ClipboardReadOperation operation, LG_ClipboardData type, + uint64_t sizeHint, uint64_t offset, const void * data, size_t size) +{ + const LG_ClipboardRequest request = x11cb.read.request; + x11cb.read.calling = true; + x11cb.read.readyPending = false; + LG_UNLOCK(x11cb.lock); + + LG_ClipboardResult result; + switch (operation) + { + case X11_CLIPBOARD_READ_BEGIN: + result = lgClipboard_dataBegin(request, type, sizeHint); + break; + + case X11_CLIPBOARD_READ_CHUNK: + result = lgClipboard_dataChunk(request, offset, data, size); + break; + + case X11_CLIPBOARD_READ_END: + result = lgClipboard_dataEnd(request, offset); + break; + + default: + result = LG_CLIPBOARD_RESULT_FAILED; + break; + } + + LG_LOCK(x11cb.lock); + if (x11cb.read.request != request) + return X11_CLIPBOARD_READ_CALL_STALE; + + x11cb.read.calling = false; + if (result == LG_CLIPBOARD_RESULT_BLOCKED) + { + if (x11cb.read.readyPending) + { + x11cb.read.readyPending = false; + return X11_CLIPBOARD_READ_CALL_RETRY; + } + + x11cb.read.blocked = true; + return X11_CLIPBOARD_READ_CALL_BLOCKED; + } + + x11cb.read.readyPending = false; + return result == LG_CLIPBOARD_RESULT_ACCEPTED ? + X11_CLIPBOARD_READ_CALL_ACCEPTED : X11_CLIPBOARD_READ_CALL_FAILED; +} + static void x11CBSelectionIncr(const XPropertyEvent e) { Atom type; @@ -281,11 +617,21 @@ static void x11CBSelectionIncr(const XPropertyEvent e) return; } + if (x11cb.read.blocked || x11cb.read.calling || x11cb.read.pendingSize || + x11cb.read.endPending) + { + x11cb.read.propertyPending = true; + LG_UNLOCK(x11cb.lock); + return; + } + +readProperty: if (XGetWindowProperty( e.display, e.window, e.atom, - 0, ~0L, // start and length + x11cb.read.propertyOffset, + (KVMFR_CLIPBOARD_DATA_BYTES + 3U) / 4U, True, // delete the property AnyPropertyType, &type, @@ -297,8 +643,8 @@ static void x11CBSelectionIncr(const XPropertyEvent e) DEBUG_ERROR("XGetWindowProperty Failed"); const struct X11ClipboardRead read = clearReadNL(); LG_UNLOCK(x11cb.lock); - free(read.buffer); - app_clipboardAbort(read.request); + free(read.pending); + lgClipboard_abort(read.request); return; } @@ -307,7 +653,8 @@ static void x11CBSelectionIncr(const XPropertyEvent e) if (x11cb.aTypes[dataType] == type) break; - if ((itemCount && !data) || dataType == LG_CLIPBOARD_DATA_NONE || + if ((itemCount && !data) || (!itemCount && after) || + dataType == LG_CLIPBOARD_DATA_NONE || dataType != x11cb.read.type || format != 8) { DEBUG_WARN("Invalid incremental clipboard data"); @@ -315,66 +662,105 @@ static void x11CBSelectionIncr(const XPropertyEvent e) LG_UNLOCK(x11cb.lock); if (data) XFree(data); - free(read.buffer); - app_clipboardAbort(read.request); + free(read.pending); + lgClipboard_abort(read.request); return; } if (itemCount == 0) { - const struct X11ClipboardRead read = clearReadNL(); - LG_UNLOCK(x11cb.lock); if (data) XFree(data); - app_clipboardData( - read.request, read.type, read.buffer, read.size); - free(read.buffer); + const uint64_t offset = x11cb.read.offset; + enum X11ClipboardReadCallResult result; + do + result = x11CBReadCallNL(X11_CLIPBOARD_READ_END, + x11cb.read.type, x11cb.read.sizeHint, offset, NULL, 0); + while (result == X11_CLIPBOARD_READ_CALL_RETRY); + if (result == X11_CLIPBOARD_READ_CALL_STALE) + { + LG_UNLOCK(x11cb.lock); + return; + } + if (result == X11_CLIPBOARD_READ_CALL_BLOCKED) + { + x11cb.read.endPending = true; + LG_UNLOCK(x11cb.lock); + return; + } + cancelReadNL(false); + LG_UNLOCK(x11cb.lock); return; } - if (itemCount > SIZE_MAX - x11cb.read.size) + if (itemCount > KVMFR_CLIPBOARD_DATA_BYTES) { const struct X11ClipboardRead read = clearReadNL(); LG_UNLOCK(x11cb.lock); XFree(data); - free(read.buffer); - app_clipboardAbort(read.request); + free(read.pending); + lgClipboard_abort(read.request); return; } - const size_t required = x11cb.read.size + itemCount; - if (required > x11cb.read.capacity) + uint8_t * copy = malloc(itemCount); + if (!copy) { - size_t capacity = x11cb.read.capacity; - while (capacity < required) - { - if (capacity > SIZE_MAX / 2) - { - capacity = required; - break; - } - capacity *= 2; - } + const struct X11ClipboardRead read = clearReadNL(); + LG_UNLOCK(x11cb.lock); + XFree(data); + free(read.pending); + lgClipboard_abort(read.request); + return; + } + memcpy(copy, data, itemCount); + XFree(data); + data = NULL; - uint8_t * buffer = realloc(x11cb.read.buffer, capacity); - if (!buffer) + const uint64_t offset = x11cb.read.offset; + enum X11ClipboardReadCallResult result; + do + result = x11CBReadCallNL(X11_CLIPBOARD_READ_CHUNK, + x11cb.read.type, x11cb.read.sizeHint, + offset, copy, itemCount); + while (result == X11_CLIPBOARD_READ_CALL_RETRY); + if (result == X11_CLIPBOARD_READ_CALL_STALE) + { + LG_UNLOCK(x11cb.lock); + free(copy); + return; + } + if (result == X11_CLIPBOARD_READ_CALL_BLOCKED) + { + x11cb.read.pending = copy; + x11cb.read.pendingSize = itemCount; + x11cb.read.pendingUnits = (itemCount + 3U) / 4U; + x11cb.read.pendingMore = after != 0; + LG_UNLOCK(x11cb.lock); + return; + } + free(copy); + if (result != X11_CLIPBOARD_READ_CALL_ACCEPTED) + { + cancelReadNL(false); + LG_UNLOCK(x11cb.lock); + return; + } + x11cb.read.offset += itemCount; + if (after) + { + if (!advanceReadPropertyNL((itemCount + 3U) / 4U)) { const struct X11ClipboardRead read = clearReadNL(); LG_UNLOCK(x11cb.lock); - XFree(data); - free(read.buffer); - app_clipboardAbort(read.request); + free(read.pending); + lgClipboard_abort(read.request); return; } - x11cb.read.buffer = buffer; - x11cb.read.capacity = capacity; + goto readProperty; } - - memcpy(x11cb.read.buffer + x11cb.read.size, data, itemCount); - x11cb.read.size += itemCount; + x11cb.read.propertyOffset = 0; LG_UNLOCK(x11cb.lock); - if (data) - XFree(data); } static void x11CBXFixesSelectionNotify(const XFixesSelectionNotifyEvent e) @@ -393,7 +779,7 @@ static void x11CBXFixesSelectionNotify(const XFixesSelectionNotifyEvent e) } const LG_ClipboardRequest oldRequest = x11cb.read.window ? - cancelReadNL() : LG_CLIPBOARD_REQUEST_INVALID; + cancelReadNL(true) : LG_CLIPBOARD_REQUEST_INVALID; clearTargetsNL(); if (e.owner == 0) @@ -403,8 +789,8 @@ static void x11CBXFixesSelectionNotify(const XFixesSelectionNotifyEvent e) XFlush(x11.display); LG_UNLOCK(x11cb.lock); if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) - app_clipboardAbort(oldRequest); - app_clipboardRelease(); + lgClipboard_abort(oldRequest); + lgClipboard_release(); return; } @@ -419,8 +805,8 @@ static void x11CBXFixesSelectionNotify(const XFixesSelectionNotifyEvent e) XFlush(x11.display); LG_UNLOCK(x11cb.lock); if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) - app_clipboardAbort(oldRequest); - app_clipboardRelease(); + lgClipboard_abort(oldRequest); + lgClipboard_release(); return; } @@ -436,7 +822,7 @@ static void x11CBXFixesSelectionNotify(const XFixesSelectionNotifyEvent e) LG_UNLOCK(x11cb.lock); if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) - app_clipboardAbort(oldRequest); + lgClipboard_abort(oldRequest); } static void x11CBSelectionNotify(const XSelectionEvent e) @@ -456,7 +842,7 @@ static void x11CBSelectionNotify(const XSelectionEvent e) { clearTargetsNL(); LG_UNLOCK(x11cb.lock); - app_clipboardRelease(); + lgClipboard_release(); return; } @@ -475,7 +861,7 @@ static void x11CBSelectionNotify(const XSelectionEvent e) { clearTargetsNL(); LG_UNLOCK(x11cb.lock); - app_clipboardRelease(); + lgClipboard_release(); return; } clearTargetsNL(); @@ -486,11 +872,11 @@ static void x11CBSelectionNotify(const XSelectionEvent e) // an array of padded 64-bit values if (!data || format != 32) { - app_clipboardRelease(); + lgClipboard_release(); goto out; } - int typeCount = 0; + size_t typeCount = 0; LG_ClipboardData types[LG_CLIPBOARD_DATA_NONE]; // see if we support any of the targets listed @@ -503,7 +889,7 @@ static void x11CBSelectionNotify(const XSelectionEvent e) break; } - app_clipboardNotifyTypes(types, typeCount); + lgClipboard_notifyTypes(types, typeCount); goto out; } LG_UNLOCK(x11cb.lock); @@ -517,9 +903,9 @@ static void x11CBSelectionNotify(const XSelectionEvent e) if (e.property == None) { - const LG_ClipboardRequest request = cancelReadNL(); + const LG_ClipboardRequest request = cancelReadNL(true); LG_UNLOCK(x11cb.lock); - app_clipboardAbort(request); + lgClipboard_abort(request); return; } @@ -536,9 +922,9 @@ static void x11CBSelectionNotify(const XSelectionEvent e) &after, &data) != Success) { - const LG_ClipboardRequest request = cancelReadNL(); + const LG_ClipboardRequest request = cancelReadNL(true); LG_UNLOCK(x11cb.lock); - app_clipboardAbort(request); + lgClipboard_abort(request); return; } @@ -546,32 +932,54 @@ static void x11CBSelectionNotify(const XSelectionEvent e) { if (!data || format != 32 || itemCount < 1) { - const LG_ClipboardRequest request = cancelReadNL(); + const LG_ClipboardRequest request = cancelReadNL(true); LG_UNLOCK(x11cb.lock); if (data) XFree(data); - app_clipboardAbort(request); + lgClipboard_abort(request); return; } - size_t capacity = *(const unsigned long *)data; - if (capacity < 4096) - capacity = 4096; - if (capacity > 1048576) - capacity = 1048576; - x11cb.read.buffer = malloc(capacity); - if (!x11cb.read.buffer) + const uint64_t hint = *(const unsigned long *)data; + const uint64_t sizeHint = hint ? hint : LG_CLIPBOARD_SIZE_UNKNOWN; + const LG_ClipboardData dataType = x11cb.read.type; + x11cb.read.incremental = true; + x11cb.read.sizeHint = sizeHint; + XFree(data); + enum X11ClipboardReadCallResult result; + do + result = x11CBReadCallNL(X11_CLIPBOARD_READ_BEGIN, + dataType, sizeHint, 0, NULL, 0); + while (result == X11_CLIPBOARD_READ_CALL_RETRY); + if (result == X11_CLIPBOARD_READ_CALL_STALE) { - const LG_ClipboardRequest request = cancelReadNL(); LG_UNLOCK(x11cb.lock); - XFree(data); - app_clipboardAbort(request); return; } - x11cb.read.capacity = capacity; - x11cb.read.incremental = true; + if (result == X11_CLIPBOARD_READ_CALL_BLOCKED) + { + LG_UNLOCK(x11cb.lock); + return; + } + if (result != X11_CLIPBOARD_READ_CALL_ACCEPTED) + { + cancelReadNL(false); + LG_UNLOCK(x11cb.lock); + return; + } + x11cb.read.begun = true; + const bool processProperty = x11cb.read.propertyPending; + const Window processWindow = x11cb.read.window; + x11cb.read.propertyPending = false; LG_UNLOCK(x11cb.lock); - XFree(data); + if (processProperty) + x11CBSelectionIncr((XPropertyEvent) + { + .display = x11.display, + .window = processWindow, + .atom = x11atoms.SEL_DATA, + .state = PropertyNewValue, + }); return; } @@ -583,17 +991,17 @@ static void x11CBSelectionNotify(const XSelectionEvent e) const LG_ClipboardRequest request = x11cb.read.request; const bool valid = dataType != LG_CLIPBOARD_DATA_NONE && dataType == x11cb.read.type && format == 8 && (!itemCount || data); - cancelReadNL(); + cancelReadNL(false); LG_UNLOCK(x11cb.lock); if (!valid) { DEBUG_WARN("Invalid clipboard data"); - app_clipboardAbort(request); + lgClipboard_abort(request); goto out; } - app_clipboardData(request, dataType, data, itemCount); + lgClipboard_data(request, dataType, data, itemCount); out: if (data) @@ -604,7 +1012,7 @@ void x11CBNotice(LG_ClipboardData type) { LG_LOCK(x11cb.lock); const LG_ClipboardRequest oldRequest = x11cb.read.window ? - cancelReadNL() : LG_CLIPBOARD_REQUEST_INVALID; + cancelReadNL(true) : LG_CLIPBOARD_REQUEST_INVALID; clearTargetsNL(); x11cb.aCurSelection = BadValue; x11cb.haveRequest = true; @@ -613,7 +1021,7 @@ void x11CBNotice(LG_ClipboardData type) XFlush(x11.display); LG_UNLOCK(x11cb.lock); if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) - app_clipboardAbort(oldRequest); + lgClipboard_abort(oldRequest); } void x11CBRelease(void) @@ -637,13 +1045,13 @@ void x11CBRequest(LG_ClipboardRequest request, LG_ClipboardData type) LG_LOCK(x11cb.lock); const LG_ClipboardRequest oldRequest = x11cb.read.window ? - cancelReadNL() : LG_CLIPBOARD_REQUEST_INVALID; + cancelReadNL(true) : LG_CLIPBOARD_REQUEST_INVALID; if (x11cb.aCurSelection == BadValue) { LG_UNLOCK(x11cb.lock); if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) - app_clipboardAbort(oldRequest); - app_clipboardAbort(request); + lgClipboard_abort(oldRequest); + lgClipboard_abort(request); return; } @@ -653,8 +1061,8 @@ void x11CBRequest(LG_ClipboardRequest request, LG_ClipboardData type) { LG_UNLOCK(x11cb.lock); if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) - app_clipboardAbort(oldRequest); - app_clipboardAbort(request); + lgClipboard_abort(oldRequest); + lgClipboard_abort(request); return; } @@ -676,5 +1084,154 @@ void x11CBRequest(LG_ClipboardRequest request, LG_ClipboardData type) LG_UNLOCK(x11cb.lock); if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) - app_clipboardAbort(oldRequest); + lgClipboard_abort(oldRequest); +} + +void x11CBRequestReady(LG_ClipboardRequest request) +{ + LG_LOCK(x11cb.lock); + if (x11cb.read.request != request) + { + LG_UNLOCK(x11cb.lock); + return; + } + if (x11cb.read.calling) + { + x11cb.read.readyPending = true; + LG_UNLOCK(x11cb.lock); + return; + } + if (!x11cb.read.blocked) + { + LG_UNLOCK(x11cb.lock); + return; + } + + x11cb.read.blocked = false; + const bool begin = !x11cb.read.begun; + const bool end = x11cb.read.endPending; + const LG_ClipboardData type = x11cb.read.type; + const uint64_t sizeHint = x11cb.read.sizeHint; + const uint64_t offset = x11cb.read.offset; + const size_t pendingSize = x11cb.read.pendingSize; + const unsigned long pendingUnits = x11cb.read.pendingUnits; + const bool pendingMore = x11cb.read.pendingMore; + uint8_t * pending = NULL; + + /* The provider call runs without x11cb.lock. Request cancellation or a + * selection replacement may therefore clear and free read.pending while + * the call is in progress. Give the call its own bounded copy so teardown + * can remain immediate without invalidating the callback's data. */ + if (pendingSize) + { + pending = malloc(pendingSize); + if (!pending) + { + const LG_ClipboardRequest abort = cancelReadNL(true); + LG_UNLOCK(x11cb.lock); + lgClipboard_abort(abort); + return; + } + memcpy(pending, x11cb.read.pending, pendingSize); + } + + enum X11ClipboardReadOperation operation; + if (begin) + operation = X11_CLIPBOARD_READ_BEGIN; + else if (pendingSize) + operation = X11_CLIPBOARD_READ_CHUNK; + else if (end) + operation = X11_CLIPBOARD_READ_END; + else + { + LG_UNLOCK(x11cb.lock); + return; + } + + enum X11ClipboardReadCallResult result; + do + result = x11CBReadCallNL(operation, type, sizeHint, + offset, pending, pendingSize); + while (result == X11_CLIPBOARD_READ_CALL_RETRY); + free(pending); + if (result == X11_CLIPBOARD_READ_CALL_STALE) + { + LG_UNLOCK(x11cb.lock); + return; + } + if (result == X11_CLIPBOARD_READ_CALL_BLOCKED) + { + LG_UNLOCK(x11cb.lock); + return; + } + if (result != X11_CLIPBOARD_READ_CALL_ACCEPTED) + { + cancelReadNL(false); + LG_UNLOCK(x11cb.lock); + return; + } + + bool processProperty = false; + Window window = None; + if (begin) + x11cb.read.begun = true; + else if (pendingSize) + { + x11cb.read.offset += pendingSize; + free(x11cb.read.pending); + x11cb.read.pending = NULL; + x11cb.read.pendingSize = 0; + x11cb.read.pendingUnits = 0; + x11cb.read.pendingMore = false; + if (pendingMore) + { + if (!advanceReadPropertyNL(pendingUnits)) + { + const struct X11ClipboardRead read = clearReadNL(); + LG_UNLOCK(x11cb.lock); + free(read.pending); + lgClipboard_abort(read.request); + return; + } + processProperty = true; + window = x11cb.read.window; + } + else + x11cb.read.propertyOffset = 0; + } + else + { + cancelReadNL(false); + LG_UNLOCK(x11cb.lock); + return; + } + + if (x11cb.read.propertyPending && x11cb.read.incremental && + !x11cb.read.blocked && !x11cb.read.pendingSize && + !x11cb.read.endPending) + { + x11cb.read.propertyPending = false; + processProperty = true; + window = x11cb.read.window; + } + LG_UNLOCK(x11cb.lock); + + if (processProperty) + x11CBSelectionIncr((XPropertyEvent) + { + .display = x11.display, + .window = window, + .atom = x11atoms.SEL_DATA, + .state = PropertyNewValue, + }); +} + +void x11CBRequestCancel(LG_ClipboardRequest request, + LG_ClipboardCancelReason reason) +{ + (void)reason; + LG_LOCK(x11cb.lock); + if (x11cb.read.request == request) + cancelReadNL(false); + LG_UNLOCK(x11cb.lock); } diff --git a/client/displayservers/X11/clipboard.h b/client/displayservers/X11/clipboard.h index be635990..faf42ff7 100644 --- a/client/displayservers/X11/clipboard.h +++ b/client/displayservers/X11/clipboard.h @@ -32,5 +32,8 @@ bool x11CBInit(void); void x11CBNotice(LG_ClipboardData type); void x11CBRelease(void); void x11CBRequest(LG_ClipboardRequest request, LG_ClipboardData type); +void x11CBRequestReady(LG_ClipboardRequest request); +void x11CBRequestCancel(LG_ClipboardRequest request, + LG_ClipboardCancelReason reason); #endif diff --git a/client/displayservers/X11/x11.c b/client/displayservers/X11/x11.c index de155554..d4f7ba31 100644 --- a/client/displayservers/X11/x11.c +++ b/client/displayservers/X11/x11.c @@ -2314,8 +2314,10 @@ struct LG_DisplayServerOps LGDS_X11 = .getFullscreen = x11GetFullscreen, .minimize = x11Minimize, - .cbInit = x11CBInit, - .cbNotice = x11CBNotice, - .cbRelease = x11CBRelease, - .cbRequest = x11CBRequest + .cbInit = x11CBInit, + .cbNotice = x11CBNotice, + .cbRelease = x11CBRelease, + .cbRequest = x11CBRequest, + .cbRequestReady = x11CBRequestReady, + .cbRequestCancel = x11CBRequestCancel, }; diff --git a/client/include/app.h b/client/include/app.h index 844421f2..7cb59534 100644 --- a/client/include/app.h +++ b/client/include/app.h @@ -173,14 +173,6 @@ void app_overlayConfigRegister(const char * title, void app_overlayConfigRegisterTab(const char * title, void (*callback)(void * udata, int * id), void * udata); -void app_clipboardRelease(void); -void app_clipboardNotifyTypes(const LG_ClipboardData types[], int count); -void app_clipboardData(LG_ClipboardRequest request, - LG_ClipboardData type, const void * data, size_t size); -void app_clipboardAbort(LG_ClipboardRequest request); -bool app_clipboardRequest(LG_ClipboardData type, - LG_ClipboardReplyFn replyFn, void * opaque); - /** * Show an alert on screen * @param type The alert type diff --git a/client/include/interface/transport.h b/client/include/interface/transport.h index 020b21f6..a8c1bdca 100644 --- a/client/include/interface/transport.h +++ b/client/include/interface/transport.h @@ -66,6 +66,7 @@ enum LG_TRANSPORT_FEATURE_WINDOW_SIZE = 0x2, LG_TRANSPORT_FEATURE_FRAME_SCHEDULE = 0x4, LG_TRANSPORT_FEATURE_INPUT = 0x8, + LG_TRANSPORT_FEATURE_CLIPBOARD = 0x10, }; typedef uint32_t LG_TransportFeatureFlags; diff --git a/client/src/app.c b/client/src/app.c index 93a4ec00..afe60345 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -23,7 +23,6 @@ #include "main.h" #include "core.h" #include "util.h" -#include "clipboard.h" #include "render_queue.h" #include "evdev.h" #include "input.h" @@ -349,35 +348,6 @@ void app_handleEnterEvent(bool entered) } } -void app_clipboardRelease(void) -{ - lgClipboard_release(); -} - -void app_clipboardNotifyTypes(const LG_ClipboardData types[], int count) -{ - if (count < 0) - return; - lgClipboard_notifyTypes(types, (size_t)count); -} - -void app_clipboardData(LG_ClipboardRequest request, - const LG_ClipboardData type, const void * data, size_t size) -{ - lgClipboard_data(request, type, data, size); -} - -void app_clipboardAbort(LG_ClipboardRequest request) -{ - lgClipboard_abort(request); -} - -bool app_clipboardRequest(LG_ClipboardData type, - LG_ClipboardReplyFn replyFn, void * opaque) -{ - return lgClipboard_request(type, replyFn, opaque); -} - static int mapInputToImGuiButton(uint32_t button) { switch (button) diff --git a/client/src/clipboard.c b/client/src/clipboard.c index 60e05b53..fa77eeff 100644 --- a/client/src/clipboard.c +++ b/client/src/clipboard.c @@ -1761,6 +1761,11 @@ static bool requestClipboard(LG_ClipboardData type, const bool queued = available && ll_push(clipboard.requests, request); + /* Publish the request before the provider can make it visible to its + * worker. A provider event may otherwise reach the stream sink before the + * caller has learned which request it belongs to. */ + if (queued && resultId) + *resultId = id; bool result = queued; if (queued) result = clipboard.active.ops->request(clipboard.active.opaque, @@ -1769,20 +1774,14 @@ static bool requestClipboard(LG_ClipboardData type, LG_UNLOCK_SHARED(clipboard.activeLock); if (result) - { - if (resultId) - *resultId = id; return true; - } if (queued) { ClipboardRequest * failed = takeRequest(id); if (!failed) - { - if (resultId) - *resultId = id; return true; - } + if (resultId) + *resultId = LG_CLIPBOARD_REQUEST_INVALID; freeRequest(failed); return false; } diff --git a/client/src/clipboard.h b/client/src/clipboard.h index 5fcc1aa7..90b238cb 100644 --- a/client/src/clipboard.h +++ b/client/src/clipboard.h @@ -45,6 +45,9 @@ LG_ClipboardResult lgClipboard_dataEnd(LG_ClipboardRequest request, void lgClipboard_data(LG_ClipboardRequest request, LG_ClipboardData type, const void * data, size_t size); void lgClipboard_abort(LG_ClipboardRequest request); +/* request is published before provider dispatch. Its storage must remain + * valid until this function returns and may be observed by stream callbacks + * before then. */ bool lgClipboard_requestStream(LG_ClipboardData type, const LG_ClipboardStreamOps * stream, void * opaque, LG_ClipboardRequest * request); diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 7e50d25a..b7eef4da 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -103,6 +103,38 @@ foreach(name IN LISTS LGMP_INPUT_CASES) ) endforeach() +add_executable(lgmp-clipboard-tests + lgmp_clipboard_test.c + ../transports/LGMP/clipboard.c +) +target_include_directories(lgmp-clipboard-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../src" +) +target_link_libraries(lgmp-clipboard-tests + ${EXE_FLAGS} + lg_common + lgmp +) +set(LGMP_CLIPBOARD_CASES + claim + offer-request + inbound-stream + outbound-grant + blocked + cancel + restart + disconnect + malformed +) +foreach(name IN LISTS LGMP_CLIPBOARD_CASES) + add_test(NAME lgmp-clipboard-${name} + COMMAND lgmp-clipboard-tests ${name} + ) + set_tests_properties(lgmp-clipboard-${name} PROPERTIES + TIMEOUT 10 + ) +endforeach() + add_executable(wayland-motion-tests motion_test.c ) @@ -165,9 +197,19 @@ if(ENABLE_WAYLAND) mime replace read-eof + read-block + read-empty + read-ready read-error source + source-release + source-external + source-replace + source-cancel + source-early + source-error teardown + teardown-external self-copy dnd ) @@ -192,14 +234,20 @@ if(ENABLE_X11) target_link_libraries(x11-clipboard-tests ${EXE_FLAGS} lg_common + pthread ) set(X11_CLIPBOARD_CASES targets normal incr + incr-block + incr-ready + incr-cancel + incr-large malformed replace source + source-early teardown ) foreach(name IN LISTS X11_CLIPBOARD_CASES) @@ -431,6 +479,7 @@ set(CLIPBOARD_CASES stream-local stream-blocked stream-remote + request-publish stream-legacy ) foreach(name IN LISTS CLIPBOARD_CASES) diff --git a/client/tests/clipboard_test.c b/client/tests/clipboard_test.c index 284f3c31..52bec160 100644 --- a/client/tests/clipboard_test.c +++ b/client/tests/clipboard_test.c @@ -24,6 +24,7 @@ #include "common/debug.h" +#include #include #include #include @@ -79,6 +80,8 @@ struct Provider uint8_t chunkBuf[MAX_CALL][MAX_DATA]; uint64_t finalSize; LG_ClipboardCancelReason cancelReason; + bool earlyBegin; + LG_ClipboardResult earlyResult; }; struct Display @@ -124,6 +127,8 @@ struct StreamSink uint64_t offset; uint64_t finalSize; LG_ClipboardCancelReason reason; + LG_ClipboardRequest request; + bool requireRequest; size_t size; uint8_t data[MAX_DATA]; }; @@ -140,6 +145,8 @@ static LG_ClipboardResult sinkBegin(void * opaque, LG_ClipboardData type, uint64_t sizeHint) { struct StreamSink * sink = opaque; + if (sink->requireRequest) + CHECK(sink->request != LG_CLIPBOARD_REQUEST_INVALID); ++sink->begin; sink->type = type; sink->sizeHint = sizeHint; @@ -322,6 +329,22 @@ static bool dataReady(void * opaque, LG_ClipboardRequest id) return provider->callOK; } +struct EarlyRequest +{ + struct Provider * provider; + LG_ClipboardRequest request; + LG_ClipboardData type; +}; + +static void * earlyRequestThread(void * opaque) +{ + struct EarlyRequest * early = opaque; + early->provider->earlyResult = early->provider->ev->dataBegin( + early->provider->evCtx, early->request, early->type, + LG_CLIPBOARD_SIZE_UNKNOWN); + return NULL; +} + static bool request(void * opaque, LG_ClipboardRequest id, LG_ClipboardData type) { @@ -330,6 +353,18 @@ static bool request(void * opaque, LG_ClipboardRequest id, const unsigned int no = provider->request++; provider->reqId[no] = id; provider->reqType[no] = type; + if (provider->earlyBegin) + { + struct EarlyRequest early = + { + .provider = provider, + .request = id, + .type = type, + }; + pthread_t thread; + CHECK(pthread_create(&thread, NULL, earlyRequestThread, &early) == 0); + CHECK(pthread_join(thread, NULL) == 0); + } return provider->reqOK; } @@ -954,6 +989,29 @@ static void testStreamRemote(void) lgClipboard_free(); } +static void testRequestPublication(void) +{ + init(); + lgClipboard_setFallback(&streamOps, &p); + const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT }; + notice(&p, types, 1); + + struct StreamSink sink = { 0 }; + initSink(&sink); + sink.requireRequest = true; + p.earlyBegin = true; + CHECK(lgClipboard_requestStream(LG_CLIPBOARD_DATA_TEXT, + &sinkOps, &sink, &sink.request)); + CHECK(sink.request == p.reqId[0]); + CHECK(p.earlyResult == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(sink.begin == 1); + CHECK(p.ev->dataEnd(p.evCtx, sink.request, 0) == + LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(sink.end == 1); + + lgClipboard_free(); +} + static void testStreamLegacy(void) { init(); @@ -1021,6 +1079,7 @@ static const struct Test tests[] = { "stream-local" , testStreamLocal }, { "stream-blocked", testStreamBlocked }, { "stream-remote" , testStreamRemote }, + { "request-publish", testRequestPublication }, { "stream-legacy" , testStreamLegacy }, }; diff --git a/client/tests/lgmp_clipboard_test.c b/client/tests/lgmp_clipboard_test.c new file mode 100644 index 00000000..07b34813 --- /dev/null +++ b/client/tests/lgmp_clipboard_test.c @@ -0,0 +1,1078 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "../transports/LGMP/clipboard.h" + +#include "common/KVMFRClipboard.h" +#include "common/LGMPConfig.h" +#include "common/debug.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#define TEST_SHM_SIZE (8U * 1024U * 1024U) +#define TEST_WAIT_MS 2500U +#define TEST_QUIET_MS 50U +#define TEST_MEMORY_MAX 128U +#define SLOT_BYTES \ + (sizeof(KVMFRClipboardSlotHeader) + KVMFR_CLIPBOARD_DATA_BYTES) + +#define CHECK(x) \ + do \ + { \ + if (!(x)) \ + { \ + fprintf(stderr, "check failed at %s:%d: %s\n", \ + __FILE__, __LINE__, #x); \ + return false; \ + } \ + } \ + while (0) + +typedef struct EventTrace +{ + atomic_uint notice; + atomic_uint release; + atomic_uint request; + atomic_uint requestCancel; + atomic_uint dataBegin; + atomic_uint dataChunk; + atomic_uint dataEnd; + atomic_uint dataCancel; + atomic_uint dataReady; + atomic_bool acceptRequest; + atomic_bool blockChunkOnce; + atomic_bool attached; + LG_ClipboardData noticeTypes[LG_CLIPBOARD_DATA_NONE]; + size_t noticeCount; + LG_ClipboardRequest requestID; + LG_ClipboardData requestType; + LG_ClipboardRequest streamID; + LG_ClipboardData streamType; + uint64_t sizeHint; + uint64_t chunkOffset; + size_t chunkSize; + uint8_t chunkData[64]; + uint64_t finalSize; + LG_ClipboardRequest cancelID; + LG_ClipboardCancelReason cancelReason; +} +EventTrace; + +typedef struct TestState +{ + void * memory; + PLGMPHost host; + PLGMPHostQueue queue; + PLGMPClient client; + LGMPClipboard * clipboard; + const LG_ClipboardOps * ops; + uint32_t clientID; + uint32_t hostSerial; + PLGMPMemory memories[TEST_MEMORY_MAX]; + unsigned memoryCount; + PLGMPMemory grantMemory[KVMFR_CLIPBOARD_SLOT_COUNT]; + atomic_uint statusCount; + atomic_bool statusAvailable; + atomic_uint statusGeneration; + atomic_bool autoAttach; + EventTrace events; +} +TestState; + +typedef struct DisconnectTask +{ + LGMPClipboard * clipboard; + atomic_bool done; +} +DisconnectTask; + +static bool disconnectAndDrain(TestState * state, + uint32_t * releaseGeneration); + +static void notice(void * opaque, const LG_ClipboardData types[], + size_t count) +{ + EventTrace * trace = opaque; + trace->noticeCount = count; + if (count) + memcpy(trace->noticeTypes, types, count * sizeof(*types)); + atomic_fetch_add_explicit(&trace->notice, 1, memory_order_release); +} + +static void releaseNotice(void * opaque) +{ + EventTrace * trace = opaque; + atomic_fetch_add_explicit(&trace->release, 1, memory_order_release); +} + +static bool request(void * opaque, LG_ClipboardRequest id, + LG_ClipboardData type) +{ + EventTrace * trace = opaque; + trace->requestID = id; + trace->requestType = type; + atomic_fetch_add_explicit(&trace->request, 1, memory_order_release); + return atomic_load_explicit(&trace->acceptRequest, memory_order_acquire); +} + +static LG_ClipboardResult dataBegin(void * opaque, + LG_ClipboardRequest id, LG_ClipboardData type, uint64_t sizeHint) +{ + EventTrace * trace = opaque; + trace->streamID = id; + trace->streamType = type; + trace->sizeHint = sizeHint; + atomic_fetch_add_explicit(&trace->dataBegin, 1, memory_order_release); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult dataChunk(void * opaque, + LG_ClipboardRequest id, uint64_t offset, + const void * data, size_t size) +{ + EventTrace * trace = opaque; + trace->streamID = id; + trace->chunkOffset = offset; + trace->chunkSize = size; + if (size <= sizeof(trace->chunkData)) + memcpy(trace->chunkData, data, size); + atomic_fetch_add_explicit(&trace->dataChunk, 1, memory_order_release); + if (atomic_exchange_explicit(&trace->blockChunkOnce, false, + memory_order_acq_rel)) + return LG_CLIPBOARD_RESULT_BLOCKED; + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult dataEnd(void * opaque, + LG_ClipboardRequest id, uint64_t size) +{ + EventTrace * trace = opaque; + trace->streamID = id; + trace->finalSize = size; + atomic_fetch_add_explicit(&trace->dataEnd, 1, memory_order_release); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static void dataCancelEvent(void * opaque, LG_ClipboardRequest id, + LG_ClipboardCancelReason reason) +{ + EventTrace * trace = opaque; + trace->cancelID = id; + trace->cancelReason = reason; + atomic_fetch_add_explicit(&trace->dataCancel, 1, memory_order_release); +} + +static void dataReadyEvent(void * opaque, LG_ClipboardRequest id) +{ + EventTrace * trace = opaque; + trace->streamID = id; + atomic_fetch_add_explicit(&trace->dataReady, 1, memory_order_release); +} + +static void requestCancelEvent(void * opaque, LG_ClipboardRequest id, + LG_ClipboardCancelReason reason) +{ + EventTrace * trace = opaque; + trace->cancelID = id; + trace->cancelReason = reason; + atomic_fetch_add_explicit( + &trace->requestCancel, 1, memory_order_release); +} + +static const LG_ClipboardEventOps EVENT_OPS = +{ + .notice = notice, + .dataBegin = dataBegin, + .dataChunk = dataChunk, + .dataEnd = dataEnd, + .dataCancel = dataCancelEvent, + .dataReady = dataReadyEvent, + .requestCancel = requestCancelEvent, + .release = releaseNotice, + .request = request, +}; + +static void statusChanged(void * opaque, const LG_ClipboardStatus * status) +{ + TestState * state = opaque; + atomic_store_explicit(&state->statusAvailable, + status->available, memory_order_release); + atomic_store_explicit(&state->statusGeneration, + status->generation, memory_order_release); + atomic_fetch_add_explicit(&state->statusCount, 1, memory_order_release); + + if (status->available && + atomic_load_explicit(&state->autoAttach, memory_order_acquire) && + !atomic_exchange_explicit( + &state->events.attached, true, memory_order_acq_rel)) + { + const bool attached = state->ops->attach( + state->clipboard, &EVENT_OPS, &state->events); + atomic_store_explicit( + &state->events.attached, attached, memory_order_release); + } +} + +static bool hostProcess(TestState * state) +{ + CHECK(lgmpHostProcess(state->host) == LGMP_OK); + return true; +} + +static bool waitAtomic(TestState * state, atomic_uint * value, + unsigned expected) +{ + for (unsigned i = 0; i < TEST_WAIT_MS; ++i) + { + CHECK(hostProcess(state)); + if (atomic_load_explicit(value, memory_order_acquire) >= expected) + return true; + usleep(1000); + } + return false; +} + +static bool waitMemory(TestState * state, PLGMPMemory memory) +{ + for (unsigned i = 0; i < TEST_WAIT_MS; ++i) + { + CHECK(hostProcess(state)); + if (!lgmpHostQueuePayloadPending(state->queue, memory)) + return true; + usleep(1000); + } + return false; +} + +static bool allocMemory(TestState * state, size_t size, + PLGMPMemory * result) +{ + CHECK(state->memoryCount < TEST_MEMORY_MAX); + CHECK(lgmpHostMemAlloc(state->host, (uint32_t)size, result) == LGMP_OK); + state->memories[state->memoryCount++] = *result; + memset(lgmpHostMemPtr(*result), 0, size); + return true; +} + +static bool postMemory(TestState * state, PLGMPMemory memory, + KVMFRClipboardQueueType type, uint32_t serial) +{ + const uint64_t udata = KVMFR_CLIPBOARD_QUEUE_UDATA(type, serial); + for (unsigned i = 0; i < TEST_WAIT_MS; ++i) + { + unsigned recipients = 0; + const LGMP_STATUS status = lgmpHostQueuePostForClients(state->queue, + udata, memory, &state->clientID, 1, &recipients); + if (status == LGMP_OK) + return recipients == 1; + CHECK(status == LGMP_ERR_QUEUE_FULL); + CHECK(hostProcess(state)); + usleep(1000); + } + return false; +} + +static uint32_t nextSerial(TestState * state) +{ + if (++state->hostSerial == 0) + ++state->hostSerial; + return state->hostSerial; +} + +static bool postStatus(TestState * state, uint32_t endpointGeneration, + uint32_t ownerClientID, uint32_t ownerGeneration, + bool available, KVMFRClipboardFormatFlags formats) +{ + PLGMPMemory memory; + CHECK(allocMemory(state, sizeof(KVMFRClipboardStatus), &memory)); + KVMFRClipboardStatus * status = lgmpHostMemPtr(memory); + *status = (KVMFRClipboardStatus) + { + .version = KVMFR_CLIPBOARD_VERSION, + .flags = (available ? KVMFR_CLIPBOARD_STATUS_AVAILABLE : 0) | + (ownerClientID ? KVMFR_CLIPBOARD_STATUS_HAS_OWNER : 0), + .generation = endpointGeneration, + .ownerClientID = ownerClientID, + .ownerGeneration = ownerGeneration, + .lease = 1000, + .formats = available ? formats : 0, + .slotBytes = KVMFR_CLIPBOARD_DATA_BYTES, + }; + CHECK(postMemory(state, memory, KVMFR_CLIPBOARD_QUEUE_STATUS, + nextSerial(state))); + CHECK(waitMemory(state, memory)); + return true; +} + +static bool postRecord(TestState * state, + const KVMFRClipboardMessage * record, + KVMFRClipboardQueueType type, const void * data) +{ + const size_t bytes = type == KVMFR_CLIPBOARD_QUEUE_DATA ? + SLOT_BYTES : sizeof(*record); + PLGMPMemory memory; + CHECK(allocMemory(state, bytes, &memory)); + memcpy(lgmpHostMemPtr(memory), record, sizeof(*record)); + if (record->length && data) + memcpy((uint8_t *)lgmpHostMemPtr(memory) + sizeof(*record), + data, record->length); + CHECK(postMemory(state, memory, type, nextSerial(state))); + return true; +} + +static bool postGrant(TestState * state, uint32_t generation, + uint32_t token) +{ + CHECK(token >= 1 && token <= KVMFR_CLIPBOARD_SLOT_COUNT); + PLGMPMemory * memory = &state->grantMemory[token - 1]; + if (!*memory) + CHECK(allocMemory(state, SLOT_BYTES, memory)); + else + CHECK(waitMemory(state, *memory)); + + KVMFRClipboardSlotHeader * header = lgmpHostMemPtr(*memory); + memset(header, 0, sizeof(*header)); + header->version = KVMFR_CLIPBOARD_VERSION; + header->type = KVMFR_CLIPBOARD_MESSAGE_GRANT; + header->generation = generation; + header->size = KVMFR_CLIPBOARD_DATA_BYTES; + header->token = token; + return postMemory(state, *memory, + KVMFR_CLIPBOARD_QUEUE_GRANT, token); +} + +static bool readClient(TestState * state, KVMFRClipboardMessage * record) +{ + for (unsigned i = 0; i < TEST_WAIT_MS; ++i) + { + CHECK(hostProcess(state)); + size_t size = sizeof(*record); + uint32_t clientID = 0; + const LGMP_STATUS status = lgmpHostReadDataWithSource( + state->queue, record, &size, &clientID); + if (status == LGMP_ERR_QUEUE_EMPTY) + { + usleep(1000); + continue; + } + CHECK(status == LGMP_OK); + CHECK(size == sizeof(*record)); + CHECK(clientID == state->clientID); + CHECK(lgmpHostAckData(state->queue) == LGMP_OK); + return true; + } + return false; +} + +static bool readType(TestState * state, KVMFRClipboardMessageType type, + KVMFRClipboardMessage * record) +{ + for (unsigned i = 0; i < 32; ++i) + { + CHECK(readClient(state, record)); + if (record->type == type) + return true; + } + return false; +} + +static bool noClientData(TestState * state) +{ + for (unsigned i = 0; i < TEST_QUIET_MS; ++i) + { + CHECK(hostProcess(state)); + KVMFRClipboardMessage record; + size_t size = sizeof(record); + uint32_t clientID = 0; + const LGMP_STATUS status = lgmpHostReadDataWithSource( + state->queue, &record, &size, &clientID); + if (status == LGMP_ERR_QUEUE_EMPTY) + { + usleep(1000); + continue; + } + if (status == LGMP_OK) + lgmpHostAckData(state->queue); + CHECK(status == LGMP_ERR_QUEUE_EMPTY); + } + return true; +} + +static bool claim(TestState * state, uint32_t endpointGeneration, + bool autoAttach, KVMFRClipboardMessage * result) +{ + atomic_store(&state->autoAttach, autoAttach); + CHECK(postStatus(state, endpointGeneration, 0, 0, true, 0)); + if (autoAttach) + { + for (unsigned i = 0; i < TEST_WAIT_MS && + !atomic_load(&state->events.attached); ++i) + { + CHECK(hostProcess(state)); + usleep(1000); + } + CHECK(atomic_load(&state->events.attached)); + } + else + { + CHECK(state->ops->attach( + state->clipboard, &EVENT_OPS, &state->events)); + atomic_store(&state->events.attached, true); + } + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CLAIM, result)); + CHECK(result->version == KVMFR_CLIPBOARD_VERSION); + CHECK(result->generation != 0); + CHECK(result->token == endpointGeneration); + return true; +} + +static bool own(TestState * state, uint32_t endpointGeneration, + uint32_t claimGeneration) +{ + CHECK(postStatus(state, endpointGeneration, state->clientID, + claimGeneration, true, 0)); + return true; +} + +static KVMFRClipboardMessage hostRecord( + KVMFRClipboardMessageType type, uint32_t generation) +{ + return (KVMFRClipboardMessage) + { + .version = KVMFR_CLIPBOARD_VERSION, + .type = type, + .generation = generation, + }; +} + +static bool offerAndRequest(TestState * state, uint32_t generation, + uint64_t clipboardGeneration, LG_ClipboardRequest appRequest, + KVMFRClipboardMessage * wireRequest) +{ + KVMFRClipboardMessage offer = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, generation); + offer.clipboardGeneration = clipboardGeneration; + offer.token = KVMFR_CLIPBOARD_FORMAT_MASK_TEXT | + KVMFR_CLIPBOARD_FORMAT_MASK_PNG; + const unsigned notices = atomic_load(&state->events.notice) + 1; + CHECK(postRecord(state, &offer, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.notice, notices)); + CHECK(state->events.noticeCount == 2); + CHECK(state->events.noticeTypes[0] == LG_CLIPBOARD_DATA_TEXT); + CHECK(state->events.noticeTypes[1] == LG_CLIPBOARD_DATA_PNG); + CHECK(!state->ops->request(state->clipboard, + appRequest + 1, LG_CLIPBOARD_DATA_JPEG)); + CHECK(state->ops->request(state->clipboard, + appRequest, LG_CLIPBOARD_DATA_TEXT)); + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_REQUEST, wireRequest)); + CHECK(kvmfrClipboardTransferFromClient(wireRequest->transfer)); + CHECK(wireRequest->generation == generation); + CHECK(wireRequest->clipboardGeneration == clipboardGeneration); + CHECK(wireRequest->format == KVMFR_CLIPBOARD_FORMAT_TEXT); + return true; +} + +static bool testClaim(TestState * state) +{ + KVMFRClipboardMessage claimRecord; + CHECK(claim(state, 10, true, &claimRecord)); + CHECK(own(state, 10, claimRecord.generation)); + + const LG_ClipboardData formats[] = { LG_CLIPBOARD_DATA_TEXT }; + CHECK(state->ops->notifyTypes(state->clipboard, formats, 1)); + KVMFRClipboardMessage offer; + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &offer)); + CHECK(offer.generation == claimRecord.generation); + CHECK(offer.clipboardGeneration != 0); + CHECK(offer.token == KVMFR_CLIPBOARD_FORMAT_MASK_TEXT); + return true; +} + +static bool testOfferRequest(TestState * state) +{ + KVMFRClipboardMessage claimRecord; + CHECK(claim(state, 20, false, &claimRecord)); + CHECK(own(state, 20, claimRecord.generation)); + KVMFRClipboardMessage requestRecord; + CHECK(offerAndRequest(state, claimRecord.generation, + 200, 77, &requestRecord)); + return true; +} + +static bool testInboundStream(TestState * state) +{ + KVMFRClipboardMessage claimRecord; + CHECK(claim(state, 30, false, &claimRecord)); + CHECK(own(state, 30, claimRecord.generation)); + KVMFRClipboardMessage requestRecord; + CHECK(offerAndRequest(state, claimRecord.generation, + 300, 88, &requestRecord)); + + const uint8_t bytes[] = { 1, 2, 3, 4, 5 }; + KVMFRClipboardMessage data = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, claimRecord.generation); + data.clipboardGeneration = 300; + data.transfer = requestRecord.transfer; + data.size = 5; + data.format = KVMFR_CLIPBOARD_FORMAT_TEXT; + data.flags = KVMFR_CLIPBOARD_FLAG_BEGIN; + CHECK(postRecord(state, &data, KVMFR_CLIPBOARD_QUEUE_DATA, NULL)); + CHECK(waitAtomic(state, &state->events.dataBegin, 1)); + CHECK(atomic_load(&state->events.dataBegin) == 1); + CHECK(atomic_load(&state->events.dataChunk) == 0); + CHECK(state->events.streamID == 88); + CHECK(state->events.sizeHint == 5); + + data.sequence = 1; + data.size = KVMFR_CLIPBOARD_SIZE_UNKNOWN; + data.flags = 0; + data.length = sizeof(bytes); + CHECK(postRecord(state, &data, KVMFR_CLIPBOARD_QUEUE_DATA, bytes)); + CHECK(waitAtomic(state, &state->events.dataChunk, 1)); + CHECK(state->events.chunkOffset == 0); + CHECK(state->events.chunkSize == sizeof(bytes)); + CHECK(memcmp(state->events.chunkData, bytes, sizeof(bytes)) == 0); + + data.sequence = 2; + data.offset = sizeof(bytes); + data.size = sizeof(bytes); + data.flags = KVMFR_CLIPBOARD_FLAG_END; + data.length = 0; + CHECK(postRecord(state, &data, KVMFR_CLIPBOARD_QUEUE_DATA, NULL)); + CHECK(waitAtomic(state, &state->events.dataEnd, 1)); + CHECK(atomic_load(&state->events.dataChunk) == 1); + CHECK(state->events.finalSize == 5); + + KVMFRClipboardMessage request2; + CHECK(offerAndRequest(state, claimRecord.generation, + 301, 89, &request2)); + data = hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, + claimRecord.generation); + data.clipboardGeneration = 301; + data.transfer = request2.transfer; + data.format = KVMFR_CLIPBOARD_FORMAT_TEXT; + data.flags = KVMFR_CLIPBOARD_FLAG_BEGIN | + KVMFR_CLIPBOARD_FLAG_END; + CHECK(postRecord(state, &data, KVMFR_CLIPBOARD_QUEUE_DATA, NULL)); + CHECK(waitAtomic(state, &state->events.dataEnd, 2)); + CHECK(atomic_load(&state->events.dataBegin) == 2); + CHECK(state->events.finalSize == 0); + return true; +} + +static bool prepareOutbound(TestState * state, uint32_t endpoint, + KVMFRClipboardMessage * claimRecord, uint64_t transfer) +{ + CHECK(claim(state, endpoint, false, claimRecord)); + CHECK(own(state, endpoint, claimRecord->generation)); + const LG_ClipboardData formats[] = { LG_CLIPBOARD_DATA_TEXT }; + CHECK(state->ops->notifyTypes(state->clipboard, formats, 1)); + KVMFRClipboardMessage offer; + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &offer)); + + KVMFRClipboardMessage requestRecord = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST, + claimRecord->generation); + requestRecord.clipboardGeneration = offer.clipboardGeneration; + requestRecord.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | transfer; + requestRecord.format = KVMFR_CLIPBOARD_FORMAT_TEXT; + const unsigned requests = atomic_load(&state->events.request) + 1; + CHECK(postRecord(state, &requestRecord, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.request, requests)); + CHECK(state->events.requestID == requestRecord.transfer); + CHECK(state->events.requestType == LG_CLIPBOARD_DATA_TEXT); + return true; +} + +static bool checkCommit(TestState * state, uint32_t token, + KVMFRClipboardMessage * commit, KVMFRClipboardMessage * slot, + const void ** data) +{ + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_COMMIT, commit)); + CHECK(commit->token == token); + *slot = *(KVMFRClipboardMessage *) + lgmpHostMemPtr(state->grantMemory[token - 1]); + CHECK(slot->type == KVMFR_CLIPBOARD_MESSAGE_DATA); + CHECK(slot->generation == commit->generation); + CHECK(slot->clipboardGeneration == commit->clipboardGeneration); + CHECK(slot->transfer == commit->transfer); + CHECK(slot->offset == commit->offset); + CHECK(slot->size == commit->size); + CHECK(slot->format == commit->format); + CHECK(slot->flags == commit->flags); + CHECK(slot->length == commit->length); + CHECK(slot->sequence == commit->sequence); + *data = (const uint8_t *)lgmpHostMemPtr( + state->grantMemory[token - 1]) + sizeof(*slot); + return true; +} + +static bool testOutboundGrant(TestState * state) +{ + KVMFRClipboardMessage claimRecord; + CHECK(prepareOutbound(state, 40, &claimRecord, 1)); + const LG_ClipboardRequest transfer = state->events.requestID; + CHECK(postGrant(state, claimRecord.generation, 1)); + CHECK(waitMemory(state, state->grantMemory[0])); + + CHECK(state->ops->dataBegin(state->clipboard, transfer, + LG_CLIPBOARD_DATA_TEXT, 5) == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(noClientData(state)); + const uint8_t first[] = { 1, 2, 3 }; + CHECK(state->ops->dataChunk(state->clipboard, transfer, + 0, first, sizeof(first)) == LG_CLIPBOARD_RESULT_ACCEPTED); + KVMFRClipboardMessage commit; + KVMFRClipboardMessage slot; + const void * data; + CHECK(checkCommit(state, 1, &commit, &slot, &data)); + CHECK(slot.flags == KVMFR_CLIPBOARD_FLAG_BEGIN); + CHECK(slot.sequence == 0 && slot.offset == 0 && slot.size == 5); + CHECK(memcmp(data, first, sizeof(first)) == 0); + + CHECK(postGrant(state, claimRecord.generation, 1)); + CHECK(waitMemory(state, state->grantMemory[0])); + const uint8_t second[] = { 4, 5 }; + CHECK(state->ops->dataChunk(state->clipboard, transfer, + 3, second, sizeof(second)) == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(checkCommit(state, 1, &commit, &slot, &data)); + CHECK(slot.flags == 0 && slot.sequence == 1 && slot.offset == 3); + CHECK(slot.size == KVMFR_CLIPBOARD_SIZE_UNKNOWN); + CHECK(memcmp(data, second, sizeof(second)) == 0); + + CHECK(postGrant(state, claimRecord.generation, 1)); + CHECK(waitMemory(state, state->grantMemory[0])); + CHECK(state->ops->dataEnd(state->clipboard, transfer, 5) == + LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(checkCommit(state, 1, &commit, &slot, &data)); + CHECK(slot.flags == KVMFR_CLIPBOARD_FLAG_END); + CHECK(slot.sequence == 2 && slot.offset == 5 && slot.size == 5); + CHECK(slot.length == 0); + + KVMFRClipboardMessage requestRecord = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST, + claimRecord.generation); + requestRecord.clipboardGeneration = slot.clipboardGeneration; + requestRecord.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | 2; + requestRecord.format = KVMFR_CLIPBOARD_FORMAT_TEXT; + CHECK(postRecord(state, &requestRecord, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.request, 2)); + CHECK(postGrant(state, claimRecord.generation, 1)); + CHECK(waitMemory(state, state->grantMemory[0])); + CHECK(state->ops->dataBegin(state->clipboard, requestRecord.transfer, + LG_CLIPBOARD_DATA_TEXT, 0) == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(state->ops->dataEnd(state->clipboard, + requestRecord.transfer, 0) == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(checkCommit(state, 1, &commit, &slot, &data)); + CHECK(slot.flags == (KVMFR_CLIPBOARD_FLAG_BEGIN | + KVMFR_CLIPBOARD_FLAG_END)); + CHECK(slot.sequence == 0 && slot.offset == 0 && slot.size == 0); + return true; +} + +static bool testBlocked(TestState * state) +{ + KVMFRClipboardMessage claimRecord; + CHECK(prepareOutbound(state, 50, &claimRecord, 3)); + const LG_ClipboardRequest transfer = state->events.requestID; + CHECK(state->ops->dataBegin(state->clipboard, transfer, + LG_CLIPBOARD_DATA_TEXT, 2) == LG_CLIPBOARD_RESULT_ACCEPTED); + const uint8_t bytes[] = { 8, 9 }; + CHECK(state->ops->dataChunk(state->clipboard, transfer, + 0, bytes, sizeof(bytes)) == LG_CLIPBOARD_RESULT_BLOCKED); + CHECK(postGrant(state, claimRecord.generation, 1)); + CHECK(waitAtomic(state, &state->events.dataReady, 1)); + CHECK(state->events.streamID == transfer); + CHECK(state->ops->dataChunk(state->clipboard, transfer, + 0, bytes, sizeof(bytes)) == LG_CLIPBOARD_RESULT_ACCEPTED); + KVMFRClipboardMessage commit; + KVMFRClipboardMessage slot; + const void * data; + CHECK(checkCommit(state, 1, &commit, &slot, &data)); + + KVMFRClipboardMessage requestRecord; + CHECK(offerAndRequest(state, claimRecord.generation, + 501, 5001, &requestRecord)); + atomic_store(&state->events.blockChunkOnce, true); + KVMFRClipboardMessage wire = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, claimRecord.generation); + wire.clipboardGeneration = 501; + wire.transfer = requestRecord.transfer; + wire.size = 1; + wire.format = KVMFR_CLIPBOARD_FORMAT_TEXT; + wire.flags = KVMFR_CLIPBOARD_FLAG_BEGIN | + KVMFR_CLIPBOARD_FLAG_END; + wire.length = 1; + CHECK(postRecord(state, &wire, KVMFR_CLIPBOARD_QUEUE_DATA, bytes)); + CHECK(waitAtomic(state, &state->events.dataChunk, 1)); + CHECK(atomic_load(&state->events.dataEnd) == 0); + CHECK(state->ops->dataReady(state->clipboard, 5001)); + CHECK(waitAtomic(state, &state->events.dataEnd, 1)); + CHECK(atomic_load(&state->events.dataChunk) == 2); + return true; +} + +static bool testCancel(TestState * state) +{ + KVMFRClipboardMessage claimRecord; + CHECK(prepareOutbound(state, 60, &claimRecord, 4)); + const LG_ClipboardRequest outbound = state->events.requestID; + KVMFRClipboardMessage cancel = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_CANCEL, claimRecord.generation); + cancel.clipboardGeneration = 1; + cancel.transfer = outbound; + cancel.format = KVMFR_CLIPBOARD_FORMAT_TEXT; + cancel.token = LG_CLIPBOARD_CANCEL_ABORTED; + CHECK(postRecord(state, &cancel, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.requestCancel, 1)); + CHECK(state->events.cancelID == outbound); + + KVMFRClipboardMessage requestRecord; + CHECK(offerAndRequest(state, claimRecord.generation, + 601, 6001, &requestRecord)); + cancel.clipboardGeneration = 601; + cancel.transfer = requestRecord.transfer; + cancel.token = LG_CLIPBOARD_CANCEL_REPLACED; + CHECK(postRecord(state, &cancel, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.dataCancel, 1)); + CHECK(state->events.cancelID == 6001); + CHECK(state->events.cancelReason == LG_CLIPBOARD_CANCEL_REPLACED); + + KVMFRClipboardMessage outboundRequest = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST, + claimRecord.generation); + outboundRequest.clipboardGeneration = 1; + outboundRequest.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | 5; + outboundRequest.format = KVMFR_CLIPBOARD_FORMAT_TEXT; + CHECK(postRecord(state, &outboundRequest, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitAtomic(state, &state->events.request, 2)); + CHECK(state->ops->dataCancel(state->clipboard, + outboundRequest.transfer, LG_CLIPBOARD_CANCEL_ABORTED)); + KVMFRClipboardMessage wireCancel; + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CANCEL, &wireCancel)); + CHECK(wireCancel.transfer == outboundRequest.transfer); + CHECK(wireCancel.clipboardGeneration == 1); + CHECK(wireCancel.format == KVMFR_CLIPBOARD_FORMAT_TEXT); + CHECK(wireCancel.token == LG_CLIPBOARD_CANCEL_ABORTED); + return true; +} + +static bool testRestart(TestState * state) +{ + KVMFRClipboardMessage oldClaim; + CHECK(claim(state, 70, false, &oldClaim)); + CHECK(own(state, 70, oldClaim.generation)); + const LG_ClipboardData formats[] = { LG_CLIPBOARD_DATA_TEXT }; + CHECK(state->ops->notifyTypes(state->clipboard, formats, 1)); + KVMFRClipboardMessage oldOffer; + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &oldOffer)); + + CHECK(postStatus(state, 71, 0, 0, true, 0)); + KVMFRClipboardMessage newClaim; + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CLAIM, &newClaim)); + CHECK(newClaim.generation != oldClaim.generation); + CHECK(newClaim.token == 71); + KVMFRClipboardMessage replay; + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &replay)); + CHECK(replay.generation == newClaim.generation); + CHECK(replay.clipboardGeneration == oldOffer.clipboardGeneration); + CHECK(own(state, 71, newClaim.generation)); + + CHECK(postStatus(state, 71, 0, 0, true, 0)); + KVMFRClipboardMessage reclaimed; + CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CLAIM, &reclaimed)); + CHECK(reclaimed.generation != newClaim.generation); + CHECK(reclaimed.token == 71); + return true; +} + +static void * disconnectThread(void * opaque) +{ + DisconnectTask * task = opaque; + lgmpClipboard_disconnect(task->clipboard); + atomic_store(&task->done, true); + return NULL; +} + +static bool disconnectAndDrain(TestState * state, + uint32_t * releaseGeneration) +{ + DisconnectTask task = { .clipboard = state->clipboard }; + atomic_init(&task.done, false); + pthread_t thread; + CHECK(pthread_create(&thread, NULL, disconnectThread, &task) == 0); + bool valid = true; + for (unsigned i = 0; i < TEST_WAIT_MS && !atomic_load(&task.done); ++i) + { + if (lgmpHostProcess(state->host) != LGMP_OK) + valid = false; + for (;;) + { + KVMFRClipboardMessage record; + size_t size = sizeof(record); + uint32_t clientID = 0; + const LGMP_STATUS status = lgmpHostReadDataWithSource( + state->queue, &record, &size, &clientID); + if (status == LGMP_ERR_QUEUE_EMPTY) + break; + if (status != LGMP_OK) + { + valid = false; + break; + } + valid &= size == sizeof(record); + valid &= clientID == state->clientID; + if (record.type == KVMFR_CLIPBOARD_MESSAGE_RELEASE && + releaseGeneration) + *releaseGeneration = record.generation; + if (lgmpHostAckData(state->queue) != LGMP_OK) + valid = false; + } + usleep(1000); + } + valid &= pthread_join(thread, NULL) == 0; + valid &= atomic_load(&task.done); + return valid; +} + +static bool testDisconnect(TestState * state) +{ + KVMFRClipboardMessage claimRecord; + CHECK(claim(state, 80, false, &claimRecord)); + CHECK(own(state, 80, claimRecord.generation)); + uint32_t releaseGeneration = 0; + CHECK(disconnectAndDrain(state, &releaseGeneration)); + CHECK(releaseGeneration == claimRecord.generation); + return true; +} + +static bool testMalformed(TestState * state) +{ + KVMFRClipboardMessage claimRecord; + CHECK(claim(state, 90, false, &claimRecord)); + CHECK(own(state, 90, claimRecord.generation)); + + const unsigned statusCount = atomic_load(&state->statusCount); + PLGMPMemory badStatusMemory; + CHECK(allocMemory(state, sizeof(KVMFRClipboardStatus), + &badStatusMemory)); + KVMFRClipboardStatus * badStatus = lgmpHostMemPtr(badStatusMemory); + *badStatus = (KVMFRClipboardStatus) + { + .version = KVMFR_CLIPBOARD_VERSION, + .flags = KVMFR_CLIPBOARD_STATUS_AVAILABLE, + .generation = 91, + .lease = 1000, + }; + CHECK(postMemory(state, badStatusMemory, + KVMFR_CLIPBOARD_QUEUE_STATUS, nextSerial(state))); + CHECK(waitMemory(state, badStatusMemory)); + CHECK(atomic_load(&state->statusCount) == statusCount); + + KVMFRClipboardMessage malformed = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, claimRecord.generation + 1); + malformed.clipboardGeneration = 900; + malformed.token = KVMFR_CLIPBOARD_FORMAT_MASK_TEXT; + CHECK(postRecord(state, &malformed, + KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL)); + CHECK(waitMemory(state, state->memories[state->memoryCount - 1])); + CHECK(atomic_load(&state->events.notice) == 0); + + malformed.generation = claimRecord.generation; + CHECK(postRecord(state, &malformed, KVMFR_CLIPBOARD_QUEUE_DATA, NULL)); + CHECK(waitMemory(state, state->memories[state->memoryCount - 1])); + CHECK(atomic_load(&state->events.notice) == 0); + + CHECK(postGrant(state, claimRecord.generation, 1)); + CHECK(waitMemory(state, state->grantMemory[0])); + CHECK(postGrant(state, claimRecord.generation, 1)); + CHECK(waitMemory(state, state->grantMemory[0])); + + KVMFRClipboardMessage requestRecord; + CHECK(offerAndRequest(state, claimRecord.generation, + 901, 9001, &requestRecord)); + KVMFRClipboardMessage data = + hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, claimRecord.generation); + data.clipboardGeneration = 901; + data.transfer = requestRecord.transfer; + data.offset = 1; + data.size = 1; + data.format = KVMFR_CLIPBOARD_FORMAT_TEXT; + data.flags = KVMFR_CLIPBOARD_FLAG_BEGIN | + KVMFR_CLIPBOARD_FLAG_END; + CHECK(postRecord(state, &data, KVMFR_CLIPBOARD_QUEUE_DATA, NULL)); + CHECK(waitMemory(state, state->memories[state->memoryCount - 1])); + CHECK(atomic_load(&state->events.dataBegin) == 0); + + const uint8_t byte = 42; + data.offset = 0; + data.length = 1; + CHECK(postRecord(state, &data, KVMFR_CLIPBOARD_QUEUE_DATA, &byte)); + CHECK(waitAtomic(state, &state->events.dataEnd, 1)); + CHECK(atomic_load(&state->events.dataBegin) == 1); + CHECK(atomic_load(&state->events.dataChunk) == 1); + CHECK(state->events.streamID == 9001); + CHECK(state->events.chunkData[0] == byte); + return true; +} + +static bool stateInit(TestState * state) +{ + memset(state, 0, sizeof(*state)); + state->memory = MAP_FAILED; + atomic_init(&state->statusCount, 0); + atomic_init(&state->statusAvailable, false); + atomic_init(&state->statusGeneration, 0); + atomic_init(&state->autoAttach, false); + atomic_init(&state->events.notice, 0); + atomic_init(&state->events.release, 0); + atomic_init(&state->events.request, 0); + atomic_init(&state->events.requestCancel, 0); + atomic_init(&state->events.dataBegin, 0); + atomic_init(&state->events.dataChunk, 0); + atomic_init(&state->events.dataEnd, 0); + atomic_init(&state->events.dataCancel, 0); + atomic_init(&state->events.dataReady, 0); + atomic_init(&state->events.acceptRequest, true); + atomic_init(&state->events.blockChunkOnce, false); + atomic_init(&state->events.attached, false); + + state->memory = mmap(NULL, TEST_SHM_SIZE, PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_ANONYMOUS, -1, 0); + CHECK(state->memory != MAP_FAILED); + const uint32_t sessionData = UINT32_C(0x12345678); + CHECK(lgmpHostInit(state->memory, TEST_SHM_SIZE, &state->host, + sizeof(sessionData), (uint8_t *)&sessionData) == LGMP_OK); + const struct LGMPQueueConfig config = + { + .queueID = LGMP_Q_CLIPBOARD, + .numMessages = LGMP_Q_CLIPBOARD_LEN, + .subTimeout = 1000, + }; + CHECK(lgmpHostQueueNew(state->host, config, &state->queue) == LGMP_OK); + CHECK(lgmpClientInit(state->memory, TEST_SHM_SIZE, &state->client) == + LGMP_OK); + usleep(300000); + CHECK(hostProcess(state)); + uint32_t dataSize; + uint32_t remoteVersion; + uint8_t * data; + CHECK(lgmpClientSessionInit(state->client, &dataSize, &data, + &state->clientID, &remoteVersion) == LGMP_OK); + CHECK(state->clientID != 0); + CHECK(dataSize == sizeof(sessionData)); + CHECK(memcmp(data, &sessionData, sizeof(sessionData)) == 0); + + CHECK(lgmpClipboard_create(state->client, &state->clipboard)); + CHECK(lgmpClipboard_connect(state->clipboard, state->clientID)); + state->ops = lgmpClipboard_getOps(); + CHECK(state->ops); + state->ops->setStatusListener( + state->clipboard, statusChanged, state); + CHECK(atomic_load(&state->statusCount) == 1); + CHECK(!atomic_load(&state->statusAvailable)); + + for (unsigned i = 0; i < TEST_WAIT_MS; ++i) + { + CHECK(hostProcess(state)); + if (lgmpHostQueueHasSubs(state->queue)) + return true; + usleep(1000); + } + return false; +} + +static void stateFree(TestState * state) +{ + if (state->clipboard) + { + state->ops->setStatusListener(state->clipboard, NULL, NULL); + if (atomic_load(&state->events.attached)) + state->ops->detach(state->clipboard); + disconnectAndDrain(state, NULL); + lgmpClipboard_destroy(&state->clipboard); + } + lgmpClientFree(&state->client); + for (unsigned i = 0; i < state->memoryCount; ++i) + lgmpHostMemFree(&state->memories[i]); + lgmpHostFree(&state->host); + if (state->memory != MAP_FAILED) + munmap(state->memory, TEST_SHM_SIZE); +} + +typedef bool (*TestFn)(TestState * state); + +static const struct +{ + const char * name; + TestFn run; +} +TESTS[] = +{ + { "claim" , testClaim }, + { "offer-request" , testOfferRequest }, + { "inbound-stream", testInboundStream }, + { "outbound-grant", testOutboundGrant }, + { "blocked" , testBlocked }, + { "cancel" , testCancel }, + { "restart" , testRestart }, + { "disconnect" , testDisconnect }, + { "malformed" , testMalformed }, +}; + +int main(int argc, char * argv[]) +{ + if (argc != 2) + { + fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + TestFn test = NULL; + for (unsigned i = 0; i < sizeof(TESTS) / sizeof(TESTS[0]); ++i) + if (strcmp(argv[1], TESTS[i].name) == 0) + { + test = TESTS[i].run; + break; + } + if (!test) + { + fprintf(stderr, "unknown test case: %s\n", argv[1]); + return 2; + } + + debug_init(); + TestState state = { 0 }; + state.memory = MAP_FAILED; + const bool initialized = stateInit(&state); + const bool passed = initialized && test(&state); + stateFree(&state); + return passed ? 0 : 1; +} diff --git a/client/tests/wayland_clipboard_test.c b/client/tests/wayland_clipboard_test.c index 759f9b99..2c2c5a93 100644 --- a/client/tests/wayland_clipboard_test.c +++ b/client/tests/wayland_clipboard_test.c @@ -20,6 +20,7 @@ #include "wayland.h" #include "test.h" +#include "../src/clipboard.h" #include "common/debug.h" @@ -76,9 +77,12 @@ struct Proto unsigned int dirtyDestroyN; unsigned int receiveN; unsigned int setActionsN; + unsigned int selectionSetN; char receiveMime[80]; const void * receiveData; size_t receiveSize; + int receiveFd; + bool keepReceiveOpen; struct Proxy * selection; uint32_t serial; }; @@ -97,7 +101,7 @@ struct Poll struct TypeNotice { LG_ClipboardData type[LG_CLIPBOARD_DATA_NONE]; - int count; + size_t count; }; struct DataNotice @@ -108,11 +112,25 @@ struct DataNotice size_t size; }; +struct StreamNotice +{ + LG_ClipboardRequest request; + LG_ClipboardData type; + uint64_t sizeHint; + uint64_t finalSize; + uint8_t data[MAX_TRANSFER]; + size_t size; + unsigned int beginN; + unsigned int chunkN; + unsigned int endN; +}; + struct Log { struct Poll poll[MAX_POLL]; struct TypeNotice notice[MAX_NOTICE]; struct DataNotice data[MAX_DATA]; + struct StreamNotice streamData; LG_ClipboardRequest abort[MAX_ABORT]; unsigned int pollN; unsigned int pollUnregisterN; @@ -122,11 +140,21 @@ struct Log unsigned int abortN; unsigned int releaseN; unsigned int requestN; + unsigned int requestReadyN; LG_ClipboardData requestType; - LG_ClipboardReplyFn reply; - void * replyOpaque; + const LG_ClipboardStreamOps * requestStream; + void * requestOpaque; + LG_ClipboardRequest requestId; + LG_ClipboardResult beginResult; + LG_ClipboardResult chunkResult; + LG_ClipboardResult endResult; + LG_ClipboardResult requestBeginResult; + bool readyDuringChunk; + bool readyDuringEnd; bool pollOK; bool requestOK; + bool requestCancelSync; + bool requestBeginSync; bool firing; }; @@ -280,6 +308,7 @@ struct wl_proxy * wl_proxy_marshal_flags(struct wl_proxy * proxy, CHECK(!interface); proto.selection = (struct Proxy *)va_arg(ap, struct wl_data_source *); proto.serial = va_arg(ap, unsigned int); + ++proto.selectionSetN; p = NULL; break; @@ -292,6 +321,12 @@ struct wl_proxy * wl_proxy_marshal_flags(struct wl_proxy * proxy, ++proto.receiveN; snprintf(proto.receiveMime, sizeof(proto.receiveMime), "%s", p->mime[p->mimeN - 1]); + if (proto.keepReceiveOpen) + { + CHECK(proto.receiveFd < 0); + proto.receiveFd = dup(fd); + CHECK(proto.receiveFd >= 0); + } if (proto.receiveSize) sendData(fd, proto.receiveData, proto.receiveSize); } @@ -364,6 +399,20 @@ bool waylandPollUnregister(int fd) return false; } +bool waylandPollUpdate(int fd, uint32_t events) +{ + for (unsigned int i = 0; i < rec.pollN; ++i) + { + struct Poll * poll = &rec.poll[i]; + if (!poll->active || poll->fd != fd) + continue; + + poll->events = events; + return true; + } + return false; +} + static void pollFire(unsigned int no, uint32_t events) { CHECK(no < rec.pollN); @@ -379,28 +428,28 @@ static void pollFire(unsigned int no, uint32_t events) pollCleanup(&rec.poll[i]); } -void app_clipboardNotifyTypes(const LG_ClipboardData types[], int count) +void lgClipboard_notifyTypes( + const LG_ClipboardData types[], size_t count) { CHECK(rec.noticeN < ARRAY_LENGTH(rec.notice)); - CHECK(count >= 0); - CHECK(count <= LG_CLIPBOARD_DATA_NONE); + CHECK(count <= ARRAY_LENGTH(rec.notice[0].type)); struct TypeNotice * notice = &rec.notice[rec.noticeN++]; - memcpy(notice->type, types, (size_t)count * sizeof(*types)); + memcpy(notice->type, types, count * sizeof(*types)); notice->count = count; } -void app_clipboardRelease(void) +void lgClipboard_release(void) { ++rec.releaseN; } -void app_clipboardAbort(LG_ClipboardRequest request) +void lgClipboard_abort(LG_ClipboardRequest request) { CHECK(rec.abortN < ARRAY_LENGTH(rec.abort)); rec.abort[rec.abortN++] = request; } -void app_clipboardData(LG_ClipboardRequest request, +void lgClipboard_data(LG_ClipboardRequest request, LG_ClipboardData type, const void * data, size_t size) { CHECK(rec.dataN < ARRAY_LENGTH(rec.data)); @@ -412,16 +461,93 @@ void app_clipboardData(LG_ClipboardRequest request, memcpy(notice->data, data, size); } -bool app_clipboardRequest(LG_ClipboardData type, - LG_ClipboardReplyFn replyFn, void * opaque) +LG_ClipboardResult lgClipboard_dataBegin(LG_ClipboardRequest request, + LG_ClipboardData type, uint64_t sizeHint) +{ + struct StreamNotice * stream = &rec.streamData; + ++stream->beginN; + stream->request = request; + stream->type = type; + stream->sizeHint = sizeHint; + return rec.beginResult; +} + +LG_ClipboardResult lgClipboard_dataChunk(LG_ClipboardRequest request, + uint64_t offset, const void * data, size_t size) +{ + const LG_ClipboardResult result = rec.chunkResult; + struct StreamNotice * stream = &rec.streamData; + ++stream->chunkN; + CHECK(request == stream->request); + CHECK(offset == stream->size); + CHECK(data); + CHECK(size); + CHECK(size <= sizeof(stream->data) - stream->size); + if (rec.readyDuringChunk) + { + rec.readyDuringChunk = false; + rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED; + waylandCBRequestReady(request); + } + if (result == LG_CLIPBOARD_RESULT_ACCEPTED) + { + memcpy(stream->data + stream->size, data, size); + stream->size += size; + } + return result; +} + +LG_ClipboardResult lgClipboard_dataEnd(LG_ClipboardRequest request, + uint64_t finalSize) +{ + const LG_ClipboardResult result = rec.endResult; + struct StreamNotice * stream = &rec.streamData; + ++stream->endN; + CHECK(request == stream->request); + stream->finalSize = finalSize; + if (rec.readyDuringEnd) + { + rec.readyDuringEnd = false; + rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED; + waylandCBRequestReady(request); + } + return result; +} + +bool lgClipboard_requestStream(LG_ClipboardData type, + const LG_ClipboardStreamOps * stream, void * opaque, + LG_ClipboardRequest * request) { ++rec.requestN; rec.requestType = type; - rec.reply = replyFn; - rec.replyOpaque = opaque; + rec.requestStream = stream; + rec.requestOpaque = opaque; + rec.requestId = 9000 + rec.requestN; + if (request) + *request = rec.requestId; + if (rec.requestBeginSync) + { + rec.requestBeginResult = stream->begin( + opaque, type, LG_CLIPBOARD_SIZE_UNKNOWN); + const uint8_t byte = 0xa5; + CHECK(stream->chunk(opaque, 0, &byte, 1) == + LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(stream->chunk(opaque, 1, &byte, 1) == + LG_CLIPBOARD_RESULT_BLOCKED); + pollFire(rec.pollN - 1, EPOLLOUT); + } + if (rec.requestCancelSync) + stream->cancel(opaque, LG_CLIPBOARD_CANCEL_REPLACED); return rec.requestOK; } +bool lgClipboard_requestReady(LG_ClipboardRequest request) +{ + CHECK(request == rec.requestId); + ++rec.requestReadyN; + return true; +} + static const struct wl_data_device_listener * deviceListener(void) { CHECK(proto.device.listener); @@ -458,6 +584,7 @@ static void offerMime(struct Proxy * offer, const char * mime) static void selectOffer(struct Proxy * offer) { + proto.selection = offer; deviceListener()->selection(proto.device.data, (struct wl_data_device *)&proto.device, (struct wl_data_offer *)offer); @@ -477,10 +604,14 @@ static void start(void) memset(&wlCb, 0, sizeof(wlCb)); memset(&proto, 0, sizeof(proto)); memset(&rec, 0, sizeof(rec)); + proto.receiveFd = -1; LG_LOCK_INIT(wlCb.lock); rec.pollOK = true; rec.requestOK = true; + rec.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED; + rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED; + rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED; proto.manager.kind = PROXY_MANAGER; proto.device.kind = PROXY_DEVICE; wlWm.dataDeviceManager = @@ -497,6 +628,11 @@ static void start(void) static void finish(void) { + if (proto.receiveFd >= 0) + { + CHECK(close(proto.receiveFd) == 0); + proto.receiveFd = -1; + } waylandCBFree(); CHECK(!wlCb.dataDevice); CHECK(!wlCb.offer); @@ -620,22 +756,120 @@ static void testReadEOF(void) pollFire(0, EPOLLIN); CHECK(rec.poll[0].active); - CHECK(rec.dataN == 0); + CHECK(rec.streamData.beginN == 1); + CHECK(rec.streamData.chunkN == 1); + CHECK(rec.streamData.endN == 0); pollFire(0, EPOLLIN); CHECK(!rec.poll[0].active); CHECK(rec.pollCleanupN == 1); - CHECK(rec.dataN == 1); - CHECK(rec.data[0].request == 201); - CHECK(rec.data[0].type == LG_CLIPBOARD_DATA_TEXT); - CHECK(rec.data[0].size == sizeof(data)); - CHECK(memcmp(rec.data[0].data, data, sizeof(data)) == 0); + CHECK(rec.streamData.request == 201); + CHECK(rec.streamData.type == LG_CLIPBOARD_DATA_TEXT); + CHECK(rec.streamData.sizeHint == LG_CLIPBOARD_SIZE_UNKNOWN); + CHECK(rec.streamData.size == sizeof(data)); + CHECK(rec.streamData.finalSize == sizeof(data)); + CHECK(rec.streamData.endN == 1); + CHECK(memcmp(rec.streamData.data, data, sizeof(data)) == 0); checkClosed(fd); finish(); - CHECK(rec.dataN == 1); CHECK(rec.abortN == 0); } +static void testReadBlocked(void) +{ + start(); + textOffer(); + const uint8_t data[] = "blocked stream"; + proto.receiveData = data; + proto.receiveSize = sizeof(data) - 1; + rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED; + + waylandCBRequest(202, LG_CLIPBOARD_DATA_TEXT); + CHECK(rec.pollN == 1); + pollFire(0, EPOLLIN); + CHECK(rec.streamData.beginN == 1); + CHECK(rec.streamData.chunkN == 1); + CHECK(rec.streamData.size == 0); + CHECK(rec.poll[0].events == 0); + + rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED; + waylandCBRequestReady(202); + CHECK(rec.streamData.chunkN == 2); + CHECK(rec.streamData.size == sizeof(data) - 1); + CHECK(rec.streamData.endN == 1); + CHECK(rec.streamData.finalSize == sizeof(data) - 1); + CHECK(memcmp(rec.streamData.data, data, sizeof(data) - 1) == 0); + CHECK(!rec.poll[0].active); + CHECK(rec.abortN == 0); + finish(); +} + +static void testReadBlockedEmpty(void) +{ + start(); + textOffer(); + const uint8_t data[] = "buffered while source stays open"; + proto.receiveData = data; + proto.receiveSize = sizeof(data) - 1; + proto.keepReceiveOpen = true; + rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED; + + waylandCBRequest(204, LG_CLIPBOARD_DATA_TEXT); + CHECK(rec.pollN == 1); + pollFire(0, EPOLLIN); + CHECK(rec.streamData.chunkN == 1); + CHECK(rec.streamData.size == 0); + CHECK(rec.poll[0].events == 0); + + /* The read drained the pipe, whose duplicated write end remains open. + * Ready must retry the buffered chunk without another poll callback. */ + rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED; + waylandCBRequestReady(204); + CHECK(rec.streamData.chunkN == 2); + CHECK(rec.streamData.size == sizeof(data) - 1); + CHECK(memcmp(rec.streamData.data, data, sizeof(data) - 1) == 0); + CHECK(rec.streamData.endN == 0); + CHECK(rec.poll[0].active); + CHECK(rec.poll[0].events == EPOLLIN); + + CHECK(close(proto.receiveFd) == 0); + proto.receiveFd = -1; + pollFire(0, EPOLLIN); + CHECK(rec.streamData.endN == 1); + CHECK(rec.streamData.finalSize == sizeof(data) - 1); + CHECK(!rec.poll[0].active); + CHECK(rec.abortN == 0); + finish(); +} + +static void testReadReadyRace(void) +{ + start(); + textOffer(); + const uint8_t data[] = "ready while returning blocked"; + proto.receiveData = data; + proto.receiveSize = sizeof(data) - 1; + rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED; + rec.readyDuringChunk = true; + + waylandCBRequest(203, LG_CLIPBOARD_DATA_TEXT); + CHECK(rec.pollN == 1); + pollFire(0, EPOLLIN); + CHECK(rec.streamData.beginN == 1); + CHECK(rec.streamData.chunkN == 2); + CHECK(rec.streamData.size == sizeof(data) - 1); + CHECK(rec.poll[0].events == EPOLLIN); + + rec.endResult = LG_CLIPBOARD_RESULT_BLOCKED; + rec.readyDuringEnd = true; + pollFire(0, EPOLLIN); + CHECK(rec.streamData.endN == 2); + CHECK(rec.streamData.finalSize == sizeof(data) - 1); + CHECK(!rec.poll[0].active); + CHECK(rec.abortN == 0); + finish(); +} + static void testReadError(void) { start(); @@ -666,13 +900,6 @@ static void testSource(void) { start(); waylandCBNotice(LG_CLIPBOARD_DATA_TEXT); - CHECK(rec.requestN == 1); - CHECK(rec.requestType == LG_CLIPBOARD_DATA_TEXT); - CHECK(rec.reply); - - const uint8_t data[] = "source data"; - rec.reply(rec.replyOpaque, LG_CLIPBOARD_DATA_TEXT, - data, sizeof(data) - 1); CHECK(proto.sourceN == 1); struct Proxy * source = &proto.source[0]; CHECK(proto.selection == source); @@ -698,17 +925,45 @@ static void testSource(void) (struct wl_data_source *)source, "text/plain", good[1]); CHECK(rec.pollN == 1); CHECK(rec.poll[0].fd == good[1]); + CHECK(rec.poll[0].events == 0); + CHECK(rec.requestN == 1); + CHECK(rec.requestType == LG_CLIPBOARD_DATA_TEXT); + CHECK(rec.requestStream); + CHECK(rec.requestOpaque); + + const uint8_t first[] = "source "; + const uint8_t second[] = "data"; + CHECK(rec.requestStream->begin(rec.requestOpaque, + LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_SIZE_UNKNOWN) == + LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(rec.requestStream->chunk(rec.requestOpaque, 0, + first, sizeof(first) - 1) == LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(rec.poll[0].events == EPOLLOUT); + CHECK(rec.requestStream->chunk(rec.requestOpaque, sizeof(first) - 1, + second, sizeof(second) - 1) == LG_CLIPBOARD_RESULT_BLOCKED); + pollFire(0, EPOLLOUT); + CHECK(rec.requestReadyN == 1); + CHECK(rec.poll[0].events == 0); + + CHECK(rec.requestStream->chunk(rec.requestOpaque, sizeof(first) - 1, + second, sizeof(second) - 1) == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(rec.requestStream->end(rec.requestOpaque, + sizeof(first) + sizeof(second) - 2) == LG_CLIPBOARD_RESULT_BLOCKED); + pollFire(0, EPOLLOUT); + CHECK(rec.requestReadyN == 2); + CHECK(rec.requestStream->end(rec.requestOpaque, + sizeof(first) + sizeof(second) - 2) == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(!rec.poll[0].active); sourceListener(source)->cancelled(source->data, (struct wl_data_source *)source); CHECK(source->dead); CHECK(proto.sourceDestroyN == 1); - pollFire(0, EPOLLOUT); CHECK(rec.pollCleanupN == 1); - uint8_t actual[sizeof(data)] = {}; - CHECK(read(good[0], actual, sizeof(actual)) == sizeof(data) - 1); - CHECK(memcmp(actual, data, sizeof(data) - 1) == 0); + uint8_t actual[sizeof(first) + sizeof(second)] = {}; + 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]); @@ -716,6 +971,176 @@ static void testSource(void) finish(); } +static void testSourceRelease(void) +{ + start(); + waylandCBNotice(LG_CLIPBOARD_DATA_TEXT); + struct Proxy * source = &proto.source[0]; + CHECK(wlCb.selectionSource == (struct wl_data_source *)source); + CHECK(wlCb.sources); + CHECK(proto.selection == source); + CHECK(proto.selectionSetN == 1); + + waylandCBRelease(); + CHECK(!wlCb.selectionSource); + CHECK(wlCb.sources); + CHECK(!proto.selection); + CHECK(proto.selectionSetN == 2); + + waylandCBRelease(); + CHECK(proto.selectionSetN == 2); + + sourceListener(source)->cancelled(source->data, + (struct wl_data_source *)source); + CHECK(source->dead); + CHECK(!wlCb.sources); + finish(); +} + +static void testSourceExternalOwner(void) +{ + start(); + waylandCBNotice(LG_CLIPBOARD_DATA_TEXT); + struct Proxy * source = &proto.source[0]; + CHECK(proto.selectionSetN == 1); + + struct Proxy * external = newOffer(); + offerMime(external, "image/png"); + selectOffer(external); + CHECK(!wlCb.selectionSource); + CHECK(proto.selection == external); + + waylandCBRelease(); + CHECK(proto.selectionSetN == 1); + CHECK(proto.selection == external); + + sourceListener(source)->cancelled(source->data, + (struct wl_data_source *)source); + CHECK(source->dead); + finish(); +} + +static void testSourceReplacement(void) +{ + start(); + waylandCBNotice(LG_CLIPBOARD_DATA_TEXT); + struct Proxy * first = &proto.source[0]; + waylandCBNotice(LG_CLIPBOARD_DATA_PNG); + struct Proxy * second = &proto.source[1]; + CHECK(proto.selection == second); + CHECK(proto.selectionSetN == 2); + CHECK(wlCb.selectionSource == (struct wl_data_source *)second); + + sourceListener(first)->cancelled(first->data, + (struct wl_data_source *)first); + CHECK(first->dead); + CHECK(wlCb.selectionSource == (struct wl_data_source *)second); + + waylandCBRelease(); + CHECK(!proto.selection); + CHECK(proto.selectionSetN == 3); + CHECK(!wlCb.selectionSource); + + sourceListener(second)->cancelled(second->data, + (struct wl_data_source *)second); + CHECK(second->dead); + CHECK(!wlCb.sources); + finish(); +} + +static void testSourceCancelDuringRequest(void) +{ + start(); + waylandCBNotice(LG_CLIPBOARD_DATA_TEXT); + struct Proxy * source = &proto.source[0]; + rec.requestCancelSync = true; + + int fds[2]; + CHECK(pipe(fds) == 0); + sourceListener(source)->send(source->data, + (struct wl_data_source *)source, "text/plain", fds[1]); + CHECK(rec.requestN == 1); + CHECK(rec.pollN == 1); + CHECK(!rec.poll[0].active); + CHECK(rec.pollCleanupN == 1); + checkClosed(fds[1]); + uint8_t value; + CHECK(read(fds[0], &value, 1) == 0); + CHECK(close(fds[0]) == 0); + + sourceListener(source)->cancelled(source->data, + (struct wl_data_source *)source); + finish(); +} + +static void testSourceBeginDuringRequest(void) +{ + start(); + waylandCBNotice(LG_CLIPBOARD_DATA_TEXT); + struct Proxy * source = &proto.source[0]; + rec.requestBeginSync = true; + + int fds[2]; + CHECK(pipe(fds) == 0); + sourceListener(source)->send(source->data, + (struct wl_data_source *)source, "text/plain", fds[1]); + CHECK(rec.requestN == 1); + CHECK(rec.requestBeginResult == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(rec.requestOpaque); + CHECK(rec.requestReadyN == 1); + + CHECK(rec.requestStream->end(rec.requestOpaque, 1) == + LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(rec.pollCleanupN == 1); + uint8_t value = 0; + CHECK(read(fds[0], &value, 1) == 1); + CHECK(value == 0xa5); + CHECK(read(fds[0], &value, 1) == 0); + CHECK(close(fds[0]) == 0); + checkClosed(fds[1]); + + sourceListener(source)->cancelled(source->data, + (struct wl_data_source *)source); + finish(); +} + +static void testSourceError(void) +{ + start(); + waylandCBNotice(LG_CLIPBOARD_DATA_TEXT); + struct Proxy * source = &proto.source[0]; + + int fds[2]; + CHECK(pipe(fds) == 0); + sourceListener(source)->send(source->data, + (struct wl_data_source *)source, "text/plain", fds[1]); + CHECK(rec.requestStream->begin(rec.requestOpaque, + LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_SIZE_UNKNOWN) == + LG_CLIPBOARD_RESULT_ACCEPTED); + + const uint8_t first[] = "first"; + const uint8_t second[] = "second"; + CHECK(rec.requestStream->chunk(rec.requestOpaque, 0, + first, sizeof(first) - 1) == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(rec.requestStream->chunk(rec.requestOpaque, sizeof(first) - 1, + second, sizeof(second) - 1) == LG_CLIPBOARD_RESULT_BLOCKED); + CHECK(close(fds[0]) == 0); + pollFire(0, EPOLLERR); + CHECK(rec.requestReadyN == 1); + CHECK(!rec.poll[0].active); + CHECK(rec.pollCleanupN == 1); + + CHECK(rec.requestStream->chunk(rec.requestOpaque, sizeof(first) - 1, + second, sizeof(second) - 1) == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(rec.requestStream->end(rec.requestOpaque, + sizeof(first) + sizeof(second) - 2) == LG_CLIPBOARD_RESULT_ACCEPTED); + checkClosed(fds[1]); + + sourceListener(source)->cancelled(source->data, + (struct wl_data_source *)source); + finish(); +} + static void testTeardown(void) { start(); @@ -725,17 +1150,40 @@ static void testTeardown(void) const int fd = rec.poll[0].fd; struct Proxy * dnd = rawOffer(); wlCb.dndOffer = (struct wl_data_offer *)dnd; + waylandCBNotice(LG_CLIPBOARD_DATA_TEXT); + struct Proxy * source = &proto.source[0]; finish(); CHECK(offer->dead); CHECK(dnd->dead); + CHECK(source->dead); CHECK(proto.offerDestroyN == 2); + CHECK(proto.sourceDestroyN == 1); CHECK(proto.dirtyDestroyN == 0); + CHECK(proto.selectionSetN == 2); + CHECK(!proto.selection); CHECK(rec.pollUnregisterN == 1); CHECK(rec.pollCleanupN == 1); checkClosed(fd); } +static void testTeardownExternalOwner(void) +{ + start(); + waylandCBNotice(LG_CLIPBOARD_DATA_TEXT); + struct Proxy * source = &proto.source[0]; + struct Proxy * external = newOffer(); + offerMime(external, "text/plain"); + selectOffer(external); + CHECK(!wlCb.selectionSource); + + finish(); + CHECK(source->dead); + CHECK(external->dead); + CHECK(proto.selectionSetN == 1); + CHECK(proto.selection == external); +} + static void testDnd(void) { start(); @@ -783,9 +1231,19 @@ static const struct Test tests[] = { "mime" , testMime }, { "replace" , testReplace }, { "read-eof" , testReadEOF }, + { "read-block", testReadBlocked }, + { "read-empty", testReadBlockedEmpty }, + { "read-ready", testReadReadyRace }, { "read-error", testReadError }, { "source" , testSource }, + { "source-release", testSourceRelease }, + { "source-external", testSourceExternalOwner }, + { "source-replace", testSourceReplacement }, + { "source-cancel", testSourceCancelDuringRequest }, + { "source-early", testSourceBeginDuringRequest }, + { "source-error", testSourceError }, { "teardown" , testTeardown }, + { "teardown-external", testTeardownExternalOwner }, { "self-copy" , testSelfCopy }, { "dnd" , testDnd }, }; diff --git a/client/tests/x11_clipboard_test.c b/client/tests/x11_clipboard_test.c index 78e41d05..52f9a043 100644 --- a/client/tests/x11_clipboard_test.c +++ b/client/tests/x11_clipboard_test.c @@ -22,6 +22,7 @@ #include "clipboard.h" #include "test.h" #include "x11.h" +#include "../src/clipboard.h" #include "common/debug.h" @@ -29,6 +30,7 @@ #include #include #include +#include #include #include @@ -42,7 +44,7 @@ #define MAX_NOTICE 8U #define MAX_PROP 16U #define MAX_SEND 8U -#define MAX_TRANSFER 8192U +#define MAX_TRANSFER (128U * 1024U) #define MAX_WINDOW 16U #define A_CLIPBOARD 10UL @@ -95,7 +97,7 @@ struct Property struct TypeNotice { LG_ClipboardData type[LG_CLIPBOARD_DATA_NONE]; - int count; + size_t count; }; struct DataNotice @@ -127,6 +129,7 @@ struct Log unsigned int abortN; unsigned int releaseN; unsigned int requestN; + unsigned int requestReadyN; unsigned int destroyN; unsigned int freeN; unsigned int flushN; @@ -134,11 +137,27 @@ struct Log unsigned int ungrabN; unsigned int selectN; LG_ClipboardData requestType; - LG_ClipboardReplyFn reply; - void * replyOpaque; + const LG_ClipboardStreamOps * requestStream; + void * requestOpaque; + LG_ClipboardRequest requestId; + LG_ClipboardRequest streamRequest; + LG_ClipboardData streamType; + uint64_t streamSizeHint; + unsigned int streamBeginN; + unsigned int streamChunkN; + unsigned int streamEndN; + size_t streamMaxChunk; + LG_ClipboardResult beginResult; + LG_ClipboardResult chunkResult; + LG_ClipboardResult endResult; + LG_ClipboardResult requestBeginResult; + bool readyDuringChunk; + bool readyDuringEnd; Window owner; Window nextWindow; bool requestOK; + bool requestCancelSync; + bool requestBeginSync; bool fixesOK; }; @@ -147,6 +166,12 @@ struct X11DSAtoms x11atoms; static struct Log rec; +static pthread_mutex_t readRaceLock = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t readRaceCond = PTHREAD_COND_INITIALIZER; +static bool readRacePause; +static bool readRaceEntered; +static bool readRaceResume; + static size_t itemSize(int format, unsigned long count) { switch (format) @@ -253,10 +278,7 @@ int XSelectInput(Display * display, Window window, long mask) { CHECK(display == x11.display); CHECK(mask == PropertyChangeMask); - bool found = false; - for (unsigned int i = 0; i < rec.windowN; ++i) - found |= rec.window[i].window == window && !rec.window[i].dead; - CHECK(found); + CHECK(window != None); return Success; } @@ -339,26 +361,44 @@ int XGetWindowProperty(Display * display, Window window, Atom property, CHECK(display == x11.display); CHECK(window != None); CHECK(property != None); - CHECK(offset == 0); - CHECK(length == ~0L); + CHECK(offset >= 0); + CHECK(length == ~0L || length == (64U * 1024U + 3U) / 4U); CHECK(delete); CHECK(requestedType == AnyPropertyType); CHECK(rec.propPos < rec.propN); - const struct Property * prop = &rec.prop[rec.propPos++]; + const struct Property * prop = &rec.prop[rec.propPos]; *actualType = prop->type; *actualFormat = prop->format; - *count = prop->count; - *after = prop->after; + size_t start = 0; + size_t size = prop->size; + if (prop->format == 8 && length != ~0L) + { + start = (size_t)offset * 4U; + CHECK(start <= prop->size); + const size_t maximum = (size_t)length * 4U; + size = prop->size - start; + if (size > maximum) + size = maximum; + *count = size; + *after = prop->size - start - size; + } + else + { + *count = prop->count; + *after = prop->after; + } if (prop->null) *data = NULL; else { - CHECK(prop->size <= sizeof(prop->data)); - *data = malloc(prop->size ? prop->size : 1); + CHECK(start + size <= sizeof(prop->data)); + *data = malloc(size ? size : 1); CHECK(*data); - if (prop->size) - memcpy(*data, prop->data, prop->size); + if (size) + memcpy(*data, prop->data + start, size); } + if (*after == 0) + ++rec.propPos; return prop->status; } @@ -370,17 +410,17 @@ int XFree(void * data) return Success; } -void app_clipboardNotifyTypes(const LG_ClipboardData types[], int count) +void lgClipboard_notifyTypes( + const LG_ClipboardData types[], size_t count) { CHECK(rec.noticeN < ARRAY_LENGTH(rec.notice)); - CHECK(count >= 0); - CHECK(count <= LG_CLIPBOARD_DATA_NONE); + CHECK(count <= ARRAY_LENGTH(rec.notice[0].type)); struct TypeNotice * notice = &rec.notice[rec.noticeN++]; notice->count = count; - memcpy(notice->type, types, (size_t)count * sizeof(*types)); + memcpy(notice->type, types, count * sizeof(*types)); } -void app_clipboardData(LG_ClipboardRequest request, +void lgClipboard_data(LG_ClipboardRequest request, LG_ClipboardData type, const void * data, size_t size) { CHECK(rec.dataN < ARRAY_LENGTH(rec.data)); @@ -394,27 +434,121 @@ void app_clipboardData(LG_ClipboardRequest request, memcpy(notice->data, data, size); } -void app_clipboardAbort(LG_ClipboardRequest request) +LG_ClipboardResult lgClipboard_dataBegin(LG_ClipboardRequest request, + LG_ClipboardData type, uint64_t sizeHint) +{ + ++rec.streamBeginN; + rec.streamRequest = request; + rec.streamType = type; + rec.streamSizeHint = sizeHint; + if (rec.beginResult == LG_CLIPBOARD_RESULT_ACCEPTED) + { + CHECK(rec.dataN < ARRAY_LENGTH(rec.data)); + rec.data[rec.dataN] = (struct DataNotice) + { + .request = request, + .type = type, + }; + } + return rec.beginResult; +} + +LG_ClipboardResult lgClipboard_dataChunk(LG_ClipboardRequest request, + uint64_t offset, const void * data, size_t size) +{ + const LG_ClipboardResult result = rec.chunkResult; + ++rec.streamChunkN; + CHECK(request == rec.streamRequest); + CHECK(rec.dataN < ARRAY_LENGTH(rec.data)); + struct DataNotice * notice = &rec.data[rec.dataN]; + CHECK(offset == notice->size); + CHECK(data); + CHECK(size); + CHECK(size <= sizeof(notice->data) - notice->size); + if (size > rec.streamMaxChunk) + rec.streamMaxChunk = size; + pthread_mutex_lock(&readRaceLock); + if (readRacePause) + { + readRacePause = false; + readRaceEntered = true; + pthread_cond_signal(&readRaceCond); + while (!readRaceResume) + pthread_cond_wait(&readRaceCond, &readRaceLock); + } + pthread_mutex_unlock(&readRaceLock); + if (rec.readyDuringChunk) + { + rec.readyDuringChunk = false; + rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED; + x11CBRequestReady(request); + } + if (result == LG_CLIPBOARD_RESULT_ACCEPTED) + { + memcpy(notice->data + notice->size, data, size); + notice->size += size; + } + return result; +} + +LG_ClipboardResult lgClipboard_dataEnd(LG_ClipboardRequest request, + uint64_t finalSize) +{ + const LG_ClipboardResult result = rec.endResult; + ++rec.streamEndN; + CHECK(request == rec.streamRequest); + CHECK(rec.dataN < ARRAY_LENGTH(rec.data)); + if (rec.readyDuringEnd) + { + rec.readyDuringEnd = false; + rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED; + x11CBRequestReady(request); + } + if (result == LG_CLIPBOARD_RESULT_ACCEPTED) + { + CHECK(finalSize == rec.data[rec.dataN].size); + ++rec.dataN; + } + return result; +} + +void lgClipboard_abort(LG_ClipboardRequest request) { CHECK(rec.abortN < ARRAY_LENGTH(rec.abort)); rec.abort[rec.abortN++] = request; } -void app_clipboardRelease(void) +void lgClipboard_release(void) { ++rec.releaseN; } -bool app_clipboardRequest(LG_ClipboardData type, - LG_ClipboardReplyFn replyFn, void * opaque) +bool lgClipboard_requestStream(LG_ClipboardData type, + const LG_ClipboardStreamOps * stream, void * opaque, + LG_ClipboardRequest * request) { ++rec.requestN; rec.requestType = type; - rec.reply = replyFn; - rec.replyOpaque = opaque; + rec.requestStream = stream; + rec.requestOpaque = opaque; + rec.requestId = 9000 + rec.requestN; + if (request) + *request = rec.requestId; + if (rec.requestBeginSync) + rec.requestBeginResult = stream->begin( + opaque, type, LG_CLIPBOARD_SIZE_UNKNOWN); + if (rec.requestCancelSync) + stream->cancel(opaque, LG_CLIPBOARD_CANCEL_REPLACED); return rec.requestOK; } +bool lgClipboard_requestReady(LG_ClipboardRequest request) +{ + CHECK(request == rec.requestId); + ++rec.requestReadyN; + return true; +} + static struct Property * prop(void) { CHECK(rec.propN < ARRAY_LENGTH(rec.prop)); @@ -469,17 +603,22 @@ static void selection(Window requestor, Atom target, Atom property) CHECK(x11CBEventThread(&event)); } -static void property(Window window) +static void propertyState(Window window, Atom atom, int state) { XEvent event = {}; event.xproperty.type = PropertyNotify; event.xproperty.display = x11.display; event.xproperty.window = window; - event.xproperty.atom = x11atoms.SEL_DATA; - event.xproperty.state = PropertyNewValue; + event.xproperty.atom = atom; + event.xproperty.state = state; CHECK(x11CBEventThread(&event)); } +static void property(Window window) +{ + propertyState(window, x11atoms.SEL_DATA, PropertyNewValue); +} + static void owner(Window owner) { rec.owner = owner; @@ -540,6 +679,15 @@ static void start(void) rec.nextWindow = 1000; rec.requestOK = true; rec.fixesOK = true; + rec.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED; + rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED; + rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED; + + pthread_mutex_lock(&readRaceLock); + readRacePause = false; + readRaceEntered = false; + readRaceResume = false; + pthread_mutex_unlock(&readRaceLock); x11.display = (Display *)(uintptr_t)1; x11.window = 50; @@ -632,6 +780,166 @@ static void testIncr(void) finish(); } +static void testIncrBlocked(void) +{ + start(); + const unsigned long targets[] = { A_TEXT }; + discover(620, targets, ARRAY_LENGTH(targets)); + const Window window = request(211, LG_CLIPBOARD_DATA_TEXT); + const unsigned long capacity = 9; + rec.beginResult = LG_CLIPBOARD_RESULT_BLOCKED; + prop32(x11atoms.INCR, &capacity, 1); + selection(window, A_TEXT, x11atoms.SEL_DATA); + CHECK(rec.streamBeginN == 1); + CHECK(rec.dataN == 0); + + const uint8_t first[] = "abc"; + prop8(A_TEXT, first, sizeof(first) - 1); + property(window); + CHECK(rec.streamChunkN == 0); + + rec.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED; + rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED; + x11CBRequestReady(211); + CHECK(rec.streamBeginN == 2); + CHECK(rec.streamSizeHint == capacity); + CHECK(rec.streamChunkN == 1); + CHECK(rec.data[0].size == 0); + + const uint8_t second[] = "defghi"; + prop8(A_TEXT, second, sizeof(second) - 1); + property(window); + CHECK(rec.streamChunkN == 1); + + rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED; + x11CBRequestReady(211); + CHECK(rec.streamChunkN == 3); + CHECK(rec.data[0].size == 9); + CHECK(memcmp(rec.data[0].data, "abcdefghi", 9) == 0); + + rec.endResult = LG_CLIPBOARD_RESULT_BLOCKED; + propNull(A_TEXT, 8, 0); + property(window); + CHECK(rec.streamEndN == 1); + CHECK(rec.dataN == 0); + rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED; + x11CBRequestReady(211); + CHECK(rec.streamEndN == 2); + CHECK(rec.dataN == 1); + CHECK(rec.abortN == 0); + finish(); +} + +static void testIncrReadyRace(void) +{ + start(); + const unsigned long targets[] = { A_TEXT }; + discover(622, targets, ARRAY_LENGTH(targets)); + const Window window = request(213, LG_CLIPBOARD_DATA_TEXT); + const unsigned long capacity = 4; + prop32(x11atoms.INCR, &capacity, 1); + selection(window, A_TEXT, x11atoms.SEL_DATA); + + const uint8_t data[] = "race"; + rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED; + rec.readyDuringChunk = true; + prop8(A_TEXT, data, sizeof(data) - 1); + property(window); + CHECK(rec.streamChunkN == 2); + CHECK(rec.data[0].size == sizeof(data) - 1); + + rec.endResult = LG_CLIPBOARD_RESULT_BLOCKED; + rec.readyDuringEnd = true; + propNull(A_TEXT, 8, 0); + property(window); + CHECK(rec.streamEndN == 2); + CHECK(rec.dataN == 1); + CHECK(rec.abortN == 0); + finish(); +} + +static void * requestReadyThread(void * opaque) +{ + x11CBRequestReady(*(const LG_ClipboardRequest *)opaque); + return NULL; +} + +static void testIncrCancelRace(void) +{ + start(); + const unsigned long targets[] = { A_TEXT }; + discover(623, targets, ARRAY_LENGTH(targets)); + const LG_ClipboardRequest id = 214; + const Window window = request(id, LG_CLIPBOARD_DATA_TEXT); + const unsigned long capacity = 6; + prop32(x11atoms.INCR, &capacity, 1); + selection(window, A_TEXT, x11atoms.SEL_DATA); + + const uint8_t data[] = "cancel"; + rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED; + prop8(A_TEXT, data, sizeof(data) - 1); + property(window); + CHECK(rec.streamChunkN == 1); + + rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED; + pthread_mutex_lock(&readRaceLock); + readRacePause = true; + pthread_mutex_unlock(&readRaceLock); + + pthread_t readyThread; + CHECK(pthread_create(&readyThread, NULL, requestReadyThread, + (void *)&id) == 0); + + pthread_mutex_lock(&readRaceLock); + while (!readRaceEntered) + pthread_cond_wait(&readRaceCond, &readRaceLock); + pthread_mutex_unlock(&readRaceLock); + + /* The callback is outside x11cb.lock and still consuming its chunk. */ + x11CBRequestCancel(id, LG_CLIPBOARD_CANCEL_REPLACED); + + pthread_mutex_lock(&readRaceLock); + readRaceResume = true; + pthread_cond_signal(&readRaceCond); + pthread_mutex_unlock(&readRaceLock); + + CHECK(pthread_join(readyThread, NULL) == 0); + CHECK(rec.streamChunkN == 2); + CHECK(rec.data[0].size == sizeof(data) - 1); + CHECK(memcmp(rec.data[0].data, data, sizeof(data) - 1) == 0); + CHECK(rec.abortN == 0); + finish(); +} + +static void testIncrLargeProperty(void) +{ + start(); + const unsigned long targets[] = { A_TEXT }; + discover(621, targets, ARRAY_LENGTH(targets)); + const Window window = request(212, LG_CLIPBOARD_DATA_TEXT); + const unsigned long capacity = 70U * 1024U; + prop32(x11atoms.INCR, &capacity, 1); + selection(window, A_TEXT, x11atoms.SEL_DATA); + + const size_t size = 70U * 1024U; + uint8_t * data = malloc(size); + CHECK(data); + for (size_t i = 0; i < size; ++i) + data[i] = (uint8_t)i; + prop8(A_TEXT, data, size); + property(window); + propNull(A_TEXT, 8, 0); + property(window); + + CHECK(rec.streamChunkN == 2); + CHECK(rec.streamMaxChunk == 64U * 1024U); + CHECK(rec.dataN == 1); + CHECK(rec.data[0].size == size); + CHECK(memcmp(rec.data[0].data, data, size) == 0); + free(data); + finish(); +} + static void testMalformed(void) { start(); @@ -717,36 +1025,88 @@ static void testSource(void) selectionRequest(A_PNG, 701); CHECK(rec.requestN == 1); CHECK(rec.requestType == LG_CLIPBOARD_DATA_PNG); - CHECK(rec.reply); + CHECK(rec.requestStream); CHECK(rec.sendN == 1); const uint8_t data[] = { 1, 2, 3, 4 }; - rec.reply(rec.replyOpaque, LG_CLIPBOARD_DATA_PNG, - data, ARRAY_LENGTH(data)); + CHECK(rec.requestStream->begin(rec.requestOpaque, + LG_CLIPBOARD_DATA_PNG, LG_CLIPBOARD_SIZE_UNKNOWN) == + LG_CLIPBOARD_RESULT_ACCEPTED); CHECK(rec.changeN == 2); CHECK(rec.change[1].property == 701); - CHECK(rec.change[1].type == A_PNG); - CHECK(rec.change[1].format == 8); - CHECK(rec.change[1].count == 4); - CHECK(memcmp(rec.change[1].data, data, sizeof(data)) == 0); + CHECK(rec.change[1].type == x11atoms.INCR); + CHECK(rec.change[1].format == 32); + CHECK(rec.change[1].count == 1); CHECK(rec.sendN == 2); CHECK(rec.send[1].xselection.property == 701); - selectionRequest(A_TEXT, 702); - CHECK(rec.requestN == 1); + CHECK(rec.requestStream->chunk(rec.requestOpaque, 0, + data, ARRAY_LENGTH(data)) == LG_CLIPBOARD_RESULT_BLOCKED); + propertyState(900, 701, PropertyDelete); + CHECK(rec.requestReadyN == 1); + CHECK(rec.requestStream->chunk(rec.requestOpaque, 0, + data, ARRAY_LENGTH(data)) == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(rec.changeN == 3); + CHECK(rec.change[2].property == 701); + CHECK(rec.change[2].type == A_PNG); + CHECK(rec.change[2].format == 8); + CHECK(rec.change[2].count == 4); + CHECK(memcmp(rec.change[2].data, data, sizeof(data)) == 0); + CHECK(rec.requestStream->end(rec.requestOpaque, ARRAY_LENGTH(data)) == + LG_CLIPBOARD_RESULT_BLOCKED); + propertyState(900, 701, PropertyDelete); + CHECK(rec.requestReadyN == 2); + CHECK(rec.requestStream->end(rec.requestOpaque, ARRAY_LENGTH(data)) == + LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(rec.changeN == 4); + CHECK(rec.change[3].property == 701); + CHECK(rec.change[3].type == A_PNG); + CHECK(rec.change[3].format == 8); + CHECK(rec.change[3].count == 0); + + rec.requestCancelSync = true; + selectionRequest(A_PNG, 704); + CHECK(rec.requestN == 2); CHECK(rec.sendN == 3); CHECK(rec.send[2].xselection.property == None); + rec.requestCancelSync = false; - rec.requestOK = false; - selectionRequest(A_PNG, 703); + selectionRequest(A_TEXT, 702); CHECK(rec.requestN == 2); CHECK(rec.sendN == 4); CHECK(rec.send[3].xselection.property == None); + rec.requestOK = false; + selectionRequest(A_PNG, 703); + CHECK(rec.requestN == 3); + CHECK(rec.sendN == 5); + CHECK(rec.send[4].xselection.property == None); + x11CBRelease(); CHECK(rec.owner == None); finish(); } +static void testSourceBeginDuringRequest(void) +{ + start(); + x11CBNotice(LG_CLIPBOARD_DATA_PNG); + rec.requestBeginSync = true; + selectionRequest(A_PNG, 705); + CHECK(rec.requestN == 1); + CHECK(rec.requestBeginResult == LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(rec.requestOpaque); + CHECK(rec.changeN == 1); + CHECK(rec.change[0].type == x11atoms.INCR); + CHECK(rec.sendN == 1); + + propertyState(900, 705, PropertyDelete); + CHECK(rec.requestStream->end(rec.requestOpaque, 0) == + LG_CLIPBOARD_RESULT_ACCEPTED); + CHECK(rec.changeN == 2); + CHECK(rec.change[1].count == 0); + finish(); +} + static void testTeardown(void) { start(); @@ -789,9 +1149,14 @@ static const struct Test tests[] = { "targets" , testTargets }, { "normal" , testNormal }, { "incr" , testIncr }, + { "incr-block", testIncrBlocked }, + { "incr-ready", testIncrReadyRace }, + { "incr-cancel", testIncrCancelRace }, + { "incr-large", testIncrLargeProperty }, { "malformed", testMalformed }, { "replace" , testReplace }, { "source" , testSource }, + { "source-early", testSourceBeginDuringRequest }, { "teardown" , testTeardown }, }; diff --git a/client/transports/LGMP/CMakeLists.txt b/client/transports/LGMP/CMakeLists.txt index 0ce8cb95..a0164bd6 100644 --- a/client/transports/LGMP/CMakeLists.txt +++ b/client/transports/LGMP/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.10) project(transport_LGMP LANGUAGES C) add_library(transport_LGMP STATIC + clipboard.c input.c lgmp.c ) diff --git a/client/transports/LGMP/clipboard.c b/client/transports/LGMP/clipboard.c new file mode 100644 index 00000000..9bc19867 --- /dev/null +++ b/client/transports/LGMP/clipboard.c @@ -0,0 +1,1679 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "clipboard.h" + +#include "common/KVMFRClipboard.h" +#include "common/LGMPConfig.h" +#include "common/debug.h" +#include "common/event.h" +#include "common/locking.h" +#include "common/thread.h" +#include "common/time.h" + +#include +#include +#include +#include + +#define CLIPBOARD_PENDING_MAX 32U +#define CLIPBOARD_GRANTS KVMFR_CLIPBOARD_SLOT_COUNT +#define CLIPBOARD_POLL_MS 10U +#define CLIPBOARD_RETRY_MS 1U +#define CLIPBOARD_KEEPALIVE_US UINT64_C(200000) +#define CLIPBOARD_RELEASE_TIMEOUT_US UINT64_C(100000) + +struct PendingRecord +{ + KVMFRClipboardMessage record; +}; + +struct Grant +{ + KVMFRClipboardSlotHeader * header; + uint8_t * data; + uint32_t token; + bool available; +}; + +enum HeldPhase +{ + HELD_PHASE_NONE, + HELD_PHASE_BEGIN, + HELD_PHASE_CHUNK, + HELD_PHASE_END, +}; + +struct LGMPClipboard +{ + PLGMPClient client; + PLGMPClientQueue queue; + LG_Lock lock; + LG_Lock eventLock; + LG_Lock statusLock; + LGEvent * event; + LGThread * thread; + atomic_bool stop; + + bool connected; + bool available; + bool statusValid; + bool claimed; + bool ownerConfirmed; + uint32_t clientID; + uint32_t endpointGeneration; + uint32_t providerGeneration; + uint32_t claimGeneration; + uint32_t publishedClaimGeneration; + uint32_t statusSerial; + uint64_t lastSend; + + struct PendingRecord pending[CLIPBOARD_PENDING_MAX]; + unsigned pendingHead; + unsigned pendingCount; + + struct Grant grants[CLIPBOARD_GRANTS]; + bool writeBlocked; + LG_ClipboardRequest writeBlockedRequest; + LG_ClipboardRequest writeTransfer; + uint64_t writeClipboardGeneration; + uint64_t writeSizeHint; + KVMFRClipboardFormat writeFormat; + uint64_t writeOffset; + uint32_t writeSequence; + bool writeBegan; + bool writeWireBegan; + + bool held; + bool heldReady; + enum HeldPhase heldPhase; + LGMPMessage heldMessage; + KVMFRClipboardMessage heldRecord; + + uint64_t localClipboardGeneration; + KVMFRClipboardFormatFlags localFormats; + uint64_t remoteClipboardGeneration; + KVMFRClipboardFormatFlags remoteFormats; + LG_ClipboardRequest readRequest; + uint64_t readTransfer; + uint64_t readClipboardGeneration; + uint64_t readSizeHint; + uint64_t readOffset; + KVMFRClipboardFormat readFormat; + uint32_t readSequence; + bool readBegan; + uint64_t transferSerial; + + const LG_ClipboardEventOps * events; + void * eventOpaque; + LG_ClipboardStatusFn statusCallback; + void * statusOpaque; +}; + +static LG_ClipboardData fromWireFormat(KVMFRClipboardFormat format) +{ + switch (format) + { + case KVMFR_CLIPBOARD_FORMAT_TEXT: return LG_CLIPBOARD_DATA_TEXT; + case KVMFR_CLIPBOARD_FORMAT_PNG : return LG_CLIPBOARD_DATA_PNG; + case KVMFR_CLIPBOARD_FORMAT_BMP : return LG_CLIPBOARD_DATA_BMP; + case KVMFR_CLIPBOARD_FORMAT_TIFF: return LG_CLIPBOARD_DATA_TIFF; + case KVMFR_CLIPBOARD_FORMAT_JPEG: return LG_CLIPBOARD_DATA_JPEG; + default: return LG_CLIPBOARD_DATA_NONE; + } +} + +static KVMFRClipboardFormat toWireFormat(LG_ClipboardData format) +{ + switch (format) + { + case LG_CLIPBOARD_DATA_TEXT: return KVMFR_CLIPBOARD_FORMAT_TEXT; + case LG_CLIPBOARD_DATA_PNG : return KVMFR_CLIPBOARD_FORMAT_PNG; + case LG_CLIPBOARD_DATA_BMP : return KVMFR_CLIPBOARD_FORMAT_BMP; + case LG_CLIPBOARD_DATA_TIFF: return KVMFR_CLIPBOARD_FORMAT_TIFF; + case LG_CLIPBOARD_DATA_JPEG: return KVMFR_CLIPBOARD_FORMAT_JPEG; + default: return KVMFR_CLIPBOARD_FORMAT_NONE; + } +} + +static LG_ClipboardCancelReason fromWireCancel(uint32_t reason) +{ + return reason <= LG_CLIPBOARD_CANCEL_INVALID ? + (LG_ClipboardCancelReason)reason : LG_CLIPBOARD_CANCEL_INVALID; +} + +static KVMFRClipboardFormatFlags formatMask( + const LG_ClipboardData formats[], size_t count) +{ + KVMFRClipboardFormatFlags result = 0; + for (size_t i = 0; i < count; ++i) + result |= kvmfrClipboardFormatFlag(toWireFormat(formats[i])); + return result; +} + +static unsigned formatsFromMask(KVMFRClipboardFormatFlags mask, + LG_ClipboardData formats[LG_CLIPBOARD_DATA_NONE]) +{ + unsigned count = 0; + for (KVMFRClipboardFormat format = KVMFR_CLIPBOARD_FORMAT_TEXT; + format <= KVMFR_CLIPBOARD_FORMAT_JPEG; ++format) + if (mask & kvmfrClipboardFormatFlag(format)) + formats[count++] = fromWireFormat(format); + return count; +} + +static uint32_t nextNonzero(uint32_t * value) +{ + if (++*value == 0) + ++*value; + return *value; +} + +static uint64_t nextClientTransfer(LGMPClipboard * clipboard) +{ + if (++clipboard->transferSerial == 0 || + kvmfrClipboardTransferFromHelper(clipboard->transferSerial)) + clipboard->transferSerial = 1; + return clipboard->transferSerial; +} + +static struct PendingRecord * pendingAt( + LGMPClipboard * clipboard, unsigned position) +{ + return &clipboard->pending[ + (clipboard->pendingHead + position) % CLIPBOARD_PENDING_MAX]; +} + +static void signalWorker(LGMPClipboard * clipboard) +{ + if (clipboard->event) + lgSignalEvent(clipboard->event); +} + +static bool enqueueRecordNL(LGMPClipboard * clipboard, + KVMFRClipboardMessage record) +{ + if (!clipboard->connected || !clipboard->queue || + clipboard->pendingCount == CLIPBOARD_PENDING_MAX) + return false; + + record.version = KVMFR_CLIPBOARD_VERSION; + record.generation = clipboard->claimGeneration; + pendingAt(clipboard, clipboard->pendingCount++)->record = record; + return true; +} + +static bool enqueueTypeNL(LGMPClipboard * clipboard, + KVMFRClipboardMessageType type) +{ + KVMFRClipboardMessage record = { 0 }; + record.type = type; + return enqueueRecordNL(clipboard, record); +} + +static struct Grant * availableGrantNL(LGMPClipboard * clipboard) +{ + for (unsigned i = 0; i < CLIPBOARD_GRANTS; ++i) + if (clipboard->grants[i].available) + return &clipboard->grants[i]; + return NULL; +} + +static bool enqueueDataNL(LGMPClipboard * clipboard, + KVMFRClipboardMessage record, const void * data) +{ + if (!clipboard->connected || !clipboard->queue || + clipboard->pendingCount == CLIPBOARD_PENDING_MAX || + record.length > KVMFR_CLIPBOARD_DATA_BYTES || + (record.length && !data)) + return false; + + struct Grant * grant = availableGrantNL(clipboard); + if (!grant) + return false; + + record.version = KVMFR_CLIPBOARD_VERSION; + record.generation = clipboard->claimGeneration; + record.token = 0; + memcpy(grant->header, &record, sizeof(record)); + if (record.length) + memcpy(grant->data, data, record.length); + + KVMFRClipboardMessage commit = record; + commit.type = KVMFR_CLIPBOARD_MESSAGE_COMMIT; + commit.token = grant->token; + pendingAt(clipboard, clipboard->pendingCount++)->record = commit; + grant->available = false; + return true; +} + +static void clearWriteNL(LGMPClipboard * clipboard) +{ + clipboard->writeBlocked = false; + clipboard->writeBlockedRequest = LG_CLIPBOARD_REQUEST_INVALID; + clipboard->writeTransfer = LG_CLIPBOARD_REQUEST_INVALID; + clipboard->writeClipboardGeneration = 0; + clipboard->writeSizeHint = 0; + clipboard->writeFormat = KVMFR_CLIPBOARD_FORMAT_NONE; + clipboard->writeOffset = 0; + clipboard->writeSequence = 0; + clipboard->writeBegan = false; + clipboard->writeWireBegan = false; +} + +static void clearReadNL(LGMPClipboard * clipboard) +{ + clipboard->readRequest = LG_CLIPBOARD_REQUEST_INVALID; + clipboard->readTransfer = 0; + clipboard->readClipboardGeneration = 0; + clipboard->readSizeHint = 0; + clipboard->readOffset = 0; + clipboard->readFormat = KVMFR_CLIPBOARD_FORMAT_NONE; + clipboard->readSequence = 0; + clipboard->readBegan = false; +} + +static void clearProtocolNL(LGMPClipboard * clipboard) +{ + clipboard->claimed = false; + clipboard->ownerConfirmed = false; + clipboard->publishedClaimGeneration = 0; + clipboard->pendingHead = 0; + clipboard->pendingCount = 0; + clipboard->remoteClipboardGeneration = 0; + clipboard->remoteFormats = 0; + clearWriteNL(clipboard); + clearReadNL(clipboard); + memset(clipboard->grants, 0, sizeof(clipboard->grants)); +} + +static bool ensureClaimNL(LGMPClipboard * clipboard) +{ + if (clipboard->claimed) + return true; + if (!clipboard->connected || !clipboard->available) + return false; + + KVMFRClipboardMessage claim = { 0 }; + nextNonzero(&clipboard->claimGeneration); + claim.type = KVMFR_CLIPBOARD_MESSAGE_CLAIM; + claim.token = clipboard->endpointGeneration; + if (!enqueueRecordNL(clipboard, claim)) + return false; + clipboard->claimed = true; + clipboard->ownerConfirmed = false; + return true; +} + +static bool restoreClipboardNL(LGMPClipboard * clipboard) +{ + if (!clipboard->events || !ensureClaimNL(clipboard)) + return false; + if (!clipboard->localClipboardGeneration) + return true; + + KVMFRClipboardMessage record = { 0 }; + record.type = clipboard->localFormats ? + KVMFR_CLIPBOARD_MESSAGE_OFFER : KVMFR_CLIPBOARD_MESSAGE_CLEAR; + record.clipboardGeneration = clipboard->localClipboardGeneration; + record.token = clipboard->localFormats; + return enqueueRecordNL(clipboard, record); +} + +static void notifyStatus(LGMPClipboard * clipboard) +{ + LG_ClipboardStatusFn callback; + void * opaque; + LG_ClipboardStatus status; + + LG_LOCK(clipboard->statusLock); + LG_LOCK(clipboard->lock); + callback = clipboard->statusCallback; + opaque = clipboard->statusOpaque; + status = (LG_ClipboardStatus) + { + .available = clipboard->connected && clipboard->available, + .generation = clipboard->providerGeneration, + }; + LG_UNLOCK(clipboard->lock); + if (callback) + callback(opaque, &status); + LG_UNLOCK(clipboard->statusLock); +} + +static void connectionFailed(LGMPClipboard * clipboard, LGMP_STATUS status) +{ + const bool changed = clipboard->connected || clipboard->available; + if (clipboard->connected) + DEBUG_WARN("LGMP clipboard transport failed: %s", + lgmpStatusString(status)); + clipboard->connected = false; + clipboard->available = false; + clipboard->statusValid = false; + clearProtocolNL(clipboard); + clipboard->held = false; + clipboard->heldReady = false; + if (changed) + nextNonzero(&clipboard->providerGeneration); + atomic_store_explicit(&clipboard->stop, true, memory_order_release); +} + +static bool validStatus(const KVMFRClipboardStatus * status) +{ + const uint32_t validFlags = KVMFR_CLIPBOARD_STATUS_AVAILABLE | + KVMFR_CLIPBOARD_STATUS_HAS_OWNER; + if (status->version != KVMFR_CLIPBOARD_VERSION || + status->flags & ~validFlags || !status->generation || + status->formats & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL || + !status->lease || + status->slotBytes != KVMFR_CLIPBOARD_DATA_BYTES) + return false; + + const bool available = + (status->flags & KVMFR_CLIPBOARD_STATUS_AVAILABLE) != 0; + const bool owner = + (status->flags & KVMFR_CLIPBOARD_STATUS_HAS_OWNER) != 0; + if (!available && (status->formats || owner)) + return false; + return owner ? status->ownerClientID && status->ownerGeneration : + !status->ownerClientID && !status->ownerGeneration; +} + +static bool validateRecord(const LGMPClipboard * clipboard, + const KVMFRClipboardMessage * record, size_t size, + KVMFRClipboardQueueType queueType) +{ + if (size < sizeof(*record) || + record->version != KVMFR_CLIPBOARD_VERSION || + record->type < KVMFR_CLIPBOARD_MESSAGE_OFFER || + record->type > KVMFR_CLIPBOARD_MESSAGE_ACK || + record->generation != clipboard->claimGeneration || + record->length > KVMFR_CLIPBOARD_DATA_BYTES || + record->offset > UINT64_MAX - record->length) + return false; + + if (record->type == KVMFR_CLIPBOARD_MESSAGE_DATA) + { + const unsigned operations = + !!(record->flags & KVMFR_CLIPBOARD_FLAG_BEGIN) + + !!record->length + !!(record->flags & KVMFR_CLIPBOARD_FLAG_END); + if (!record->transfer || + kvmfrClipboardTransferFromHelper(record->transfer) || + !kvmfrClipboardFormatValid(record->format) || + record->flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN | + KVMFR_CLIPBOARD_FLAG_END) || + !operations || + size != sizeof(*record) + KVMFR_CLIPBOARD_DATA_BYTES || + queueType != KVMFR_CLIPBOARD_QUEUE_DATA) + return false; + } + else if (queueType != KVMFR_CLIPBOARD_QUEUE_MESSAGE || + size != sizeof(*record) || record->length || record->flags) + return false; + + switch (record->type) + { + case KVMFR_CLIPBOARD_MESSAGE_OFFER: + return record->clipboardGeneration && record->token && + !(record->token & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL) && + !record->transfer && !record->offset && !record->size && + !record->format && !record->flags && !record->length && + !record->sequence; + + case KVMFR_CLIPBOARD_MESSAGE_CLEAR: + return record->clipboardGeneration && !record->token && + !record->transfer && !record->offset && !record->size && + !record->format && !record->flags && !record->length && + !record->sequence; + + case KVMFR_CLIPBOARD_MESSAGE_REQUEST: + return record->clipboardGeneration && + kvmfrClipboardTransferFromHelper(record->transfer) && + kvmfrClipboardFormatValid(record->format) && + !record->offset && !record->size && !record->flags && + !record->token && !record->length && !record->sequence; + + case KVMFR_CLIPBOARD_MESSAGE_DATA: + { + const uint64_t end = record->offset + record->length; + if (!record->clipboardGeneration || record->token || + ((record->flags & KVMFR_CLIPBOARD_FLAG_BEGIN) && + (record->offset || record->sequence)) || + ((record->flags & KVMFR_CLIPBOARD_FLAG_END) && + record->size != end)) + return false; + if ((record->flags & KVMFR_CLIPBOARD_FLAG_BEGIN) && + record->size != KVMFR_CLIPBOARD_SIZE_UNKNOWN && + end > record->size) + return false; + return true; + } + + case KVMFR_CLIPBOARD_MESSAGE_CANCEL: + return record->transfer && !record->offset && !record->size && + (!record->format || kvmfrClipboardFormatValid(record->format)) && + !record->flags && !record->length && + !record->sequence; + + default: + return false; + } +} + +static bool validateReadChunkNL(const LGMPClipboard * clipboard, + const KVMFRClipboardMessage * record) +{ + if (clipboard->readRequest == LG_CLIPBOARD_REQUEST_INVALID || + record->transfer != clipboard->readTransfer || + record->clipboardGeneration != + clipboard->readClipboardGeneration || + record->format != clipboard->readFormat || + record->offset != clipboard->readOffset || + record->sequence != clipboard->readSequence) + return false; + + const uint64_t end = record->offset + record->length; + if (!record->length && + !(record->flags & (KVMFR_CLIPBOARD_FLAG_BEGIN | + KVMFR_CLIPBOARD_FLAG_END))) + return false; + if (!clipboard->readBegan) + { + if (record->offset || record->sequence || + !(record->flags & KVMFR_CLIPBOARD_FLAG_BEGIN)) + return false; + } + else if (record->flags & KVMFR_CLIPBOARD_FLAG_BEGIN) + return false; + else if (!(record->flags & KVMFR_CLIPBOARD_FLAG_END) && + record->size != KVMFR_CLIPBOARD_SIZE_UNKNOWN) + return false; + + const uint64_t hint = clipboard->readBegan ? + clipboard->readSizeHint : record->size; + if (hint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && end > hint) + return false; + if (record->flags & KVMFR_CLIPBOARD_FLAG_END) + return record->size == end && + (hint == KVMFR_CLIPBOARD_SIZE_UNKNOWN || hint == end); + return true; +} + +static void advanceReadNL(LGMPClipboard * clipboard, + const KVMFRClipboardMessage * record) +{ + if (!clipboard->readBegan) + { + clipboard->readBegan = true; + clipboard->readSizeHint = record->size; + } + clipboard->readOffset += record->length; + ++clipboard->readSequence; + if (record->flags & KVMFR_CLIPBOARD_FLAG_END) + clearReadNL(clipboard); +} + +static void dispatchOffer(LGMPClipboard * clipboard, + KVMFRClipboardFormatFlags mask) +{ + LG_ClipboardData formats[LG_CLIPBOARD_DATA_NONE]; + const unsigned count = formatsFromMask(mask, formats); + + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + const LG_ClipboardEventOps * events = clipboard->events; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->lock); + if (events && events->notice) + events->notice(opaque, formats, count); + LG_UNLOCK(clipboard->eventLock); +} + +static void dispatchRelease(LGMPClipboard * clipboard) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + const LG_ClipboardEventOps * events = clipboard->events; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->lock); + if (events && events->release) + events->release(opaque); + LG_UNLOCK(clipboard->eventLock); +} + +static bool dispatchRequest(LGMPClipboard * clipboard, + const KVMFRClipboardMessage * record) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + const LG_ClipboardEventOps * events = clipboard->events; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->lock); + const bool result = events && events->request && events->request(opaque, + record->transfer, fromWireFormat(record->format)); + LG_UNLOCK(clipboard->eventLock); + return result; +} + +static void dispatchCancel(LGMPClipboard * clipboard, + const KVMFRClipboardMessage * record) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + const LG_ClipboardEventOps * events = clipboard->events; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->lock); + if (events) + { + const LG_ClipboardCancelReason reason = fromWireCancel(record->token); + if (kvmfrClipboardTransferFromHelper(record->transfer)) + { + if (events->requestCancel) + events->requestCancel(opaque, record->transfer, reason); + } + else if (events->dataCancel) + events->dataCancel(opaque, record->transfer, reason); + } + LG_UNLOCK(clipboard->eventLock); +} + +static void dispatchDataCancel(LGMPClipboard * clipboard, + LG_ClipboardRequest request, uint32_t reason) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + const LG_ClipboardEventOps * events = clipboard->events; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->lock); + if (events && events->dataCancel) + events->dataCancel( + opaque, request, fromWireCancel(reason)); + LG_UNLOCK(clipboard->eventLock); +} + +static LG_ClipboardResult dispatchData(LGMPClipboard * clipboard, + const KVMFRClipboardMessage * record, const uint8_t * data, + enum HeldPhase phase) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + const LG_ClipboardEventOps * events = clipboard->events; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->lock); + + LG_ClipboardResult result = LG_CLIPBOARD_RESULT_FAILED; + if (events) + { + switch (phase) + { + case HELD_PHASE_BEGIN: + if (events->dataBegin) + result = events->dataBegin(opaque, record->transfer, + fromWireFormat(record->format), record->size); + break; + + case HELD_PHASE_CHUNK: + if (events->dataChunk) + result = events->dataChunk(opaque, record->transfer, + record->offset, data, record->length); + break; + + case HELD_PHASE_END: + if (events->dataEnd) + result = events->dataEnd( + opaque, record->transfer, record->size); + break; + + case HELD_PHASE_NONE: + break; + } + } + LG_UNLOCK(clipboard->eventLock); + return result; +} + +static void dispatchReady(LGMPClipboard * clipboard, + LG_ClipboardRequest request) +{ + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + const LG_ClipboardEventOps * events = clipboard->events; + void * opaque = clipboard->eventOpaque; + LG_UNLOCK(clipboard->lock); + if (events && events->dataReady) + events->dataReady(opaque, request); + LG_UNLOCK(clipboard->eventLock); +} + +static void applyStatusNL(LGMPClipboard * clipboard, + const KVMFRClipboardStatus * status, uint32_t serial, + bool * changed) +{ + const bool wasValid = clipboard->statusValid; + const bool wasAvailable = clipboard->available; + const uint32_t oldGeneration = clipboard->endpointGeneration; + clipboard->statusValid = true; + clipboard->statusSerial = serial; + const bool endpointAvailable = + (status->flags & KVMFR_CLIPBOARD_STATUS_AVAILABLE) != 0; + clipboard->endpointGeneration = status->generation; + + const bool owned = (status->flags & KVMFR_CLIPBOARD_STATUS_HAS_OWNER) && + clipboard->claimed && status->ownerClientID == clipboard->clientID && + status->ownerGeneration == clipboard->claimGeneration; + const bool ownedByOther = + (status->flags & KVMFR_CLIPBOARD_STATUS_HAS_OWNER) && !owned; + clipboard->available = endpointAvailable && !ownedByOther; + bool restore = false; + + if (!endpointAvailable || oldGeneration != status->generation || + ownedByOther) + { + clearProtocolNL(clipboard); + restore = endpointAvailable && !ownedByOther; + } + else if (status->flags & KVMFR_CLIPBOARD_STATUS_HAS_OWNER) + clipboard->ownerConfirmed = owned; + else + { + if (clipboard->claimed && clipboard->ownerConfirmed) + { + clearProtocolNL(clipboard); + restore = endpointAvailable; + } + clipboard->ownerConfirmed = false; + } + + if (restore && !restoreClipboardNL(clipboard)) + clearProtocolNL(clipboard); + + *changed = !wasValid || wasAvailable != clipboard->available || + oldGeneration != clipboard->endpointGeneration; + if (*changed) + nextNonzero(&clipboard->providerGeneration); +} + +static bool flushPendingNL(LGMPClipboard * clipboard) +{ + while (clipboard->pendingCount) + { + KVMFRClipboardMessage * record = + &pendingAt(clipboard, 0)->record; + const LGMP_STATUS status = lgmpClientTrySendData( + clipboard->queue, record, sizeof(*record), NULL); + if (status == LGMP_ERR_QUEUE_BUSY || status == LGMP_ERR_QUEUE_FULL) + return true; + if (status != LGMP_OK) + { + connectionFailed(clipboard, status); + return false; + } + if (record->type == KVMFR_CLIPBOARD_MESSAGE_CLAIM) + clipboard->publishedClaimGeneration = record->generation; + else if (record->type == KVMFR_CLIPBOARD_MESSAGE_RELEASE && + clipboard->publishedClaimGeneration == record->generation) + clipboard->publishedClaimGeneration = 0; + clipboard->lastSend = microtime(); + clipboard->pendingHead = + (clipboard->pendingHead + 1) % CLIPBOARD_PENDING_MAX; + --clipboard->pendingCount; + } + return true; +} + +static bool processHeld(LGMPClipboard * clipboard) +{ + for (;;) + { + KVMFRClipboardMessage record; + const uint8_t * data; + enum HeldPhase phase; + LG_LOCK(clipboard->lock); + if (!clipboard->held || !clipboard->heldReady) + { + LG_UNLOCK(clipboard->lock); + return true; + } + record = clipboard->heldRecord; + data = record.length ? + (const uint8_t *)clipboard->heldMessage.mem + sizeof(record) : NULL; + phase = clipboard->heldPhase; + clipboard->heldReady = false; + LG_UNLOCK(clipboard->lock); + + const LG_ClipboardResult result = + dispatchData(clipboard, &record, data, phase); + LG_LOCK(clipboard->lock); + if (!clipboard->held) + { + LG_UNLOCK(clipboard->lock); + return true; + } + if (result == LG_CLIPBOARD_RESULT_BLOCKED) + { + LG_UNLOCK(clipboard->lock); + return true; + } + + if (result == LG_CLIPBOARD_RESULT_ACCEPTED) + { + switch (phase) + { + case HELD_PHASE_BEGIN: + clipboard->heldPhase = record.length ? HELD_PHASE_CHUNK : + (record.flags & KVMFR_CLIPBOARD_FLAG_END) ? + HELD_PHASE_END : HELD_PHASE_NONE; + break; + + case HELD_PHASE_CHUNK: + clipboard->heldPhase = + (record.flags & KVMFR_CLIPBOARD_FLAG_END) ? + HELD_PHASE_END : HELD_PHASE_NONE; + break; + + case HELD_PHASE_END: + clipboard->heldPhase = HELD_PHASE_NONE; + break; + + case HELD_PHASE_NONE: + break; + } + } + else + clipboard->heldPhase = HELD_PHASE_NONE; + + if (clipboard->heldPhase != HELD_PHASE_NONE) + { + clipboard->heldReady = true; + LG_UNLOCK(clipboard->lock); + continue; + } + + const bool matchingRead = + clipboard->readRequest == record.transfer && + clipboard->readTransfer; + if (result == LG_CLIPBOARD_RESULT_FAILED && matchingRead) + { + KVMFRClipboardMessage cancel = { 0 }; + cancel.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL; + cancel.clipboardGeneration = clipboard->readClipboardGeneration; + cancel.transfer = clipboard->readTransfer; + cancel.format = clipboard->readFormat; + cancel.token = LG_CLIPBOARD_CANCEL_INVALID; + enqueueRecordNL(clipboard, cancel); + } + if (matchingRead && result == LG_CLIPBOARD_RESULT_ACCEPTED) + advanceReadNL(clipboard, &record); + else if (matchingRead) + clearReadNL(clipboard); + + const LGMP_STATUS done = lgmpClientMessageDone(clipboard->queue); + clipboard->held = false; + memset(&clipboard->heldMessage, 0, sizeof(clipboard->heldMessage)); + memset(&clipboard->heldRecord, 0, sizeof(clipboard->heldRecord)); + if (done != LGMP_OK) + { + connectionFailed(clipboard, done); + LG_UNLOCK(clipboard->lock); + return false; + } + LG_UNLOCK(clipboard->lock); + return true; + } +} + +static bool processMessage(LGMPClipboard * clipboard) +{ + LG_LOCK(clipboard->lock); + if (!clipboard->connected || !clipboard->queue || clipboard->held) + { + LG_UNLOCK(clipboard->lock); + return true; + } + + LGMPMessage message; + LGMP_STATUS status = lgmpClientProcess(clipboard->queue, &message); + if (status == LGMP_ERR_QUEUE_EMPTY) + { + LG_UNLOCK(clipboard->lock); + return true; + } + if (status != LGMP_OK) + { + connectionFailed(clipboard, status); + LG_UNLOCK(clipboard->lock); + return false; + } + + const KVMFRClipboardQueueType type = + KVMFR_CLIPBOARD_QUEUE_TYPE(message.udata); + if (type == KVMFR_CLIPBOARD_QUEUE_STATUS) + { + KVMFRClipboardStatus snapshot = { 0 }; + const bool valid = message.size == sizeof(snapshot); + if (valid) + memcpy(&snapshot, message.mem, sizeof(snapshot)); + status = lgmpClientMessageDone(clipboard->queue); + bool changed = false; + if (status == LGMP_OK && valid && validStatus(&snapshot)) + { + const uint32_t serial = KVMFR_CLIPBOARD_QUEUE_SERIAL(message.udata); + if (!clipboard->statusValid || + (int32_t)(serial - clipboard->statusSerial) > 0) + applyStatusNL(clipboard, &snapshot, serial, &changed); + } + else if (status == LGMP_OK) + DEBUG_WARN("Ignoring invalid LGMP clipboard status"); + if (status != LGMP_OK) + connectionFailed(clipboard, status); + LG_UNLOCK(clipboard->lock); + if (changed) + notifyStatus(clipboard); + return status == LGMP_OK; + } + + if (type == KVMFR_CLIPBOARD_QUEUE_GRANT) + { + KVMFRClipboardSlotHeader * header = message.mem; + const uint32_t token = KVMFR_CLIPBOARD_QUEUE_SERIAL(message.udata); + const bool valid = message.size == + sizeof(*header) + KVMFR_CLIPBOARD_DATA_BYTES && + header->version == KVMFR_CLIPBOARD_VERSION && + header->type == KVMFR_CLIPBOARD_MESSAGE_GRANT && + header->generation == clipboard->claimGeneration && + header->size == KVMFR_CLIPBOARD_DATA_BYTES && + header->token == token && token > 0 && + token <= KVMFR_CLIPBOARD_SLOT_COUNT && + !header->sequence && !header->clipboardGeneration && + !header->transfer && !header->offset && !header->format && + !header->flags && !header->length; + bool stored = false; + if (valid) + { + struct Grant * grant = &clipboard->grants[token - 1]; + if (!grant->available) + { + grant->header = header; + grant->data = (uint8_t *)(header + 1); + grant->token = token; + grant->available = true; + stored = true; + } + } + status = lgmpClientMessageDone(clipboard->queue); + const bool ready = stored && clipboard->writeBlocked; + const LG_ClipboardRequest request = clipboard->writeBlockedRequest; + if (ready) + clipboard->writeBlocked = false; + if (status != LGMP_OK) + connectionFailed(clipboard, status); + LG_UNLOCK(clipboard->lock); + if (!stored) + DEBUG_WARN("Ignoring invalid LGMP clipboard grant"); + if (ready) + dispatchReady(clipboard, request); + return status == LGMP_OK; + } + + if ((type != KVMFR_CLIPBOARD_QUEUE_MESSAGE && + type != KVMFR_CLIPBOARD_QUEUE_DATA) || + message.size < sizeof(KVMFRClipboardMessage)) + { + status = lgmpClientMessageDone(clipboard->queue); + if (status != LGMP_OK) + connectionFailed(clipboard, status); + LG_UNLOCK(clipboard->lock); + DEBUG_WARN("Ignoring invalid LGMP clipboard message"); + return status == LGMP_OK; + } + + KVMFRClipboardMessage record; + memcpy(&record, message.mem, sizeof(record)); + const bool valid = validateRecord( + clipboard, &record, message.size, type); + if (!valid) + { + status = lgmpClientMessageDone(clipboard->queue); + if (status != LGMP_OK) + connectionFailed(clipboard, status); + LG_UNLOCK(clipboard->lock); + DEBUG_WARN("Ignoring malformed LGMP clipboard record"); + return status == LGMP_OK; + } + + if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA) + { + if (!validateReadChunkNL(clipboard, &record)) + { + status = lgmpClientMessageDone(clipboard->queue); + if (status != LGMP_OK) + connectionFailed(clipboard, status); + LG_UNLOCK(clipboard->lock); + DEBUG_WARN("Ignoring stale or malformed LGMP clipboard data"); + return status == LGMP_OK; + } + record.transfer = clipboard->readRequest; + clipboard->held = true; + clipboard->heldReady = true; + clipboard->heldMessage = message; + clipboard->heldRecord = record; + clipboard->heldPhase = + (record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN) ? HELD_PHASE_BEGIN : + record.length ? HELD_PHASE_CHUNK : HELD_PHASE_END; + LG_UNLOCK(clipboard->lock); + return processHeld(clipboard); + } + + status = lgmpClientMessageDone(clipboard->queue); + if (status != LGMP_OK) + { + connectionFailed(clipboard, status); + LG_UNLOCK(clipboard->lock); + return false; + } + bool dispatch = true; + LG_ClipboardRequest cancelledRead = LG_CLIPBOARD_REQUEST_INVALID; + switch (record.type) + { + case KVMFR_CLIPBOARD_MESSAGE_OFFER: + clipboard->remoteClipboardGeneration = record.clipboardGeneration; + clipboard->remoteFormats = record.token; + clearReadNL(clipboard); + break; + + case KVMFR_CLIPBOARD_MESSAGE_CLEAR: + clipboard->remoteClipboardGeneration = record.clipboardGeneration; + clipboard->remoteFormats = 0; + clearReadNL(clipboard); + break; + + case KVMFR_CLIPBOARD_MESSAGE_REQUEST: + if (clipboard->writeTransfer != LG_CLIPBOARD_REQUEST_INVALID || + record.clipboardGeneration != + clipboard->localClipboardGeneration || + !(clipboard->localFormats & + kvmfrClipboardFormatFlag(record.format))) + { + dispatch = false; + break; + } + clearWriteNL(clipboard); + clipboard->writeTransfer = record.transfer; + clipboard->writeClipboardGeneration = record.clipboardGeneration; + clipboard->writeFormat = record.format; + break; + + case KVMFR_CLIPBOARD_MESSAGE_CANCEL: + { + const bool write = clipboard->writeTransfer == record.transfer; + const bool read = clipboard->readTransfer == record.transfer; + if (!write && !read) + { + dispatch = false; + break; + } + const uint64_t clipboardGeneration = write ? + clipboard->writeClipboardGeneration : + clipboard->readClipboardGeneration; + const KVMFRClipboardFormat format = write ? + clipboard->writeFormat : clipboard->readFormat; + if ((record.clipboardGeneration && + record.clipboardGeneration != clipboardGeneration) || + (record.format && record.format != format)) + { + dispatch = false; + break; + } + if (write) + clearWriteNL(clipboard); + if (read) + { + cancelledRead = clipboard->readRequest; + clearReadNL(clipboard); + } + break; + } + } + LG_UNLOCK(clipboard->lock); + + if (!dispatch) + { + DEBUG_WARN("Ignoring stale LGMP clipboard control record"); + return true; + } + + switch (record.type) + { + case KVMFR_CLIPBOARD_MESSAGE_OFFER: + dispatchOffer(clipboard, record.token); + break; + case KVMFR_CLIPBOARD_MESSAGE_CLEAR: + dispatchRelease(clipboard); + break; + case KVMFR_CLIPBOARD_MESSAGE_REQUEST: + if (!dispatchRequest(clipboard, &record)) + { + LG_LOCK(clipboard->lock); + KVMFRClipboardMessage cancel = { 0 }; + cancel.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL; + cancel.clipboardGeneration = record.clipboardGeneration; + cancel.transfer = record.transfer; + cancel.format = record.format; + cancel.token = LG_CLIPBOARD_CANCEL_UNAVAILABLE; + enqueueRecordNL(clipboard, cancel); + clearWriteNL(clipboard); + LG_UNLOCK(clipboard->lock); + signalWorker(clipboard); + } + break; + case KVMFR_CLIPBOARD_MESSAGE_CANCEL: + if (cancelledRead != LG_CLIPBOARD_REQUEST_INVALID) + dispatchDataCancel(clipboard, cancelledRead, record.token); + else + dispatchCancel(clipboard, &record); + break; + } + return true; +} + +static int clipboardThread(void * opaque) +{ + LGMPClipboard * clipboard = opaque; + while (!atomic_load_explicit(&clipboard->stop, memory_order_acquire)) + { + if (!processHeld(clipboard) || !processMessage(clipboard)) + break; + + LG_ClipboardRequest readyRequest = LG_CLIPBOARD_REQUEST_INVALID; + LG_LOCK(clipboard->lock); + flushPendingNL(clipboard); + const uint64_t now = microtime(); + if (clipboard->connected && clipboard->claimed && + !clipboard->pendingCount && + now - clipboard->lastSend >= CLIPBOARD_KEEPALIVE_US) + enqueueTypeNL(clipboard, KVMFR_CLIPBOARD_MESSAGE_KEEPALIVE); + if (clipboard->writeBlocked && + clipboard->pendingCount < CLIPBOARD_PENDING_MAX && + availableGrantNL(clipboard)) + { + readyRequest = clipboard->writeBlockedRequest; + clipboard->writeBlocked = false; + } + const bool retry = clipboard->pendingCount != 0; + LG_UNLOCK(clipboard->lock); + + if (readyRequest != LG_CLIPBOARD_REQUEST_INVALID) + dispatchReady(clipboard, readyRequest); + + if (atomic_load_explicit(&clipboard->stop, memory_order_acquire)) + break; + lgWaitEvent(clipboard->event, + retry ? CLIPBOARD_RETRY_MS : CLIPBOARD_POLL_MS); + } + notifyStatus(clipboard); + return 0; +} + +bool lgmpClipboard_create(PLGMPClient client, LGMPClipboard ** result) +{ + if (!client || !result) + return false; + LGMPClipboard * clipboard = calloc(1, sizeof(*clipboard)); + if (!clipboard) + return false; + clipboard->client = client; + clipboard->providerGeneration = 1; + LG_LOCK_INIT(clipboard->lock); + LG_LOCK_INIT(clipboard->eventLock); + LG_LOCK_INIT(clipboard->statusLock); + atomic_init(&clipboard->stop, false); + *result = clipboard; + return true; +} + +void lgmpClipboard_destroy(LGMPClipboard ** clipboard) +{ + if (!clipboard || !*clipboard) + return; + lgmpClipboard_disconnect(*clipboard); + LG_LOCK_FREE((*clipboard)->statusLock); + LG_LOCK_FREE((*clipboard)->eventLock); + LG_LOCK_FREE((*clipboard)->lock); + free(*clipboard); + *clipboard = NULL; +} + +bool lgmpClipboard_connect(LGMPClipboard * clipboard, uint32_t clientID) +{ + if (!clipboard || !clientID) + return false; + + LG_LOCK(clipboard->lock); + if (clipboard->connected) + { + const bool same = clipboard->clientID == clientID; + LG_UNLOCK(clipboard->lock); + return same; + } + if (clipboard->thread || clipboard->queue) + { + LG_UNLOCK(clipboard->lock); + return false; + } + + LGMP_STATUS status = lgmpClientSubscribe( + clipboard->client, LGMP_Q_CLIPBOARD, &clipboard->queue); + if (status != LGMP_OK) + { + LG_UNLOCK(clipboard->lock); + DEBUG_WARN("Failed to subscribe to LGMP clipboard queue: %s", + lgmpStatusString(status)); + return false; + } + clipboard->event = lgCreateEvent(true, 0); + if (!clipboard->event) + { + lgmpClientUnsubscribe(&clipboard->queue); + LG_UNLOCK(clipboard->lock); + return false; + } + + clipboard->connected = true; + clipboard->available = false; + clipboard->statusValid = false; + clipboard->claimed = false; + clipboard->ownerConfirmed = false; + clipboard->clientID = clientID; + clipboard->endpointGeneration = 0; + clipboard->statusSerial = 0; + clipboard->pendingHead = 0; + clipboard->pendingCount = 0; + clipboard->held = false; + clipboard->heldReady = false; + clearWriteNL(clipboard); + clipboard->remoteFormats = 0; + clipboard->remoteClipboardGeneration = 0; + clearReadNL(clipboard); + clipboard->publishedClaimGeneration = 0; + memset(clipboard->grants, 0, sizeof(clipboard->grants)); + atomic_store_explicit(&clipboard->stop, false, memory_order_release); + + LGThread * thread; + if (lgCreateThread("lgmpClipboard", clipboardThread, + clipboard, &thread)) + { + clipboard->thread = thread; + LG_UNLOCK(clipboard->lock); + return true; + } + + clipboard->connected = false; + LGEvent * event = clipboard->event; + PLGMPClientQueue queue = clipboard->queue; + clipboard->event = NULL; + clipboard->queue = NULL; + LG_UNLOCK(clipboard->lock); + lgmpClientUnsubscribe(&queue); + lgFreeEvent(event); + return false; +} + +static void releaseOnDisconnect(LGMPClipboard * clipboard) +{ + if (!clipboard->queue || !clipboard->publishedClaimGeneration) + return; + + const KVMFRClipboardMessage release = + { + .version = KVMFR_CLIPBOARD_VERSION, + .type = KVMFR_CLIPBOARD_MESSAGE_RELEASE, + .generation = clipboard->publishedClaimGeneration, + }; + const uint64_t deadline = + microtime() + CLIPBOARD_RELEASE_TIMEOUT_US; + uint32_t serial = 0; + LGMP_STATUS status; + do + { + status = lgmpClientTrySendData(clipboard->queue, + &release, sizeof(release), &serial); + if (status == LGMP_OK) + break; + if (status != LGMP_ERR_QUEUE_BUSY && status != LGMP_ERR_QUEUE_FULL) + return; + if (clipboard->event) + lgWaitEvent(clipboard->event, CLIPBOARD_RETRY_MS); + } + while (microtime() < deadline); + + if (status != LGMP_OK) + { + DEBUG_WARN("Timed out releasing LGMP clipboard ownership"); + return; + } + + do + { + uint32_t processed; + status = lgmpClientGetSerial(clipboard->queue, &processed); + if (status != LGMP_OK) + return; + if ((int32_t)(processed - serial) >= 0) + return; + if (clipboard->event) + lgWaitEvent(clipboard->event, CLIPBOARD_RETRY_MS); + } + while (microtime() < deadline); + DEBUG_WARN("Timed out waiting for LGMP clipboard release"); +} + +void lgmpClipboard_disconnect(LGMPClipboard * clipboard) +{ + if (!clipboard) + return; + + LG_LOCK(clipboard->lock); + if (!clipboard->thread && !clipboard->queue) + { + LG_UNLOCK(clipboard->lock); + return; + } + if (clipboard->connected || clipboard->available) + nextNonzero(&clipboard->providerGeneration); + clipboard->connected = false; + clipboard->available = false; + clipboard->claimed = false; + atomic_store_explicit(&clipboard->stop, true, memory_order_release); + LGThread * thread = clipboard->thread; + LGEvent * event = clipboard->event; + LG_UNLOCK(clipboard->lock); + if (event) + lgSignalEvent(event); + if (thread) + lgJoinThread(thread, NULL); + + LG_LOCK(clipboard->lock); + if (clipboard->held && clipboard->queue) + lgmpClientMessageDone(clipboard->queue); + releaseOnDisconnect(clipboard); + PLGMPClientQueue queue = clipboard->queue; + clipboard->queue = NULL; + clipboard->thread = NULL; + clipboard->event = NULL; + clipboard->held = false; + clearProtocolNL(clipboard); + LG_UNLOCK(clipboard->lock); + if (queue) + lgmpClientUnsubscribe(&queue); + if (event) + lgFreeEvent(event); +} + +static void setStatusListener(void * opaque, + LG_ClipboardStatusFn callback, void * callbackOpaque) +{ + LGMPClipboard * clipboard = opaque; + LG_LOCK(clipboard->statusLock); + LG_LOCK(clipboard->lock); + clipboard->statusCallback = callback; + clipboard->statusOpaque = callbackOpaque; + LG_ClipboardStatus status = + { + .available = clipboard->connected && clipboard->available, + .generation = clipboard->providerGeneration, + }; + LG_UNLOCK(clipboard->lock); + if (callback) + callback(callbackOpaque, &status); + LG_UNLOCK(clipboard->statusLock); +} + +static bool attach(void * opaque, const LG_ClipboardEventOps * events, + void * eventOpaque) +{ + LGMPClipboard * clipboard = opaque; + KVMFRClipboardFormatFlags formats; + LG_ClipboardData replay[LG_CLIPBOARD_DATA_NONE]; + bool available; + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + available = clipboard->connected && clipboard->available && events; + if (available) + { + clipboard->events = events; + clipboard->eventOpaque = eventOpaque; + if (!ensureClaimNL(clipboard)) + { + clipboard->events = NULL; + clipboard->eventOpaque = NULL; + available = false; + } + } + formats = clipboard->remoteFormats; + LG_UNLOCK(clipboard->lock); + const unsigned replayCount = available ? + formatsFromMask(formats, replay) : 0; + if (replayCount && events->notice) + events->notice(eventOpaque, replay, replayCount); + LG_UNLOCK(clipboard->eventLock); + if (available) + signalWorker(clipboard); + return available; +} + +static void detach(void * opaque) +{ + LGMPClipboard * clipboard = opaque; + bool wake = false; + LG_LOCK(clipboard->eventLock); + LG_LOCK(clipboard->lock); + clipboard->events = NULL; + clipboard->eventOpaque = NULL; + if (clipboard->claimed) + { + wake = enqueueTypeNL( + clipboard, KVMFR_CLIPBOARD_MESSAGE_RELEASE); + clipboard->claimed = false; + } + LG_UNLOCK(clipboard->lock); + LG_UNLOCK(clipboard->eventLock); + if (wake) + signalWorker(clipboard); +} + +static bool releaseClipboard(void * opaque) +{ + LGMPClipboard * clipboard = opaque; + LG_LOCK(clipboard->lock); + KVMFRClipboardMessage clear = { 0 }; + clear.type = KVMFR_CLIPBOARD_MESSAGE_CLEAR; + clear.clipboardGeneration = clipboard->localClipboardGeneration + 1; + if (!clear.clipboardGeneration) + ++clear.clipboardGeneration; + const unsigned required = clipboard->claimed ? 1U : 2U; + const bool result = + clipboard->pendingCount <= CLIPBOARD_PENDING_MAX - required && + ensureClaimNL(clipboard) && enqueueRecordNL(clipboard, clear); + if (result) + { + clipboard->localClipboardGeneration = clear.clipboardGeneration; + clipboard->localFormats = 0; + } + LG_UNLOCK(clipboard->lock); + if (result) + signalWorker(clipboard); + return result; +} + +static bool notifyTypes(void * opaque, const LG_ClipboardData types[], + size_t count) +{ + LGMPClipboard * clipboard = opaque; + if (!types || !count || count > LG_CLIPBOARD_DATA_NONE) + return false; + for (size_t i = 0; i < count; ++i) + if (!kvmfrClipboardFormatValid(toWireFormat(types[i]))) + return false; + const KVMFRClipboardFormatFlags formats = formatMask(types, count); + + LG_LOCK(clipboard->lock); + KVMFRClipboardMessage offer = { 0 }; + offer.type = KVMFR_CLIPBOARD_MESSAGE_OFFER; + offer.token = formats; + offer.clipboardGeneration = clipboard->localClipboardGeneration + 1; + if (!offer.clipboardGeneration) + ++offer.clipboardGeneration; + const unsigned required = clipboard->claimed ? 1U : 2U; + const bool result = + clipboard->pendingCount <= CLIPBOARD_PENDING_MAX - required && + ensureClaimNL(clipboard) && + enqueueRecordNL(clipboard, offer); + if (result) + { + clipboard->localClipboardGeneration = offer.clipboardGeneration; + clipboard->localFormats = formats; + } + LG_UNLOCK(clipboard->lock); + if (result) + signalWorker(clipboard); + return result; +} + +static LG_ClipboardResult dataBegin(void * opaque, + LG_ClipboardRequest request, LG_ClipboardData type, uint64_t sizeHint) +{ + LGMPClipboard * clipboard = opaque; + const KVMFRClipboardFormat format = toWireFormat(type); + if (!kvmfrClipboardTransferFromHelper(request) || + !kvmfrClipboardFormatValid(format)) + return LG_CLIPBOARD_RESULT_FAILED; + + LG_LOCK(clipboard->lock); + if (!clipboard->connected || !clipboard->available || + !clipboard->claimed || clipboard->writeBegan || + clipboard->writeTransfer != request || + clipboard->writeFormat != format || + !clipboard->writeClipboardGeneration) + { + LG_UNLOCK(clipboard->lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + clipboard->writeSizeHint = sizeHint; + clipboard->writeOffset = 0; + clipboard->writeSequence = 0; + clipboard->writeBegan = true; + clipboard->writeWireBegan = false; + clipboard->writeBlocked = false; + LG_UNLOCK(clipboard->lock); + return LG_CLIPBOARD_RESULT_ACCEPTED; +} + +static LG_ClipboardResult dataChunk(void * opaque, + LG_ClipboardRequest request, uint64_t offset, + const void * data, size_t size) +{ + LGMPClipboard * clipboard = opaque; + if (!kvmfrClipboardTransferFromHelper(request) || !data || !size || + size > KVMFR_CLIPBOARD_DATA_BYTES || offset > UINT64_MAX - size) + return LG_CLIPBOARD_RESULT_FAILED; + + LG_LOCK(clipboard->lock); + if (!clipboard->connected || !clipboard->available || + !clipboard->claimed || !clipboard->writeBegan || + clipboard->writeTransfer != request || + clipboard->writeOffset != offset || + (clipboard->writeSizeHint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && + offset + size > clipboard->writeSizeHint)) + { + LG_UNLOCK(clipboard->lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + KVMFRClipboardMessage record = { 0 }; + record.type = KVMFR_CLIPBOARD_MESSAGE_DATA; + record.clipboardGeneration = clipboard->writeClipboardGeneration; + record.sequence = clipboard->writeSequence; + record.transfer = request; + record.offset = offset; + record.size = clipboard->writeWireBegan ? + KVMFR_CLIPBOARD_SIZE_UNKNOWN : clipboard->writeSizeHint; + record.format = clipboard->writeFormat; + record.flags = clipboard->writeWireBegan ? + 0 : KVMFR_CLIPBOARD_FLAG_BEGIN; + record.length = (uint32_t)size; + const bool result = enqueueDataNL(clipboard, record, data); + if (result) + { + clipboard->writeOffset += size; + ++clipboard->writeSequence; + clipboard->writeWireBegan = true; + } + else + { + clipboard->writeBlocked = true; + clipboard->writeBlockedRequest = request; + } + LG_UNLOCK(clipboard->lock); + if (result) + signalWorker(clipboard); + return result ? LG_CLIPBOARD_RESULT_ACCEPTED : + LG_CLIPBOARD_RESULT_BLOCKED; +} + +static LG_ClipboardResult dataEnd(void * opaque, + LG_ClipboardRequest request, uint64_t finalSize) +{ + LGMPClipboard * clipboard = opaque; + if (!kvmfrClipboardTransferFromHelper(request)) + return LG_CLIPBOARD_RESULT_FAILED; + LG_LOCK(clipboard->lock); + if (!clipboard->connected || !clipboard->available || + !clipboard->claimed || !clipboard->writeBegan || + clipboard->writeTransfer != request || + clipboard->writeOffset != finalSize || + (clipboard->writeSizeHint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && + clipboard->writeSizeHint != finalSize)) + { + LG_UNLOCK(clipboard->lock); + return LG_CLIPBOARD_RESULT_FAILED; + } + KVMFRClipboardMessage record = { 0 }; + record.type = KVMFR_CLIPBOARD_MESSAGE_DATA; + record.clipboardGeneration = clipboard->writeClipboardGeneration; + record.transfer = request; + record.format = clipboard->writeFormat; + record.flags = KVMFR_CLIPBOARD_FLAG_END | + (clipboard->writeWireBegan ? 0 : KVMFR_CLIPBOARD_FLAG_BEGIN); + record.size = finalSize; + record.offset = finalSize; + record.sequence = clipboard->writeSequence; + const bool result = enqueueDataNL(clipboard, record, NULL); + if (result) + clearWriteNL(clipboard); + else + { + clipboard->writeBlocked = true; + clipboard->writeBlockedRequest = request; + } + LG_UNLOCK(clipboard->lock); + if (result) + signalWorker(clipboard); + return result ? LG_CLIPBOARD_RESULT_ACCEPTED : + LG_CLIPBOARD_RESULT_BLOCKED; +} + +static bool dataCancel(void * opaque, LG_ClipboardRequest request, + LG_ClipboardCancelReason reason) +{ + LGMPClipboard * clipboard = opaque; + if (!kvmfrClipboardTransferFromHelper(request) || + (unsigned)reason > LG_CLIPBOARD_CANCEL_INVALID) + return false; + LG_LOCK(clipboard->lock); + KVMFRClipboardMessage record = { 0 }; + record.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL; + record.clipboardGeneration = clipboard->writeClipboardGeneration; + record.transfer = request; + record.format = clipboard->writeFormat; + record.token = reason; + const bool result = clipboard->claimed && + clipboard->writeTransfer == request && + enqueueRecordNL(clipboard, record); + if (clipboard->writeTransfer == request) + clearWriteNL(clipboard); + LG_UNLOCK(clipboard->lock); + if (result) + signalWorker(clipboard); + return result; +} + +static bool dataReady(void * opaque, LG_ClipboardRequest request) +{ + LGMPClipboard * clipboard = opaque; + LG_LOCK(clipboard->lock); + const bool result = clipboard->held && + clipboard->heldRecord.transfer == request; + if (result) + clipboard->heldReady = true; + LG_UNLOCK(clipboard->lock); + if (result) + signalWorker(clipboard); + return result; +} + +static bool requestData(void * opaque, LG_ClipboardRequest request, + LG_ClipboardData type) +{ + LGMPClipboard * clipboard = opaque; + const KVMFRClipboardFormat format = toWireFormat(type); + if (request == LG_CLIPBOARD_REQUEST_INVALID || + !kvmfrClipboardFormatValid(format)) + return false; + LG_LOCK(clipboard->lock); + if (clipboard->readRequest || !clipboard->remoteClipboardGeneration || + !(clipboard->remoteFormats & kvmfrClipboardFormatFlag(format))) + { + LG_UNLOCK(clipboard->lock); + return false; + } + KVMFRClipboardMessage record = { 0 }; + record.type = KVMFR_CLIPBOARD_MESSAGE_REQUEST; + record.transfer = nextClientTransfer(clipboard); + record.format = format; + record.clipboardGeneration = clipboard->remoteClipboardGeneration; + const bool result = ensureClaimNL(clipboard) && + enqueueRecordNL(clipboard, record); + if (result) + { + clipboard->readRequest = request; + clipboard->readTransfer = record.transfer; + clipboard->readClipboardGeneration = + record.clipboardGeneration; + clipboard->readSizeHint = 0; + clipboard->readOffset = 0; + clipboard->readFormat = format; + clipboard->readSequence = 0; + clipboard->readBegan = false; + } + LG_UNLOCK(clipboard->lock); + if (result) + signalWorker(clipboard); + return result; +} + +static const LG_ClipboardOps CLIPBOARD_OPS = +{ + .name = "LGMP", + .setStatusListener = setStatusListener, + .attach = attach, + .detach = detach, + .release = releaseClipboard, + .notifyTypes = notifyTypes, + .dataBegin = dataBegin, + .dataChunk = dataChunk, + .dataEnd = dataEnd, + .dataCancel = dataCancel, + .dataReady = dataReady, + .request = requestData, +}; + +const LG_ClipboardOps * lgmpClipboard_getOps(void) +{ + return &CLIPBOARD_OPS; +} diff --git a/client/transports/LGMP/clipboard.h b/client/transports/LGMP/clipboard.h new file mode 100644 index 00000000..59f80ca8 --- /dev/null +++ b/client/transports/LGMP/clipboard.h @@ -0,0 +1,41 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _H_LG_CLIENT_TRANSPORT_LGMP_CLIPBOARD_ +#define _H_LG_CLIENT_TRANSPORT_LGMP_CLIPBOARD_ + +#include "interface/clipboard.h" + +#include + +#include +#include + +typedef struct LGMPClipboard LGMPClipboard; + +bool lgmpClipboard_create(PLGMPClient client, LGMPClipboard ** result); +void lgmpClipboard_destroy(LGMPClipboard ** clipboard); + +bool lgmpClipboard_connect(LGMPClipboard * clipboard, uint32_t clientID); +void lgmpClipboard_disconnect(LGMPClipboard * clipboard); + +const LG_ClipboardOps * lgmpClipboard_getOps(void); + +#endif diff --git a/client/transports/LGMP/lgmp.c b/client/transports/LGMP/lgmp.c index 17cdfaa2..25cb3544 100644 --- a/client/transports/LGMP/lgmp.c +++ b/client/transports/LGMP/lgmp.c @@ -20,6 +20,7 @@ #include "interface/transport.h" +#include "clipboard.h" #include "input.h" #include "common/KVMFR.h" @@ -109,6 +110,7 @@ struct LG_Transport PLGMPClientQueue ownerFrameQueue[LGMP_Q_FRAME_LEN]; PLGMPClientQueue pointerQueue; LGMPInput * input; + LGMPClipboard * clipboard; LG_Lock frameLock; LG_Lock pointerLock; LG_RWLock videoStatusLock; @@ -133,6 +135,7 @@ struct LG_Transport bool frameStopRequested; bool frameScheduleSupported; atomic_bool inputSupported; + atomic_bool clipboardSupported; uint64_t frameLeaseHandle; uint32_t frameGeneration; uint32_t clientID; @@ -628,6 +631,13 @@ static LGMP_STATUS lgmp_initializeClient(struct LG_Transport * this) return LGMP_ERR_NO_MEM; } + if (!lgmpClipboard_create(this->client, &this->clipboard)) + { + lgmpInput_destroy(&this->input); + lgmpClientFree(&this->client); + return LGMP_ERR_NO_MEM; + } + return LGMP_OK; } @@ -650,6 +660,7 @@ static bool lgmp_create(LG_Transport ** result) this->cursorPollInterval = cursorPoll; atomic_init(&this->connected, false); atomic_init(&this->inputSupported, false); + atomic_init(&this->clipboardSupported, false); atomic_init(&this->destroyPending, false); this->frameWake = lgCreateEvent(true, 0); @@ -860,6 +871,7 @@ static void lgmp_destroyNow(LG_Transport * this) { if (this->client) { + lgmpClipboard_destroy(&this->clipboard); lgmpInput_destroy(&this->input); lgmp_closeQueues(this); lgmpClientFree(&this->client); @@ -958,6 +970,8 @@ static bool lgmp_parseSession(struct LG_Transport * this, session->features |= LG_TRANSPORT_FEATURE_FRAME_SCHEDULE; if (header->features & KVMFR_FEATURE_INPUT) session->features |= LG_TRANSPORT_FEATURE_INPUT; + if (header->features & KVMFR_FEATURE_CLIPBOARD) + session->features |= LG_TRANSPORT_FEATURE_CLIPBOARD; data += sizeof(*header); size -= sizeof(*header); @@ -1076,6 +1090,9 @@ static LG_TransportStatus lgmp_connectInternal(LG_Transport * this, atomic_store_explicit(&this->inputSupported, session->features & LG_TRANSPORT_FEATURE_INPUT, memory_order_release); + atomic_store_explicit(&this->clipboardSupported, + session->features & LG_TRANSPORT_FEATURE_CLIPBOARD, + memory_order_release); this->frameSerial = 0; this->frameSerialValid = false; this->formatValid = false; @@ -1136,6 +1153,7 @@ static void lgmp_disconnect(LG_Transport * this) lgmp_retainVideoStatusLifetime(this); lgmpInput_disconnect(this->input); + lgmpClipboard_disconnect(this->clipboard); LG_LOCK(this->frameLock); if (++this->frameGeneration == 0) @@ -1145,6 +1163,8 @@ static void lgmp_disconnect(LG_Transport * this) this->frameScheduleSupported = false; atomic_store_explicit( &this->inputSupported, false, memory_order_release); + atomic_store_explicit( + &this->clipboardSupported, false, memory_order_release); this->clientID = 0; LG_UNLOCK(this->frameLock); @@ -2474,6 +2494,20 @@ static const LG_InputOps * lgmp_getInputOps(LG_Transport * this, return lgmpInput_getOps(); } +static const LG_ClipboardOps * lgmp_getClipboardOps( + LG_Transport * this, void ** opaque) +{ + *opaque = NULL; + if (!atomic_load_explicit(&this->connected, memory_order_acquire) || + !atomic_load_explicit( + &this->clipboardSupported, memory_order_acquire) || + !lgmpClipboard_connect(this->clipboard, this->clientID)) + return NULL; + + *opaque = this->clipboard; + return lgmpClipboard_getOps(); +} + static void lgmp_probeVideoStatus(LG_Transport * this) { bool frameAvailable = false; @@ -2617,6 +2651,7 @@ const LG_TransportOps LGT_LGMP = .sessionValid = lgmp_sessionValid, .getVideoOps = lgmp_getVideoOps, .getInputOps = lgmp_getInputOps, + .getClipboardOps = lgmp_getClipboardOps, .getRecoveryInfo = lgmp_getRecoveryInfo, .requestRecovery = lgmp_requestRecovery, .sendControl = lgmp_sendControl,