Skip to content

feat(arch): RadioSession owns TciServer + CatPorts (#3351 session v2)#3545

Merged
jensenpat merged 1 commit into
mainfrom
refactor/3351-session-v2-ownership
Jun 13, 2026
Merged

feat(arch): RadioSession owns TciServer + CatPorts (#3351 session v2)#3545
jensenpat merged 1 commit into
mainfrom
refactor/3351-session-v2-ownership

Conversation

@ten9876

@ten9876 ten9876 commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

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 TciServer and CatPort hold a raw RadioModel*. #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_ASSERTed), 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
#2385 shutdown block null applet back-ref → deletenullptr 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*:

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

Refs #3351 #3445 #2385

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 ten9876 requested a review from a team as a code owner June 13, 2026 01:32

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

Comment thread src/models/RadioSession.cpp
Comment thread src/models/RadioSession.h

ten9876 commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator Author

Two nits, neither blocking — filing alongside the inline review for completeness.

setCatPort runtime-replace semantics aren't documented. The implementation does delete m_catPorts[size_t(i)] before storing the new pointer, which means calling setCatPort(i, newPort) twice silently frees the old one. Not exercised by this PR (the only call site is the one-shot wire in wireCatPorts()), but a future caller reading the header signature alone wouldn't know that. Either a one-line doc comment ("idempotent install — previous port is deleted if present"), or split into installCatPort() (asserts the slot is null) + replaceCatPort() (current behavior). Doc comment is fine for now.

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 CatPort and TciServer held RadioModel* in the old layout. Add: "Quit while a rigctld client is connected on a Linux PTY". Same structural guarantee proves it, but it's worth confirming on the bench so the symmetric coverage actually ships.


Generated by Claude Code

@jensenpat jensenpat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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_tciServer is TciApplet's own back-reference field, not the removed MainWindow member.
  • Destruction order verified (the #2385 surface): in ~RadioSession the body deletes TciServer + CatPorts before any member destructs, and m_radioModel is 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.

@jensenpat jensenpat merged commit 2e3c6cd into main Jun 13, 2026
6 checks passed
@jensenpat jensenpat deleted the refactor/3351-session-v2-ownership branch June 13, 2026 04:29
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…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
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.

2 participants