From a668ac2b60a4b933744f04cb946faa6cda04c73c Mon Sep 17 00:00:00 2001 From: vibesoftwarecoder Date: Wed, 16 Sep 2026 08:03:15 -0500 Subject: [PATCH] fix: stop 6.3.4 hanging on launch (seat discovery self-deadlock) ComputerManager::startPolling() holds m_Lock for write and calls MultiSeatDiscovery::start(). start() called poll() directly, and poll() emits aboutToPoll(), whose slot updateMultiSeatProbeTargets() takes m_Lock for read on the same thread. QReadWriteLock is not recursive, so the main thread waited on itself and the window stopped responding on every launch. start() now queues the first poll on the event loop, so it runs after startPolling() has released the lock. poll() returns early if stop() ran before the queued call was delivered. Verified: the 6.3.4 release zip hangs on launch (main thread blocked in QReadLocker under startPolling, per cdb); 6.3.3 does not. A harness that builds multiseatdiscovery.cpp with the same lock order deadlocks on the 6.3.4 source and passes on this one (first poll still runs within 1 s; no poll after an immediate stop). Co-Authored-By: Claude Opus 5 --- app/backend/computermanager.cpp | 5 +++++ app/backend/multiseatdiscovery.cpp | 16 ++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/backend/computermanager.cpp b/app/backend/computermanager.cpp index 7e82e0de9..187c6e6d5 100644 --- a/app/backend/computermanager.cpp +++ b/app/backend/computermanager.cpp @@ -397,6 +397,8 @@ void ComputerManager::startPolling() }); connect(m_MultiSeatDiscovery, &MultiSeatDiscovery::aboutToPoll, this, &ComputerManager::updateMultiSeatProbeTargets); + // start() only queues the first poll. It must not poll synchronously: this function holds + // m_Lock for write, and updateMultiSeatProbeTargets() takes it for read. m_MultiSeatDiscovery->start(); // Start polling threads for each known host @@ -438,6 +440,9 @@ void ComputerManager::startPollingComputer(NvComputer* computer) // beside share one address — probing it twice would just double the requests for nothing. Local // addresses only: seat ports are not port-forwarded, so probing a remote address would be four // guaranteed failures per tick against someone else's network. +// +// ⛔ Takes m_Lock for read. It must never run on a thread that already holds m_Lock, because +// QReadWriteLock is not recursive and that thread would wait on itself. void ComputerManager::updateMultiSeatProbeTargets() { if (m_MultiSeatDiscovery == nullptr) { diff --git a/app/backend/multiseatdiscovery.cpp b/app/backend/multiseatdiscovery.cpp index af1eb6c6e..a22046fe5 100644 --- a/app/backend/multiseatdiscovery.cpp +++ b/app/backend/multiseatdiscovery.cpp @@ -26,8 +26,15 @@ MultiSeatDiscovery::~MultiSeatDiscovery() void MultiSeatDiscovery::start() { m_Timer->start(); - // Poll immediately on start, don't wait for first interval - poll(); + + // Poll soon after start rather than waiting a full interval, but from the event loop, never + // from inside start(). + // + // ⛔ Do not call poll() directly here. ComputerManager::startPolling() calls start() while it + // holds m_Lock for write, and poll() emits aboutToPoll(), whose slot takes that same lock for + // read on the same thread. QReadWriteLock is not recursive, so the main thread waited on + // itself forever: 6.3.4 hung ("Not Responding") on every launch. + QMetaObject::invokeMethod(this, &MultiSeatDiscovery::poll, Qt::QueuedConnection); } void MultiSeatDiscovery::stop() @@ -42,6 +49,11 @@ void MultiSeatDiscovery::setHostsToProbe(const QStringList& addresses) void MultiSeatDiscovery::poll() { + // The first poll is queued by start(). If stop() ran before it was delivered, do nothing. + if (!m_Timer->isActive()) { + return; + } + // Refresh the address list first — hosts may have been added or removed since the last tick. emit aboutToPoll();