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

Route clipboard exchange through a provider interface. Connected
transports take priority while SPICE remains the fallback.

Track asynchronous X11 and Wayland transfers by request, preserving
clipboard ownership across provider changes.

Use PureSpice clipboard status notifications for capability and
lifecycle updates.
This commit is contained in:
Geoffrey McRae
2026-08-09 20:10:46 +10:00
parent 0da4bfa8dd
commit 4c53f5b9f8
22 changed files with 2287 additions and 539 deletions

View File

@@ -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)

View File

@@ -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 <limits.h>
#include <stdlib.h>
#include <string.h>
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(&current, 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(&current);
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(&current);
LG_UNLOCK_SHARED(clipboard.activeLock);
if (!deliver || !active || !bindingEqual(&request->binding, &current) ||
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(&current);
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;
}

View File

@@ -18,13 +18,26 @@
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include <purespice.h>
#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

View File

@@ -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 <stdatomic.h>
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);
}

View File

@@ -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 <purespice.h>
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

View File

@@ -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");

View File

@@ -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);

View File

@@ -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;