diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 8749d81f..66f45fb6 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -158,6 +158,7 @@ set(SOURCES src/font.c src/util.c src/clipboard.c + src/clipboard_spice.c src/kb.c src/kb_hid.c src/gl_dynprocs.c diff --git a/client/displayservers/Wayland/clipboard.c b/client/displayservers/Wayland/clipboard.c index cb879e8d..034c5c8a 100644 --- a/client/displayservers/Wayland/clipboard.c +++ b/client/displayservers/Wayland/clipboard.c @@ -191,6 +191,44 @@ static bool hasImageMimetype(char ** mimetypes) return false; } +/* wlCb.lock must be held. */ +static struct ClipboardRead * clipboardReadTakeCurrentNL(void) +{ + struct ClipboardRead * data = wlCb.currentRead; + if (!data) + return NULL; + + wlCb.currentRead = NULL; + return data; +} + +static void clipboardReadRetire(struct ClipboardRead * data) +{ + if (data) + waylandPollUnregister(data->fd); +} + +static bool clipboardReadClaim(struct ClipboardRead * data) +{ + LG_LOCK(wlCb.lock); + const bool current = wlCb.currentRead == data; + if (current) + clipboardReadTakeCurrentNL(); + LG_UNLOCK(wlCb.lock); + + if (current) + clipboardReadRetire(data); + return current; +} + +static void clipboardReadCleanup(void * opaque) +{ + struct ClipboardRead * data = opaque; + close(data->fd); + free(data->buf); + free(data); +} + // Destination client handlers. static void dataOfferHandleOffer(void * opaque, struct wl_data_offer * offer, @@ -276,21 +314,36 @@ static void dataDeviceHandleSelection(void * opaque, return; } - wlCb.offer = offer; - - for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) - free(wlCb.mimetypes[i]); - memcpy(wlCb.mimetypes, extra->mimetypes, sizeof(wlCb.mimetypes)); - - wl_data_offer_set_user_data(offer, NULL); - free(extra); - int idx = 0; enum LG_ClipboardData types[LG_CLIPBOARD_DATA_NONE]; for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) - if (wlCb.mimetypes[i]) + if (extra->mimetypes[i]) types[idx++] = i; + char * oldMimetypes[LG_CLIPBOARD_DATA_NONE]; + LG_LOCK(wlCb.lock); + struct ClipboardRead * read = clipboardReadTakeCurrentNL(); + struct wl_data_offer * oldOffer = wlCb.offer; + memcpy(oldMimetypes, wlCb.mimetypes, sizeof(oldMimetypes)); + wlCb.offer = offer; + memcpy(wlCb.mimetypes, extra->mimetypes, sizeof(wlCb.mimetypes)); + LG_UNLOCK(wlCb.lock); + + wl_data_offer_set_user_data(offer, NULL); + memset(extra->mimetypes, 0, sizeof(extra->mimetypes)); + free(extra); + + if (read) + { + const LG_ClipboardRequest request = read->request; + clipboardReadRetire(read); + app_clipboardAbort(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); } @@ -340,8 +393,6 @@ static const struct wl_data_device_listener dataDeviceListener = { bool waylandCBInit(void) { - memset(&wlCb, 0, sizeof(wlCb)); - if (!wlWm.seat) { DEBUG_WARN("Clipboard unavailable without wl_seat"); @@ -370,37 +421,37 @@ bool waylandCBInit(void) return true; } -static void clipboardReadCancel(struct ClipboardRead * data) -{ - waylandPollUnregister(data->fd); - close(data->fd); - free(data->buf); - free(data); - wlCb.currentRead = NULL; -} - static void clipboardReadCallback(uint32_t events, void * opaque) { struct ClipboardRead * data = opaque; + LG_LOCK(wlCb.lock); + const bool active = wlCb.currentRead == data; + LG_UNLOCK(wlCb.lock); + if (!active) + return; + if (events & EPOLLERR) { - clipboardReadCancel(data); + if (clipboardReadClaim(data)) + app_clipboardAbort(data->request); return; } - ssize_t result = read(data->fd, data->buf + data->numRead, data->size - data->numRead); + ssize_t result = read(data->fd, + data->buf + data->numRead, data->size - data->numRead); if (result < 0) { DEBUG_ERROR("Failed to read from clipboard: %s", strerror(errno)); - clipboardReadCancel(data); + if (clipboardReadClaim(data)) + app_clipboardAbort(data->request); return; } if (result == 0) { - app_clipboardNotifySize(data->type, data->numRead); - app_clipboardData(data->type, data->buf, data->numRead); - clipboardReadCancel(data); + if (clipboardReadClaim(data)) + app_clipboardData( + data->request, data->type, data->buf, data->numRead); return; } @@ -409,9 +460,11 @@ static void clipboardReadCallback(uint32_t events, void * opaque) { data->size *= 2; void * nbuf = realloc(data->buf, data->size); - if (!nbuf) { + if (!nbuf) + { DEBUG_ERROR("Failed to realloc clipboard buffer: %s", strerror(errno)); - clipboardReadCancel(data); + if (clipboardReadClaim(data)) + app_clipboardAbort(data->request); return; } @@ -421,70 +474,103 @@ static void clipboardReadCallback(uint32_t events, void * opaque) void waylandCBInvalidate(void) { - if (wlCb.currentRead) - clipboardReadCancel(wlCb.currentRead); + char * mimetypes[LG_CLIPBOARD_DATA_NONE]; + LG_LOCK(wlCb.lock); + struct ClipboardRead * read = clipboardReadTakeCurrentNL(); + struct wl_data_offer * offer = wlCb.offer; + wlCb.offer = NULL; + memcpy(mimetypes, wlCb.mimetypes, sizeof(mimetypes)); + memset(wlCb.mimetypes, 0, sizeof(wlCb.mimetypes)); + LG_UNLOCK(wlCb.lock); + + if (read) + { + const LG_ClipboardRequest request = read->request; + clipboardReadRetire(read); + app_clipboardAbort(request); + } app_clipboardRelease(); - if (wlCb.offer) - wl_data_offer_destroy(wlCb.offer); - wlCb.offer = NULL; + if (offer) + wl_data_offer_destroy(offer); + for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) + free(mimetypes[i]); } -void waylandCBRequest(LG_ClipboardData type) +void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type) { - if (!wlCb.offer || !wlCb.mimetypes[type]) - { - app_clipboardRelease(); + if (request == LG_CLIPBOARD_REQUEST_INVALID || + type < LG_CLIPBOARD_DATA_TEXT || type >= LG_CLIPBOARD_DATA_NONE) return; - } - - if (wlCb.currentRead) - clipboardReadCancel(wlCb.currentRead); int fds[2]; if (pipe(fds) < 0) { DEBUG_ERROR("Failed to get a clipboard pipe: %s", strerror(errno)); - abort(); + app_clipboardAbort(request); + return; } - wl_data_offer_receive(wlCb.offer, wlCb.mimetypes[type], fds[1]); - close(fds[1]); - struct ClipboardRead * data = malloc(sizeof(*data)); if (!data) { DEBUG_ERROR("Failed to allocate memory to read clipboard"); close(fds[0]); + close(fds[1]); + app_clipboardAbort(request); return; } - data->fd = fds[0]; - data->size = 4096; - data->numRead = 0; - data->buf = malloc(data->size); - data->offer = wlCb.offer; - data->type = type; + data->fd = fds[0]; + data->size = 4096; + data->numRead = 0; + data->buf = malloc(data->size); + data->request = request; + data->type = type; if (!data->buf) { DEBUG_ERROR("Failed to allocate memory to receive clipboard data"); close(data->fd); + close(fds[1]); free(data); + app_clipboardAbort(request); return; } - if (!waylandPollRegister(data->fd, clipboardReadCallback, data, EPOLLIN)) + LG_LOCK(wlCb.lock); + struct ClipboardRead * old = clipboardReadTakeCurrentNL(); + const bool available = wlCb.offer && wlCb.mimetypes[type]; + if (available) { - DEBUG_ERROR("Failed to register clipboard read into epoll: %s", strerror(errno)); - close(data->fd); - free(data->buf); - free(data); + wl_data_offer_receive( + wlCb.offer, wlCb.mimetypes[type], fds[1]); + } + close(fds[1]); + + const bool registered = available && + waylandPollRegisterWithCleanup(data->fd, + clipboardReadCallback, data, clipboardReadCleanup, EPOLLIN); + if (registered) + wlCb.currentRead = data; + LG_UNLOCK(wlCb.lock); + + if (old) + { + const LG_ClipboardRequest oldRequest = old->request; + clipboardReadRetire(old); + app_clipboardAbort(oldRequest); + } + if (!registered) + { + if (available) + DEBUG_ERROR("Failed to register clipboard read into epoll: %s", + strerror(errno)); + clipboardReadCleanup(data); + app_clipboardAbort(request); return; } - - wlCb.currentRead = data; } struct ClipboardWrite @@ -494,6 +580,14 @@ struct ClipboardWrite struct CountedBuffer * buffer; }; +static void clipboardWriteCleanup(void * opaque) +{ + struct ClipboardWrite * data = opaque; + close(data->fd); + countedBufferRelease(&data->buffer); + free(data); +} + static void clipboardWriteCallback(uint32_t events, void * opaque) { struct ClipboardWrite * data = opaque; @@ -514,9 +608,6 @@ static void clipboardWriteCallback(uint32_t events, void * opaque) error: waylandPollUnregister(data->fd); - close(data->fd); - countedBufferRelease(&data->buffer); - free(data); } static void dataSourceHandleTarget(void * data, struct wl_data_source * source, @@ -543,8 +634,12 @@ static void dataSourceHandleSend(void * data, struct wl_data_source * source, data->pos = 0; data->buffer = transfer->data; countedBufferAddRef(transfer->data); - waylandPollRegister(fd, clipboardWriteCallback, data, EPOLLOUT); - return; + if (waylandPollRegisterWithCleanup(fd, + clipboardWriteCallback, data, clipboardWriteCleanup, EPOLLOUT)) + return; + + countedBufferRelease(&data->buffer); + free(data); } error: @@ -567,7 +662,7 @@ static const struct wl_data_source_listener dataSourceListener = { }; static void waylandCBReplyFn(void * opaque, LG_ClipboardData type, - uint8_t * data, uint32_t size) + const uint8_t * data, uint32_t size) { if (type == LG_CLIPBOARD_DATA_NONE) return; @@ -587,7 +682,8 @@ static void waylandCBReplyFn(void * opaque, LG_ClipboardData type, free(transfer); return; } - memcpy(transfer->data->data, data, size); + if (size) + memcpy(transfer->data->data, data, size); struct wl_data_source * source = wl_data_device_manager_create_data_source(wlWm.dataDeviceManager); @@ -602,13 +698,38 @@ static void waylandCBReplyFn(void * opaque, LG_ClipboardData type, void waylandCBNotice(LG_ClipboardData type) { - wlCb.haveRequest = true; - wlCb.type = type; - if (!app_clipboardRequest(waylandCBReplyFn, NULL)) - DEBUG_ERROR("Failed to request SPICE clipboard data"); + if (!app_clipboardRequest(type, waylandCBReplyFn, NULL)) + DEBUG_ERROR("Failed to request remote clipboard data"); } void waylandCBRelease(void) { - wlCb.haveRequest = false; +} + +void waylandCBFree(void) +{ + char * mimetypes[LG_CLIPBOARD_DATA_NONE]; + LG_LOCK(wlCb.lock); + struct ClipboardRead * read = clipboardReadTakeCurrentNL(); + struct wl_data_offer * offer = wlCb.offer; + struct wl_data_offer * dndOffer = wlCb.dndOffer; + struct wl_data_device * dataDevice = wlCb.dataDevice; + wlCb.offer = NULL; + wlCb.dndOffer = NULL; + wlCb.dataDevice = NULL; + memcpy(mimetypes, wlCb.mimetypes, sizeof(mimetypes)); + memset(wlCb.mimetypes, 0, sizeof(wlCb.mimetypes)); + LG_UNLOCK(wlCb.lock); + + clipboardReadRetire(read); + if (offer) + wl_data_offer_destroy(offer); + if (dndOffer && dndOffer != offer) + wl_data_offer_destroy(dndOffer); + if (dataDevice) + wl_data_device_release(dataDevice); + for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) + free(mimetypes[i]); + + LG_LOCK_FREE(wlCb.lock); } diff --git a/client/displayservers/Wayland/desktops/libdecor/libdecor.c b/client/displayservers/Wayland/desktops/libdecor/libdecor.c index a588fbfc..65fcf499 100644 --- a/client/displayservers/Wayland/desktops/libdecor/libdecor.c +++ b/client/displayservers/Wayland/desktops/libdecor/libdecor.c @@ -263,7 +263,7 @@ void libdecor_pollWait(struct wl_display * display, int epollFd, for (int i = 0; i < count; ++i) { struct WaylandPoll * poll = events[i].data.ptr; - if (!poll->removed) + if (!atomic_load_explicit(&poll->removed, memory_order_acquire)) poll->callback(events[i].events, poll->opaque); } } diff --git a/client/displayservers/Wayland/desktops/xdg/xdg.c b/client/displayservers/Wayland/desktops/xdg/xdg.c index 248f455a..550a33b5 100644 --- a/client/displayservers/Wayland/desktops/xdg/xdg.c +++ b/client/displayservers/Wayland/desktops/xdg/xdg.c @@ -300,7 +300,7 @@ void xdg_pollWait(struct wl_display * display, int epollFd, bool sawDisplay = false; for (int i = 0; i < count; ++i) { struct WaylandPoll * poll = events[i].data.ptr; - if (!poll->removed) + if (!atomic_load_explicit(&poll->removed, memory_order_acquire)) poll->callback(events[i].events, poll->opaque); if (poll->fd == state.displayFd) sawDisplay = true; diff --git a/client/displayservers/Wayland/poll.c b/client/displayservers/Wayland/poll.c index 744b8c60..51380e4e 100644 --- a/client/displayservers/Wayland/poll.c +++ b/client/displayservers/Wayland/poll.c @@ -25,6 +25,7 @@ #include #include +#include #include #include "common/debug.h" @@ -47,84 +48,150 @@ bool waylandPollInit(void) return wlWm.desktop->pollInit(wlWm.display); } -void waylandWait(unsigned int time) +static void waylandPollDestroyNode(struct WaylandPoll * node) { - wlWm.desktop->pollWait(wlWm.display, wlWm.epollFd, time); - INTERLOCKED_SECTION(wlWm.pollFreeLock, - { - struct WaylandPoll * node; - struct WaylandPoll * temp; - wl_list_for_each_safe(node, temp, &wlWm.pollFree, link) - { - wl_list_remove(&node->link); - free(node); - } - }); + if (node->cleanup) + node->cleanup(node->opaque); + free(node); } -static void waylandPollRemoveNode(struct WaylandPoll * node) +static void waylandPollDestroyList(struct wl_list * list) { - INTERLOCKED_SECTION(wlWm.pollLock, + struct WaylandPoll * node; + struct WaylandPoll * temp; + wl_list_for_each_safe(node, temp, list, link) { wl_list_remove(&node->link); - }); + waylandPollDestroyNode(node); + } } -bool waylandPollRegister(int fd, WaylandPollCallback callback, void * opaque, uint32_t events) +static void waylandPollTakeList(struct wl_list * source, + struct wl_list * destination) +{ + struct WaylandPoll * node; + struct WaylandPoll * temp; + wl_list_for_each_safe(node, temp, source, link) + { + wl_list_remove(&node->link); + wl_list_insert(destination, &node->link); + } +} + +void waylandWait(unsigned int time) +{ + INTERLOCKED_SECTION(wlWm.pollFreeLock, + { + ++wlWm.pollWaiters; + }); + + wlWm.desktop->pollWait(wlWm.display, wlWm.epollFd, time); + + struct wl_list retired; + wl_list_init(&retired); + INTERLOCKED_SECTION(wlWm.pollFreeLock, + { + if (--wlWm.pollWaiters == 0) + waylandPollTakeList(&wlWm.pollFree, &retired); + }); + waylandPollDestroyList(&retired); +} + +void waylandPollFree(void) +{ + if (wlWm.epollFd >= 0) + { + close(wlWm.epollFd); + wlWm.epollFd = -1; + } + + struct wl_list active; + struct wl_list retired; + wl_list_init(&active); + wl_list_init(&retired); + INTERLOCKED_SECTION(wlWm.pollLock, + { + waylandPollTakeList(&wlWm.poll, &active); + }); + + INTERLOCKED_SECTION(wlWm.pollFreeLock, + { + waylandPollTakeList(&wlWm.pollFree, &retired); + }); + waylandPollDestroyList(&active); + waylandPollDestroyList(&retired); + + LG_LOCK_FREE(wlWm.pollFreeLock); + LG_LOCK_FREE(wlWm.pollLock); +} + +bool waylandPollRegisterWithCleanup(int fd, WaylandPollCallback callback, + void * opaque, WaylandPollCleanup cleanup, uint32_t events) { struct WaylandPoll * node = malloc(sizeof(*node)); if (!node) return false; node->fd = fd; - node->removed = false; node->callback = callback; + node->cleanup = cleanup; node->opaque = opaque; + atomic_init(&node->removed, false); - INTERLOCKED_SECTION(wlWm.pollLock, - { - wl_list_insert(&wlWm.poll, &node->link); - }); - + 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) { - waylandPollRemoveNode(node); + LG_UNLOCK(wlWm.pollLock); free(node); return false; } + wl_list_insert(&wlWm.poll, &node->link); + LG_UNLOCK(wlWm.pollLock); return true; } +bool waylandPollRegister(int fd, WaylandPollCallback callback, + void * opaque, uint32_t events) +{ + return waylandPollRegisterWithCleanup( + fd, callback, opaque, NULL, events); +} + bool waylandPollUnregister(int fd) { struct WaylandPoll * node = NULL; - INTERLOCKED_SECTION(wlWm.pollLock, + LG_LOCK(wlWm.pollLock); + struct WaylandPoll * current; + wl_list_for_each(current, &wlWm.poll, link) { - wl_list_for_each(node, &wlWm.poll, link) + if (current->fd == fd) { - if (node->fd == fd) - break; + node = current; + atomic_store_explicit( + &node->removed, true, memory_order_release); + break; } - }); + } if (!node) { + LG_UNLOCK(wlWm.pollLock); DEBUG_ERROR("Attempt to unregister a fd that was not registered: %d", fd); return false; } - node->removed = true; if (epoll_ctl(wlWm.epollFd, EPOLL_CTL_DEL, fd, NULL) < 0) { - DEBUG_ERROR("Failed to unregistered from epoll: %s", strerror(errno)); + LG_UNLOCK(wlWm.pollLock); + DEBUG_ERROR("Failed to unregister from epoll: %s", strerror(errno)); return false; } - - waylandPollRemoveNode(node); + wl_list_remove(&node->link); + LG_UNLOCK(wlWm.pollLock); INTERLOCKED_SECTION(wlWm.pollFreeLock, { diff --git a/client/displayservers/Wayland/wayland.c b/client/displayservers/Wayland/wayland.c index e2992143..3f9483d1 100644 --- a/client/displayservers/Wayland/wayland.c +++ b/client/displayservers/Wayland/wayland.c @@ -108,9 +108,11 @@ static bool getCompositor(char * dst, size_t size) static bool waylandInit(const LG_DSInitParams params) { memset(&wlWm, 0, sizeof(wlWm)); + memset(&wlCb, 0, sizeof(wlCb)); LG_LOCK_INIT(wlWm.surfaceLock); LG_LOCK_INIT(wlWm.pendingHDRLock); LG_LOCK_INIT(wlWm.hdrLock); + LG_LOCK_INIT(wlCb.lock); wlWm.desktop = WL_Desktops[0]; atomic_init(&wlWm.lockActive, false); atomic_init(&wlWm.cmFeaturesDone, false); @@ -207,6 +209,7 @@ static void waylandShutdown(void) static void waylandFree(void) { + waylandCBFree(); waylandIdleFree(); waylandPresentationFree(); waylandWindowFree(); @@ -215,6 +218,7 @@ static void waylandFree(void) waylandColorMgmtFree(); waylandRegistryFree(); waylandCursorFree(); + waylandPollFree(); wl_display_disconnect(wlWm.display); LG_LOCK_FREE(wlWm.hdrLock); LG_LOCK_FREE(wlWm.pendingHDRLock); diff --git a/client/displayservers/Wayland/wayland.h b/client/displayservers/Wayland/wayland.h index de7d6df8..ce448851 100644 --- a/client/displayservers/Wayland/wayland.h +++ b/client/displayservers/Wayland/wayland.h @@ -59,14 +59,16 @@ #include "motion.h" typedef void (*WaylandPollCallback)(uint32_t events, void * opaque); +typedef void (*WaylandPollCleanup)(void * opaque); struct WaylandPoll { - int fd; - bool removed; + int fd; + atomic_bool removed; WaylandPollCallback callback; - void * opaque; - struct wl_list link; + WaylandPollCleanup cleanup; + void * opaque; + struct wl_list link; }; struct OutputColorDescription; @@ -273,40 +275,39 @@ struct WaylandDSState struct wl_list poll; // WaylandPoll::link struct wl_list pollFree; // WaylandPoll::link - LG_Lock pollLock; - LG_Lock pollFreeLock; - int epollFd; - int displayFd; + LG_Lock pollLock; + LG_Lock pollFreeLock; + unsigned int pollWaiters; + int epollFd; + int displayFd; }; struct WCBTransfer { struct CountedBuffer * data; - const char ** mimetypes; + const char ** mimetypes; }; struct ClipboardRead { - int fd; - size_t size; - size_t numRead; - uint8_t * buf; - enum LG_ClipboardData type; - struct wl_data_offer * offer; + int fd; + size_t size; + size_t numRead; + uint8_t * buf; + LG_ClipboardRequest request; + LG_ClipboardData type; }; struct WCBState { struct wl_data_device * dataDevice; - char lgMimetype[64]; + char lgMimetype[64]; - char * mimetypes[LG_CLIPBOARD_DATA_NONE]; + char * mimetypes[LG_CLIPBOARD_DATA_NONE]; struct wl_data_offer * offer; struct wl_data_offer * dndOffer; - bool haveRequest; - LG_ClipboardData type; - + LG_Lock lock; struct ClipboardRead * currentRead; }; @@ -320,7 +321,8 @@ void waylandActivationRequestActivation(void); // clipboard module bool waylandCBInit(void); -void waylandCBRequest(LG_ClipboardData type); +void waylandCBFree(void); +void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type); void waylandCBNotice(LG_ClipboardData type); void waylandCBRelease(void); void waylandCBInvalidate(void); @@ -407,8 +409,11 @@ void waylandOutputUpdateHDRWhiteLevel(void); // poll module bool waylandPollInit(void); +void waylandPollFree(void); void waylandWait(unsigned int time); bool waylandPollRegister(int fd, WaylandPollCallback callback, void * opaque, uint32_t events); +bool waylandPollRegisterWithCleanup(int fd, WaylandPollCallback callback, + void * opaque, WaylandPollCleanup cleanup, uint32_t events); bool waylandPollUnregister(int fd); // presentation module diff --git a/client/displayservers/X11/clipboard.c b/client/displayservers/X11/clipboard.c index fb411824..987ab5e6 100644 --- a/client/displayservers/X11/clipboard.c +++ b/client/displayservers/X11/clipboard.c @@ -31,16 +31,29 @@ #include "app.h" #include "common/array.h" #include "common/debug.h" +#include "common/locking.h" + +struct X11ClipboardRead +{ + Window window; + LG_ClipboardRequest request; + LG_ClipboardData type; + bool incremental; + uint8_t * buffer; + size_t size; + size_t capacity; +}; struct X11ClipboardState { + LG_Lock lock; Atom aCurSelection; Atom aTypes[LG_CLIPBOARD_DATA_NONE]; + Window targetsWindow; LG_ClipboardData type; bool haveRequest; - bool incrStart; - unsigned int lowerBound; + struct X11ClipboardRead read; }; static const char * atomTypes[] = @@ -60,6 +73,8 @@ 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 void clearTargetsNL(void); bool x11CBEventThread(const XEvent * xe) { @@ -83,9 +98,6 @@ bool x11CBEventThread(const XEvent * xe) if (xe->xproperty.atom == x11atoms.SEL_DATA) { - if (x11cb.lowerBound == 0) - return true; - x11CBSelectionIncr(xe->xproperty); return true; } @@ -106,6 +118,7 @@ bool x11CBEventThread(const XEvent * xe) bool x11CBInit(void) { + LG_LOCK_INIT(x11cb.lock); x11cb.aCurSelection = BadValue; for(int i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) { @@ -131,7 +144,7 @@ bool x11CBInit(void) } static void x11CBReplyFn(void * opaque, LG_ClipboardData type, - uint8_t * data, uint32_t size) + const uint8_t * data, uint32_t size) { XEvent *s = (XEvent *)opaque; @@ -169,7 +182,12 @@ static void x11CBSelectionRequest(const XSelectionRequestEvent e) s->xselection.property = e.property; s->xselection.time = e.time; - if (!x11cb.haveRequest) + LG_LOCK(x11cb.lock); + const bool haveRequest = x11cb.haveRequest; + const LG_ClipboardData requestType = x11cb.type; + LG_UNLOCK(x11cb.lock); + + if (!haveRequest) goto nodata; // target list requested @@ -177,7 +195,7 @@ static void x11CBSelectionRequest(const XSelectionRequestEvent e) { Atom targets[2]; targets[0] = x11atoms.TARGETS; - targets[1] = x11cb.aTypes[x11cb.type]; + targets[1] = x11cb.aTypes[requestType]; XChangeProperty( e.display, @@ -195,10 +213,10 @@ static void x11CBSelectionRequest(const XSelectionRequestEvent e) // look to see if we can satisfy the data type for(int i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) - if (x11cb.aTypes[i] == e.target && x11cb.type == i) + if (x11cb.aTypes[i] == e.target && requestType == i) { // request the data - if (app_clipboardRequest(x11CBReplyFn, s)) + if (app_clipboardRequest(requestType, x11CBReplyFn, s)) return; goto nodata; } @@ -215,20 +233,53 @@ send: static void x11CBSelectionClear(const XSelectionClearEvent e) { - if (e.selection != x11atoms.CLIPBOARD) + (void)e; +} + +/* x11cb.lock must be held. */ +static struct X11ClipboardRead clearReadNL(void) +{ + const struct X11ClipboardRead read = x11cb.read; + x11cb.read = (struct X11ClipboardRead) { 0 }; + if (read.window) + XDestroyWindow(x11.display, read.window); + return read; +} + +/* x11cb.lock must be held. */ +static LG_ClipboardRequest cancelReadNL(void) +{ + const struct X11ClipboardRead read = clearReadNL(); + free(read.buffer); + return read.request; +} + +/* x11cb.lock must be held. */ +static void clearTargetsNL(void) +{ + if (!x11cb.targetsWindow) return; - x11cb.aCurSelection = BadValue; - app_clipboardRelease(); - return; + XDestroyWindow(x11.display, x11cb.targetsWindow); + x11cb.targetsWindow = 0; } static void x11CBSelectionIncr(const XPropertyEvent e) { Atom type; int format; - unsigned long itemCount, after; - unsigned char *data; + unsigned long itemCount; + unsigned long after; + unsigned char * data = NULL; + + LG_LOCK(x11cb.lock); + if (!x11cb.read.window || !x11cb.read.incremental || + e.window != x11cb.read.window || + e.atom != x11atoms.SEL_DATA) + { + LG_UNLOCK(x11cb.lock); + return; + } if (XGetWindowProperty( e.display, @@ -236,49 +287,7 @@ static void x11CBSelectionIncr(const XPropertyEvent e) e.atom, 0, ~0L, // start and length True, // delete the property - x11atoms.INCR, - &type, - &format, - &itemCount, - &after, - &data) != Success) - { - DEBUG_INFO("GetProp Failed"); - app_clipboardNotifySize(LG_CLIPBOARD_DATA_NONE, 0); - goto out; - } - - LG_ClipboardData dataType; - for(dataType = 0; dataType < LG_CLIPBOARD_DATA_NONE; ++dataType) - if (x11cb.aTypes[dataType] == type) - break; - - if (dataType == LG_CLIPBOARD_DATA_NONE) - { - DEBUG_WARN("clipboard data (%s) not in a supported format", - XGetAtomName(x11.display, type)); - - x11cb.lowerBound = 0; - app_clipboardNotifySize(LG_CLIPBOARD_DATA_NONE, 0); - goto out; - } - - if (x11cb.incrStart) - { - app_clipboardNotifySize(dataType, x11cb.lowerBound); - x11cb.incrStart = false; - } - - XFree(data); - data = NULL; - - if (XGetWindowProperty( - e.display, - e.window, - e.atom, - 0, ~0L, // start and length - True, // delete the property - type, + AnyPropertyType, &type, &format, &itemCount, @@ -286,49 +295,233 @@ static void x11CBSelectionIncr(const XPropertyEvent e) &data) != Success) { DEBUG_ERROR("XGetWindowProperty Failed"); - app_clipboardNotifySize(LG_CLIPBOARD_DATA_NONE, 0); - goto out; + const struct X11ClipboardRead read = clearReadNL(); + LG_UNLOCK(x11cb.lock); + free(read.buffer); + app_clipboardAbort(read.request); + return; } - app_clipboardData(dataType, data, itemCount); - x11cb.lowerBound -= itemCount; + LG_ClipboardData dataType; + for(dataType = 0; dataType < LG_CLIPBOARD_DATA_NONE; ++dataType) + if (x11cb.aTypes[dataType] == type) + break; -out: + if ((itemCount && !data) || dataType == LG_CLIPBOARD_DATA_NONE || + dataType != x11cb.read.type || format != 8) + { + DEBUG_WARN("Invalid incremental clipboard data"); + const struct X11ClipboardRead read = clearReadNL(); + LG_UNLOCK(x11cb.lock); + if (data) + XFree(data); + free(read.buffer); + app_clipboardAbort(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); + return; + } + + if (itemCount > SIZE_MAX - x11cb.read.size) + { + const struct X11ClipboardRead read = clearReadNL(); + LG_UNLOCK(x11cb.lock); + XFree(data); + free(read.buffer); + app_clipboardAbort(read.request); + return; + } + + const size_t required = x11cb.read.size + itemCount; + if (required > x11cb.read.capacity) + { + size_t capacity = x11cb.read.capacity; + while (capacity < required) + { + if (capacity > SIZE_MAX / 2) + { + capacity = required; + break; + } + capacity *= 2; + } + + uint8_t * buffer = realloc(x11cb.read.buffer, capacity); + if (!buffer) + { + const struct X11ClipboardRead read = clearReadNL(); + LG_UNLOCK(x11cb.lock); + XFree(data); + free(read.buffer); + app_clipboardAbort(read.request); + return; + } + x11cb.read.buffer = buffer; + x11cb.read.capacity = capacity; + } + + memcpy(x11cb.read.buffer + x11cb.read.size, data, itemCount); + x11cb.read.size += itemCount; + LG_UNLOCK(x11cb.lock); if (data) XFree(data); } static void x11CBXFixesSelectionNotify(const XFixesSelectionNotifyEvent e) { - // check if the selection is valid and it isn't ourself - if (e.selection != x11atoms.CLIPBOARD || - e.owner == x11.window || e.owner == 0) + if (e.selection != x11atoms.CLIPBOARD || e.owner == x11.window) + return; + + LG_LOCK(x11cb.lock); + XGrabServer(x11.display); + if (XGetSelectionOwner(x11.display, x11atoms.CLIPBOARD) != e.owner) { + XUngrabServer(x11.display); + XFlush(x11.display); + LG_UNLOCK(x11cb.lock); + return; + } + + const LG_ClipboardRequest oldRequest = x11cb.read.window ? + cancelReadNL() : LG_CLIPBOARD_REQUEST_INVALID; + clearTargetsNL(); + + if (e.owner == 0) + { + x11cb.aCurSelection = BadValue; + XUngrabServer(x11.display); + XFlush(x11.display); + LG_UNLOCK(x11cb.lock); + if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) + app_clipboardAbort(oldRequest); + app_clipboardRelease(); return; } // remember which selection we are working with x11cb.aCurSelection = e.selection; + x11cb.targetsWindow = XCreateSimpleWindow( + x11.display, x11.window, 0, 0, 1, 1, 0, 0, 0); + if (!x11cb.targetsWindow) + { + x11cb.aCurSelection = BadValue; + XUngrabServer(x11.display); + XFlush(x11.display); + LG_UNLOCK(x11cb.lock); + if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) + app_clipboardAbort(oldRequest); + app_clipboardRelease(); + return; + } + XConvertSelection( x11.display, e.selection, x11atoms.TARGETS, x11atoms.TARGETS, - x11.window, + x11cb.targetsWindow, CurrentTime); + XUngrabServer(x11.display); + XFlush(x11.display); + LG_UNLOCK(x11cb.lock); - return; + if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) + app_clipboardAbort(oldRequest); } static void x11CBSelectionNotify(const XSelectionEvent e) { - if (e.property == None) - return; - Atom type; int format; - unsigned long itemCount, after; - unsigned char *data; + unsigned long itemCount; + unsigned long after; + unsigned char * data = NULL; + + LG_LOCK(x11cb.lock); + const bool targetReply = x11cb.targetsWindow && + e.requestor == x11cb.targetsWindow && e.target == x11atoms.TARGETS; + if (targetReply) + { + if (e.property == None) + { + clearTargetsNL(); + LG_UNLOCK(x11cb.lock); + app_clipboardRelease(); + return; + } + + if (XGetWindowProperty( + e.display, + e.requestor, + e.property, + 0, ~0L, // start and length + True, // delete the property + AnyPropertyType, + &type, + &format, + &itemCount, + &after, + &data) != Success) + { + clearTargetsNL(); + LG_UNLOCK(x11cb.lock); + app_clipboardRelease(); + return; + } + clearTargetsNL(); + LG_UNLOCK(x11cb.lock); + + // the format is 32-bit and we must have data + // this is technically incorrect however as it's + // an array of padded 64-bit values + if (!data || format != 32) + { + app_clipboardRelease(); + goto out; + } + + int typeCount = 0; + LG_ClipboardData types[LG_CLIPBOARD_DATA_NONE]; + + // see if we support any of the targets listed + const Atom * targets = (const Atom *)data; + for(int n = 0; n < LG_CLIPBOARD_DATA_NONE; ++n) + for(unsigned long i = 0; i < itemCount; ++i) + if (x11cb.aTypes[n] == targets[i]) + { + types[typeCount++] = n; + break; + } + + app_clipboardNotifyTypes(types, typeCount); + goto out; + } + LG_UNLOCK(x11cb.lock); + + LG_LOCK(x11cb.lock); + if (!x11cb.read.window || e.requestor != x11cb.read.window) + { + LG_UNLOCK(x11cb.lock); + return; + } + + if (e.property == None) + { + const LG_ClipboardRequest request = cancelReadNL(); + LG_UNLOCK(x11cb.lock); + app_clipboardAbort(request); + return; + } if (XGetWindowProperty( e.display, @@ -343,60 +536,65 @@ static void x11CBSelectionNotify(const XSelectionEvent e) &after, &data) != Success) { - app_clipboardNotifySize(LG_CLIPBOARD_DATA_NONE, 0); - goto out; + const LG_ClipboardRequest request = cancelReadNL(); + LG_UNLOCK(x11cb.lock); + app_clipboardAbort(request); + return; } if (type == x11atoms.INCR) { - x11cb.incrStart = true; - x11cb.lowerBound = *(unsigned int *)data; - goto out; - } - - // the target list - if (e.property == x11atoms.TARGETS) - { - // the format is 32-bit and we must have data - // this is technically incorrect however as it's - // an array of padded 64-bit values - if (!data || format != 32) - goto out; - - int typeCount = 0; - LG_ClipboardData types[itemCount]; - - // see if we support any of the targets listed - const uint64_t * targets = (const uint64_t *)data; - for(unsigned long i = 0; i < itemCount; ++i) + if (!data || format != 32 || itemCount < 1) { - for(int n = 0; n < LG_CLIPBOARD_DATA_NONE; ++n) - if (x11cb.aTypes[n] == targets[i]) - types[typeCount++] = n; + const LG_ClipboardRequest request = cancelReadNL(); + LG_UNLOCK(x11cb.lock); + if (data) + XFree(data); + app_clipboardAbort(request); + return; } - app_clipboardNotifyTypes(types, typeCount); - goto out; - } - - if (e.property == x11atoms.SEL_DATA) - { - LG_ClipboardData dataType; - for(dataType = 0; dataType < LG_CLIPBOARD_DATA_NONE; ++dataType) - if (x11cb.aTypes[dataType] == type) - break; - - if (dataType == LG_CLIPBOARD_DATA_NONE) + 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) { - DEBUG_WARN("clipboard data (%s) not in a supported format", - XGetAtomName(x11.display, type)); - goto out; + const LG_ClipboardRequest request = cancelReadNL(); + LG_UNLOCK(x11cb.lock); + XFree(data); + app_clipboardAbort(request); + return; } + x11cb.read.capacity = capacity; + x11cb.read.incremental = true; + LG_UNLOCK(x11cb.lock); + XFree(data); + return; + } - app_clipboardData(dataType, data, itemCount); + LG_ClipboardData dataType; + for(dataType = 0; dataType < LG_CLIPBOARD_DATA_NONE; ++dataType) + if (x11cb.aTypes[dataType] == type) + break; + + const LG_ClipboardRequest request = x11cb.read.request; + const bool valid = dataType != LG_CLIPBOARD_DATA_NONE && + dataType == x11cb.read.type && format == 8; + cancelReadNL(); + LG_UNLOCK(x11cb.lock); + + if (!valid) + { + DEBUG_WARN("Invalid clipboard data"); + app_clipboardAbort(request); goto out; } + app_clipboardData(request, dataType, data, itemCount); + out: if (data) XFree(data); @@ -404,29 +602,79 @@ out: void x11CBNotice(LG_ClipboardData type) { - x11cb.haveRequest = true; - x11cb.type = type; + LG_LOCK(x11cb.lock); + const LG_ClipboardRequest oldRequest = x11cb.read.window ? + cancelReadNL() : LG_CLIPBOARD_REQUEST_INVALID; + clearTargetsNL(); + x11cb.aCurSelection = BadValue; + x11cb.haveRequest = true; + x11cb.type = type; XSetSelectionOwner(x11.display, x11atoms.CLIPBOARD, x11.window, CurrentTime); XFlush(x11.display); + LG_UNLOCK(x11cb.lock); + if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) + app_clipboardAbort(oldRequest); } void x11CBRelease(void) { + LG_LOCK(x11cb.lock); x11cb.haveRequest = false; - XSetSelectionOwner(x11.display, x11atoms.CLIPBOARD, None, CurrentTime); + XGrabServer(x11.display); + if (XGetSelectionOwner(x11.display, x11atoms.CLIPBOARD) == x11.window) + XSetSelectionOwner( + x11.display, x11atoms.CLIPBOARD, None, CurrentTime); + XUngrabServer(x11.display); XFlush(x11.display); + LG_UNLOCK(x11cb.lock); } -void x11CBRequest(LG_ClipboardData type) +void x11CBRequest(LG_ClipboardRequest request, LG_ClipboardData type) { - if (x11cb.aCurSelection == BadValue) + if (request == LG_CLIPBOARD_REQUEST_INVALID || + type < LG_CLIPBOARD_DATA_TEXT || type >= LG_CLIPBOARD_DATA_NONE) return; + LG_LOCK(x11cb.lock); + const LG_ClipboardRequest oldRequest = x11cb.read.window ? + cancelReadNL() : LG_CLIPBOARD_REQUEST_INVALID; + if (x11cb.aCurSelection == BadValue) + { + LG_UNLOCK(x11cb.lock); + if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) + app_clipboardAbort(oldRequest); + app_clipboardAbort(request); + return; + } + + const Window window = XCreateSimpleWindow( + x11.display, x11.window, 0, 0, 1, 1, 0, 0, 0); + if (!window) + { + LG_UNLOCK(x11cb.lock); + if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) + app_clipboardAbort(oldRequest); + app_clipboardAbort(request); + return; + } + + XSelectInput(x11.display, window, PropertyChangeMask); + x11cb.read = (struct X11ClipboardRead) + { + .window = window, + .request = request, + .type = type, + }; XConvertSelection( x11.display, x11cb.aCurSelection, x11cb.aTypes[type], x11atoms.SEL_DATA, - x11.window, + window, CurrentTime); + XFlush(x11.display); + LG_UNLOCK(x11cb.lock); + + if (oldRequest != LG_CLIPBOARD_REQUEST_INVALID) + app_clipboardAbort(oldRequest); } diff --git a/client/displayservers/X11/clipboard.h b/client/displayservers/X11/clipboard.h index 3765b350..be635990 100644 --- a/client/displayservers/X11/clipboard.h +++ b/client/displayservers/X11/clipboard.h @@ -31,6 +31,6 @@ bool x11CBEventThread(const XEvent * xe); bool x11CBInit(void); void x11CBNotice(LG_ClipboardData type); void x11CBRelease(void); -void x11CBRequest(LG_ClipboardData type); +void x11CBRequest(LG_ClipboardRequest request, LG_ClipboardData type); #endif diff --git a/client/include/app.h b/client/include/app.h index 194e1bfa..de8b072c 100644 --- a/client/include/app.h +++ b/client/include/app.h @@ -165,9 +165,11 @@ void app_overlayConfigRegisterTab(const char * title, void app_clipboardRelease(void); void app_clipboardNotifyTypes(const LG_ClipboardData types[], int count); -void app_clipboardNotifySize(const LG_ClipboardData type, size_t size); -void app_clipboardData(const LG_ClipboardData type, uint8_t * data, size_t size); -bool app_clipboardRequest(const LG_ClipboardReplyFn replyFn, void * opaque); +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 diff --git a/client/include/interface/clipboard.h b/client/include/interface/clipboard.h new file mode 100644 index 00000000..4c286571 --- /dev/null +++ b/client/include/interface/clipboard.h @@ -0,0 +1,108 @@ +/** + * 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_CLIPBOARD_INTERFACE_ +#define _H_LG_CLIENT_CLIPBOARD_INTERFACE_ + +#include +#include +#include + +typedef enum LG_ClipboardData +{ + LG_CLIPBOARD_DATA_TEXT = 0, + LG_CLIPBOARD_DATA_PNG, + LG_CLIPBOARD_DATA_BMP, + LG_CLIPBOARD_DATA_TIFF, + LG_CLIPBOARD_DATA_JPEG, + + LG_CLIPBOARD_DATA_NONE +} +LG_ClipboardData; + +typedef void (*LG_ClipboardReplyFn)(void * opaque, + LG_ClipboardData type, const uint8_t * data, uint32_t size); + +typedef uint64_t LG_ClipboardRequest; + +#define LG_CLIPBOARD_REQUEST_INVALID UINT64_C(0) + +typedef struct LG_ClipboardStatus +{ + bool available; + uint32_t generation; +} +LG_ClipboardStatus; + +typedef void (*LG_ClipboardStatusFn)(void * opaque, + const LG_ClipboardStatus * status); + +typedef struct LG_ClipboardEventOps +{ + /* The type list and data are borrowed for the duration of the callback. + * Event callbacks are always invoked after releasing backend locks. */ + void (*notice)(void * opaque, const LG_ClipboardData types[], size_t count); + void (*data)(void * opaque, LG_ClipboardRequest request, + LG_ClipboardData type, const void * data, size_t size); + void (*release)(void * opaque); + bool (*request)(void * opaque, LG_ClipboardRequest request, + LG_ClipboardData type); +} +LG_ClipboardEventOps; + +typedef struct LG_ClipboardOps +{ + const char * name; + + /* Operations must fail safely if the remote endpoint disappears. */ + + /* Registration must synchronously report the current status after releasing + * any backend locks. Status callbacks must be serialized and generations + * must advance whenever availability changes. Passing NULL unregisters the + * listener and synchronously quiesces its callbacks. Status callbacks must + * not be delivered from another operation or event callback. */ + void (*setStatusListener)(void * opaque, LG_ClipboardStatusFn callback, + void * callbackOpaque); + + /* Attach begins serialized event delivery and may synchronously replay the + * current remote notice. Detach synchronously quiesces event callbacks. + * Other operations must not deliver event callbacks synchronously. */ + bool (*attach)(void * opaque, const LG_ClipboardEventOps * events, + void * eventOpaque); + void (*detach)(void * opaque); + + bool (*release)(void * opaque); + /* All outbound arrays and data are borrowed only until the call returns. */ + bool (*notifyTypes)(void * opaque, const LG_ClipboardData types[], + size_t count); + /* Data is a complete response to a remote request. NONE with no payload + * reports that the request could not be completed. The provider must finish + * any transport framing before returning. */ + bool (*data)(void * opaque, LG_ClipboardRequest request, + LG_ClipboardData type, const void * data, size_t size); + /* A successful request produces exactly one matching data event unless the + * provider is detached, becomes unavailable, or publishes a newer notice + * or release first. */ + bool (*request)(void * opaque, LG_ClipboardRequest request, + LG_ClipboardData type); +} +LG_ClipboardOps; + +#endif diff --git a/client/include/interface/displayserver.h b/client/include/interface/displayserver.h index e7713cf9..b542f891 100644 --- a/client/include/interface/displayserver.h +++ b/client/include/interface/displayserver.h @@ -26,18 +26,7 @@ #include #include "common/types.h" #include "common/debug.h" - -typedef enum LG_ClipboardData -{ - LG_CLIPBOARD_DATA_TEXT = 0, - LG_CLIPBOARD_DATA_PNG, - LG_CLIPBOARD_DATA_BMP, - LG_CLIPBOARD_DATA_TIFF, - LG_CLIPBOARD_DATA_JPEG, - - LG_CLIPBOARD_DATA_NONE // enum max, not a data type -} -LG_ClipboardData; +#include "interface/clipboard.h" typedef enum LG_DSProperty { @@ -136,9 +125,6 @@ typedef struct LG_DSInitParams } LG_DSInitParams; -typedef void (* LG_ClipboardReplyFn)(void * opaque, const LG_ClipboardData type, - uint8_t * data, uint32_t size); - typedef struct LG_DSGLContext * LG_DSGLContext; @@ -262,7 +248,7 @@ struct LG_DisplayServerOps bool (*cbInit)(void); void (*cbNotice)(LG_ClipboardData type); void (*cbRelease)(void); - void (*cbRequest)(LG_ClipboardData type); + void (*cbRequest)(LG_ClipboardRequest request, LG_ClipboardData type); }; #ifdef ENABLE_EGL diff --git a/client/include/interface/transport.h b/client/include/interface/transport.h index 6fae07fa..9fad5198 100644 --- a/client/include/interface/transport.h +++ b/client/include/interface/transport.h @@ -28,6 +28,7 @@ #include "common/framebuffer.h" #include "common/types.h" #include "interface/audio.h" +#include "interface/clipboard.h" #include "interface/input.h" #define LG_TRANSPORT_MAX_DAMAGE_RECTS LG_MAX_FRAME_DAMAGE_RECTS @@ -255,6 +256,11 @@ typedef struct LG_TransportOps * valid until disconnect; NULL indicates that this session has no audio. */ const LG_AudioOps *(*getAudioOps)(LG_Transport * transport, void ** opaque); + /* Queried after connect. The returned operations and opaque value remain + * valid until disconnect; NULL indicates that this session has no + * clipboard. */ + const LG_ClipboardOps *(*getClipboardOps)(LG_Transport * transport, + void ** opaque); bool (*attachRenderer)(LG_Transport * transport, const LG_RendererInterop * interop); void (*detachRenderer)(LG_Transport * transport); diff --git a/client/src/app.c b/client/src/app.c index be0b7a6d..8dc25784 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -299,148 +299,31 @@ void app_handleEnterEvent(bool entered) void app_clipboardRelease(void) { - if (!g_params.clipboardToVM) - return; - - purespice_clipboardRelease(); + lgClipboard_release(); } void app_clipboardNotifyTypes(const LG_ClipboardData types[], int count) { - if (!g_params.clipboardToVM) + if (count < 0) return; - - if (count == 0) - { - purespice_clipboardRelease(); - return; - } - - PSDataType conv[count]; - for(int i = 0; i < count; ++i) - conv[i] = cb_lgTypeToSpiceType(types[i]); - - purespice_clipboardGrab(conv, count); + lgClipboard_notifyTypes(types, (size_t)count); } -void app_clipboardNotifySize(const LG_ClipboardData type, size_t size) +void app_clipboardData(LG_ClipboardRequest request, + const LG_ClipboardData type, const void * data, size_t size) { - if (!g_params.clipboardToVM) - return; - - if (type == LG_CLIPBOARD_DATA_NONE) - { - purespice_clipboardRelease(); - return; - } - - const PSDataType spiceType = cb_lgTypeToSpiceType(type); - if (spiceType == SPICE_DATA_NONE) - return; - - if (!purespice_clipboardDataStart(spiceType, size)) - { - DEBUG_ERROR("Failed to start a %zu-byte SPICE clipboard transfer", size); - return; - } - - g_state.cbWriteType = spiceType; - g_state.cbChunked = true; - g_state.cbXfer = size; + lgClipboard_data(request, type, data, size); } -void app_clipboardData(const LG_ClipboardData type, uint8_t * data, size_t size) +void app_clipboardAbort(LG_ClipboardRequest request) { - if (!g_params.clipboardToVM) - return; - - const PSDataType spiceType = cb_lgTypeToSpiceType(type); - if (spiceType == SPICE_DATA_NONE) - return; - - if (size && !data) - { - DEBUG_ERROR("SPICE clipboard data is NULL"); - return; - } - - if (g_state.cbChunked) - { - if (spiceType != g_state.cbWriteType) - { - DEBUG_ERROR("SPICE clipboard transfer type changed"); - return; - } - - if (size > g_state.cbXfer) - { - DEBUG_ERROR("SPICE clipboard chunk exceeds the remaining transfer size"); - return; - } - - if (size && !purespice_clipboardData(spiceType, data, size)) - { - DEBUG_ERROR("Failed to send SPICE clipboard data"); - return; - } - - g_state.cbXfer -= size; - if (g_state.cbXfer == 0) - { - g_state.cbWriteType = SPICE_DATA_NONE; - g_state.cbChunked = false; - } - return; - } - - if (!purespice_clipboardDataStart(spiceType, size)) - { - DEBUG_ERROR("Failed to start a %zu-byte SPICE clipboard transfer", size); - return; - } - - if (size && !purespice_clipboardData(spiceType, data, size)) - DEBUG_ERROR("Failed to send SPICE clipboard data"); + lgClipboard_abort(request); } -bool app_clipboardRequest(const LG_ClipboardReplyFn replyFn, void * opaque) +bool app_clipboardRequest(LG_ClipboardData type, + LG_ClipboardReplyFn replyFn, void * opaque) { - if (!g_params.clipboardToLocal || !replyFn || !g_state.cbRequestList) - return false; - - const PSDataType type = atomic_load_explicit( - &g_state.cbRemoteType, memory_order_acquire); - struct CBRequest * cbr = malloc(sizeof(*cbr)); - if (!cbr) - { - DEBUG_ERROR("out of memory"); - return false; - } - - cbr->type = type; - cbr->replyFn = replyFn; - cbr->opaque = opaque; - - if (!ll_push(g_state.cbRequestList, cbr)) - { - free(cbr); - return false; - } - - if (purespice_clipboardRequest(type)) - return true; - - /* - * Queue the callback before sending so a fast response cannot arrive before - * its request is visible. If another thread has already consumed it, the - * callback owns both cbr and opaque and the request completed successfully - * from the caller's perspective. - */ - if (!ll_removeData(g_state.cbRequestList, cbr)) - return true; - - free(cbr); - return false; + return lgClipboard_request(type, replyFn, opaque); } static int mapSpiceToImGuiButton(uint32_t button) diff --git a/client/src/clipboard.c b/client/src/clipboard.c index 04a058cf..48a67560 100644 --- a/client/src/clipboard.c +++ b/client/src/clipboard.c @@ -19,113 +19,885 @@ */ #include "clipboard.h" - #include "main.h" #include "common/debug.h" +#include "common/ll.h" +#include "common/locking.h" -LG_ClipboardData cb_spiceTypeToLGType(const PSDataType type) +#include +#include +#include + +typedef struct ClipboardBinding { - switch(type) + const LG_ClipboardOps * ops; + void * opaque; + bool available; + uint32_t generation; +} +ClipboardBinding; + +typedef struct ClipboardRequest +{ + LG_ClipboardRequest request; + ClipboardBinding binding; + uint32_t remoteGeneration; + LG_ClipboardData type; + LG_ClipboardReplyFn replyFn; + void * opaque; +} +ClipboardRequest; + +static struct +{ + LG_Lock registrationLock; + LG_Lock providerLock; + LG_RWLock activeLock; + LG_Lock stateLock; + LG_Lock requestLock; + LG_Lock writeLock; + LG_Lock callbackLock; + + ClipboardBinding fallback; + ClipboardBinding transport; + ClipboardBinding active; + + bool localAvailable; + LG_ClipboardData localTypes[LG_CLIPBOARD_DATA_NONE]; + size_t localTypeCount; + + bool remoteNotice; + LG_ClipboardData remoteType; + uint32_t remoteGeneration; + + bool remoteRequest; + ClipboardBinding remoteRequestBinding; + LG_ClipboardRequest remoteRequestId; + LG_ClipboardRequest remoteTransferId; + LG_ClipboardData remoteRequestType; + + LG_ClipboardRequest requestSerial; + LG_ClipboardRequest transferSerial; + struct ll * requests; +} +clipboard; + +static bool validType(LG_ClipboardData type) +{ + return type >= LG_CLIPBOARD_DATA_TEXT && + type < LG_CLIPBOARD_DATA_NONE; +} + +static uint32_t nextGeneration(uint32_t generation) +{ + if (++generation == 0) + ++generation; + return generation; +} + +static LG_ClipboardRequest nextRequestNL(void) +{ + if (++clipboard.requestSerial == LG_CLIPBOARD_REQUEST_INVALID) + ++clipboard.requestSerial; + return clipboard.requestSerial; +} + +static LG_ClipboardRequest nextTransferNL(void) +{ + if (++clipboard.transferSerial == LG_CLIPBOARD_REQUEST_INVALID) + ++clipboard.transferSerial; + return clipboard.transferSerial; +} + +static bool bindingEqual(const ClipboardBinding * a, + const ClipboardBinding * b) +{ + return a->ops == b->ops && + a->opaque == b->opaque && + a->generation == b->generation; +} + +static bool bindingActiveNL(const ClipboardBinding * binding) +{ + return binding->ops && bindingEqual(binding, &clipboard.active); +} + +static bool localHasTypeNL(LG_ClipboardData type) +{ + for (size_t i = 0; i < clipboard.localTypeCount; ++i) + if (clipboard.localTypes[i] == type) + return true; + + return false; +} + +static void clearRemoteRequestNL(void) +{ + clipboard.remoteRequest = false; + clipboard.remoteRequestBinding = (ClipboardBinding) { 0 }; + clipboard.remoteRequestId = LG_CLIPBOARD_REQUEST_INVALID; + clipboard.remoteTransferId = LG_CLIPBOARD_REQUEST_INVALID; + clipboard.remoteRequestType = LG_CLIPBOARD_DATA_NONE; +} + +static ClipboardRequest * takeCancelableRequest( + const ClipboardBinding * binding, uint32_t generation, bool all) +{ + if (!clipboard.requests) + return NULL; + + ClipboardRequest * request; + ll_lock(clipboard.requests); + ll_forEachNL(clipboard.requests, item, request) { - case SPICE_DATA_TEXT: return LG_CLIPBOARD_DATA_TEXT; break; - case SPICE_DATA_PNG : return LG_CLIPBOARD_DATA_PNG ; break; - case SPICE_DATA_BMP : return LG_CLIPBOARD_DATA_BMP ; break; - case SPICE_DATA_TIFF: return LG_CLIPBOARD_DATA_TIFF; break; - case SPICE_DATA_JPEG: return LG_CLIPBOARD_DATA_JPEG; break; - default: - DEBUG_ERROR("invalid spice data type"); - return LG_CLIPBOARD_DATA_NONE; + if (!all && bindingEqual(&request->binding, binding) && + request->remoteGeneration == generation) + continue; + + ll_removeNL(clipboard.requests, item); + free(item); + ll_unlock(clipboard.requests); + return request; + } + ll_unlock(clipboard.requests); + return NULL; +} + +static ClipboardRequest * takeRequest(LG_ClipboardRequest id) +{ + if (!clipboard.requests) + return NULL; + + ClipboardRequest * request; + ll_lock(clipboard.requests); + ll_forEachNL(clipboard.requests, item, request) + { + if (request->request != id) + continue; + + ll_removeNL(clipboard.requests, item); + free(item); + ll_unlock(clipboard.requests); + return request; + } + ll_unlock(clipboard.requests); + return NULL; +} + +/* callbackLock must be held while completing canceled requests. */ +static void cancelRequestsNL(const ClipboardBinding * binding, + uint32_t generation, bool all) +{ + ClipboardRequest * request; + while ((request = takeCancelableRequest(binding, generation, all))) + { + request->replyFn( + request->opaque, LG_CLIPBOARD_DATA_NONE, NULL, 0); + free(request); } } -PSDataType cb_lgTypeToSpiceType(const LG_ClipboardData type) +static void resetRemote(void) { - switch(type) - { - case LG_CLIPBOARD_DATA_TEXT: return SPICE_DATA_TEXT; break; - case LG_CLIPBOARD_DATA_PNG : return SPICE_DATA_PNG ; break; - case LG_CLIPBOARD_DATA_BMP : return SPICE_DATA_BMP ; break; - case LG_CLIPBOARD_DATA_TIFF: return SPICE_DATA_TIFF; break; - case LG_CLIPBOARD_DATA_JPEG: return SPICE_DATA_JPEG; break; - default: - DEBUG_ERROR("invalid clipboard data type"); - return SPICE_DATA_NONE; - } -} + bool release; -void cb_spiceNotice(const PSDataType type) -{ - if (!g_params.clipboardToLocal) - return; + LG_LOCK(clipboard.requestLock); + LG_LOCK(clipboard.stateLock); + release = clipboard.remoteNotice && clipboard.localAvailable && + g_params.clipboardToLocal; + clipboard.remoteNotice = false; + clipboard.remoteType = LG_CLIPBOARD_DATA_NONE; + clipboard.remoteGeneration = nextGeneration( + clipboard.remoteGeneration); + clearRemoteRequestNL(); + LG_UNLOCK(clipboard.stateLock); + LG_UNLOCK(clipboard.requestLock); - if (!g_state.cbAvailable) - return; - - atomic_store_explicit( - &g_state.cbRemoteType, type, memory_order_release); - g_state.ds->cbNotice(cb_spiceTypeToLGType(type)); -} - -void cb_spiceData(const PSDataType type, uint8_t * buffer, uint32_t size) -{ - if (!g_params.clipboardToLocal) - return; - - struct CBRequest * cbr; - if (!ll_shift(g_state.cbRequestList, (void **)&cbr)) - { - DEBUG_ERROR("Received unsolicited SPICE clipboard data"); - return; - } - - if (type != cbr->type || (size && !buffer)) - { - DEBUG_ERROR("Invalid SPICE clipboard response"); - cbr->replyFn(cbr->opaque, LG_CLIPBOARD_DATA_NONE, NULL, 0); - free(cbr); - return; - } - - if (type == SPICE_DATA_TEXT) - { - // dos2unix - uint8_t * p = buffer; - uint32_t newSize = size; - for(uint32_t i = 0; i < size; ++i) - { - uint8_t c = buffer[i]; - if (c == '\r') - { - --newSize; - continue; - } - *p++ = c; - } - size = newSize; - } - - cbr->replyFn(cbr->opaque, cb_spiceTypeToLGType(type), buffer, size); - free(cbr); -} - -void cb_spiceRelease(void) -{ - if (!g_params.clipboardToLocal) - return; - - atomic_store_explicit( - &g_state.cbRemoteType, SPICE_DATA_NONE, memory_order_release); - - if (g_state.cbAvailable) + LG_LOCK(clipboard.callbackLock); + cancelRequestsNL(NULL, 0, true); + LG_LOCK(clipboard.stateLock); + release = release && clipboard.localAvailable && + !clipboard.remoteNotice; + LG_UNLOCK(clipboard.stateLock); + if (release) g_state.ds->cbRelease(); + LG_UNLOCK(clipboard.callbackLock); } -void cb_spiceRequest(const PSDataType type) +static bool validOps(const LG_ClipboardOps * ops) +{ + return ops && ops->name && ops->attach && ops->detach && + ops->release && ops->notifyTypes && ops->data && ops->request; +} + +static ClipboardBinding makeBinding( + const LG_ClipboardOps * ops, void * opaque) +{ + return (ClipboardBinding) + { + .ops = ops, + .opaque = opaque, + .available = ops && !ops->setStatusListener, + .generation = 0, + }; +} + +static ClipboardBinding * nextBindingSlotNL(void) +{ + if (clipboard.transport.available) + return &clipboard.transport; + if (clipboard.fallback.available) + return &clipboard.fallback; + return NULL; +} + +static void publishLocal(const ClipboardBinding * binding) +{ + LG_LOCK(clipboard.writeLock); + LG_ClipboardData types[LG_CLIPBOARD_DATA_NONE]; + size_t count; + + LG_LOCK(clipboard.stateLock); + count = g_params.clipboardToVM ? clipboard.localTypeCount : 0; + memcpy(types, clipboard.localTypes, count * sizeof(*types)); + LG_UNLOCK(clipboard.stateLock); + + const bool result = count ? + binding->ops->notifyTypes(binding->opaque, types, count) : + binding->ops->release(binding->opaque); + if (!result) + DEBUG_WARN("Failed to publish the local clipboard to %s", + binding->ops->name); + LG_UNLOCK(clipboard.writeLock); +} + +static void eventNotice(void * opaque, + const LG_ClipboardData types[], size_t count) +{ + ClipboardBinding * binding = opaque; + if (!types || count == 0) + return; + + LG_ClipboardData type = LG_CLIPBOARD_DATA_NONE; + for (size_t i = 0; i < count; ++i) + if (validType(types[i])) + { + type = types[i]; + break; + } + if (type == LG_CLIPBOARD_DATA_NONE) + return; + + LG_LOCK_SHARED(clipboard.activeLock); + if (!bindingActiveNL(binding)) + { + LG_UNLOCK_SHARED(clipboard.activeLock); + return; + } + + const ClipboardBinding current = *binding; + LG_UNLOCK_SHARED(clipboard.activeLock); + bool notice; + uint32_t generation; + + LG_LOCK(clipboard.requestLock); + LG_LOCK(clipboard.stateLock); + clipboard.remoteNotice = true; + clipboard.remoteType = type; + clipboard.remoteGeneration = nextGeneration( + clipboard.remoteGeneration); + generation = clipboard.remoteGeneration; + clearRemoteRequestNL(); + notice = clipboard.localAvailable && g_params.clipboardToLocal; + LG_UNLOCK(clipboard.stateLock); + LG_UNLOCK(clipboard.requestLock); + + LG_LOCK(clipboard.callbackLock); + cancelRequestsNL(¤t, generation, false); + LG_LOCK(clipboard.stateLock); + notice = notice && clipboard.localAvailable && + clipboard.remoteNotice && + clipboard.remoteGeneration == generation; + LG_UNLOCK(clipboard.stateLock); + LG_LOCK_SHARED(clipboard.activeLock); + const bool active = bindingActiveNL(¤t); + LG_UNLOCK_SHARED(clipboard.activeLock); + if (notice && active) + g_state.ds->cbNotice(type); + LG_UNLOCK(clipboard.callbackLock); +} + +static void eventData(void * opaque, LG_ClipboardRequest id, + LG_ClipboardData type, const void * data, size_t size) +{ + ClipboardBinding * binding = opaque; + if (id == LG_CLIPBOARD_REQUEST_INVALID) + return; + + LG_LOCK_SHARED(clipboard.activeLock); + if (!bindingActiveNL(binding)) + { + LG_UNLOCK_SHARED(clipboard.activeLock); + return; + } + + const ClipboardBinding current = *binding; + LG_UNLOCK_SHARED(clipboard.activeLock); + LG_LOCK(clipboard.callbackLock); + ClipboardRequest * request = takeRequest(id); + if (!request) + { + LG_UNLOCK(clipboard.callbackLock); + DEBUG_WARN("Ignoring stale clipboard data from %s", + current.ops->name); + return; + } + + LG_LOCK(clipboard.stateLock); + const bool deliver = clipboard.localAvailable && + g_params.clipboardToLocal && clipboard.remoteNotice && + request->remoteGeneration == clipboard.remoteGeneration; + LG_UNLOCK(clipboard.stateLock); + + LG_LOCK_SHARED(clipboard.activeLock); + const bool active = bindingActiveNL(¤t); + LG_UNLOCK_SHARED(clipboard.activeLock); + if (!deliver || !active || !bindingEqual(&request->binding, ¤t) || + type != request->type || !validType(type) || + size > UINT32_MAX || (size && !data)) + { + DEBUG_ERROR("Invalid clipboard response from %s", current.ops->name); + request->replyFn( + request->opaque, LG_CLIPBOARD_DATA_NONE, NULL, 0); + } + else + request->replyFn( + request->opaque, type, data, (uint32_t)size); + + LG_UNLOCK(clipboard.callbackLock); + free(request); +} + +static void eventRelease(void * opaque) +{ + ClipboardBinding * binding = opaque; + + LG_LOCK_SHARED(clipboard.activeLock); + if (!bindingActiveNL(binding)) + { + LG_UNLOCK_SHARED(clipboard.activeLock); + return; + } + LG_UNLOCK_SHARED(clipboard.activeLock); + + bool release; + LG_LOCK(clipboard.requestLock); + LG_LOCK(clipboard.stateLock); + release = clipboard.remoteNotice && clipboard.localAvailable && + g_params.clipboardToLocal; + clipboard.remoteNotice = false; + clipboard.remoteType = LG_CLIPBOARD_DATA_NONE; + clipboard.remoteGeneration = nextGeneration( + clipboard.remoteGeneration); + LG_UNLOCK(clipboard.stateLock); + LG_UNLOCK(clipboard.requestLock); + + LG_LOCK(clipboard.callbackLock); + cancelRequestsNL(NULL, 0, true); + LG_LOCK(clipboard.stateLock); + release = release && clipboard.localAvailable && + !clipboard.remoteNotice; + LG_UNLOCK(clipboard.stateLock); + if (release) + g_state.ds->cbRelease(); + LG_UNLOCK(clipboard.callbackLock); +} + +static bool eventRequest(void * opaque, LG_ClipboardRequest id, + LG_ClipboardData type) +{ + ClipboardBinding * binding = opaque; + if (id == LG_CLIPBOARD_REQUEST_INVALID || !validType(type)) + return false; + + LG_LOCK_SHARED(clipboard.activeLock); + if (!bindingActiveNL(binding)) + { + LG_UNLOCK_SHARED(clipboard.activeLock); + return false; + } + + const ClipboardBinding current = *binding; + const char * name = current.ops->name; + LG_UNLOCK_SHARED(clipboard.activeLock); + LG_ClipboardRequest transfer = LG_CLIPBOARD_REQUEST_INVALID; + LG_LOCK(clipboard.stateLock); + const bool request = clipboard.localAvailable && + g_params.clipboardToVM && localHasTypeNL(type) && + !clipboard.remoteRequest; + if (request) + { + clipboard.remoteRequest = true; + clipboard.remoteRequestBinding = current; + clipboard.remoteRequestId = id; + clipboard.remoteTransferId = nextTransferNL(); + clipboard.remoteRequestType = type; + transfer = clipboard.remoteTransferId; + } + LG_UNLOCK(clipboard.stateLock); + + LG_LOCK(clipboard.callbackLock); + LG_LOCK(clipboard.stateLock); + const bool deliver = request && clipboard.localAvailable && + clipboard.remoteRequest && + clipboard.remoteRequestId == id && + clipboard.remoteTransferId == transfer; + LG_UNLOCK(clipboard.stateLock); + LG_LOCK_SHARED(clipboard.activeLock); + const bool active = bindingActiveNL(¤t); + LG_UNLOCK_SHARED(clipboard.activeLock); + if (deliver && active) + g_state.ds->cbRequest(transfer, type); + else + { + LG_LOCK(clipboard.stateLock); + if (clipboard.remoteRequest && + clipboard.remoteRequestId == id && + clipboard.remoteTransferId == transfer) + clearRemoteRequestNL(); + LG_UNLOCK(clipboard.stateLock); + DEBUG_WARN("Ignoring invalid clipboard request from %s", + name); + } + LG_UNLOCK(clipboard.callbackLock); + return deliver && active; +} + +static const LG_ClipboardEventOps eventOps = +{ + .notice = eventNotice, + .data = eventData, + .release = eventRelease, + .request = eventRequest, +}; + +/* providerLock must be held. dropActive suppresses remote release when the + * endpoint has already disappeared; detach must always quiesce local events. */ +static void updateActive(bool dropActive) +{ + for (;;) + { + LG_LOCK_EXCLUSIVE(clipboard.activeLock); + ClipboardBinding * slot = nextBindingSlotNL(); + ClipboardBinding next = slot ? *slot : (ClipboardBinding) { 0 }; + const ClipboardBinding old = clipboard.active; + if (bindingEqual(&old, &next)) + { + clipboard.active = next; + LG_UNLOCK_EXCLUSIVE(clipboard.activeLock); + return; + } + clipboard.active = (ClipboardBinding) { 0 }; + LG_UNLOCK_EXCLUSIVE(clipboard.activeLock); + + if (old.ops) + { + if (!dropActive && !old.ops->release(old.opaque)) + DEBUG_WARN("Failed to release Clipboard provider: %s", + old.ops->name); + old.ops->detach(old.opaque); + } + dropActive = false; + resetRemote(); + + LG_LOCK_EXCLUSIVE(clipboard.activeLock); + slot = nextBindingSlotNL(); + if (!slot) + { + LG_UNLOCK_EXCLUSIVE(clipboard.activeLock); + DEBUG_INFO("Clipboard is unavailable"); + return; + } + next = *slot; + clipboard.active = next; + LG_UNLOCK_EXCLUSIVE(clipboard.activeLock); + + if (next.ops->attach(next.opaque, &eventOps, slot)) + { + publishLocal(&next); + DEBUG_INFO("Using Clipboard: %s", next.ops->name); + return; + } + + next.ops->detach(next.opaque); + resetRemote(); + + LG_LOCK_EXCLUSIVE(clipboard.activeLock); + if (bindingEqual(&clipboard.active, &next)) + clipboard.active = (ClipboardBinding) { 0 }; + if (bindingEqual(slot, &next)) + slot->available = false; + LG_UNLOCK_EXCLUSIVE(clipboard.activeLock); + + DEBUG_WARN("Failed to attach Clipboard provider: %s", next.ops->name); + } +} + +static void statusChanged(ClipboardBinding * binding, void * opaque, + const LG_ClipboardStatus * status) +{ + if (!status) + return; + + LG_LOCK(clipboard.providerLock); + LG_LOCK_EXCLUSIVE(clipboard.activeLock); + const bool current = binding->ops && binding->opaque == opaque; + const bool wasActive = current && bindingActiveNL(binding); + const bool changed = current && + (binding->available != status->available || + binding->generation != status->generation); + if (changed) + { + binding->available = status->available; + binding->generation = status->generation; + } + LG_UNLOCK_EXCLUSIVE(clipboard.activeLock); + if (changed) + updateActive(wasActive && !status->available); + LG_UNLOCK(clipboard.providerLock); +} + +static void fallbackStatusChanged(void * opaque, + const LG_ClipboardStatus * status) +{ + statusChanged(&clipboard.fallback, opaque, status); +} + +static void transportStatusChanged(void * opaque, + const LG_ClipboardStatus * status) +{ + statusChanged(&clipboard.transport, opaque, status); +} + +static void setBinding(ClipboardBinding * target, + const LG_ClipboardOps * ops, void * opaque, + LG_ClipboardStatusFn statusFn) +{ + if (ops && !validOps(ops)) + { + DEBUG_ERROR("Invalid clipboard operations"); + ops = NULL; + opaque = NULL; + } + + LG_LOCK(clipboard.registrationLock); + LG_LOCK(clipboard.providerLock); + const ClipboardBinding old = *target; + LG_UNLOCK(clipboard.providerLock); + + if (old.ops && old.ops->setStatusListener) + old.ops->setStatusListener(old.opaque, NULL, NULL); + + const ClipboardBinding next = makeBinding(ops, opaque); + LG_LOCK(clipboard.providerLock); + LG_LOCK_EXCLUSIVE(clipboard.activeLock); + *target = next; + LG_UNLOCK_EXCLUSIVE(clipboard.activeLock); + updateActive(false); + LG_UNLOCK(clipboard.providerLock); + + if (next.ops && next.ops->setStatusListener) + next.ops->setStatusListener(next.opaque, statusFn, next.opaque); + LG_UNLOCK(clipboard.registrationLock); +} + +void lgClipboard_init(void) +{ + memset(&clipboard, 0, sizeof(clipboard)); + LG_LOCK_INIT(clipboard.registrationLock); + LG_LOCK_INIT(clipboard.providerLock); + LG_RWLOCK_INIT(clipboard.activeLock); + LG_LOCK_INIT(clipboard.stateLock); + LG_LOCK_INIT(clipboard.requestLock); + LG_LOCK_INIT(clipboard.writeLock); + LG_LOCK_INIT(clipboard.callbackLock); + clipboard.remoteType = LG_CLIPBOARD_DATA_NONE; + clipboard.remoteRequestType = LG_CLIPBOARD_DATA_NONE; + clipboard.requests = ll_new(); +} + +void lgClipboard_free(void) +{ + lgClipboard_setLocalAvailable(false); + lgClipboard_setTransport(NULL, NULL); + lgClipboard_setFallback(NULL, NULL); + LG_LOCK(clipboard.callbackLock); + cancelRequestsNL(NULL, 0, true); + LG_UNLOCK(clipboard.callbackLock); + if (clipboard.requests) + { + ll_free(clipboard.requests); + clipboard.requests = NULL; + } + + LG_LOCK_FREE(clipboard.writeLock); + LG_LOCK_FREE(clipboard.requestLock); + LG_LOCK_FREE(clipboard.stateLock); + LG_LOCK_FREE(clipboard.callbackLock); + LG_RWLOCK_FREE(clipboard.activeLock); + LG_LOCK_FREE(clipboard.providerLock); + LG_LOCK_FREE(clipboard.registrationLock); +} + +void lgClipboard_setLocalAvailable(bool available) +{ + bool notice; + bool release; + LG_ClipboardData type; + + LG_LOCK(clipboard.callbackLock); + LG_LOCK(clipboard.requestLock); + LG_LOCK(clipboard.stateLock); + release = !available && clipboard.localAvailable && + clipboard.remoteNotice && g_params.clipboardToLocal; + clipboard.localAvailable = available; + notice = available && clipboard.remoteNotice && + g_params.clipboardToLocal; + type = clipboard.remoteType; + if (!available) + clearRemoteRequestNL(); + LG_UNLOCK(clipboard.stateLock); + LG_UNLOCK(clipboard.requestLock); + + if (notice) + g_state.ds->cbNotice(type); + if (!available) + { + cancelRequestsNL(NULL, 0, true); + if (release) + g_state.ds->cbRelease(); + } + LG_UNLOCK(clipboard.callbackLock); +} + +void lgClipboard_setFallback(const LG_ClipboardOps * ops, void * opaque) +{ + setBinding(&clipboard.fallback, + ops, opaque, fallbackStatusChanged); +} + +void lgClipboard_setTransport(const LG_ClipboardOps * ops, void * opaque) +{ + setBinding(&clipboard.transport, + ops, opaque, transportStatusChanged); +} + +void lgClipboard_dropTransport(void) +{ + LG_LOCK(clipboard.registrationLock); + LG_LOCK(clipboard.providerLock); + const ClipboardBinding old = clipboard.transport; + LG_UNLOCK(clipboard.providerLock); + + if (old.ops && old.ops->setStatusListener) + old.ops->setStatusListener(old.opaque, NULL, NULL); + + LG_LOCK(clipboard.providerLock); + LG_LOCK_EXCLUSIVE(clipboard.activeLock); + const bool wasActive = bindingActiveNL(&clipboard.transport); + clipboard.transport = (ClipboardBinding) { 0 }; + LG_UNLOCK_EXCLUSIVE(clipboard.activeLock); + updateActive(wasActive); + LG_UNLOCK(clipboard.providerLock); + LG_UNLOCK(clipboard.registrationLock); +} + +void lgClipboard_release(void) { if (!g_params.clipboardToVM) return; - if (g_state.cbAvailable) - g_state.ds->cbRequest(cb_spiceTypeToLGType(type)); + LG_LOCK(clipboard.writeLock); + LG_LOCK(clipboard.stateLock); + clipboard.localTypeCount = 0; + clearRemoteRequestNL(); + LG_UNLOCK(clipboard.stateLock); + + LG_LOCK_SHARED(clipboard.activeLock); + if (clipboard.active.ops && + !clipboard.active.ops->release(clipboard.active.opaque)) + DEBUG_WARN("Failed to release the remote clipboard"); + LG_UNLOCK_SHARED(clipboard.activeLock); + LG_UNLOCK(clipboard.writeLock); +} + +void lgClipboard_notifyTypes( + const LG_ClipboardData types[], size_t count) +{ + if (!g_params.clipboardToVM) + return; + if (count == 0) + { + lgClipboard_release(); + return; + } + if (!types || count > LG_CLIPBOARD_DATA_NONE) + return; + + for (size_t i = 0; i < count; ++i) + if (!validType(types[i])) + return; + + LG_LOCK(clipboard.writeLock); + LG_LOCK(clipboard.stateLock); + memcpy(clipboard.localTypes, types, count * sizeof(*types)); + clipboard.localTypeCount = count; + clearRemoteRequestNL(); + LG_UNLOCK(clipboard.stateLock); + + LG_LOCK_SHARED(clipboard.activeLock); + if (clipboard.active.ops && + !clipboard.active.ops->notifyTypes( + clipboard.active.opaque, types, count)) + DEBUG_WARN("Failed to publish the local clipboard types"); + LG_UNLOCK_SHARED(clipboard.activeLock); + LG_UNLOCK(clipboard.writeLock); +} + +void lgClipboard_data(LG_ClipboardRequest transfer, + LG_ClipboardData type, const void * data, size_t size) +{ + if (!g_params.clipboardToVM || + transfer == LG_CLIPBOARD_REQUEST_INVALID) + return; + + LG_LOCK(clipboard.writeLock); + LG_LOCK_SHARED(clipboard.activeLock); + LG_LOCK(clipboard.stateLock); + + const bool matches = clipboard.remoteRequest && + clipboard.remoteTransferId == transfer; + if (!matches) + { + LG_UNLOCK(clipboard.stateLock); + LG_UNLOCK_SHARED(clipboard.activeLock); + LG_UNLOCK(clipboard.writeLock); + return; + } + + const LG_ClipboardRequest request = clipboard.remoteRequestId; + const bool provider = clipboard.active.ops && + bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active); + const bool valid = provider && clipboard.remoteRequestType == type && + validType(type) && (!size || data); + clearRemoteRequestNL(); + LG_UNLOCK(clipboard.stateLock); + + const bool result = provider && clipboard.active.ops->data( + clipboard.active.opaque, request, + valid ? type : LG_CLIPBOARD_DATA_NONE, + valid ? data : NULL, valid ? size : 0); + LG_UNLOCK_SHARED(clipboard.activeLock); + LG_UNLOCK(clipboard.writeLock); + + if (!valid) + DEBUG_WARN("Ignoring unexpected local clipboard data"); + else if (!result) + DEBUG_WARN("Failed to send remote clipboard data"); +} + +void lgClipboard_abort(LG_ClipboardRequest transfer) +{ + if (!g_params.clipboardToVM || + transfer == LG_CLIPBOARD_REQUEST_INVALID) + return; + + LG_LOCK(clipboard.writeLock); + LG_LOCK_SHARED(clipboard.activeLock); + LG_LOCK(clipboard.stateLock); + const bool matches = clipboard.remoteRequest && + clipboard.remoteTransferId == transfer; + const bool valid = matches && clipboard.active.ops && + bindingEqual(&clipboard.remoteRequestBinding, &clipboard.active); + const LG_ClipboardRequest request = clipboard.remoteRequestId; + if (matches) + clearRemoteRequestNL(); + LG_UNLOCK(clipboard.stateLock); + + const bool result = !valid || clipboard.active.ops->data( + clipboard.active.opaque, request, + LG_CLIPBOARD_DATA_NONE, NULL, 0); + LG_UNLOCK_SHARED(clipboard.activeLock); + LG_UNLOCK(clipboard.writeLock); + + if (!result) + DEBUG_WARN("Failed to abort remote clipboard data"); +} + +bool lgClipboard_request(LG_ClipboardData type, + LG_ClipboardReplyFn replyFn, void * opaque) +{ + if (!g_params.clipboardToLocal || !validType(type) || + !replyFn || !clipboard.requests) + return false; + + ClipboardRequest * request = malloc(sizeof(*request)); + if (!request) + { + DEBUG_ERROR("Out of memory"); + return false; + } + + LG_LOCK_SHARED(clipboard.activeLock); + if (!clipboard.active.ops) + { + LG_UNLOCK_SHARED(clipboard.activeLock); + free(request); + return false; + } + + LG_LOCK(clipboard.requestLock); + LG_LOCK(clipboard.stateLock); + const bool available = clipboard.localAvailable && + clipboard.remoteNotice && clipboard.remoteType == type; + const uint32_t generation = clipboard.remoteGeneration; + LG_ClipboardRequest id = LG_CLIPBOARD_REQUEST_INVALID; + if (available) + { + id = nextRequestNL(); + *request = (ClipboardRequest) + { + .request = id, + .binding = clipboard.active, + .remoteGeneration = generation, + .type = type, + .replyFn = replyFn, + .opaque = opaque, + }; + } + LG_UNLOCK(clipboard.stateLock); + + const bool queued = available && + ll_push(clipboard.requests, request); + bool result = queued; + if (queued) + result = clipboard.active.ops->request(clipboard.active.opaque, + request->request, type); + LG_UNLOCK(clipboard.requestLock); + LG_UNLOCK_SHARED(clipboard.activeLock); + + if (result) + return true; + if (queued) + { + ClipboardRequest * failed = takeRequest(id); + if (!failed) + return true; + free(failed); + return false; + } + + free(request); + return false; } diff --git a/client/src/clipboard.h b/client/src/clipboard.h index 121a49c9..0a458d7a 100644 --- a/client/src/clipboard.h +++ b/client/src/clipboard.h @@ -18,13 +18,26 @@ * Temple Place, Suite 330, Boston, MA 02111-1307 USA */ -#include -#include "interface/displayserver.h" +#ifndef _H_LG_CLIENT_CLIPBOARD_ +#define _H_LG_CLIENT_CLIPBOARD_ -LG_ClipboardData cb_spiceTypeToLGType(const PSDataType type); -PSDataType cb_lgTypeToSpiceType(const LG_ClipboardData type); +#include "interface/clipboard.h" -void cb_spiceNotice(const PSDataType type); -void cb_spiceData(const PSDataType type, uint8_t * buffer, uint32_t size); -void cb_spiceRelease(void); -void cb_spiceRequest(const PSDataType type); +void lgClipboard_init(void); +void lgClipboard_free(void); +void lgClipboard_setLocalAvailable(bool available); + +void lgClipboard_setFallback(const LG_ClipboardOps * ops, void * opaque); +void lgClipboard_setTransport(const LG_ClipboardOps * ops, void * opaque); +void lgClipboard_dropTransport(void); + +void lgClipboard_release(void); +void lgClipboard_notifyTypes( + const LG_ClipboardData types[], size_t count); +void lgClipboard_data(LG_ClipboardRequest request, + LG_ClipboardData type, const void * data, size_t size); +void lgClipboard_abort(LG_ClipboardRequest request); +bool lgClipboard_request(LG_ClipboardData type, + LG_ClipboardReplyFn replyFn, void * opaque); + +#endif diff --git a/client/src/clipboard_spice.c b/client/src/clipboard_spice.c new file mode 100644 index 00000000..e0f37eb7 --- /dev/null +++ b/client/src/clipboard_spice.c @@ -0,0 +1,491 @@ +/** + * 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_spice.h" + +#include "common/debug.h" +#include "common/locking.h" + +#include + +typedef struct ClipboardEventTarget +{ + const LG_ClipboardEventOps * events; + void * opaque; +} +ClipboardEventTarget; + +typedef struct PendingRequest +{ + bool pending; + LG_ClipboardRequest request; + LG_ClipboardData type; +} +PendingRequest; + +static struct +{ + LG_Lock stateLock; + LG_Lock eventDispatch; + LG_Lock statusDispatch; + + bool available; + uint32_t statusGeneration; + LG_ClipboardStatusFn statusCallback; + void * statusOpaque; + + const LG_ClipboardEventOps * events; + void * eventOpaque; + + bool remoteNotice; + LG_ClipboardData remoteType; + PendingRequest read; + PendingRequest write; + + LG_ClipboardRequest requestSerial; +} +l_spice = +{ + .stateLock = ATOMIC_FLAG_INIT, + .eventDispatch = ATOMIC_FLAG_INIT, + .statusDispatch = ATOMIC_FLAG_INIT, + .remoteType = LG_CLIPBOARD_DATA_NONE, +}; + +static uint32_t nextGeneration(uint32_t generation) +{ + if (++generation == 0) + ++generation; + return generation; +} + +static LG_ClipboardRequest nextRequestNL(void) +{ + if (++l_spice.requestSerial == LG_CLIPBOARD_REQUEST_INVALID) + ++l_spice.requestSerial; + return l_spice.requestSerial; +} + +static bool spiceType(PSDataType source, LG_ClipboardData * type) +{ + switch (source) + { + case SPICE_DATA_TEXT : *type = LG_CLIPBOARD_DATA_TEXT; return true; + case SPICE_DATA_PNG : *type = LG_CLIPBOARD_DATA_PNG ; return true; + case SPICE_DATA_BMP : *type = LG_CLIPBOARD_DATA_BMP ; return true; + case SPICE_DATA_TIFF : *type = LG_CLIPBOARD_DATA_TIFF; return true; + case SPICE_DATA_JPEG : *type = LG_CLIPBOARD_DATA_JPEG; return true; + case SPICE_DATA_NONE : break; + } + + return false; +} + +static bool lgType(LG_ClipboardData source, PSDataType * type) +{ + switch (source) + { + case LG_CLIPBOARD_DATA_TEXT : *type = SPICE_DATA_TEXT; return true; + case LG_CLIPBOARD_DATA_PNG : *type = SPICE_DATA_PNG ; return true; + case LG_CLIPBOARD_DATA_BMP : *type = SPICE_DATA_BMP ; return true; + case LG_CLIPBOARD_DATA_TIFF : *type = SPICE_DATA_TIFF; return true; + case LG_CLIPBOARD_DATA_JPEG : *type = SPICE_DATA_JPEG; return true; + case LG_CLIPBOARD_DATA_NONE : *type = SPICE_DATA_NONE; return true; + } + + return false; +} + +static void spiceSetStatusListener(void * opaque, + LG_ClipboardStatusFn callback, void * callbackOpaque) +{ + (void)opaque; + LG_LOCK(l_spice.statusDispatch); + + LG_LOCK(l_spice.stateLock); + l_spice.statusCallback = callback; + l_spice.statusOpaque = callbackOpaque; + const LG_ClipboardStatus status = + { + .available = l_spice.available, + .generation = l_spice.statusGeneration, + }; + LG_UNLOCK(l_spice.stateLock); + + if (callback) + callback(callbackOpaque, &status); + LG_UNLOCK(l_spice.statusDispatch); +} + +static bool spiceAttach(void * opaque, + const LG_ClipboardEventOps * events, void * eventOpaque) +{ + (void)opaque; + if (!events) + return false; + + LG_LOCK(l_spice.eventDispatch); + LG_LOCK(l_spice.stateLock); + if (!l_spice.available) + { + LG_UNLOCK(l_spice.stateLock); + LG_UNLOCK(l_spice.eventDispatch); + return false; + } + + l_spice.events = events; + l_spice.eventOpaque = eventOpaque; + const bool notice = l_spice.remoteNotice; + const LG_ClipboardData type = l_spice.remoteType; + LG_UNLOCK(l_spice.stateLock); + + if (notice && events->notice) + events->notice(eventOpaque, &type, 1); + LG_UNLOCK(l_spice.eventDispatch); + return true; +} + +static void spiceDetach(void * opaque) +{ + (void)opaque; + LG_LOCK(l_spice.eventDispatch); + LG_LOCK(l_spice.stateLock); + const bool failWrite = l_spice.available && l_spice.write.pending; + l_spice.events = NULL; + l_spice.eventOpaque = NULL; + l_spice.read = (PendingRequest) { 0 }; + l_spice.write = (PendingRequest) { 0 }; + LG_UNLOCK(l_spice.stateLock); + if (failWrite) + purespice_clipboardDataStart(SPICE_DATA_NONE, 0); + LG_UNLOCK(l_spice.eventDispatch); +} + +static bool spiceRelease(void * opaque) +{ + (void)opaque; + LG_LOCK(l_spice.stateLock); + const bool available = l_spice.available; + const bool failWrite = l_spice.write.pending; + l_spice.write = (PendingRequest) { 0 }; + LG_UNLOCK(l_spice.stateLock); + + if (!available) + return false; + + const bool failed = !failWrite || + purespice_clipboardDataStart(SPICE_DATA_NONE, 0); + return purespice_clipboardRelease() && failed; +} + +static bool spiceNotifyTypes(void * opaque, + const LG_ClipboardData types[], size_t count) +{ + (void)opaque; + if (count == 0) + return spiceRelease(NULL); + if (!types || count > LG_CLIPBOARD_DATA_NONE) + return false; + + PSDataType converted[LG_CLIPBOARD_DATA_NONE]; + for (size_t i = 0; i < count; ++i) + if (types[i] == LG_CLIPBOARD_DATA_NONE || + !lgType(types[i], &converted[i])) + return false; + + LG_LOCK(l_spice.stateLock); + const bool available = l_spice.available; + const bool failWrite = l_spice.write.pending; + l_spice.write = (PendingRequest) { 0 }; + LG_UNLOCK(l_spice.stateLock); + + if (!available) + return false; + + const bool failed = !failWrite || + purespice_clipboardDataStart(SPICE_DATA_NONE, 0); + return purespice_clipboardGrab(converted, (int)count) && failed; +} + +static bool spiceData(void * opaque, LG_ClipboardRequest request, + LG_ClipboardData type, const void * data, size_t size) +{ + (void)opaque; + PSDataType converted; + if (request == LG_CLIPBOARD_REQUEST_INVALID || !lgType(type, &converted) || + (type == LG_CLIPBOARD_DATA_NONE && size != 0) || + (size && !data)) + return false; + + LG_LOCK(l_spice.stateLock); + const bool valid = l_spice.available && l_spice.write.pending && + l_spice.write.request == request && + (type == LG_CLIPBOARD_DATA_NONE || l_spice.write.type == type); + if (valid) + l_spice.write = (PendingRequest) { 0 }; + LG_UNLOCK(l_spice.stateLock); + + if (!valid || !purespice_clipboardDataStart(converted, size)) + return false; + + return !size || purespice_clipboardData( + converted, (uint8_t *)data, size); +} + +static bool spiceRequest(void * opaque, LG_ClipboardRequest request, + LG_ClipboardData type) +{ + (void)opaque; + PSDataType converted; + if (request == LG_CLIPBOARD_REQUEST_INVALID || + type == LG_CLIPBOARD_DATA_NONE || !lgType(type, &converted)) + return false; + + LG_LOCK(l_spice.stateLock); + const bool valid = l_spice.available && l_spice.events && + !l_spice.read.pending; + if (valid) + l_spice.read = (PendingRequest) + { + .pending = true, + .request = request, + .type = type, + }; + LG_UNLOCK(l_spice.stateLock); + + if (!valid) + return false; + + if (purespice_clipboardRequest(converted)) + return true; + + LG_LOCK(l_spice.stateLock); + if (!l_spice.read.pending || l_spice.read.request != request) + { + LG_UNLOCK(l_spice.stateLock); + return true; + } + l_spice.read = (PendingRequest) { 0 }; + LG_UNLOCK(l_spice.stateLock); + return false; +} + +const LG_ClipboardOps LGC_Spice = +{ + .name = "SPICE", + .setStatusListener = spiceSetStatusListener, + .attach = spiceAttach, + .detach = spiceDetach, + .release = spiceRelease, + .notifyTypes = spiceNotifyTypes, + .data = spiceData, + .request = spiceRequest, +}; + +void lgcSpice_setAvailable(bool available) +{ + LG_LOCK(l_spice.statusDispatch); + LG_LOCK(l_spice.eventDispatch); + LG_LOCK(l_spice.stateLock); + + const bool changed = l_spice.available != available; + if (changed) + { + l_spice.available = available; + l_spice.statusGeneration = nextGeneration( + l_spice.statusGeneration); + } + if (!available) + { + l_spice.remoteNotice = false; + l_spice.remoteType = LG_CLIPBOARD_DATA_NONE; + l_spice.read = (PendingRequest) { 0 }; + l_spice.write = (PendingRequest) { 0 }; + } + + const LG_ClipboardStatusFn callback = l_spice.statusCallback; + void * callbackOpaque = l_spice.statusOpaque; + const LG_ClipboardStatus status = + { + .available = available, + .generation = l_spice.statusGeneration, + }; + LG_UNLOCK(l_spice.stateLock); + LG_UNLOCK(l_spice.eventDispatch); + + if (changed && callback) + callback(callbackOpaque, &status); + LG_UNLOCK(l_spice.statusDispatch); +} + +void lgcSpice_notice(PSDataType source) +{ + LG_ClipboardData type; + if (!spiceType(source, &type)) + { + if (source != SPICE_DATA_NONE) + DEBUG_ERROR("Invalid SPICE clipboard notice type: %d", source); + lgcSpice_release(); + return; + } + + LG_LOCK(l_spice.eventDispatch); + LG_LOCK(l_spice.stateLock); + const bool failWrite = l_spice.write.pending; + l_spice.remoteNotice = true; + l_spice.remoteType = type; + l_spice.read = (PendingRequest) { 0 }; + l_spice.write = (PendingRequest) { 0 }; + const ClipboardEventTarget target = + { + .events = l_spice.available ? l_spice.events : NULL, + .opaque = l_spice.eventOpaque, + }; + LG_UNLOCK(l_spice.stateLock); + + if (failWrite) + purespice_clipboardDataStart(SPICE_DATA_NONE, 0); + if (target.events && target.events->notice) + target.events->notice(target.opaque, &type, 1); + LG_UNLOCK(l_spice.eventDispatch); +} + +void lgcSpice_data(PSDataType source, uint8_t * buffer, uint32_t size) +{ + LG_ClipboardData type = LG_CLIPBOARD_DATA_NONE; + const bool converted = spiceType(source, &type); + const bool validData = source == SPICE_DATA_NONE || + (converted && (!size || buffer)); + + LG_LOCK(l_spice.eventDispatch); + LG_LOCK(l_spice.stateLock); + if (!l_spice.read.pending) + { + LG_UNLOCK(l_spice.stateLock); + LG_UNLOCK(l_spice.eventDispatch); + DEBUG_WARN("Ignoring unsolicited SPICE clipboard data"); + return; + } + + const PendingRequest request = l_spice.read; + l_spice.read = (PendingRequest) { 0 }; + const ClipboardEventTarget target = + { + .events = l_spice.available ? l_spice.events : NULL, + .opaque = l_spice.eventOpaque, + }; + LG_UNLOCK(l_spice.stateLock); + + if (target.events && target.events->data) + { + if (!validData || (source != SPICE_DATA_NONE && type != request.type)) + { + DEBUG_ERROR("Invalid SPICE clipboard response"); + target.events->data(target.opaque, request.request, + LG_CLIPBOARD_DATA_NONE, NULL, 0); + } + else + { + if (type == LG_CLIPBOARD_DATA_TEXT && size) + { + uint8_t * output = buffer; + for (uint32_t i = 0; i < size; ++i) + if (buffer[i] != '\r') + *output++ = buffer[i]; + size = (uint32_t)(output - buffer); + } + + target.events->data(target.opaque, request.request, + type, buffer, size); + } + } + LG_UNLOCK(l_spice.eventDispatch); +} + +void lgcSpice_release(void) +{ + LG_LOCK(l_spice.eventDispatch); + LG_LOCK(l_spice.stateLock); + l_spice.remoteNotice = false; + l_spice.remoteType = LG_CLIPBOARD_DATA_NONE; + l_spice.read = (PendingRequest) { 0 }; + const ClipboardEventTarget target = + { + .events = l_spice.available ? l_spice.events : NULL, + .opaque = l_spice.eventOpaque, + }; + LG_UNLOCK(l_spice.stateLock); + + if (target.events && target.events->release) + target.events->release(target.opaque); + LG_UNLOCK(l_spice.eventDispatch); +} + +void lgcSpice_request(PSDataType source) +{ + LG_ClipboardData type; + if (!spiceType(source, &type)) + { + DEBUG_ERROR("Invalid SPICE clipboard request type: %d", source); + purespice_clipboardDataStart(SPICE_DATA_NONE, 0); + return; + } + + LG_LOCK(l_spice.eventDispatch); + LG_LOCK(l_spice.stateLock); + const bool busy = l_spice.write.pending; + const ClipboardEventTarget target = + { + .events = l_spice.available && !busy ? + l_spice.events : NULL, + .opaque = l_spice.eventOpaque, + }; + LG_ClipboardRequest request = LG_CLIPBOARD_REQUEST_INVALID; + if (target.events) + { + request = nextRequestNL(); + l_spice.write = (PendingRequest) + { + .pending = true, + .request = request, + .type = type, + }; + } + LG_UNLOCK(l_spice.stateLock); + + const bool accepted = target.events && target.events->request && + target.events->request(target.opaque, request, type); + if (!accepted) + { + bool fail = !busy && request == LG_CLIPBOARD_REQUEST_INVALID; + LG_LOCK(l_spice.stateLock); + if (l_spice.write.pending && l_spice.write.request == request) + { + l_spice.write = (PendingRequest) { 0 }; + fail = true; + } + LG_UNLOCK(l_spice.stateLock); + if (fail) + purespice_clipboardDataStart(SPICE_DATA_NONE, 0); + else if (busy) + DEBUG_WARN("Ignoring overlapping SPICE clipboard request"); + } + LG_UNLOCK(l_spice.eventDispatch); +} diff --git a/client/src/clipboard_spice.h b/client/src/clipboard_spice.h new file mode 100644 index 00000000..1b288e07 --- /dev/null +++ b/client/src/clipboard_spice.h @@ -0,0 +1,37 @@ +/** + * 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_CLIPBOARD_SPICE_ +#define _H_LG_CLIENT_CLIPBOARD_SPICE_ + +#include "interface/clipboard.h" + +#include + +extern const LG_ClipboardOps LGC_Spice; + +void lgcSpice_setAvailable(bool available); + +void lgcSpice_notice(PSDataType type); +void lgcSpice_data(PSDataType type, uint8_t * buffer, uint32_t size); +void lgcSpice_release(void); +void lgcSpice_request(PSDataType type); + +#endif diff --git a/client/src/config.c b/client/src/config.c index 43f24a8e..56db2fe2 100644 --- a/client/src/config.c +++ b/client/src/config.c @@ -755,26 +755,19 @@ bool config_load(int argc, char * argv[]) g_params.minimizeOnFocusLoss = option_get_bool("win", "minimizeOnFocusLoss"); g_params.setGuestRes = option_get_bool("win", "setGuestRes" ); + g_params.clipboardToVM = option_get_bool("spice", "clipboardToVM" ); + g_params.clipboardToLocal = option_get_bool("spice", "clipboardToLocal"); + if ((g_params.useSpice = option_get_bool("spice", "enable"))) { g_params.spiceHost = option_get_string("spice", "host"); g_params.spicePort = option_get_int ("spice", "port"); g_params.useSpiceInput = option_get_bool("spice", "input" ); - g_params.useSpiceClipboard = option_get_bool("spice", "clipboard"); + g_params.useSpiceClipboard = + option_get_bool("spice", "clipboard") && + (g_params.clipboardToVM || g_params.clipboardToLocal); g_params.useSpiceAudio = option_get_bool("spice", "audio" ); - - if (g_params.useSpiceClipboard) - { - g_params.clipboardToVM = option_get_bool("spice", "clipboardToVM" ); - g_params.clipboardToLocal = option_get_bool("spice", "clipboardToLocal"); - g_params.useSpiceClipboard = g_params.clipboardToVM || g_params.clipboardToLocal; - } - else - { - g_params.clipboardToVM = false; - g_params.clipboardToLocal = false; - } } g_params.audioDebug = option_get_bool("audio", "debug"); diff --git a/client/src/main.c b/client/src/main.c index cc549bb5..e430074d 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -63,6 +63,7 @@ #endif #include "keybind.h" #include "clipboard.h" +#include "clipboard_spice.h" #include "kb.h" #include "egl_dynprocs.h" #include "gl_dynprocs.h" @@ -108,11 +109,7 @@ l_cursorRepaint; _Atomic(enum RunState) p_appState = APP_STATE_RUNNING; -struct AppState g_state = -{ - .cbRemoteType = SPICE_DATA_NONE, - .cbWriteType = SPICE_DATA_NONE, -}; +struct AppState g_state = { 0 }; struct CursorState g_cursor; // this structure is initialized in config.c @@ -1031,11 +1028,13 @@ static int renderThread(void * unused) { lgInput_setTransport(NULL, NULL); lgAudio_setTransport(NULL, NULL); + lgClipboard_setTransport(NULL, NULL); } else { lgInput_dropTransport(); lgAudio_dropTransport(); + lgClipboard_dropTransport(); } core_stopCursorThread(); @@ -1087,6 +1086,7 @@ int main_cursorThread(void * unused) { lgInput_dropTransport(); lgAudio_dropTransport(); + lgClipboard_dropTransport(); } app_setState(status == LG_TRANSPORT_DISCONNECTED ? APP_STATE_RESTART : APP_STATE_SHUTDOWN); @@ -1229,6 +1229,7 @@ int main_frameThread(void * unused) { lgInput_dropTransport(); lgAudio_dropTransport(); + lgClipboard_dropTransport(); app_setState(APP_STATE_RESTART); } else if (status == LG_TRANSPORT_END) @@ -1507,6 +1508,8 @@ static void checkUUID(void) g_params.useSpiceInput = false; lgInput_setFallback(NULL, NULL); + lgcSpice_setAvailable(false); + lgClipboard_setFallback(NULL, NULL); atomic_store_explicit(&g_state.spiceClose, true, memory_order_release); purespice_disconnect(); } @@ -1554,6 +1557,10 @@ void spiceReady(void) else DEBUG_WARN("Failed to obtain SPICE server information"); + if (g_params.useSpiceClipboard && + !atomic_load_explicit(&g_state.spiceClose, memory_order_acquire)) + lgClipboard_setFallback(&LGC_Spice, NULL); + if (g_params.useSpiceInput) keybind_inputRegister(); @@ -1721,10 +1728,11 @@ int spiceThread(void * arg) .clipboard = { .enable = g_params.useSpiceClipboard, - .notice = cb_spiceNotice, - .data = cb_spiceData, - .release = cb_spiceRelease, - .request = cb_spiceRequest + .notice = lgcSpice_notice, + .data = lgcSpice_data, + .release = lgcSpice_release, + .request = lgcSpice_request, + .status = lgcSpice_setAvailable, }, .display = { @@ -1784,10 +1792,13 @@ int spiceThread(void * arg) DEBUG_ERROR("failed to process spice messages"); goto end; } + } lgInput_setFallback(NULL, NULL); lgAudio_setFallback(NULL, NULL); + lgcSpice_setAvailable(false); + lgClipboard_setFallback(NULL, NULL); #if ENABLE_AUDIO lgaSpice_setAvailable(false); #endif @@ -1795,8 +1806,10 @@ int spiceThread(void * arg) end: + lgcSpice_setAvailable(false); lgInput_setFallback(NULL, NULL); lgAudio_setFallback(NULL, NULL); + lgClipboard_setFallback(NULL, NULL); #if ENABLE_AUDIO lgaSpice_setAvailable(false); #endif @@ -1911,6 +1924,7 @@ static int lg_run(void) frameTimingInit(); lgInput_init(); lgAudio_init(); + lgClipboard_init(); #ifdef ENABLE_TESTS memset(&l_testCapture, 0, sizeof(l_testCapture)); @@ -2214,9 +2228,9 @@ static int lg_run(void) } g_state.ds->startup(); - g_state.cbAvailable = g_state.ds->cbInit && g_state.ds->cbInit(); - if (g_state.cbAvailable) - g_state.cbRequestList = ll_new(); + const bool clipboardAvailable = + g_state.ds->cbInit && g_state.ds->cbInit(); + lgClipboard_setLocalAvailable(clipboardAvailable); if (g_params.captureOnStart) core_setGrab(true); @@ -2386,6 +2400,13 @@ restart: g_state.transportOps->getAudioOps(g_state.transport, &audioOpaque) : NULL; lgAudio_setTransport(audioOps, audioOpaque); + void * clipboardOpaque = NULL; + const LG_ClipboardOps * clipboardOps = + g_state.transportOps->getClipboardOps ? + g_state.transportOps->getClipboardOps( + g_state.transport, &clipboardOpaque) : NULL; + lgClipboard_setTransport(clipboardOps, clipboardOpaque); + if (inputOps || lgInput_available()) keybind_inputRegister(); checkUUID(); @@ -2404,6 +2425,7 @@ restart: { lgInput_dropTransport(); lgAudio_dropTransport(); + lgClipboard_dropTransport(); atomic_store_explicit( &g_state.lgHostConnected, false, memory_order_release); DEBUG_INFO("Waiting for the host to restart..."); @@ -2426,6 +2448,7 @@ restart: core_stopCursorThread(); lgInput_dropTransport(); lgAudio_dropTransport(); + lgClipboard_dropTransport(); g_state.transportOps->disconnect(g_state.transport); app_setState(APP_STATE_RUNNING); @@ -2474,14 +2497,21 @@ static void lg_shutdown(void) lgInput_dropTransport(); if (g_state.transport && g_state.transportOps->sessionValid(g_state.transport)) + { lgAudio_setTransport(NULL, NULL); + lgClipboard_setTransport(NULL, NULL); + } else + { lgAudio_dropTransport(); + lgClipboard_dropTransport(); + } g_state.transportOps->destroy(&g_state.transport); } lgInput_free(); lgAudio_free(); + lgClipboard_setLocalAvailable(false); if (g_state.frameEvent) { @@ -2503,12 +2533,7 @@ static void lg_shutdown(void) if (g_state.ds) g_state.ds->shutdown(); - - if (g_state.cbRequestList) - { - ll_free(g_state.cbRequestList); - g_state.cbRequestList = NULL; - } + lgClipboard_free(); app_releaseAllKeybinds(); ll_free(g_state.bindings); diff --git a/client/src/main.h b/client/src/main.h index 9b49a858..e75e2201 100644 --- a/client/src/main.h +++ b/client/src/main.h @@ -123,13 +123,6 @@ struct AppState LG_Lock lgrLock; bool useDMA; - bool cbAvailable; - _Atomic(PSDataType) cbRemoteType; - PSDataType cbWriteType; - bool cbChunked; - size_t cbXfer; - struct ll * cbRequestList; - LG_Transport * transport; const LG_TransportOps * transportOps; LG_TransportFeatureFlags transportFeatures; @@ -243,13 +236,6 @@ struct AppParams bool audioSyncVolume; }; -struct CBRequest -{ - PSDataType type; - LG_ClipboardReplyFn replyFn; - void * opaque; -}; - struct KeybindHandle { int sc; diff --git a/repos/PureSpice b/repos/PureSpice index 4f0f5b5b..b66cdfa4 160000 --- a/repos/PureSpice +++ b/repos/PureSpice @@ -1 +1 @@ -Subproject commit 4f0f5b5b9f513eba6b65b8f5e66bc30feb858436 +Subproject commit b66cdfa4d4da0a9205405ab4ab535de653db9952