Skip to content

fix(spot): defer socket construction to SpotClients thread (Windows crash)#2420

Merged
ten9876 merged 1 commit into
mainfrom
aether/issue-1929-2418-spotclients-thread
May 7, 2026
Merged

fix(spot): defer socket construction to SpotClients thread (Windows crash)#2420
ten9876 merged 1 commit into
mainfrom
aether/issue-1929-2418-spotclients-thread

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

Summary

  • Fixes the cross-thread QCoreApplication::sendEvent assertion that crashes AetherSDR on Windows when the radio TCP connection drops while DX Cluster / RBN / WSJT-X / SpotCollector / POTA / FreeDV clients are running on the SpotClients worker thread.
  • Each spot client previously constructed its QTcpSocket / QUdpSocket / QWebSocket (and associated timers / QNetworkAccessManager) in its constructor on the main thread, then moveToThread()'d to SpotClients. On Windows the internal QSocketNotifier keeps Win32 message-loop affinity to the construction thread, so socket events delivered during a disconnect cascade trip the cross-thread assert.
  • New initialize() slot on each client constructs sockets/timers; MainWindow invokes it via Qt::QueuedConnection after moveToThread() so the internal notifiers bind to SpotClients.

Closes #1929
Closes #2418 (kn7k's v0.9.7 overnight crash matched the same code path — duplicate of #1929 per support-bundle analysis).

Test plan

🤖 Generated with Claude Code

@jensenpat jensenpat requested a review from ten9876 as a code owner May 6, 2026 19:44

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the careful write-up — the QSocketNotifier construction-thread affinity gotcha on Windows is real and the symptom (cross-thread sendEvent assert during a disconnect cascade) matches the root cause exactly. The fix is consistent across all six clients and the FIFO ordering of Qt::QueuedConnection posts means initialize() is guaranteed to run before any subsequent queued connectToCluster / startListening / startPolling / startConnection call from MainWindow, so the "method called before init" race isn't a concern in practice.

A few small things worth a look:

  1. Init slot called twice is silently ignored. if (m_socket) return; in each initialize() makes a second call a no-op, but it doesn't log. If MainWindow ever queues init twice by accident (e.g. during a future connect/disconnect refactor), that swallow could hide the bug. A qCWarning on the early return would be cheap insurance — but optional.

  2. Public non-slot methods still deref unconditionally. DxClusterClient::connectToCluster, disconnect, sendCommand, WsjtxClient::startListening, PotaClient::startPolling, FreeDvClient::startConnection, etc. all touch m_socket / m_ws / m_nam / m_pollTimer directly without a null check. Today they're only reachable via QMetaObject::invokeMethod(..., QueuedConnection) from MainWindow, so the queue-ordering invariant protects them. But the destructors and stopConnection/stopListening/stopPolling have null guards (correctly — those can run from arbitrary threads at shutdown), so the asymmetry is a tripwire. Not a blocker, just worth a comment on initialize() calling out the invariant — "must be queued before any other slot on this object after moveToThread."

  3. FreeDvClient's ping-timer lambda moves into initialize() cleanly, but the m_ws->sendTextMessage("2") deref inside it is fine because the timer can't fire before initialize() runs (timer is also constructed there). Worth noting in case a reviewer flags it — it's correct.

  4. Out of stated scope: none. All 11 files line up with the SpotClients-thread-construction story.

LGTM in substance — the build passes, the Linux/Windows smoke tests in the PR template are the right gates. Worth a quick test of a rapid radio drop/reconnect cycle on Windows to make sure the disconnect cascade really does land on SpotClients now.

…rash)

On Windows, QTcpSocket / QUdpSocket / QWebSocket each create an internal
QSocketNotifier whose Win32 message-loop affinity is bound to the *construction*
thread, not the QObject's current thread. Because all five spot clients
(DxCluster, RBN, WSJT-X, SpotCollector, POTA, FreeDV) were constructed on the
main thread and then moveToThread()'d to the SpotClients worker, socket events
delivered during a TCP disconnect cascade were dispatched via the main thread's
event dispatcher into a QObject living on SpotClients — tripping
QCoreApplication::sendEvent's cross-thread assert and killing the process.

Move socket / network / timer construction into a new initialize() slot on each
spot client. After moveToThread(), MainWindow invokes initialize() with
Qt::QueuedConnection so it runs on the SpotClients thread before any other
slot, giving the internal notifiers correct thread affinity.

Linux is unaffected because epoll-based socket notifiers have no thread
affinity, but the fix is portable and matches the v0.9.7 user crash report
that pointed at the same code path.

Fixes #1929
Fixes #2418
@jensenpat jensenpat force-pushed the aether/issue-1929-2418-spotclients-thread branch from 4275926 to ada8119 Compare May 7, 2026 00:08
@ten9876 ten9876 merged commit d4573a2 into main May 7, 2026
5 checks passed
@ten9876 ten9876 deleted the aether/issue-1929-2418-spotclients-thread branch May 7, 2026 02:10
@ten9876

ten9876 commented May 7, 2026

Copy link
Copy Markdown
Collaborator

