mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-22 15:11: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.
415 lines
11 KiB
C
415 lines
11 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 "common/debug.h"
|
|
#include "common/time.h"
|
|
|
|
#include <purespice.h>
|
|
|
|
#include <inttypes.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#define USB_REDIR_DISCONNECT_TIMEOUT_US UINT64_C(500000)
|
|
|
|
static _Atomic(LG_Transport *) l_callbackTarget;
|
|
|
|
static LG_Transport * callbackTarget(void)
|
|
{
|
|
return atomic_load_explicit(&l_callbackTarget, memory_order_acquire);
|
|
}
|
|
|
|
static void ready(void)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (!transport)
|
|
return;
|
|
|
|
snprintf(transport->session.version,
|
|
sizeof(transport->session.version), "SPICE");
|
|
transport->session.os = LG_TRANSPORT_OS_OTHER;
|
|
|
|
purespice_mouseMode(true);
|
|
|
|
PSServerInfo info;
|
|
if (purespice_getServerInfo(&info))
|
|
{
|
|
if (info.name)
|
|
snprintf(transport->session.name,
|
|
sizeof(transport->session.name), "%s", info.name);
|
|
|
|
for (unsigned int i = 0; i < sizeof(info.uuid); ++i)
|
|
if (info.uuid[i])
|
|
{
|
|
transport->session.uuidValid = true;
|
|
memcpy(transport->session.uuid, info.uuid, sizeof(info.uuid));
|
|
break;
|
|
}
|
|
|
|
purespice_freeServerInfo(&info);
|
|
}
|
|
else
|
|
DEBUG_WARN("Failed to obtain SPICE server information");
|
|
|
|
if (transport->input)
|
|
spiceInput_setAvailable(transport->input, true);
|
|
#if ENABLE_AUDIO
|
|
if (transport->audio && !transport->usbAudioEnabled)
|
|
spiceAudio_setAvailable(transport->audio, true);
|
|
#endif
|
|
|
|
transport->connectStatus = LG_TRANSPORT_OK;
|
|
atomic_store_explicit(
|
|
&transport->sessionValid, true, memory_order_release);
|
|
lgSignalEvent(transport->connectEvent);
|
|
}
|
|
|
|
static void keyModifiers(uint32_t modifiers)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport && transport->input)
|
|
spiceInput_setKeyboardLEDs(transport->input, modifiers);
|
|
}
|
|
|
|
static void surfaceCreate(unsigned int surfaceId, PSSurfaceFormat format,
|
|
unsigned int width, unsigned int height)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport)
|
|
spiceSurface_create(
|
|
transport->surface, surfaceId, format, width, height);
|
|
}
|
|
|
|
static void surfaceDestroy(unsigned int surfaceId)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport)
|
|
spiceSurface_destroy(transport->surface, surfaceId);
|
|
}
|
|
|
|
static void drawFill(unsigned int surfaceId, int x, int y,
|
|
int width, int height, uint32_t color)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport)
|
|
spiceSurface_drawFill(
|
|
transport->surface, surfaceId, x, y, width, height, color);
|
|
}
|
|
|
|
static void drawBitmap(unsigned int surfaceId, PSBitmapFormat format,
|
|
bool topDown, int x, int y, int width, int height, int stride, void * data)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport)
|
|
spiceSurface_drawBitmap(transport->surface, surfaceId, format,
|
|
topDown, x, y, width, height, stride, data);
|
|
}
|
|
|
|
static void setRGBAImage(int width, int height, int hx, int hy,
|
|
const void * data)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport)
|
|
spiceSurface_setRGBAImage(
|
|
transport->surface, width, height, hx, hy, data);
|
|
}
|
|
|
|
static void setMonoImage(int width, int height, int hx, int hy,
|
|
const void * xorMask, const void * andMask)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport)
|
|
spiceSurface_setMonoImage(transport->surface,
|
|
width, height, hx, hy, xorMask, andMask);
|
|
}
|
|
|
|
static void setColorImage(int width, int height, int hx, int hy,
|
|
const void * data, const void * maskData)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport)
|
|
spiceSurface_setColorImage(transport->surface,
|
|
width, height, hx, hy, data, maskData);
|
|
}
|
|
|
|
static void setPointerState(bool visible, int x, int y)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport)
|
|
spiceSurface_setPointerState(transport->surface, visible, x, y);
|
|
}
|
|
|
|
#if ENABLE_AUDIO
|
|
static void playbackStart(int channels, int sampleRate,
|
|
PSAudioFormat format, uint32_t time)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport && transport->audio)
|
|
spiceAudio_playbackStart(
|
|
transport->audio, channels, sampleRate, format, time);
|
|
}
|
|
|
|
static void playbackVolume(int channels, const uint16_t volume[])
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport && transport->audio)
|
|
spiceAudio_playbackVolume(transport->audio, channels, volume);
|
|
}
|
|
|
|
static void playbackMute(bool mute)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport && transport->audio)
|
|
spiceAudio_playbackMute(transport->audio, mute);
|
|
}
|
|
|
|
static void playbackStop(void)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport && transport->audio)
|
|
spiceAudio_playbackStop(transport->audio);
|
|
}
|
|
|
|
static void playbackData(uint8_t * data, size_t size, uint32_t time)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport && transport->audio)
|
|
spiceAudio_playbackData(transport->audio, data, size, time);
|
|
}
|
|
|
|
static void recordStart(int channels, int sampleRate, PSAudioFormat format)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport && transport->audio)
|
|
spiceAudio_recordStart(
|
|
transport->audio, channels, sampleRate, format);
|
|
}
|
|
|
|
static void recordVolume(int channels, const uint16_t volume[])
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport && transport->audio)
|
|
spiceAudio_recordVolume(transport->audio, channels, volume);
|
|
}
|
|
|
|
static void recordMute(bool mute)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport && transport->audio)
|
|
spiceAudio_recordMute(transport->audio, mute);
|
|
}
|
|
|
|
static void recordStop(void)
|
|
{
|
|
LG_Transport * transport = callbackTarget();
|
|
if (transport && transport->audio)
|
|
spiceAudio_recordStop(transport->audio);
|
|
}
|
|
#endif
|
|
|
|
static void disconnectProviders(LG_Transport * transport)
|
|
{
|
|
atomic_store_explicit(
|
|
&transport->sessionValid, false, memory_order_release);
|
|
|
|
if (transport->input)
|
|
spiceInput_setAvailable(transport->input, false);
|
|
#if ENABLE_AUDIO
|
|
if (transport->audio)
|
|
spiceAudio_setAvailable(transport->audio, false);
|
|
#endif
|
|
if (transport->clipboard)
|
|
spiceClipboard_setAvailable(transport->clipboard, false);
|
|
spiceSurface_sessionStopped(transport->surface);
|
|
}
|
|
|
|
int spiceSession_thread(void * opaque)
|
|
{
|
|
LG_Transport * transport = opaque;
|
|
LG_Transport * expected = NULL;
|
|
if (!atomic_compare_exchange_strong_explicit(&l_callbackTarget,
|
|
&expected, transport, memory_order_acq_rel, memory_order_acquire))
|
|
{
|
|
DEBUG_ERROR("A SPICE session is already active");
|
|
lgSignalEvent(transport->connectEvent);
|
|
return 0;
|
|
}
|
|
|
|
if (transport->clipboard)
|
|
spiceClipboard_setCallbackTarget(transport->clipboard);
|
|
|
|
const PSConfig config =
|
|
{
|
|
.host = transport->host,
|
|
.port = transport->port,
|
|
.password = "",
|
|
.ready = ready,
|
|
.inputs =
|
|
{
|
|
.enable = transport->inputEnabled,
|
|
.autoConnect = true,
|
|
.keyModifiers = keyModifiers,
|
|
},
|
|
.clipboard =
|
|
{
|
|
.enable = transport->clipboardEnabled,
|
|
.notice = spiceClipboard_notice,
|
|
.data = spiceClipboard_data,
|
|
.release = spiceClipboard_release,
|
|
.request = spiceClipboard_request,
|
|
.status = spiceClipboard_status,
|
|
},
|
|
.display =
|
|
{
|
|
.enable = true,
|
|
.autoConnect = false,
|
|
.surfaceCreate = surfaceCreate,
|
|
.surfaceDestroy = surfaceDestroy,
|
|
.drawFill = drawFill,
|
|
.drawBitmap = drawBitmap,
|
|
},
|
|
.cursor =
|
|
{
|
|
.enable = true,
|
|
.autoConnect = false,
|
|
.setRGBAImage = setRGBAImage,
|
|
.setMonoImage = setMonoImage,
|
|
.setColorImage = setColorImage,
|
|
.setState = setPointerState,
|
|
},
|
|
#if ENABLE_AUDIO
|
|
.playback =
|
|
{
|
|
.enable = transport->playbackEnabled &&
|
|
!transport->usbAudioEnabled,
|
|
.autoConnect = true,
|
|
.start = playbackStart,
|
|
.volume = playbackVolume,
|
|
.mute = playbackMute,
|
|
.stop = playbackStop,
|
|
.data = playbackData,
|
|
},
|
|
.record =
|
|
{
|
|
.enable = transport->recordEnabled &&
|
|
!transport->usbAudioEnabled,
|
|
.autoConnect = true,
|
|
.start = recordStart,
|
|
.volume = recordVolume,
|
|
.mute = recordMute,
|
|
.stop = recordStop,
|
|
},
|
|
#endif
|
|
#if ENABLE_USB_AUDIO
|
|
.usbRedir =
|
|
{
|
|
.enable = transport->usbAudio != NULL,
|
|
.autoConnect = false,
|
|
.opaque = transport->usbRedir,
|
|
.state = lgUsbRedir_state,
|
|
.data = lgUsbRedir_data,
|
|
},
|
|
#endif
|
|
};
|
|
|
|
PSStatus status = PS_STATUS_SHUTDOWN;
|
|
if (!purespice_connect(&config))
|
|
goto done;
|
|
|
|
transport->connected = true;
|
|
status = PS_STATUS_RUN;
|
|
int processTimeout = 100;
|
|
|
|
while (!atomic_load_explicit(&transport->stop, memory_order_acquire))
|
|
{
|
|
#if ENABLE_USB_AUDIO
|
|
if (transport->usbRedir && !lgUsbRedir_process(transport->usbRedir))
|
|
DEBUG_WARN("Failed to process USB audio redirection");
|
|
if (transport->usbAudio)
|
|
{
|
|
const uint64_t delay = lgaUsb_processDelayNs(transport->usbAudio);
|
|
if (delay == UINT64_MAX)
|
|
processTimeout = 10;
|
|
else
|
|
{
|
|
const uint64_t timeout = delay / UINT64_C(1000000) +
|
|
(delay % UINT64_C(1000000) != 0);
|
|
processTimeout = (int)min(timeout, UINT64_C(10));
|
|
}
|
|
}
|
|
#endif
|
|
|
|
status = purespice_process(processTimeout);
|
|
if (status != PS_STATUS_RUN)
|
|
{
|
|
if (status != PS_STATUS_SHUTDOWN)
|
|
DEBUG_ERROR("Failed to process SPICE messages");
|
|
break;
|
|
}
|
|
}
|
|
|
|
disconnectProviders(transport);
|
|
|
|
#if ENABLE_USB_AUDIO
|
|
if (status == PS_STATUS_RUN && transport->usbRedir)
|
|
{
|
|
if (!lgUsbRedir_process(transport->usbRedir))
|
|
DEBUG_WARN("Failed to disconnect USB audio device");
|
|
else
|
|
{
|
|
const uint64_t deadline =
|
|
microtime() + USB_REDIR_DISCONNECT_TIMEOUT_US;
|
|
while (lgUsbRedir_disconnectPending(transport->usbRedir) &&
|
|
microtime() < deadline)
|
|
{
|
|
if (!lgUsbRedir_process(transport->usbRedir))
|
|
{
|
|
DEBUG_WARN("Failed to process USB audio device removal");
|
|
break;
|
|
}
|
|
status = purespice_process(10);
|
|
if (status != PS_STATUS_RUN)
|
|
break;
|
|
}
|
|
|
|
if (status == PS_STATUS_RUN &&
|
|
lgUsbRedir_disconnectPending(transport->usbRedir))
|
|
DEBUG_WARN("Timed out disconnecting USB audio device");
|
|
}
|
|
}
|
|
#endif
|
|
|
|
purespice_disconnect();
|
|
transport->connected = false;
|
|
|
|
done:
|
|
disconnectProviders(transport);
|
|
if (transport->clipboard)
|
|
spiceClipboard_setCallbackTarget(NULL);
|
|
|
|
expected = transport;
|
|
atomic_compare_exchange_strong_explicit(&l_callbackTarget,
|
|
&expected, NULL, memory_order_acq_rel, memory_order_acquire);
|
|
lgSignalEvent(transport->connectEvent);
|
|
return 0;
|
|
}
|