Files
LookingGlass/client/transports/LGMP/clipboard.c
2026-08-14 10:22:37 +10:00

1680 lines
52 KiB
C

/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "clipboard.h"
#include "common/KVMFRClipboard.h"
#include "common/LGMPConfig.h"
#include "common/debug.h"
#include "common/event.h"
#include "common/locking.h"
#include "common/thread.h"
#include "common/time.h"
#include <stdatomic.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#define CLIPBOARD_PENDING_MAX 32U
#define CLIPBOARD_GRANTS KVMFR_CLIPBOARD_SLOT_COUNT
#define CLIPBOARD_POLL_MS 10U
#define CLIPBOARD_RETRY_MS 1U
#define CLIPBOARD_KEEPALIVE_US UINT64_C(200000)
#define CLIPBOARD_RELEASE_TIMEOUT_US UINT64_C(100000)
struct PendingRecord
{
KVMFRClipboardMessage record;
};
struct Grant
{
KVMFRClipboardSlotHeader * header;
uint8_t * data;
uint32_t token;
bool available;
};
enum HeldPhase
{
HELD_PHASE_NONE,
HELD_PHASE_BEGIN,
HELD_PHASE_CHUNK,
HELD_PHASE_END,
};
struct LGMPClipboard
{
PLGMPClient client;
PLGMPClientQueue queue;
LG_Lock lock;
LG_Lock eventLock;
LG_Lock statusLock;
LGEvent * event;
LGThread * thread;
atomic_bool stop;
bool connected;
bool available;
bool statusValid;
bool claimed;
bool ownerConfirmed;
uint32_t clientID;
uint32_t endpointGeneration;
uint32_t providerGeneration;
uint32_t claimGeneration;
uint32_t publishedClaimGeneration;
uint32_t statusSerial;
uint64_t lastSend;
struct PendingRecord pending[CLIPBOARD_PENDING_MAX];
unsigned pendingHead;
unsigned pendingCount;
struct Grant grants[CLIPBOARD_GRANTS];
bool writeBlocked;
LG_ClipboardRequest writeBlockedRequest;
LG_ClipboardRequest writeTransfer;
uint64_t writeClipboardGeneration;
uint64_t writeSizeHint;
KVMFRClipboardFormat writeFormat;
uint64_t writeOffset;
uint32_t writeSequence;
bool writeBegan;
bool writeWireBegan;
bool held;
bool heldReady;
enum HeldPhase heldPhase;
LGMPMessage heldMessage;
KVMFRClipboardMessage heldRecord;
uint64_t localClipboardGeneration;
KVMFRClipboardFormatFlags localFormats;
uint64_t remoteClipboardGeneration;
KVMFRClipboardFormatFlags remoteFormats;
LG_ClipboardRequest readRequest;
uint64_t readTransfer;
uint64_t readClipboardGeneration;
uint64_t readSizeHint;
uint64_t readOffset;
KVMFRClipboardFormat readFormat;
uint32_t readSequence;
bool readBegan;
uint64_t transferSerial;
const LG_ClipboardEventOps * events;
void * eventOpaque;
LG_ClipboardStatusFn statusCallback;
void * statusOpaque;
};
static LG_ClipboardData fromWireFormat(KVMFRClipboardFormat format)
{
switch (format)
{
case KVMFR_CLIPBOARD_FORMAT_TEXT: return LG_CLIPBOARD_DATA_TEXT;
case KVMFR_CLIPBOARD_FORMAT_PNG : return LG_CLIPBOARD_DATA_PNG;
case KVMFR_CLIPBOARD_FORMAT_BMP : return LG_CLIPBOARD_DATA_BMP;
case KVMFR_CLIPBOARD_FORMAT_TIFF: return LG_CLIPBOARD_DATA_TIFF;
case KVMFR_CLIPBOARD_FORMAT_JPEG: return LG_CLIPBOARD_DATA_JPEG;
default: return LG_CLIPBOARD_DATA_NONE;
}
}
static KVMFRClipboardFormat toWireFormat(LG_ClipboardData format)
{
switch (format)
{
case LG_CLIPBOARD_DATA_TEXT: return KVMFR_CLIPBOARD_FORMAT_TEXT;
case LG_CLIPBOARD_DATA_PNG : return KVMFR_CLIPBOARD_FORMAT_PNG;
case LG_CLIPBOARD_DATA_BMP : return KVMFR_CLIPBOARD_FORMAT_BMP;
case LG_CLIPBOARD_DATA_TIFF: return KVMFR_CLIPBOARD_FORMAT_TIFF;
case LG_CLIPBOARD_DATA_JPEG: return KVMFR_CLIPBOARD_FORMAT_JPEG;
default: return KVMFR_CLIPBOARD_FORMAT_NONE;
}
}
static LG_ClipboardCancelReason fromWireCancel(uint32_t reason)
{
return reason <= LG_CLIPBOARD_CANCEL_INVALID ?
(LG_ClipboardCancelReason)reason : LG_CLIPBOARD_CANCEL_INVALID;
}
static KVMFRClipboardFormatFlags formatMask(
const LG_ClipboardData formats[], size_t count)
{
KVMFRClipboardFormatFlags result = 0;
for (size_t i = 0; i < count; ++i)
result |= kvmfrClipboardFormatFlag(toWireFormat(formats[i]));
return result;
}
static unsigned formatsFromMask(KVMFRClipboardFormatFlags mask,
LG_ClipboardData formats[LG_CLIPBOARD_DATA_NONE])
{
unsigned count = 0;
for (KVMFRClipboardFormat format = KVMFR_CLIPBOARD_FORMAT_TEXT;
format <= KVMFR_CLIPBOARD_FORMAT_JPEG; ++format)
if (mask & kvmfrClipboardFormatFlag(format))
formats[count++] = fromWireFormat(format);
return count;
}
static uint32_t nextNonzero(uint32_t * value)
{
if (++*value == 0)
++*value;
return *value;
}
static uint64_t nextClientTransfer(LGMPClipboard * clipboard)
{
if (++clipboard->transferSerial == 0 ||
kvmfrClipboardTransferFromHelper(clipboard->transferSerial))
clipboard->transferSerial = 1;
return clipboard->transferSerial;
}
static struct PendingRecord * pendingAt(
LGMPClipboard * clipboard, unsigned position)
{
return &clipboard->pending[
(clipboard->pendingHead + position) % CLIPBOARD_PENDING_MAX];
}
static void signalWorker(LGMPClipboard * clipboard)
{
if (clipboard->event)
lgSignalEvent(clipboard->event);
}
static bool enqueueRecordNL(LGMPClipboard * clipboard,
KVMFRClipboardMessage record)
{
if (!clipboard->connected || !clipboard->queue ||
clipboard->pendingCount == CLIPBOARD_PENDING_MAX)
return false;
record.version = KVMFR_CLIPBOARD_VERSION;
record.generation = clipboard->claimGeneration;
pendingAt(clipboard, clipboard->pendingCount++)->record = record;
return true;
}
static bool enqueueTypeNL(LGMPClipboard * clipboard,
KVMFRClipboardMessageType type)
{
KVMFRClipboardMessage record = { 0 };
record.type = type;
return enqueueRecordNL(clipboard, record);
}
static struct Grant * availableGrantNL(LGMPClipboard * clipboard)
{
for (unsigned i = 0; i < CLIPBOARD_GRANTS; ++i)
if (clipboard->grants[i].available)
return &clipboard->grants[i];
return NULL;
}
static bool enqueueDataNL(LGMPClipboard * clipboard,
KVMFRClipboardMessage record, const void * data)
{
if (!clipboard->connected || !clipboard->queue ||
clipboard->pendingCount == CLIPBOARD_PENDING_MAX ||
record.length > KVMFR_CLIPBOARD_DATA_BYTES ||
(record.length && !data))
return false;
struct Grant * grant = availableGrantNL(clipboard);
if (!grant)
return false;
record.version = KVMFR_CLIPBOARD_VERSION;
record.generation = clipboard->claimGeneration;
record.token = 0;
memcpy(grant->header, &record, sizeof(record));
if (record.length)
memcpy(grant->data, data, record.length);
KVMFRClipboardMessage commit = record;
commit.type = KVMFR_CLIPBOARD_MESSAGE_COMMIT;
commit.token = grant->token;
pendingAt(clipboard, clipboard->pendingCount++)->record = commit;
grant->available = false;
return true;
}
static void clearWriteNL(LGMPClipboard * clipboard)
{
clipboard->writeBlocked = false;
clipboard->writeBlockedRequest = LG_CLIPBOARD_REQUEST_INVALID;
clipboard->writeTransfer = LG_CLIPBOARD_REQUEST_INVALID;
clipboard->writeClipboardGeneration = 0;
clipboard->writeSizeHint = 0;
clipboard->writeFormat = KVMFR_CLIPBOARD_FORMAT_NONE;
clipboard->writeOffset = 0;
clipboard->writeSequence = 0;
clipboard->writeBegan = false;
clipboard->writeWireBegan = false;
}
static void clearReadNL(LGMPClipboard * clipboard)
{
clipboard->readRequest = LG_CLIPBOARD_REQUEST_INVALID;
clipboard->readTransfer = 0;
clipboard->readClipboardGeneration = 0;
clipboard->readSizeHint = 0;
clipboard->readOffset = 0;
clipboard->readFormat = KVMFR_CLIPBOARD_FORMAT_NONE;
clipboard->readSequence = 0;
clipboard->readBegan = false;
}
static void clearProtocolNL(LGMPClipboard * clipboard)
{
clipboard->claimed = false;
clipboard->ownerConfirmed = false;
clipboard->publishedClaimGeneration = 0;
clipboard->pendingHead = 0;
clipboard->pendingCount = 0;
clipboard->remoteClipboardGeneration = 0;
clipboard->remoteFormats = 0;
clearWriteNL(clipboard);
clearReadNL(clipboard);
memset(clipboard->grants, 0, sizeof(clipboard->grants));
}
static bool ensureClaimNL(LGMPClipboard * clipboard)
{
if (clipboard->claimed)
return true;
if (!clipboard->connected || !clipboard->available)
return false;
KVMFRClipboardMessage claim = { 0 };
nextNonzero(&clipboard->claimGeneration);
claim.type = KVMFR_CLIPBOARD_MESSAGE_CLAIM;
claim.token = clipboard->endpointGeneration;
if (!enqueueRecordNL(clipboard, claim))
return false;
clipboard->claimed = true;
clipboard->ownerConfirmed = false;
return true;
}
static bool restoreClipboardNL(LGMPClipboard * clipboard)
{
if (!clipboard->events || !ensureClaimNL(clipboard))
return false;
if (!clipboard->localClipboardGeneration)
return true;
KVMFRClipboardMessage record = { 0 };
record.type = clipboard->localFormats ?
KVMFR_CLIPBOARD_MESSAGE_OFFER : KVMFR_CLIPBOARD_MESSAGE_CLEAR;
record.clipboardGeneration = clipboard->localClipboardGeneration;
record.token = clipboard->localFormats;
return enqueueRecordNL(clipboard, record);
}
static void notifyStatus(LGMPClipboard * clipboard)
{
LG_ClipboardStatusFn callback;
void * opaque;
LG_ClipboardStatus status;
LG_LOCK(clipboard->statusLock);
LG_LOCK(clipboard->lock);
callback = clipboard->statusCallback;
opaque = clipboard->statusOpaque;
status = (LG_ClipboardStatus)
{
.available = clipboard->connected && clipboard->available,
.generation = clipboard->providerGeneration,
};
LG_UNLOCK(clipboard->lock);
if (callback)
callback(opaque, &status);
LG_UNLOCK(clipboard->statusLock);
}
static void connectionFailed(LGMPClipboard * clipboard, LGMP_STATUS status)
{
const bool changed = clipboard->connected || clipboard->available;
if (clipboard->connected)
DEBUG_WARN("LGMP clipboard transport failed: %s",
lgmpStatusString(status));
clipboard->connected = false;
clipboard->available = false;
clipboard->statusValid = false;
clearProtocolNL(clipboard);
clipboard->held = false;
clipboard->heldReady = false;
if (changed)
nextNonzero(&clipboard->providerGeneration);
atomic_store_explicit(&clipboard->stop, true, memory_order_release);
}
static bool validStatus(const KVMFRClipboardStatus * status)
{
const uint32_t validFlags = KVMFR_CLIPBOARD_STATUS_AVAILABLE |
KVMFR_CLIPBOARD_STATUS_HAS_OWNER;
if (status->version != KVMFR_CLIPBOARD_VERSION ||
status->flags & ~validFlags || !status->generation ||
status->formats & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL ||
!status->lease ||
status->slotBytes != KVMFR_CLIPBOARD_DATA_BYTES)
return false;
const bool available =
(status->flags & KVMFR_CLIPBOARD_STATUS_AVAILABLE) != 0;
const bool owner =
(status->flags & KVMFR_CLIPBOARD_STATUS_HAS_OWNER) != 0;
if (!available && (status->formats || owner))
return false;
return owner ? status->ownerClientID && status->ownerGeneration :
!status->ownerClientID && !status->ownerGeneration;
}
static bool validateRecord(const LGMPClipboard * clipboard,
const KVMFRClipboardMessage * record, size_t size,
KVMFRClipboardQueueType queueType)
{
if (size < sizeof(*record) ||
record->version != KVMFR_CLIPBOARD_VERSION ||
record->type < KVMFR_CLIPBOARD_MESSAGE_OFFER ||
record->type > KVMFR_CLIPBOARD_MESSAGE_ACK ||
record->generation != clipboard->claimGeneration ||
record->length > KVMFR_CLIPBOARD_DATA_BYTES ||
record->offset > UINT64_MAX - record->length)
return false;
if (record->type == KVMFR_CLIPBOARD_MESSAGE_DATA)
{
const unsigned operations =
!!(record->flags & KVMFR_CLIPBOARD_FLAG_BEGIN) +
!!record->length + !!(record->flags & KVMFR_CLIPBOARD_FLAG_END);
if (!record->transfer ||
kvmfrClipboardTransferFromHelper(record->transfer) ||
!kvmfrClipboardFormatValid(record->format) ||
record->flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END) ||
!operations ||
size != sizeof(*record) + KVMFR_CLIPBOARD_DATA_BYTES ||
queueType != KVMFR_CLIPBOARD_QUEUE_DATA)
return false;
}
else if (queueType != KVMFR_CLIPBOARD_QUEUE_MESSAGE ||
size != sizeof(*record) || record->length || record->flags)
return false;
switch (record->type)
{
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
return record->clipboardGeneration && record->token &&
!(record->token & ~KVMFR_CLIPBOARD_FORMAT_MASK_ALL) &&
!record->transfer && !record->offset && !record->size &&
!record->format && !record->flags && !record->length &&
!record->sequence;
case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
return record->clipboardGeneration && !record->token &&
!record->transfer && !record->offset && !record->size &&
!record->format && !record->flags && !record->length &&
!record->sequence;
case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
return record->clipboardGeneration &&
kvmfrClipboardTransferFromHelper(record->transfer) &&
kvmfrClipboardFormatValid(record->format) &&
!record->offset && !record->size && !record->flags &&
!record->token && !record->length && !record->sequence;
case KVMFR_CLIPBOARD_MESSAGE_DATA:
{
const uint64_t end = record->offset + record->length;
if (!record->clipboardGeneration || record->token ||
((record->flags & KVMFR_CLIPBOARD_FLAG_BEGIN) &&
(record->offset || record->sequence)) ||
((record->flags & KVMFR_CLIPBOARD_FLAG_END) &&
record->size != end))
return false;
if ((record->flags & KVMFR_CLIPBOARD_FLAG_BEGIN) &&
record->size != KVMFR_CLIPBOARD_SIZE_UNKNOWN &&
end > record->size)
return false;
return true;
}
case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
return record->transfer && !record->offset && !record->size &&
(!record->format || kvmfrClipboardFormatValid(record->format)) &&
!record->flags && !record->length &&
!record->sequence;
default:
return false;
}
}
static bool validateReadChunkNL(const LGMPClipboard * clipboard,
const KVMFRClipboardMessage * record)
{
if (clipboard->readRequest == LG_CLIPBOARD_REQUEST_INVALID ||
record->transfer != clipboard->readTransfer ||
record->clipboardGeneration !=
clipboard->readClipboardGeneration ||
record->format != clipboard->readFormat ||
record->offset != clipboard->readOffset ||
record->sequence != clipboard->readSequence)
return false;
const uint64_t end = record->offset + record->length;
if (!record->length &&
!(record->flags & (KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END)))
return false;
if (!clipboard->readBegan)
{
if (record->offset || record->sequence ||
!(record->flags & KVMFR_CLIPBOARD_FLAG_BEGIN))
return false;
}
else if (record->flags & KVMFR_CLIPBOARD_FLAG_BEGIN)
return false;
else if (!(record->flags & KVMFR_CLIPBOARD_FLAG_END) &&
record->size != KVMFR_CLIPBOARD_SIZE_UNKNOWN)
return false;
const uint64_t hint = clipboard->readBegan ?
clipboard->readSizeHint : record->size;
if (hint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && end > hint)
return false;
if (record->flags & KVMFR_CLIPBOARD_FLAG_END)
return record->size == end &&
(hint == KVMFR_CLIPBOARD_SIZE_UNKNOWN || hint == end);
return true;
}
static void advanceReadNL(LGMPClipboard * clipboard,
const KVMFRClipboardMessage * record)
{
if (!clipboard->readBegan)
{
clipboard->readBegan = true;
clipboard->readSizeHint = record->size;
}
clipboard->readOffset += record->length;
++clipboard->readSequence;
if (record->flags & KVMFR_CLIPBOARD_FLAG_END)
clearReadNL(clipboard);
}
static void dispatchOffer(LGMPClipboard * clipboard,
KVMFRClipboardFormatFlags mask)
{
LG_ClipboardData formats[LG_CLIPBOARD_DATA_NONE];
const unsigned count = formatsFromMask(mask, formats);
LG_LOCK(clipboard->eventLock);
LG_LOCK(clipboard->lock);
const LG_ClipboardEventOps * events = clipboard->events;
void * opaque = clipboard->eventOpaque;
LG_UNLOCK(clipboard->lock);
if (events && events->notice)
events->notice(opaque, formats, count);
LG_UNLOCK(clipboard->eventLock);
}
static void dispatchRelease(LGMPClipboard * clipboard)
{
LG_LOCK(clipboard->eventLock);
LG_LOCK(clipboard->lock);
const LG_ClipboardEventOps * events = clipboard->events;
void * opaque = clipboard->eventOpaque;
LG_UNLOCK(clipboard->lock);
if (events && events->release)
events->release(opaque);
LG_UNLOCK(clipboard->eventLock);
}
static bool dispatchRequest(LGMPClipboard * clipboard,
const KVMFRClipboardMessage * record)
{
LG_LOCK(clipboard->eventLock);
LG_LOCK(clipboard->lock);
const LG_ClipboardEventOps * events = clipboard->events;
void * opaque = clipboard->eventOpaque;
LG_UNLOCK(clipboard->lock);
const bool result = events && events->request && events->request(opaque,
record->transfer, fromWireFormat(record->format));
LG_UNLOCK(clipboard->eventLock);
return result;
}
static void dispatchCancel(LGMPClipboard * clipboard,
const KVMFRClipboardMessage * record)
{
LG_LOCK(clipboard->eventLock);
LG_LOCK(clipboard->lock);
const LG_ClipboardEventOps * events = clipboard->events;
void * opaque = clipboard->eventOpaque;
LG_UNLOCK(clipboard->lock);
if (events)
{
const LG_ClipboardCancelReason reason = fromWireCancel(record->token);
if (kvmfrClipboardTransferFromHelper(record->transfer))
{
if (events->requestCancel)
events->requestCancel(opaque, record->transfer, reason);
}
else if (events->dataCancel)
events->dataCancel(opaque, record->transfer, reason);
}
LG_UNLOCK(clipboard->eventLock);
}
static void dispatchDataCancel(LGMPClipboard * clipboard,
LG_ClipboardRequest request, uint32_t reason)
{
LG_LOCK(clipboard->eventLock);
LG_LOCK(clipboard->lock);
const LG_ClipboardEventOps * events = clipboard->events;
void * opaque = clipboard->eventOpaque;
LG_UNLOCK(clipboard->lock);
if (events && events->dataCancel)
events->dataCancel(
opaque, request, fromWireCancel(reason));
LG_UNLOCK(clipboard->eventLock);
}
static LG_ClipboardResult dispatchData(LGMPClipboard * clipboard,
const KVMFRClipboardMessage * record, const uint8_t * data,
enum HeldPhase phase)
{
LG_LOCK(clipboard->eventLock);
LG_LOCK(clipboard->lock);
const LG_ClipboardEventOps * events = clipboard->events;
void * opaque = clipboard->eventOpaque;
LG_UNLOCK(clipboard->lock);
LG_ClipboardResult result = LG_CLIPBOARD_RESULT_FAILED;
if (events)
{
switch (phase)
{
case HELD_PHASE_BEGIN:
if (events->dataBegin)
result = events->dataBegin(opaque, record->transfer,
fromWireFormat(record->format), record->size);
break;
case HELD_PHASE_CHUNK:
if (events->dataChunk)
result = events->dataChunk(opaque, record->transfer,
record->offset, data, record->length);
break;
case HELD_PHASE_END:
if (events->dataEnd)
result = events->dataEnd(
opaque, record->transfer, record->size);
break;
case HELD_PHASE_NONE:
break;
}
}
LG_UNLOCK(clipboard->eventLock);
return result;
}
static void dispatchReady(LGMPClipboard * clipboard,
LG_ClipboardRequest request)
{
LG_LOCK(clipboard->eventLock);
LG_LOCK(clipboard->lock);
const LG_ClipboardEventOps * events = clipboard->events;
void * opaque = clipboard->eventOpaque;
LG_UNLOCK(clipboard->lock);
if (events && events->dataReady)
events->dataReady(opaque, request);
LG_UNLOCK(clipboard->eventLock);
}
static void applyStatusNL(LGMPClipboard * clipboard,
const KVMFRClipboardStatus * status, uint32_t serial,
bool * changed)
{
const bool wasValid = clipboard->statusValid;
const bool wasAvailable = clipboard->available;
const uint32_t oldGeneration = clipboard->endpointGeneration;
clipboard->statusValid = true;
clipboard->statusSerial = serial;
const bool endpointAvailable =
(status->flags & KVMFR_CLIPBOARD_STATUS_AVAILABLE) != 0;
clipboard->endpointGeneration = status->generation;
const bool owned = (status->flags & KVMFR_CLIPBOARD_STATUS_HAS_OWNER) &&
clipboard->claimed && status->ownerClientID == clipboard->clientID &&
status->ownerGeneration == clipboard->claimGeneration;
const bool ownedByOther =
(status->flags & KVMFR_CLIPBOARD_STATUS_HAS_OWNER) && !owned;
clipboard->available = endpointAvailable && !ownedByOther;
bool restore = false;
if (!endpointAvailable || oldGeneration != status->generation ||
ownedByOther)
{
clearProtocolNL(clipboard);
restore = endpointAvailable && !ownedByOther;
}
else if (status->flags & KVMFR_CLIPBOARD_STATUS_HAS_OWNER)
clipboard->ownerConfirmed = owned;
else
{
if (clipboard->claimed && clipboard->ownerConfirmed)
{
clearProtocolNL(clipboard);
restore = endpointAvailable;
}
clipboard->ownerConfirmed = false;
}
if (restore && !restoreClipboardNL(clipboard))
clearProtocolNL(clipboard);
*changed = !wasValid || wasAvailable != clipboard->available ||
oldGeneration != clipboard->endpointGeneration;
if (*changed)
nextNonzero(&clipboard->providerGeneration);
}
static bool flushPendingNL(LGMPClipboard * clipboard)
{
while (clipboard->pendingCount)
{
KVMFRClipboardMessage * record =
&pendingAt(clipboard, 0)->record;
const LGMP_STATUS status = lgmpClientTrySendData(
clipboard->queue, record, sizeof(*record), NULL);
if (status == LGMP_ERR_QUEUE_BUSY || status == LGMP_ERR_QUEUE_FULL)
return true;
if (status != LGMP_OK)
{
connectionFailed(clipboard, status);
return false;
}
if (record->type == KVMFR_CLIPBOARD_MESSAGE_CLAIM)
clipboard->publishedClaimGeneration = record->generation;
else if (record->type == KVMFR_CLIPBOARD_MESSAGE_RELEASE &&
clipboard->publishedClaimGeneration == record->generation)
clipboard->publishedClaimGeneration = 0;
clipboard->lastSend = microtime();
clipboard->pendingHead =
(clipboard->pendingHead + 1) % CLIPBOARD_PENDING_MAX;
--clipboard->pendingCount;
}
return true;
}
static bool processHeld(LGMPClipboard * clipboard)
{
for (;;)
{
KVMFRClipboardMessage record;
const uint8_t * data;
enum HeldPhase phase;
LG_LOCK(clipboard->lock);
if (!clipboard->held || !clipboard->heldReady)
{
LG_UNLOCK(clipboard->lock);
return true;
}
record = clipboard->heldRecord;
data = record.length ?
(const uint8_t *)clipboard->heldMessage.mem + sizeof(record) : NULL;
phase = clipboard->heldPhase;
clipboard->heldReady = false;
LG_UNLOCK(clipboard->lock);
const LG_ClipboardResult result =
dispatchData(clipboard, &record, data, phase);
LG_LOCK(clipboard->lock);
if (!clipboard->held)
{
LG_UNLOCK(clipboard->lock);
return true;
}
if (result == LG_CLIPBOARD_RESULT_BLOCKED)
{
LG_UNLOCK(clipboard->lock);
return true;
}
if (result == LG_CLIPBOARD_RESULT_ACCEPTED)
{
switch (phase)
{
case HELD_PHASE_BEGIN:
clipboard->heldPhase = record.length ? HELD_PHASE_CHUNK :
(record.flags & KVMFR_CLIPBOARD_FLAG_END) ?
HELD_PHASE_END : HELD_PHASE_NONE;
break;
case HELD_PHASE_CHUNK:
clipboard->heldPhase =
(record.flags & KVMFR_CLIPBOARD_FLAG_END) ?
HELD_PHASE_END : HELD_PHASE_NONE;
break;
case HELD_PHASE_END:
clipboard->heldPhase = HELD_PHASE_NONE;
break;
case HELD_PHASE_NONE:
break;
}
}
else
clipboard->heldPhase = HELD_PHASE_NONE;
if (clipboard->heldPhase != HELD_PHASE_NONE)
{
clipboard->heldReady = true;
LG_UNLOCK(clipboard->lock);
continue;
}
const bool matchingRead =
clipboard->readRequest == record.transfer &&
clipboard->readTransfer;
if (result == LG_CLIPBOARD_RESULT_FAILED && matchingRead)
{
KVMFRClipboardMessage cancel = { 0 };
cancel.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL;
cancel.clipboardGeneration = clipboard->readClipboardGeneration;
cancel.transfer = clipboard->readTransfer;
cancel.format = clipboard->readFormat;
cancel.token = LG_CLIPBOARD_CANCEL_INVALID;
enqueueRecordNL(clipboard, cancel);
}
if (matchingRead && result == LG_CLIPBOARD_RESULT_ACCEPTED)
advanceReadNL(clipboard, &record);
else if (matchingRead)
clearReadNL(clipboard);
const LGMP_STATUS done = lgmpClientMessageDone(clipboard->queue);
clipboard->held = false;
memset(&clipboard->heldMessage, 0, sizeof(clipboard->heldMessage));
memset(&clipboard->heldRecord, 0, sizeof(clipboard->heldRecord));
if (done != LGMP_OK)
{
connectionFailed(clipboard, done);
LG_UNLOCK(clipboard->lock);
return false;
}
LG_UNLOCK(clipboard->lock);
return true;
}
}
static bool processMessage(LGMPClipboard * clipboard)
{
LG_LOCK(clipboard->lock);
if (!clipboard->connected || !clipboard->queue || clipboard->held)
{
LG_UNLOCK(clipboard->lock);
return true;
}
LGMPMessage message;
LGMP_STATUS status = lgmpClientProcess(clipboard->queue, &message);
if (status == LGMP_ERR_QUEUE_EMPTY)
{
LG_UNLOCK(clipboard->lock);
return true;
}
if (status != LGMP_OK)
{
connectionFailed(clipboard, status);
LG_UNLOCK(clipboard->lock);
return false;
}
const KVMFRClipboardQueueType type =
KVMFR_CLIPBOARD_QUEUE_TYPE(message.udata);
if (type == KVMFR_CLIPBOARD_QUEUE_STATUS)
{
KVMFRClipboardStatus snapshot = { 0 };
const bool valid = message.size == sizeof(snapshot);
if (valid)
memcpy(&snapshot, message.mem, sizeof(snapshot));
status = lgmpClientMessageDone(clipboard->queue);
bool changed = false;
if (status == LGMP_OK && valid && validStatus(&snapshot))
{
const uint32_t serial = KVMFR_CLIPBOARD_QUEUE_SERIAL(message.udata);
if (!clipboard->statusValid ||
(int32_t)(serial - clipboard->statusSerial) > 0)
applyStatusNL(clipboard, &snapshot, serial, &changed);
}
else if (status == LGMP_OK)
DEBUG_WARN("Ignoring invalid LGMP clipboard status");
if (status != LGMP_OK)
connectionFailed(clipboard, status);
LG_UNLOCK(clipboard->lock);
if (changed)
notifyStatus(clipboard);
return status == LGMP_OK;
}
if (type == KVMFR_CLIPBOARD_QUEUE_GRANT)
{
KVMFRClipboardSlotHeader * header = message.mem;
const uint32_t token = KVMFR_CLIPBOARD_QUEUE_SERIAL(message.udata);
const bool valid = message.size ==
sizeof(*header) + KVMFR_CLIPBOARD_DATA_BYTES &&
header->version == KVMFR_CLIPBOARD_VERSION &&
header->type == KVMFR_CLIPBOARD_MESSAGE_GRANT &&
header->generation == clipboard->claimGeneration &&
header->size == KVMFR_CLIPBOARD_DATA_BYTES &&
header->token == token && token > 0 &&
token <= KVMFR_CLIPBOARD_SLOT_COUNT &&
!header->sequence && !header->clipboardGeneration &&
!header->transfer && !header->offset && !header->format &&
!header->flags && !header->length;
bool stored = false;
if (valid)
{
struct Grant * grant = &clipboard->grants[token - 1];
if (!grant->available)
{
grant->header = header;
grant->data = (uint8_t *)(header + 1);
grant->token = token;
grant->available = true;
stored = true;
}
}
status = lgmpClientMessageDone(clipboard->queue);
const bool ready = stored && clipboard->writeBlocked;
const LG_ClipboardRequest request = clipboard->writeBlockedRequest;
if (ready)
clipboard->writeBlocked = false;
if (status != LGMP_OK)
connectionFailed(clipboard, status);
LG_UNLOCK(clipboard->lock);
if (!stored)
DEBUG_WARN("Ignoring invalid LGMP clipboard grant");
if (ready)
dispatchReady(clipboard, request);
return status == LGMP_OK;
}
if ((type != KVMFR_CLIPBOARD_QUEUE_MESSAGE &&
type != KVMFR_CLIPBOARD_QUEUE_DATA) ||
message.size < sizeof(KVMFRClipboardMessage))
{
status = lgmpClientMessageDone(clipboard->queue);
if (status != LGMP_OK)
connectionFailed(clipboard, status);
LG_UNLOCK(clipboard->lock);
DEBUG_WARN("Ignoring invalid LGMP clipboard message");
return status == LGMP_OK;
}
KVMFRClipboardMessage record;
memcpy(&record, message.mem, sizeof(record));
const bool valid = validateRecord(
clipboard, &record, message.size, type);
if (!valid)
{
status = lgmpClientMessageDone(clipboard->queue);
if (status != LGMP_OK)
connectionFailed(clipboard, status);
LG_UNLOCK(clipboard->lock);
DEBUG_WARN("Ignoring malformed LGMP clipboard record");
return status == LGMP_OK;
}
if (record.type == KVMFR_CLIPBOARD_MESSAGE_DATA)
{
if (!validateReadChunkNL(clipboard, &record))
{
status = lgmpClientMessageDone(clipboard->queue);
if (status != LGMP_OK)
connectionFailed(clipboard, status);
LG_UNLOCK(clipboard->lock);
DEBUG_WARN("Ignoring stale or malformed LGMP clipboard data");
return status == LGMP_OK;
}
record.transfer = clipboard->readRequest;
clipboard->held = true;
clipboard->heldReady = true;
clipboard->heldMessage = message;
clipboard->heldRecord = record;
clipboard->heldPhase =
(record.flags & KVMFR_CLIPBOARD_FLAG_BEGIN) ? HELD_PHASE_BEGIN :
record.length ? HELD_PHASE_CHUNK : HELD_PHASE_END;
LG_UNLOCK(clipboard->lock);
return processHeld(clipboard);
}
status = lgmpClientMessageDone(clipboard->queue);
if (status != LGMP_OK)
{
connectionFailed(clipboard, status);
LG_UNLOCK(clipboard->lock);
return false;
}
bool dispatch = true;
LG_ClipboardRequest cancelledRead = LG_CLIPBOARD_REQUEST_INVALID;
switch (record.type)
{
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
clipboard->remoteClipboardGeneration = record.clipboardGeneration;
clipboard->remoteFormats = record.token;
clearReadNL(clipboard);
break;
case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
clipboard->remoteClipboardGeneration = record.clipboardGeneration;
clipboard->remoteFormats = 0;
clearReadNL(clipboard);
break;
case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
if (clipboard->writeTransfer != LG_CLIPBOARD_REQUEST_INVALID ||
record.clipboardGeneration !=
clipboard->localClipboardGeneration ||
!(clipboard->localFormats &
kvmfrClipboardFormatFlag(record.format)))
{
dispatch = false;
break;
}
clearWriteNL(clipboard);
clipboard->writeTransfer = record.transfer;
clipboard->writeClipboardGeneration = record.clipboardGeneration;
clipboard->writeFormat = record.format;
break;
case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
{
const bool write = clipboard->writeTransfer == record.transfer;
const bool read = clipboard->readTransfer == record.transfer;
if (!write && !read)
{
dispatch = false;
break;
}
const uint64_t clipboardGeneration = write ?
clipboard->writeClipboardGeneration :
clipboard->readClipboardGeneration;
const KVMFRClipboardFormat format = write ?
clipboard->writeFormat : clipboard->readFormat;
if ((record.clipboardGeneration &&
record.clipboardGeneration != clipboardGeneration) ||
(record.format && record.format != format))
{
dispatch = false;
break;
}
if (write)
clearWriteNL(clipboard);
if (read)
{
cancelledRead = clipboard->readRequest;
clearReadNL(clipboard);
}
break;
}
}
LG_UNLOCK(clipboard->lock);
if (!dispatch)
{
DEBUG_WARN("Ignoring stale LGMP clipboard control record");
return true;
}
switch (record.type)
{
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
dispatchOffer(clipboard, record.token);
break;
case KVMFR_CLIPBOARD_MESSAGE_CLEAR:
dispatchRelease(clipboard);
break;
case KVMFR_CLIPBOARD_MESSAGE_REQUEST:
if (!dispatchRequest(clipboard, &record))
{
LG_LOCK(clipboard->lock);
KVMFRClipboardMessage cancel = { 0 };
cancel.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL;
cancel.clipboardGeneration = record.clipboardGeneration;
cancel.transfer = record.transfer;
cancel.format = record.format;
cancel.token = LG_CLIPBOARD_CANCEL_UNAVAILABLE;
enqueueRecordNL(clipboard, cancel);
clearWriteNL(clipboard);
LG_UNLOCK(clipboard->lock);
signalWorker(clipboard);
}
break;
case KVMFR_CLIPBOARD_MESSAGE_CANCEL:
if (cancelledRead != LG_CLIPBOARD_REQUEST_INVALID)
dispatchDataCancel(clipboard, cancelledRead, record.token);
else
dispatchCancel(clipboard, &record);
break;
}
return true;
}
static int clipboardThread(void * opaque)
{
LGMPClipboard * clipboard = opaque;
while (!atomic_load_explicit(&clipboard->stop, memory_order_acquire))
{
if (!processHeld(clipboard) || !processMessage(clipboard))
break;
LG_ClipboardRequest readyRequest = LG_CLIPBOARD_REQUEST_INVALID;
LG_LOCK(clipboard->lock);
flushPendingNL(clipboard);
const uint64_t now = microtime();
if (clipboard->connected && clipboard->claimed &&
!clipboard->pendingCount &&
now - clipboard->lastSend >= CLIPBOARD_KEEPALIVE_US)
enqueueTypeNL(clipboard, KVMFR_CLIPBOARD_MESSAGE_KEEPALIVE);
if (clipboard->writeBlocked &&
clipboard->pendingCount < CLIPBOARD_PENDING_MAX &&
availableGrantNL(clipboard))
{
readyRequest = clipboard->writeBlockedRequest;
clipboard->writeBlocked = false;
}
const bool retry = clipboard->pendingCount != 0;
LG_UNLOCK(clipboard->lock);
if (readyRequest != LG_CLIPBOARD_REQUEST_INVALID)
dispatchReady(clipboard, readyRequest);
if (atomic_load_explicit(&clipboard->stop, memory_order_acquire))
break;
lgWaitEvent(clipboard->event,
retry ? CLIPBOARD_RETRY_MS : CLIPBOARD_POLL_MS);
}
notifyStatus(clipboard);
return 0;
}
bool lgmpClipboard_create(PLGMPClient client, LGMPClipboard ** result)
{
if (!client || !result)
return false;
LGMPClipboard * clipboard = calloc(1, sizeof(*clipboard));
if (!clipboard)
return false;
clipboard->client = client;
clipboard->providerGeneration = 1;
LG_LOCK_INIT(clipboard->lock);
LG_LOCK_INIT(clipboard->eventLock);
LG_LOCK_INIT(clipboard->statusLock);
atomic_init(&clipboard->stop, false);
*result = clipboard;
return true;
}
void lgmpClipboard_destroy(LGMPClipboard ** clipboard)
{
if (!clipboard || !*clipboard)
return;
lgmpClipboard_disconnect(*clipboard);
LG_LOCK_FREE((*clipboard)->statusLock);
LG_LOCK_FREE((*clipboard)->eventLock);
LG_LOCK_FREE((*clipboard)->lock);
free(*clipboard);
*clipboard = NULL;
}
bool lgmpClipboard_connect(LGMPClipboard * clipboard, uint32_t clientID)
{
if (!clipboard || !clientID)
return false;
LG_LOCK(clipboard->lock);
if (clipboard->connected)
{
const bool same = clipboard->clientID == clientID;
LG_UNLOCK(clipboard->lock);
return same;
}
if (clipboard->thread || clipboard->queue)
{
LG_UNLOCK(clipboard->lock);
return false;
}
LGMP_STATUS status = lgmpClientSubscribe(
clipboard->client, LGMP_Q_CLIPBOARD, &clipboard->queue);
if (status != LGMP_OK)
{
LG_UNLOCK(clipboard->lock);
DEBUG_WARN("Failed to subscribe to LGMP clipboard queue: %s",
lgmpStatusString(status));
return false;
}
clipboard->event = lgCreateEvent(true, 0);
if (!clipboard->event)
{
lgmpClientUnsubscribe(&clipboard->queue);
LG_UNLOCK(clipboard->lock);
return false;
}
clipboard->connected = true;
clipboard->available = false;
clipboard->statusValid = false;
clipboard->claimed = false;
clipboard->ownerConfirmed = false;
clipboard->clientID = clientID;
clipboard->endpointGeneration = 0;
clipboard->statusSerial = 0;
clipboard->pendingHead = 0;
clipboard->pendingCount = 0;
clipboard->held = false;
clipboard->heldReady = false;
clearWriteNL(clipboard);
clipboard->remoteFormats = 0;
clipboard->remoteClipboardGeneration = 0;
clearReadNL(clipboard);
clipboard->publishedClaimGeneration = 0;
memset(clipboard->grants, 0, sizeof(clipboard->grants));
atomic_store_explicit(&clipboard->stop, false, memory_order_release);
LGThread * thread;
if (lgCreateThread("lgmpClipboard", clipboardThread,
clipboard, &thread))
{
clipboard->thread = thread;
LG_UNLOCK(clipboard->lock);
return true;
}
clipboard->connected = false;
LGEvent * event = clipboard->event;
PLGMPClientQueue queue = clipboard->queue;
clipboard->event = NULL;
clipboard->queue = NULL;
LG_UNLOCK(clipboard->lock);
lgmpClientUnsubscribe(&queue);
lgFreeEvent(event);
return false;
}
static void releaseOnDisconnect(LGMPClipboard * clipboard)
{
if (!clipboard->queue || !clipboard->publishedClaimGeneration)
return;
const KVMFRClipboardMessage release =
{
.version = KVMFR_CLIPBOARD_VERSION,
.type = KVMFR_CLIPBOARD_MESSAGE_RELEASE,
.generation = clipboard->publishedClaimGeneration,
};
const uint64_t deadline =
microtime() + CLIPBOARD_RELEASE_TIMEOUT_US;
uint32_t serial = 0;
LGMP_STATUS status;
do
{
status = lgmpClientTrySendData(clipboard->queue,
&release, sizeof(release), &serial);
if (status == LGMP_OK)
break;
if (status != LGMP_ERR_QUEUE_BUSY && status != LGMP_ERR_QUEUE_FULL)
return;
if (clipboard->event)
lgWaitEvent(clipboard->event, CLIPBOARD_RETRY_MS);
}
while (microtime() < deadline);
if (status != LGMP_OK)
{
DEBUG_WARN("Timed out releasing LGMP clipboard ownership");
return;
}
do
{
uint32_t processed;
status = lgmpClientGetSerial(clipboard->queue, &processed);
if (status != LGMP_OK)
return;
if ((int32_t)(processed - serial) >= 0)
return;
if (clipboard->event)
lgWaitEvent(clipboard->event, CLIPBOARD_RETRY_MS);
}
while (microtime() < deadline);
DEBUG_WARN("Timed out waiting for LGMP clipboard release");
}
void lgmpClipboard_disconnect(LGMPClipboard * clipboard)
{
if (!clipboard)
return;
LG_LOCK(clipboard->lock);
if (!clipboard->thread && !clipboard->queue)
{
LG_UNLOCK(clipboard->lock);
return;
}
if (clipboard->connected || clipboard->available)
nextNonzero(&clipboard->providerGeneration);
clipboard->connected = false;
clipboard->available = false;
clipboard->claimed = false;
atomic_store_explicit(&clipboard->stop, true, memory_order_release);
LGThread * thread = clipboard->thread;
LGEvent * event = clipboard->event;
LG_UNLOCK(clipboard->lock);
if (event)
lgSignalEvent(event);
if (thread)
lgJoinThread(thread, NULL);
LG_LOCK(clipboard->lock);
if (clipboard->held && clipboard->queue)
lgmpClientMessageDone(clipboard->queue);
releaseOnDisconnect(clipboard);
PLGMPClientQueue queue = clipboard->queue;
clipboard->queue = NULL;
clipboard->thread = NULL;
clipboard->event = NULL;
clipboard->held = false;
clearProtocolNL(clipboard);
LG_UNLOCK(clipboard->lock);
if (queue)
lgmpClientUnsubscribe(&queue);
if (event)
lgFreeEvent(event);
}
static void setStatusListener(void * opaque,
LG_ClipboardStatusFn callback, void * callbackOpaque)
{
LGMPClipboard * clipboard = opaque;
LG_LOCK(clipboard->statusLock);
LG_LOCK(clipboard->lock);
clipboard->statusCallback = callback;
clipboard->statusOpaque = callbackOpaque;
LG_ClipboardStatus status =
{
.available = clipboard->connected && clipboard->available,
.generation = clipboard->providerGeneration,
};
LG_UNLOCK(clipboard->lock);
if (callback)
callback(callbackOpaque, &status);
LG_UNLOCK(clipboard->statusLock);
}
static bool attach(void * opaque, const LG_ClipboardEventOps * events,
void * eventOpaque)
{
LGMPClipboard * clipboard = opaque;
KVMFRClipboardFormatFlags formats;
LG_ClipboardData replay[LG_CLIPBOARD_DATA_NONE];
bool available;
LG_LOCK(clipboard->eventLock);
LG_LOCK(clipboard->lock);
available = clipboard->connected && clipboard->available && events;
if (available)
{
clipboard->events = events;
clipboard->eventOpaque = eventOpaque;
if (!ensureClaimNL(clipboard))
{
clipboard->events = NULL;
clipboard->eventOpaque = NULL;
available = false;
}
}
formats = clipboard->remoteFormats;
LG_UNLOCK(clipboard->lock);
const unsigned replayCount = available ?
formatsFromMask(formats, replay) : 0;
if (replayCount && events->notice)
events->notice(eventOpaque, replay, replayCount);
LG_UNLOCK(clipboard->eventLock);
if (available)
signalWorker(clipboard);
return available;
}
static void detach(void * opaque)
{
LGMPClipboard * clipboard = opaque;
bool wake = false;
LG_LOCK(clipboard->eventLock);
LG_LOCK(clipboard->lock);
clipboard->events = NULL;
clipboard->eventOpaque = NULL;
if (clipboard->claimed)
{
wake = enqueueTypeNL(
clipboard, KVMFR_CLIPBOARD_MESSAGE_RELEASE);
clipboard->claimed = false;
}
LG_UNLOCK(clipboard->lock);
LG_UNLOCK(clipboard->eventLock);
if (wake)
signalWorker(clipboard);
}
static bool releaseClipboard(void * opaque)
{
LGMPClipboard * clipboard = opaque;
LG_LOCK(clipboard->lock);
KVMFRClipboardMessage clear = { 0 };
clear.type = KVMFR_CLIPBOARD_MESSAGE_CLEAR;
clear.clipboardGeneration = clipboard->localClipboardGeneration + 1;
if (!clear.clipboardGeneration)
++clear.clipboardGeneration;
const unsigned required = clipboard->claimed ? 1U : 2U;
const bool result =
clipboard->pendingCount <= CLIPBOARD_PENDING_MAX - required &&
ensureClaimNL(clipboard) && enqueueRecordNL(clipboard, clear);
if (result)
{
clipboard->localClipboardGeneration = clear.clipboardGeneration;
clipboard->localFormats = 0;
}
LG_UNLOCK(clipboard->lock);
if (result)
signalWorker(clipboard);
return result;
}
static bool notifyTypes(void * opaque, const LG_ClipboardData types[],
size_t count)
{
LGMPClipboard * clipboard = opaque;
if (!types || !count || count > LG_CLIPBOARD_DATA_NONE)
return false;
for (size_t i = 0; i < count; ++i)
if (!kvmfrClipboardFormatValid(toWireFormat(types[i])))
return false;
const KVMFRClipboardFormatFlags formats = formatMask(types, count);
LG_LOCK(clipboard->lock);
KVMFRClipboardMessage offer = { 0 };
offer.type = KVMFR_CLIPBOARD_MESSAGE_OFFER;
offer.token = formats;
offer.clipboardGeneration = clipboard->localClipboardGeneration + 1;
if (!offer.clipboardGeneration)
++offer.clipboardGeneration;
const unsigned required = clipboard->claimed ? 1U : 2U;
const bool result =
clipboard->pendingCount <= CLIPBOARD_PENDING_MAX - required &&
ensureClaimNL(clipboard) &&
enqueueRecordNL(clipboard, offer);
if (result)
{
clipboard->localClipboardGeneration = offer.clipboardGeneration;
clipboard->localFormats = formats;
}
LG_UNLOCK(clipboard->lock);
if (result)
signalWorker(clipboard);
return result;
}
static LG_ClipboardResult dataBegin(void * opaque,
LG_ClipboardRequest request, LG_ClipboardData type, uint64_t sizeHint)
{
LGMPClipboard * clipboard = opaque;
const KVMFRClipboardFormat format = toWireFormat(type);
if (!kvmfrClipboardTransferFromHelper(request) ||
!kvmfrClipboardFormatValid(format))
return LG_CLIPBOARD_RESULT_FAILED;
LG_LOCK(clipboard->lock);
if (!clipboard->connected || !clipboard->available ||
!clipboard->claimed || clipboard->writeBegan ||
clipboard->writeTransfer != request ||
clipboard->writeFormat != format ||
!clipboard->writeClipboardGeneration)
{
LG_UNLOCK(clipboard->lock);
return LG_CLIPBOARD_RESULT_FAILED;
}
clipboard->writeSizeHint = sizeHint;
clipboard->writeOffset = 0;
clipboard->writeSequence = 0;
clipboard->writeBegan = true;
clipboard->writeWireBegan = false;
clipboard->writeBlocked = false;
LG_UNLOCK(clipboard->lock);
return LG_CLIPBOARD_RESULT_ACCEPTED;
}
static LG_ClipboardResult dataChunk(void * opaque,
LG_ClipboardRequest request, uint64_t offset,
const void * data, size_t size)
{
LGMPClipboard * clipboard = opaque;
if (!kvmfrClipboardTransferFromHelper(request) || !data || !size ||
size > KVMFR_CLIPBOARD_DATA_BYTES || offset > UINT64_MAX - size)
return LG_CLIPBOARD_RESULT_FAILED;
LG_LOCK(clipboard->lock);
if (!clipboard->connected || !clipboard->available ||
!clipboard->claimed || !clipboard->writeBegan ||
clipboard->writeTransfer != request ||
clipboard->writeOffset != offset ||
(clipboard->writeSizeHint != KVMFR_CLIPBOARD_SIZE_UNKNOWN &&
offset + size > clipboard->writeSizeHint))
{
LG_UNLOCK(clipboard->lock);
return LG_CLIPBOARD_RESULT_FAILED;
}
KVMFRClipboardMessage record = { 0 };
record.type = KVMFR_CLIPBOARD_MESSAGE_DATA;
record.clipboardGeneration = clipboard->writeClipboardGeneration;
record.sequence = clipboard->writeSequence;
record.transfer = request;
record.offset = offset;
record.size = clipboard->writeWireBegan ?
KVMFR_CLIPBOARD_SIZE_UNKNOWN : clipboard->writeSizeHint;
record.format = clipboard->writeFormat;
record.flags = clipboard->writeWireBegan ?
0 : KVMFR_CLIPBOARD_FLAG_BEGIN;
record.length = (uint32_t)size;
const bool result = enqueueDataNL(clipboard, record, data);
if (result)
{
clipboard->writeOffset += size;
++clipboard->writeSequence;
clipboard->writeWireBegan = true;
}
else
{
clipboard->writeBlocked = true;
clipboard->writeBlockedRequest = request;
}
LG_UNLOCK(clipboard->lock);
if (result)
signalWorker(clipboard);
return result ? LG_CLIPBOARD_RESULT_ACCEPTED :
LG_CLIPBOARD_RESULT_BLOCKED;
}
static LG_ClipboardResult dataEnd(void * opaque,
LG_ClipboardRequest request, uint64_t finalSize)
{
LGMPClipboard * clipboard = opaque;
if (!kvmfrClipboardTransferFromHelper(request))
return LG_CLIPBOARD_RESULT_FAILED;
LG_LOCK(clipboard->lock);
if (!clipboard->connected || !clipboard->available ||
!clipboard->claimed || !clipboard->writeBegan ||
clipboard->writeTransfer != request ||
clipboard->writeOffset != finalSize ||
(clipboard->writeSizeHint != KVMFR_CLIPBOARD_SIZE_UNKNOWN &&
clipboard->writeSizeHint != finalSize))
{
LG_UNLOCK(clipboard->lock);
return LG_CLIPBOARD_RESULT_FAILED;
}
KVMFRClipboardMessage record = { 0 };
record.type = KVMFR_CLIPBOARD_MESSAGE_DATA;
record.clipboardGeneration = clipboard->writeClipboardGeneration;
record.transfer = request;
record.format = clipboard->writeFormat;
record.flags = KVMFR_CLIPBOARD_FLAG_END |
(clipboard->writeWireBegan ? 0 : KVMFR_CLIPBOARD_FLAG_BEGIN);
record.size = finalSize;
record.offset = finalSize;
record.sequence = clipboard->writeSequence;
const bool result = enqueueDataNL(clipboard, record, NULL);
if (result)
clearWriteNL(clipboard);
else
{
clipboard->writeBlocked = true;
clipboard->writeBlockedRequest = request;
}
LG_UNLOCK(clipboard->lock);
if (result)
signalWorker(clipboard);
return result ? LG_CLIPBOARD_RESULT_ACCEPTED :
LG_CLIPBOARD_RESULT_BLOCKED;
}
static bool dataCancel(void * opaque, LG_ClipboardRequest request,
LG_ClipboardCancelReason reason)
{
LGMPClipboard * clipboard = opaque;
if (!kvmfrClipboardTransferFromHelper(request) ||
(unsigned)reason > LG_CLIPBOARD_CANCEL_INVALID)
return false;
LG_LOCK(clipboard->lock);
KVMFRClipboardMessage record = { 0 };
record.type = KVMFR_CLIPBOARD_MESSAGE_CANCEL;
record.clipboardGeneration = clipboard->writeClipboardGeneration;
record.transfer = request;
record.format = clipboard->writeFormat;
record.token = reason;
const bool result = clipboard->claimed &&
clipboard->writeTransfer == request &&
enqueueRecordNL(clipboard, record);
if (clipboard->writeTransfer == request)
clearWriteNL(clipboard);
LG_UNLOCK(clipboard->lock);
if (result)
signalWorker(clipboard);
return result;
}
static bool dataReady(void * opaque, LG_ClipboardRequest request)
{
LGMPClipboard * clipboard = opaque;
LG_LOCK(clipboard->lock);
const bool result = clipboard->held &&
clipboard->heldRecord.transfer == request;
if (result)
clipboard->heldReady = true;
LG_UNLOCK(clipboard->lock);
if (result)
signalWorker(clipboard);
return result;
}
static bool requestData(void * opaque, LG_ClipboardRequest request,
LG_ClipboardData type)
{
LGMPClipboard * clipboard = opaque;
const KVMFRClipboardFormat format = toWireFormat(type);
if (request == LG_CLIPBOARD_REQUEST_INVALID ||
!kvmfrClipboardFormatValid(format))
return false;
LG_LOCK(clipboard->lock);
if (clipboard->readRequest || !clipboard->remoteClipboardGeneration ||
!(clipboard->remoteFormats & kvmfrClipboardFormatFlag(format)))
{
LG_UNLOCK(clipboard->lock);
return false;
}
KVMFRClipboardMessage record = { 0 };
record.type = KVMFR_CLIPBOARD_MESSAGE_REQUEST;
record.transfer = nextClientTransfer(clipboard);
record.format = format;
record.clipboardGeneration = clipboard->remoteClipboardGeneration;
const bool result = ensureClaimNL(clipboard) &&
enqueueRecordNL(clipboard, record);
if (result)
{
clipboard->readRequest = request;
clipboard->readTransfer = record.transfer;
clipboard->readClipboardGeneration =
record.clipboardGeneration;
clipboard->readSizeHint = 0;
clipboard->readOffset = 0;
clipboard->readFormat = format;
clipboard->readSequence = 0;
clipboard->readBegan = false;
}
LG_UNLOCK(clipboard->lock);
if (result)
signalWorker(clipboard);
return result;
}
static const LG_ClipboardOps CLIPBOARD_OPS =
{
.name = "LGMP",
.setStatusListener = setStatusListener,
.attach = attach,
.detach = detach,
.release = releaseClipboard,
.notifyTypes = notifyTypes,
.dataBegin = dataBegin,
.dataChunk = dataChunk,
.dataEnd = dataEnd,
.dataCancel = dataCancel,
.dataReady = dataReady,
.request = requestData,
};
const LG_ClipboardOps * lgmpClipboard_getOps(void)
{
return &CLIPBOARD_OPS;
}