From 8fb3c4d9f60f5f4c2d843ff3bb1708b72efa37a4 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Wed, 12 Aug 2026 12:13:48 +1000 Subject: [PATCH] [client] transport: stop after guest sessions end Track unexpected loss of an admitted fallback independently from normal stop and UUID revocation. Terminate once both primary and fallback sessions have disappeared, regardless of notification order. Make shutdown terminal so concurrent disconnect handling cannot replace it with restart or running state. --- client/include/app.h | 10 +++++ client/src/app.c | 31 ++++++++++++++ client/src/main.c | 76 ++++++++++++++++++++++++++++----- client/src/main.h | 21 +-------- client/src/transport_fallback.c | 24 +++++++++-- client/src/transport_fallback.h | 2 + client/tests/keybind_test.c | 10 +++++ 7 files changed, 141 insertions(+), 33 deletions(-) diff --git a/client/include/app.h b/client/include/app.h index 9fbfc18d..844421f2 100644 --- a/client/include/app.h +++ b/client/include/app.h @@ -30,6 +30,13 @@ #include "interface/displayserver.h" #include "interface/overlay.h" +enum RunState +{ + APP_STATE_RUNNING, + APP_STATE_RESTART, + APP_STATE_SHUTDOWN +}; + typedef enum LG_MsgAlert { LG_ALERT_INFO , @@ -53,6 +60,9 @@ typedef struct LG_MouseState LG_MouseState; bool app_isRunning(void); +enum RunState app_getState(void); +void app_setState(enum RunState state); +bool app_transitionState(enum RunState from, enum RunState to); bool app_inputEnabled(void); bool app_isCaptureMode(void); bool app_isCaptureOnlyMode(void); diff --git a/client/src/app.c b/client/src/app.c index 9ac55a7d..5c076a26 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -57,6 +57,37 @@ } \ while (0) +extern _Atomic(enum RunState) p_appState; + +enum RunState app_getState(void) +{ + return atomic_load_explicit(&p_appState, memory_order_acquire); +} + +void app_setState(enum RunState state) +{ + if (state == APP_STATE_SHUTDOWN) + { + atomic_store_explicit(&p_appState, state, memory_order_release); + return; + } + + enum RunState current = atomic_load_explicit( + &p_appState, memory_order_acquire); + while (current != APP_STATE_SHUTDOWN && + !atomic_compare_exchange_weak_explicit(&p_appState, ¤t, state, + memory_order_acq_rel, memory_order_acquire)); +} + +bool app_transitionState(enum RunState from, enum RunState to) +{ + if (from == APP_STATE_SHUTDOWN) + return false; + + return atomic_compare_exchange_strong_explicit( + &p_appState, &from, to, memory_order_acq_rel, memory_order_acquire); +} + bool app_isRunning(void) { const enum RunState state = app_getState(); diff --git a/client/src/main.c b/client/src/main.c index e3748b8d..586101ba 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -81,12 +81,18 @@ _Static_assert((int)LG_CAPTURE_RGBA32F == (int)LG_TEST_CAPTURE_RGBA32F, "capture format mismatch"); #endif +#define TRANSPORT_LOST_PRIMARY (1U << 0) +#define TRANSPORT_LOST_FALLBACK (1U << 1) +#define TRANSPORT_LOST_ALL \ + (TRANSPORT_LOST_PRIMARY | TRANSPORT_LOST_FALLBACK) + // forwards static int renderThread(void * unused); static RenderQueueSource renderQueueSource(LG_VideoSource source); static bool videoSourceInvalidate(LG_VideoSource source); static void videoSourceShowSplashIfNeeded(void); static bool fallbackRequestVideo(void); +static void primaryLost(void); static LGEvent *e_startup = NULL; static LGEvent *e_cursorRepaint = NULL; @@ -1148,11 +1154,13 @@ int main_cursorThread(void * unused) lgInput_dropTransport(); lgAudio_dropTransport(); lgClipboard_dropTransport(); + primaryLost(); } - app_setState(status == LG_TRANSPORT_DISCONNECTED ? - APP_STATE_RESTART : APP_STATE_SHUTDOWN); - if (status != LG_TRANSPORT_DISCONNECTED) + else + { DEBUG_ERROR("Pointer transport failed with status %d", status); + app_setState(APP_STATE_SHUTDOWN); + } break; } @@ -1334,7 +1342,7 @@ int main_frameThread(void * unused) lgInput_dropTransport(); lgAudio_dropTransport(); lgClipboard_dropTransport(); - app_setState(APP_STATE_RESTART); + primaryLost(); } else if (status == LG_TRANSPORT_END) app_setState(APP_STATE_SHUTDOWN); @@ -2196,6 +2204,8 @@ static void fallbackConnected(void * opaque, const LG_TransportSession * session) { (void)opaque; + atomic_fetch_and_explicit(&g_state.transportLost, + ~TRANSPORT_LOST_FALLBACK, memory_order_acq_rel); if (app_getState() == APP_STATE_SHUTDOWN) return; @@ -2219,6 +2229,19 @@ static void fallbackDisconnected(void * opaque) videoSourceShowSplashIfNeeded(); } +static void fallbackLost(void * opaque) +{ + (void)opaque; + const unsigned int lost = atomic_fetch_or_explicit(&g_state.transportLost, + TRANSPORT_LOST_FALLBACK, memory_order_acq_rel) | + TRANSPORT_LOST_FALLBACK; + if ((lost & TRANSPORT_LOST_ALL) == TRANSPORT_LOST_ALL) + { + DEBUG_INFO("Primary and fallback transport sessions disconnected"); + app_setState(APP_STATE_SHUTDOWN); + } +} + static void fallbackUUIDMismatch(void * opaque, const uint8_t primary[16], const uint8_t fallback[16]) { @@ -2233,10 +2256,40 @@ static void fallbackUUIDMismatch(void * opaque, const uint8_t primary[16], static const LG_TransportFallbackEventOps fallbackEvents = { .connected = fallbackConnected, + .lost = fallbackLost, .disconnected = fallbackDisconnected, .uuidMismatch = fallbackUUIDMismatch, }; +static void primaryLost(void) +{ + atomic_store_explicit( + &g_state.lgHostConnected, false, memory_order_release); + unsigned int lost = atomic_load_explicit( + &g_state.transportLost, memory_order_acquire); + for (;;) + { + unsigned int next = lost | TRANSPORT_LOST_PRIMARY; + if (lgTransportFallback_ready(g_state.fallback)) + next &= ~TRANSPORT_LOST_FALLBACK; + if (atomic_compare_exchange_weak_explicit(&g_state.transportLost, + &lost, next, memory_order_acq_rel, memory_order_acquire)) + { + lost = next; + break; + } + } + if ((lost & TRANSPORT_LOST_ALL) == TRANSPORT_LOST_ALL) + { + DEBUG_INFO("Primary and fallback transport sessions disconnected"); + app_setState(APP_STATE_SHUTDOWN); + return; + } + + DEBUG_INFO("Waiting for the host to restart..."); + app_setState(APP_STATE_RESTART); +} + static bool fallbackStart(const uint8_t primaryUUID[16]) { if (!option_get_bool("spice", "enable") || @@ -3268,6 +3321,8 @@ restart: if (probe.status == LG_TRANSPORT_OK) { session = probe.session; + atomic_fetch_and_explicit(&g_state.transportLost, + ~TRANSPORT_LOST_PRIMARY, memory_order_acq_rel); initialFallbackEnable = 0; break; } @@ -3454,10 +3509,7 @@ restart: lgInput_dropTransport(); lgAudio_dropTransport(); lgClipboard_dropTransport(); - atomic_store_explicit( - &g_state.lgHostConnected, false, memory_order_release); - DEBUG_INFO("Waiting for the host to restart..."); - app_setState(APP_STATE_RESTART); + primaryLost(); break; } lgMessage_process(); @@ -3491,9 +3543,11 @@ restart: lgClipboard_dropTransport(); g_state.transport.ops->disconnect(g_state.transport.handle); - app_setState(APP_STATE_RUNNING); - lgInit(); - goto restart; + if (app_transitionState(APP_STATE_RESTART, APP_STATE_RUNNING)) + { + lgInit(); + goto restart; + } } return recoveryExit(&recoveryPrompt, 0); diff --git a/client/src/main.h b/client/src/main.h index 9eac12e8..0a9f08fe 100644 --- a/client/src/main.h +++ b/client/src/main.h @@ -34,16 +34,10 @@ #include "common/ll.h" #include "cimgui.h" +#include "app.h" #include "interface/transport.h" #include "transport_fallback.h" -enum RunState -{ - APP_STATE_RUNNING, - APP_STATE_RESTART, - APP_STATE_SHUTDOWN -}; - enum MicDefaultState { MIC_DEFAULT_PROMPT, MIC_DEFAULT_ALLOW, @@ -112,6 +106,7 @@ struct AppState bool videoGeometryDirty; atomic_bool fallbackUUIDMismatch; + atomic_uint transportLost; uint8_t guestUUID[16]; bool guestUUIDValid; @@ -359,18 +354,6 @@ extern struct AppState g_state; extern struct CursorState g_cursor; extern struct AppParams g_params; -static inline enum RunState app_getState(void) -{ - extern _Atomic(enum RunState) p_appState; - return atomic_load_explicit(&p_appState, memory_order_acquire); -} - -static inline void app_setState(enum RunState state) -{ - extern _Atomic(enum RunState) p_appState; - atomic_store_explicit(&p_appState, state, memory_order_release); -} - int main_cursorThread(void * unused); int main_frameThread(void * unused); diff --git a/client/src/transport_fallback.c b/client/src/transport_fallback.c index 7e1b4b3d..3873f46a 100644 --- a/client/src/transport_fallback.c +++ b/client/src/transport_fallback.c @@ -164,6 +164,16 @@ static void notifyDisconnected( LG_UNLOCK(fallback->eventLock); } +static void notifyLost(LG_TransportFallback * fallback, bool requested) +{ + if (!requested || !fallback->eventOps.lost) + return; + + LG_LOCK(fallback->eventLock); + fallback->eventOps.lost(fallback->eventOpaque); + LG_UNLOCK(fallback->eventLock); +} + static void unpublishProviders(LG_TransportFallback * fallback, bool live) { LG_LOCK(fallback->providerLock); @@ -420,9 +430,12 @@ static bool connectFallback(LG_TransportFallback * fallback) notifyConnected(fallback); + bool lost = false; while (!atomic_load_explicit(&fallback->stop, memory_order_acquire)) { lgWaitEvent(fallback->wakeEvent, SESSION_POLL_MS); + if (atomic_load_explicit(&fallback->stop, memory_order_acquire)) + break; applyVideoRequest(fallback); LG_LOCK_EXCLUSIVE(fallback->lock); @@ -436,16 +449,21 @@ static bool connectFallback(LG_TransportFallback * fallback) if (reject || !admitted || closing) break; if (!sessionLive(fallback)) + { + lost = !atomic_load_explicit( + &fallback->stop, memory_order_acquire); break; + } } - const bool knownDead = !atomic_load_explicit( - &fallback->stop, memory_order_acquire) && !sessionLive(fallback); - const bool reportDisconnect = cleanupConnection(fallback, knownDead); + const bool reportDisconnect = cleanupConnection(fallback, lost); if (reportMismatch && fallback->eventOps.uuidMismatch) fallback->eventOps.uuidMismatch( fallback->eventOpaque, primaryUUID, fallbackUUID); + const bool reportLost = lost && reportDisconnect && !atomic_load_explicit( + &fallback->stop, memory_order_acquire); + notifyLost(fallback, reportLost); notifyDisconnected(fallback, reportDisconnect); return true; } diff --git a/client/src/transport_fallback.h b/client/src/transport_fallback.h index e1bfa118..51180760 100644 --- a/client/src/transport_fallback.h +++ b/client/src/transport_fallback.h @@ -32,6 +32,8 @@ typedef struct LG_TransportFallbackEventOps { /* The session is valid only for the duration of this callback. */ void (*connected)(void * opaque, const LG_TransportSession * session); + /* Called only when an admitted session is unexpectedly lost. */ + void (*lost)(void * opaque); void (*disconnected)(void * opaque); void (*uuidMismatch)(void * opaque, const uint8_t primary[16], const uint8_t fallback[16]); diff --git a/client/tests/keybind_test.c b/client/tests/keybind_test.c index 60e18197..f661cef1 100644 --- a/client/tests/keybind_test.c +++ b/client/tests/keybind_test.c @@ -81,6 +81,16 @@ struct CursorState g_cursor; struct AppParams g_params; _Atomic(enum RunState) p_appState; +enum RunState app_getState(void) +{ + return atomic_load(&p_appState); +} + +void app_setState(enum RunState state) +{ + atomic_store(&p_appState, state); +} + static const int fKeys[] = { KEY_F1 , KEY_F2 , KEY_F3 , KEY_F4,