Skip to content

[codex] Harden automation popup handling#3728

Merged
ten9876 merged 2 commits into
aethersdr:mainfrom
rfoust:codex/automation-server-socket-menu
Jun 23, 2026
Merged

[codex] Harden automation popup handling#3728
ten9876 merged 2 commits into
aethersdr:mainfrom
rfoust:codex/automation-server-socket-menu

Conversation

@rfoust

@rfoust rfoust commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes the automation bridge crash path where a client can disconnect while an invoke request is blocked inside nested Qt UI work such as QMenu::exec(). The server now guards sockets with QPointer, avoids holding buffer references across request handling, and drops responses when the originating client disappears.

Popup menus are also easier to inspect and drive: dumpTree now exposes QMenu QAction entries, and invoke can trigger visible popup actions by label or action metadata.

Constitution principle honored

Principle VII - automation bridge requests and local socket state are boundary inputs; the server now validates socket/action lifetime before writing responses or driving visible popup actions.

Test plan

  • Local build passes (cmake --build build)
    • cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo
    • cmake --build build --target AetherSDR --parallel
  • Behavior verified on a real radio if applicable
    • Not run; no live radio/slice was available in this worktree. Offscreen automation smoke covered the bridge transport and QAction metadata path.
  • Existing tests pass (CI)
    • Local focused checks only; CI should run the full suite.
  • Reproduction steps documented if user-reported bug
    • Original crash shape: automation client disconnects while invoke is blocked in nested popup/menu UI work. The fix drops stale responses after handleLine() if the socket disconnected.

Additional local checks:

  • git diff --check
  • Offscreen automation smoke with AETHER_AUTOMATION=1: tools/automation_probe.py ping returned ok; compact dumpTree check returned {"ok": true, "roots": 39, "actions": 229}.

Manual verification launch command:

  • cd /Users/rfoust/.codex/worktrees/ffd8/AetherSDR && AETHER_AUTOMATION=1 ./build/AetherSDR.app/Contents/MacOS/AetherSDR

Checklist

  • Commits are signed (docs/COMMIT-SIGNING.md)
  • No new flat-key AppSettings calls - use nested-JSON-under-one-key
    (Principle V)
  • Code is clean-room - not decompiled, disassembled, or
    reverse-engineered from a proprietary binary (Principle IV)
  • All meter UI uses MeterSmoother (AGENTS.md convention)
  • Documentation updated if user-visible behavior changed
  • Security-sensitive changes reference a GHSA if applicable
    • Not applicable.

@rfoust rfoust self-assigned this Jun 22, 2026
@rfoust rfoust marked this pull request as ready for review June 22, 2026 01:48
@rfoust rfoust requested review from a team as code owners June 22, 2026 01:48
Copilot AI review requested due to automatic review settings June 22, 2026 01:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Hardens the in-process automation bridge (AutomationServer) against client disconnects during nested Qt event loops (e.g., QMenu::exec()), and expands the bridge’s UI introspection/drive capabilities to include popup menu actions.

Changes:

  • Guard QLocalSocket lifetime with QPointer, and avoid holding buffer references across request handling in onReadyRead().
  • Extend dumpTree to serialize QMenu actions as synthetic nodes (with metadata + geometry when visible).
  • Allow invoke to resolve and trigger visible popup QActions by label/metadata, with TX-safety guarding.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/core/AutomationServer.cpp Adds socket lifetime guards, safer request processing, and popup-action introspection + invocation.
docs/automation-bridge.md Documents new QMenu/QAction exposure in dumpTree and popup-action targeting in invoke.

