[client] clipboard: add direct LGMP synchronization

This commit is contained in:
Geoffrey McRae
2026-08-14 06:27:12 +10:00
parent b786167f72
commit be2f79a412
21 changed files with 5228 additions and 350 deletions

View File

@@ -29,8 +29,9 @@
#include <unistd.h>
#include <wayland-client.h>
#include "app.h"
#include "../../src/clipboard.h"
#include "common/debug.h"
#include "common/KVMFRClipboard.h"
struct DataOffer {
bool isSelfCopy;
@@ -221,12 +222,18 @@ static bool clipboardReadClaim(struct ClipboardRead * data)
return current;
}
static void clipboardReadRelease(struct ClipboardRead * data)
{
if (atomic_fetch_sub_explicit(
&data->references, 1, memory_order_acq_rel) == 1)
free(data);
}
static void clipboardReadCleanup(void * opaque)
{
struct ClipboardRead * data = opaque;
close(data->fd);
free(data->buf);
free(data);
clipboardReadRelease(data);
}
// Destination client handlers.
@@ -302,6 +309,9 @@ static void dataDeviceHandleSelection(void * opaque,
{
if (!offer)
{
LG_LOCK(wlCb.lock);
wlCb.selectionSource = NULL;
LG_UNLOCK(wlCb.lock);
waylandCBInvalidate();
return;
}
@@ -309,6 +319,12 @@ static void dataDeviceHandleSelection(void * opaque,
struct DataOffer * extra = wl_data_offer_get_user_data(offer);
if (!hasAnyMimetype(extra->mimetypes) || extra->isSelfCopy)
{
if (!extra->isSelfCopy)
{
LG_LOCK(wlCb.lock);
wlCb.selectionSource = NULL;
LG_UNLOCK(wlCb.lock);
}
wl_data_offer_set_user_data(offer, NULL);
for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i)
free(extra->mimetypes[i]);
@@ -318,7 +334,7 @@ static void dataDeviceHandleSelection(void * opaque,
return;
}
int idx = 0;
size_t idx = 0;
enum LG_ClipboardData types[LG_CLIPBOARD_DATA_NONE];
for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i)
if (extra->mimetypes[i])
@@ -329,6 +345,7 @@ static void dataDeviceHandleSelection(void * opaque,
struct ClipboardRead * read = clipboardReadTakeCurrentNL();
struct wl_data_offer * oldOffer = wlCb.offer;
memcpy(oldMimetypes, wlCb.mimetypes, sizeof(oldMimetypes));
wlCb.selectionSource = NULL;
wlCb.offer = offer;
memcpy(wlCb.mimetypes, extra->mimetypes, sizeof(wlCb.mimetypes));
LG_UNLOCK(wlCb.lock);
@@ -341,14 +358,14 @@ static void dataDeviceHandleSelection(void * opaque,
{
const LG_ClipboardRequest request = read->request;
clipboardReadRetire(read);
app_clipboardAbort(request);
lgClipboard_abort(request);
}
if (oldOffer && oldOffer != offer)
wl_data_offer_destroy(oldOffer);
for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i)
free(oldMimetypes[i]);
app_clipboardNotifyTypes(types, idx);
lgClipboard_notifyTypes(types, idx);
}
static void dataDeviceHandleEnter(void * data, struct wl_data_device * device,
@@ -425,54 +442,208 @@ bool waylandCBInit(void)
return true;
}
enum ClipboardReadOperation
{
CLIPBOARD_READ_BEGIN,
CLIPBOARD_READ_CHUNK,
CLIPBOARD_READ_END,
};
enum ClipboardReadCallResult
{
CLIPBOARD_READ_CALL_STALE,
CLIPBOARD_READ_CALL_ACCEPTED,
CLIPBOARD_READ_CALL_BLOCKED,
CLIPBOARD_READ_CALL_RETRY,
CLIPBOARD_READ_CALL_FAILED,
};
/* The provider's ready callback can run after its operation has returned but
* before this thread records BLOCKED. Keep the call in progress until the
* result is committed under wlCb.lock so that edge is retained. */
static enum ClipboardReadCallResult clipboardReadCall(
struct ClipboardRead * data, enum ClipboardReadOperation operation)
{
LG_ClipboardRequest request;
LG_ClipboardData type;
uint64_t offset;
size_t pending;
LG_LOCK(wlCb.lock);
if (wlCb.currentRead != data || data->blocked || data->calling)
{
LG_UNLOCK(wlCb.lock);
return CLIPBOARD_READ_CALL_STALE;
}
data->calling = true;
data->readyPending = false;
request = data->request;
type = data->type;
offset = data->offset;
pending = data->pending;
LG_UNLOCK(wlCb.lock);
LG_ClipboardResult result;
switch (operation)
{
case CLIPBOARD_READ_BEGIN:
result = lgClipboard_dataBegin(
request, type, LG_CLIPBOARD_SIZE_UNKNOWN);
break;
case CLIPBOARD_READ_CHUNK:
result = lgClipboard_dataChunk(
request, offset, data->buffer, pending);
break;
case CLIPBOARD_READ_END:
result = lgClipboard_dataEnd(request, offset);
break;
default:
result = LG_CLIPBOARD_RESULT_FAILED;
break;
}
LG_LOCK(wlCb.lock);
if (wlCb.currentRead != data)
{
LG_UNLOCK(wlCb.lock);
return CLIPBOARD_READ_CALL_STALE;
}
data->calling = false;
if (result == LG_CLIPBOARD_RESULT_BLOCKED)
{
if (data->readyPending)
{
data->readyPending = false;
LG_UNLOCK(wlCb.lock);
return CLIPBOARD_READ_CALL_RETRY;
}
data->blocked = true;
waylandPollUpdate(data->fd, 0);
LG_UNLOCK(wlCb.lock);
return CLIPBOARD_READ_CALL_BLOCKED;
}
data->readyPending = false;
if (result != LG_CLIPBOARD_RESULT_ACCEPTED)
{
LG_UNLOCK(wlCb.lock);
return CLIPBOARD_READ_CALL_FAILED;
}
switch (operation)
{
case CLIPBOARD_READ_BEGIN:
data->begun = true;
break;
case CLIPBOARD_READ_CHUNK:
data->offset += data->pending;
data->pending = 0;
break;
case CLIPBOARD_READ_END:
break;
}
LG_UNLOCK(wlCb.lock);
return CLIPBOARD_READ_CALL_ACCEPTED;
}
static void clipboardReadCallback(uint32_t events, void * opaque)
{
struct ClipboardRead * data = opaque;
LG_LOCK(wlCb.lock);
const bool active = wlCb.currentRead == data;
const bool busy = active && (data->blocked || data->calling);
LG_UNLOCK(wlCb.lock);
if (!active)
if (!active || busy)
return;
if (events & EPOLLERR)
{
if (clipboardReadClaim(data))
app_clipboardAbort(data->request);
lgClipboard_abort(data->request);
return;
}
ssize_t result = read(data->fd,
data->buf + data->numRead, data->size - data->numRead);
if (result < 0)
bool readAttempted = false;
for (;;)
{
DEBUG_ERROR("Failed to read from clipboard: %s", strerror(errno));
if (clipboardReadClaim(data))
app_clipboardAbort(data->request);
return;
}
enum ClipboardReadOperation operation;
bool haveOperation = true;
int readError = 0;
if (result == 0)
{
if (clipboardReadClaim(data))
app_clipboardData(
data->request, data->type, data->buf, data->numRead);
return;
}
data->numRead += result;
if (data->numRead >= data->size)
{
data->size *= 2;
void * nbuf = realloc(data->buf, data->size);
if (!nbuf)
LG_LOCK(wlCb.lock);
if (wlCb.currentRead != data || data->blocked || data->calling)
{
DEBUG_ERROR("Failed to realloc clipboard buffer: %s", strerror(errno));
if (clipboardReadClaim(data))
app_clipboardAbort(data->request);
LG_UNLOCK(wlCb.lock);
return;
}
data->buf = nbuf;
if (!data->begun)
operation = CLIPBOARD_READ_BEGIN;
else if (data->pending)
operation = CLIPBOARD_READ_CHUNK;
else if (data->eof)
operation = CLIPBOARD_READ_END;
else if (!readAttempted)
{
const ssize_t result = read(
data->fd, data->buffer, sizeof(data->buffer));
readAttempted = true;
if (result < 0)
{
readError = errno;
haveOperation = false;
}
else if (result == 0)
{
data->eof = true;
operation = CLIPBOARD_READ_END;
}
else
{
data->pending = (size_t)result;
operation = CLIPBOARD_READ_CHUNK;
}
}
else
haveOperation = false;
LG_UNLOCK(wlCb.lock);
if (readError)
{
if (readError == EAGAIN || readError == EWOULDBLOCK)
return;
DEBUG_ERROR("Failed to read from clipboard: %s", strerror(readError));
if (clipboardReadClaim(data))
lgClipboard_abort(data->request);
return;
}
if (!haveOperation)
return;
const enum ClipboardReadCallResult result =
clipboardReadCall(data, operation);
if (result == CLIPBOARD_READ_CALL_RETRY ||
result == CLIPBOARD_READ_CALL_ACCEPTED)
{
if (operation == CLIPBOARD_READ_END &&
result == CLIPBOARD_READ_CALL_ACCEPTED)
{
clipboardReadClaim(data);
return;
}
continue;
}
if (result == CLIPBOARD_READ_CALL_FAILED)
clipboardReadClaim(data);
return;
}
}
@@ -491,10 +662,10 @@ void waylandCBInvalidate(void)
{
const LG_ClipboardRequest request = read->request;
clipboardReadRetire(read);
app_clipboardAbort(request);
lgClipboard_abort(request);
}
app_clipboardRelease();
lgClipboard_release();
if (offer)
wl_data_offer_destroy(offer);
@@ -512,7 +683,21 @@ void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type)
if (pipe(fds) < 0)
{
DEBUG_ERROR("Failed to get a clipboard pipe: %s", strerror(errno));
app_clipboardAbort(request);
lgClipboard_abort(request);
return;
}
const int readFlags = fcntl(fds[0], F_GETFL);
const int readFdFlags = fcntl(fds[0], F_GETFD);
const int writeFdFlags = fcntl(fds[1], F_GETFD);
if (readFlags < 0 || readFdFlags < 0 || writeFdFlags < 0 ||
fcntl(fds[0], F_SETFD, readFdFlags | FD_CLOEXEC) < 0 ||
fcntl(fds[1], F_SETFD, writeFdFlags | FD_CLOEXEC) < 0 ||
fcntl(fds[0], F_SETFL, readFlags | O_NONBLOCK) < 0)
{
DEBUG_ERROR("Failed to configure clipboard pipe: %s", strerror(errno));
close(fds[0]);
close(fds[1]);
lgClipboard_abort(request);
return;
}
@@ -522,26 +707,17 @@ void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type)
DEBUG_ERROR("Failed to allocate memory to read clipboard");
close(fds[0]);
close(fds[1]);
app_clipboardAbort(request);
lgClipboard_abort(request);
return;
}
data->fd = fds[0];
data->size = 4096;
data->numRead = 0;
data->buf = malloc(data->size);
data->request = request;
data->type = type;
if (!data->buf)
*data = (struct ClipboardRead)
{
DEBUG_ERROR("Failed to allocate memory to receive clipboard data");
close(data->fd);
close(fds[1]);
free(data);
app_clipboardAbort(request);
return;
}
.fd = fds[0],
.request = request,
.type = type,
};
atomic_init(&data->references, 1);
LG_LOCK(wlCb.lock);
struct ClipboardRead * old = clipboardReadTakeCurrentNL();
@@ -564,7 +740,7 @@ void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type)
{
const LG_ClipboardRequest oldRequest = old->request;
clipboardReadRetire(old);
app_clipboardAbort(oldRequest);
lgClipboard_abort(oldRequest);
}
if (!registered)
{
@@ -572,46 +748,302 @@ void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type)
DEBUG_ERROR("Failed to register clipboard read into epoll: %s",
strerror(errno));
clipboardReadCleanup(data);
app_clipboardAbort(request);
lgClipboard_abort(request);
return;
}
}
void waylandCBRequestReady(LG_ClipboardRequest request)
{
struct ClipboardRead * failed = NULL;
struct ClipboardRead * retry = NULL;
LG_LOCK(wlCb.lock);
struct ClipboardRead * data = wlCb.currentRead;
if (data && data->request == request && data->calling)
data->readyPending = true;
else if (data && data->request == request && data->blocked)
{
data->blocked = false;
if (!waylandPollUpdate(data->fd, EPOLLIN))
failed = clipboardReadTakeCurrentNL();
else
{
atomic_fetch_add_explicit(
&data->references, 1, memory_order_relaxed);
retry = data;
}
}
LG_UNLOCK(wlCb.lock);
if (retry)
{
/* A blocked operation may already own bytes in data->buffer. Retry it
* directly: an empty pipe with an open writer has no EPOLLIN state with
* which to wake the pending operation. The extra reference covers
* cancellation while this non-poll dispatch is in progress. */
clipboardReadCallback(0, retry);
clipboardReadRelease(retry);
}
if (failed)
{
const LG_ClipboardRequest id = failed->request;
clipboardReadRetire(failed);
lgClipboard_abort(id);
}
}
void waylandCBRequestCancel(LG_ClipboardRequest request,
LG_ClipboardCancelReason reason)
{
(void)reason;
LG_LOCK(wlCb.lock);
struct ClipboardRead * data = wlCb.currentRead;
if (!data || data->request != request)
data = NULL;
else
clipboardReadTakeCurrentNL();
LG_UNLOCK(wlCb.lock);
clipboardReadRetire(data);
}
struct ClipboardWrite
{
int fd;
size_t pos;
struct CountedBuffer * buffer;
LG_Lock lock;
int fd;
LG_ClipboardRequest request;
LG_ClipboardData type;
uint64_t offset;
size_t pos;
size_t pending;
bool begun;
bool ended;
bool blocked;
bool discard;
bool pollOwned;
bool pollRegistered;
bool streamActive;
bool requesting;
uint8_t buffer[KVMFR_CLIPBOARD_DATA_BYTES];
};
static void clipboardWriteDestroy(struct ClipboardWrite * data)
{
LG_LOCK_FREE(data->lock);
free(data);
}
static void clipboardWriteRetireStream(struct ClipboardWrite * data)
{
int fd = -1;
bool destroy;
LG_LOCK(data->lock);
data->streamActive = false;
if (data->pollRegistered)
{
data->pollRegistered = false;
fd = data->fd;
}
destroy = !data->pollOwned && !data->requesting;
LG_UNLOCK(data->lock);
if (fd >= 0)
waylandPollUnregister(fd);
else if (destroy)
clipboardWriteDestroy(data);
}
static LG_ClipboardResult clipboardWriteBegin(void * opaque,
LG_ClipboardData type, uint64_t sizeHint)
{
(void)sizeHint;
struct ClipboardWrite * data = opaque;
LG_LOCK(data->lock);
if (data->begun || data->ended || !data->streamActive ||
type != data->type)
{
LG_UNLOCK(data->lock);
return LG_CLIPBOARD_RESULT_FAILED;
}
data->begun = true;
LG_UNLOCK(data->lock);
return LG_CLIPBOARD_RESULT_ACCEPTED;
}
static LG_ClipboardResult clipboardWriteChunk(void * opaque,
uint64_t offset, const void * buffer, size_t size)
{
struct ClipboardWrite * data = opaque;
LG_LOCK(data->lock);
if (!data->begun || data->ended || !data->streamActive ||
!buffer || !size)
{
LG_UNLOCK(data->lock);
return LG_CLIPBOARD_RESULT_FAILED;
}
if (data->pending)
{
data->blocked = true;
LG_UNLOCK(data->lock);
return LG_CLIPBOARD_RESULT_BLOCKED;
}
if (offset != data->offset)
{
LG_UNLOCK(data->lock);
return LG_CLIPBOARD_RESULT_FAILED;
}
if (data->discard)
{
data->offset += size;
LG_UNLOCK(data->lock);
return LG_CLIPBOARD_RESULT_ACCEPTED;
}
if (size > sizeof(data->buffer))
{
LG_UNLOCK(data->lock);
return LG_CLIPBOARD_RESULT_FAILED;
}
memcpy(data->buffer, buffer, size);
data->pending = size;
data->pos = 0;
if (!waylandPollUpdate(data->fd, EPOLLOUT))
{
data->discard = true;
data->pending = 0;
data->offset += size;
}
LG_UNLOCK(data->lock);
return LG_CLIPBOARD_RESULT_ACCEPTED;
}
static LG_ClipboardResult clipboardWriteEnd(void * opaque,
uint64_t finalSize)
{
struct ClipboardWrite * data = opaque;
LG_LOCK(data->lock);
if (!data->begun || data->ended || !data->streamActive)
{
LG_UNLOCK(data->lock);
return LG_CLIPBOARD_RESULT_FAILED;
}
if (data->pending)
{
data->blocked = true;
LG_UNLOCK(data->lock);
return LG_CLIPBOARD_RESULT_BLOCKED;
}
if (finalSize != data->offset)
{
LG_UNLOCK(data->lock);
return LG_CLIPBOARD_RESULT_FAILED;
}
data->ended = true;
LG_UNLOCK(data->lock);
clipboardWriteRetireStream(data);
return LG_CLIPBOARD_RESULT_ACCEPTED;
}
static void clipboardWriteCancel(void * opaque,
LG_ClipboardCancelReason reason)
{
(void)reason;
struct ClipboardWrite * data = opaque;
clipboardWriteRetireStream(data);
}
static const LG_ClipboardStreamOps clipboardWriteStream =
{
.begin = clipboardWriteBegin,
.chunk = clipboardWriteChunk,
.end = clipboardWriteEnd,
.cancel = clipboardWriteCancel,
};
static void clipboardWriteCleanup(void * opaque)
{
struct ClipboardWrite * data = opaque;
LG_LOCK(data->lock);
data->pollOwned = false;
data->pollRegistered = false;
close(data->fd);
countedBufferRelease(&data->buffer);
free(data);
data->fd = -1;
const bool destroy = !data->streamActive && !data->requesting;
LG_UNLOCK(data->lock);
if (destroy)
clipboardWriteDestroy(data);
}
static void clipboardWriteCallback(uint32_t events, void * opaque)
{
struct ClipboardWrite * data = opaque;
if (events & EPOLLERR)
if (events & (EPOLLERR | EPOLLHUP))
goto error;
ssize_t written = write(data->fd, data->buffer->data + data->pos, data->buffer->size - data->pos);
LG_LOCK(data->lock);
if (!data->pending)
{
waylandPollUpdate(data->fd, 0);
LG_UNLOCK(data->lock);
return;
}
const ssize_t written = write(data->fd, data->buffer + data->pos,
data->pending - data->pos);
if (written < 0)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
LG_UNLOCK(data->lock);
return;
}
if (errno != EPIPE)
DEBUG_ERROR("Failed to write clipboard data: %s", strerror(errno));
LG_UNLOCK(data->lock);
goto error;
}
data->pos += written;
if (data->pos < data->buffer->size)
if (data->pos < data->pending)
{
LG_UNLOCK(data->lock);
return;
}
data->offset += data->pending;
data->pending = 0;
data->pos = 0;
waylandPollUpdate(data->fd, 0);
const bool ready = data->blocked;
data->blocked = false;
const LG_ClipboardRequest request = data->request;
LG_UNLOCK(data->lock);
if (ready)
lgClipboard_requestReady(request);
return;
error:
waylandPollUnregister(data->fd);
LG_LOCK(data->lock);
data->discard = true;
data->offset += data->pending;
data->pending = 0;
data->pos = 0;
int fd = -1;
if (data->pollRegistered)
{
data->pollRegistered = false;
fd = data->fd;
}
const bool errorReady = data->blocked;
data->blocked = false;
const LG_ClipboardRequest errorRequest = data->request;
LG_UNLOCK(data->lock);
if (fd >= 0)
waylandPollUnregister(fd);
if (errorReady)
lgClipboard_requestReady(errorRequest);
}
static void dataSourceHandleTarget(void * data, struct wl_data_source * source,
@@ -627,23 +1059,68 @@ static void dataSourceHandleSend(void * data, struct wl_data_source * source,
struct WCBTransfer * transfer = (struct WCBTransfer *) data;
if (containsMimetype(transfer->mimetypes, mimetype))
{
struct ClipboardWrite * data = malloc(sizeof(*data));
struct ClipboardWrite * data = calloc(1, sizeof(*data));
if (!data)
{
DEBUG_ERROR("Out of memory trying to allocate ClipboardWrite");
goto error;
}
data->fd = fd;
data->pos = 0;
data->buffer = transfer->data;
countedBufferAddRef(transfer->data);
data->fd = fd;
data->request = LG_CLIPBOARD_REQUEST_INVALID;
data->type = transfer->type;
LG_LOCK_INIT(data->lock);
const int flags = fcntl(fd, F_GETFL);
const int fdFlags = fcntl(fd, F_GETFD);
if (flags < 0 || fdFlags < 0 ||
fcntl(fd, F_SETFD, fdFlags | FD_CLOEXEC) < 0 ||
fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0)
{
DEBUG_ERROR("Failed to make clipboard pipe nonblocking: %s",
strerror(errno));
clipboardWriteDestroy(data);
goto error;
}
data->pollOwned = true;
data->pollRegistered = true;
data->streamActive = true;
if (waylandPollRegisterWithCleanup(fd,
clipboardWriteCallback, data, clipboardWriteCleanup, EPOLLOUT))
return;
clipboardWriteCallback, data, clipboardWriteCleanup, 0))
{
LG_LOCK(data->lock);
data->requesting = true;
LG_UNLOCK(data->lock);
const bool result = lgClipboard_requestStream(transfer->type,
&clipboardWriteStream, data, &data->request);
countedBufferRelease(&data->buffer);
free(data);
int retireFd = -1;
bool destroy = false;
LG_LOCK(data->lock);
data->requesting = false;
if (!result || data->request == LG_CLIPBOARD_REQUEST_INVALID ||
!data->streamActive)
{
data->streamActive = false;
if (data->pollRegistered)
{
data->pollRegistered = false;
retireFd = data->fd;
}
destroy = !data->pollOwned;
}
LG_UNLOCK(data->lock);
if (retireFd >= 0)
waylandPollUnregister(retireFd);
else if (destroy)
clipboardWriteDestroy(data);
return;
}
data->pollOwned = false;
data->pollRegistered = false;
data->streamActive = false;
clipboardWriteDestroy(data);
}
error:
@@ -654,7 +1131,18 @@ static void dataSourceHandleCancelled(void * data,
struct wl_data_source * source)
{
struct WCBTransfer * transfer = (struct WCBTransfer *) data;
countedBufferRelease(&transfer->data);
DEBUG_ASSERT(transfer->source == source);
LG_LOCK(wlCb.lock);
struct WCBTransfer ** link = &wlCb.sources;
while (*link && *link != transfer)
link = &(*link)->next;
if (*link)
*link = transfer->next;
if (wlCb.selectionSource == source)
wlCb.selectionSource = NULL;
LG_UNLOCK(wlCb.lock);
free(transfer);
wl_data_source_destroy(source);
}
@@ -665,12 +1153,8 @@ static const struct wl_data_source_listener dataSourceListener = {
.cancelled = dataSourceHandleCancelled,
};
static void waylandCBReplyFn(void * opaque, LG_ClipboardData type,
const uint8_t * data, uint32_t size)
static void waylandCBPublish(LG_ClipboardData type)
{
if (type == LG_CLIPBOARD_DATA_NONE)
return;
struct WCBTransfer * transfer = malloc(sizeof(*transfer));
if (!transfer)
{
@@ -679,35 +1163,61 @@ static void waylandCBReplyFn(void * opaque, LG_ClipboardData type,
}
transfer->mimetypes = cbTypeToMimetypes(type);
transfer->data = countedBufferNew(size);
if (!transfer->data)
{
DEBUG_ERROR("Out of memory when allocating clipboard buffer");
free(transfer);
return;
}
if (size)
memcpy(transfer->data->data, data, size);
transfer->type = type;
transfer->next = NULL;
struct wl_data_source * source =
wl_data_device_manager_create_data_source(wlWm.dataDeviceManager);
wl_data_source_add_listener(source, &dataSourceListener, transfer);
if (!source)
{
DEBUG_ERROR("Failed to create clipboard data source");
free(transfer);
return;
}
transfer->source = source;
if (wl_data_source_add_listener(source, &dataSourceListener, transfer) < 0)
{
DEBUG_ERROR("Failed to listen to clipboard data source");
wl_data_source_destroy(source);
free(transfer);
return;
}
for (const char ** mimetype = transfer->mimetypes; *mimetype; mimetype++)
wl_data_source_offer(source, *mimetype);
wl_data_source_offer(source, wlCb.lgMimetype);
wl_data_device_set_selection(wlCb.dataDevice, source,
wlWm.keyboardEnterSerial);
LG_LOCK(wlCb.lock);
if (wlCb.dataDevice)
{
transfer->next = wlCb.sources;
wlCb.sources = transfer;
wlCb.selectionSource = source;
wl_data_device_set_selection(wlCb.dataDevice, source,
wlWm.keyboardEnterSerial);
}
else
{
wl_data_source_destroy(source);
free(transfer);
}
LG_UNLOCK(wlCb.lock);
}
void waylandCBNotice(LG_ClipboardData type)
{
if (!app_clipboardRequest(type, waylandCBReplyFn, NULL))
DEBUG_ERROR("Failed to request remote clipboard data");
waylandCBPublish(type);
}
void waylandCBRelease(void)
{
LG_LOCK(wlCb.lock);
if (wlCb.dataDevice && wlCb.selectionSource)
{
wlCb.selectionSource = NULL;
wl_data_device_set_selection(wlCb.dataDevice, NULL,
wlWm.keyboardEnterSerial);
}
LG_UNLOCK(wlCb.lock);
}
void waylandCBFree(void)
@@ -718,9 +1228,15 @@ void waylandCBFree(void)
struct wl_data_offer * offer = wlCb.offer;
struct wl_data_offer * dndOffer = wlCb.dndOffer;
struct wl_data_device * dataDevice = wlCb.dataDevice;
struct WCBTransfer * sources = wlCb.sources;
if (dataDevice && wlCb.selectionSource)
wl_data_device_set_selection(dataDevice, NULL,
wlWm.keyboardEnterSerial);
wlCb.offer = NULL;
wlCb.dndOffer = NULL;
wlCb.dataDevice = NULL;
wlCb.selectionSource = NULL;
wlCb.sources = NULL;
memcpy(mimetypes, wlCb.mimetypes, sizeof(mimetypes));
memset(wlCb.mimetypes, 0, sizeof(wlCb.mimetypes));
LG_UNLOCK(wlCb.lock);
@@ -732,6 +1248,13 @@ void waylandCBFree(void)
wl_data_offer_destroy(dndOffer);
if (dataDevice)
wl_data_device_release(dataDevice);
while (sources)
{
struct WCBTransfer * next = sources->next;
wl_data_source_destroy(sources->source);
free(sources);
sources = next;
}
for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i)
free(mimetypes[i]);

