mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-22 15:11:31 +00:00
[client] spice: move protocol into transport
This commit is contained in:
558
client/transports/SPICE/clipboard.c
Normal file
558
client/transports/SPICE/clipboard.c
Normal file
@@ -0,0 +1,558 @@
|
||||
/**
|
||||
* 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_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);
|
||||
}
|
||||
Reference in New Issue
Block a user