mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-22 07:01:30 +00:00
560 lines
16 KiB
C
560 lines
16 KiB
C
/**
|
|
* Looking Glass
|
|
* Copyright © 2017-2026 The Looking Glass Authors
|
|
* https://looking-glass.io
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the Free
|
|
* Software Foundation; either version 2 of the License, or (at your option)
|
|
* any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc., 59
|
|
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
|
|
#include "clipboard.h"
|
|
|
|
#include "common/debug.h"
|
|
#include "common/locking.h"
|
|
|
|
#include <stdatomic.h>
|
|
#include <stdlib.h>
|
|
|
|
typedef struct ClipboardEventTarget
|
|
{
|
|
const LG_ClipboardEventOps * events;
|
|
void * opaque;
|
|
}
|
|
ClipboardEventTarget;
|
|
|
|
typedef struct PendingRequest
|
|
{
|
|
bool pending;
|
|
LG_ClipboardRequest request;
|
|
LG_ClipboardData type;
|
|
}
|
|
PendingRequest;
|
|
|
|
struct SpiceClipboard
|
|
{
|
|
LG_Lock stateLock;
|
|
LG_Lock eventDispatch;
|
|
LG_Lock statusDispatch;
|
|
|
|
bool available;
|
|
uint32_t statusGeneration;
|
|
LG_ClipboardStatusFn statusCallback;
|
|
void * statusOpaque;
|
|
|
|
const LG_ClipboardEventOps * events;
|
|
void * eventOpaque;
|
|
|
|
bool remoteNotice;
|
|
LG_ClipboardData remoteType;
|
|
PendingRequest read;
|
|
PendingRequest write;
|
|
|
|
LG_ClipboardRequest requestSerial;
|
|
};
|
|
|
|
static _Atomic(SpiceClipboard *) l_callbackTarget;
|
|
|
|
static uint32_t nextGeneration(uint32_t generation)
|
|
{
|
|
if (++generation == 0)
|
|
++generation;
|
|
return generation;
|
|
}
|
|
|
|
static LG_ClipboardRequest nextRequestNL(SpiceClipboard * clipboard)
|
|
{
|
|
if (++clipboard->requestSerial == LG_CLIPBOARD_REQUEST_INVALID)
|
|
++clipboard->requestSerial;
|
|
return clipboard->requestSerial;
|
|
}
|
|
|
|
static bool spiceType(PSDataType source, LG_ClipboardData * type)
|
|
{
|
|
switch (source)
|
|
{
|
|
case SPICE_DATA_TEXT : *type = LG_CLIPBOARD_DATA_TEXT; return true;
|
|
case SPICE_DATA_PNG : *type = LG_CLIPBOARD_DATA_PNG ; return true;
|
|
case SPICE_DATA_BMP : *type = LG_CLIPBOARD_DATA_BMP ; return true;
|
|
case SPICE_DATA_TIFF : *type = LG_CLIPBOARD_DATA_TIFF; return true;
|
|
case SPICE_DATA_JPEG : *type = LG_CLIPBOARD_DATA_JPEG; return true;
|
|
case SPICE_DATA_NONE : break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool lgType(LG_ClipboardData source, PSDataType * type)
|
|
{
|
|
switch (source)
|
|
{
|
|
case LG_CLIPBOARD_DATA_TEXT : *type = SPICE_DATA_TEXT; return true;
|
|
case LG_CLIPBOARD_DATA_PNG : *type = SPICE_DATA_PNG ; return true;
|
|
case LG_CLIPBOARD_DATA_BMP : *type = SPICE_DATA_BMP ; return true;
|
|
case LG_CLIPBOARD_DATA_TIFF : *type = SPICE_DATA_TIFF; return true;
|
|
case LG_CLIPBOARD_DATA_JPEG : *type = SPICE_DATA_JPEG; return true;
|
|
case LG_CLIPBOARD_DATA_FILES: return false;
|
|
case LG_CLIPBOARD_DATA_NONE : *type = SPICE_DATA_NONE; return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static void spiceSetStatusListener(void * opaque,
|
|
LG_ClipboardStatusFn callback, void * callbackOpaque)
|
|
{
|
|
SpiceClipboard * clipboard = opaque;
|
|
LG_LOCK(clipboard->statusDispatch);
|
|
|
|
LG_LOCK(clipboard->stateLock);
|
|
clipboard->statusCallback = callback;
|
|
clipboard->statusOpaque = callbackOpaque;
|
|
const LG_ClipboardStatus status =
|
|
{
|
|
.available = clipboard->available,
|
|
.generation = clipboard->statusGeneration,
|
|
};
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
|
|
if (callback)
|
|
callback(callbackOpaque, &status);
|
|
LG_UNLOCK(clipboard->statusDispatch);
|
|
}
|
|
|
|
static bool spiceAttach(void * opaque,
|
|
const LG_ClipboardEventOps * events, void * eventOpaque)
|
|
{
|
|
SpiceClipboard * clipboard = opaque;
|
|
if (!events)
|
|
return false;
|
|
|
|
LG_LOCK(clipboard->eventDispatch);
|
|
LG_LOCK(clipboard->stateLock);
|
|
if (!clipboard->available)
|
|
{
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
LG_UNLOCK(clipboard->eventDispatch);
|
|
return false;
|
|
}
|
|
|
|
clipboard->events = events;
|
|
clipboard->eventOpaque = eventOpaque;
|
|
const bool notice = clipboard->remoteNotice;
|
|
const LG_ClipboardData type = clipboard->remoteType;
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
|
|
if (notice && events->notice)
|
|
events->notice(eventOpaque, &type, 1);
|
|
LG_UNLOCK(clipboard->eventDispatch);
|
|
return true;
|
|
}
|
|
|
|
static void spiceDetach(void * opaque)
|
|
{
|
|
SpiceClipboard * clipboard = opaque;
|
|
LG_LOCK(clipboard->eventDispatch);
|
|
LG_LOCK(clipboard->stateLock);
|
|
const bool failWrite = clipboard->available && clipboard->write.pending;
|
|
clipboard->events = NULL;
|
|
clipboard->eventOpaque = NULL;
|
|
clipboard->read = (PendingRequest) { 0 };
|
|
clipboard->write = (PendingRequest) { 0 };
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
if (failWrite)
|
|
purespice_clipboardDataStart(SPICE_DATA_NONE, 0);
|
|
LG_UNLOCK(clipboard->eventDispatch);
|
|
}
|
|
|
|
static bool spiceRelease(void * opaque)
|
|
{
|
|
SpiceClipboard * clipboard = opaque;
|
|
LG_LOCK(clipboard->stateLock);
|
|
const bool available = clipboard->available;
|
|
const bool failWrite = clipboard->write.pending;
|
|
clipboard->write = (PendingRequest) { 0 };
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
|
|
if (!available)
|
|
return false;
|
|
|
|
const bool failed = !failWrite ||
|
|
purespice_clipboardDataStart(SPICE_DATA_NONE, 0);
|
|
return purespice_clipboardRelease() && failed;
|
|
}
|
|
|
|
static bool spiceNotifyTypes(void * opaque,
|
|
const LG_ClipboardData types[], size_t count)
|
|
{
|
|
SpiceClipboard * clipboard = opaque;
|
|
if (count == 0)
|
|
return spiceRelease(clipboard);
|
|
if (!types || count > LG_CLIPBOARD_DATA_NONE)
|
|
return false;
|
|
|
|
PSDataType converted[LG_CLIPBOARD_DATA_NONE];
|
|
for (size_t i = 0; i < count; ++i)
|
|
if (types[i] == LG_CLIPBOARD_DATA_NONE ||
|
|
!lgType(types[i], &converted[i]))
|
|
return false;
|
|
|
|
LG_LOCK(clipboard->stateLock);
|
|
const bool available = clipboard->available;
|
|
const bool failWrite = clipboard->write.pending;
|
|
clipboard->write = (PendingRequest) { 0 };
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
|
|
if (!available)
|
|
return false;
|
|
|
|
const bool failed = !failWrite ||
|
|
purespice_clipboardDataStart(SPICE_DATA_NONE, 0);
|
|
return purespice_clipboardGrab(converted, (int)count) && failed;
|
|
}
|
|
|
|
static bool spiceData(void * opaque, LG_ClipboardRequest request,
|
|
LG_ClipboardData type, const void * data, size_t size)
|
|
{
|
|
SpiceClipboard * clipboard = opaque;
|
|
PSDataType converted;
|
|
if (request == LG_CLIPBOARD_REQUEST_INVALID || !lgType(type, &converted) ||
|
|
(type == LG_CLIPBOARD_DATA_NONE && size != 0) ||
|
|
(size && !data))
|
|
return false;
|
|
|
|
LG_LOCK(clipboard->stateLock);
|
|
const bool valid = clipboard->available && clipboard->write.pending &&
|
|
clipboard->write.request == request &&
|
|
(type == LG_CLIPBOARD_DATA_NONE || clipboard->write.type == type);
|
|
if (valid)
|
|
clipboard->write = (PendingRequest) { 0 };
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
|
|
if (!valid || !purespice_clipboardDataStart(converted, size))
|
|
return false;
|
|
|
|
return !size || purespice_clipboardData(
|
|
converted, (uint8_t *)data, size);
|
|
}
|
|
|
|
static bool spiceRequest(void * opaque, LG_ClipboardRequest request,
|
|
LG_ClipboardData type)
|
|
{
|
|
SpiceClipboard * clipboard = opaque;
|
|
PSDataType converted;
|
|
if (request == LG_CLIPBOARD_REQUEST_INVALID ||
|
|
type == LG_CLIPBOARD_DATA_NONE || !lgType(type, &converted))
|
|
return false;
|
|
|
|
LG_LOCK(clipboard->stateLock);
|
|
const bool valid = clipboard->available && clipboard->events &&
|
|
!clipboard->read.pending;
|
|
if (valid)
|
|
clipboard->read = (PendingRequest)
|
|
{
|
|
.pending = true,
|
|
.request = request,
|
|
.type = type,
|
|
};
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
|
|
if (!valid)
|
|
return false;
|
|
|
|
if (purespice_clipboardRequest(converted))
|
|
return true;
|
|
|
|
LG_LOCK(clipboard->stateLock);
|
|
if (!clipboard->read.pending || clipboard->read.request != request)
|
|
{
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
return true;
|
|
}
|
|
clipboard->read = (PendingRequest) { 0 };
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
return false;
|
|
}
|
|
|
|
static const LG_ClipboardOps l_clipboardOps =
|
|
{
|
|
.name = "SPICE",
|
|
.setStatusListener = spiceSetStatusListener,
|
|
.attach = spiceAttach,
|
|
.detach = spiceDetach,
|
|
.release = spiceRelease,
|
|
.notifyTypes = spiceNotifyTypes,
|
|
.data = spiceData,
|
|
.request = spiceRequest,
|
|
};
|
|
|
|
bool spiceClipboard_init(SpiceClipboard ** clipboard)
|
|
{
|
|
*clipboard = calloc(1, sizeof(**clipboard));
|
|
if (!*clipboard)
|
|
return false;
|
|
|
|
LG_LOCK_INIT((*clipboard)->stateLock);
|
|
LG_LOCK_INIT((*clipboard)->eventDispatch);
|
|
LG_LOCK_INIT((*clipboard)->statusDispatch);
|
|
(*clipboard)->remoteType = LG_CLIPBOARD_DATA_NONE;
|
|
return true;
|
|
}
|
|
|
|
void spiceClipboard_free(SpiceClipboard ** clipboard)
|
|
{
|
|
if (!clipboard || !*clipboard)
|
|
return;
|
|
|
|
SpiceClipboard * expected = *clipboard;
|
|
atomic_compare_exchange_strong_explicit(&l_callbackTarget,
|
|
&expected, NULL, memory_order_acq_rel, memory_order_acquire);
|
|
|
|
LG_LOCK_FREE((*clipboard)->statusDispatch);
|
|
LG_LOCK_FREE((*clipboard)->eventDispatch);
|
|
LG_LOCK_FREE((*clipboard)->stateLock);
|
|
free(*clipboard);
|
|
*clipboard = NULL;
|
|
}
|
|
|
|
const LG_ClipboardOps * spiceClipboard_getOps(void)
|
|
{
|
|
return &l_clipboardOps;
|
|
}
|
|
|
|
void spiceClipboard_setAvailable(SpiceClipboard * clipboard, bool available)
|
|
{
|
|
LG_LOCK(clipboard->statusDispatch);
|
|
LG_LOCK(clipboard->eventDispatch);
|
|
LG_LOCK(clipboard->stateLock);
|
|
|
|
const bool changed = clipboard->available != available;
|
|
if (changed)
|
|
{
|
|
clipboard->available = available;
|
|
clipboard->statusGeneration = nextGeneration(
|
|
clipboard->statusGeneration);
|
|
}
|
|
if (!available)
|
|
{
|
|
clipboard->remoteNotice = false;
|
|
clipboard->remoteType = LG_CLIPBOARD_DATA_NONE;
|
|
clipboard->read = (PendingRequest) { 0 };
|
|
clipboard->write = (PendingRequest) { 0 };
|
|
}
|
|
|
|
const LG_ClipboardStatusFn callback = clipboard->statusCallback;
|
|
void * callbackOpaque = clipboard->statusOpaque;
|
|
const LG_ClipboardStatus status =
|
|
{
|
|
.available = available,
|
|
.generation = clipboard->statusGeneration,
|
|
};
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
LG_UNLOCK(clipboard->eventDispatch);
|
|
|
|
if (changed && callback)
|
|
callback(callbackOpaque, &status);
|
|
LG_UNLOCK(clipboard->statusDispatch);
|
|
}
|
|
|
|
void spiceClipboard_setCallbackTarget(SpiceClipboard * clipboard)
|
|
{
|
|
atomic_store_explicit(
|
|
&l_callbackTarget, clipboard, memory_order_release);
|
|
}
|
|
|
|
static SpiceClipboard * callbackTarget(void)
|
|
{
|
|
return atomic_load_explicit(&l_callbackTarget, memory_order_acquire);
|
|
}
|
|
|
|
void spiceClipboard_notice(PSDataType source)
|
|
{
|
|
SpiceClipboard * clipboard = callbackTarget();
|
|
if (!clipboard)
|
|
return;
|
|
|
|
LG_ClipboardData type;
|
|
if (!spiceType(source, &type))
|
|
{
|
|
if (source != SPICE_DATA_NONE)
|
|
DEBUG_ERROR("Invalid SPICE clipboard notice type: %d", source);
|
|
spiceClipboard_release();
|
|
return;
|
|
}
|
|
|
|
LG_LOCK(clipboard->eventDispatch);
|
|
LG_LOCK(clipboard->stateLock);
|
|
const bool failWrite = clipboard->write.pending;
|
|
clipboard->remoteNotice = true;
|
|
clipboard->remoteType = type;
|
|
clipboard->read = (PendingRequest) { 0 };
|
|
clipboard->write = (PendingRequest) { 0 };
|
|
const ClipboardEventTarget target =
|
|
{
|
|
.events = clipboard->available ? clipboard->events : NULL,
|
|
.opaque = clipboard->eventOpaque,
|
|
};
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
|
|
if (failWrite)
|
|
purespice_clipboardDataStart(SPICE_DATA_NONE, 0);
|
|
if (target.events && target.events->notice)
|
|
target.events->notice(target.opaque, &type, 1);
|
|
LG_UNLOCK(clipboard->eventDispatch);
|
|
}
|
|
|
|
void spiceClipboard_data(PSDataType source, uint8_t * buffer, uint32_t size)
|
|
{
|
|
SpiceClipboard * clipboard = callbackTarget();
|
|
if (!clipboard)
|
|
return;
|
|
|
|
LG_ClipboardData type = LG_CLIPBOARD_DATA_NONE;
|
|
const bool converted = spiceType(source, &type);
|
|
const bool validData = source == SPICE_DATA_NONE ||
|
|
(converted && (!size || buffer));
|
|
|
|
LG_LOCK(clipboard->eventDispatch);
|
|
LG_LOCK(clipboard->stateLock);
|
|
if (!clipboard->read.pending)
|
|
{
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
LG_UNLOCK(clipboard->eventDispatch);
|
|
DEBUG_WARN("Ignoring unsolicited SPICE clipboard data");
|
|
return;
|
|
}
|
|
|
|
const PendingRequest request = clipboard->read;
|
|
clipboard->read = (PendingRequest) { 0 };
|
|
const ClipboardEventTarget target =
|
|
{
|
|
.events = clipboard->available ? clipboard->events : NULL,
|
|
.opaque = clipboard->eventOpaque,
|
|
};
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
|
|
if (target.events && target.events->data)
|
|
{
|
|
if (!validData || (source != SPICE_DATA_NONE && type != request.type))
|
|
{
|
|
DEBUG_ERROR("Invalid SPICE clipboard response");
|
|
target.events->data(target.opaque, request.request,
|
|
LG_CLIPBOARD_DATA_NONE, NULL, 0);
|
|
}
|
|
else
|
|
{
|
|
if (type == LG_CLIPBOARD_DATA_TEXT && size)
|
|
{
|
|
uint8_t * output = buffer;
|
|
for (uint32_t i = 0; i < size; ++i)
|
|
if (buffer[i] != '\r')
|
|
*output++ = buffer[i];
|
|
size = (uint32_t)(output - buffer);
|
|
}
|
|
|
|
target.events->data(target.opaque, request.request,
|
|
type, buffer, size);
|
|
}
|
|
}
|
|
LG_UNLOCK(clipboard->eventDispatch);
|
|
}
|
|
|
|
void spiceClipboard_release(void)
|
|
{
|
|
SpiceClipboard * clipboard = callbackTarget();
|
|
if (!clipboard)
|
|
return;
|
|
|
|
LG_LOCK(clipboard->eventDispatch);
|
|
LG_LOCK(clipboard->stateLock);
|
|
clipboard->remoteNotice = false;
|
|
clipboard->remoteType = LG_CLIPBOARD_DATA_NONE;
|
|
clipboard->read = (PendingRequest) { 0 };
|
|
const ClipboardEventTarget target =
|
|
{
|
|
.events = clipboard->available ? clipboard->events : NULL,
|
|
.opaque = clipboard->eventOpaque,
|
|
};
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
|
|
if (target.events && target.events->release)
|
|
target.events->release(target.opaque);
|
|
LG_UNLOCK(clipboard->eventDispatch);
|
|
}
|
|
|
|
void spiceClipboard_request(PSDataType source)
|
|
{
|
|
SpiceClipboard * clipboard = callbackTarget();
|
|
if (!clipboard)
|
|
{
|
|
purespice_clipboardDataStart(SPICE_DATA_NONE, 0);
|
|
return;
|
|
}
|
|
|
|
LG_ClipboardData type;
|
|
if (!spiceType(source, &type))
|
|
{
|
|
DEBUG_ERROR("Invalid SPICE clipboard request type: %d", source);
|
|
purespice_clipboardDataStart(SPICE_DATA_NONE, 0);
|
|
return;
|
|
}
|
|
|
|
LG_LOCK(clipboard->eventDispatch);
|
|
LG_LOCK(clipboard->stateLock);
|
|
const bool busy = clipboard->write.pending;
|
|
const ClipboardEventTarget target =
|
|
{
|
|
.events = clipboard->available && !busy ?
|
|
clipboard->events : NULL,
|
|
.opaque = clipboard->eventOpaque,
|
|
};
|
|
LG_ClipboardRequest request = LG_CLIPBOARD_REQUEST_INVALID;
|
|
if (target.events)
|
|
{
|
|
request = nextRequestNL(clipboard);
|
|
clipboard->write = (PendingRequest)
|
|
{
|
|
.pending = true,
|
|
.request = request,
|
|
.type = type,
|
|
};
|
|
}
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
|
|
const bool accepted = target.events && target.events->request &&
|
|
target.events->request(target.opaque, request, type);
|
|
if (!accepted)
|
|
{
|
|
bool fail = !busy && request == LG_CLIPBOARD_REQUEST_INVALID;
|
|
LG_LOCK(clipboard->stateLock);
|
|
if (clipboard->write.pending && clipboard->write.request == request)
|
|
{
|
|
clipboard->write = (PendingRequest) { 0 };
|
|
fail = true;
|
|
}
|
|
LG_UNLOCK(clipboard->stateLock);
|
|
if (fail)
|
|
purespice_clipboardDataStart(SPICE_DATA_NONE, 0);
|
|
else if (busy)
|
|
DEBUG_WARN("Ignoring overlapping SPICE clipboard request");
|
|
}
|
|
LG_UNLOCK(clipboard->eventDispatch);
|
|
}
|
|
|
|
void spiceClipboard_status(bool available)
|
|
{
|
|
SpiceClipboard * clipboard = callbackTarget();
|
|
if (clipboard)
|
|
spiceClipboard_setAvailable(clipboard, available);
|
|
}
|