View File

@@ -133,16 +133,18 @@ bool waylandPollRegisterWithCleanup(int fd, WaylandPollCallback callback,
return false;
node->fd = fd;
node->events = events;
node->callback = callback;
node->cleanup = cleanup;
node->opaque = opaque;
atomic_init(&node->removed, false);
LG_LOCK(wlWm.pollLock);
if (epoll_ctl(wlWm.epollFd, EPOLL_CTL_ADD, fd, &(struct epoll_event) {
.events = events,
.data = (epoll_data_t) { .ptr = node },
}) < 0)
if (events && epoll_ctl(wlWm.epollFd, EPOLL_CTL_ADD, fd,
&(struct epoll_event) {
.events = events,
.data = (epoll_data_t) { .ptr = node },
}) < 0)
{
LG_UNLOCK(wlWm.pollLock);
free(node);
@@ -184,7 +186,8 @@ bool waylandPollUnregister(int fd)
return false;
}
if (epoll_ctl(wlWm.epollFd, EPOLL_CTL_DEL, fd, NULL) < 0)
if (node->events &&
epoll_ctl(wlWm.epollFd, EPOLL_CTL_DEL, fd, NULL) < 0)
{
LG_UNLOCK(wlWm.pollLock);
DEBUG_ERROR("Failed to unregister from epoll: %s", strerror(errno));
@@ -199,3 +202,45 @@ bool waylandPollUnregister(int fd)
});
return true;
}
bool waylandPollUpdate(int fd, uint32_t events)
{
bool result = false;
LG_LOCK(wlWm.pollLock);
struct WaylandPoll * node;
wl_list_for_each(node, &wlWm.poll, link)
{
if (node->fd != fd)
continue;
int operation;
if (!node->events && events)
operation = EPOLL_CTL_ADD;
else if (node->events && !events)
operation = EPOLL_CTL_DEL;
else if (events)
operation = EPOLL_CTL_MOD;
else
{
result = true;
break;
}
struct epoll_event event =
{
.events = events,
.data = (epoll_data_t) { .ptr = node },
};
if (epoll_ctl(wlWm.epollFd, operation, fd,
operation == EPOLL_CTL_DEL ? NULL : &event) < 0)
DEBUG_ERROR("Failed to update epoll events: %s", strerror(errno));
else
{
node->events = events;
result = true;
}
break;
}
LG_UNLOCK(wlWm.pollLock);
return result;
}

