Files
LookingGlass/client/tests/lgmp_clipboard_test.c
2026-08-14 10:22:37 +10:00

1079 lines
36 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 "../transports/LGMP/clipboard.h"
#include "common/KVMFRClipboard.h"
#include "common/LGMPConfig.h"
#include "common/debug.h"
#include <lgmp/client.h>
#include <lgmp/host.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#define TEST_SHM_SIZE (8U * 1024U * 1024U)
#define TEST_WAIT_MS 2500U
#define TEST_QUIET_MS 50U
#define TEST_MEMORY_MAX 128U
#define SLOT_BYTES \
(sizeof(KVMFRClipboardSlotHeader) + KVMFR_CLIPBOARD_DATA_BYTES)
#define CHECK(x) \
do \
{ \
if (!(x)) \
{ \
fprintf(stderr, "check failed at %s:%d: %s\n", \
__FILE__, __LINE__, #x); \
return false; \
} \
} \
while (0)
typedef struct EventTrace
{
atomic_uint notice;
atomic_uint release;
atomic_uint request;
atomic_uint requestCancel;
atomic_uint dataBegin;
atomic_uint dataChunk;
atomic_uint dataEnd;
atomic_uint dataCancel;
atomic_uint dataReady;
atomic_bool acceptRequest;
atomic_bool blockChunkOnce;
atomic_bool attached;
LG_ClipboardData noticeTypes[LG_CLIPBOARD_DATA_NONE];
size_t noticeCount;
LG_ClipboardRequest requestID;
LG_ClipboardData requestType;
LG_ClipboardRequest streamID;
LG_ClipboardData streamType;
uint64_t sizeHint;
uint64_t chunkOffset;
size_t chunkSize;
uint8_t chunkData[64];
uint64_t finalSize;
LG_ClipboardRequest cancelID;
LG_ClipboardCancelReason cancelReason;
}
EventTrace;
typedef struct TestState
{
void * memory;
PLGMPHost host;
PLGMPHostQueue queue;
PLGMPClient client;
LGMPClipboard * clipboard;
const LG_ClipboardOps * ops;
uint32_t clientID;
uint32_t hostSerial;
PLGMPMemory memories[TEST_MEMORY_MAX];
unsigned memoryCount;
PLGMPMemory grantMemory[KVMFR_CLIPBOARD_SLOT_COUNT];
atomic_uint statusCount;
atomic_bool statusAvailable;
atomic_uint statusGeneration;
atomic_bool autoAttach;
EventTrace events;
}
TestState;
typedef struct DisconnectTask
{
LGMPClipboard * clipboard;
atomic_bool done;
}
DisconnectTask;
static bool disconnectAndDrain(TestState * state,
uint32_t * releaseGeneration);
static void notice(void * opaque, const LG_ClipboardData types[],
size_t count)
{
EventTrace * trace = opaque;
trace->noticeCount = count;
if (count)
memcpy(trace->noticeTypes, types, count * sizeof(*types));
atomic_fetch_add_explicit(&trace->notice, 1, memory_order_release);
}
static void releaseNotice(void * opaque)
{
EventTrace * trace = opaque;
atomic_fetch_add_explicit(&trace->release, 1, memory_order_release);
}
static bool request(void * opaque, LG_ClipboardRequest id,
LG_ClipboardData type)
{
EventTrace * trace = opaque;
trace->requestID = id;
trace->requestType = type;
atomic_fetch_add_explicit(&trace->request, 1, memory_order_release);
return atomic_load_explicit(&trace->acceptRequest, memory_order_acquire);
}
static LG_ClipboardResult dataBegin(void * opaque,
LG_ClipboardRequest id, LG_ClipboardData type, uint64_t sizeHint)
{
EventTrace * trace = opaque;
trace->streamID = id;
trace->streamType = type;
trace->sizeHint = sizeHint;
atomic_fetch_add_explicit(&trace->dataBegin, 1, memory_order_release);
return LG_CLIPBOARD_RESULT_ACCEPTED;
}
static LG_ClipboardResult dataChunk(void * opaque,
LG_ClipboardRequest id, uint64_t offset,
const void * data, size_t size)
{
EventTrace * trace = opaque;
trace->streamID = id;
trace->chunkOffset = offset;
trace->chunkSize = size;
if (size <= sizeof(trace->chunkData))
memcpy(trace->chunkData, data, size);
atomic_fetch_add_explicit(&trace->dataChunk, 1, memory_order_release);
if (atomic_exchange_explicit(&trace->blockChunkOnce, false,
memory_order_acq_rel))
return LG_CLIPBOARD_RESULT_BLOCKED;
return LG_CLIPBOARD_RESULT_ACCEPTED;
}
static LG_ClipboardResult dataEnd(void * opaque,
LG_ClipboardRequest id, uint64_t size)
{
EventTrace * trace = opaque;
trace->streamID = id;
trace->finalSize = size;
atomic_fetch_add_explicit(&trace->dataEnd, 1, memory_order_release);
return LG_CLIPBOARD_RESULT_ACCEPTED;
}
static void dataCancelEvent(void * opaque, LG_ClipboardRequest id,
LG_ClipboardCancelReason reason)
{
EventTrace * trace = opaque;
trace->cancelID = id;
trace->cancelReason = reason;
atomic_fetch_add_explicit(&trace->dataCancel, 1, memory_order_release);
}
static void dataReadyEvent(void * opaque, LG_ClipboardRequest id)
{
EventTrace * trace = opaque;
trace->streamID = id;
atomic_fetch_add_explicit(&trace->dataReady, 1, memory_order_release);
}
static void requestCancelEvent(void * opaque, LG_ClipboardRequest id,
LG_ClipboardCancelReason reason)
{
EventTrace * trace = opaque;
trace->cancelID = id;
trace->cancelReason = reason;
atomic_fetch_add_explicit(
&trace->requestCancel, 1, memory_order_release);
}
static const LG_ClipboardEventOps EVENT_OPS =
{
.notice = notice,
.dataBegin = dataBegin,
.dataChunk = dataChunk,
.dataEnd = dataEnd,
.dataCancel = dataCancelEvent,
.dataReady = dataReadyEvent,
.requestCancel = requestCancelEvent,
.release = releaseNotice,
.request = request,
};
static void statusChanged(void * opaque, const LG_ClipboardStatus * status)
{
TestState * state = opaque;
atomic_store_explicit(&state->statusAvailable,
status->available, memory_order_release);
atomic_store_explicit(&state->statusGeneration,
status->generation, memory_order_release);
atomic_fetch_add_explicit(&state->statusCount, 1, memory_order_release);
if (status->available &&
atomic_load_explicit(&state->autoAttach, memory_order_acquire) &&
!atomic_exchange_explicit(
&state->events.attached, true, memory_order_acq_rel))
{
const bool attached = state->ops->attach(
state->clipboard, &EVENT_OPS, &state->events);
atomic_store_explicit(
&state->events.attached, attached, memory_order_release);
}
}
static bool hostProcess(TestState * state)
{
CHECK(lgmpHostProcess(state->host) == LGMP_OK);
return true;
}
static bool waitAtomic(TestState * state, atomic_uint * value,
unsigned expected)
{
for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
{
CHECK(hostProcess(state));
if (atomic_load_explicit(value, memory_order_acquire) >= expected)
return true;
usleep(1000);
}
return false;
}
static bool waitMemory(TestState * state, PLGMPMemory memory)
{
for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
{
CHECK(hostProcess(state));
if (!lgmpHostQueuePayloadPending(state->queue, memory))
return true;
usleep(1000);
}
return false;
}
static bool allocMemory(TestState * state, size_t size,
PLGMPMemory * result)
{
CHECK(state->memoryCount < TEST_MEMORY_MAX);
CHECK(lgmpHostMemAlloc(state->host, (uint32_t)size, result) == LGMP_OK);
state->memories[state->memoryCount++] = *result;
memset(lgmpHostMemPtr(*result), 0, size);
return true;
}
static bool postMemory(TestState * state, PLGMPMemory memory,
KVMFRClipboardQueueType type, uint32_t serial)
{
const uint64_t udata = KVMFR_CLIPBOARD_QUEUE_UDATA(type, serial);
for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
{
unsigned recipients = 0;
const LGMP_STATUS status = lgmpHostQueuePostForClients(state->queue,
udata, memory, &state->clientID, 1, &recipients);
if (status == LGMP_OK)
return recipients == 1;
CHECK(status == LGMP_ERR_QUEUE_FULL);
CHECK(hostProcess(state));
usleep(1000);
}
return false;
}
static uint32_t nextSerial(TestState * state)
{
if (++state->hostSerial == 0)
++state->hostSerial;
return state->hostSerial;
}
static bool postStatus(TestState * state, uint32_t endpointGeneration,
uint32_t ownerClientID, uint32_t ownerGeneration,
bool available, KVMFRClipboardFormatFlags formats)
{
PLGMPMemory memory;
CHECK(allocMemory(state, sizeof(KVMFRClipboardStatus), &memory));
KVMFRClipboardStatus * status = lgmpHostMemPtr(memory);
*status = (KVMFRClipboardStatus)
{
.version = KVMFR_CLIPBOARD_VERSION,
.flags = (available ? KVMFR_CLIPBOARD_STATUS_AVAILABLE : 0) |
(ownerClientID ? KVMFR_CLIPBOARD_STATUS_HAS_OWNER : 0),
.generation = endpointGeneration,
.ownerClientID = ownerClientID,
.ownerGeneration = ownerGeneration,
.lease = 1000,
.formats = available ? formats : 0,
.slotBytes = KVMFR_CLIPBOARD_DATA_BYTES,
};
CHECK(postMemory(state, memory, KVMFR_CLIPBOARD_QUEUE_STATUS,
nextSerial(state)));
CHECK(waitMemory(state, memory));
return true;
}
static bool postRecord(TestState * state,
const KVMFRClipboardMessage * record,
KVMFRClipboardQueueType type, const void * data)
{
const size_t bytes = type == KVMFR_CLIPBOARD_QUEUE_DATA ?
SLOT_BYTES : sizeof(*record);
PLGMPMemory memory;
CHECK(allocMemory(state, bytes, &memory));
memcpy(lgmpHostMemPtr(memory), record, sizeof(*record));
if (record->length && data)
memcpy((uint8_t *)lgmpHostMemPtr(memory) + sizeof(*record),
data, record->length);
CHECK(postMemory(state, memory, type, nextSerial(state)));
return true;
}
static bool postGrant(TestState * state, uint32_t generation,
uint32_t token)
{
CHECK(token >= 1 && token <= KVMFR_CLIPBOARD_SLOT_COUNT);
PLGMPMemory * memory = &state->grantMemory[token - 1];
if (!*memory)
CHECK(allocMemory(state, SLOT_BYTES, memory));
else
CHECK(waitMemory(state, *memory));
KVMFRClipboardSlotHeader * header = lgmpHostMemPtr(*memory);
memset(header, 0, sizeof(*header));
header->version = KVMFR_CLIPBOARD_VERSION;
header->type = KVMFR_CLIPBOARD_MESSAGE_GRANT;
header->generation = generation;
header->size = KVMFR_CLIPBOARD_DATA_BYTES;
header->token = token;
return postMemory(state, *memory,
KVMFR_CLIPBOARD_QUEUE_GRANT, token);
}
static bool readClient(TestState * state, KVMFRClipboardMessage * record)
{
for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
{
CHECK(hostProcess(state));
size_t size = sizeof(*record);
uint32_t clientID = 0;
const LGMP_STATUS status = lgmpHostReadDataWithSource(
state->queue, record, &size, &clientID);
if (status == LGMP_ERR_QUEUE_EMPTY)
{
usleep(1000);
continue;
}
CHECK(status == LGMP_OK);
CHECK(size == sizeof(*record));
CHECK(clientID == state->clientID);
CHECK(lgmpHostAckData(state->queue) == LGMP_OK);
return true;
}
return false;
}
static bool readType(TestState * state, KVMFRClipboardMessageType type,
KVMFRClipboardMessage * record)
{
for (unsigned i = 0; i < 32; ++i)
{
CHECK(readClient(state, record));
if (record->type == type)
return true;
}
return false;
}
static bool noClientData(TestState * state)
{
for (unsigned i = 0; i < TEST_QUIET_MS; ++i)
{
CHECK(hostProcess(state));
KVMFRClipboardMessage record;
size_t size = sizeof(record);
uint32_t clientID = 0;
const LGMP_STATUS status = lgmpHostReadDataWithSource(
state->queue, &record, &size, &clientID);
if (status == LGMP_ERR_QUEUE_EMPTY)
{
usleep(1000);
continue;
}
if (status == LGMP_OK)
lgmpHostAckData(state->queue);
CHECK(status == LGMP_ERR_QUEUE_EMPTY);
}
return true;
}
static bool claim(TestState * state, uint32_t endpointGeneration,
bool autoAttach, KVMFRClipboardMessage * result)
{
atomic_store(&state->autoAttach, autoAttach);
CHECK(postStatus(state, endpointGeneration, 0, 0, true, 0));
if (autoAttach)
{
for (unsigned i = 0; i < TEST_WAIT_MS &&
!atomic_load(&state->events.attached); ++i)
{
CHECK(hostProcess(state));
usleep(1000);
}
CHECK(atomic_load(&state->events.attached));
}
else
{
CHECK(state->ops->attach(
state->clipboard, &EVENT_OPS, &state->events));
atomic_store(&state->events.attached, true);
}
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CLAIM, result));
CHECK(result->version == KVMFR_CLIPBOARD_VERSION);
CHECK(result->generation != 0);
CHECK(result->token == endpointGeneration);
return true;
}
static bool own(TestState * state, uint32_t endpointGeneration,
uint32_t claimGeneration)
{
CHECK(postStatus(state, endpointGeneration, state->clientID,
claimGeneration, true, 0));
return true;
}
static KVMFRClipboardMessage hostRecord(
KVMFRClipboardMessageType type, uint32_t generation)
{
return (KVMFRClipboardMessage)
{
.version = KVMFR_CLIPBOARD_VERSION,
.type = type,
.generation = generation,
};
}
static bool offerAndRequest(TestState * state, uint32_t generation,
uint64_t clipboardGeneration, LG_ClipboardRequest appRequest,
KVMFRClipboardMessage * wireRequest)
{
KVMFRClipboardMessage offer =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, generation);
offer.clipboardGeneration = clipboardGeneration;
offer.token = KVMFR_CLIPBOARD_FORMAT_MASK_TEXT |
KVMFR_CLIPBOARD_FORMAT_MASK_PNG;
const unsigned notices = atomic_load(&state->events.notice) + 1;
CHECK(postRecord(state, &offer, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.notice, notices));
CHECK(state->events.noticeCount == 2);
CHECK(state->events.noticeTypes[0] == LG_CLIPBOARD_DATA_TEXT);
CHECK(state->events.noticeTypes[1] == LG_CLIPBOARD_DATA_PNG);
CHECK(!state->ops->request(state->clipboard,
appRequest + 1, LG_CLIPBOARD_DATA_JPEG));
CHECK(state->ops->request(state->clipboard,
appRequest, LG_CLIPBOARD_DATA_TEXT));
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_REQUEST, wireRequest));
CHECK(kvmfrClipboardTransferFromClient(wireRequest->transfer));
CHECK(wireRequest->generation == generation);
CHECK(wireRequest->clipboardGeneration == clipboardGeneration);
CHECK(wireRequest->format == KVMFR_CLIPBOARD_FORMAT_TEXT);
return true;
}
static bool testClaim(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(claim(state, 10, true, &claimRecord));
CHECK(own(state, 10, claimRecord.generation));
const LG_ClipboardData formats[] = { LG_CLIPBOARD_DATA_TEXT };
CHECK(state->ops->notifyTypes(state->clipboard, formats, 1));
KVMFRClipboardMessage offer;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &offer));
CHECK(offer.generation == claimRecord.generation);
CHECK(offer.clipboardGeneration != 0);
CHECK(offer.token == KVMFR_CLIPBOARD_FORMAT_MASK_TEXT);
return true;
}
static bool testOfferRequest(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(claim(state, 20, false, &claimRecord));
CHECK(own(state, 20, claimRecord.generation));
KVMFRClipboardMessage requestRecord;
CHECK(offerAndRequest(state, claimRecord.generation,
200, 77, &requestRecord));
return true;
}
static bool testInboundStream(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(claim(state, 30, false, &claimRecord));
CHECK(own(state, 30, claimRecord.generation));
KVMFRClipboardMessage requestRecord;
CHECK(offerAndRequest(state, claimRecord.generation,
300, 88, &requestRecord));
const uint8_t bytes[] = { 1, 2, 3, 4, 5 };
KVMFRClipboardMessage data =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, claimRecord.generation);
data.clipboardGeneration = 300;
data.transfer = requestRecord.transfer;
data.size = 5;
data.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
data.flags = KVMFR_CLIPBOARD_FLAG_BEGIN;
CHECK(postRecord(state, &data, KVMFR_CLIPBOARD_QUEUE_DATA, NULL));
CHECK(waitAtomic(state, &state->events.dataBegin, 1));
CHECK(atomic_load(&state->events.dataBegin) == 1);
CHECK(atomic_load(&state->events.dataChunk) == 0);
CHECK(state->events.streamID == 88);
CHECK(state->events.sizeHint == 5);
data.sequence = 1;
data.size = KVMFR_CLIPBOARD_SIZE_UNKNOWN;
data.flags = 0;
data.length = sizeof(bytes);
CHECK(postRecord(state, &data, KVMFR_CLIPBOARD_QUEUE_DATA, bytes));
CHECK(waitAtomic(state, &state->events.dataChunk, 1));
CHECK(state->events.chunkOffset == 0);
CHECK(state->events.chunkSize == sizeof(bytes));
CHECK(memcmp(state->events.chunkData, bytes, sizeof(bytes)) == 0);
data.sequence = 2;
data.offset = sizeof(bytes);
data.size = sizeof(bytes);
data.flags = KVMFR_CLIPBOARD_FLAG_END;
data.length = 0;
CHECK(postRecord(state, &data, KVMFR_CLIPBOARD_QUEUE_DATA, NULL));
CHECK(waitAtomic(state, &state->events.dataEnd, 1));
CHECK(atomic_load(&state->events.dataChunk) == 1);
CHECK(state->events.finalSize == 5);
KVMFRClipboardMessage request2;
CHECK(offerAndRequest(state, claimRecord.generation,
301, 89, &request2));
data = hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA,
claimRecord.generation);
data.clipboardGeneration = 301;
data.transfer = request2.transfer;
data.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
data.flags = KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END;
CHECK(postRecord(state, &data, KVMFR_CLIPBOARD_QUEUE_DATA, NULL));
CHECK(waitAtomic(state, &state->events.dataEnd, 2));
CHECK(atomic_load(&state->events.dataBegin) == 2);
CHECK(state->events.finalSize == 0);
return true;
}
static bool prepareOutbound(TestState * state, uint32_t endpoint,
KVMFRClipboardMessage * claimRecord, uint64_t transfer)
{
CHECK(claim(state, endpoint, false, claimRecord));
CHECK(own(state, endpoint, claimRecord->generation));
const LG_ClipboardData formats[] = { LG_CLIPBOARD_DATA_TEXT };
CHECK(state->ops->notifyTypes(state->clipboard, formats, 1));
KVMFRClipboardMessage offer;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &offer));
KVMFRClipboardMessage requestRecord =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST,
claimRecord->generation);
requestRecord.clipboardGeneration = offer.clipboardGeneration;
requestRecord.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | transfer;
requestRecord.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
const unsigned requests = atomic_load(&state->events.request) + 1;
CHECK(postRecord(state, &requestRecord,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.request, requests));
CHECK(state->events.requestID == requestRecord.transfer);
CHECK(state->events.requestType == LG_CLIPBOARD_DATA_TEXT);
return true;
}
static bool checkCommit(TestState * state, uint32_t token,
KVMFRClipboardMessage * commit, KVMFRClipboardMessage * slot,
const void ** data)
{
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_COMMIT, commit));
CHECK(commit->token == token);
*slot = *(KVMFRClipboardMessage *)
lgmpHostMemPtr(state->grantMemory[token - 1]);
CHECK(slot->type == KVMFR_CLIPBOARD_MESSAGE_DATA);
CHECK(slot->generation == commit->generation);
CHECK(slot->clipboardGeneration == commit->clipboardGeneration);
CHECK(slot->transfer == commit->transfer);
CHECK(slot->offset == commit->offset);
CHECK(slot->size == commit->size);
CHECK(slot->format == commit->format);
CHECK(slot->flags == commit->flags);
CHECK(slot->length == commit->length);
CHECK(slot->sequence == commit->sequence);
*data = (const uint8_t *)lgmpHostMemPtr(
state->grantMemory[token - 1]) + sizeof(*slot);
return true;
}
static bool testOutboundGrant(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(prepareOutbound(state, 40, &claimRecord, 1));
const LG_ClipboardRequest transfer = state->events.requestID;
CHECK(postGrant(state, claimRecord.generation, 1));
CHECK(waitMemory(state, state->grantMemory[0]));
CHECK(state->ops->dataBegin(state->clipboard, transfer,
LG_CLIPBOARD_DATA_TEXT, 5) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(noClientData(state));
const uint8_t first[] = { 1, 2, 3 };
CHECK(state->ops->dataChunk(state->clipboard, transfer,
0, first, sizeof(first)) == LG_CLIPBOARD_RESULT_ACCEPTED);
KVMFRClipboardMessage commit;
KVMFRClipboardMessage slot;
const void * data;
CHECK(checkCommit(state, 1, &commit, &slot, &data));
CHECK(slot.flags == KVMFR_CLIPBOARD_FLAG_BEGIN);
CHECK(slot.sequence == 0 && slot.offset == 0 && slot.size == 5);
CHECK(memcmp(data, first, sizeof(first)) == 0);
CHECK(postGrant(state, claimRecord.generation, 1));
CHECK(waitMemory(state, state->grantMemory[0]));
const uint8_t second[] = { 4, 5 };
CHECK(state->ops->dataChunk(state->clipboard, transfer,
3, second, sizeof(second)) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(checkCommit(state, 1, &commit, &slot, &data));
CHECK(slot.flags == 0 && slot.sequence == 1 && slot.offset == 3);
CHECK(slot.size == KVMFR_CLIPBOARD_SIZE_UNKNOWN);
CHECK(memcmp(data, second, sizeof(second)) == 0);
CHECK(postGrant(state, claimRecord.generation, 1));
CHECK(waitMemory(state, state->grantMemory[0]));
CHECK(state->ops->dataEnd(state->clipboard, transfer, 5) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(checkCommit(state, 1, &commit, &slot, &data));
CHECK(slot.flags == KVMFR_CLIPBOARD_FLAG_END);
CHECK(slot.sequence == 2 && slot.offset == 5 && slot.size == 5);
CHECK(slot.length == 0);
KVMFRClipboardMessage requestRecord =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST,
claimRecord.generation);
requestRecord.clipboardGeneration = slot.clipboardGeneration;
requestRecord.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | 2;
requestRecord.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
CHECK(postRecord(state, &requestRecord,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.request, 2));
CHECK(postGrant(state, claimRecord.generation, 1));
CHECK(waitMemory(state, state->grantMemory[0]));
CHECK(state->ops->dataBegin(state->clipboard, requestRecord.transfer,
LG_CLIPBOARD_DATA_TEXT, 0) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(state->ops->dataEnd(state->clipboard,
requestRecord.transfer, 0) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(checkCommit(state, 1, &commit, &slot, &data));
CHECK(slot.flags == (KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END));
CHECK(slot.sequence == 0 && slot.offset == 0 && slot.size == 0);
return true;
}
static bool testBlocked(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(prepareOutbound(state, 50, &claimRecord, 3));
const LG_ClipboardRequest transfer = state->events.requestID;
CHECK(state->ops->dataBegin(state->clipboard, transfer,
LG_CLIPBOARD_DATA_TEXT, 2) == LG_CLIPBOARD_RESULT_ACCEPTED);
const uint8_t bytes[] = { 8, 9 };
CHECK(state->ops->dataChunk(state->clipboard, transfer,
0, bytes, sizeof(bytes)) == LG_CLIPBOARD_RESULT_BLOCKED);
CHECK(postGrant(state, claimRecord.generation, 1));
CHECK(waitAtomic(state, &state->events.dataReady, 1));
CHECK(state->events.streamID == transfer);
CHECK(state->ops->dataChunk(state->clipboard, transfer,
0, bytes, sizeof(bytes)) == LG_CLIPBOARD_RESULT_ACCEPTED);
KVMFRClipboardMessage commit;
KVMFRClipboardMessage slot;
const void * data;
CHECK(checkCommit(state, 1, &commit, &slot, &data));
KVMFRClipboardMessage requestRecord;
CHECK(offerAndRequest(state, claimRecord.generation,
501, 5001, &requestRecord));
atomic_store(&state->events.blockChunkOnce, true);
KVMFRClipboardMessage wire =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, claimRecord.generation);
wire.clipboardGeneration = 501;
wire.transfer = requestRecord.transfer;
wire.size = 1;
wire.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
wire.flags = KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END;
wire.length = 1;
CHECK(postRecord(state, &wire, KVMFR_CLIPBOARD_QUEUE_DATA, bytes));
CHECK(waitAtomic(state, &state->events.dataChunk, 1));
CHECK(atomic_load(&state->events.dataEnd) == 0);
CHECK(state->ops->dataReady(state->clipboard, 5001));
CHECK(waitAtomic(state, &state->events.dataEnd, 1));
CHECK(atomic_load(&state->events.dataChunk) == 2);
return true;
}
static bool testCancel(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(prepareOutbound(state, 60, &claimRecord, 4));
const LG_ClipboardRequest outbound = state->events.requestID;
KVMFRClipboardMessage cancel =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_CANCEL, claimRecord.generation);
cancel.clipboardGeneration = 1;
cancel.transfer = outbound;
cancel.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
cancel.token = LG_CLIPBOARD_CANCEL_ABORTED;
CHECK(postRecord(state, &cancel, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.requestCancel, 1));
CHECK(state->events.cancelID == outbound);
KVMFRClipboardMessage requestRecord;
CHECK(offerAndRequest(state, claimRecord.generation,
601, 6001, &requestRecord));
cancel.clipboardGeneration = 601;
cancel.transfer = requestRecord.transfer;
cancel.token = LG_CLIPBOARD_CANCEL_REPLACED;
CHECK(postRecord(state, &cancel, KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.dataCancel, 1));
CHECK(state->events.cancelID == 6001);
CHECK(state->events.cancelReason == LG_CLIPBOARD_CANCEL_REPLACED);
KVMFRClipboardMessage outboundRequest =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_REQUEST,
claimRecord.generation);
outboundRequest.clipboardGeneration = 1;
outboundRequest.transfer = KVMFR_CLIPBOARD_TRANSFER_HELPER | 5;
outboundRequest.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
CHECK(postRecord(state, &outboundRequest,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitAtomic(state, &state->events.request, 2));
CHECK(state->ops->dataCancel(state->clipboard,
outboundRequest.transfer, LG_CLIPBOARD_CANCEL_ABORTED));
KVMFRClipboardMessage wireCancel;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CANCEL, &wireCancel));
CHECK(wireCancel.transfer == outboundRequest.transfer);
CHECK(wireCancel.clipboardGeneration == 1);
CHECK(wireCancel.format == KVMFR_CLIPBOARD_FORMAT_TEXT);
CHECK(wireCancel.token == LG_CLIPBOARD_CANCEL_ABORTED);
return true;
}
static bool testRestart(TestState * state)
{
KVMFRClipboardMessage oldClaim;
CHECK(claim(state, 70, false, &oldClaim));
CHECK(own(state, 70, oldClaim.generation));
const LG_ClipboardData formats[] = { LG_CLIPBOARD_DATA_TEXT };
CHECK(state->ops->notifyTypes(state->clipboard, formats, 1));
KVMFRClipboardMessage oldOffer;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &oldOffer));
CHECK(postStatus(state, 71, 0, 0, true, 0));
KVMFRClipboardMessage newClaim;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CLAIM, &newClaim));
CHECK(newClaim.generation != oldClaim.generation);
CHECK(newClaim.token == 71);
KVMFRClipboardMessage replay;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_OFFER, &replay));
CHECK(replay.generation == newClaim.generation);
CHECK(replay.clipboardGeneration == oldOffer.clipboardGeneration);
CHECK(own(state, 71, newClaim.generation));
CHECK(postStatus(state, 71, 0, 0, true, 0));
KVMFRClipboardMessage reclaimed;
CHECK(readType(state, KVMFR_CLIPBOARD_MESSAGE_CLAIM, &reclaimed));
CHECK(reclaimed.generation != newClaim.generation);
CHECK(reclaimed.token == 71);
return true;
}
static void * disconnectThread(void * opaque)
{
DisconnectTask * task = opaque;
lgmpClipboard_disconnect(task->clipboard);
atomic_store(&task->done, true);
return NULL;
}
static bool disconnectAndDrain(TestState * state,
uint32_t * releaseGeneration)
{
DisconnectTask task = { .clipboard = state->clipboard };
atomic_init(&task.done, false);
pthread_t thread;
CHECK(pthread_create(&thread, NULL, disconnectThread, &task) == 0);
bool valid = true;
for (unsigned i = 0; i < TEST_WAIT_MS && !atomic_load(&task.done); ++i)
{
if (lgmpHostProcess(state->host) != LGMP_OK)
valid = false;
for (;;)
{
KVMFRClipboardMessage record;
size_t size = sizeof(record);
uint32_t clientID = 0;
const LGMP_STATUS status = lgmpHostReadDataWithSource(
state->queue, &record, &size, &clientID);
if (status == LGMP_ERR_QUEUE_EMPTY)
break;
if (status != LGMP_OK)
{
valid = false;
break;
}
valid &= size == sizeof(record);
valid &= clientID == state->clientID;
if (record.type == KVMFR_CLIPBOARD_MESSAGE_RELEASE &&
releaseGeneration)
*releaseGeneration = record.generation;
if (lgmpHostAckData(state->queue) != LGMP_OK)
valid = false;
}
usleep(1000);
}
valid &= pthread_join(thread, NULL) == 0;
valid &= atomic_load(&task.done);
return valid;
}
static bool testDisconnect(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(claim(state, 80, false, &claimRecord));
CHECK(own(state, 80, claimRecord.generation));
uint32_t releaseGeneration = 0;
CHECK(disconnectAndDrain(state, &releaseGeneration));
CHECK(releaseGeneration == claimRecord.generation);
return true;
}
static bool testMalformed(TestState * state)
{
KVMFRClipboardMessage claimRecord;
CHECK(claim(state, 90, false, &claimRecord));
CHECK(own(state, 90, claimRecord.generation));
const unsigned statusCount = atomic_load(&state->statusCount);
PLGMPMemory badStatusMemory;
CHECK(allocMemory(state, sizeof(KVMFRClipboardStatus),
&badStatusMemory));
KVMFRClipboardStatus * badStatus = lgmpHostMemPtr(badStatusMemory);
*badStatus = (KVMFRClipboardStatus)
{
.version = KVMFR_CLIPBOARD_VERSION,
.flags = KVMFR_CLIPBOARD_STATUS_AVAILABLE,
.generation = 91,
.lease = 1000,
};
CHECK(postMemory(state, badStatusMemory,
KVMFR_CLIPBOARD_QUEUE_STATUS, nextSerial(state)));
CHECK(waitMemory(state, badStatusMemory));
CHECK(atomic_load(&state->statusCount) == statusCount);
KVMFRClipboardMessage malformed =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_OFFER, claimRecord.generation + 1);
malformed.clipboardGeneration = 900;
malformed.token = KVMFR_CLIPBOARD_FORMAT_MASK_TEXT;
CHECK(postRecord(state, &malformed,
KVMFR_CLIPBOARD_QUEUE_MESSAGE, NULL));
CHECK(waitMemory(state, state->memories[state->memoryCount - 1]));
CHECK(atomic_load(&state->events.notice) == 0);
malformed.generation = claimRecord.generation;
CHECK(postRecord(state, &malformed, KVMFR_CLIPBOARD_QUEUE_DATA, NULL));
CHECK(waitMemory(state, state->memories[state->memoryCount - 1]));
CHECK(atomic_load(&state->events.notice) == 0);
CHECK(postGrant(state, claimRecord.generation, 1));
CHECK(waitMemory(state, state->grantMemory[0]));
CHECK(postGrant(state, claimRecord.generation, 1));
CHECK(waitMemory(state, state->grantMemory[0]));
KVMFRClipboardMessage requestRecord;
CHECK(offerAndRequest(state, claimRecord.generation,
901, 9001, &requestRecord));
KVMFRClipboardMessage data =
hostRecord(KVMFR_CLIPBOARD_MESSAGE_DATA, claimRecord.generation);
data.clipboardGeneration = 901;
data.transfer = requestRecord.transfer;
data.offset = 1;
data.size = 1;
data.format = KVMFR_CLIPBOARD_FORMAT_TEXT;
data.flags = KVMFR_CLIPBOARD_FLAG_BEGIN |
KVMFR_CLIPBOARD_FLAG_END;
CHECK(postRecord(state, &data, KVMFR_CLIPBOARD_QUEUE_DATA, NULL));
CHECK(waitMemory(state, state->memories[state->memoryCount - 1]));
CHECK(atomic_load(&state->events.dataBegin) == 0);
const uint8_t byte = 42;
data.offset = 0;
data.length = 1;
CHECK(postRecord(state, &data, KVMFR_CLIPBOARD_QUEUE_DATA, &byte));
CHECK(waitAtomic(state, &state->events.dataEnd, 1));
CHECK(atomic_load(&state->events.dataBegin) == 1);
CHECK(atomic_load(&state->events.dataChunk) == 1);
CHECK(state->events.streamID == 9001);
CHECK(state->events.chunkData[0] == byte);
return true;
}
static bool stateInit(TestState * state)
{
memset(state, 0, sizeof(*state));
state->memory = MAP_FAILED;
atomic_init(&state->statusCount, 0);
atomic_init(&state->statusAvailable, false);
atomic_init(&state->statusGeneration, 0);
atomic_init(&state->autoAttach, false);
atomic_init(&state->events.notice, 0);
atomic_init(&state->events.release, 0);
atomic_init(&state->events.request, 0);
atomic_init(&state->events.requestCancel, 0);
atomic_init(&state->events.dataBegin, 0);
atomic_init(&state->events.dataChunk, 0);
atomic_init(&state->events.dataEnd, 0);
atomic_init(&state->events.dataCancel, 0);
atomic_init(&state->events.dataReady, 0);
atomic_init(&state->events.acceptRequest, true);
atomic_init(&state->events.blockChunkOnce, false);
atomic_init(&state->events.attached, false);
state->memory = mmap(NULL, TEST_SHM_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
CHECK(state->memory != MAP_FAILED);
const uint32_t sessionData = UINT32_C(0x12345678);
CHECK(lgmpHostInit(state->memory, TEST_SHM_SIZE, &state->host,
sizeof(sessionData), (uint8_t *)&sessionData) == LGMP_OK);
const struct LGMPQueueConfig config =
{
.queueID = LGMP_Q_CLIPBOARD,
.numMessages = LGMP_Q_CLIPBOARD_LEN,
.subTimeout = 1000,
};
CHECK(lgmpHostQueueNew(state->host, config, &state->queue) == LGMP_OK);
CHECK(lgmpClientInit(state->memory, TEST_SHM_SIZE, &state->client) ==
LGMP_OK);
usleep(300000);
CHECK(hostProcess(state));
uint32_t dataSize;
uint32_t remoteVersion;
uint8_t * data;
CHECK(lgmpClientSessionInit(state->client, &dataSize, &data,
&state->clientID, &remoteVersion) == LGMP_OK);
CHECK(state->clientID != 0);
CHECK(dataSize == sizeof(sessionData));
CHECK(memcmp(data, &sessionData, sizeof(sessionData)) == 0);
CHECK(lgmpClipboard_create(state->client, &state->clipboard));
CHECK(lgmpClipboard_connect(state->clipboard, state->clientID));
state->ops = lgmpClipboard_getOps();
CHECK(state->ops);
state->ops->setStatusListener(
state->clipboard, statusChanged, state);
CHECK(atomic_load(&state->statusCount) == 1);
CHECK(!atomic_load(&state->statusAvailable));
for (unsigned i = 0; i < TEST_WAIT_MS; ++i)
{
CHECK(hostProcess(state));
if (lgmpHostQueueHasSubs(state->queue))
return true;
usleep(1000);
}
return false;
}
static void stateFree(TestState * state)
{
if (state->clipboard)
{
state->ops->setStatusListener(state->clipboard, NULL, NULL);
if (atomic_load(&state->events.attached))
state->ops->detach(state->clipboard);
disconnectAndDrain(state, NULL);
lgmpClipboard_destroy(&state->clipboard);
}
lgmpClientFree(&state->client);
for (unsigned i = 0; i < state->memoryCount; ++i)
lgmpHostMemFree(&state->memories[i]);
lgmpHostFree(&state->host);
if (state->memory != MAP_FAILED)
munmap(state->memory, TEST_SHM_SIZE);
}
typedef bool (*TestFn)(TestState * state);
static const struct
{
const char * name;
TestFn run;
}
TESTS[] =
{
{ "claim" , testClaim },
{ "offer-request" , testOfferRequest },
{ "inbound-stream", testInboundStream },
{ "outbound-grant", testOutboundGrant },
{ "blocked" , testBlocked },
{ "cancel" , testCancel },
{ "restart" , testRestart },
{ "disconnect" , testDisconnect },
{ "malformed" , testMalformed },
};
int main(int argc, char * argv[])
{
if (argc != 2)
{
fprintf(stderr, "usage: %s <test-case>\n", argv[0]);
return 2;
}
TestFn test = NULL;
for (unsigned i = 0; i < sizeof(TESTS) / sizeof(TESTS[0]); ++i)
if (strcmp(argv[1], TESTS[i].name) == 0)
{
test = TESTS[i].run;
break;
}
if (!test)
{
fprintf(stderr, "unknown test case: %s\n", argv[1]);
return 2;
}
debug_init();
TestState state = { 0 };
state.memory = MAP_FAILED;
const bool initialized = stateInit(&state);
const bool passed = initialized && test(&state);
stateFree(&state);
return passed ? 0 : 1;
}