Files
LookingGlass/client/transports/LGMP/clipboard.c
Geoffrey McRae be4bb19fa4 [clipboard] lgmp: make payload streams mandatory
Carry DATA and FILE_DATA only through the duplex SPSC streams. Keep LGMP
queues for control and status. Remove grants, commits, ACKs, and
queue-payload transport negotiation.

Poll adaptively while ownership or release is active. Preserve FIFO
release and graceful draining. Quarantine payloads crossed by a newer
OFFER or CLEAR, and retire data-plane state on detach.
2026-08-15 14:51:24 +10:00

3235 lines
104 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 <lgmp/stream.h>
#include <errno.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/random.h>
#define CLIPBOARD_PENDING_MAX 128U
#define CLIPBOARD_PENDING_NORMAL_MAX 64U
#define CLIPBOARD_DRAIN_MAX 64U
#define CLIPBOARD_IDLE_POLL_MS 10U
#define CLIPBOARD_ACTIVE_POLL_MS 1U
#define CLIPBOARD_KEEPALIVE_US UINT64_C(200000)
#define CLIPBOARD_RELEASE_TIMEOUT_US UINT64_C(100000)
struct PendingRecord
{
KVMFRClipboardMessage record;
};
struct FileTransfer
{
struct FileTransfer * next;
LG_ClipboardFileRequest request;
uint64_t sizeHint;
uint64_t responseOffset;
uint32_t sequence;
bool began;
bool blocked;
bool wireEnded;
};
struct FileAcquisition
{
struct FileAcquisition * next;
uint64_t dataset;
uint64_t acquisition;
bool fromHelper;
bool active;
};
enum HeldPhase
{
HELD_PHASE_NONE,
HELD_PHASE_BEGIN,
HELD_PHASE_CHUNK,
HELD_PHASE_END,
};
struct LGMPClipboard
{
PLGMPClient client;
PLGMPClientQueue queue;
PLGMPClientStream hostToClientStream;
PLGMPClientStream clientToHostStream;
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;
bool releasing;
KVMFRStreamDescriptor hostToClientDescriptor;
KVMFRStreamDescriptor clientToHostDescriptor;
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;
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;
LGMPStreamBuffer heldStreamBuffer;
KVMFRClipboardMessage heldRecord;
bool heldFile;
LG_ClipboardFileRequest heldFileRequest;
uint64_t localClipboardGeneration;
uint64_t localGenerationSerial;
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;
struct FileTransfer * fileReads;
struct FileTransfer * fileWrites;
struct FileAcquisition * fileAcquisitions;
struct FileTransfer * retiredFileReads;
struct FileTransfer * retiredFileWrites;
struct FileAcquisition * retiredFileAcquisitions;
const LG_ClipboardEventOps * events;
void * eventOpaque;
LG_ClipboardStatusFn statusCallback;
void * statusOpaque;
};
static void connectionFailed(LGMPClipboard * clipboard,
LGMP_STATUS status);
static void connectionCorrupted(LGMPClipboard * clipboard,
const char * reason);
_Static_assert(sizeof(struct LGMPStreamDescriptor) ==
sizeof(KVMFRStreamDescriptor),
"LGMP and KVMFR stream descriptor sizes differ");
_Static_assert(offsetof(struct LGMPStreamDescriptor, offset) ==
offsetof(KVMFRStreamDescriptor, offset),
"LGMP and KVMFR stream descriptor offsets differ");
_Static_assert(offsetof(struct LGMPStreamDescriptor, slotSize) ==
offsetof(KVMFRStreamDescriptor, slotSize),
"LGMP and KVMFR stream descriptor geometry differs");
static struct LGMPStreamDescriptor streamDescriptorFromWire(
const KVMFRStreamDescriptor * wire)
{
return (struct LGMPStreamDescriptor)
{
.magic = wire->magic,
.version = wire->version,
.size = wire->size,
.offset = wire->offset,
.regionSize = wire->regionSize,
.direction = wire->direction,
.policy = wire->policy,
.slotCount = wire->slotCount,
.slotSize = wire->slotSize,
};
}
static bool validStreamDescriptor(const KVMFRStreamDescriptor * descriptor,
enum LGMPStreamDirection direction)
{
return descriptor->magic == LGMP_STREAM_DESCRIPTOR_MAGIC &&
descriptor->version == LGMP_STREAM_DESCRIPTOR_VERSION &&
descriptor->size == sizeof(*descriptor) && descriptor->offset &&
descriptor->regionSize && descriptor->direction == (uint32_t)direction &&
descriptor->policy == LGMP_STREAM_RELIABLE_FIFO &&
descriptor->slotCount == KVMFR_CLIPBOARD_STREAM_SLOT_COUNT &&
descriptor->slotSize == KVMFR_CLIPBOARD_STREAM_RECORD_BYTES;
}
static void detachStreamsNL(LGMPClipboard * clipboard)
{
lgmpClientStreamDetach(&clipboard->clientToHostStream);
lgmpClientStreamDetach(&clipboard->hostToClientStream);
memset(&clipboard->clientToHostDescriptor, 0,
sizeof(clipboard->clientToHostDescriptor));
memset(&clipboard->hostToClientDescriptor, 0,
sizeof(clipboard->hostToClientDescriptor));
}
static bool attachStreamsNL(LGMPClipboard * clipboard,
const KVMFRClipboardStatus * status)
{
if (clipboard->hostToClientStream && clipboard->clientToHostStream &&
!memcmp(&clipboard->hostToClientDescriptor, &status->hostToClient,
sizeof(status->hostToClient)) &&
!memcmp(&clipboard->clientToHostDescriptor, &status->clientToHost,
sizeof(status->clientToHost)))
return true;
detachStreamsNL(clipboard);
const struct LGMPStreamDescriptor hostToClient =
streamDescriptorFromWire(&status->hostToClient);
LGMP_STATUS result = lgmpClientStreamAttach(clipboard->client,
&hostToClient, &clipboard->hostToClientStream);
if (result != LGMP_OK)
{
DEBUG_WARN("Failed to attach host-to-client clipboard stream: %s",
lgmpStatusString(result));
return false;
}
const struct LGMPStreamDescriptor clientToHost =
streamDescriptorFromWire(&status->clientToHost);
result = lgmpClientStreamAttach(clipboard->client,
&clientToHost, &clipboard->clientToHostStream);
if (result != LGMP_OK)
{
DEBUG_WARN("Failed to attach client-to-host clipboard stream: %s",
lgmpStatusString(result));
detachStreamsNL(clipboard);
return false;
}
clipboard->hostToClientDescriptor = status->hostToClient;
clipboard->clientToHostDescriptor = status->clientToHost;
return true;
}
static LGMP_STATUS activateStreamsNL(LGMPClipboard * clipboard)
{
if (!clipboard->hostToClientStream || !clipboard->clientToHostStream)
return LGMP_ERR_STREAM_UNBOUND;
LGMP_STATUS status = lgmpClientStreamActivate(
clipboard->hostToClientStream, NULL);
if (status != LGMP_OK)
return status;
return lgmpClientStreamActivate(
clipboard->clientToHostStream, NULL);
}
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;
case KVMFR_CLIPBOARD_FORMAT_FILES: return LG_CLIPBOARD_DATA_FILES;
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;
case LG_CLIPBOARD_DATA_FILES: return KVMFR_CLIPBOARD_FORMAT_FILES;
default: return KVMFR_CLIPBOARD_FORMAT_NONE;
}
}
static LG_ClipboardFileOperation fromWireFileOperation(uint32_t operation)
{
return operation == KVMFR_CLIPBOARD_FILE_OP_READ ?
LG_CLIPBOARD_FILE_READ : LG_CLIPBOARD_FILE_LIST;
}
static uint32_t toWireFileOperation(LG_ClipboardFileOperation operation)
{
return operation == LG_CLIPBOARD_FILE_READ ?
KVMFR_CLIPBOARD_FILE_OP_READ : KVMFR_CLIPBOARD_FILE_OP_LIST;
}
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_FILES; ++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 uint64_t nextClientGeneration(LGMPClipboard * clipboard)
{
if (++clipboard->localGenerationSerial == 0 ||
kvmfrClipboardTransferFromHelper(
clipboard->localGenerationSerial))
clipboard->localGenerationSerial = 1;
return clipboard->localGenerationSerial;
}
static bool randomBytes(void * buffer, size_t size)
{
uint8_t * output = buffer;
while (size)
{
const ssize_t received = getrandom(output, size, 0);
if (received < 0 && errno == EINTR)
continue;
if (received <= 0)
{
if (!received)
errno = EIO;
return false;
}
output += (size_t)received;
size -= (size_t)received;
}
return true;
}
static LG_ClipboardFileError fromWireFileError(uint32_t error)
{
switch (error)
{
case KVMFR_CLIPBOARD_FILE_ERROR_NONE:
return LG_CLIPBOARD_FILE_ERROR_NONE;
case KVMFR_CLIPBOARD_FILE_ERROR_NOT_FOUND:
return LG_CLIPBOARD_FILE_ERROR_NOT_FOUND;
case KVMFR_CLIPBOARD_FILE_ERROR_ACCESS:
return LG_CLIPBOARD_FILE_ERROR_ACCESS;
case KVMFR_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY:
return LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY;
case KVMFR_CLIPBOARD_FILE_ERROR_IS_DIRECTORY:
return LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY;
case KVMFR_CLIPBOARD_FILE_ERROR_IO:
return LG_CLIPBOARD_FILE_ERROR_IO;
case KVMFR_CLIPBOARD_FILE_ERROR_INVALID:
return LG_CLIPBOARD_FILE_ERROR_INVALID;
case KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY:
return LG_CLIPBOARD_FILE_ERROR_NO_MEMORY;
case KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE:
return LG_CLIPBOARD_FILE_ERROR_NO_SPACE;
case KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED:
return LG_CLIPBOARD_FILE_ERROR_DISCONNECTED;
case KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED:
return LG_CLIPBOARD_FILE_ERROR_CANCELLED;
case KVMFR_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED:
return LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED;
case KVMFR_CLIPBOARD_FILE_ERROR_STALE:
return LG_CLIPBOARD_FILE_ERROR_STALE;
default:
return LG_CLIPBOARD_FILE_ERROR_INVALID;
}
}
static KVMFRClipboardFileError toWireFileError(
LG_ClipboardFileError error)
{
switch (error)
{
case LG_CLIPBOARD_FILE_ERROR_NONE:
return KVMFR_CLIPBOARD_FILE_ERROR_NONE;
case LG_CLIPBOARD_FILE_ERROR_NOT_FOUND:
return KVMFR_CLIPBOARD_FILE_ERROR_NOT_FOUND;
case LG_CLIPBOARD_FILE_ERROR_ACCESS:
return KVMFR_CLIPBOARD_FILE_ERROR_ACCESS;
case LG_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY:
return KVMFR_CLIPBOARD_FILE_ERROR_NOT_DIRECTORY;
case LG_CLIPBOARD_FILE_ERROR_IS_DIRECTORY:
return KVMFR_CLIPBOARD_FILE_ERROR_IS_DIRECTORY;
case LG_CLIPBOARD_FILE_ERROR_IO:
return KVMFR_CLIPBOARD_FILE_ERROR_IO;
case LG_CLIPBOARD_FILE_ERROR_INVALID:
return KVMFR_CLIPBOARD_FILE_ERROR_INVALID;
case LG_CLIPBOARD_FILE_ERROR_NO_MEMORY:
return KVMFR_CLIPBOARD_FILE_ERROR_NO_MEMORY;
case LG_CLIPBOARD_FILE_ERROR_NO_SPACE:
return KVMFR_CLIPBOARD_FILE_ERROR_NO_SPACE;
case LG_CLIPBOARD_FILE_ERROR_DISCONNECTED:
return KVMFR_CLIPBOARD_FILE_ERROR_DISCONNECTED;
case LG_CLIPBOARD_FILE_ERROR_CANCELLED:
return KVMFR_CLIPBOARD_FILE_ERROR_CANCELLED;
case LG_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED:
return KVMFR_CLIPBOARD_FILE_ERROR_NOT_SUPPORTED;
case LG_CLIPBOARD_FILE_ERROR_STALE:
return KVMFR_CLIPBOARD_FILE_ERROR_STALE;
}
return KVMFR_CLIPBOARD_FILE_ERROR_INVALID;
}
static bool validFileError(LG_ClipboardFileError error)
{
return error >= LG_CLIPBOARD_FILE_ERROR_NONE &&
error <= LG_CLIPBOARD_FILE_ERROR_STALE;
}
static struct FileTransfer * fileTransferFindNL(
struct FileTransfer * transfers, uint64_t request)
{
for (; transfers; transfers = transfers->next)
if (transfers->request.request == request)
return transfers;
return NULL;
}
static struct FileTransfer * fileTransferTakeNL(
struct FileTransfer ** transfers, uint64_t request)
{
while (*transfers && (*transfers)->request.request != request)
transfers = &(*transfers)->next;
if (!*transfers)
return NULL;
struct FileTransfer * result = *transfers;
*transfers = result->next;
result->next = NULL;
return result;
}
static struct FileAcquisition * fileAcquisitionFindNL(
LGMPClipboard * clipboard, uint64_t dataset, uint64_t acquisition,
bool fromHelper)
{
for (struct FileAcquisition * item = clipboard->fileAcquisitions;
item; item = item->next)
if (item->dataset == dataset && item->acquisition == acquisition &&
item->fromHelper == fromHelper)
return item;
return NULL;
}
static bool fileDatasetAcquiredNL(LGMPClipboard * clipboard,
uint64_t dataset, bool fromHelper)
{
for (struct FileAcquisition * item = clipboard->fileAcquisitions;
item; item = item->next)
if (item->dataset == dataset && item->fromHelper == fromHelper &&
item->active)
return true;
return false;
}
static struct FileAcquisition * fileAcquisitionTakeNL(
LGMPClipboard * clipboard, uint64_t dataset, uint64_t acquisition,
bool fromHelper)
{
struct FileAcquisition ** link = &clipboard->fileAcquisitions;
while (*link && ((*link)->dataset != dataset ||
(*link)->acquisition != acquisition ||
(*link)->fromHelper != fromHelper))
link = &(*link)->next;
if (!*link)
return NULL;
struct FileAcquisition * result = *link;
*link = result->next;
result->next = NULL;
return result;
}
static unsigned fileAcquisitionCountNL(const LGMPClipboard * clipboard)
{
unsigned count = 0;
for (const struct FileAcquisition * item = clipboard->fileAcquisitions;
item; item = item->next)
++count;
return count;
}
static unsigned fileTransferCountNL(const LGMPClipboard * clipboard)
{
unsigned count = 0;
for (const struct FileTransfer * item = clipboard->fileReads;
item; item = item->next)
++count;
for (const struct FileTransfer * item = clipboard->fileWrites;
item; item = item->next)
++count;
return count;
}
static void clearFilesNL(LGMPClipboard * clipboard)
{
struct FileTransfer ** readTail = &clipboard->fileReads;
while (*readTail)
readTail = &(*readTail)->next;
*readTail = clipboard->retiredFileReads;
clipboard->retiredFileReads = clipboard->fileReads;
clipboard->fileReads = NULL;
struct FileTransfer ** writeTail = &clipboard->fileWrites;
while (*writeTail)
writeTail = &(*writeTail)->next;
*writeTail = clipboard->retiredFileWrites;
clipboard->retiredFileWrites = clipboard->fileWrites;
clipboard->fileWrites = NULL;
struct FileAcquisition ** acquisitionTail =
&clipboard->fileAcquisitions;
while (*acquisitionTail)
acquisitionTail = &(*acquisitionTail)->next;
*acquisitionTail = clipboard->retiredFileAcquisitions;
clipboard->retiredFileAcquisitions = clipboard->fileAcquisitions;
clipboard->fileAcquisitions = NULL;
}
static void freeFileTransfers(struct FileTransfer * transfers)
{
while (transfers)
{
struct FileTransfer * next = transfers->next;
free(transfers);
transfers = next;
}
}
static void freeFileAcquisitions(struct FileAcquisition * acquisitions)
{
while (acquisitions)
{
struct FileAcquisition * next = acquisitions->next;
free(acquisitions);
acquisitions = next;
}
}
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 enqueueRecordLimitNL(LGMPClipboard * clipboard,
KVMFRClipboardMessage record, unsigned limit)
{
if (!clipboard->connected || !clipboard->queue ||
clipboard->pendingCount >= limit)
return false;
record.version = KVMFR_CLIPBOARD_VERSION;
record.generation = clipboard->claimGeneration;
pendingAt(clipboard, clipboard->pendingCount++)->record = record;
return true;
}
static bool enqueueRecordNL(LGMPClipboard * clipboard,
KVMFRClipboardMessage record)
{
return enqueueRecordLimitNL(
clipboard, record, CLIPBOARD_PENDING_NORMAL_MAX);
}
static bool enqueueUrgentRecordNL(LGMPClipboard * clipboard,
KVMFRClipboardMessage record)
{
return enqueueRecordLimitNL(clipboard, record, CLIPBOARD_PENDING_MAX);
}
static bool enqueueFileFailureNL(LGMPClipboard * clipboard,
const KVMFRClipboardMessage * request, LG_ClipboardFileError error)
{
KVMFRClipboardMessage response = { 0 };
response.type = request->type == KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE ?
KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED :
KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL;
response.clipboardGeneration = request->clipboardGeneration;
response.transfer = request->transfer;
response.format = KVMFR_CLIPBOARD_FORMAT_FILES;
response.token = toWireFileError(error);
return enqueueUrgentRecordNL(clipboard, response);
}
static bool enqueueTypeNL(LGMPClipboard * clipboard,
KVMFRClipboardMessageType type)
{
KVMFRClipboardMessage record = { 0 };
record.type = type;
return enqueueRecordNL(clipboard, record);
}
static bool streamWriteNL(LGMPClipboard * clipboard,
const KVMFRClipboardMessage * record, const void * data)
{
if (!clipboard->clientToHostStream)
return false;
LGMPStreamBuffer buffer = { 0 };
LGMP_STATUS status = lgmpClientStreamWriteAcquire(
clipboard->clientToHostStream, &buffer);
if (status == LGMP_ERR_STREAM_FULL ||
status == LGMP_ERR_STREAM_UNBOUND ||
status == LGMP_ERR_STREAM_STALE)
return false;
if (status != LGMP_OK)
{
connectionFailed(clipboard, status);
return false;
}
const uint32_t bytes = sizeof(*record) + record->length;
if (!buffer.data || buffer.capacity < bytes)
{
const LGMP_STATUS cancel = lgmpClientStreamWriteCancel(
clipboard->clientToHostStream, &buffer);
if (cancel != LGMP_OK)
DEBUG_WARN("Failed to cancel an undersized clipboard stream "
"reservation: %s", lgmpStatusString(cancel));
DEBUG_WARN("Client-to-host clipboard stream returned an undersized "
"slot: capacity=%u required=%u", buffer.capacity, bytes);
connectionCorrupted(clipboard, "undersized stream slot");
return false;
}
memcpy(buffer.data, record, sizeof(*record));
if (record->length)
memcpy((uint8_t *)buffer.data + sizeof(*record), data,
record->length);
status = lgmpClientStreamWriteCommit(
clipboard->clientToHostStream, &buffer, bytes);
if (status == LGMP_OK)
return true;
DEBUG_WARN("Failed to commit client-to-host clipboard stream data: %s",
lgmpStatusString(status));
connectionFailed(clipboard, status);
return false;
}
static bool enqueueDataNL(LGMPClipboard * clipboard,
KVMFRClipboardMessage record, const void * data)
{
if (!clipboard->connected || !clipboard->queue ||
record.length > KVMFR_CLIPBOARD_DATA_BYTES ||
(record.length && !data))
return false;
record.version = KVMFR_CLIPBOARD_VERSION;
record.generation = clipboard->claimGeneration;
return streamWriteNL(clipboard, &record, data);
}
static bool streamWritableNL(LGMPClipboard * clipboard)
{
if (!clipboard->clientToHostStream || !clipboard->claimed)
return false;
LGMPStreamBuffer buffer = { 0 };
LGMP_STATUS status = lgmpClientStreamWriteAcquire(
clipboard->clientToHostStream, &buffer);
if (status == LGMP_ERR_STREAM_FULL ||
status == LGMP_ERR_STREAM_UNBOUND ||
status == LGMP_ERR_STREAM_STALE)
return false;
if (status != LGMP_OK)
{
connectionFailed(clipboard, status);
return false;
}
status = lgmpClientStreamWriteCancel(
clipboard->clientToHostStream, &buffer);
if (status == LGMP_OK)
return true;
if (status != LGMP_ERR_STREAM_STALE)
connectionFailed(clipboard, status);
return false;
}
static bool dataWritableNL(LGMPClipboard * clipboard)
{
return streamWritableNL(clipboard);
}
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 clearDataPlaneNL(LGMPClipboard * clipboard)
{
clipboard->remoteClipboardGeneration = 0;
clipboard->remoteFormats = 0;
clearWriteNL(clipboard);
clearReadNL(clipboard);
clearFilesNL(clipboard);
}
static void clearProtocolNL(LGMPClipboard * clipboard)
{
clipboard->claimed = false;
clipboard->ownerConfirmed = false;
clipboard->releasing = false;
clipboard->publishedClaimGeneration = 0;
clipboard->pendingHead = 0;
clipboard->pendingCount = 0;
clearDataPlaneNL(clipboard);
}
static bool ensureClaimNL(LGMPClipboard * clipboard)
{
if (clipboard->claimed)
return true;
if (clipboard->releasing || !clipboard->connected ||
!clipboard->available ||
!clipboard->hostToClientStream || !clipboard->clientToHostStream)
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 connectionLostNL(LGMPClipboard * clipboard)
{
const bool changed = clipboard->connected || clipboard->available;
clipboard->connected = false;
clipboard->available = false;
clipboard->statusValid = false;
detachStreamsNL(clipboard);
clearProtocolNL(clipboard);
clipboard->held = false;
clipboard->heldReady = false;
memset(&clipboard->heldStreamBuffer, 0,
sizeof(clipboard->heldStreamBuffer));
memset(&clipboard->heldRecord, 0, sizeof(clipboard->heldRecord));
clipboard->heldFile = false;
memset(&clipboard->heldFileRequest, 0,
sizeof(clipboard->heldFileRequest));
if (changed)
nextNonzero(&clipboard->providerGeneration);
atomic_store_explicit(&clipboard->stop, true, memory_order_release);
}
static void connectionFailed(LGMPClipboard * clipboard, LGMP_STATUS status)
{
if (clipboard->connected)
DEBUG_WARN("LGMP clipboard transport failed: %s",
lgmpStatusString(status));
connectionLostNL(clipboard);
}
static void connectionCorrupted(LGMPClipboard * clipboard,
const char * reason)
{
if (clipboard->connected)
DEBUG_WARN("LGMP clipboard transport is corrupted: %s", reason);
connectionLostNL(clipboard);
}
static bool validStatus(const KVMFRClipboardStatus * status)
{
const uint32_t validFlags = KVMFR_CLIPBOARD_STATUS_AVAILABLE |
KVMFR_CLIPBOARD_STATUS_HAS_OWNER;
const uint32_t emptyReserved[6] = { 0 };
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 ||
status->streamVersion != KVMFR_CLIPBOARD_STREAM_VERSION ||
status->streamSlotCount != KVMFR_CLIPBOARD_STREAM_SLOT_COUNT ||
memcmp(status->reserved, emptyReserved, sizeof(emptyReserved)) ||
!validStreamDescriptor(&status->hostToClient,
LGMP_STREAM_HOST_TO_CLIENT) ||
!validStreamDescriptor(&status->clientToHost,
LGMP_STREAM_CLIENT_TO_HOST))
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, bool stream)
{
if (size < sizeof(*record) ||
record->version != KVMFR_CLIPBOARD_VERSION ||
record->type < KVMFR_CLIPBOARD_MESSAGE_OFFER ||
record->type > KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL ||
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 ||
record->type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA)
{
if (!stream || size != sizeof(*record) + record->length)
return false;
}
else if (stream || size != sizeof(*record) || record->length ||
(record->flags &&
record->type != KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST))
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) &&
kvmfrClipboardRepresentationFormatValid(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;
const unsigned operations =
!!(record->flags & KVMFR_CLIPBOARD_FLAG_BEGIN) +
!!record->length + !!(record->flags & KVMFR_CLIPBOARD_FLAG_END);
if (!record->clipboardGeneration || record->token ||
kvmfrClipboardTransferFromHelper(record->transfer) ||
!kvmfrClipboardRepresentationFormatValid(record->format) ||
record->flags & ~(KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END) || !operations ||
((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 ||
kvmfrClipboardRepresentationFormatValid(record->format)) &&
!record->flags && !record->length &&
!record->sequence;
case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE:
case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED:
case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE:
case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST:
case KVMFR_CLIPBOARD_MESSAGE_FILE_DATA:
case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL:
return kvmfrClipboardFileMessageValid(record);
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 bool validateFileReadChunkNL(const LGMPClipboard * clipboard,
const KVMFRClipboardMessage * record,
LG_ClipboardFileRequest * descriptor)
{
struct FileTransfer * transfer = fileTransferFindNL(
clipboard->fileReads, record->transfer);
if (!transfer || record->clipboardGeneration != transfer->request.dataset ||
record->format != KVMFR_CLIPBOARD_FORMAT_FILES ||
record->token != toWireFileOperation(transfer->request.operation) ||
record->offset != transfer->responseOffset ||
record->sequence != transfer->sequence)
return false;
if (!transfer->began)
{
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 end = record->offset + record->length;
const uint64_t hint = transfer->began ? transfer->sizeHint : record->size;
if (hint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && end > hint)
return false;
if (transfer->request.operation == LG_CLIPBOARD_FILE_READ &&
end > transfer->request.length)
return false;
if ((record->flags & KVMFR_CLIPBOARD_FLAG_END) &&
(record->size != end ||
(hint != KVMFR_CLIPBOARD_SIZE_UNKNOWN && hint != end)))
return false;
*descriptor = transfer->request;
return true;
}
static void advanceFileReadNL(LGMPClipboard * clipboard,
const KVMFRClipboardMessage * record)
{
struct FileTransfer * transfer = fileTransferFindNL(
clipboard->fileReads, record->transfer);
if (!transfer)
return;
if (!transfer->began)
{
transfer->began = true;
transfer->sizeHint = record->size;
}
transfer->responseOffset += record->length;
++transfer->sequence;
if (record->flags & KVMFR_CLIPBOARD_FLAG_END)
free(fileTransferTakeNL(&clipboard->fileReads, record->transfer));
}
static bool rejectMalformedFileReadNL(LGMPClipboard * clipboard,
const KVMFRClipboardMessage * record,
KVMFRClipboardMessage * cancellation, bool * queued)
{
if (record->type != KVMFR_CLIPBOARD_MESSAGE_FILE_DATA)
return false;
struct FileTransfer * transfer = fileTransferFindNL(
clipboard->fileReads, record->transfer);
if (!transfer ||
transfer->request.dataset != record->clipboardGeneration)
return false;
*cancellation = (KVMFRClipboardMessage)
{
.type = KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL,
.clipboardGeneration = transfer->request.dataset,
.transfer = transfer->request.request,
.format = KVMFR_CLIPBOARD_FORMAT_FILES,
.token = KVMFR_CLIPBOARD_FILE_ERROR_INVALID,
};
free(fileTransferTakeNL(&clipboard->fileReads, record->transfer));
*queued = enqueueUrgentRecordNL(clipboard, *cancellation);
return true;
}
static void dispatchOffer(LGMPClipboard * clipboard,
uint64_t dataset, 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->fileOffer &&
(mask & KVMFR_CLIPBOARD_FORMAT_MASK_FILES))
events->fileOffer(opaque, dataset);
if (events && events->notice)
events->notice(opaque, formats, count);
LG_UNLOCK(clipboard->eventLock);
}
static void dispatchFileAcquire(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 && events->fileAcquire)
events->fileAcquire(opaque,
record->clipboardGeneration, record->transfer);
LG_UNLOCK(clipboard->eventLock);
}
static void dispatchFileAcquired(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 && events->fileAcquired)
events->fileAcquired(opaque, record->clipboardGeneration,
record->transfer, fromWireFileError(record->token));
LG_UNLOCK(clipboard->eventLock);
}
static void dispatchFileRelease(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 && events->fileRelease)
events->fileRelease(opaque,
record->clipboardGeneration, record->transfer);
LG_UNLOCK(clipboard->eventLock);
}
static void dispatchFileRequest(LGMPClipboard * clipboard,
const LG_ClipboardFileRequest * 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->fileRequest)
events->fileRequest(opaque, request);
LG_UNLOCK(clipboard->eventLock);
}
static LG_ClipboardResult dispatchFileData(LGMPClipboard * clipboard,
const KVMFRClipboardMessage * record,
const LG_ClipboardFileRequest * request, 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->fileDataBegin)
result = events->fileDataBegin(opaque, request, record->size);
break;
case HELD_PHASE_CHUNK:
if (events->fileDataChunk)
result = events->fileDataChunk(opaque, request,
record->offset, data, record->length);
break;
case HELD_PHASE_END:
if (events->fileDataEnd)
result = events->fileDataEnd(opaque, request, record->size);
break;
case HELD_PHASE_NONE:
break;
}
LG_UNLOCK(clipboard->eventLock);
return result;
}
static void dispatchFileCancel(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 && events->fileCancel)
events->fileCancel(opaque, record->clipboardGeneration,
record->transfer, fromWireFileError(record->token));
LG_UNLOCK(clipboard->eventLock);
}
static void dispatchRetiredFiles(LGMPClipboard * clipboard)
{
LG_LOCK(clipboard->eventLock);
LG_LOCK(clipboard->lock);
struct FileTransfer * reads = clipboard->retiredFileReads;
struct FileTransfer * writes = clipboard->retiredFileWrites;
struct FileAcquisition * acquisitions =
clipboard->retiredFileAcquisitions;
clipboard->retiredFileReads = NULL;
clipboard->retiredFileWrites = NULL;
clipboard->retiredFileAcquisitions = NULL;
const LG_ClipboardEventOps * events = clipboard->events;
void * opaque = clipboard->eventOpaque;
LG_UNLOCK(clipboard->lock);
if (events && events->fileCancel)
{
for (struct FileTransfer * transfer = reads; transfer;
transfer = transfer->next)
events->fileCancel(opaque, transfer->request.dataset,
transfer->request.request,
LG_CLIPBOARD_FILE_ERROR_DISCONNECTED);
for (struct FileTransfer * transfer = writes; transfer;
transfer = transfer->next)
events->fileCancel(opaque, transfer->request.dataset,
transfer->request.request,
LG_CLIPBOARD_FILE_ERROR_DISCONNECTED);
for (struct FileAcquisition * acquisition = acquisitions;
acquisition; acquisition = acquisition->next)
events->fileCancel(opaque, acquisition->dataset,
acquisition->acquisition,
LG_CLIPBOARD_FILE_ERROR_DISCONNECTED);
}
freeFileTransfers(reads);
freeFileTransfers(writes);
freeFileAcquisitions(acquisitions);
LG_UNLOCK(clipboard->eventLock);
}
static void dispatchFileReady(LGMPClipboard * clipboard, uint64_t 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->fileDataReady)
events->fileDataReady(opaque, request);
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 streamsAttached = attachStreamsNL(clipboard, status);
const bool endpointAvailable =
(status->flags & KVMFR_CLIPBOARD_STATUS_AVAILABLE) != 0;
clipboard->endpointGeneration = status->generation;
const bool hasOwner =
(status->flags & KVMFR_CLIPBOARD_STATUS_HAS_OWNER) != 0;
const bool owned = hasOwner &&
(clipboard->claimed || clipboard->releasing) &&
status->ownerClientID == clipboard->clientID &&
status->ownerGeneration == clipboard->claimGeneration;
const bool ownedByOther = hasOwner && !owned;
const bool selectedUnavailable =
(clipboard->claimed || clipboard->releasing) && !streamsAttached;
clipboard->available = endpointAvailable && streamsAttached &&
!ownedByOther && !clipboard->releasing;
bool restore = false;
if (owned)
{
const LGMP_STATUS activate = activateStreamsNL(clipboard);
if (activate != LGMP_OK)
{
connectionFailed(clipboard, activate);
*changed = true;
return;
}
}
if (!endpointAvailable || oldGeneration != status->generation ||
ownedByOther || selectedUnavailable)
{
clearProtocolNL(clipboard);
restore = endpointAvailable && streamsAttached && !ownedByOther;
}
else if (hasOwner)
clipboard->ownerConfirmed = owned;
else
{
if (clipboard->releasing)
{
clipboard->releasing = false;
clipboard->ownerConfirmed = false;
clipboard->available = endpointAvailable && streamsAttached;
}
else if (clipboard->claimed && clipboard->ownerConfirmed)
{
clearProtocolNL(clipboard);
restore = endpointAvailable && streamsAttached;
}
else
clipboard->ownerConfirmed = false;
}
if (!wasAvailable && clipboard->available && !clipboard->claimed &&
clipboard->events)
restore = true;
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;
bool file;
LGMPStreamBuffer streamBuffer;
LG_ClipboardFileRequest fileRequest;
LG_LOCK(clipboard->lock);
if (!clipboard->held || !clipboard->heldReady)
{
LG_UNLOCK(clipboard->lock);
return true;
}
record = clipboard->heldRecord;
streamBuffer = clipboard->heldStreamBuffer;
data = !record.length ? NULL :
(const uint8_t *)streamBuffer.data + sizeof(record);
phase = clipboard->heldPhase;
file = clipboard->heldFile;
fileRequest = clipboard->heldFileRequest;
clipboard->heldReady = false;
LG_UNLOCK(clipboard->lock);
const LG_ClipboardResult result = file ?
dispatchFileData(clipboard, &record, &fileRequest, data, phase) :
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 = !file &&
clipboard->readRequest == record.transfer &&
clipboard->readTransfer;
struct FileTransfer * matchingFile = file ? fileTransferFindNL(
clipboard->fileReads, record.transfer) : NULL;
if (result == LG_CLIPBOARD_RESULT_FAILED && (matchingRead || matchingFile))
{
KVMFRClipboardMessage cancel = { 0 };
cancel.type = file ? KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL :
KVMFR_CLIPBOARD_MESSAGE_CANCEL;
cancel.clipboardGeneration = file ? fileRequest.dataset :
clipboard->readClipboardGeneration;
cancel.transfer = file ? fileRequest.request : clipboard->readTransfer;
cancel.format = file ? KVMFR_CLIPBOARD_FORMAT_FILES :
clipboard->readFormat;
cancel.token = file ?
toWireFileError(LG_CLIPBOARD_FILE_ERROR_INVALID) :
LG_CLIPBOARD_CANCEL_INVALID;
if (file)
enqueueUrgentRecordNL(clipboard, cancel);
else
enqueueRecordNL(clipboard, cancel);
}
if (matchingFile && result == LG_CLIPBOARD_RESULT_ACCEPTED)
advanceFileReadNL(clipboard, &record);
else if (matchingFile)
free(fileTransferTakeNL(&clipboard->fileReads, record.transfer));
else if (matchingRead && result == LG_CLIPBOARD_RESULT_ACCEPTED)
advanceReadNL(clipboard, &record);
else if (matchingRead)
clearReadNL(clipboard);
const LGMP_STATUS done = lgmpClientStreamReadRelease(
clipboard->hostToClientStream, &streamBuffer);
clipboard->held = false;
memset(&clipboard->heldStreamBuffer, 0,
sizeof(clipboard->heldStreamBuffer));
memset(&clipboard->heldRecord, 0, sizeof(clipboard->heldRecord));
clipboard->heldFile = false;
memset(&clipboard->heldFileRequest, 0,
sizeof(clipboard->heldFileRequest));
if (done != LGMP_OK)
{
connectionFailed(clipboard, done);
LG_UNLOCK(clipboard->lock);
return false;
}
LG_UNLOCK(clipboard->lock);
return true;
}
}
static bool processStream(LGMPClipboard * clipboard, bool * processed)
{
*processed = false;
LG_LOCK(clipboard->lock);
if (!clipboard->connected || clipboard->held ||
(!clipboard->claimed && !clipboard->releasing) ||
!clipboard->hostToClientStream)
{
LG_UNLOCK(clipboard->lock);
return true;
}
LGMPStreamBuffer buffer = { 0 };
LGMP_STATUS status = lgmpClientStreamReadPeek(
clipboard->hostToClientStream, &buffer);
if (status == LGMP_ERR_STREAM_EMPTY ||
status == LGMP_ERR_STREAM_UNBOUND ||
status == LGMP_ERR_STREAM_STALE)
{
LG_UNLOCK(clipboard->lock);
return true;
}
if (status != LGMP_OK)
{
connectionFailed(clipboard, status);
LG_UNLOCK(clipboard->lock);
return false;
}
*processed = true;
if (clipboard->releasing)
{
status = lgmpClientStreamReadRelease(
clipboard->hostToClientStream, &buffer);
if (status != LGMP_OK && status != LGMP_ERR_STREAM_UNBOUND &&
status != LGMP_ERR_STREAM_STALE)
connectionFailed(clipboard, status);
LG_UNLOCK(clipboard->lock);
return status == LGMP_OK || status == LGMP_ERR_STREAM_UNBOUND ||
status == LGMP_ERR_STREAM_STALE;
}
KVMFRClipboardMessage record = { 0 };
if (buffer.data && buffer.size >= sizeof(record))
memcpy(&record, buffer.data, sizeof(record));
const bool valid = buffer.data &&
validateRecord(clipboard, &record, buffer.size, true);
if (!valid)
{
KVMFRClipboardMessage cancellation = { 0 };
bool queued = false;
const bool cancelled = rejectMalformedFileReadNL(
clipboard, &record, &cancellation, &queued);
status = lgmpClientStreamReadRelease(
clipboard->hostToClientStream, &buffer);
if (status != LGMP_OK)
connectionFailed(clipboard, status);
LG_UNLOCK(clipboard->lock);
if (cancelled)
dispatchFileCancel(clipboard, &cancellation);
if (queued)
signalWorker(clipboard);
DEBUG_WARN("Ignoring malformed LGMP clipboard stream record");
return status == LGMP_OK;
}
LG_ClipboardFileRequest fileRequest = { 0 };
const bool file =
record.type == KVMFR_CLIPBOARD_MESSAGE_FILE_DATA;
const bool chunkValid = file ? validateFileReadChunkNL(
clipboard, &record, &fileRequest) :
validateReadChunkNL(clipboard, &record);
if (!chunkValid)
{
KVMFRClipboardMessage cancellation = { 0 };
bool queued = false;
const bool cancelled = file && rejectMalformedFileReadNL(
clipboard, &record, &cancellation, &queued);
status = lgmpClientStreamReadRelease(
clipboard->hostToClientStream, &buffer);
if (status != LGMP_OK)
connectionFailed(clipboard, status);
LG_UNLOCK(clipboard->lock);
if (cancelled)
dispatchFileCancel(clipboard, &cancellation);
if (queued)
signalWorker(clipboard);
DEBUG_WARN("Ignoring stale or malformed LGMP clipboard stream data");
return status == LGMP_OK;
}
if (!file)
record.transfer = clipboard->readRequest;
clipboard->held = true;
clipboard->heldReady = true;
clipboard->heldStreamBuffer = buffer;
clipboard->heldRecord = record;
clipboard->heldFile = file;
clipboard->heldFileRequest = fileRequest;
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);
}
static bool processMessage(LGMPClipboard * clipboard, bool * processed)
{
*processed = false;
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;
}
*processed = true;
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);
dispatchRetiredFiles(clipboard);
if (changed)
notifyStatus(clipboard);
return status == LGMP_OK;
}
if (!clipboard->claimed)
{
status = lgmpClientMessageDone(clipboard->queue);
if (status != LGMP_OK)
connectionFailed(clipboard, status);
LG_UNLOCK(clipboard->lock);
return status == LGMP_OK;
}
if (type != KVMFR_CLIPBOARD_QUEUE_MESSAGE ||
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, false);
if (!valid)
{
KVMFRClipboardMessage cancellation = { 0 };
bool queued = false;
const bool cancelled = rejectMalformedFileReadNL(
clipboard, &record, &cancellation, &queued);
status = lgmpClientMessageDone(clipboard->queue);
if (status != LGMP_OK)
connectionFailed(clipboard, status);
LG_UNLOCK(clipboard->lock);
if (cancelled)
dispatchFileCancel(clipboard, &cancellation);
if (queued)
signalWorker(clipboard);
DEBUG_WARN("Ignoring malformed LGMP clipboard record");
return status == LGMP_OK;
}
status = lgmpClientMessageDone(clipboard->queue);
if (status != LGMP_OK)
{
connectionFailed(clipboard, status);
LG_UNLOCK(clipboard->lock);
return false;
}
bool dispatch = true;
bool fileRejected = false;
LG_ClipboardRequest cancelledRead = LG_CLIPBOARD_REQUEST_INVALID;
LG_ClipboardFileRequest fileRequest = { 0 };
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;
}
case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE:
{
if (!kvmfrClipboardTransferFromHelper(record.transfer))
{
dispatch = false;
break;
}
if (fileAcquisitionFindNL(clipboard, record.clipboardGeneration,
record.transfer, true))
{
fileRejected = enqueueFileFailureNL(clipboard, &record,
LG_CLIPBOARD_FILE_ERROR_INVALID);
dispatch = false;
break;
}
if (record.clipboardGeneration !=
clipboard->localClipboardGeneration ||
!(clipboard->localFormats & KVMFR_CLIPBOARD_FORMAT_MASK_FILES))
{
fileRejected = enqueueFileFailureNL(clipboard, &record,
LG_CLIPBOARD_FILE_ERROR_STALE);
dispatch = false;
break;
}
if (fileAcquisitionCountNL(clipboard) >=
KVMFR_CLIPBOARD_FILE_MAX_ACQUISITIONS)
{
fileRejected = enqueueFileFailureNL(clipboard, &record,
LG_CLIPBOARD_FILE_ERROR_NO_SPACE);
dispatch = false;
break;
}
struct FileAcquisition * acquisition = calloc(1, sizeof(*acquisition));
if (!acquisition)
{
fileRejected = enqueueFileFailureNL(clipboard, &record,
LG_CLIPBOARD_FILE_ERROR_NO_MEMORY);
dispatch = false;
break;
}
*acquisition = (struct FileAcquisition)
{
.dataset = record.clipboardGeneration,
.acquisition = record.transfer,
.fromHelper = true,
.next = clipboard->fileAcquisitions,
};
clipboard->fileAcquisitions = acquisition;
break;
}
case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED:
{
if (kvmfrClipboardTransferFromHelper(record.transfer))
{
dispatch = false;
break;
}
struct FileAcquisition * acquisition = fileAcquisitionFindNL(
clipboard, record.clipboardGeneration, record.transfer, false);
if (!acquisition || acquisition->active)
{
dispatch = false;
break;
}
if (record.token)
free(fileAcquisitionTakeNL(clipboard,
record.clipboardGeneration, record.transfer, false));
else
acquisition->active = true;
break;
}
case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE:
{
struct FileAcquisition * acquisition = fileAcquisitionTakeNL(
clipboard, record.clipboardGeneration, record.transfer, true);
if (!acquisition)
{
dispatch = false;
break;
}
free(acquisition);
break;
}
case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST:
{
if (!kvmfrClipboardTransferFromHelper(record.transfer))
{
dispatch = false;
break;
}
if (fileTransferFindNL(clipboard->fileWrites, record.transfer))
{
fileRejected = enqueueFileFailureNL(clipboard, &record,
LG_CLIPBOARD_FILE_ERROR_INVALID);
dispatch = false;
break;
}
if (!fileDatasetAcquiredNL(clipboard,
record.clipboardGeneration, true))
{
fileRejected = enqueueFileFailureNL(clipboard, &record,
LG_CLIPBOARD_FILE_ERROR_STALE);
dispatch = false;
break;
}
if (fileTransferCountNL(clipboard) >=
KVMFR_CLIPBOARD_FILE_MAX_REQUESTS)
{
fileRejected = enqueueFileFailureNL(clipboard, &record,
LG_CLIPBOARD_FILE_ERROR_NO_SPACE);
dispatch = false;
break;
}
struct FileTransfer * transfer = calloc(1, sizeof(*transfer));
if (!transfer)
{
fileRejected = enqueueFileFailureNL(clipboard, &record,
LG_CLIPBOARD_FILE_ERROR_NO_MEMORY);
dispatch = false;
break;
}
fileRequest = (LG_ClipboardFileRequest)
{
.dataset = record.clipboardGeneration,
.request = record.transfer,
.node = record.size,
.offset = record.offset,
.length = record.token == KVMFR_CLIPBOARD_FILE_OP_READ ?
record.flags : 0,
.operation = fromWireFileOperation(record.token),
};
transfer->request = fileRequest;
transfer->sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN;
transfer->next = clipboard->fileWrites;
clipboard->fileWrites = transfer;
break;
}
case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL:
{
const bool fromHelper = kvmfrClipboardTransferFromHelper(
record.transfer);
struct FileTransfer ** transfers = fromHelper ?
&clipboard->fileWrites : &clipboard->fileReads;
struct FileTransfer * found = fileTransferFindNL(
*transfers, record.transfer);
struct FileTransfer * transfer = found &&
found->request.dataset == record.clipboardGeneration ?
fileTransferTakeNL(transfers, record.transfer) : NULL;
struct FileAcquisition * acquisition = fileAcquisitionTakeNL(
clipboard, record.clipboardGeneration, record.transfer,
fromHelper);
if (!transfer && !acquisition)
{
dispatch = false;
break;
}
free(transfer);
free(acquisition);
break;
}
}
LG_UNLOCK(clipboard->lock);
if (!dispatch)
{
if (fileRejected)
signalWorker(clipboard);
else
DEBUG_WARN("Ignoring stale LGMP clipboard control record");
return true;
}
switch (record.type)
{
case KVMFR_CLIPBOARD_MESSAGE_OFFER:
dispatchOffer(clipboard, record.clipboardGeneration, 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;
case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE:
dispatchFileAcquire(clipboard, &record);
break;
case KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED:
dispatchFileAcquired(clipboard, &record);
break;
case KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE:
dispatchFileRelease(clipboard, &record);
break;
case KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST:
dispatchFileRequest(clipboard, &fileRequest);
break;
case KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL:
dispatchFileCancel(clipboard, &record);
break;
}
return true;
}
static int clipboardThread(void * opaque)
{
LGMPClipboard * clipboard = opaque;
LGMPStreamPollState streamPoll;
const LGMP_STATUS pollStatus = lgmpStreamPollInit(&streamPoll,
(struct LGMPStreamPollConfig)
{
.spinCount = 32U,
.minWaitUs = 50U,
.maxWaitUs = 1000U,
});
if (pollStatus != LGMP_OK)
{
DEBUG_ERROR("Failed to initialize LGMP clipboard polling: %s",
lgmpStatusString(pollStatus));
return -1;
}
while (!atomic_load_explicit(&clipboard->stop, memory_order_acquire))
{
bool running = true;
bool streamProgress = false;
unsigned drained = 0;
for (; drained < CLIPBOARD_DRAIN_MAX; ++drained)
{
if (!processHeld(clipboard))
{
running = false;
break;
}
dispatchRetiredFiles(clipboard);
bool queueProcessed = false;
if (!processMessage(clipboard, &queueProcessed))
{
running = false;
break;
}
dispatchRetiredFiles(clipboard);
bool streamProcessed = false;
if (!processStream(clipboard, &streamProcessed))
{
running = false;
break;
}
streamProgress |= streamProcessed;
dispatchRetiredFiles(clipboard);
if (!queueProcessed && !streamProcessed)
break;
}
if (!running)
break;
LG_ClipboardRequest readyRequest = LG_CLIPBOARD_REQUEST_INVALID;
uint64_t fileReady[CLIPBOARD_PENDING_MAX];
size_t fileReadyCount = 0;
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);
bool blockedFile = false;
for (struct FileTransfer * transfer = clipboard->fileWrites;
transfer; transfer = transfer->next)
if (transfer->blocked)
{
blockedFile = true;
break;
}
const bool writable =
(clipboard->writeBlocked || blockedFile) &&
dataWritableNL(clipboard);
if (writable)
streamProgress = true;
if (clipboard->writeBlocked && writable)
{
readyRequest = clipboard->writeBlockedRequest;
clipboard->writeBlocked = false;
}
if (writable)
for (struct FileTransfer * transfer = clipboard->fileWrites;
transfer && fileReadyCount < CLIPBOARD_PENDING_MAX;
transfer = transfer->next)
if (transfer->blocked)
{
transfer->blocked = false;
fileReady[fileReadyCount++] = transfer->request.request;
}
const bool active = drained == CLIPBOARD_DRAIN_MAX ||
clipboard->pendingCount || clipboard->held ||
clipboard->writeBlocked || clipboard->fileReads ||
clipboard->fileWrites;
const bool streamActive = clipboard->connected &&
(clipboard->claimed || clipboard->releasing);
LG_UNLOCK(clipboard->lock);
if (readyRequest != LG_CLIPBOARD_REQUEST_INVALID)
dispatchReady(clipboard, readyRequest);
for (size_t i = 0; i < fileReadyCount; ++i)
dispatchFileReady(clipboard, fileReady[i]);
if (atomic_load_explicit(&clipboard->stop, memory_order_acquire))
break;
if (drained == CLIPBOARD_DRAIN_MAX)
{
if (streamProgress)
lgmpStreamPollActivity(&streamPoll);
continue;
}
bool signaled;
if (streamActive)
{
if (streamProgress)
lgmpStreamPollActivity(&streamPoll);
const uint32_t waitUs = lgmpStreamPollIdle(&streamPoll);
if (!waitUs)
continue;
signaled = lgWaitEventNS(clipboard->event, waitUs * 1000U);
}
else
{
lgmpStreamPollActivity(&streamPoll);
signaled = lgWaitEvent(clipboard->event,
active ? CLIPBOARD_ACTIVE_POLL_MS : CLIPBOARD_IDLE_POLL_MS);
}
if (signaled)
lgmpStreamPollActivity(&streamPoll);
}
dispatchRetiredFiles(clipboard);
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;
uint64_t nonce = 0;
if (!randomBytes(&nonce, sizeof(nonce)))
{
free(clipboard);
return false;
}
nonce &= ~KVMFR_CLIPBOARD_TRANSFER_HELPER;
if (!nonce)
nonce = 1;
clipboard->transferSerial = nonce;
clipboard->localGenerationSerial =
(nonce ^ UINT64_C(0x4c47434c49504244)) &
~KVMFR_CLIPBOARD_TRANSFER_HELPER;
if (!clipboard->localGenerationSerial)
clipboard->localGenerationSerial = 1;
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->releasing = 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;
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_ACTIVE_POLL_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_ACTIVE_POLL_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)
{
const LGMP_STATUS status = lgmpClientStreamReadRelease(
clipboard->hostToClientStream, &clipboard->heldStreamBuffer);
if (status != LGMP_OK && status != LGMP_ERR_STREAM_STALE &&
status != LGMP_ERR_STREAM_UNBOUND)
DEBUG_WARN("Failed to release held clipboard data during "
"disconnect: %s", lgmpStatusString(status));
}
releaseOnDisconnect(clipboard);
PLGMPClientQueue queue = clipboard->queue;
clipboard->queue = NULL;
clipboard->thread = NULL;
clipboard->event = NULL;
clipboard->held = false;
memset(&clipboard->heldStreamBuffer, 0,
sizeof(clipboard->heldStreamBuffer));
detachStreamsNL(clipboard);
clearProtocolNL(clipboard);
LG_UNLOCK(clipboard->lock);
dispatchRetiredFiles(clipboard);
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;
uint64_t dataset;
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;
dataset = clipboard->remoteClipboardGeneration;
LG_UNLOCK(clipboard->lock);
const unsigned replayCount = available ?
formatsFromMask(formats, replay) : 0;
if (replayCount)
{
if ((formats & KVMFR_CLIPBOARD_FORMAT_MASK_FILES) &&
events->fileOffer)
events->fileOffer(eventOpaque, dataset);
if (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 releaseQueued = false;
bool releaseRequired = false;
LG_LOCK(clipboard->eventLock);
LG_LOCK(clipboard->lock);
clipboard->events = NULL;
clipboard->eventOpaque = NULL;
if (clipboard->claimed)
{
releaseRequired = true;
KVMFRClipboardMessage release = { 0 };
release.type = KVMFR_CLIPBOARD_MESSAGE_RELEASE;
releaseQueued = enqueueUrgentRecordNL(clipboard, release);
clipboard->claimed = false;
clipboard->releasing = true;
clipboard->available = false;
}
clearDataPlaneNL(clipboard);
if (clipboard->held)
clipboard->heldReady = true;
LG_UNLOCK(clipboard->lock);
LG_UNLOCK(clipboard->eventLock);
if (!releaseQueued && releaseRequired)
DEBUG_WARN("Failed to queue LGMP clipboard release; ownership will "
"expire");
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 = nextClientGeneration(clipboard);
const unsigned required = clipboard->claimed ? 1U : 2U;
const bool result =
clipboard->pendingCount <= CLIPBOARD_PENDING_NORMAL_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 (!kvmfrClipboardRepresentationFormatValid(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 = nextClientGeneration(clipboard);
const unsigned required = clipboard->claimed ? 1U : 2U;
const bool result =
clipboard->pendingCount <= CLIPBOARD_PENDING_NORMAL_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) ||
!kvmfrClipboardRepresentationFormatValid(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 ||
!kvmfrClipboardRepresentationFormatValid(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 bool offerFiles(void * opaque, uint64_t dataset)
{
LGMPClipboard * clipboard = opaque;
if (!dataset || kvmfrClipboardTransferFromHelper(dataset))
return false;
KVMFRClipboardMessage offer = { 0 };
offer.type = KVMFR_CLIPBOARD_MESSAGE_OFFER;
offer.clipboardGeneration = dataset;
offer.token = KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
LG_LOCK(clipboard->lock);
const unsigned required = clipboard->claimed ? 1U : 2U;
const bool result = clipboard->pendingCount <=
CLIPBOARD_PENDING_NORMAL_MAX - required && ensureClaimNL(clipboard) &&
enqueueRecordNL(clipboard, offer);
if (result)
{
clipboard->localClipboardGeneration = dataset;
clipboard->localFormats = KVMFR_CLIPBOARD_FORMAT_MASK_FILES;
}
LG_UNLOCK(clipboard->lock);
if (result)
signalWorker(clipboard);
return result;
}
static bool fileAcquire(void * opaque, uint64_t dataset,
uint64_t acquisitionId)
{
LGMPClipboard * clipboard = opaque;
if (!dataset || !kvmfrClipboardTransferFromClient(acquisitionId))
return false;
LG_LOCK(clipboard->lock);
if (fileAcquisitionCountNL(clipboard) >=
KVMFR_CLIPBOARD_FILE_MAX_ACQUISITIONS)
{
LG_UNLOCK(clipboard->lock);
return false;
}
struct FileAcquisition * acquisition = calloc(1, sizeof(*acquisition));
if (!acquisition)
{
LG_UNLOCK(clipboard->lock);
return false;
}
KVMFRClipboardMessage record = { 0 };
record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRE;
record.clipboardGeneration = dataset;
record.transfer = acquisitionId;
record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
const bool result = dataset == clipboard->remoteClipboardGeneration &&
(clipboard->remoteFormats & KVMFR_CLIPBOARD_FORMAT_MASK_FILES) &&
!fileAcquisitionFindNL(clipboard, dataset, acquisitionId, false) &&
ensureClaimNL(clipboard) && enqueueRecordNL(clipboard, record);
if (result)
{
*acquisition = (struct FileAcquisition)
{
.dataset = dataset,
.acquisition = acquisitionId,
.next = clipboard->fileAcquisitions,
};
clipboard->fileAcquisitions = acquisition;
}
LG_UNLOCK(clipboard->lock);
if (!result)
free(acquisition);
else
signalWorker(clipboard);
return result;
}
static bool fileAcquired(void * opaque, uint64_t dataset,
uint64_t acquisitionId, LG_ClipboardFileError error)
{
LGMPClipboard * clipboard = opaque;
if (!dataset || !kvmfrClipboardTransferFromHelper(acquisitionId) ||
!validFileError(error))
return false;
LG_LOCK(clipboard->lock);
struct FileAcquisition * acquisition = fileAcquisitionFindNL(
clipboard, dataset, acquisitionId, true);
KVMFRClipboardMessage record = { 0 };
record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_ACQUIRED;
record.clipboardGeneration = dataset;
record.transfer = acquisitionId;
record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
record.token = toWireFileError(error);
const bool result = acquisition && !acquisition->active &&
enqueueUrgentRecordNL(clipboard, record);
if (result && error == LG_CLIPBOARD_FILE_ERROR_NONE)
acquisition->active = true;
else if (result)
free(fileAcquisitionTakeNL(
clipboard, dataset, acquisitionId, true));
LG_UNLOCK(clipboard->lock);
if (result)
signalWorker(clipboard);
return result;
}
static bool fileRelease(void * opaque, uint64_t dataset,
uint64_t acquisitionId)
{
LGMPClipboard * clipboard = opaque;
if (!dataset || !kvmfrClipboardTransferFromClient(acquisitionId))
return false;
LG_LOCK(clipboard->lock);
struct FileAcquisition * acquisition = fileAcquisitionFindNL(
clipboard, dataset, acquisitionId, false);
KVMFRClipboardMessage record = { 0 };
record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_RELEASE;
record.clipboardGeneration = dataset;
record.transfer = acquisitionId;
record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
const bool result = acquisition && acquisition->active &&
enqueueUrgentRecordNL(clipboard, record);
if (result)
free(fileAcquisitionTakeNL(
clipboard, dataset, acquisitionId, false));
LG_UNLOCK(clipboard->lock);
if (result)
signalWorker(clipboard);
return result;
}
static bool fileRequest(void * opaque,
const LG_ClipboardFileRequest * descriptor)
{
LGMPClipboard * clipboard = opaque;
if (!descriptor || !descriptor->dataset ||
!kvmfrClipboardTransferFromClient(descriptor->request) ||
(descriptor->operation != LG_CLIPBOARD_FILE_LIST &&
descriptor->operation != LG_CLIPBOARD_FILE_READ) ||
(descriptor->operation == LG_CLIPBOARD_FILE_LIST &&
(descriptor->offset || descriptor->length)) ||
(descriptor->operation == LG_CLIPBOARD_FILE_READ &&
(!descriptor->length ||
descriptor->length > KVMFR_CLIPBOARD_FILE_READ_BYTES ||
descriptor->offset > UINT64_MAX - descriptor->length)))
return false;
KVMFRClipboardMessage record = { 0 };
record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_REQUEST;
record.clipboardGeneration = descriptor->dataset;
record.transfer = descriptor->request;
record.offset = descriptor->offset;
record.size = descriptor->node;
record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
record.flags = descriptor->operation ==
LG_CLIPBOARD_FILE_READ ? descriptor->length : 0;
record.token = toWireFileOperation(descriptor->operation);
LG_LOCK(clipboard->lock);
if (fileTransferCountNL(clipboard) >=
KVMFR_CLIPBOARD_FILE_MAX_REQUESTS)
{
LG_UNLOCK(clipboard->lock);
return false;
}
struct FileTransfer * transfer = calloc(1, sizeof(*transfer));
if (!transfer)
{
LG_UNLOCK(clipboard->lock);
return false;
}
const bool result = fileDatasetAcquiredNL(
clipboard, descriptor->dataset, false) &&
!fileTransferFindNL(clipboard->fileReads, descriptor->request) &&
enqueueRecordNL(clipboard, record);
if (result)
{
transfer->request = *descriptor;
transfer->sizeHint = KVMFR_CLIPBOARD_SIZE_UNKNOWN;
transfer->next = clipboard->fileReads;
clipboard->fileReads = transfer;
}
LG_UNLOCK(clipboard->lock);
if (!result)
free(transfer);
else
signalWorker(clipboard);
return result;
}
static bool sameFileRequest(const LG_ClipboardFileRequest * a,
const LG_ClipboardFileRequest * b)
{
return a->dataset == b->dataset && a->request == b->request &&
a->node == b->node && a->offset == b->offset &&
a->length == b->length && a->operation == b->operation;
}
static LG_ClipboardResult fileDataBegin(void * opaque,
const LG_ClipboardFileRequest * descriptor, uint64_t sizeHint)
{
LGMPClipboard * clipboard = opaque;
if (!descriptor || !kvmfrClipboardTransferFromHelper(descriptor->request))
return LG_CLIPBOARD_RESULT_FAILED;
LG_LOCK(clipboard->lock);
struct FileTransfer * transfer = fileTransferFindNL(
clipboard->fileWrites, descriptor->request);
const bool valid = transfer && !transfer->began &&
sameFileRequest(&transfer->request, descriptor) &&
(descriptor->operation != LG_CLIPBOARD_FILE_READ ||
sizeHint <= descriptor->length);
if (valid)
{
transfer->began = true;
transfer->sizeHint = sizeHint;
transfer->responseOffset = 0;
transfer->sequence = 0;
transfer->blocked = false;
transfer->wireEnded = false;
}
LG_UNLOCK(clipboard->lock);
return valid ? LG_CLIPBOARD_RESULT_ACCEPTED :
LG_CLIPBOARD_RESULT_FAILED;
}
static LG_ClipboardResult fileDataChunk(void * opaque,
const LG_ClipboardFileRequest * descriptor, uint64_t responseOffset,
const void * data, size_t size, bool end)
{
LGMPClipboard * clipboard = opaque;
if (!descriptor || !data || !size ||
size > KVMFR_CLIPBOARD_DATA_BYTES ||
responseOffset > UINT64_MAX - size)
return LG_CLIPBOARD_RESULT_FAILED;
LG_LOCK(clipboard->lock);
struct FileTransfer * transfer = fileTransferFindNL(
clipboard->fileWrites, descriptor->request);
if (!transfer || !transfer->began || transfer->wireEnded ||
!sameFileRequest(&transfer->request, descriptor) ||
transfer->responseOffset != responseOffset ||
(transfer->sizeHint != KVMFR_CLIPBOARD_SIZE_UNKNOWN &&
responseOffset + size > transfer->sizeHint) ||
(end && transfer->sizeHint != KVMFR_CLIPBOARD_SIZE_UNKNOWN &&
transfer->sizeHint != responseOffset + size) ||
(descriptor->operation == LG_CLIPBOARD_FILE_READ &&
responseOffset + size > descriptor->length))
{
LG_UNLOCK(clipboard->lock);
return LG_CLIPBOARD_RESULT_FAILED;
}
KVMFRClipboardMessage record = { 0 };
record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_DATA;
record.clipboardGeneration = descriptor->dataset;
record.transfer = descriptor->request;
record.offset = responseOffset;
record.sequence = transfer->sequence;
record.size = end ? responseOffset + size :
transfer->sequence ? KVMFR_CLIPBOARD_SIZE_UNKNOWN : transfer->sizeHint;
record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
record.flags = (transfer->sequence ? 0 :
KVMFR_CLIPBOARD_FLAG_BEGIN) |
(end ? KVMFR_CLIPBOARD_FLAG_END : 0);
record.token = toWireFileOperation(descriptor->operation);
record.length = (uint32_t)size;
const bool result = enqueueDataNL(clipboard, record, data);
if (result)
{
transfer->responseOffset += size;
++transfer->sequence;
transfer->wireEnded = end;
}
else
transfer->blocked = true;
LG_UNLOCK(clipboard->lock);
if (result)
signalWorker(clipboard);
return result ? LG_CLIPBOARD_RESULT_ACCEPTED :
LG_CLIPBOARD_RESULT_BLOCKED;
}
static LG_ClipboardResult fileDataEnd(void * opaque,
const LG_ClipboardFileRequest * descriptor, uint64_t finalSize)
{
LGMPClipboard * clipboard = opaque;
if (!descriptor)
return LG_CLIPBOARD_RESULT_FAILED;
LG_LOCK(clipboard->lock);
struct FileTransfer * transfer = fileTransferFindNL(
clipboard->fileWrites, descriptor->request);
if (!transfer || !transfer->began ||
!sameFileRequest(&transfer->request, descriptor) ||
transfer->responseOffset != finalSize ||
(transfer->sizeHint != KVMFR_CLIPBOARD_SIZE_UNKNOWN &&
transfer->sizeHint != finalSize))
{
LG_UNLOCK(clipboard->lock);
return LG_CLIPBOARD_RESULT_FAILED;
}
if (transfer->wireEnded)
{
free(fileTransferTakeNL(
&clipboard->fileWrites, descriptor->request));
LG_UNLOCK(clipboard->lock);
return LG_CLIPBOARD_RESULT_ACCEPTED;
}
KVMFRClipboardMessage record = { 0 };
record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_DATA;
record.clipboardGeneration = descriptor->dataset;
record.transfer = descriptor->request;
record.offset = finalSize;
record.sequence = transfer->sequence;
record.size = finalSize;
record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
record.flags = KVMFR_CLIPBOARD_FLAG_END |
(transfer->sequence ? 0 : KVMFR_CLIPBOARD_FLAG_BEGIN);
record.token = toWireFileOperation(descriptor->operation);
const bool result = enqueueDataNL(clipboard, record, NULL);
if (result)
free(fileTransferTakeNL(
&clipboard->fileWrites, descriptor->request));
else
transfer->blocked = true;
LG_UNLOCK(clipboard->lock);
if (result)
signalWorker(clipboard);
return result ? LG_CLIPBOARD_RESULT_ACCEPTED :
LG_CLIPBOARD_RESULT_BLOCKED;
}
static bool fileCancel(void * opaque, uint64_t dataset,
uint64_t request, LG_ClipboardFileError reason)
{
LGMPClipboard * clipboard = opaque;
if (!dataset || !request || reason == LG_CLIPBOARD_FILE_ERROR_NONE ||
!validFileError(reason))
return false;
LG_LOCK(clipboard->lock);
struct FileTransfer * read = fileTransferFindNL(
clipboard->fileReads, request);
struct FileTransfer * write = fileTransferFindNL(
clipboard->fileWrites, request);
struct FileAcquisition * acquisition = fileAcquisitionFindNL(
clipboard, dataset, request,
kvmfrClipboardTransferFromHelper(request));
KVMFRClipboardMessage record = { 0 };
record.type = KVMFR_CLIPBOARD_MESSAGE_FILE_CANCEL;
record.clipboardGeneration = dataset;
record.transfer = request;
record.format = KVMFR_CLIPBOARD_FORMAT_FILES;
record.token = toWireFileError(reason);
const bool matches = (read && read->request.dataset == dataset) ||
(write && write->request.dataset == dataset) || acquisition;
const bool result = matches && enqueueUrgentRecordNL(clipboard, record);
if (result)
{
free(fileTransferTakeNL(&clipboard->fileReads, request));
free(fileTransferTakeNL(&clipboard->fileWrites, request));
if (acquisition)
free(fileAcquisitionTakeNL(clipboard, dataset, request,
kvmfrClipboardTransferFromHelper(request)));
}
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,
.offerFiles = offerFiles,
.dataBegin = dataBegin,
.dataChunk = dataChunk,
.dataEnd = dataEnd,
.dataCancel = dataCancel,
.dataReady = dataReady,
.request = requestData,
.fileAcquire = fileAcquire,
.fileAcquired = fileAcquired,
.fileRelease = fileRelease,
.fileRequest = fileRequest,
.fileDataBegin = fileDataBegin,
.fileDataChunk = fileDataChunk,
.fileDataEnd = fileDataEnd,
.fileCancel = fileCancel,
};
const LG_ClipboardOps * lgmpClipboard_getOps(void)
{
return &CLIPBOARD_OPS;
}