[idd] recovery: unplug monitor without helper

When LGIddHelper is unavailable, recovery requests could remain
pending until timeout while the virtual monitor stayed connected.
This could leave a logged-out guest without a usable display.

Fall back to departing the IDD monitor for active recovery, then
re-arrive it before normal recovery completes. Serialize monitor
lifecycle changes and preserve recovery ordering across Helper
reconnects, timeouts, and stale responses.
This commit is contained in:
Geoffrey McRae
2026-08-20 16:03:58 +10:00
parent 7b059ad67e
commit 60a51fccdf
6 changed files with 777 additions and 47 deletions

View File

@@ -369,7 +369,8 @@ void CPipeServer::OnPipeConnected()
// latest request whenever the helper reconnects so a helper restart cannot
// silently restore the IDD-only topology while recovery is active.
if (m_recoveryValid)
m_endpoint.Send(&m_recoveryRequest, sizeof(m_recoveryRequest));
m_recoveryPending = m_endpoint.Send(
&m_recoveryRequest, sizeof(m_recoveryRequest));
}
void CPipeServer::OnPipeDisconnected()
@@ -384,6 +385,37 @@ void CPipeServer::OnPipeDisconnected()
m_clientAuthorityId[0] = 0;
m_clientAuthorityId[1] = 0;
}
CSRWExclusiveLock queueLock(m_queueLock);
CSRWSharedLock recoveryLock(m_recoveryLock);
if (m_recoveryValid &&
(m_recoveryPending || m_recoveryHelperActive) &&
m_recoveryHandler)
{
const uint64_t route = m_recoveryRoute;
const uint64_t session = m_recoveryRequest.recovery.session;
const uint32_t serial = m_recoveryRequest.recovery.request &
~LGPipeMsg::RECOVERY_ACTIVE;
const bool active = (m_recoveryRequest.recovery.request &
LGPipeMsg::RECOVERY_ACTIVE) != 0;
// If NORMAL was sent while Helper still owned the recovery topology,
// do not replay it after a disconnect until the IDD monitor is present.
// Preserve the preceding ACTIVE command for reconnect in the meantime.
if (!active && !m_recoveryReplaySafe && m_recoveryActiveValid)
{
m_recoveryValid = true;
m_recoveryRoute = m_recoveryActiveRoute;
m_recoveryRequest = m_recoveryActiveRequest;
m_recoveryReplaySafe = true;
}
m_recoveryPending = false;
m_recoveryHelperActive = false;
queueLock.Unlock();
m_recoveryHandler(m_recoveryOpaque, route, session, serial, active,
RecoveryResult::HELPER_UNAVAILABLE);
}
}
bool CPipeServer::OnPipeMessage(const void * message, size_t size)
@@ -494,7 +526,7 @@ void CPipeServer::HandleReloadSettings()
void CPipeServer::HandleRecovery(const LGPipeMsg & msg)
{
CSRWSharedLock queueLock(m_queueLock);
CSRWExclusiveLock queueLock(m_queueLock);
if (!m_recoveryValid ||
msg.recovery.session != m_recoveryRequest.recovery.session ||
msg.recovery.request != m_recoveryRequest.recovery.request)
@@ -505,14 +537,44 @@ void CPipeServer::HandleRecovery(const LGPipeMsg & msg)
const uint32_t serial =
msg.recovery.request & ~LGPipeMsg::RECOVERY_ACTIVE;
const bool active =
const bool active =
(msg.recovery.request & LGPipeMsg::RECOVERY_ACTIVE) != 0;
const uint64_t route = m_recoveryRoute;
RecoveryResult result;
switch (msg.type)
{
case LGPipeMsg::RECOVERY_OFF:
result = RecoveryResult::NORMAL;
m_recoveryHelperActive = false;
m_recoveryReplaySafe = true;
m_recoveryActiveValid = false;
m_recoveryActiveRoute = 0;
m_recoveryActiveRequest = {};
break;
case LGPipeMsg::RECOVERY_ON:
result = RecoveryResult::ACTIVE;
m_recoveryHelperActive = true;
break;
case LGPipeMsg::RECOVERY_FAILED:
result = RecoveryResult::FAILED;
break;
case LGPipeMsg::RECOVERY_NO_DISPLAY:
result = RecoveryResult::NO_DISPLAY;
break;
default:
return;
}
m_recoveryPending = false;
CSRWSharedLock recoveryLock(m_recoveryLock);
queueLock.Unlock();
if (m_recoveryHandler)
m_recoveryHandler(m_recoveryOpaque,
m_recoveryRoute, msg.recovery.session, serial, active, msg.type);
route, msg.recovery.session, serial, active, result);
}
void CPipeServer::SetDeviceContext(CDeviceContext * context)
@@ -526,11 +588,19 @@ void CPipeServer::SetRecoveryHandler(
{
CSRWExclusiveLock queueLock(m_queueLock);
CSRWExclusiveLock recoveryLock(m_recoveryLock);
m_recoveryRoute = 0;
m_recoveryValid = false;
m_recoveryRequest = {};
m_recoveryHandler = handler;
m_recoveryOpaque = opaque;
m_recoveryRoute = 0;
m_recoveryValid = false;
m_recoveryPending = false;
m_recoveryHelperActive = false;
m_recoveryReplaySafe = true;
m_recoveryActiveValid = false;
m_recoveryRequest = {};
m_recoveryActiveRequest = {};
m_recoveryNewestRequest = {};
m_recoveryActiveRoute = 0;
m_recoveryNewestRoute = 0;
m_recoveryHandler = handler;
m_recoveryOpaque = opaque;
}
void CPipeServer::ClearRecoveryHandler(void * opaque)
@@ -540,11 +610,19 @@ void CPipeServer::ClearRecoveryHandler(void * opaque)
if (m_recoveryOpaque != opaque)
return;
m_recoveryRoute = 0;
m_recoveryValid = false;
m_recoveryRequest = {};
m_recoveryHandler = nullptr;
m_recoveryOpaque = nullptr;
m_recoveryRoute = 0;
m_recoveryValid = false;
m_recoveryPending = false;
m_recoveryHelperActive = false;
m_recoveryReplaySafe = true;
m_recoveryActiveValid = false;
m_recoveryRequest = {};
m_recoveryActiveRequest = {};
m_recoveryNewestRequest = {};
m_recoveryActiveRoute = 0;
m_recoveryNewestRoute = 0;
m_recoveryHandler = nullptr;
m_recoveryOpaque = nullptr;
}
bool CPipeServer::SetCursorPos(int32_t x, int32_t y)
@@ -596,14 +674,16 @@ void CPipeServer::ResolutionRejected(uint32_t width, uint32_t height,
WriteMsg(msg);
}
bool CPipeServer::SetRecovery(void * owner, uint64_t route,
uint64_t session, uint32_t serial, bool active)
CPipeServer::RecoveryDispatch CPipeServer::SetRecovery(
void * owner, uint64_t route,
uint64_t session, uint32_t serial, bool active,
bool replayIfUnavailable)
{
if (!route || !session || !serial ||
(serial & LGPipeMsg::RECOVERY_ACTIVE))
{
DEBUG_ERROR("Invalid recovery request correlation");
return false;
return RecoveryDispatch::REJECTED;
}
LGPipeMsg msg = {};
@@ -616,11 +696,57 @@ bool CPipeServer::SetRecovery(void * owner, uint64_t route,
CSRWExclusiveLock queueLock(m_queueLock);
CSRWExclusiveLock recoveryLock(m_recoveryLock);
if (!m_recoveryHandler || m_recoveryOpaque != owner)
return false;
return RecoveryDispatch::REJECTED;
m_recoveryValid = true;
m_recoveryRoute = route;
m_recoveryRequest = msg;
m_endpoint.Send(&msg, sizeof(msg));
return true;
// RecoveryHub routes increase for the lifetime of this handler. Keep a
// watermark separate from the replay request so restoring retained ACTIVE
// state cannot let a late, older NORMAL overwrite newer work.
if (m_recoveryNewestRoute)
{
if (route < m_recoveryNewestRoute)
return RecoveryDispatch::REJECTED;
if (route == m_recoveryNewestRoute &&
(msg.recovery.session !=
m_recoveryNewestRequest.recovery.session ||
msg.recovery.request !=
m_recoveryNewestRequest.recovery.request))
return RecoveryDispatch::REJECTED;
}
if (route > m_recoveryNewestRoute)
{
m_recoveryNewestRoute = route;
m_recoveryNewestRequest = msg;
}
const bool previousValid = m_recoveryValid;
const bool previousPending = m_recoveryPending;
const bool previousReplaySafe = m_recoveryReplaySafe;
const uint64_t previousRoute = m_recoveryRoute;
const LGPipeMsg previousRequest = m_recoveryRequest;
m_recoveryValid = true;
m_recoveryRoute = route;
m_recoveryRequest = msg;
m_recoveryReplaySafe = active || replayIfUnavailable;
if (active)
{
m_recoveryActiveValid = true;
m_recoveryActiveRoute = route;
m_recoveryActiveRequest = msg;
}
m_recoveryPending = m_endpoint.Send(&msg, sizeof(msg));
if (m_recoveryPending)
return RecoveryDispatch::SENT;
if (replayIfUnavailable)
return RecoveryDispatch::QUEUED;
// A NORMAL request cannot be replayed while the IDD monitor is locally
// absent. Retain the prior ACTIVE request until the monitor has re-arrived.
m_recoveryValid = previousValid;
m_recoveryPending = previousPending;
m_recoveryReplaySafe = previousReplaySafe;
m_recoveryRoute = previousRoute;
m_recoveryRequest = previousRequest;
return RecoveryDispatch::UNAVAILABLE;
}

View File

@@ -44,17 +44,42 @@ class CPipeServer : private IPipeEndpointHandler,
public IClipboardChannelDoorbell
{
public:
enum class RecoveryResult
{
NORMAL,
ACTIVE,
FAILED,
NO_DISPLAY,
HELPER_UNAVAILABLE,
};
enum class RecoveryDispatch
{
REJECTED,
UNAVAILABLE,
QUEUED,
SENT,
};
using RecoveryHandler = void (*)(void * opaque,
uint64_t route, uint64_t session, uint32_t serial, bool active,
LGPipeMsg::Type result);
RecoveryResult result);
private:
CPipeEndpoint m_endpoint;
CClipboardChannel m_clipboard;
CSRWLock m_queueLock;
std::vector<LGPipeMsg> m_queue;
bool m_recoveryValid = false;
LGPipeMsg m_recoveryRequest = {};
bool m_recoveryValid = false;
bool m_recoveryPending = false;
bool m_recoveryHelperActive = false;
bool m_recoveryReplaySafe = true;
bool m_recoveryActiveValid = false;
LGPipeMsg m_recoveryRequest = {};
LGPipeMsg m_recoveryActiveRequest = {};
LGPipeMsg m_recoveryNewestRequest = {};
uint64_t m_recoveryActiveRoute = 0;
uint64_t m_recoveryNewestRoute = 0;
CSRWLock m_deviceContextLock;
CDeviceContext * m_deviceContext = nullptr;
@@ -115,8 +140,9 @@ class CPipeServer : private IPipeEndpointHandler,
void SetGPUStatus(bool software);
void ResolutionRejected(uint32_t width, uint32_t height,
uint32_t requiredSizeMiB);
bool SetRecovery(void * owner, uint64_t route,
uint64_t session, uint32_t serial, bool active);
RecoveryDispatch SetRecovery(void * owner, uint64_t route,
uint64_t session, uint32_t serial, bool active,
bool replayIfUnavailable = true);
CClipboardChannel& Clipboard() { return m_clipboard; }
bool ClipboardKick(uint64_t epoch) override;