Skip to content

[Windows] SpotClients thread: QNativeSocketEngine cross-thread sendEvent assertion on radio disconnect #1929

Description

@chibondking

Environment

OS: Windows 11 (observed in debug build)
AetherSDR version: 0.8.22
Radio: FLEX-6600, firmware 4.1.5 (SmartSDR protocol v1.4.0.0)
Platform: x64, Qt 6.11.0 debug build, D3D11 backend
Bug description

When the radio TCP connection drops (intentional or not), a fatal Qt assertion fires on Windows. The application continues running in the session but the assert dialog is raised:

ASSERT failure in QCoreApplication::sendEvent:
"Cannot send events to objects owned by a different thread.
Current thread QThread(... name = "Qt mainThread").
Receiver 'QNativeSocketEngine(...)' was created in thread
QThread(... name = "SpotClients")"
file: qcoreapplication.cpp, line 542
The same log also shows several QRingBuffer assertion failures earlier in the session that may share the same root cause:

ASSERT: "bytes <= bufferSize" qringbuffer.cpp:76
ASSERT: "size() - offset > 0" qringbuffer_p.h:95
ASSERT: "size() + offset > 0" qringbuffer_p.h:101
These do not reproduce on Linux.

Steps to reproduce

Launch AetherSDR on Windows with a DX cluster configured to auto-connect on startup.
Wait for the radio to fully connect and all panadapters to initialize.
Either disconnect from the radio manually, or allow the radio to drop the TCP connection.
Observe the Qt assert dialog for QCoreApplication::sendEvent.
Expected behavior

Radio disconnect is handled cleanly. No cross-thread violations. Spot clients remain connected and auto-reconnect when the radio comes back.

Root cause analysis

DxClusterClient (and likely RbnClient) creates its QTcpSocket* m_socket as a child object in the constructor, which runs on the main thread:

// src/core/DxClusterClient.cpp:14
DxClusterClient::DxClusterClient(QObject* parent)
: QObject(parent)
, m_socket(new QTcpSocket(this)) // ← created on main thread
, m_reconnectTimer(new QTimer(this)) // ← same
The object is then moved to the SpotClients thread after construction:

// src/gui/MainWindow.cpp:1041
m_dxCluster->moveToThread(m_spotThread);
moveToThread() transfers the object's thread affinity, but on Windows, QTcpSocket internally creates a QSocketNotifier that registers with the Win32 message loop of the thread where the socket was constructed (the main thread's QEventDispatcherWin32). When a socket event fires on Windows — specifically during the cascade of connection teardown triggered by the radio dropping TCP — Win32 delivers the socket notification via the main thread's message pump, which then calls QCoreApplication::sendEvent targeting the QNativeSocketEngine that now lives in SpotClients. Qt's cross-thread guard fires.

On Linux this is not observed because epoll-based socket notifiers have no such thread affinity.

A secondary violation occurs at shutdown in closeEvent():

// src/gui/MainWindow.cpp:3425-3428
m_spotThread->quit();
m_spotThread->wait(3000);
delete m_dxCluster; // destructor calls m_socket->abort() from main thread
The destructor runs on the main thread while m_socket still has SpotClients affinity. Safe on Linux (epoll is thread-agnostic), but another assertion risk on Windows.

Proposed fix

Move socket and timer construction into a dedicated initialize() slot that runs inside the SpotClients thread after moveToThread():

// DxClusterClient.h — add:
public slots:
void initialize(); // called via QMetaObject::invokeMethod after moveToThread

private:
QTcpSocket* m_socket{nullptr};
QTimer* m_reconnectTimer{nullptr};

// DxClusterClient.cpp
DxClusterClient::DxClusterClient(QObject* parent)
: QObject(parent)
{
// socket and timer created in initialize(), not here
}

void DxClusterClient::initialize()
{
m_socket = new QTcpSocket(this);
m_reconnectTimer = new QTimer(this);
// move all connect() calls here from constructor
}

// MainWindow.cpp — after moveToThread():
m_dxCluster->moveToThread(m_spotThread);
QMetaObject::invokeMethod(m_dxCluster, &DxClusterClient::initialize,
Qt::QueuedConnection);
The same pattern applies to RbnClient and any other spot clients that own sockets.

For the shutdown path, queue the socket cleanup into the SpotClients thread before calling quit()/wait(), or use deleteLater() before stopping the thread so Qt cleans up the objects in the correct thread.

Affected files

src/core/DxClusterClient.h
src/core/DxClusterClient.cpp
src/gui/MainWindow.cpp (spot thread setup ~line 1039, closeEvent ~line 3425)
Labels: bug, windows, threading, spot

Metadata

Metadata

Assignees

No one assigned

    Labels

    WindowsWindows-specific issuebugSomething isn't workingmaintainer-reviewRequires maintainer review before any action is takenpriority: highHigh priority

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions