fix(win): prevent process lingering in Task Manager after close#2802
Conversation
On Windows 11, the AetherSDR process could remain visible in Task Manager
indefinitely after the main window was closed. Three independent root causes
were identified.
────────────────────────────────────────────────────────────────────────────
Root cause 1 — AetherialAudioStrip blocks quitOnLastWindowClosed
────────────────────────────────────────────────────────────────────────────
`m_aetherialStrip` is created with `parent=MainWindow` but then given the
`Qt::Window` flag so it appears as an independent taskbar entry. Qt 6 treats
any widget carrying `Qt::Window` as a top-level window for the purpose of
`QApplication::quitOnLastWindowClosed`. The attribute `WA_QuitOnClose`
defaults to `true` on every widget.
Consequence: if the strip is visible when the user closes the main window,
`QApplication` sees a second visible top-level window with `WA_QuitOnClose`
set and refuses to emit `lastWindowClosed`. `app.exec()` never returns,
`~MainWindow()` never runs, and the process hangs forever.
Fix: set `WA_QuitOnClose = false` on `m_aetherialStrip` immediately after
the `Qt::Window` flag is applied. The strip is a secondary, inspector-style
window and must not control application lifetime.
────────────────────────────────────────────────────────────────────────────
Root cause 2 — floated applet-panel window lacks WA_QuitOnClose = false
────────────────────────────────────────────────────────────────────────────
`m_appletPanelFloatWindow` is created with `parent = nullptr` (a genuine
top-level window). `WA_QuitOnClose` was never set to `false` on it, so if
a close-event ordering quirk (e.g. the float window's native WM_DESTROY
arrives before MainWindow's hide completes) caused it to be the apparent
"last window", Qt could fail to call `quit()`. `FloatingContainerWindow`
already sets this attribute; the applet-panel float window was the only
top-level secondary window that did not.
Fix: add `setAttribute(Qt::WA_QuitOnClose, false)` alongside the existing
`WA_DeleteOnClose = false` in `floatAppletPanel()`. This matches the
pattern used by all other secondary/floating windows in the codebase.
────────────────────────────────────────────────────────────────────────────
Root cause 3 — MqttClient destroys mosquitto while loop thread still runs
────────────────────────────────────────────────────────────────────────────
`MqttClient::disconnect()` contains a `#ifndef Q_OS_WIN` guard that skips
`mosquitto_loop_stop()` on Windows. The guard was presumably added to avoid
a deadlock in the `force=false` (graceful) stop path, where the loop thread
tries to deliver a callback to the Qt main thread while the main thread is
already blocked waiting for the stop to complete.
The destructor calls `disconnect()` (which on Windows leaves the background
network thread running) and then immediately calls `mosquitto_destroy()`.
The libmosquitto documentation explicitly states that `mosquitto_loop_stop()`
must be called before `mosquitto_destroy()`. Violating this contract on
Windows can leave the thread in an undefined state — holding a WINSOCK
handle or a Windows timer — that prevents `ExitProcess()` from completing
cleanly (or at all).
Fix: add `mosquitto_loop_stop(m_mosq, true)` (force=true, safe on any
thread) between the `disconnect()` call and `mosquitto_destroy()` in the
destructor. The `force=true` variant does not attempt a graceful handshake
with the loop thread and therefore cannot deadlock; it is the same path
used by `connectToBroker()` when reinitialising the client. The `#ifndef
Q_OS_WIN` guard in `disconnect()` is left in place — that code path handles
user-initiated disconnect during a session and has different threading
constraints.
────────────────────────────────────────────────────────────────────────────
Files changed
────────────────────────────────────────────────────────────────────────────
src/core/MqttClient.cpp
— ~MqttClient(): add mosquitto_loop_stop(m_mosq, true) before destroy
src/gui/MainWindow.cpp
— toggleAetherialStrip(): set WA_QuitOnClose=false on m_aetherialStrip
— floatAppletPanel(): set WA_QuitOnClose=false on m_appletPanelFloatWindow
────────────────────────────────────────────────────────────────────────────
Testing notes
────────────────────────────────────────────────────────────────────────────
To reproduce the original bug:
1. Open AetherSDR on Windows 11.
2. Open the Aetherial Audio Strip (egg-nub button) — ensures root cause 1.
OR pop out the applet panel (View → Pop Out Applet Panel) — root cause 2.
OR connect to an MQTT broker in the MQTT applet — root cause 3.
3. Close the main window.
4. Observe Task Manager: process should exit within ~1 second.
Before this fix: process remains in Task Manager indefinitely.
After this fix: process exits promptly on close.
|
@AetherClaude thoughts on this PR? |
|
Reviewed the diff against the current tree. All three fixes are sound and minimal — happy to see this land. Root cause 1 — Root cause 2 — Root cause 3 — One small forward-looking note (not a blocker for this PR): the LGTM. A maintainer can authorize the orchestrator to land this fix by adding the 73, Jeremy KK7GWY & Claude (AI dev partner) |
There was a problem hiding this comment.
Thanks @chibondking — careful diagnosis and the fix is minimal and well-targeted. The PR looks good to merge.
A few notes:
MainWindow.cpp (both hunks): Correct and consistent with the existing pattern. FloatingContainerWindow.cpp:35 already does the same thing, and m_aetherialStrip + m_appletPanelFloatWindow were the two remaining secondary top-levels missing it. The Qt::WA_QuitOnClose reasoning is right — once Qt::Window is applied (or parent=nullptr is used) the widget counts toward quitOnLastWindowClosed.
MqttClient.cpp: The change is reasonable defense-in-depth and makes the destructor symmetric with the reinit path in connectToBroker() (lines 66–70 already do loop_stop(force=true) + destroy).
One small caveat on the diagnosis in root cause 3: on Windows we never call mosquitto_loop_start() — connectToBroker() uses the Q_OS_WIN branch at line 118–120 to drive mosquitto_loop() via m_pollTimer instead, precisely because libmosquitto's threaded loop isn't reliably available on the Windows build. So in practice there is no background libmosquitto thread for mosquitto_destroy() to race with on Windows. The poll timer is a QObject member that gets stopped in disconnect() (line 134) and destroyed with the client. That doesn't make this fix wrong — mosquitto_loop_stop() on a client that never started a thread is documented as harmless (returns MOSQ_ERR_NOT_SUPPORTED), and matching the connectToBroker() cleanup pattern is good hygiene. But if Windows-side process lingering came back after this lands, root cause 3 probably isn't where to look next; the more likely culprit would be something else holding app.exec() open (e.g. another stray top-level, a QThread not joined, or a QSocketNotifier on a WSA handle). Worth keeping in mind, not a blocker.
LGTM.
|
Claude here — merged. Thanks CJ! Three independent root causes with three minimal one-line fixes, each tracing to an existing precedent in the codebase (FloatingContainerWindow + connectToBroker), and a textbook-quality root-cause analysis in the PR body. The Windows-process-hanging-in-Task-Manager symptom has been a recurring user complaint and this targets all three structural causes at once. 73, Jeremy KK7GWY & Claude (AI dev partner) |
Hotfix recommended for TCI digital-mode operators, Windows users with MQTT or floating-panel windows, and Linux Ubuntu 22.04 builders. - TCI TX audio level regression vs v26.5.1 (#2806/#2807) — restores WSJT-X / JTDX output to full level by reverting only the device + protocol identity strings from #2597 to a form that bypasses WSJT-X's TCITransceiver SunSDR2DX/non-ExpertSDR3 gain trap. - Windows process lingering in Task Manager after close (#2802, chibondking) — three independent root causes addressed: Channel Strip + applet-panel float window WA_QuitOnClose, MqttClient destructor loop_stop. - AppImage CI build failure on gcc 11 strict mode (#2799) — qualifies bitstream.h:192 member access with `this->` for two-phase name lookup. Restores the v26.5.2 x86_64 AppImage artifact. - SSA (Sweden) band plan (#2805, NF0T) — Swedish national overlay with PTSFS 2025:1 constraints, Swedish labels, 89 spots. Bumps version 26.5.2 → 26.5.2.1 in CMakeLists.txt, README.md, CLAUDE.md; regenerates WhatsNewData.cpp from CHANGELOG.md. CalVer scheme expanded to YY.M.patch[.hotfix] to document the 4th-component hotfix convention used historically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On Windows 11, the AetherSDR process could remain visible in Task Manager indefinitely after the main window was closed. Three independent root causes were identified.
──────────────────────────────────────────────────────────────────────────── Root cause 1 — AetherialAudioStrip blocks quitOnLastWindowClosed ────────────────────────────────────────────────────────────────────────────
m_aetherialStripis created withparent=MainWindowbut then given theQt::Windowflag so it appears as an independent taskbar entry. Qt 6 treats any widget carryingQt::Windowas a top-level window for the purpose ofQApplication::quitOnLastWindowClosed. The attributeWA_QuitOnClosedefaults totrueon every widget.Consequence: if the strip is visible when the user closes the main window,
QApplicationsees a second visible top-level window withWA_QuitOnCloseset and refuses to emitlastWindowClosed.app.exec()never returns,~MainWindow()never runs, and the process hangs forever.Fix: set
WA_QuitOnClose = falseonm_aetherialStripimmediately after theQt::Windowflag is applied. The strip is a secondary, inspector-style window and must not control application lifetime.──────────────────────────────────────────────────────────────────────────── Root cause 2 — floated applet-panel window lacks WA_QuitOnClose = false ────────────────────────────────────────────────────────────────────────────
m_appletPanelFloatWindowis created withparent = nullptr(a genuine top-level window).WA_QuitOnClosewas never set tofalseon it, so if a close-event ordering quirk (e.g. the float window's native WM_DESTROY arrives before MainWindow's hide completes) caused it to be the apparent "last window", Qt could fail to callquit().FloatingContainerWindowalready sets this attribute; the applet-panel float window was the only top-level secondary window that did not.Fix: add
setAttribute(Qt::WA_QuitOnClose, false)alongside the existingWA_DeleteOnClose = falseinfloatAppletPanel(). This matches the pattern used by all other secondary/floating windows in the codebase.──────────────────────────────────────────────────────────────────────────── Root cause 3 — MqttClient destroys mosquitto while loop thread still runs ────────────────────────────────────────────────────────────────────────────
MqttClient::disconnect()contains a#ifndef Q_OS_WINguard that skipsmosquitto_loop_stop()on Windows. The guard was presumably added to avoid a deadlock in theforce=false(graceful) stop path, where the loop thread tries to deliver a callback to the Qt main thread while the main thread is already blocked waiting for the stop to complete.The destructor calls
disconnect()(which on Windows leaves the background network thread running) and then immediately callsmosquitto_destroy(). The libmosquitto documentation explicitly states thatmosquitto_loop_stop()must be called beforemosquitto_destroy(). Violating this contract on Windows can leave the thread in an undefined state — holding a WINSOCK handle or a Windows timer — that preventsExitProcess()from completing cleanly (or at all).Fix: add
mosquitto_loop_stop(m_mosq, true)(force=true, safe on any thread) between thedisconnect()call andmosquitto_destroy()in the destructor. Theforce=truevariant does not attempt a graceful handshake with the loop thread and therefore cannot deadlock; it is the same path used byconnectToBroker()when reinitialising the client. The#ifndef Q_OS_WINguard indisconnect()is left in place — that code path handles user-initiated disconnect during a session and has different threading constraints.──────────────────────────────────────────────────────────────────────────── Files changed
──────────────────────────────────────────────────────────────────────────── src/core/MqttClient.cpp
— ~MqttClient(): add mosquitto_loop_stop(m_mosq, true) before destroy
src/gui/MainWindow.cpp
— toggleAetherialStrip(): set WA_QuitOnClose=false on m_aetherialStrip
— floatAppletPanel(): set WA_QuitOnClose=false on m_appletPanelFloatWindow
──────────────────────────────────────────────────────────────────────────── Testing notes
──────────────────────────────────────────────────────────────────────────── To reproduce the original bug:
Before this fix: process remains in Task Manager indefinitely.
After this fix: process exits promptly on close.