View File

@@ -355,8 +355,10 @@ struct LG_DisplayServerOps LGDS_Wayland =
.setHDRImageDesc = waylandHDRCallback,
.cbInit = waylandCBInit,
.cbNotice = waylandCBNotice,
.cbRelease = waylandCBRelease,
.cbRequest = waylandCBRequest
.cbInit = waylandCBInit,
.cbNotice = waylandCBNotice,
.cbRelease = waylandCBRelease,
.cbRequest = waylandCBRequest,
.cbRequestReady = waylandCBRequestReady,
.cbRequestCancel = waylandCBRequestCancel,
};

View File

@@ -35,8 +35,8 @@
#include "app.h"
#include "egl_dynprocs.h"
#include "common/KVMFRClipboard.h"
#include "common/locking.h"
#include "common/countedbuffer.h"
#include "common/ringbuffer.h"
#include "interface/displayserver.h"
#include "interface/desktop.h"
@@ -65,6 +65,7 @@ typedef void (*WaylandPollCleanup)(void * opaque);
struct WaylandPoll
{
int fd;
uint32_t events;
atomic_bool removed;
WaylandPollCallback callback;
WaylandPollCleanup cleanup;
@@ -286,18 +287,26 @@ struct WaylandDSState
struct WCBTransfer
{
struct CountedBuffer * data;
const char ** mimetypes;
LG_ClipboardData type;
const char ** mimetypes;
struct wl_data_source * source;
struct WCBTransfer * next;
};
struct ClipboardRead
{
int fd;
size_t size;
size_t numRead;
uint8_t * buf;
LG_ClipboardRequest request;
LG_ClipboardData type;
atomic_uint references;
int fd;
LG_ClipboardRequest request;
LG_ClipboardData type;
uint64_t offset;
size_t pending;
bool begun;
bool blocked;
bool calling;
bool readyPending;
bool eof;
uint8_t buffer[KVMFR_CLIPBOARD_DATA_BYTES];
};
struct WCBState
@@ -309,6 +318,9 @@ struct WCBState
struct wl_data_offer * offer;
struct wl_data_offer * dndOffer;
struct wl_data_source * selectionSource;
struct WCBTransfer * sources;
LG_Lock lock;
struct ClipboardRead * currentRead;
};
@@ -325,6 +337,9 @@ void waylandActivationRequestActivation(void);
bool waylandCBInit(void);
void waylandCBFree(void);
void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type);
void waylandCBRequestReady(LG_ClipboardRequest request);
void waylandCBRequestCancel(LG_ClipboardRequest request,
LG_ClipboardCancelReason reason);
void waylandCBNotice(LG_ClipboardData type);
void waylandCBRelease(void);
void waylandCBInvalidate(void);
@@ -417,6 +432,7 @@ bool waylandPollRegister(int fd, WaylandPollCallback callback, void * opaque, ui
bool waylandPollRegisterWithCleanup(int fd, WaylandPollCallback callback,
void * opaque, WaylandPollCleanup cleanup, uint32_t events);
bool waylandPollUnregister(int fd);
bool waylandPollUpdate(int fd, uint32_t events);
// presentation module
struct WaylandPresentationFrame;

File diff suppressed because it is too large Load Diff

View File

@@ -32,5 +32,8 @@ bool x11CBInit(void);
void x11CBNotice(LG_ClipboardData type);
void x11CBRelease(void);
void x11CBRequest(LG_ClipboardRequest request, LG_ClipboardData type);
void x11CBRequestReady(LG_ClipboardRequest request);
void x11CBRequestCancel(LG_ClipboardRequest request,
LG_ClipboardCancelReason reason);
#endif

View File

@@ -2314,8 +2314,10 @@ struct LG_DisplayServerOps LGDS_X11 =
.getFullscreen = x11GetFullscreen,
.minimize = x11Minimize,
.cbInit = x11CBInit,
.cbNotice = x11CBNotice,
.cbRelease = x11CBRelease,
.cbRequest = x11CBRequest
.cbInit = x11CBInit,
.cbNotice = x11CBNotice,
.cbRelease = x11CBRelease,
.cbRequest = x11CBRequest,
.cbRequestReady = x11CBRequestReady,
.cbRequestCancel = x11CBRequestCancel,
};

View File

