mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-21 22:51:31 +00:00
Report guest HID keyboard LED state from LGInput to LGIdd and publish it with the LGMP input endpoint status. Forward SPICE modifier updates through the same provider state so evdev can synchronize physical keyboard LEDs across transport changes and reconnects. Bump the versioned pipe and KVMFR input status contracts for the new feedback state. Accept earlier LGMP input status versions without LED feedback so the client retains input while the guest driver is being upgraded.
1113 lines
29 KiB
C
1113 lines
29 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 "spice.h"
|
|
#include "test.h"
|
|
|
|
#include "common/debug.h"
|
|
|
|
#include <purespice.h>
|
|
|
|
#include <pthread.h>
|
|
#include <stdatomic.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#define ARRAY_LENGTH(a) (sizeof(a) / sizeof((a)[0]))
|
|
#define WAIT_MS 2000U
|
|
|
|
extern const LG_TransportOps LGT_SPICE;
|
|
|
|
struct SpiceInput
|
|
{
|
|
bool available;
|
|
uint32_t keyModifiers;
|
|
unsigned int onN;
|
|
unsigned int offN;
|
|
unsigned int onOrder;
|
|
unsigned int offFirst;
|
|
unsigned int offOrder;
|
|
};
|
|
|
|
struct SpiceClipboard
|
|
{
|
|
bool available;
|
|
unsigned int onN;
|
|
unsigned int offN;
|
|
unsigned int offFirst;
|
|
unsigned int offOrder;
|
|
};
|
|
|
|
struct Pure
|
|
{
|
|
PSConfig config;
|
|
atomic_bool connectOK;
|
|
atomic_bool callReady;
|
|
atomic_bool blockConnect;
|
|
atomic_bool enterConnect;
|
|
atomic_bool allowConnect;
|
|
atomic_bool blockChannelConnect;
|
|
atomic_bool enterChannelConnect;
|
|
atomic_bool channelCancel[PS_CHANNEL_MAX];
|
|
atomic_bool cancel;
|
|
atomic_bool drop;
|
|
PSStatus dropStatus;
|
|
bool infoOK;
|
|
const char * serverName;
|
|
uint8_t uuid[16];
|
|
|
|
atomic_uint connectN;
|
|
atomic_uint disconnectN;
|
|
atomic_uint processN;
|
|
atomic_uint mouseModeN;
|
|
atomic_uint infoN;
|
|
atomic_uint freeInfoN;
|
|
atomic_uint order;
|
|
unsigned int targetSetOrder;
|
|
unsigned int connectOrder;
|
|
unsigned int disconnectOrder;
|
|
|
|
bool has[PS_CHANNEL_MAX];
|
|
bool connected[PS_CHANNEL_MAX];
|
|
bool connectResult[PS_CHANNEL_MAX];
|
|
bool disconnectResult[PS_CHANNEL_MAX];
|
|
unsigned int connectChannelN[PS_CHANNEL_MAX];
|
|
unsigned int disconnectChannelN[PS_CHANNEL_MAX];
|
|
};
|
|
|
|
struct SurfaceLog
|
|
{
|
|
unsigned int configN;
|
|
unsigned int destroyN;
|
|
unsigned int fillN;
|
|
unsigned int bitmapN;
|
|
unsigned int pointerN;
|
|
unsigned int width;
|
|
unsigned int height;
|
|
int x;
|
|
int y;
|
|
int w;
|
|
int h;
|
|
int stride;
|
|
uint32_t color;
|
|
bool topDown;
|
|
uint8_t bitmap[32];
|
|
size_t bitmapSize;
|
|
LG_TransportPointer pointer;
|
|
uint8_t shape[32];
|
|
size_t shapeSize;
|
|
atomic_bool block;
|
|
atomic_bool entered;
|
|
atomic_bool release;
|
|
};
|
|
|
|
struct Fixture
|
|
{
|
|
LG_Transport transport;
|
|
SpiceInput input;
|
|
SpiceClipboard clipboard;
|
|
struct SurfaceLog log;
|
|
};
|
|
|
|
static struct Pure ps;
|
|
static _Atomic(SpiceClipboard *) cbTarget;
|
|
|
|
static unsigned int nextOrder(void)
|
|
{
|
|
return atomic_fetch_add(&ps.order, 1) + 1;
|
|
}
|
|
|
|
void purespice_init(const PSInit * init)
|
|
{
|
|
(void)init;
|
|
}
|
|
|
|
void purespice_beginConnect(void)
|
|
{
|
|
atomic_store_explicit(&ps.cancel, false, memory_order_release);
|
|
}
|
|
|
|
bool purespice_connect(const PSConfig * config)
|
|
{
|
|
while (!atomic_load_explicit(&ps.allowConnect, memory_order_acquire) &&
|
|
!atomic_load_explicit(&ps.cancel, memory_order_acquire))
|
|
usleep(1000);
|
|
atomic_store_explicit(&ps.enterConnect, true, memory_order_release);
|
|
|
|
ps.config = *config;
|
|
ps.connectOrder = nextOrder();
|
|
atomic_fetch_add(&ps.connectN, 1);
|
|
|
|
while (atomic_load(&ps.blockConnect) && !atomic_load(&ps.cancel))
|
|
usleep(1000);
|
|
|
|
if (atomic_load(&ps.cancel) || !atomic_load(&ps.connectOK))
|
|
return false;
|
|
if (atomic_load(&ps.callReady) && config->ready)
|
|
config->ready();
|
|
return true;
|
|
}
|
|
|
|
void purespice_cancelConnect(void)
|
|
{
|
|
atomic_store_explicit(&ps.cancel, true, memory_order_release);
|
|
}
|
|
|
|
void purespice_beginChannelConnect(PSChannelType channel)
|
|
{
|
|
atomic_store_explicit(
|
|
&ps.channelCancel[channel], false, memory_order_release);
|
|
}
|
|
|
|
void purespice_cancelChannelConnect(PSChannelType channel)
|
|
{
|
|
atomic_store_explicit(
|
|
&ps.channelCancel[channel], true, memory_order_release);
|
|
}
|
|
|
|
void purespice_disconnect(void)
|
|
{
|
|
ps.disconnectOrder = nextOrder();
|
|
atomic_fetch_add(&ps.disconnectN, 1);
|
|
}
|
|
|
|
PSStatus purespice_process(int timeout)
|
|
{
|
|
(void)timeout;
|
|
atomic_fetch_add(&ps.processN, 1);
|
|
if (atomic_load(&ps.drop))
|
|
return ps.dropStatus;
|
|
usleep(1000);
|
|
return PS_STATUS_RUN;
|
|
}
|
|
|
|
bool purespice_getServerInfo(PSServerInfo * info)
|
|
{
|
|
atomic_fetch_add(&ps.infoN, 1);
|
|
if (!ps.infoOK)
|
|
return false;
|
|
|
|
memset(info, 0, sizeof(*info));
|
|
if (ps.serverName)
|
|
info->name = strdup(ps.serverName);
|
|
memcpy(info->uuid, ps.uuid, sizeof(info->uuid));
|
|
return true;
|
|
}
|
|
|
|
void purespice_freeServerInfo(PSServerInfo * info)
|
|
{
|
|
free(info->name);
|
|
info->name = NULL;
|
|
atomic_fetch_add(&ps.freeInfoN, 1);
|
|
}
|
|
|
|
bool purespice_hasChannel(PSChannelType channel)
|
|
{
|
|
return ps.has[channel];
|
|
}
|
|
|
|
bool purespice_channelConnected(PSChannelType channel)
|
|
{
|
|
return ps.connected[channel];
|
|
}
|
|
|
|
bool purespice_connectChannel(PSChannelType channel)
|
|
{
|
|
++ps.connectChannelN[channel];
|
|
atomic_store_explicit(
|
|
&ps.enterChannelConnect, true, memory_order_release);
|
|
while (atomic_load_explicit(
|
|
&ps.blockChannelConnect, memory_order_acquire) &&
|
|
!atomic_load_explicit(&ps.cancel, memory_order_acquire) &&
|
|
!atomic_load_explicit(
|
|
&ps.channelCancel[channel], memory_order_acquire))
|
|
usleep(1000);
|
|
if (atomic_load_explicit(&ps.cancel, memory_order_acquire) ||
|
|
atomic_load_explicit(
|
|
&ps.channelCancel[channel], memory_order_acquire))
|
|
return false;
|
|
if (!ps.connectResult[channel])
|
|
return false;
|
|
ps.connected[channel] = true;
|
|
return true;
|
|
}
|
|
|
|
bool purespice_disconnectChannel(PSChannelType channel)
|
|
{
|
|
++ps.disconnectChannelN[channel];
|
|
if (!ps.disconnectResult[channel])
|
|
return false;
|
|
ps.connected[channel] = false;
|
|
return true;
|
|
}
|
|
|
|
bool purespice_mouseMode(bool server)
|
|
{
|
|
(void)server;
|
|
atomic_fetch_add(&ps.mouseModeN, 1);
|
|
return true;
|
|
}
|
|
|
|
bool spiceInput_init(SpiceInput ** input)
|
|
{
|
|
*input = calloc(1, sizeof(**input));
|
|
return *input != NULL;
|
|
}
|
|
|
|
void spiceInput_free(SpiceInput ** input)
|
|
{
|
|
if (!input)
|
|
return;
|
|
free(*input);
|
|
*input = NULL;
|
|
}
|
|
|
|
const LG_InputOps * spiceInput_getOps(void)
|
|
{
|
|
static const LG_InputOps ops = { .name = "test" };
|
|
return &ops;
|
|
}
|
|
|
|
void spiceInput_setAvailable(SpiceInput * input, bool available)
|
|
{
|
|
input->available = available;
|
|
if (available)
|
|
{
|
|
++input->onN;
|
|
input->onOrder = nextOrder();
|
|
}
|
|
else
|
|
{
|
|
++input->offN;
|
|
input->offOrder = nextOrder();
|
|
if (input->offN == 1)
|
|
input->offFirst = input->offOrder;
|
|
}
|
|
}
|
|
|
|
void spiceInput_setKeyboardLEDs(SpiceInput * input, uint32_t modifiers)
|
|
{
|
|
input->keyModifiers = modifiers;
|
|
}
|
|
|
|
bool spiceClipboard_init(SpiceClipboard ** clipboard)
|
|
{
|
|
*clipboard = calloc(1, sizeof(**clipboard));
|
|
return *clipboard != NULL;
|
|
}
|
|
|
|
void spiceClipboard_free(SpiceClipboard ** clipboard)
|
|
{
|
|
if (!clipboard)
|
|
return;
|
|
free(*clipboard);
|
|
*clipboard = NULL;
|
|
}
|
|
|
|
const LG_ClipboardOps * spiceClipboard_getOps(void)
|
|
{
|
|
static const LG_ClipboardOps ops = { .name = "test" };
|
|
return &ops;
|
|
}
|
|
|
|
void spiceClipboard_setAvailable(SpiceClipboard * clipboard, bool available)
|
|
{
|
|
clipboard->available = available;
|
|
if (available)
|
|
++clipboard->onN;
|
|
else
|
|
{
|
|
++clipboard->offN;
|
|
clipboard->offOrder = nextOrder();
|
|
if (clipboard->offN == 1)
|
|
clipboard->offFirst = clipboard->offOrder;
|
|
}
|
|
}
|
|
|
|
void spiceClipboard_setCallbackTarget(SpiceClipboard * clipboard)
|
|
{
|
|
atomic_store(&cbTarget, clipboard);
|
|
if (clipboard)
|
|
ps.targetSetOrder = nextOrder();
|
|
}
|
|
|
|
void spiceClipboard_notice(PSDataType type)
|
|
{
|
|
(void)type;
|
|
}
|
|
|
|
void spiceClipboard_data(PSDataType type, uint8_t * buffer, uint32_t size)
|
|
{
|
|
(void)type;
|
|
(void)buffer;
|
|
(void)size;
|
|
}
|
|
|
|
void spiceClipboard_release(void)
|
|
{
|
|
}
|
|
|
|
void spiceClipboard_request(PSDataType type)
|
|
{
|
|
(void)type;
|
|
}
|
|
|
|
void spiceClipboard_status(bool available)
|
|
{
|
|
SpiceClipboard * clipboard = atomic_load(&cbTarget);
|
|
if (clipboard)
|
|
spiceClipboard_setAvailable(clipboard, available);
|
|
}
|
|
|
|
static void resetPure(void)
|
|
{
|
|
memset(&ps, 0, sizeof(ps));
|
|
atomic_init(&ps.connectOK, true);
|
|
atomic_init(&ps.callReady, true);
|
|
atomic_init(&ps.blockConnect, false);
|
|
atomic_init(&ps.enterConnect, false);
|
|
atomic_init(&ps.allowConnect, true);
|
|
atomic_init(&ps.blockChannelConnect, false);
|
|
atomic_init(&ps.enterChannelConnect, false);
|
|
for(unsigned int i = 0; i < PS_CHANNEL_MAX; ++i)
|
|
atomic_init(&ps.channelCancel[i], false);
|
|
atomic_init(&ps.cancel, false);
|
|
atomic_init(&ps.drop, false);
|
|
atomic_init(&ps.connectN, 0);
|
|
atomic_init(&ps.disconnectN, 0);
|
|
atomic_init(&ps.processN, 0);
|
|
atomic_init(&ps.mouseModeN, 0);
|
|
atomic_init(&ps.infoN, 0);
|
|
atomic_init(&ps.freeInfoN, 0);
|
|
atomic_init(&ps.order, 0);
|
|
atomic_store(&cbTarget, NULL);
|
|
|
|
ps.dropStatus = PS_STATUS_ERR_READ;
|
|
ps.infoOK = true;
|
|
ps.serverName = "test-vm";
|
|
ps.uuid[0] = 0x42;
|
|
for (unsigned int i = 0; i < PS_CHANNEL_MAX; ++i)
|
|
{
|
|
ps.has[i] = true;
|
|
ps.connectResult[i] = true;
|
|
ps.disconnectResult[i] = true;
|
|
}
|
|
}
|
|
|
|
static void configure(void * opaque, unsigned int width,
|
|
unsigned int height)
|
|
{
|
|
struct SurfaceLog * log = opaque;
|
|
++log->configN;
|
|
log->width = width;
|
|
log->height = height;
|
|
if (atomic_load(&log->block))
|
|
{
|
|
atomic_store(&log->entered, true);
|
|
while (!atomic_load(&log->release))
|
|
usleep(1000);
|
|
}
|
|
}
|
|
|
|
static void destroy(void * opaque)
|
|
{
|
|
struct SurfaceLog * log = opaque;
|
|
++log->destroyN;
|
|
}
|
|
|
|
static void drawFill(void * opaque, int x, int y,
|
|
int width, int height, uint32_t color)
|
|
{
|
|
struct SurfaceLog * log = opaque;
|
|
++log->fillN;
|
|
log->x = x;
|
|
log->y = y;
|
|
log->w = width;
|
|
log->h = height;
|
|
log->color = color;
|
|
}
|
|
|
|
static void drawBitmap(void * opaque, bool topDown,
|
|
int x, int y, int width, int height, int stride, const void * data)
|
|
{
|
|
struct SurfaceLog * log = opaque;
|
|
++log->bitmapN;
|
|
log->topDown = topDown;
|
|
log->x = x;
|
|
log->y = y;
|
|
log->w = width;
|
|
log->h = height;
|
|
log->stride = stride;
|
|
log->bitmapSize = (size_t)height * (size_t)stride;
|
|
CHECK(log->bitmapSize <= sizeof(log->bitmap));
|
|
memcpy(log->bitmap, data, log->bitmapSize);
|
|
}
|
|
|
|
static void pointer(void * opaque, const LG_TransportPointer * pointer)
|
|
{
|
|
struct SurfaceLog * log = opaque;
|
|
++log->pointerN;
|
|
log->pointer = *pointer;
|
|
if (pointer->shape)
|
|
{
|
|
log->shapeSize = (size_t)pointer->height * pointer->pitch;
|
|
CHECK(log->shapeSize <= sizeof(log->shape));
|
|
memcpy(log->shape, pointer->shape, log->shapeSize);
|
|
log->pointer.shape = log->shape;
|
|
}
|
|
}
|
|
|
|
static const LG_SwSurfaceEventOps surfaceEvents =
|
|
{
|
|
.configure = configure,
|
|
.destroy = destroy,
|
|
.drawFill = drawFill,
|
|
.drawBitmap = drawBitmap,
|
|
.pointer = pointer,
|
|
};
|
|
|
|
static void initFixture(struct Fixture * f)
|
|
{
|
|
memset(f, 0, sizeof(*f));
|
|
f->transport.host = "test";
|
|
f->transport.port = 5900;
|
|
f->transport.inputEnabled = true;
|
|
f->transport.clipboardEnabled = true;
|
|
f->transport.input = &f->input;
|
|
f->transport.clipboard = &f->clipboard;
|
|
CHECK(spiceSurface_init(&f->transport.surface));
|
|
f->transport.connectEvent = lgCreateEvent(false, 0);
|
|
CHECK(f->transport.connectEvent);
|
|
atomic_init(&f->transport.stop, false);
|
|
atomic_init(&f->transport.sessionValid, false);
|
|
atomic_init(&f->log.block, false);
|
|
atomic_init(&f->log.entered, false);
|
|
atomic_init(&f->log.release, false);
|
|
}
|
|
|
|
static void freeFixture(struct Fixture * f)
|
|
{
|
|
LGT_SPICE.disconnect(&f->transport);
|
|
lgFreeEvent(f->transport.connectEvent);
|
|
f->transport.connectEvent = NULL;
|
|
spiceSurface_free(&f->transport.surface);
|
|
}
|
|
|
|
static void waitInvalid(LG_Transport * transport)
|
|
{
|
|
for (unsigned int i = 0; i < WAIT_MS; ++i)
|
|
{
|
|
if (!LGT_SPICE.sessionValid(transport))
|
|
return;
|
|
usleep(1000);
|
|
}
|
|
|
|
CHECK(!LGT_SPICE.sessionValid(transport));
|
|
}
|
|
|
|
static void waitBool(atomic_bool * value)
|
|
{
|
|
for (unsigned int i = 0; i < WAIT_MS; ++i)
|
|
{
|
|
if (atomic_load_explicit(value, memory_order_acquire))
|
|
return;
|
|
usleep(1000);
|
|
}
|
|
|
|
CHECK(atomic_load(value));
|
|
}
|
|
|
|
static bool cancel(void * opaque)
|
|
{
|
|
unsigned int * calls = opaque;
|
|
++*calls;
|
|
return true;
|
|
}
|
|
|
|
static bool cancelBeforeEntry(void * opaque)
|
|
{
|
|
unsigned int * calls = opaque;
|
|
++*calls;
|
|
return !atomic_load_explicit(&ps.enterConnect, memory_order_acquire);
|
|
}
|
|
|
|
static void testConnectFail(void)
|
|
{
|
|
resetPure();
|
|
atomic_store(&ps.connectOK, false);
|
|
struct Fixture f;
|
|
initFixture(&f);
|
|
|
|
LG_TransportSession session;
|
|
CHECK(LGT_SPICE.connect(&f.transport, &session) == LG_TRANSPORT_ERROR);
|
|
CHECK(!f.transport.thread);
|
|
CHECK(!LGT_SPICE.sessionValid(&f.transport));
|
|
CHECK(atomic_load(&ps.connectN) == 1);
|
|
CHECK(atomic_load(&ps.disconnectN) == 0);
|
|
CHECK(f.input.onN == 0);
|
|
CHECK(f.input.offN == 1);
|
|
CHECK(f.clipboard.offN == 1);
|
|
CHECK(!atomic_load(&cbTarget));
|
|
CHECK(ps.targetSetOrder < ps.connectOrder);
|
|
|
|
ps.config.ready();
|
|
CHECK(f.input.onN == 0);
|
|
CHECK(!LGT_SPICE.sessionValid(&f.transport));
|
|
freeFixture(&f);
|
|
}
|
|
|
|
static void testPreReadyDrop(void)
|
|
{
|
|
resetPure();
|
|
atomic_store(&ps.callReady, false);
|
|
atomic_store(&ps.drop, true);
|
|
ps.dropStatus = PS_STATUS_SHUTDOWN;
|
|
struct Fixture f;
|
|
initFixture(&f);
|
|
|
|
LG_TransportSession session;
|
|
CHECK(LGT_SPICE.connect(&f.transport, &session) == LG_TRANSPORT_ERROR);
|
|
CHECK(!f.transport.thread);
|
|
CHECK(!LGT_SPICE.sessionValid(&f.transport));
|
|
CHECK(atomic_load(&ps.connectN) == 1);
|
|
CHECK(atomic_load(&ps.processN) == 1);
|
|
CHECK(atomic_load(&ps.disconnectN) == 1);
|
|
CHECK(f.input.onN == 0);
|
|
CHECK(f.input.offN == 2);
|
|
CHECK(f.clipboard.offN == 2);
|
|
CHECK(f.input.offFirst < ps.disconnectOrder);
|
|
CHECK(f.clipboard.offFirst < ps.disconnectOrder);
|
|
CHECK(!atomic_load(&cbTarget));
|
|
freeFixture(&f);
|
|
}
|
|
|
|
static void testCancel(void)
|
|
{
|
|
resetPure();
|
|
atomic_store(&ps.blockConnect, true);
|
|
struct Fixture f;
|
|
initFixture(&f);
|
|
unsigned int cancelN = 0;
|
|
LG_TransportSession session;
|
|
|
|
CHECK(LGT_SPICE.connectCancellable(&f.transport, &session,
|
|
cancel, &cancelN) == LG_TRANSPORT_DISCONNECTED);
|
|
CHECK(cancelN == 1);
|
|
CHECK(!f.transport.thread);
|
|
CHECK(!LGT_SPICE.sessionValid(&f.transport));
|
|
CHECK(atomic_load(&ps.connectN) == 1);
|
|
CHECK(atomic_load(&ps.disconnectN) == 0);
|
|
CHECK(f.input.onN == 0);
|
|
CHECK(f.input.offN == 1);
|
|
CHECK(f.clipboard.offN == 1);
|
|
CHECK(!atomic_load(&cbTarget));
|
|
freeFixture(&f);
|
|
|
|
resetPure();
|
|
atomic_store_explicit(&ps.allowConnect, false, memory_order_release);
|
|
atomic_store_explicit(&ps.enterConnect, false, memory_order_release);
|
|
initFixture(&f);
|
|
cancelN = 0;
|
|
CHECK(LGT_SPICE.connectCancellable(&f.transport, &session,
|
|
cancelBeforeEntry, &cancelN) == LG_TRANSPORT_DISCONNECTED);
|
|
CHECK(cancelN == 1);
|
|
CHECK(!f.transport.thread);
|
|
CHECK(!LGT_SPICE.sessionValid(&f.transport));
|
|
CHECK(atomic_load_explicit(&ps.cancel, memory_order_acquire));
|
|
freeFixture(&f);
|
|
}
|
|
|
|
static void testReadyDrop(void)
|
|
{
|
|
resetPure();
|
|
struct Fixture f;
|
|
initFixture(&f);
|
|
const LG_VideoOps * video = LGT_SPICE.getVideoOps(&f.transport);
|
|
CHECK(video && video->type == LG_VIDEO_TYPE_SW_SURFACE);
|
|
CHECK(video->swSurface->attach(
|
|
&f.transport, &surfaceEvents, &f.log));
|
|
|
|
LG_TransportSession session;
|
|
CHECK(LGT_SPICE.connect(&f.transport, &session) == LG_TRANSPORT_OK);
|
|
CHECK(LGT_SPICE.sessionValid(&f.transport));
|
|
CHECK(strcmp(session.version, "SPICE") == 0);
|
|
CHECK(strcmp(session.name, "test-vm") == 0);
|
|
CHECK(session.uuidValid);
|
|
CHECK(session.uuid[0] == 0x42);
|
|
CHECK(session.os == LG_TRANSPORT_OS_OTHER);
|
|
CHECK(f.input.available);
|
|
CHECK(f.input.onN == 1);
|
|
CHECK(atomic_load(&ps.mouseModeN) == 1);
|
|
CHECK(atomic_load(&ps.infoN) == 1);
|
|
CHECK(atomic_load(&ps.freeInfoN) == 1);
|
|
CHECK(ps.targetSetOrder < ps.connectOrder);
|
|
CHECK(ps.connectOrder < f.input.onOrder);
|
|
|
|
ps.config.clipboard.status(true);
|
|
CHECK(f.clipboard.available);
|
|
ps.config.display.surfaceCreate(
|
|
0, PS_SURFACE_FMT_32_xRGB, 640, 480);
|
|
CHECK(f.log.configN == 1);
|
|
CHECK(video->swSurface->setActive(&f.transport, true));
|
|
|
|
atomic_store(&ps.drop, true);
|
|
waitInvalid(&f.transport);
|
|
LGT_SPICE.disconnect(&f.transport);
|
|
CHECK(!f.transport.thread);
|
|
CHECK(!f.input.available);
|
|
CHECK(!f.clipboard.available);
|
|
CHECK(f.input.offN >= 1);
|
|
CHECK(f.clipboard.offN >= 1);
|
|
CHECK(atomic_load(&ps.disconnectN) == 1);
|
|
CHECK(f.input.offFirst < ps.disconnectOrder);
|
|
CHECK(f.clipboard.offFirst < ps.disconnectOrder);
|
|
CHECK(!atomic_load(&cbTarget));
|
|
CHECK(LGT_SPICE.sendControl(&f.transport, NULL, NULL) ==
|
|
LG_TRANSPORT_DISCONNECTED);
|
|
|
|
const unsigned int inputOn = f.input.onN;
|
|
const unsigned int configN = f.log.configN;
|
|
ps.config.ready();
|
|
ps.config.display.surfaceCreate(
|
|
0, PS_SURFACE_FMT_32_xRGB, 800, 600);
|
|
CHECK(f.input.onN == inputOn);
|
|
CHECK(f.log.configN == configN);
|
|
video->swSurface->detach(&f.transport);
|
|
freeFixture(&f);
|
|
}
|
|
|
|
static void testSessionOrder(void)
|
|
{
|
|
resetPure();
|
|
struct Fixture first;
|
|
struct Fixture second;
|
|
initFixture(&first);
|
|
initFixture(&second);
|
|
|
|
LG_TransportSession session;
|
|
CHECK(LGT_SPICE.connect(&first.transport, &session) == LG_TRANSPORT_OK);
|
|
CHECK(LGT_SPICE.connect(&second.transport, &session) ==
|
|
LG_TRANSPORT_ERROR);
|
|
CHECK(!second.transport.thread);
|
|
CHECK(atomic_load(&ps.connectN) == 1);
|
|
CHECK(LGT_SPICE.sessionValid(&first.transport));
|
|
CHECK(!LGT_SPICE.sessionValid(&second.transport));
|
|
|
|
LGT_SPICE.disconnect(&first.transport);
|
|
CHECK(!LGT_SPICE.sessionValid(&first.transport));
|
|
CHECK(LGT_SPICE.connect(&second.transport, &session) == LG_TRANSPORT_OK);
|
|
CHECK(atomic_load(&ps.connectN) == 2);
|
|
CHECK(LGT_SPICE.sessionValid(&second.transport));
|
|
LGT_SPICE.disconnect(&second.transport);
|
|
freeFixture(&second);
|
|
freeFixture(&first);
|
|
}
|
|
|
|
static void testSurfaceActive(void)
|
|
{
|
|
resetPure();
|
|
struct Fixture f;
|
|
initFixture(&f);
|
|
const LG_SwSurfaceOps * sw =
|
|
spiceSurface_getVideoOps()->swSurface;
|
|
|
|
LG_SwSurfaceEventOps bad = surfaceEvents;
|
|
bad.pointer = NULL;
|
|
CHECK(!sw->attach(&f.transport, &bad, &f.log));
|
|
CHECK(sw->attach(&f.transport, &surfaceEvents, &f.log));
|
|
CHECK(!sw->setActive(&f.transport, true));
|
|
atomic_store(&f.transport.sessionValid, true);
|
|
|
|
ps.has[PS_CHANNEL_DISPLAY] = false;
|
|
CHECK(!sw->setActive(&f.transport, true));
|
|
ps.has[PS_CHANNEL_DISPLAY] = true;
|
|
ps.connectResult[PS_CHANNEL_DISPLAY] = false;
|
|
CHECK(!sw->setActive(&f.transport, true));
|
|
CHECK(ps.connectChannelN[PS_CHANNEL_DISPLAY] == 1);
|
|
CHECK(ps.connectChannelN[PS_CHANNEL_CURSOR] == 0);
|
|
|
|
ps.connectResult[PS_CHANNEL_DISPLAY] = true;
|
|
ps.connectResult[PS_CHANNEL_CURSOR] = false;
|
|
CHECK(!sw->setActive(&f.transport, true));
|
|
CHECK(ps.disconnectChannelN[PS_CHANNEL_DISPLAY] == 1);
|
|
CHECK(!ps.connected[PS_CHANNEL_DISPLAY]);
|
|
|
|
ps.connectResult[PS_CHANNEL_CURSOR] = true;
|
|
CHECK(sw->setActive(&f.transport, true));
|
|
const unsigned int displayConnect =
|
|
ps.connectChannelN[PS_CHANNEL_DISPLAY];
|
|
const unsigned int cursorConnect =
|
|
ps.connectChannelN[PS_CHANNEL_CURSOR];
|
|
CHECK(sw->setActive(&f.transport, true));
|
|
CHECK(ps.connectChannelN[PS_CHANNEL_DISPLAY] == displayConnect);
|
|
CHECK(ps.connectChannelN[PS_CHANNEL_CURSOR] == cursorConnect);
|
|
|
|
ps.disconnectResult[PS_CHANNEL_DISPLAY] = false;
|
|
CHECK(!sw->setActive(&f.transport, false));
|
|
CHECK(ps.disconnectChannelN[PS_CHANNEL_CURSOR] == 0);
|
|
ps.disconnectResult[PS_CHANNEL_DISPLAY] = true;
|
|
ps.disconnectResult[PS_CHANNEL_CURSOR] = false;
|
|
CHECK(!sw->setActive(&f.transport, false));
|
|
CHECK(ps.connected[PS_CHANNEL_DISPLAY]);
|
|
CHECK(ps.connectChannelN[PS_CHANNEL_DISPLAY] == displayConnect + 1);
|
|
|
|
ps.disconnectResult[PS_CHANNEL_CURSOR] = true;
|
|
CHECK(sw->setActive(&f.transport, false));
|
|
CHECK(!ps.connected[PS_CHANNEL_DISPLAY]);
|
|
CHECK(!ps.connected[PS_CHANNEL_CURSOR]);
|
|
sw->detach(&f.transport);
|
|
freeFixture(&f);
|
|
}
|
|
|
|
struct ActiveCall
|
|
{
|
|
const LG_SwSurfaceOps * ops;
|
|
LG_Transport * transport;
|
|
atomic_bool done;
|
|
bool result;
|
|
};
|
|
|
|
static void * activateSurface(void * opaque)
|
|
{
|
|
struct ActiveCall * call = opaque;
|
|
call->result = call->ops->setActive(call->transport, true);
|
|
atomic_store_explicit(&call->done, true, memory_order_release);
|
|
return NULL;
|
|
}
|
|
|
|
static void * holdActivationLock(void * opaque)
|
|
{
|
|
LG_Transport * transport = opaque;
|
|
spiceSurface_testLockActivation(transport->surface);
|
|
atomic_store_explicit(
|
|
&ps.enterChannelConnect, true, memory_order_release);
|
|
while (atomic_load_explicit(
|
|
&ps.blockChannelConnect, memory_order_acquire))
|
|
usleep(1000);
|
|
spiceSurface_testUnlockActivation(transport->surface);
|
|
return NULL;
|
|
}
|
|
|
|
static void testSurfaceCancel(void)
|
|
{
|
|
resetPure();
|
|
struct Fixture f;
|
|
initFixture(&f);
|
|
const LG_SwSurfaceOps * sw = spiceSurface_getVideoOps()->swSurface;
|
|
CHECK(sw->attach(&f.transport, &surfaceEvents, &f.log));
|
|
atomic_store_explicit(
|
|
&f.transport.sessionValid, true, memory_order_release);
|
|
atomic_store_explicit(
|
|
&ps.blockChannelConnect, true, memory_order_release);
|
|
|
|
struct ActiveCall call =
|
|
{
|
|
.ops = sw,
|
|
.transport = &f.transport,
|
|
};
|
|
atomic_init(&call.done, false);
|
|
pthread_t thread;
|
|
CHECK(pthread_create(&thread, NULL, activateSurface, &call) == 0);
|
|
waitBool(&ps.enterChannelConnect);
|
|
|
|
sw->cancelPending(&f.transport);
|
|
waitBool(&call.done);
|
|
CHECK(pthread_join(thread, NULL) == 0);
|
|
CHECK(!call.result);
|
|
CHECK(atomic_load_explicit(
|
|
&ps.channelCancel[PS_CHANNEL_DISPLAY], memory_order_acquire));
|
|
CHECK(!atomic_load_explicit(&ps.cancel, memory_order_acquire));
|
|
CHECK(ps.connectChannelN[PS_CHANNEL_DISPLAY] == 1);
|
|
CHECK(ps.connectChannelN[PS_CHANNEL_CURSOR] == 0);
|
|
CHECK(LGT_SPICE.sessionValid(&f.transport));
|
|
|
|
atomic_store_explicit(
|
|
&ps.blockChannelConnect, false, memory_order_release);
|
|
CHECK(sw->setActive(&f.transport, true));
|
|
CHECK(LGT_SPICE.sessionValid(&f.transport));
|
|
CHECK(sw->setActive(&f.transport, false));
|
|
|
|
sw->detach(&f.transport);
|
|
freeFixture(&f);
|
|
|
|
resetPure();
|
|
initFixture(&f);
|
|
sw = spiceSurface_getVideoOps()->swSurface;
|
|
CHECK(sw->attach(&f.transport, &surfaceEvents, &f.log));
|
|
atomic_store_explicit(
|
|
&f.transport.sessionValid, true, memory_order_release);
|
|
atomic_store_explicit(
|
|
&ps.blockChannelConnect, true, memory_order_release);
|
|
|
|
pthread_t holdThread;
|
|
CHECK(pthread_create(
|
|
&holdThread, NULL, holdActivationLock, &f.transport) == 0);
|
|
waitBool(&ps.enterChannelConnect);
|
|
|
|
call = (struct ActiveCall)
|
|
{
|
|
.ops = sw,
|
|
.transport = &f.transport,
|
|
};
|
|
atomic_init(&call.done, false);
|
|
CHECK(pthread_create(&thread, NULL, activateSurface, &call) == 0);
|
|
usleep(10000);
|
|
sw->cancelPending(&f.transport);
|
|
atomic_store_explicit(
|
|
&ps.blockChannelConnect, false, memory_order_release);
|
|
CHECK(pthread_join(holdThread, NULL) == 0);
|
|
waitBool(&call.done);
|
|
CHECK(pthread_join(thread, NULL) == 0);
|
|
CHECK(!call.result);
|
|
CHECK(ps.connectChannelN[PS_CHANNEL_DISPLAY] == 0);
|
|
|
|
sw->detach(&f.transport);
|
|
freeFixture(&f);
|
|
|
|
resetPure();
|
|
initFixture(&f);
|
|
sw = spiceSurface_getVideoOps()->swSurface;
|
|
CHECK(sw->attach(&f.transport, &surfaceEvents, &f.log));
|
|
atomic_store_explicit(
|
|
&f.transport.sessionValid, true, memory_order_release);
|
|
atomic_store_explicit(
|
|
&ps.blockChannelConnect, true, memory_order_release);
|
|
|
|
call = (struct ActiveCall)
|
|
{
|
|
.ops = sw,
|
|
.transport = &f.transport,
|
|
};
|
|
atomic_init(&call.done, false);
|
|
CHECK(pthread_create(&thread, NULL, activateSurface, &call) == 0);
|
|
waitBool(&ps.enterChannelConnect);
|
|
|
|
atomic_store_explicit(
|
|
&f.transport.sessionValid, false, memory_order_release);
|
|
spiceSurface_sessionStopped(f.transport.surface);
|
|
waitBool(&call.done);
|
|
CHECK(pthread_join(thread, NULL) == 0);
|
|
CHECK(!call.result);
|
|
CHECK(atomic_load_explicit(
|
|
&ps.channelCancel[PS_CHANNEL_DISPLAY], memory_order_acquire));
|
|
CHECK(atomic_load_explicit(
|
|
&ps.channelCancel[PS_CHANNEL_CURSOR], memory_order_acquire));
|
|
CHECK(!atomic_load_explicit(&ps.cancel, memory_order_acquire));
|
|
CHECK(sw->setActive(&f.transport, false));
|
|
|
|
atomic_store_explicit(
|
|
&ps.blockChannelConnect, false, memory_order_release);
|
|
sw->detach(&f.transport);
|
|
freeFixture(&f);
|
|
}
|
|
|
|
static void testSurfaceEvents(void)
|
|
{
|
|
resetPure();
|
|
struct Fixture f;
|
|
initFixture(&f);
|
|
const LG_SwSurfaceOps * sw =
|
|
spiceSurface_getVideoOps()->swSurface;
|
|
CHECK(sw->attach(&f.transport, &surfaceEvents, &f.log));
|
|
|
|
spiceSurface_create(f.transport.surface,
|
|
1, PS_SURFACE_FMT_32_xRGB, 20, 10);
|
|
CHECK(f.log.configN == 0);
|
|
spiceSurface_create(f.transport.surface,
|
|
0, PS_SURFACE_FMT_32_xRGB, 2, 1);
|
|
CHECK(f.log.configN == 1);
|
|
CHECK(f.log.width == 2);
|
|
CHECK(f.log.height == 1);
|
|
CHECK(f.log.fillN == 0);
|
|
|
|
spiceSurface_drawFill(
|
|
f.transport.surface, 0, 1, 2, 3, 4, 0xaabbccdd);
|
|
CHECK(f.log.fillN == 1);
|
|
CHECK(f.log.x == 1);
|
|
CHECK(f.log.y == 2);
|
|
CHECK(f.log.w == 3);
|
|
CHECK(f.log.h == 4);
|
|
CHECK(f.log.color == 0xaabbccdd);
|
|
|
|
uint8_t bitmap[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
|
|
spiceSurface_drawBitmap(f.transport.surface, 0,
|
|
PS_BITMAP_FMT_24BIT, true, 0, 0, 2, 1, 8, bitmap);
|
|
CHECK(f.log.bitmapN == 0);
|
|
spiceSurface_drawBitmap(f.transport.surface, 0,
|
|
PS_BITMAP_FMT_RGBA, true, 4, 5, 2, 1, 8, bitmap);
|
|
CHECK(f.log.bitmapN == 1);
|
|
CHECK(f.log.topDown);
|
|
CHECK(f.log.x == 4);
|
|
CHECK(f.log.y == 5);
|
|
CHECK(f.log.bitmapSize == sizeof(bitmap));
|
|
CHECK(memcmp(f.log.bitmap, bitmap, sizeof(bitmap)) == 0);
|
|
|
|
const uint8_t rgba[] =
|
|
{
|
|
1, 2, 3, 4,
|
|
5, 6, 7, 8,
|
|
};
|
|
const uint8_t bgra[] =
|
|
{
|
|
3, 2, 1, 4,
|
|
7, 6, 5, 8,
|
|
};
|
|
spiceSurface_setRGBAImage(
|
|
f.transport.surface, 2, 1, 6, 7, rgba);
|
|
CHECK(f.log.pointerN == 1);
|
|
CHECK(f.log.pointer.flags == LG_TRANSPORT_POINTER_SHAPE);
|
|
CHECK(f.log.pointer.type == CURSOR_TYPE_COLOR);
|
|
CHECK(f.log.pointer.hx == 6);
|
|
CHECK(f.log.pointer.hy == 7);
|
|
CHECK(f.log.shapeSize == sizeof(bgra));
|
|
CHECK(memcmp(f.log.shape, bgra, sizeof(bgra)) == 0);
|
|
|
|
spiceSurface_setPointerState(f.transport.surface, true, 8, 9);
|
|
CHECK(f.log.pointerN == 2);
|
|
CHECK(f.log.pointer.flags & LG_TRANSPORT_POINTER_VISIBLE);
|
|
CHECK(f.log.pointer.x == 8);
|
|
CHECK(f.log.pointer.y == 9);
|
|
CHECK(f.log.pointer.hx == 6);
|
|
CHECK(f.log.pointer.hy == 7);
|
|
|
|
spiceSurface_destroy(f.transport.surface, 0);
|
|
CHECK(f.log.destroyN == 1);
|
|
spiceSurface_drawFill(f.transport.surface, 0, 0, 0, 1, 1, 0);
|
|
CHECK(f.log.fillN == 1);
|
|
sw->detach(&f.transport);
|
|
freeFixture(&f);
|
|
}
|
|
|
|
struct CreateCall
|
|
{
|
|
SpiceSurface * surface;
|
|
};
|
|
|
|
static void * createSurface(void * opaque)
|
|
{
|
|
struct CreateCall * call = opaque;
|
|
spiceSurface_create(
|
|
call->surface, 0, PS_SURFACE_FMT_32_xRGB, 640, 480);
|
|
return NULL;
|
|
}
|
|
|
|
struct DetachCall
|
|
{
|
|
LG_Transport * transport;
|
|
const LG_SwSurfaceOps * ops;
|
|
atomic_bool started;
|
|
atomic_bool done;
|
|
};
|
|
|
|
static void * detachSurface(void * opaque)
|
|
{
|
|
struct DetachCall * call = opaque;
|
|
atomic_store(&call->started, true);
|
|
call->ops->detach(call->transport);
|
|
atomic_store(&call->done, true);
|
|
return NULL;
|
|
}
|
|
|
|
static void testSurfaceDetach(void)
|
|
{
|
|
resetPure();
|
|
struct Fixture f;
|
|
initFixture(&f);
|
|
const LG_SwSurfaceOps * sw =
|
|
spiceSurface_getVideoOps()->swSurface;
|
|
CHECK(sw->attach(&f.transport, &surfaceEvents, &f.log));
|
|
atomic_store(&f.log.block, true);
|
|
|
|
struct CreateCall create = { .surface = f.transport.surface };
|
|
pthread_t createThread;
|
|
CHECK(pthread_create(&createThread, NULL, createSurface, &create) == 0);
|
|
waitBool(&f.log.entered);
|
|
|
|
struct DetachCall detach =
|
|
{
|
|
.transport = &f.transport,
|
|
.ops = sw,
|
|
};
|
|
atomic_init(&detach.started, false);
|
|
atomic_init(&detach.done, false);
|
|
pthread_t detachThread;
|
|
CHECK(pthread_create(&detachThread, NULL, detachSurface, &detach) == 0);
|
|
waitBool(&detach.started);
|
|
usleep(10000);
|
|
CHECK(!atomic_load(&detach.done));
|
|
|
|
atomic_store(&f.log.release, true);
|
|
CHECK(pthread_join(createThread, NULL) == 0);
|
|
CHECK(pthread_join(detachThread, NULL) == 0);
|
|
CHECK(atomic_load(&detach.done));
|
|
CHECK(f.log.configN == 1);
|
|
CHECK(f.log.fillN == 0);
|
|
spiceSurface_drawFill(f.transport.surface, 0, 0, 0, 1, 1, 0);
|
|
CHECK(f.log.fillN == 0);
|
|
freeFixture(&f);
|
|
}
|
|
|
|
struct Test
|
|
{
|
|
const char * name;
|
|
void (*run)(void);
|
|
};
|
|
|
|
static const struct Test tests[] =
|
|
{
|
|
{ "connect-fail" , testConnectFail },
|
|
{ "pre-ready-drop", testPreReadyDrop },
|
|
{ "cancel" , testCancel },
|
|
{ "ready-drop" , testReadyDrop },
|
|
{ "session-order" , testSessionOrder },
|
|
{ "surface-active", testSurfaceActive },
|
|
{ "surface-cancel", testSurfaceCancel },
|
|
{ "surface-events", testSurfaceEvents },
|
|
{ "surface-detach", testSurfaceDetach },
|
|
};
|
|
|
|
int main(int argc, char ** argv)
|
|
{
|
|
debug_init();
|
|
|
|
if (argc == 2)
|
|
{
|
|
for (size_t i = 0; i < ARRAY_LENGTH(tests); ++i)
|
|
if (strcmp(argv[1], tests[i].name) == 0)
|
|
{
|
|
tests[i].run();
|
|
return 0;
|
|
}
|
|
|
|
fprintf(stderr, "unknown test: %s\n", argv[1]);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
if (argc != 1)
|
|
return EXIT_FAILURE;
|
|
|
|
for (size_t i = 0; i < ARRAY_LENGTH(tests); ++i)
|
|
tests[i].run();
|
|
return 0;
|
|
}
|