Comment thread src/core/AutomationServer.cpp Outdated
Comment on lines +736 to +740
while (sock && m_buffers.contains(sock)) {
QByteArray line;
{
QByteArray& buf = m_buffers[sock];
const int nl = buf.indexOf('\n');
Comment on lines +303 to +311
if (actionMatchesTarget(action, target)) {
return {action, menu};
}
if (QMenu* submenu = action->menu()) {
const ResolvedAction match = matchMenuAction(submenu, target);
if (match.action) {
return match;
}
}
Comment thread docs/automation-bridge.md
Comment on lines 183 to +185
| `setCurrentIndex` | `QComboBox` | integer index |
| `trigger` / `click` | visible `QMenu` `QAction` | — |
| `setChecked` | checkable visible `QMenu` `QAction` | `true`/`false`/`on`/`off`/`1`/`0` |
@rfoust

rfoust commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the three Copilot review notes in 132b85d8:

  • onReadyRead() now uses m_buffers.find()/iterators through the socket read/write path, so a disconnected socket cannot reinsert a stale buffer entry.
  • Popup-action resolution now only recurses into visible submenus, preserving the visible-popup invocation contract.
  • docs/automation-bridge.md now documents toggle as a visible QMenu QAction alias.

Local validation after the follow-up: git diff --check, confirmed no remaining m_buffers[...] access, and cmake --build build --target AetherSDR --parallel. GitHub now marks the original inline anchors as outdated after the push.

@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 this, @rfoust — solid, careful work. The lifetime hardening is the right shape: QPointer-guarded socket, iterator-based buffer lookups (no operator[] re-insertion), buffer mutation scoped before handleLine(), and a re-validate (null + m_buffers.find + UnconnectedState) gate before writing the response back. That correctly covers the disconnect-during-nested-event-loop crash path against the m_buffers.remove(sock) + deleteLater() in onDisconnected. CI is green across all six checks (build, macOS, Windows, CodeQL, a11y).

On the Copilot comments — all three are already handled in this revision

  1. operator[] re-insertion in onReadyRead — false positive against the current head. The new code uses m_buffers.find(socket) with iterators throughout and returns on end(); there's no operator[] access left on the hot path, so no silent empty-buffer re-insertion. This is exactly the fix Copilot recommended.

  2. matchMenuAction recursing into non-visible submenus — already restricted. The recursion is guarded: if (QMenu* submenu = action->menu(); submenu && submenu->isVisible()). Top-level matchActionRecursive also gates on menu->isVisible(), so the "visible popup" contract holds.

  3. toggle undocumented — it is documented. docs/automation-bridge.md adds the row | trigger / click / toggle | visible QMenu QAction | — | to the invoke table, matching the code's accepted aliases.

Two minor, non-blocking observations

  • setChecked on a QAction emits toggled(bool), not triggered(). Handlers wired to QAction::triggered (common for menu items) won't fire on the setChecked path the way they do for trigger/click. That's a reasonable distinction (set-state vs. activate), but worth a one-line doc note so a driver knows setChecked may not run the same slot as trigger for some actions.

  • Each QAction is emitted into both actions and synthetic children on a QMenu node — intentional and documented, just duplicates payload in dumpTree. Fine as-is given the inspect-while-blocked use case.

I also like that isTransmitAction keeps the keyword fallback (mox/ptt/transmit/cwx) deliberately broad and logged, defaulting toward blocking — the correct fail-safe direction for Principle VI. Nice touch noting in the comment that genuine keying actions should still carry an explicit kTxKeyingProperty marker.

Nothing blocking from me. 👍


🤖 aethersdr-agent · cost: $4.9210 · model: claude-opus-4-8

@ten9876 ten9876 force-pushed the codex/automation-server-socket-menu branch from 132b85d to 820ead8 Compare June 23, 2026 02:48

@ten9876 ten9876 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.

Rebased onto current main and pushed (force-with-lease) — head 820ead81, now MERGEABLE.

It was 14 commits behind and conflicted in AutomationServer.cpp against both since-merged automation PRs (#3727, #3738). The collision was in onReadyRead(), where two changes met:

  • #3738 changed handleLine(line)handleLine(line, sock) (the socket param for log subscribe).
  • this PR added the QPointer/iterator socket-lifetime hardening + the revalidate-before-write guard.

Resolved to keep both: the final onReadyRead uses QPointer<QLocalSocket> sock, socket = sock.data() per iteration, find()/iterator buffer lookups (no operator[] re-insertion), the disconnect revalidate (!socket || find==end || UnconnectedState) before write — and the 2-arg handleLine(line, sock). Both Copilot-feedback refinements from 132b85d8 are preserved.

Verified locally: full app builds clean (AutomationServer.cpp compiles, link OK); diff vs main is +403/−14, identical footprint to the pre-rebase PR.

Re-approving (force-push reset the prior approval). Verdict unchanged: the socket-lifetime UAF fix is sound, and the QAction invoke path is TX-guarded by isTransmitAction (marker-first + narrow keyword fallback, enforced before trigger()). I'll merge once CI is green.

(Commits are committer-signed by me from the rebase; the squash-merge will be GitHub-signed.)

@ten9876 ten9876 merged commit 80a63ca into aethersdr:main Jun 23, 2026
5 checks passed
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.

3 participants