feat(arch): RadioSession owns TciServer + CatPorts (#3351 session v2)#3545
Conversation
Twelfth PR of the #3351 series — session v2. The per-radio servers move from loose MainWindow members onto the RadioSession aggregate, making the #2385 crash class structurally impossible. Why this is the right owner: both TciServer and CatPort hold a raw RadioModel*. The #2385 crash-on-quit happened because Qt's child sweep deleted TciServer *after* MainWindow's value members (including the model) were gone; the fix was a carefully-ordered manual teardown. With the session owning both the model AND the servers, the ordering is now a language guarantee: servers are deleted in ~RadioSession's BODY, which always runs before the m_radioModel member destructs. Changes: - RadioSession gains TciServer* (HAVE_WEBSOCKETS-guarded) and CatPort*[kCatPorts] ownership: setTciServer()/setCatPort() take ownership and Q_ASSERT the object has no QObject parent (a parent would recreate #2385); shutdownTciServer() keeps the early-teardown path the shutdown sequence needs (DAX stream-remove must reach the radio while audio is stopped); ~RadioSession deletes everything before the model. - MainWindow drops both members; inline accessors tciServer()/catPort(i) forward to the session, so the ~40 read sites are a mechanical rename. kCatPorts now aliases RadioSession::kCatPorts. - Construction sites (wireRadioModel/wireCatPorts in the Session TU) hand ownership to the session; servers are no longer parented to MainWindow. - The #2385 shutdown block keeps its TciApplet back-reference nulling and now routes through m_session->shutdownTciServer(). - CatControlApplet::setPorts() feeds from m_session->catPortsArray(); verified the applet only touches ports from signal-driven slots, so the (session-dies-before-child-sweep) window is inert — connections auto-disconnect and no event loop spins during destruction. Verified: full build clean, all 33 ctest suites pass, a11y lint clean, zero residual m_tciServer/m_catPorts references. Refs #3351 #3445 #2385. Principle XI.
ten9876
left a comment
There was a problem hiding this comment.
Architecture move is sound — destruction-order claim is correct and the doc comments in the header capture the contract well. Two correctness concerns inline (Q_ASSERT debug-only hardening, and the catPortsArray() lifetime contract with CatControlApplet), one of which I think is worth fixing in this PR.
Mechanical renames look clean and complete; grep-verified no residual m_tciServer / m_catPorts references.
Generated by Claude Code
|
Two nits, neither blocking — filing alongside the inline review for completeness.
QA checklist is missing the CAT analog of the #2385 trigger. The current "Quit while connected with TCI clients attached" tick covers the TciServer path. The exact same crash class applies on the CAT side — both Generated by Claude Code |
jensenpat
left a comment
There was a problem hiding this comment.
Reviewed: clean ownership refactor, no blockers.
- All 6 CI checks green (build on macOS/Windows/Linux, CodeQL, analyze-cpp, a11y).
- Member migration complete and grep-clean; the only residual
m_tciServeris TciApplet's own back-reference field, not the removed MainWindow member. - Destruction order verified (the #2385 surface): in
~RadioSessionthe body deletes TciServer + CatPorts before any member destructs, andm_radioModelis declared first so it destructs last — fully alive throughout teardown. The crash class is now structurally impossible. - CatControlApplet's dangling-pointer window is genuinely inert: no destructor deref, no polling timers, slots are signal-driven and Qt auto-disconnects on CatPort deletion (no event loop spins during teardown).
- TciApplet back-ref is nulled before
shutdownTciServer(), preserving the original #2385 fix's early-teardown timing. wireCatPorts()has a single call site in the constructor (no re-entry into setCatPort's delete), and bounds match (kCatPorts == kMaxPorts == 8).
Strictly safer than the old parent-based teardown.
…ssion v2) (aethersdr#3545) ## Summary Twelfth PR of the aethersdr#3351 series — **session v2**. The per-radio servers move from loose MainWindow members onto the RadioSession aggregate. Beyond the architecture, this makes the **aethersdr#2385 crash class structurally impossible**. ## The ownership argument Both `TciServer` and `CatPort` hold a raw `RadioModel*`. aethersdr#2385 (crash on quit) happened because Qt's child sweep deleted TciServer *after* MainWindow's value members — including the model — were already gone; the fix was a carefully-ordered manual teardown with a long explanatory comment. With the session owning **both the model and the servers**, the ordering becomes a language guarantee: servers die in `~RadioSession`'s body, which always runs before the `m_radioModel` member destructs. ## Changes | Piece | Before | After | |---|---|---| | `TciServer` | `MainWindow::m_tciServer`, parented to MainWindow, manual delete dance | session-owned, no parent (`Q_ASSERT`ed), deleted in `~RadioSession` body | | `CatPort[8]` | `MainWindow::m_catPorts[]`, parented to MainWindow | session-owned array, same structural guarantee | | Read sites (~40 across 5 TUs) | `m_tciServer` / `m_catPorts[i]` | inline forwarding accessors `tciServer()` / `catPort(i)` — mechanical rename | | aethersdr#2385 shutdown block | null applet back-ref → `delete` → `nullptr` | null applet back-ref → `m_session->shutdownTciServer()` (early-teardown timing preserved: DAX stream-removes still reach the radio while audio is stopped) | | `CatControlApplet::setPorts` | fed from the raw member array | fed from `m_session->catPortsArray()` | ## Destruction-order analysis (the review surface) The one subtle window: the session (a member) dies before the applet panel (a QObject child, swept later). The applets hold copied `CatPort*` / `TciServer*`: - **CatControlApplet** — verified it only dereferences ports from signal-driven slots; Qt auto-disconnects on CatPort destruction and no event loop spins during destructor teardown. Inert. - **TciApplet** — its back-reference is explicitly nulled before `shutdownTciServer()`, same as the original aethersdr#2385 fix. ## Verification - Full build clean, zero new warnings - All 33 ctest suites pass - a11y lint: 0 findings - Zero residual `m_tciServer` / `m_catPorts` references (grep-verified) ## QA checklist for the bench - [ ] TCI: WSJT-X connects, audio flows, disconnect/reconnect WSJT-X mid-session - [ ] TCI port toggle off/on from the menu (exercises start/stop on the session-owned server) - [ ] CAT: rigctld client connects on each configured port; PTY symlinks present (Linux) - [ ] CAT applet shows live client counts + PTY paths - [ ] **Quit while connected with TCI clients attached** — the aethersdr#2385 regression test, now structurally guaranteed - [ ] Quit while disconnected — both paths clean Refs aethersdr#3351 aethersdr#3445 aethersdr#2385
Summary
Twelfth PR of the #3351 series — session v2. The per-radio servers move from loose MainWindow members onto the RadioSession aggregate. Beyond the architecture, this makes the #2385 crash class structurally impossible.
The ownership argument
Both
TciServerandCatPorthold a rawRadioModel*. #2385 (crash on quit) happened because Qt's child sweep deleted TciServer after MainWindow's value members — including the model — were already gone; the fix was a carefully-ordered manual teardown with a long explanatory comment. With the session owning both the model and the servers, the ordering becomes a language guarantee: servers die in~RadioSession's body, which always runs before them_radioModelmember destructs.Changes
TciServerMainWindow::m_tciServer, parented to MainWindow, manual delete danceQ_ASSERTed), deleted in~RadioSessionbodyCatPort[8]MainWindow::m_catPorts[], parented to MainWindowm_tciServer/m_catPorts[i]tciServer()/catPort(i)— mechanical renamedelete→nullptrm_session->shutdownTciServer()(early-teardown timing preserved: DAX stream-removes still reach the radio while audio is stopped)CatControlApplet::setPortsm_session->catPortsArray()Destruction-order analysis (the review surface)
The one subtle window: the session (a member) dies before the applet panel (a QObject child, swept later). The applets hold copied
CatPort*/TciServer*:shutdownTciServer(), same as the original Crash on quit —TciServer::releaseDaxForTci()dereferences null/danglingRadioModelduringMainWindowteardown #2385 fix.Verification
m_tciServer/m_catPortsreferences (grep-verified)QA checklist for the bench
TciServer::releaseDaxForTci()dereferences null/danglingRadioModelduringMainWindowteardown #2385 regression test, now structurally guaranteedRefs #3351 #3445 #2385