From 053d4e9d9baa6e9b57daf46a68e0c0350f7906dc Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Tue, 11 Aug 2026 16:16:19 +1000 Subject: [PATCH] [client] transport: manage persistent fallback sessions --- client/CMakeLists.txt | 1 + client/include/interface/transport.h | 7 + client/src/app.c | 10 +- client/src/main.c | 208 ++++------ client/src/main.h | 7 +- client/src/transport_fallback.c | 582 +++++++++++++++++++++++++++ client/src/transport_fallback.h | 57 +++ client/transports/SPICE/spice.c | 57 ++- 8 files changed, 764 insertions(+), 165 deletions(-) create mode 100644 client/src/transport_fallback.c create mode 100644 client/src/transport_fallback.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 0f21b2d0..71b9f7ce 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -187,6 +187,7 @@ set(SOURCES src/render_queue.c src/evdev.c src/transport.c + src/transport_fallback.c src/input.c src/overlay/splash.c diff --git a/client/include/interface/transport.h b/client/include/interface/transport.h index 39be8c45..8b41cb0d 100644 --- a/client/include/interface/transport.h +++ b/client/include/interface/transport.h @@ -36,6 +36,8 @@ typedef struct LG_Transport LG_Transport; typedef struct LG_RendererInterop LG_RendererInterop; +typedef bool (*LG_TransportCancelledFn)(void * opaque); + typedef enum LG_TransportStatus { LG_TRANSPORT_OK, @@ -320,6 +322,11 @@ typedef struct LG_TransportOps LG_TransportStatus (*connect)(LG_Transport * transport, LG_TransportSession * session); + /* Optional cancellable form used by transports which may wait while + * establishing a session. */ + LG_TransportStatus (*connectCancellable)(LG_Transport * transport, + LG_TransportSession * session, LG_TransportCancelledFn cancelled, + void * opaque); void (*disconnect)(LG_Transport * transport); bool (*sessionValid)(LG_Transport * transport); /* Queried after create. The returned operations remain valid until the diff --git a/client/src/app.c b/client/src/app.c index 3f89c02e..8719c1b5 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -1151,16 +1151,14 @@ void app_stopVideo(bool stop) bool app_useSpiceDisplay(bool enable) { - if (!g_params.useSpice || !g_state.fallbackVideoOps || - g_state.fallbackVideoOps->type != LG_VIDEO_TYPE_SW_SURFACE || - !g_state.fallbackVideoOps->swSurface) + if (!g_params.useSpice || !g_state.fallback) return false; atomic_store_explicit(&g_state.fallbackDisplayRequested, enable, memory_order_release); // if the fallback is not yet ready, retain the requested state - if (!atomic_load_explicit(&g_state.fallbackReady, memory_order_acquire)) + if (!lgTransportFallback_ready(g_state.fallback)) return false; bool active = atomic_load_explicit(&g_state.fallbackDisplayActive, @@ -1189,8 +1187,7 @@ bool app_useSpiceDisplay(bool enable) if (active == enable) goto done; - if (!g_state.fallbackVideoOps->swSurface->setActive( - g_state.fallbackTransport.handle, enable)) + if (!lgTransportFallback_setVideoActive(g_state.fallback, enable)) goto fail; renderQueue_swSurfaceShow(enable); @@ -1215,6 +1212,7 @@ done: fail: DEBUG_ERROR("Failed to %s the SPICE display", enable ? "enable" : "disable"); + lgTransportFallback_setVideoActive(g_state.fallback, active); atomic_store_explicit(&g_state.fallbackDisplayRequested, active, memory_order_release); goto done; diff --git a/client/src/main.c b/client/src/main.c index d6edf10d..8e0dd972 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -1508,8 +1508,6 @@ static void fallbackSurfaceDestroy(void * opaque) { (void)opaque; g_state.fallbackSurfaceValid = false; - atomic_store_explicit(&g_state.fallbackDisplayRequested, false, - memory_order_release); if (atomic_exchange_explicit(&g_state.fallbackDisplayActive, false, memory_order_acq_rel)) { @@ -1542,7 +1540,7 @@ static void fallbackSurfacePointer(void * opaque, if (pointer->flags & LG_TRANSPORT_POINTER_SHAPE) { LG_RendererCursor type; - switch(pointer->type) + switch (pointer->type) { case CURSOR_TYPE_COLOR: type = LG_CURSOR_COLOR; @@ -1585,137 +1583,73 @@ static const LG_SwSurfaceEventOps fallbackSurfaceEvents = .pointer = fallbackSurfacePointer, }; -static void fallbackDisconnect(void) +static void fallbackConnected(void * opaque, + const LG_TransportSession * session) { - if (!g_state.fallbackTransport.ops) - return; + (void)opaque; + if (session->name[0] && !atomic_load_explicit( + &g_state.lgHostConnected, memory_order_acquire)) + core_setTitle(session->name); - const bool connected = g_state.fallbackTransport.handle && - g_state.fallbackTransport.ops->sessionValid( - g_state.fallbackTransport.handle); - - atomic_store_explicit( - &g_state.fallbackReady, false, memory_order_release); - - if (connected) - { - lgInput_setFallback(NULL, NULL); - lgAudio_setFallback(NULL, NULL); - lgClipboard_setFallback(NULL, NULL); - } - else - { - lgInput_dropFallback(); - lgAudio_dropFallback(); - lgClipboard_dropFallback(); - } - - if (g_state.fallbackVideoOps && - g_state.fallbackVideoOps->type == LG_VIDEO_TYPE_SW_SURFACE && - g_state.fallbackVideoOps->swSurface) - { - if (connected) - g_state.fallbackVideoOps->swSurface->setActive( - g_state.fallbackTransport.handle, false); - g_state.fallbackVideoOps->swSurface->detach( - g_state.fallbackTransport.handle); - } - - if (atomic_exchange_explicit(&g_state.fallbackDisplayActive, false, - memory_order_acq_rel)) - { - renderQueue_swSurfaceShow(false); - lgInput_useTransport(true); - overlayStatus_set(LG_USER_STATUS_SPICE, false); - } - - g_state.fallbackTransport.ops->disconnect( - g_state.fallbackTransport.handle); - lgTransport_destroy(&g_state.fallbackTransport); - g_state.fallbackVideoOps = NULL; - g_state.fallbackSurfaceValid = false; + if (atomic_load_explicit(&g_state.fallbackDisplayRequested, + memory_order_acquire)) + app_useSpiceDisplay(true); } -static bool fallbackConnect(void) +static void fallbackDisconnected(void * opaque) +{ + fallbackSurfaceDestroy(opaque); + if (!atomic_load_explicit( + &g_state.lgHostConnected, memory_order_acquire)) + overlaySplash_show(true); +} + +static void fallbackUUIDMismatch(void * opaque, const uint8_t primary[16], + const uint8_t fallback[16]) +{ + (void)opaque; + (void)primary; + (void)fallback; + atomic_store_explicit( + &g_state.fallbackUUIDMismatch, true, memory_order_release); + app_invalidateWindow(false); +} + +static const LG_TransportFallbackEventOps fallbackEvents = +{ + .connected = fallbackConnected, + .disconnected = fallbackDisconnected, + .uuidMismatch = fallbackUUIDMismatch, +}; + +static bool fallbackStart(void) { if (!g_params.useSpice || strcmp(g_params.transport, "spice") == 0) return true; - if (!lgTransport_create("spice", &g_state.fallbackTransport)) - { - DEBUG_ERROR("Failed to create the SPICE fallback transport"); - return false; - } + if (lgTransportFallback_start("spice", &fallbackSurfaceEvents, NULL, + &fallbackEvents, NULL, &g_state.fallback)) + return true; - g_state.fallbackVideoOps = - g_state.fallbackTransport.ops->getVideoOps( - g_state.fallbackTransport.handle); - if (!g_state.fallbackVideoOps || - g_state.fallbackVideoOps->type != LG_VIDEO_TYPE_SW_SURFACE || - !g_state.fallbackVideoOps->swSurface || - !g_state.fallbackVideoOps->swSurface->attach( - g_state.fallbackTransport.handle, &fallbackSurfaceEvents, NULL)) - { - DEBUG_ERROR("SPICE does not provide a software surface"); - fallbackDisconnect(); - return false; - } - - const LG_TransportStatus status = - g_state.fallbackTransport.ops->connect( - g_state.fallbackTransport.handle, &g_state.fallbackSession); - if (status != LG_TRANSPORT_OK) - { - DEBUG_ERROR("Failed to connect the SPICE fallback transport: %d", status); - fallbackDisconnect(); - return false; - } - - void * inputOpaque = NULL; - const LG_InputOps * inputOps = - g_state.fallbackTransport.ops->getInputOps ? - g_state.fallbackTransport.ops->getInputOps( - g_state.fallbackTransport.handle, &inputOpaque) : NULL; - lgInput_setFallback(inputOps, inputOpaque); - - void * audioOpaque = NULL; - const LG_AudioOps * audioOps = - g_state.fallbackTransport.ops->getAudioOps ? - g_state.fallbackTransport.ops->getAudioOps( - g_state.fallbackTransport.handle, &audioOpaque) : NULL; - lgAudio_setFallback(audioOps, audioOpaque); - - void * clipboardOpaque = NULL; - const LG_ClipboardOps * clipboardOps = - g_state.fallbackTransport.ops->getClipboardOps ? - g_state.fallbackTransport.ops->getClipboardOps( - g_state.fallbackTransport.handle, &clipboardOpaque) : NULL; - lgClipboard_setFallback(clipboardOps, clipboardOpaque); - - atomic_store_explicit( - &g_state.fallbackReady, true, memory_order_release); - if (g_state.fallbackSession.name[0]) - core_setTitle(g_state.fallbackSession.name); - if (inputOps) - keybind_inputRegister(); - return true; + DEBUG_ERROR("Failed to start the SPICE fallback transport"); + return false; } -static void checkUUID(void) +static void fallbackStop(void) { - if (!atomic_load_explicit(&g_state.fallbackReady, memory_order_acquire) || - !g_state.fallbackSession.uuidValid || !g_state.guestUUIDValid) - return; + lgTransportFallback_stop(&g_state.fallback); +} - if (memcmp(g_state.fallbackSession.uuid, g_state.guestUUID, - sizeof(g_state.guestUUID)) == 0) +static void fallbackHandleEvents(void) +{ + if (!atomic_exchange_explicit( + &g_state.fallbackUUIDMismatch, false, memory_order_acq_rel)) return; app_msgBox( "SPICE Configuration Error", "You have connected SPICE to the wrong guest.\n" "SPICE fallback services will not function until this is corrected."); - fallbackDisconnect(); } void intHandler(int sig) @@ -1965,7 +1899,7 @@ static int lg_run(void) g_state.micDefaultState = g_params.micDefaultState; - if (!fallbackConnect()) + if (!fallbackStart()) return -1; // select and init a renderer @@ -2072,6 +2006,7 @@ static int lg_run(void) } keybind_commonRegister(); + keybind_inputRegister(); if (g_state.jitRender) DEBUG_INFO("Using JIT render mode"); @@ -2127,15 +2062,7 @@ restart: while(app_getState() == APP_STATE_RUNNING) { - if (atomic_load_explicit( - &g_state.fallbackReady, memory_order_acquire) && - !g_state.fallbackTransport.ops->sessionValid( - g_state.fallbackTransport.handle)) - { - DEBUG_ERROR("SPICE fallback transport disconnected"); - app_setState(APP_STATE_SHUTDOWN); - break; - } + fallbackHandleEvents(); if (initialSpiceEnable && microtime() > initialSpiceEnable) { @@ -2158,7 +2085,10 @@ restart: while (app_getState() == APP_STATE_RUNNING && !atomic_load_explicit(&probe.done, memory_order_acquire)) + { g_state.ds->wait(100); + fallbackHandleEvents(); + } if (!lgJoinThread(probeThread, NULL)) { @@ -2274,7 +2204,13 @@ restart: g_state.guestOS = session.os; g_state.guestUUIDValid = session.uuidValid; if (session.uuidValid) + { memcpy(g_state.guestUUID, session.uuid, sizeof(g_state.guestUUID)); + lgTransportFallback_setPrimaryUUID( + g_state.fallback, g_state.guestUUID); + } + else + lgTransportFallback_clearPrimaryUUID(g_state.fallback); g_state.transportFeatures = session.features; frameScheduler_start(session.features); @@ -2297,9 +2233,6 @@ restart: g_state.transport.handle, &clipboardOpaque) : NULL; lgClipboard_setTransport(clipboardOps, clipboardOpaque); - if (inputOps || lgInput_available()) - keybind_inputRegister(); - checkUUID(); DEBUG_INFO("Starting session"); atomic_store_explicit( &g_state.lgHostConnected, true, memory_order_release); @@ -2311,15 +2244,7 @@ restart: while(likely(app_getState() == APP_STATE_RUNNING)) { - if (atomic_load_explicit( - &g_state.fallbackReady, memory_order_acquire) && - !g_state.fallbackTransport.ops->sessionValid( - g_state.fallbackTransport.handle)) - { - DEBUG_ERROR("SPICE fallback transport disconnected"); - app_setState(APP_STATE_SHUTDOWN); - break; - } + fallbackHandleEvents(); if (unlikely(!g_state.transport.ops->sessionValid( g_state.transport.handle))) @@ -2342,6 +2267,11 @@ restart: if (app_getState() == APP_STATE_RESTART) { + atomic_store_explicit( + &g_state.lgHostConnected, false, memory_order_release); + g_state.guestUUIDValid = false; + lgTransportFallback_clearPrimaryUUID(g_state.fallback); + lgSignalEvent(e_startup); lgSignalEvent(g_state.frameEvent); @@ -2368,8 +2298,6 @@ static void lg_shutdown(void) if (e_cursorRepaint) lgSignalEvent(e_cursorRepaint); - fallbackDisconnect(); - if (t_render) { if (g_state.jitRender && g_state.ds->stopWaitFrame) @@ -2379,6 +2307,8 @@ static void lg_shutdown(void) lgJoinThread(t_render, NULL); } + fallbackStop(); + if (t_cursorRepaint) { lgJoinThread(t_cursorRepaint, NULL); diff --git a/client/src/main.h b/client/src/main.h index 738f8538..ec658dcb 100644 --- a/client/src/main.h +++ b/client/src/main.h @@ -35,6 +35,7 @@ #include "cimgui.h" #include "interface/transport.h" +#include "transport_fallback.h" enum RunState { @@ -78,10 +79,10 @@ struct AppState bool dsInitialized; bool jitRender; - atomic_bool fallbackReady; atomic_bool fallbackDisplayRequested; atomic_bool fallbackDisplayActive; atomic_bool fallbackDisplayTransition; + atomic_bool fallbackUUIDMismatch; bool fallbackSurfaceValid; uint8_t guestUUID[16]; @@ -123,9 +124,7 @@ struct AppState const LG_VideoOps * videoOps; LG_TransportFeatureFlags transportFeatures; - LG_TransportInstance fallbackTransport; - LG_TransportSession fallbackSession; - const LG_VideoOps * fallbackVideoOps; + LG_TransportFallback * fallback; LGThread * cursorThread; LGThread * frameThread; diff --git a/client/src/transport_fallback.c b/client/src/transport_fallback.c new file mode 100644 index 00000000..58cdaca5 --- /dev/null +++ b/client/src/transport_fallback.c @@ -0,0 +1,582 @@ +/** + * 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 "transport_fallback.h" + +#include "audio.h" +#include "clipboard.h" +#include "input.h" + +#include "common/debug.h" +#include "common/event.h" +#include "common/locking.h" +#include "common/thread.h" +#include "common/util.h" + +#include +#include +#include + +#define RETRY_INITIAL_MS 250U +#define RETRY_MAX_MS 5000U +#define SESSION_POLL_MS 100U + +struct LG_TransportFallback +{ + char * transportName; + + LG_SwSurfaceEventOps surfaceEvents; + void * surfaceOpaque; + LG_TransportFallbackEventOps eventOps; + void * eventOpaque; + + LGThread * thread; + LGEvent * wakeEvent; + LGEvent * videoIdleEvent; + LG_RWLock lock; + + atomic_bool stop; + atomic_bool ready; + + LG_TransportInstance transport; + LG_TransportSession session; + const LG_VideoOps * videoOps; + uint64_t connectionSerial; + bool attached; + bool connected; + bool providersPublished; + bool connectedReported; + bool closing; + bool videoRequested; + bool videoActive; + bool videoBusy; + + bool primaryUUIDValid; + uint8_t primaryUUID[16]; + + bool mismatchReported; + uint8_t mismatchPrimary[16]; + uint8_t mismatchFallback[16]; +}; + +static bool connectCancelled(void * opaque) +{ + const LG_TransportFallback * fallback = opaque; + return atomic_load_explicit(&fallback->stop, memory_order_acquire); +} + +static bool uuidMismatchLocked(const LG_TransportFallback * fallback) +{ + return fallback->primaryUUIDValid && fallback->session.uuidValid && + memcmp(fallback->primaryUUID, fallback->session.uuid, + sizeof(fallback->primaryUUID)) != 0; +} + +static bool recordMismatchLocked(LG_TransportFallback * fallback, + uint8_t primary[16], uint8_t remote[16]) +{ + if (!uuidMismatchLocked(fallback)) + return false; + + const bool duplicate = fallback->mismatchReported && + memcmp(fallback->mismatchPrimary, fallback->primaryUUID, + sizeof(fallback->mismatchPrimary)) == 0 && + memcmp(fallback->mismatchFallback, fallback->session.uuid, + sizeof(fallback->mismatchFallback)) == 0; + if (duplicate) + return false; + + memcpy(fallback->mismatchPrimary, fallback->primaryUUID, + sizeof(fallback->mismatchPrimary)); + memcpy(fallback->mismatchFallback, fallback->session.uuid, + sizeof(fallback->mismatchFallback)); + memcpy(primary, fallback->primaryUUID, sizeof(fallback->primaryUUID)); + memcpy(remote, fallback->session.uuid, sizeof(fallback->session.uuid)); + fallback->mismatchReported = true; + return true; +} + +static bool sessionLive(const LG_TransportFallback * fallback) +{ + return fallback->connected && fallback->transport.ops && + fallback->transport.ops->sessionValid(fallback->transport.handle); +} + +static void unpublishProviders(LG_TransportFallback * fallback, bool live) +{ + if (!fallback->providersPublished) + return; + + if (live) + { + lgInput_setFallback(NULL, NULL); + lgAudio_setFallback(NULL, NULL); + lgClipboard_setFallback(NULL, NULL); + } + else + { + lgInput_dropFallback(); + lgAudio_dropFallback(); + lgClipboard_dropFallback(); + } + fallback->providersPublished = false; +} + +static void waitForVideoIdle(LG_TransportFallback * fallback) +{ + for (;;) + { + LG_LOCK_EXCLUSIVE(fallback->lock); + atomic_store_explicit(&fallback->ready, false, memory_order_release); + fallback->closing = true; + const bool busy = fallback->videoBusy; + LG_UNLOCK_EXCLUSIVE(fallback->lock); + + if (!busy) + return; + lgWaitEvent(fallback->videoIdleEvent, TIMEOUT_INFINITE); + } +} + +static bool cleanupConnection(LG_TransportFallback * fallback, + bool knownDead) +{ + waitForVideoIdle(fallback); + + LG_LOCK_EXCLUSIVE(fallback->lock); + const bool reportDisconnected = fallback->connectedReported; + fallback->connectedReported = false; + LG_UNLOCK_EXCLUSIVE(fallback->lock); + + const bool live = !knownDead && sessionLive(fallback); + unpublishProviders(fallback, live); + + if (fallback->attached) + { + if (live && fallback->videoActive) + fallback->videoOps->swSurface->setActive( + fallback->transport.handle, false); + fallback->videoActive = false; + fallback->videoOps->swSurface->detach(fallback->transport.handle); + fallback->attached = false; + } + + if (fallback->transport.ops) + fallback->transport.ops->disconnect(fallback->transport.handle); + lgTransport_destroy(&fallback->transport); + + LG_LOCK_EXCLUSIVE(fallback->lock); + fallback->session = (LG_TransportSession) { 0 }; + fallback->videoOps = NULL; + fallback->connected = false; + fallback->closing = false; + ++fallback->connectionSerial; + LG_UNLOCK_EXCLUSIVE(fallback->lock); + return reportDisconnected; +} + +static void publishProviders(LG_TransportFallback * fallback) +{ + void * inputOpaque = NULL; + const LG_InputOps * inputOps = fallback->transport.ops->getInputOps ? + fallback->transport.ops->getInputOps( + fallback->transport.handle, &inputOpaque) : NULL; + lgInput_setFallback(inputOps, inputOpaque); + + void * audioOpaque = NULL; + const LG_AudioOps * audioOps = fallback->transport.ops->getAudioOps ? + fallback->transport.ops->getAudioOps( + fallback->transport.handle, &audioOpaque) : NULL; + lgAudio_setFallback(audioOps, audioOpaque); + + void * clipboardOpaque = NULL; + const LG_ClipboardOps * clipboardOps = + fallback->transport.ops->getClipboardOps ? + fallback->transport.ops->getClipboardOps( + fallback->transport.handle, &clipboardOpaque) : NULL; + lgClipboard_setFallback(clipboardOps, clipboardOpaque); + fallback->providersPublished = true; +} + +static bool publishConnection(LG_TransportFallback * fallback, + bool * reportMismatch, uint8_t primaryUUID[16], + uint8_t fallbackUUID[16]) +{ + LG_LOCK_EXCLUSIVE(fallback->lock); + const bool reject = uuidMismatchLocked(fallback); + if (reject) + *reportMismatch = recordMismatchLocked( + fallback, primaryUUID, fallbackUUID); + const bool stop = atomic_load_explicit( + &fallback->stop, memory_order_acquire); + LG_UNLOCK_EXCLUSIVE(fallback->lock); + if (reject || stop) + return false; + + publishProviders(fallback); + + for (;;) + { + LG_LOCK_EXCLUSIVE(fallback->lock); + const bool reject = uuidMismatchLocked(fallback); + if (reject) + *reportMismatch = recordMismatchLocked( + fallback, primaryUUID, fallbackUUID); + const bool stop = atomic_load_explicit( + &fallback->stop, memory_order_acquire); + const bool requested = fallback->videoRequested; + LG_UNLOCK_EXCLUSIVE(fallback->lock); + + if (reject || stop) + return false; + if (requested == fallback->videoActive) + break; + + if (!fallback->videoOps->swSurface->setActive( + fallback->transport.handle, requested)) + break; + fallback->videoActive = requested; + } + + LG_LOCK_EXCLUSIVE(fallback->lock); + const bool finalReject = uuidMismatchLocked(fallback); + if (finalReject) + *reportMismatch = recordMismatchLocked( + fallback, primaryUUID, fallbackUUID); + const bool finalStop = atomic_load_explicit( + &fallback->stop, memory_order_acquire); + if (!finalReject && !finalStop) + { + fallback->mismatchReported = false; + atomic_store_explicit(&fallback->ready, true, memory_order_release); + } + LG_UNLOCK_EXCLUSIVE(fallback->lock); + return !finalReject && !finalStop; +} + +static bool connectFallback(LG_TransportFallback * fallback) +{ + LG_TransportInstance transport = { 0 }; + if (!lgTransport_create(fallback->transportName, &transport)) + { + DEBUG_ERROR("Failed to create fallback transport %s", + fallback->transportName); + return false; + } + + const LG_VideoOps * videoOps = transport.ops->getVideoOps ? + transport.ops->getVideoOps(transport.handle) : NULL; + if (!videoOps || videoOps->type != LG_VIDEO_TYPE_SW_SURFACE || + !videoOps->swSurface || !videoOps->swSurface->attach || + !videoOps->swSurface->detach || !videoOps->swSurface->setActive) + { + DEBUG_ERROR("Fallback transport %s does not provide a software surface", + fallback->transportName); + lgTransport_destroy(&transport); + return false; + } + + if (!videoOps->swSurface->attach(transport.handle, + &fallback->surfaceEvents, fallback->surfaceOpaque)) + { + DEBUG_ERROR("Failed to attach fallback software surface"); + lgTransport_destroy(&transport); + return false; + } + + LG_LOCK_EXCLUSIVE(fallback->lock); + fallback->transport = transport; + fallback->videoOps = videoOps; + fallback->attached = true; + LG_UNLOCK_EXCLUSIVE(fallback->lock); + + LG_TransportSession session = { 0 }; + const LG_TransportStatus status = transport.ops->connectCancellable ? + transport.ops->connectCancellable(transport.handle, &session, + connectCancelled, fallback) : + transport.ops->connect(transport.handle, &session); + + if (status != LG_TRANSPORT_OK) + { + if (!atomic_load_explicit(&fallback->stop, memory_order_acquire)) + DEBUG_ERROR("Fallback transport %s failed to connect: %d", + fallback->transportName, status); + cleanupConnection(fallback, false); + return false; + } + + LG_LOCK_EXCLUSIVE(fallback->lock); + fallback->connected = true; + fallback->session = session; + ++fallback->connectionSerial; + LG_UNLOCK_EXCLUSIVE(fallback->lock); + + bool reportMismatch = false; + uint8_t primaryUUID[16]; + uint8_t fallbackUUID[16]; + if (!publishConnection(fallback, &reportMismatch, + primaryUUID, fallbackUUID)) + { + cleanupConnection(fallback, false); + if (reportMismatch && fallback->eventOps.uuidMismatch) + fallback->eventOps.uuidMismatch( + fallback->eventOpaque, primaryUUID, fallbackUUID); + return false; + } + + LG_LOCK_EXCLUSIVE(fallback->lock); + fallback->connectedReported = true; + const LG_TransportSession reportedSession = fallback->session; + LG_UNLOCK_EXCLUSIVE(fallback->lock); + if (fallback->eventOps.connected) + fallback->eventOps.connected( + fallback->eventOpaque, &reportedSession); + + while (!atomic_load_explicit(&fallback->stop, memory_order_acquire)) + { + lgWaitEvent(fallback->wakeEvent, SESSION_POLL_MS); + + LG_LOCK_EXCLUSIVE(fallback->lock); + const bool reject = uuidMismatchLocked(fallback); + if (reject) + reportMismatch = recordMismatchLocked( + fallback, primaryUUID, fallbackUUID); + LG_UNLOCK_EXCLUSIVE(fallback->lock); + if (reject) + break; + if (!sessionLive(fallback)) + break; + } + + const bool knownDead = !atomic_load_explicit( + &fallback->stop, memory_order_acquire) && !sessionLive(fallback); + const bool reportDisconnect = cleanupConnection(fallback, knownDead); + + if (reportMismatch && fallback->eventOps.uuidMismatch) + fallback->eventOps.uuidMismatch( + fallback->eventOpaque, primaryUUID, fallbackUUID); + if (reportDisconnect && fallback->eventOps.disconnected) + fallback->eventOps.disconnected(fallback->eventOpaque); + return true; +} + +static int fallbackThread(void * opaque) +{ + LG_TransportFallback * fallback = opaque; + unsigned int retry = RETRY_INITIAL_MS; + + while (!atomic_load_explicit(&fallback->stop, memory_order_acquire)) + { + if (connectFallback(fallback)) + retry = RETRY_INITIAL_MS; + + if (atomic_load_explicit(&fallback->stop, memory_order_acquire)) + break; + + lgWaitEvent(fallback->wakeEvent, retry); + retry = min(retry * 2U, RETRY_MAX_MS); + } + + const bool reportDisconnect = cleanupConnection(fallback, false); + if (reportDisconnect && fallback->eventOps.disconnected) + fallback->eventOps.disconnected(fallback->eventOpaque); + return 0; +} + +bool lgTransportFallback_start(const char * transportName, + const LG_SwSurfaceEventOps * surfaceEvents, void * surfaceOpaque, + const LG_TransportFallbackEventOps * eventOps, void * eventOpaque, + LG_TransportFallback ** result) +{ + if (!result) + return false; + + *result = NULL; + if (!transportName || !*transportName || !surfaceEvents || + !lgTransport_isValid(transportName)) + return false; + + LG_TransportFallback * fallback = calloc(1, sizeof(*fallback)); + if (!fallback) + return false; + + LG_RWLOCK_INIT(fallback->lock); + atomic_init(&fallback->stop, false); + atomic_init(&fallback->ready, false); + + fallback->transportName = strdup(transportName); + if (!fallback->transportName) + goto fail; + + fallback->surfaceEvents = *surfaceEvents; + fallback->surfaceOpaque = surfaceOpaque; + if (eventOps) + fallback->eventOps = *eventOps; + fallback->eventOpaque = eventOpaque; + + fallback->wakeEvent = lgCreateEvent(true, 0); + if (!fallback->wakeEvent) + goto fail; + + fallback->videoIdleEvent = lgCreateEvent(true, 0); + if (!fallback->videoIdleEvent) + goto fail; + + *result = fallback; + if (!lgCreateThread("transportFallback", fallbackThread, + fallback, &fallback->thread)) + goto fail; + + return true; + +fail: + *result = NULL; + if (fallback->videoIdleEvent) + lgFreeEvent(fallback->videoIdleEvent); + if (fallback->wakeEvent) + lgFreeEvent(fallback->wakeEvent); + LG_RWLOCK_FREE(fallback->lock); + free(fallback->transportName); + free(fallback); + return false; +} + +void lgTransportFallback_stop(LG_TransportFallback ** fallbackPtr) +{ + if (!fallbackPtr || !*fallbackPtr) + return; + + LG_TransportFallback * fallback = *fallbackPtr; + *fallbackPtr = NULL; + + atomic_store_explicit(&fallback->stop, true, memory_order_release); + lgSignalEvent(fallback->wakeEvent); + + LGThread * thread = fallback->thread; + fallback->thread = NULL; + if (!lgJoinThread(thread, NULL)) + { + DEBUG_ERROR("Failed to stop fallback transport worker; preserving state"); + return; + } + + lgFreeEvent(fallback->videoIdleEvent); + lgFreeEvent(fallback->wakeEvent); + LG_RWLOCK_FREE(fallback->lock); + free(fallback->transportName); + free(fallback); +} + +bool lgTransportFallback_ready(const LG_TransportFallback * fallback) +{ + return fallback && atomic_load_explicit( + &fallback->ready, memory_order_acquire); +} + +bool lgTransportFallback_setVideoActive( + LG_TransportFallback * fallback, bool active) +{ + if (!fallback) + return false; + + uint64_t connectionSerial; + LG_Transport * transport; + const LG_SwSurfaceOps * surfaceOps; + + for (;;) + { + LG_LOCK_EXCLUSIVE(fallback->lock); + fallback->videoRequested = active; + if (!atomic_load_explicit(&fallback->ready, memory_order_acquire) || + fallback->closing) + { + LG_UNLOCK_EXCLUSIVE(fallback->lock); + return false; + } + + if (fallback->videoActive == active) + { + LG_UNLOCK_EXCLUSIVE(fallback->lock); + return true; + } + + if (!fallback->videoBusy) + { + fallback->videoBusy = true; + connectionSerial = fallback->connectionSerial; + transport = fallback->transport.handle; + surfaceOps = fallback->videoOps->swSurface; + LG_UNLOCK_EXCLUSIVE(fallback->lock); + break; + } + + LG_UNLOCK_EXCLUSIVE(fallback->lock); + lgWaitEvent(fallback->videoIdleEvent, TIMEOUT_INFINITE); + } + + lgResetEvent(fallback->videoIdleEvent); + const bool result = surfaceOps->setActive(transport, active); + + LG_LOCK_EXCLUSIVE(fallback->lock); + const bool current = fallback->connectionSerial == connectionSerial; + if (current && result) + fallback->videoActive = active; + const bool accepted = current && !fallback->closing && result; + fallback->videoBusy = false; + LG_UNLOCK_EXCLUSIVE(fallback->lock); + lgSignalEvent(fallback->videoIdleEvent); + return accepted; +} + +void lgTransportFallback_setPrimaryUUID( + LG_TransportFallback * fallback, const uint8_t uuid[16]) +{ + if (!fallback || !uuid) + return; + + LG_LOCK_EXCLUSIVE(fallback->lock); + const bool changed = !fallback->primaryUUIDValid || + memcmp(fallback->primaryUUID, uuid, + sizeof(fallback->primaryUUID)) != 0; + if (changed) + { + memcpy(fallback->primaryUUID, uuid, sizeof(fallback->primaryUUID)); + fallback->primaryUUIDValid = true; + } + LG_UNLOCK_EXCLUSIVE(fallback->lock); + + if (changed) + lgSignalEvent(fallback->wakeEvent); +} + +void lgTransportFallback_clearPrimaryUUID( + LG_TransportFallback * fallback) +{ + if (!fallback) + return; + + LG_LOCK_EXCLUSIVE(fallback->lock); + const bool changed = fallback->primaryUUIDValid; + fallback->primaryUUIDValid = false; + LG_UNLOCK_EXCLUSIVE(fallback->lock); + + if (changed) + lgSignalEvent(fallback->wakeEvent); +} diff --git a/client/src/transport_fallback.h b/client/src/transport_fallback.h new file mode 100644 index 00000000..32d00c51 --- /dev/null +++ b/client/src/transport_fallback.h @@ -0,0 +1,57 @@ +/** + * 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_FALLBACK_ +#define _H_LG_CLIENT_TRANSPORT_FALLBACK_ + +#include "interface/transport.h" + +#include +#include + +typedef struct LG_TransportFallback LG_TransportFallback; + +typedef struct LG_TransportFallbackEventOps +{ + /* The session is valid only for the duration of this callback. */ + void (*connected)(void * opaque, const LG_TransportSession * session); + void (*disconnected)(void * opaque); + void (*uuidMismatch)(void * opaque, const uint8_t primary[16], + const uint8_t fallback[16]); +} +LG_TransportFallbackEventOps; + +bool lgTransportFallback_start(const char * transportName, + const LG_SwSurfaceEventOps * surfaceEvents, void * surfaceOpaque, + const LG_TransportFallbackEventOps * eventOps, void * eventOpaque, + LG_TransportFallback ** result); +void lgTransportFallback_stop(LG_TransportFallback ** fallback); + +bool lgTransportFallback_ready(const LG_TransportFallback * fallback); +/* The requested state is retained across reconnects. */ +bool lgTransportFallback_setVideoActive( + LG_TransportFallback * fallback, bool active); + +void lgTransportFallback_setPrimaryUUID( + LG_TransportFallback * fallback, const uint8_t uuid[16]); +void lgTransportFallback_clearPrimaryUUID( + LG_TransportFallback * fallback); + +#endif diff --git a/client/transports/SPICE/spice.c b/client/transports/SPICE/spice.c index 4c4b7173..3c84e7ad 100644 --- a/client/transports/SPICE/spice.c +++ b/client/transports/SPICE/spice.c @@ -157,8 +157,9 @@ static void spiceDestroy(LG_Transport ** transport) *transport = NULL; } -static LG_TransportStatus spiceConnect(LG_Transport * transport, - LG_TransportSession * session) +static LG_TransportStatus spiceConnectCancellable(LG_Transport * transport, + LG_TransportSession * session, LG_TransportCancelledFn cancelled, + void * opaque) { if (transport->thread) return LG_TRANSPORT_ERROR; @@ -174,7 +175,24 @@ static LG_TransportStatus spiceConnect(LG_Transport * transport, "spiceProcess", spiceSession_thread, transport, &transport->thread)) return LG_TRANSPORT_ERROR; - lgWaitEvent(transport->connectEvent, TIMEOUT_INFINITE); + bool cancel = false; + while (!lgWaitEvent(transport->connectEvent, 100)) + if (cancelled && cancelled(opaque)) + { + cancel = true; + atomic_store_explicit(&transport->stop, true, memory_order_release); + lgWaitEvent(transport->connectEvent, TIMEOUT_INFINITE); + break; + } + + if (cancel || (cancelled && cancelled(opaque))) + { + atomic_store_explicit(&transport->stop, true, memory_order_release); + lgJoinThread(transport->thread, NULL); + transport->thread = NULL; + return LG_TRANSPORT_DISCONNECTED; + } + if (transport->connectStatus != LG_TRANSPORT_OK) { lgJoinThread(transport->thread, NULL); @@ -186,6 +204,12 @@ static LG_TransportStatus spiceConnect(LG_Transport * transport, return LG_TRANSPORT_OK; } +static LG_TransportStatus spiceConnect(LG_Transport * transport, + LG_TransportSession * session) +{ + return spiceConnectCancellable(transport, session, NULL, NULL); +} + static void spiceDisconnect(LG_Transport * transport) { if (!transport->thread) @@ -261,17 +285,18 @@ static LG_TransportStatus spiceControlStatus(LG_Transport * transport, const LG_TransportOps LGT_SPICE = { - .name = "spice", - .setup = spiceSetup, - .create = spiceCreate, - .destroy = spiceDestroy, - .connect = spiceConnect, - .disconnect = spiceDisconnect, - .sessionValid = spiceSessionValid, - .getVideoOps = spiceGetVideoOps, - .getInputOps = spiceGetInputOps, - .getAudioOps = spiceGetAudioOps, - .getClipboardOps = spiceGetClipboardOps, - .sendControl = spiceSendControl, - .controlStatus = spiceControlStatus, + .name = "spice", + .setup = spiceSetup, + .create = spiceCreate, + .destroy = spiceDestroy, + .connect = spiceConnect, + .connectCancellable = spiceConnectCancellable, + .disconnect = spiceDisconnect, + .sessionValid = spiceSessionValid, + .getVideoOps = spiceGetVideoOps, + .getInputOps = spiceGetInputOps, + .getAudioOps = spiceGetAudioOps, + .getClipboardOps = spiceGetClipboardOps, + .sendControl = spiceSendControl, + .controlStatus = spiceControlStatus, };