@@ -173,14 +173,6 @@ void app_overlayConfigRegister(const char * title,
void app_overlayConfigRegisterTab(const char * title,
void (*callback)(void * udata, int * id), void * udata);
void app_clipboardRelease(void);
void app_clipboardNotifyTypes(const LG_ClipboardData types[], int count);
void app_clipboardData(LG_ClipboardRequest request,
LG_ClipboardData type, const void * data, size_t size);
void app_clipboardAbort(LG_ClipboardRequest request);
bool app_clipboardRequest(LG_ClipboardData type,
LG_ClipboardReplyFn replyFn, void * opaque);
/**
* Show an alert on screen
* @param type The alert type

View File

@@ -66,6 +66,7 @@ enum
LG_TRANSPORT_FEATURE_WINDOW_SIZE = 0x2,
LG_TRANSPORT_FEATURE_FRAME_SCHEDULE = 0x4,
LG_TRANSPORT_FEATURE_INPUT = 0x8,
LG_TRANSPORT_FEATURE_CLIPBOARD = 0x10,
};
typedef uint32_t LG_TransportFeatureFlags;

View File

@@ -23,7 +23,6 @@
#include "main.h"
#include "core.h"
#include "util.h"
#include "clipboard.h"
#include "render_queue.h"
#include "evdev.h"
#include "input.h"
@@ -349,35 +348,6 @@ void app_handleEnterEvent(bool entered)
}
}
void app_clipboardRelease(void)
{
lgClipboard_release();
}
void app_clipboardNotifyTypes(const LG_ClipboardData types[], int count)
{
if (count < 0)
return;
lgClipboard_notifyTypes(types, (size_t)count);
}
void app_clipboardData(LG_ClipboardRequest request,
const LG_ClipboardData type, const void * data, size_t size)
{
lgClipboard_data(request, type, data, size);
}
void app_clipboardAbort(LG_ClipboardRequest request)
{
lgClipboard_abort(request);
}
bool app_clipboardRequest(LG_ClipboardData type,
LG_ClipboardReplyFn replyFn, void * opaque)
{
return lgClipboard_request(type, replyFn, opaque);
}
static int mapInputToImGuiButton(uint32_t button)
{
switch (button)

View File

@@ -1761,6 +1761,11 @@ static bool requestClipboard(LG_ClipboardData type,
const bool queued = available &&
ll_push(clipboard.requests, request);
/* Publish the request before the provider can make it visible to its
* worker. A provider event may otherwise reach the stream sink before the
* caller has learned which request it belongs to. */
if (queued && resultId)
*resultId = id;
bool result = queued;
if (queued)
result = clipboard.active.ops->request(clipboard.active.opaque,
@@ -1769,20 +1774,14 @@ static bool requestClipboard(LG_ClipboardData type,
LG_UNLOCK_SHARED(clipboard.activeLock);
if (result)
{
if (resultId)
*resultId = id;
return true;
}
if (queued)
{
ClipboardRequest * failed = takeRequest(id);
if (!failed)
{
if (resultId)
*resultId = id;
return true;
}
if (resultId)
*resultId = LG_CLIPBOARD_REQUEST_INVALID;
freeRequest(failed);
return false;
}

View File

@@ -45,6 +45,9 @@ LG_ClipboardResult lgClipboard_dataEnd(LG_ClipboardRequest request,
void lgClipboard_data(LG_ClipboardRequest request,
LG_ClipboardData type, const void * data, size_t size);
void lgClipboard_abort(LG_ClipboardRequest request);
/* request is published before provider dispatch. Its storage must remain
* valid until this function returns and may be observed by stream callbacks
* before then. */
bool lgClipboard_requestStream(LG_ClipboardData type,
const LG_ClipboardStreamOps * stream, void * opaque,
LG_ClipboardRequest * request);

View File

@@ -103,6 +103,38 @@ foreach(name IN LISTS LGMP_INPUT_CASES)
)
endforeach()
add_executable(lgmp-clipboard-tests
lgmp_clipboard_test.c
../transports/LGMP/clipboard.c
)
target_include_directories(lgmp-clipboard-tests PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/../src"
)
target_link_libraries(lgmp-clipboard-tests
${EXE_FLAGS}
lg_common
lgmp
)
set(LGMP_CLIPBOARD_CASES
claim
offer-request
inbound-stream
outbound-grant
blocked
cancel
restart
disconnect
malformed
)
foreach(name IN LISTS LGMP_CLIPBOARD_CASES)
add_test(NAME lgmp-clipboard-${name}
COMMAND lgmp-clipboard-tests ${name}
)
set_tests_properties(lgmp-clipboard-${name} PROPERTIES
TIMEOUT 10
)
endforeach()
add_executable(wayland-motion-tests
motion_test.c
)
@@ -165,9 +197,19 @@ if(ENABLE_WAYLAND)
mime
replace
read-eof
read-block
read-empty
read-ready
read-error
source
source-release
source-external
source-replace
source-cancel
source-early
source-error
teardown
teardown-external
self-copy
dnd
)
@@ -192,14 +234,20 @@ if(ENABLE_X11)
target_link_libraries(x11-clipboard-tests
${EXE_FLAGS}
lg_common
pthread
)
set(X11_CLIPBOARD_CASES
targets
normal
incr
incr-block
incr-ready
incr-cancel
incr-large
malformed
replace
source
source-early
teardown
)
foreach(name IN LISTS X11_CLIPBOARD_CASES)
@@ -431,6 +479,7 @@ set(CLIPBOARD_CASES
stream-local
stream-blocked
stream-remote
request-publish
stream-legacy
)
foreach(name IN LISTS CLIPBOARD_CASES)

View File

@@ -24,6 +24,7 @@
#include "common/debug.h"
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
@@ -79,6 +80,8 @@ struct Provider
uint8_t chunkBuf[MAX_CALL][MAX_DATA];
uint64_t finalSize;
LG_ClipboardCancelReason cancelReason;
bool earlyBegin;
LG_ClipboardResult earlyResult;
};
struct Display
@@ -124,6 +127,8 @@ struct StreamSink
uint64_t offset;
uint64_t finalSize;
LG_ClipboardCancelReason reason;
LG_ClipboardRequest request;
bool requireRequest;
size_t size;
uint8_t data[MAX_DATA];
};
@@ -140,6 +145,8 @@ static LG_ClipboardResult sinkBegin(void * opaque,
LG_ClipboardData type, uint64_t sizeHint)
{
struct StreamSink * sink = opaque;
if (sink->requireRequest)
CHECK(sink->request != LG_CLIPBOARD_REQUEST_INVALID);
++sink->begin;
sink->type = type;
sink->sizeHint = sizeHint;
@@ -322,6 +329,22 @@ static bool dataReady(void * opaque, LG_ClipboardRequest id)
return provider->callOK;
}
struct EarlyRequest
{
struct Provider * provider;
LG_ClipboardRequest request;
LG_ClipboardData type;
};
static void * earlyRequestThread(void * opaque)
{
struct EarlyRequest * early = opaque;
early->provider->earlyResult = early->provider->ev->dataBegin(
early->provider->evCtx, early->request, early->type,
LG_CLIPBOARD_SIZE_UNKNOWN);
return NULL;
}
static bool request(void * opaque, LG_ClipboardRequest id,
LG_ClipboardData type)
{
@@ -330,6 +353,18 @@ static bool request(void * opaque, LG_ClipboardRequest id,
const unsigned int no = provider->request++;
provider->reqId[no] = id;
provider->reqType[no] = type;
if (provider->earlyBegin)
{
struct EarlyRequest early =
{
.provider = provider,
.request = id,
.type = type,
};
pthread_t thread;
CHECK(pthread_create(&thread, NULL, earlyRequestThread, &early) == 0);
CHECK(pthread_join(thread, NULL) == 0);
}
return provider->reqOK;
}
@@ -954,6 +989,29 @@ static void testStreamRemote(void)
lgClipboard_free();
}
static void testRequestPublication(void)
{
init();
lgClipboard_setFallback(&streamOps, &p);
const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT };
notice(&p, types, 1);
struct StreamSink sink = { 0 };
initSink(&sink);
sink.requireRequest = true;
p.earlyBegin = true;
CHECK(lgClipboard_requestStream(LG_CLIPBOARD_DATA_TEXT,
&sinkOps, &sink, &sink.request));
CHECK(sink.request == p.reqId[0]);
CHECK(p.earlyResult == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(sink.begin == 1);
CHECK(p.ev->dataEnd(p.evCtx, sink.request, 0) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(sink.end == 1);
lgClipboard_free();
}
static void testStreamLegacy(void)
{
init();
@@ -1021,6 +1079,7 @@ static const struct Test tests[] =
{ "stream-local" , testStreamLocal },
{ "stream-blocked", testStreamBlocked },
{ "stream-remote" , testStreamRemote },
{ "request-publish", testRequestPublication },
{ "stream-legacy" , testStreamLegacy },
};

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@
#include "wayland.h"
#include "test.h"
#include "../src/clipboard.h"
#include "common/debug.h"
@@ -76,9 +77,12 @@ struct Proto
unsigned int dirtyDestroyN;
unsigned int receiveN;
unsigned int setActionsN;
unsigned int selectionSetN;
char receiveMime[80];
const void * receiveData;
size_t receiveSize;
int receiveFd;
bool keepReceiveOpen;
struct Proxy * selection;
uint32_t serial;
};
@@ -97,7 +101,7 @@ struct Poll
struct TypeNotice
{
LG_ClipboardData type[LG_CLIPBOARD_DATA_NONE];
int count;
size_t count;
};
struct DataNotice
@@ -108,11 +112,25 @@ struct DataNotice
size_t size;
};
struct StreamNotice
{
LG_ClipboardRequest request;
LG_ClipboardData type;
uint64_t sizeHint;
uint64_t finalSize;
uint8_t data[MAX_TRANSFER];
size_t size;
unsigned int beginN;
unsigned int chunkN;
unsigned int endN;
};
struct Log
{
struct Poll poll[MAX_POLL];
struct TypeNotice notice[MAX_NOTICE];
struct DataNotice data[MAX_DATA];
struct StreamNotice streamData;
LG_ClipboardRequest abort[MAX_ABORT];
unsigned int pollN;
unsigned int pollUnregisterN;
@@ -122,11 +140,21 @@ struct Log
unsigned int abortN;
unsigned int releaseN;
unsigned int requestN;
unsigned int requestReadyN;
LG_ClipboardData requestType;
LG_ClipboardReplyFn reply;
void * replyOpaque;
const LG_ClipboardStreamOps * requestStream;
void * requestOpaque;
LG_ClipboardRequest requestId;
LG_ClipboardResult beginResult;
LG_ClipboardResult chunkResult;
LG_ClipboardResult endResult;
LG_ClipboardResult requestBeginResult;
bool readyDuringChunk;
bool readyDuringEnd;
bool pollOK;
bool requestOK;
bool requestCancelSync;
bool requestBeginSync;
bool firing;
};
@@ -280,6 +308,7 @@ struct wl_proxy * wl_proxy_marshal_flags(struct wl_proxy * proxy,
CHECK(!interface);
proto.selection = (struct Proxy *)va_arg(ap, struct wl_data_source *);
proto.serial = va_arg(ap, unsigned int);
++proto.selectionSetN;
p = NULL;
break;
@@ -292,6 +321,12 @@ struct wl_proxy * wl_proxy_marshal_flags(struct wl_proxy * proxy,
++proto.receiveN;
snprintf(proto.receiveMime, sizeof(proto.receiveMime),
"%s", p->mime[p->mimeN - 1]);
if (proto.keepReceiveOpen)
{
CHECK(proto.receiveFd < 0);
proto.receiveFd = dup(fd);
CHECK(proto.receiveFd >= 0);
}
if (proto.receiveSize)
sendData(fd, proto.receiveData, proto.receiveSize);
}
@@ -364,6 +399,20 @@ bool waylandPollUnregister(int fd)
return false;
}
bool waylandPollUpdate(int fd, uint32_t events)
{
for (unsigned int i = 0; i < rec.pollN; ++i)
{
struct Poll * poll = &rec.poll[i];
if (!poll->active || poll->fd != fd)
continue;
poll->events = events;
return true;
}
return false;
}
static void pollFire(unsigned int no, uint32_t events)
{
CHECK(no < rec.pollN);
@@ -379,28 +428,28 @@ static void pollFire(unsigned int no, uint32_t events)
pollCleanup(&rec.poll[i]);
}
void app_clipboardNotifyTypes(const LG_ClipboardData types[], int count)
void lgClipboard_notifyTypes(
const LG_ClipboardData types[], size_t count)
{
CHECK(rec.noticeN < ARRAY_LENGTH(rec.notice));
CHECK(count >= 0);
CHECK(count <= LG_CLIPBOARD_DATA_NONE);
CHECK(count <= ARRAY_LENGTH(rec.notice[0].type));
struct TypeNotice * notice = &rec.notice[rec.noticeN++];
memcpy(notice->type, types, (size_t)count * sizeof(*types));
memcpy(notice->type, types, count * sizeof(*types));
notice->count = count;
}
void app_clipboardRelease(void)
void lgClipboard_release(void)
{
++rec.releaseN;
}
void app_clipboardAbort(LG_ClipboardRequest request)
void lgClipboard_abort(LG_ClipboardRequest request)
{
CHECK(rec.abortN < ARRAY_LENGTH(rec.abort));
rec.abort[rec.abortN++] = request;
}
void app_clipboardData(LG_ClipboardRequest request,
void lgClipboard_data(LG_ClipboardRequest request,
LG_ClipboardData type, const void * data, size_t size)
{
CHECK(rec.dataN < ARRAY_LENGTH(rec.data));
@@ -412,16 +461,93 @@ void app_clipboardData(LG_ClipboardRequest request,
memcpy(notice->data, data, size);
}
bool app_clipboardRequest(LG_ClipboardData type,
LG_ClipboardReplyFn replyFn, void * opaque)
LG_ClipboardResult lgClipboard_dataBegin(LG_ClipboardRequest request,
LG_ClipboardData type, uint64_t sizeHint)
{
struct StreamNotice * stream = &rec.streamData;
++stream->beginN;
stream->request = request;
stream->type = type;
stream->sizeHint = sizeHint;
return rec.beginResult;
}
LG_ClipboardResult lgClipboard_dataChunk(LG_ClipboardRequest request,
uint64_t offset, const void * data, size_t size)
{
const LG_ClipboardResult result = rec.chunkResult;
struct StreamNotice * stream = &rec.streamData;
++stream->chunkN;
CHECK(request == stream->request);
CHECK(offset == stream->size);
CHECK(data);
CHECK(size);
CHECK(size <= sizeof(stream->data) - stream->size);
if (rec.readyDuringChunk)
{
rec.readyDuringChunk = false;
rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
waylandCBRequestReady(request);
}
if (result == LG_CLIPBOARD_RESULT_ACCEPTED)
{
memcpy(stream->data + stream->size, data, size);
stream->size += size;
}
return result;
}
LG_ClipboardResult lgClipboard_dataEnd(LG_ClipboardRequest request,
uint64_t finalSize)
{
const LG_ClipboardResult result = rec.endResult;
struct StreamNotice * stream = &rec.streamData;
++stream->endN;
CHECK(request == stream->request);
stream->finalSize = finalSize;
if (rec.readyDuringEnd)
{
rec.readyDuringEnd = false;
rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED;
waylandCBRequestReady(request);
}
return result;
}
bool lgClipboard_requestStream(LG_ClipboardData type,
const LG_ClipboardStreamOps * stream, void * opaque,
LG_ClipboardRequest * request)
{
++rec.requestN;
rec.requestType = type;
rec.reply = replyFn;
rec.replyOpaque = opaque;
rec.requestStream = stream;
rec.requestOpaque = opaque;
rec.requestId = 9000 + rec.requestN;
if (request)
*request = rec.requestId;
if (rec.requestBeginSync)
{
rec.requestBeginResult = stream->begin(
opaque, type, LG_CLIPBOARD_SIZE_UNKNOWN);
const uint8_t byte = 0xa5;
CHECK(stream->chunk(opaque, 0, &byte, 1) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(stream->chunk(opaque, 1, &byte, 1) ==
LG_CLIPBOARD_RESULT_BLOCKED);
pollFire(rec.pollN - 1, EPOLLOUT);
}
if (rec.requestCancelSync)
stream->cancel(opaque, LG_CLIPBOARD_CANCEL_REPLACED);
return rec.requestOK;
}
bool lgClipboard_requestReady(LG_ClipboardRequest request)
{
CHECK(request == rec.requestId);
++rec.requestReadyN;
return true;
}
static const struct wl_data_device_listener * deviceListener(void)
{
CHECK(proto.device.listener);
@@ -458,6 +584,7 @@ static void offerMime(struct Proxy * offer, const char * mime)
static void selectOffer(struct Proxy * offer)
{
proto.selection = offer;
deviceListener()->selection(proto.device.data,
(struct wl_data_device *)&proto.device,
(struct wl_data_offer *)offer);
@@ -477,10 +604,14 @@ static void start(void)
memset(&wlCb, 0, sizeof(wlCb));
memset(&proto, 0, sizeof(proto));
memset(&rec, 0, sizeof(rec));
proto.receiveFd = -1;
LG_LOCK_INIT(wlCb.lock);
rec.pollOK = true;
rec.requestOK = true;
rec.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED;
rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED;
proto.manager.kind = PROXY_MANAGER;
proto.device.kind = PROXY_DEVICE;
wlWm.dataDeviceManager =
@@ -497,6 +628,11 @@ static void start(void)
static void finish(void)
{
if (proto.receiveFd >= 0)
{
CHECK(close(proto.receiveFd) == 0);
proto.receiveFd = -1;
}
waylandCBFree();
CHECK(!wlCb.dataDevice);
CHECK(!wlCb.offer);
@@ -620,22 +756,120 @@ static void testReadEOF(void)
pollFire(0, EPOLLIN);
CHECK(rec.poll[0].active);
CHECK(rec.dataN == 0);
CHECK(rec.streamData.beginN == 1);
CHECK(rec.streamData.chunkN == 1);
CHECK(rec.streamData.endN == 0);
pollFire(0, EPOLLIN);
CHECK(!rec.poll[0].active);
CHECK(rec.pollCleanupN == 1);
CHECK(rec.dataN == 1);
CHECK(rec.data[0].request == 201);
CHECK(rec.data[0].type == LG_CLIPBOARD_DATA_TEXT);
CHECK(rec.data[0].size == sizeof(data));
CHECK(memcmp(rec.data[0].data, data, sizeof(data)) == 0);
CHECK(rec.streamData.request == 201);
CHECK(rec.streamData.type == LG_CLIPBOARD_DATA_TEXT);
CHECK(rec.streamData.sizeHint == LG_CLIPBOARD_SIZE_UNKNOWN);
CHECK(rec.streamData.size == sizeof(data));
CHECK(rec.streamData.finalSize == sizeof(data));
CHECK(rec.streamData.endN == 1);
CHECK(memcmp(rec.streamData.data, data, sizeof(data)) == 0);
checkClosed(fd);
finish();
CHECK(rec.dataN == 1);
CHECK(rec.abortN == 0);
}
static void testReadBlocked(void)
{
start();
textOffer();
const uint8_t data[] = "blocked stream";
proto.receiveData = data;
proto.receiveSize = sizeof(data) - 1;
rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED;
waylandCBRequest(202, LG_CLIPBOARD_DATA_TEXT);
CHECK(rec.pollN == 1);
pollFire(0, EPOLLIN);
CHECK(rec.streamData.beginN == 1);
CHECK(rec.streamData.chunkN == 1);
CHECK(rec.streamData.size == 0);
CHECK(rec.poll[0].events == 0);
rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
waylandCBRequestReady(202);
CHECK(rec.streamData.chunkN == 2);
CHECK(rec.streamData.size == sizeof(data) - 1);
CHECK(rec.streamData.endN == 1);
CHECK(rec.streamData.finalSize == sizeof(data) - 1);
CHECK(memcmp(rec.streamData.data, data, sizeof(data) - 1) == 0);
CHECK(!rec.poll[0].active);
CHECK(rec.abortN == 0);
finish();
}
static void testReadBlockedEmpty(void)
{
start();
textOffer();
const uint8_t data[] = "buffered while source stays open";
proto.receiveData = data;
proto.receiveSize = sizeof(data) - 1;
proto.keepReceiveOpen = true;
rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED;
waylandCBRequest(204, LG_CLIPBOARD_DATA_TEXT);
CHECK(rec.pollN == 1);
pollFire(0, EPOLLIN);
CHECK(rec.streamData.chunkN == 1);
CHECK(rec.streamData.size == 0);
CHECK(rec.poll[0].events == 0);
/* The read drained the pipe, whose duplicated write end remains open.
* Ready must retry the buffered chunk without another poll callback. */
rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
waylandCBRequestReady(204);
CHECK(rec.streamData.chunkN == 2);
CHECK(rec.streamData.size == sizeof(data) - 1);
CHECK(memcmp(rec.streamData.data, data, sizeof(data) - 1) == 0);
CHECK(rec.streamData.endN == 0);
CHECK(rec.poll[0].active);
CHECK(rec.poll[0].events == EPOLLIN);
CHECK(close(proto.receiveFd) == 0);
proto.receiveFd = -1;
pollFire(0, EPOLLIN);
CHECK(rec.streamData.endN == 1);
CHECK(rec.streamData.finalSize == sizeof(data) - 1);
CHECK(!rec.poll[0].active);
CHECK(rec.abortN == 0);
finish();
}
static void testReadReadyRace(void)
{
start();
textOffer();
const uint8_t data[] = "ready while returning blocked";
proto.receiveData = data;
proto.receiveSize = sizeof(data) - 1;
rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED;
rec.readyDuringChunk = true;
waylandCBRequest(203, LG_CLIPBOARD_DATA_TEXT);
CHECK(rec.pollN == 1);
pollFire(0, EPOLLIN);
CHECK(rec.streamData.beginN == 1);
CHECK(rec.streamData.chunkN == 2);
CHECK(rec.streamData.size == sizeof(data) - 1);
CHECK(rec.poll[0].events == EPOLLIN);
rec.endResult = LG_CLIPBOARD_RESULT_BLOCKED;
rec.readyDuringEnd = true;
pollFire(0, EPOLLIN);
CHECK(rec.streamData.endN == 2);
CHECK(rec.streamData.finalSize == sizeof(data) - 1);
CHECK(!rec.poll[0].active);
CHECK(rec.abortN == 0);
finish();
}
static void testReadError(void)
{
start();
@@ -666,13 +900,6 @@ static void testSource(void)
{
start();
waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
CHECK(rec.requestN == 1);
CHECK(rec.requestType == LG_CLIPBOARD_DATA_TEXT);
CHECK(rec.reply);
const uint8_t data[] = "source data";
rec.reply(rec.replyOpaque, LG_CLIPBOARD_DATA_TEXT,
data, sizeof(data) - 1);
CHECK(proto.sourceN == 1);
struct Proxy * source = &proto.source[0];
CHECK(proto.selection == source);
@@ -698,17 +925,45 @@ static void testSource(void)
(struct wl_data_source *)source, "text/plain", good[1]);
CHECK(rec.pollN == 1);
CHECK(rec.poll[0].fd == good[1]);
CHECK(rec.poll[0].events == 0);
CHECK(rec.requestN == 1);
CHECK(rec.requestType == LG_CLIPBOARD_DATA_TEXT);
CHECK(rec.requestStream);
CHECK(rec.requestOpaque);
const uint8_t first[] = "source ";
const uint8_t second[] = "data";
CHECK(rec.requestStream->begin(rec.requestOpaque,
LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_SIZE_UNKNOWN) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(rec.requestStream->chunk(rec.requestOpaque, 0,
first, sizeof(first) - 1) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(rec.poll[0].events == EPOLLOUT);
CHECK(rec.requestStream->chunk(rec.requestOpaque, sizeof(first) - 1,
second, sizeof(second) - 1) == LG_CLIPBOARD_RESULT_BLOCKED);
pollFire(0, EPOLLOUT);
CHECK(rec.requestReadyN == 1);
CHECK(rec.poll[0].events == 0);
CHECK(rec.requestStream->chunk(rec.requestOpaque, sizeof(first) - 1,
second, sizeof(second) - 1) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(rec.requestStream->end(rec.requestOpaque,
sizeof(first) + sizeof(second) - 2) == LG_CLIPBOARD_RESULT_BLOCKED);
pollFire(0, EPOLLOUT);
CHECK(rec.requestReadyN == 2);
CHECK(rec.requestStream->end(rec.requestOpaque,
sizeof(first) + sizeof(second) - 2) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(!rec.poll[0].active);
sourceListener(source)->cancelled(source->data,
(struct wl_data_source *)source);
CHECK(source->dead);
CHECK(proto.sourceDestroyN == 1);
pollFire(0, EPOLLOUT);
CHECK(rec.pollCleanupN == 1);
uint8_t actual[sizeof(data)] = {};
CHECK(read(good[0], actual, sizeof(actual)) == sizeof(data) - 1);
CHECK(memcmp(actual, data, sizeof(data) - 1) == 0);
uint8_t actual[sizeof(first) + sizeof(second)] = {};
const size_t expected = sizeof(first) + sizeof(second) - 2;
CHECK(read(good[0], actual, sizeof(actual)) == (ssize_t)expected);
CHECK(memcmp(actual, "source data", expected) == 0);
CHECK(read(good[0], &value, 1) == 0);
CHECK(close(good[0]) == 0);
checkClosed(good[1]);
@@ -716,6 +971,176 @@ static void testSource(void)
finish();
}
static void testSourceRelease(void)
{
start();
waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
struct Proxy * source = &proto.source[0];
CHECK(wlCb.selectionSource == (struct wl_data_source *)source);
CHECK(wlCb.sources);
CHECK(proto.selection == source);
CHECK(proto.selectionSetN == 1);
waylandCBRelease();
CHECK(!wlCb.selectionSource);
CHECK(wlCb.sources);
CHECK(!proto.selection);
CHECK(proto.selectionSetN == 2);
waylandCBRelease();
CHECK(proto.selectionSetN == 2);
sourceListener(source)->cancelled(source->data,
(struct wl_data_source *)source);
CHECK(source->dead);
CHECK(!wlCb.sources);
finish();
}
static void testSourceExternalOwner(void)
{
start();
waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
struct Proxy * source = &proto.source[0];
CHECK(proto.selectionSetN == 1);
struct Proxy * external = newOffer();
offerMime(external, "image/png");
selectOffer(external);
CHECK(!wlCb.selectionSource);
CHECK(proto.selection == external);
waylandCBRelease();
CHECK(proto.selectionSetN == 1);
CHECK(proto.selection == external);
sourceListener(source)->cancelled(source->data,
(struct wl_data_source *)source);
CHECK(source->dead);
finish();
}
static void testSourceReplacement(void)
{
start();
waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
struct Proxy * first = &proto.source[0];
waylandCBNotice(LG_CLIPBOARD_DATA_PNG);
struct Proxy * second = &proto.source[1];
CHECK(proto.selection == second);
CHECK(proto.selectionSetN == 2);
CHECK(wlCb.selectionSource == (struct wl_data_source *)second);
sourceListener(first)->cancelled(first->data,
(struct wl_data_source *)first);
CHECK(first->dead);
CHECK(wlCb.selectionSource == (struct wl_data_source *)second);
waylandCBRelease();
CHECK(!proto.selection);
CHECK(proto.selectionSetN == 3);
CHECK(!wlCb.selectionSource);
sourceListener(second)->cancelled(second->data,
(struct wl_data_source *)second);
CHECK(second->dead);
CHECK(!wlCb.sources);
finish();
}
static void testSourceCancelDuringRequest(void)
{
start();
waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
struct Proxy * source = &proto.source[0];
rec.requestCancelSync = true;
int fds[2];
CHECK(pipe(fds) == 0);
sourceListener(source)->send(source->data,
(struct wl_data_source *)source, "text/plain", fds[1]);
CHECK(rec.requestN == 1);
CHECK(rec.pollN == 1);
CHECK(!rec.poll[0].active);
CHECK(rec.pollCleanupN == 1);
checkClosed(fds[1]);
uint8_t value;
CHECK(read(fds[0], &value, 1) == 0);
CHECK(close(fds[0]) == 0);
sourceListener(source)->cancelled(source->data,
(struct wl_data_source *)source);
finish();
}
static void testSourceBeginDuringRequest(void)
{
start();
waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
struct Proxy * source = &proto.source[0];
rec.requestBeginSync = true;
int fds[2];
CHECK(pipe(fds) == 0);
sourceListener(source)->send(source->data,
(struct wl_data_source *)source, "text/plain", fds[1]);
CHECK(rec.requestN == 1);
CHECK(rec.requestBeginResult == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(rec.requestOpaque);
CHECK(rec.requestReadyN == 1);
CHECK(rec.requestStream->end(rec.requestOpaque, 1) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(rec.pollCleanupN == 1);
uint8_t value = 0;
CHECK(read(fds[0], &value, 1) == 1);
CHECK(value == 0xa5);
CHECK(read(fds[0], &value, 1) == 0);
CHECK(close(fds[0]) == 0);
checkClosed(fds[1]);
sourceListener(source)->cancelled(source->data,
(struct wl_data_source *)source);
finish();
}
static void testSourceError(void)
{
start();
waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
struct Proxy * source = &proto.source[0];
int fds[2];
CHECK(pipe(fds) == 0);
sourceListener(source)->send(source->data,
(struct wl_data_source *)source, "text/plain", fds[1]);
CHECK(rec.requestStream->begin(rec.requestOpaque,
LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_SIZE_UNKNOWN) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
const uint8_t first[] = "first";
const uint8_t second[] = "second";
CHECK(rec.requestStream->chunk(rec.requestOpaque, 0,
first, sizeof(first) - 1) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(rec.requestStream->chunk(rec.requestOpaque, sizeof(first) - 1,
second, sizeof(second) - 1) == LG_CLIPBOARD_RESULT_BLOCKED);
CHECK(close(fds[0]) == 0);
pollFire(0, EPOLLERR);
CHECK(rec.requestReadyN == 1);
CHECK(!rec.poll[0].active);
CHECK(rec.pollCleanupN == 1);
CHECK(rec.requestStream->chunk(rec.requestOpaque, sizeof(first) - 1,
second, sizeof(second) - 1) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(rec.requestStream->end(rec.requestOpaque,
sizeof(first) + sizeof(second) - 2) == LG_CLIPBOARD_RESULT_ACCEPTED);
checkClosed(fds[1]);
sourceListener(source)->cancelled(source->data,
(struct wl_data_source *)source);
finish();
}
static void testTeardown(void)
{
start();
@@ -725,17 +1150,40 @@ static void testTeardown(void)
const int fd = rec.poll[0].fd;
struct Proxy * dnd = rawOffer();
wlCb.dndOffer = (struct wl_data_offer *)dnd;
waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
struct Proxy * source = &proto.source[0];
finish();
CHECK(offer->dead);
CHECK(dnd->dead);
CHECK(source->dead);
CHECK(proto.offerDestroyN == 2);
CHECK(proto.sourceDestroyN == 1);
CHECK(proto.dirtyDestroyN == 0);
CHECK(proto.selectionSetN == 2);
CHECK(!proto.selection);
CHECK(rec.pollUnregisterN == 1);
CHECK(rec.pollCleanupN == 1);
checkClosed(fd);
}
static void testTeardownExternalOwner(void)
{
start();
waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
struct Proxy * source = &proto.source[0];
struct Proxy * external = newOffer();
offerMime(external, "text/plain");
selectOffer(external);
CHECK(!wlCb.selectionSource);
finish();
CHECK(source->dead);
CHECK(external->dead);
CHECK(proto.selectionSetN == 1);
CHECK(proto.selection == external);
}
static void testDnd(void)
{
start();
@@ -783,9 +1231,19 @@ static const struct Test tests[] =
{ "mime" , testMime },
{ "replace" , testReplace },
{ "read-eof" , testReadEOF },
{ "read-block", testReadBlocked },
{ "read-empty", testReadBlockedEmpty },
{ "read-ready", testReadReadyRace },
{ "read-error", testReadError },
{ "source" , testSource },
{ "source-release", testSourceRelease },
{ "source-external", testSourceExternalOwner },
{ "source-replace", testSourceReplacement },
{ "source-cancel", testSourceCancelDuringRequest },
{ "source-early", testSourceBeginDuringRequest },
{ "source-error", testSourceError },
{ "teardown" , testTeardown },
{ "teardown-external", testTeardownExternalOwner },
{ "self-copy" , testSelfCopy },
{ "dnd" , testDnd },
};

View File

@@ -22,6 +22,7 @@
#include "clipboard.h"
#include "test.h"
#include "x11.h"
#include "../src/clipboard.h"
#include "common/debug.h"
@@ -29,6 +30,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <X11/Xatom.h>
#include <X11/Xlib.h>
@@ -42,7 +44,7 @@
#define MAX_NOTICE 8U
#define MAX_PROP 16U
#define MAX_SEND 8U
#define MAX_TRANSFER 8192U
#define MAX_TRANSFER (128U * 1024U)
#define MAX_WINDOW 16U
#define A_CLIPBOARD 10UL
@@ -95,7 +97,7 @@ struct Property
struct TypeNotice
{
LG_ClipboardData type[LG_CLIPBOARD_DATA_NONE];
int count;
size_t count;
};
struct DataNotice
@@ -127,6 +129,7 @@ struct Log
unsigned int abortN;
unsigned int releaseN;
unsigned int requestN;
unsigned int requestReadyN;
unsigned int destroyN;
unsigned int freeN;
unsigned int flushN;
@@ -134,11 +137,27 @@ struct Log
unsigned int ungrabN;
unsigned int selectN;
LG_ClipboardData requestType;
LG_ClipboardReplyFn reply;
void * replyOpaque;
const LG_ClipboardStreamOps * requestStream;
void * requestOpaque;
LG_ClipboardRequest requestId;
LG_ClipboardRequest streamRequest;
LG_ClipboardData streamType;
uint64_t streamSizeHint;
unsigned int streamBeginN;
unsigned int streamChunkN;
unsigned int streamEndN;
size_t streamMaxChunk;
LG_ClipboardResult beginResult;
LG_ClipboardResult chunkResult;
LG_ClipboardResult endResult;
LG_ClipboardResult requestBeginResult;
bool readyDuringChunk;
bool readyDuringEnd;
Window owner;
Window nextWindow;
bool requestOK;
bool requestCancelSync;
bool requestBeginSync;
bool fixesOK;
};
@@ -147,6 +166,12 @@ struct X11DSAtoms x11atoms;
static struct Log rec;
static pthread_mutex_t readRaceLock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t readRaceCond = PTHREAD_COND_INITIALIZER;
static bool readRacePause;
static bool readRaceEntered;
static bool readRaceResume;
static size_t itemSize(int format, unsigned long count)
{
switch (format)
@@ -253,10 +278,7 @@ int XSelectInput(Display * display, Window window, long mask)
{
CHECK(display == x11.display);
CHECK(mask == PropertyChangeMask);
bool found = false;
for (unsigned int i = 0; i < rec.windowN; ++i)
found |= rec.window[i].window == window && !rec.window[i].dead;
CHECK(found);
CHECK(window != None);
return Success;
}
@@ -339,26 +361,44 @@ int XGetWindowProperty(Display * display, Window window, Atom property,
CHECK(display == x11.display);
CHECK(window != None);
CHECK(property != None);
CHECK(offset == 0);
CHECK(length == ~0L);
CHECK(offset >= 0);
CHECK(length == ~0L || length == (64U * 1024U + 3U) / 4U);
CHECK(delete);
CHECK(requestedType == AnyPropertyType);
CHECK(rec.propPos < rec.propN);
const struct Property * prop = &rec.prop[rec.propPos++];
const struct Property * prop = &rec.prop[rec.propPos];
*actualType = prop->type;
*actualFormat = prop->format;
*count = prop->count;
*after = prop->after;
size_t start = 0;
size_t size = prop->size;
if (prop->format == 8 && length != ~0L)
{
start = (size_t)offset * 4U;
CHECK(start <= prop->size);
const size_t maximum = (size_t)length * 4U;
size = prop->size - start;
if (size > maximum)
size = maximum;
*count = size;
*after = prop->size - start - size;
}
else
{
*count = prop->count;
*after = prop->after;
}
if (prop->null)
*data = NULL;
else
{
CHECK(prop->size <= sizeof(prop->data));
*data = malloc(prop->size ? prop->size : 1);
CHECK(start + size <= sizeof(prop->data));
*data = malloc(size ? size : 1);
CHECK(*data);
if (prop->size)
memcpy(*data, prop->data, prop->size);
if (size)
memcpy(*data, prop->data + start, size);
}
if (*after == 0)
++rec.propPos;
return prop->status;
}
@@ -370,17 +410,17 @@ int XFree(void * data)
return Success;
}
void app_clipboardNotifyTypes(const LG_ClipboardData types[], int count)
void lgClipboard_notifyTypes(
const LG_ClipboardData types[], size_t count)
{
CHECK(rec.noticeN < ARRAY_LENGTH(rec.notice));
CHECK(count >= 0);
CHECK(count <= LG_CLIPBOARD_DATA_NONE);
CHECK(count <= ARRAY_LENGTH(rec.notice[0].type));
struct TypeNotice * notice = &rec.notice[rec.noticeN++];
notice->count = count;
memcpy(notice->type, types, (size_t)count * sizeof(*types));
memcpy(notice->type, types, count * sizeof(*types));
}
void app_clipboardData(LG_ClipboardRequest request,
void lgClipboard_data(LG_ClipboardRequest request,
LG_ClipboardData type, const void * data, size_t size)
{
CHECK(rec.dataN < ARRAY_LENGTH(rec.data));
@@ -394,27 +434,121 @@ void app_clipboardData(LG_ClipboardRequest request,
memcpy(notice->data, data, size);
}
void app_clipboardAbort(LG_ClipboardRequest request)
LG_ClipboardResult lgClipboard_dataBegin(LG_ClipboardRequest request,
LG_ClipboardData type, uint64_t sizeHint)
{
++rec.streamBeginN;
rec.streamRequest = request;
rec.streamType = type;
rec.streamSizeHint = sizeHint;
if (rec.beginResult == LG_CLIPBOARD_RESULT_ACCEPTED)
{
CHECK(rec.dataN < ARRAY_LENGTH(rec.data));
rec.data[rec.dataN] = (struct DataNotice)
{
.request = request,
.type = type,
};
}
return rec.beginResult;
}
LG_ClipboardResult lgClipboard_dataChunk(LG_ClipboardRequest request,
uint64_t offset, const void * data, size_t size)
{
const LG_ClipboardResult result = rec.chunkResult;
++rec.streamChunkN;
CHECK(request == rec.streamRequest);
CHECK(rec.dataN < ARRAY_LENGTH(rec.data));
struct DataNotice * notice = &rec.data[rec.dataN];
CHECK(offset == notice->size);
CHECK(data);
CHECK(size);
CHECK(size <= sizeof(notice->data) - notice->size);
if (size > rec.streamMaxChunk)
rec.streamMaxChunk = size;
pthread_mutex_lock(&readRaceLock);
if (readRacePause)
{
readRacePause = false;
readRaceEntered = true;
pthread_cond_signal(&readRaceCond);
while (!readRaceResume)
pthread_cond_wait(&readRaceCond, &readRaceLock);
}
pthread_mutex_unlock(&readRaceLock);
if (rec.readyDuringChunk)
{
rec.readyDuringChunk = false;
rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
x11CBRequestReady(request);
}
if (result == LG_CLIPBOARD_RESULT_ACCEPTED)
{
memcpy(notice->data + notice->size, data, size);
notice->size += size;
}
return result;
}
LG_ClipboardResult lgClipboard_dataEnd(LG_ClipboardRequest request,
uint64_t finalSize)
{
const LG_ClipboardResult result = rec.endResult;
++rec.streamEndN;
CHECK(request == rec.streamRequest);
CHECK(rec.dataN < ARRAY_LENGTH(rec.data));
if (rec.readyDuringEnd)
{
rec.readyDuringEnd = false;
rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED;
x11CBRequestReady(request);
}
if (result == LG_CLIPBOARD_RESULT_ACCEPTED)
{
CHECK(finalSize == rec.data[rec.dataN].size);
++rec.dataN;
}
return result;
}
void lgClipboard_abort(LG_ClipboardRequest request)
{
CHECK(rec.abortN < ARRAY_LENGTH(rec.abort));
rec.abort[rec.abortN++] = request;
}
void app_clipboardRelease(void)
void lgClipboard_release(void)
{
++rec.releaseN;
}
bool app_clipboardRequest(LG_ClipboardData type,
LG_ClipboardReplyFn replyFn, void * opaque)
bool lgClipboard_requestStream(LG_ClipboardData type,
const LG_ClipboardStreamOps * stream, void * opaque,
LG_ClipboardRequest * request)
{
++rec.requestN;
rec.requestType = type;
rec.reply = replyFn;
rec.replyOpaque = opaque;
rec.requestStream = stream;
rec.requestOpaque = opaque;
rec.requestId = 9000 + rec.requestN;
if (request)
*request = rec.requestId;
if (rec.requestBeginSync)
rec.requestBeginResult = stream->begin(
opaque, type, LG_CLIPBOARD_SIZE_UNKNOWN);
if (rec.requestCancelSync)
stream->cancel(opaque, LG_CLIPBOARD_CANCEL_REPLACED);
return rec.requestOK;
}
bool lgClipboard_requestReady(LG_ClipboardRequest request)
{
CHECK(request == rec.requestId);
++rec.requestReadyN;
return true;
}
static struct Property * prop(void)
{
CHECK(rec.propN < ARRAY_LENGTH(rec.prop));
@@ -469,17 +603,22 @@ static void selection(Window requestor, Atom target, Atom property)
CHECK(x11CBEventThread(&event));
}
static void property(Window window)
static void propertyState(Window window, Atom atom, int state)
{
XEvent event = {};
event.xproperty.type = PropertyNotify;
event.xproperty.display = x11.display;
event.xproperty.window = window;
event.xproperty.atom = x11atoms.SEL_DATA;
event.xproperty.state = PropertyNewValue;
event.xproperty.atom = atom;
event.xproperty.state = state;
CHECK(x11CBEventThread(&event));
}
static void property(Window window)
{
propertyState(window, x11atoms.SEL_DATA, PropertyNewValue);
}
static void owner(Window owner)
{
rec.owner = owner;
@@ -540,6 +679,15 @@ static void start(void)
rec.nextWindow = 1000;
rec.requestOK = true;
rec.fixesOK = true;
rec.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED;
rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED;
pthread_mutex_lock(&readRaceLock);
readRacePause = false;
readRaceEntered = false;
readRaceResume = false;
pthread_mutex_unlock(&readRaceLock);
x11.display = (Display *)(uintptr_t)1;
x11.window = 50;
@@ -632,6 +780,166 @@ static void testIncr(void)
finish();
}
static void testIncrBlocked(void)
{
start();
const unsigned long targets[] = { A_TEXT };
discover(620, targets, ARRAY_LENGTH(targets));
const Window window = request(211, LG_CLIPBOARD_DATA_TEXT);
const unsigned long capacity = 9;
rec.beginResult = LG_CLIPBOARD_RESULT_BLOCKED;
prop32(x11atoms.INCR, &capacity, 1);
selection(window, A_TEXT, x11atoms.SEL_DATA);
CHECK(rec.streamBeginN == 1);
CHECK(rec.dataN == 0);
const uint8_t first[] = "abc";
prop8(A_TEXT, first, sizeof(first) - 1);
property(window);
CHECK(rec.streamChunkN == 0);
rec.beginResult = LG_CLIPBOARD_RESULT_ACCEPTED;
rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED;
x11CBRequestReady(211);
CHECK(rec.streamBeginN == 2);
CHECK(rec.streamSizeHint == capacity);
CHECK(rec.streamChunkN == 1);
CHECK(rec.data[0].size == 0);
const uint8_t second[] = "defghi";
prop8(A_TEXT, second, sizeof(second) - 1);
property(window);
CHECK(rec.streamChunkN == 1);
rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
x11CBRequestReady(211);
CHECK(rec.streamChunkN == 3);
CHECK(rec.data[0].size == 9);
CHECK(memcmp(rec.data[0].data, "abcdefghi", 9) == 0);
rec.endResult = LG_CLIPBOARD_RESULT_BLOCKED;
propNull(A_TEXT, 8, 0);
property(window);
CHECK(rec.streamEndN == 1);
CHECK(rec.dataN == 0);
rec.endResult = LG_CLIPBOARD_RESULT_ACCEPTED;
x11CBRequestReady(211);
CHECK(rec.streamEndN == 2);
CHECK(rec.dataN == 1);
CHECK(rec.abortN == 0);
finish();
}
static void testIncrReadyRace(void)
{
start();
const unsigned long targets[] = { A_TEXT };
discover(622, targets, ARRAY_LENGTH(targets));
const Window window = request(213, LG_CLIPBOARD_DATA_TEXT);
const unsigned long capacity = 4;
prop32(x11atoms.INCR, &capacity, 1);
selection(window, A_TEXT, x11atoms.SEL_DATA);
const uint8_t data[] = "race";
rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED;
rec.readyDuringChunk = true;
prop8(A_TEXT, data, sizeof(data) - 1);
property(window);
CHECK(rec.streamChunkN == 2);
CHECK(rec.data[0].size == sizeof(data) - 1);
rec.endResult = LG_CLIPBOARD_RESULT_BLOCKED;
rec.readyDuringEnd = true;
propNull(A_TEXT, 8, 0);
property(window);
CHECK(rec.streamEndN == 2);
CHECK(rec.dataN == 1);
CHECK(rec.abortN == 0);
finish();
}
static void * requestReadyThread(void * opaque)
{
x11CBRequestReady(*(const LG_ClipboardRequest *)opaque);
return NULL;
}
static void testIncrCancelRace(void)
{
start();
const unsigned long targets[] = { A_TEXT };
discover(623, targets, ARRAY_LENGTH(targets));
const LG_ClipboardRequest id = 214;
const Window window = request(id, LG_CLIPBOARD_DATA_TEXT);
const unsigned long capacity = 6;
prop32(x11atoms.INCR, &capacity, 1);
selection(window, A_TEXT, x11atoms.SEL_DATA);
const uint8_t data[] = "cancel";
rec.chunkResult = LG_CLIPBOARD_RESULT_BLOCKED;
prop8(A_TEXT, data, sizeof(data) - 1);
property(window);
CHECK(rec.streamChunkN == 1);
rec.chunkResult = LG_CLIPBOARD_RESULT_ACCEPTED;
pthread_mutex_lock(&readRaceLock);
readRacePause = true;
pthread_mutex_unlock(&readRaceLock);
pthread_t readyThread;
CHECK(pthread_create(&readyThread, NULL, requestReadyThread,
(void *)&id) == 0);
pthread_mutex_lock(&readRaceLock);
while (!readRaceEntered)
pthread_cond_wait(&readRaceCond, &readRaceLock);
pthread_mutex_unlock(&readRaceLock);
/* The callback is outside x11cb.lock and still consuming its chunk. */
x11CBRequestCancel(id, LG_CLIPBOARD_CANCEL_REPLACED);
pthread_mutex_lock(&readRaceLock);
readRaceResume = true;
pthread_cond_signal(&readRaceCond);
pthread_mutex_unlock(&readRaceLock);
CHECK(pthread_join(readyThread, NULL) == 0);
CHECK(rec.streamChunkN == 2);
CHECK(rec.data[0].size == sizeof(data) - 1);
CHECK(memcmp(rec.data[0].data, data, sizeof(data) - 1) == 0);
CHECK(rec.abortN == 0);
finish();
}
static void testIncrLargeProperty(void)
{
start();
const unsigned long targets[] = { A_TEXT };
discover(621, targets, ARRAY_LENGTH(targets));
const Window window = request(212, LG_CLIPBOARD_DATA_TEXT);
const unsigned long capacity = 70U * 1024U;
prop32(x11atoms.INCR, &capacity, 1);
selection(window, A_TEXT, x11atoms.SEL_DATA);
const size_t size = 70U * 1024U;
uint8_t * data = malloc(size);
CHECK(data);
for (size_t i = 0; i < size; ++i)
data[i] = (uint8_t)i;
prop8(A_TEXT, data, size);
property(window);
propNull(A_TEXT, 8, 0);
property(window);
CHECK(rec.streamChunkN == 2);
CHECK(rec.streamMaxChunk == 64U * 1024U);
CHECK(rec.dataN == 1);
CHECK(rec.data[0].size == size);
CHECK(memcmp(rec.data[0].data, data, size) == 0);
free(data);
finish();
}
static void testMalformed(void)
{
start();
@@ -717,36 +1025,88 @@ static void testSource(void)
selectionRequest(A_PNG, 701);
CHECK(rec.requestN == 1);
CHECK(rec.requestType == LG_CLIPBOARD_DATA_PNG);
CHECK(rec.reply);
CHECK(rec.requestStream);
CHECK(rec.sendN == 1);
const uint8_t data[] = { 1, 2, 3, 4 };
rec.reply(rec.replyOpaque, LG_CLIPBOARD_DATA_PNG,
data, ARRAY_LENGTH(data));
CHECK(rec.requestStream->begin(rec.requestOpaque,
LG_CLIPBOARD_DATA_PNG, LG_CLIPBOARD_SIZE_UNKNOWN) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(rec.changeN == 2);
CHECK(rec.change[1].property == 701);
CHECK(rec.change[1].type == A_PNG);
CHECK(rec.change[1].format == 8);
CHECK(rec.change[1].count == 4);
CHECK(memcmp(rec.change[1].data, data, sizeof(data)) == 0);
CHECK(rec.change[1].type == x11atoms.INCR);
CHECK(rec.change[1].format == 32);
CHECK(rec.change[1].count == 1);
CHECK(rec.sendN == 2);
CHECK(rec.send[1].xselection.property == 701);
selectionRequest(A_TEXT, 702);
CHECK(rec.requestN == 1);
CHECK(rec.requestStream->chunk(rec.requestOpaque, 0,
data, ARRAY_LENGTH(data)) == LG_CLIPBOARD_RESULT_BLOCKED);
propertyState(900, 701, PropertyDelete);
CHECK(rec.requestReadyN == 1);
CHECK(rec.requestStream->chunk(rec.requestOpaque, 0,
data, ARRAY_LENGTH(data)) == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(rec.changeN == 3);
CHECK(rec.change[2].property == 701);
CHECK(rec.change[2].type == A_PNG);
CHECK(rec.change[2].format == 8);
CHECK(rec.change[2].count == 4);
CHECK(memcmp(rec.change[2].data, data, sizeof(data)) == 0);
CHECK(rec.requestStream->end(rec.requestOpaque, ARRAY_LENGTH(data)) ==
LG_CLIPBOARD_RESULT_BLOCKED);
propertyState(900, 701, PropertyDelete);
CHECK(rec.requestReadyN == 2);
CHECK(rec.requestStream->end(rec.requestOpaque, ARRAY_LENGTH(data)) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(rec.changeN == 4);
CHECK(rec.change[3].property == 701);
CHECK(rec.change[3].type == A_PNG);
CHECK(rec.change[3].format == 8);
CHECK(rec.change[3].count == 0);
rec.requestCancelSync = true;
selectionRequest(A_PNG, 704);
CHECK(rec.requestN == 2);
CHECK(rec.sendN == 3);
CHECK(rec.send[2].xselection.property == None);
rec.requestCancelSync = false;
rec.requestOK = false;
selectionRequest(A_PNG, 703);
selectionRequest(A_TEXT, 702);
CHECK(rec.requestN == 2);
CHECK(rec.sendN == 4);
CHECK(rec.send[3].xselection.property == None);
rec.requestOK = false;
selectionRequest(A_PNG, 703);
CHECK(rec.requestN == 3);
CHECK(rec.sendN == 5);
CHECK(rec.send[4].xselection.property == None);
x11CBRelease();
CHECK(rec.owner == None);
finish();
}
static void testSourceBeginDuringRequest(void)
{
start();
x11CBNotice(LG_CLIPBOARD_DATA_PNG);
rec.requestBeginSync = true;
selectionRequest(A_PNG, 705);
CHECK(rec.requestN == 1);
CHECK(rec.requestBeginResult == LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(rec.requestOpaque);
CHECK(rec.changeN == 1);
CHECK(rec.change[0].type == x11atoms.INCR);
CHECK(rec.sendN == 1);
propertyState(900, 705, PropertyDelete);
CHECK(rec.requestStream->end(rec.requestOpaque, 0) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(rec.changeN == 2);
CHECK(rec.change[1].count == 0);
finish();
}
static void testTeardown(void)
{
start();
@@ -789,9 +1149,14 @@ static const struct Test tests[] =
{ "targets" , testTargets },
{ "normal" , testNormal },
{ "incr" , testIncr },
{ "incr-block", testIncrBlocked },
{ "incr-ready", testIncrReadyRace },
{ "incr-cancel", testIncrCancelRace },
{ "incr-large", testIncrLargeProperty },
{ "malformed", testMalformed },
{ "replace" , testReplace },
{ "source" , testSource },
{ "source-early", testSourceBeginDuringRequest },
{ "teardown" , testTeardown },
};

View File

@@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.10)
project(transport_LGMP LANGUAGES C)
add_library(transport_LGMP STATIC
clipboard.c
input.c
lgmp.c
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifndef _H_LG_CLIENT_TRANSPORT_LGMP_CLIPBOARD_
#define _H_LG_CLIENT_TRANSPORT_LGMP_CLIPBOARD_
#include "interface/clipboard.h"
#include <lgmp/client.h>
#include <stdbool.h>
#include <stdint.h>
typedef struct LGMPClipboard LGMPClipboard;
bool lgmpClipboard_create(PLGMPClient client, LGMPClipboard ** result);
void lgmpClipboard_destroy(LGMPClipboard ** clipboard);
bool lgmpClipboard_connect(LGMPClipboard * clipboard, uint32_t clientID);
void lgmpClipboard_disconnect(LGMPClipboard * clipboard);
const LG_ClipboardOps * lgmpClipboard_getOps(void);
#endif

View File

@@ -20,6 +20,7 @@
#include "interface/transport.h"
#include "clipboard.h"
#include "input.h"
#include "common/KVMFR.h"
@@ -109,6 +110,7 @@ struct LG_Transport
PLGMPClientQueue ownerFrameQueue[LGMP_Q_FRAME_LEN];
PLGMPClientQueue pointerQueue;
LGMPInput * input;
LGMPClipboard * clipboard;
LG_Lock frameLock;
LG_Lock pointerLock;
LG_RWLock videoStatusLock;
@@ -133,6 +135,7 @@ struct LG_Transport
bool frameStopRequested;
bool frameScheduleSupported;
atomic_bool inputSupported;
atomic_bool clipboardSupported;
uint64_t frameLeaseHandle;
uint32_t frameGeneration;
uint32_t clientID;
@@ -628,6 +631,13 @@ static LGMP_STATUS lgmp_initializeClient(struct LG_Transport * this)
return LGMP_ERR_NO_MEM;
}
if (!lgmpClipboard_create(this->client, &this->clipboard))
{
lgmpInput_destroy(&this->input);
lgmpClientFree(&this->client);
return LGMP_ERR_NO_MEM;
}
return LGMP_OK;
}
@@ -650,6 +660,7 @@ static bool lgmp_create(LG_Transport ** result)
this->cursorPollInterval = cursorPoll;
atomic_init(&this->connected, false);
atomic_init(&this->inputSupported, false);
atomic_init(&this->clipboardSupported, false);
atomic_init(&this->destroyPending, false);
this->frameWake = lgCreateEvent(true, 0);
@@ -860,6 +871,7 @@ static void lgmp_destroyNow(LG_Transport * this)
{
if (this->client)
{
lgmpClipboard_destroy(&this->clipboard);
lgmpInput_destroy(&this->input);
lgmp_closeQueues(this);
lgmpClientFree(&this->client);
@@ -958,6 +970,8 @@ static bool lgmp_parseSession(struct LG_Transport * this,
session->features |= LG_TRANSPORT_FEATURE_FRAME_SCHEDULE;
if (header->features & KVMFR_FEATURE_INPUT)
session->features |= LG_TRANSPORT_FEATURE_INPUT;
if (header->features & KVMFR_FEATURE_CLIPBOARD)
session->features |= LG_TRANSPORT_FEATURE_CLIPBOARD;
data += sizeof(*header);
size -= sizeof(*header);
@@ -1076,6 +1090,9 @@ static LG_TransportStatus lgmp_connectInternal(LG_Transport * this,
atomic_store_explicit(&this->inputSupported,
session->features & LG_TRANSPORT_FEATURE_INPUT,
memory_order_release);
atomic_store_explicit(&this->clipboardSupported,
session->features & LG_TRANSPORT_FEATURE_CLIPBOARD,
memory_order_release);
this->frameSerial = 0;
this->frameSerialValid = false;
this->formatValid = false;
@@ -1136,6 +1153,7 @@ static void lgmp_disconnect(LG_Transport * this)
lgmp_retainVideoStatusLifetime(this);
lgmpInput_disconnect(this->input);
lgmpClipboard_disconnect(this->clipboard);
LG_LOCK(this->frameLock);
if (++this->frameGeneration == 0)
@@ -1145,6 +1163,8 @@ static void lgmp_disconnect(LG_Transport * this)
this->frameScheduleSupported = false;
atomic_store_explicit(
&this->inputSupported, false, memory_order_release);
atomic_store_explicit(
&this->clipboardSupported, false, memory_order_release);
this->clientID = 0;
LG_UNLOCK(this->frameLock);
@@ -2474,6 +2494,20 @@ static const LG_InputOps * lgmp_getInputOps(LG_Transport * this,
return lgmpInput_getOps();
}
static const LG_ClipboardOps * lgmp_getClipboardOps(
LG_Transport * this, void ** opaque)
{
*opaque = NULL;
if (!atomic_load_explicit(&this->connected, memory_order_acquire) ||
!atomic_load_explicit(
&this->clipboardSupported, memory_order_acquire) ||
!lgmpClipboard_connect(this->clipboard, this->clientID))
return NULL;
*opaque = this->clipboard;
return lgmpClipboard_getOps();
}
static void lgmp_probeVideoStatus(LG_Transport * this)
{
bool frameAvailable = false;
@@ -2617,6 +2651,7 @@ const LG_TransportOps LGT_LGMP =
.sessionValid = lgmp_sessionValid,
.getVideoOps = lgmp_getVideoOps,
.getInputOps = lgmp_getInputOps,
.getClipboardOps = lgmp_getClipboardOps,
.getRecoveryInfo = lgmp_getRecoveryInfo,
.requestRecovery = lgmp_requestRecovery,
.sendControl = lgmp_sendControl,