Claude here — really clean root-cause analysis, jensenpat. The Win32 QSocketNotifier construction-thread affinity is the kind of cross-platform Qt quirk that's easy to miss until it bites. Deferred-init via QueuedConnection is exactly the right pattern, and the idempotent initialize() guard plus the m_listening sentinels keep the unguarded dereferences in stop paths safe. Closing #1929 and #2418 in one go is a nice bonus. Merged.

73, Jeremy KK7GWY & Claude (AI dev partner)

NF0T pushed a commit that referenced this pull request May 31, 2026
…es (#2481) (#3310)

Fixes #2481

## Problem
With 2 panadapters and 2 LogHX3 logs each running Spot Machine over TCI,
the waterfall intermittently froze. The reporter's isolation was
decisive — disabling Spot Machine in **both** logs resolved it, and
turning off the verbose logs did not. So the trigger is the
**spot-marker render path on the main thread**, not the TCI audio
fan-out the first triage suspected.

`wirePanadapter()` wired `rebuildSpots` directly to five `SpotModel`
signals, **once per panadapter**. Every incoming spot emitted
`spotAdded`/`spotUpdated`, firing `rebuildSpots`, which re-iterates
*every* spot, runs `DxccColorProvider::colorForSpot()` (CTY resolution +
QString allocations) per spot, and diffs the whole marker vector. Two
simultaneous cluster feeds drove roughly **O(spots × spots/sec × pans)**
main-thread work in bursts — the 10–170 ms paintEvent stalls seen in the
support-bundle logs.

## Fix
Coalesce the five signals through a single **50 ms** (~20 Hz, below the
FFT repaint cadence) single-shot timer per pan, so a burst of N spot
events collapses to **one** `rebuildSpots` per pan per window instead of
N × pans. Visual output is identical, just debounced. The timer is
parented to the spectrum widget (per-pan lifetime) and
`QPointer`-guarded, matching the sibling `rebuildTnfMarkers` pattern. No
threading or signal-routing changes.

## Files
- `src/gui/MainWindow.cpp` — `wirePanadapter()` spot-signal wiring

## Structural follow-up (flagged, not done here)
`colorForSpot()` / DXCC resolution still runs on the GUI thread inside
`rebuildSpots`. Moving it onto the existing `SpotClients` worker thread
(PR #2420 already moved the spot *sockets* there) is the larger,
structural fix and a maintainer call — deliberately left out of this
contained mitigation per the autonomous-agent boundaries.

## Testing
Builds clean (985/985, macOS). Manual:
1. 2 panadapters + 2 LogHX3 logs (Spot Machine on) + 2 JTDX over TCI;
let the cluster feeds run hot for several minutes.
2. Watch the waterfall for stutter and the title bar for "loose
connection to radio" — both should be gone.
3. Confirm spots still appear/expire and DXCC colors render on both pans
(refreshed at ≤20 Hz, visually indistinguishable).

💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…es (aethersdr#2481) (aethersdr#3310)

Fixes aethersdr#2481

## Problem
With 2 panadapters and 2 LogHX3 logs each running Spot Machine over TCI,
the waterfall intermittently froze. The reporter's isolation was
decisive — disabling Spot Machine in **both** logs resolved it, and
turning off the verbose logs did not. So the trigger is the
**spot-marker render path on the main thread**, not the TCI audio
fan-out the first triage suspected.

`wirePanadapter()` wired `rebuildSpots` directly to five `SpotModel`
signals, **once per panadapter**. Every incoming spot emitted
`spotAdded`/`spotUpdated`, firing `rebuildSpots`, which re-iterates
*every* spot, runs `DxccColorProvider::colorForSpot()` (CTY resolution +
QString allocations) per spot, and diffs the whole marker vector. Two
simultaneous cluster feeds drove roughly **O(spots × spots/sec × pans)**
main-thread work in bursts — the 10–170 ms paintEvent stalls seen in the
support-bundle logs.

## Fix
Coalesce the five signals through a single **50 ms** (~20 Hz, below the
FFT repaint cadence) single-shot timer per pan, so a burst of N spot
events collapses to **one** `rebuildSpots` per pan per window instead of
N × pans. Visual output is identical, just debounced. The timer is
parented to the spectrum widget (per-pan lifetime) and
`QPointer`-guarded, matching the sibling `rebuildTnfMarkers` pattern. No
threading or signal-routing changes.

## Files
- `src/gui/MainWindow.cpp` — `wirePanadapter()` spot-signal wiring

## Structural follow-up (flagged, not done here)
`colorForSpot()` / DXCC resolution still runs on the GUI thread inside
`rebuildSpots`. Moving it onto the existing `SpotClients` worker thread
(PR aethersdr#2420 already moved the spot *sockets* there) is the larger,
structural fix and a maintainer call — deliberately left out of this
contained mitigation per the autonomous-agent boundaries.

## Testing
Builds clean (985/985, macOS). Manual:
1. 2 panadapters + 2 LogHX3 logs (Spot Machine on) + 2 JTDX over TCI;
let the cluster feeds run hot for several minutes.
2. Watch the waterfall for stutter and the title bar for "loose
connection to radio" — both should be gone.
3. Confirm spots still appear/expire and DXCC colors render on both pans
(refreshed at ≤20 Hz, visually indistinguishable).

💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ver 0.9.7 crashed after standby over night [Windows] SpotClients thread: QNativeSocketEngine cross-thread sendEvent assertion on radio disconnect

2 participants