[codex] Harden automation popup handling#3728
Conversation
There was a problem hiding this comment.
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
QLocalSocketlifetime withQPointer, and avoid holding buffer references across request handling inonReadyRead(). - Extend
dumpTreeto serializeQMenuactions as synthetic nodes (with metadata + geometry when visible). - Allow
invoketo resolve and trigger visible popupQActions 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. |
| while (sock && m_buffers.contains(sock)) { | ||
| QByteArray line; | ||
| { | ||
| QByteArray& buf = m_buffers[sock]; | ||
| const int nl = buf.indexOf('\n'); |
| if (actionMatchesTarget(action, target)) { | ||
| return {action, menu}; | ||
| } | ||
| if (QMenu* submenu = action->menu()) { | ||
| const ResolvedAction match = matchMenuAction(submenu, target); | ||
| if (match.action) { | ||
| return match; | ||
| } | ||
| } |
| | `setCurrentIndex` | `QComboBox` | integer index | | ||
| | `trigger` / `click` | visible `QMenu` `QAction` | — | | ||
| | `setChecked` | checkable visible `QMenu` `QAction` | `true`/`false`/`on`/`off`/`1`/`0` | |
|
Addressed the three Copilot review notes in
Local validation after the follow-up: |
There was a problem hiding this comment.
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
-
operator[]re-insertion inonReadyRead— false positive against the current head. The new code usesm_buffers.find(socket)with iterators throughout and returns onend(); there's nooperator[]access left on the hot path, so no silent empty-buffer re-insertion. This is exactly the fix Copilot recommended. -
matchMenuActionrecursing into non-visible submenus — already restricted. The recursion is guarded:if (QMenu* submenu = action->menu(); submenu && submenu->isVisible()). Top-levelmatchActionRecursivealso gates onmenu->isVisible(), so the "visible popup" contract holds. -
toggleundocumented — it is documented.docs/automation-bridge.mdadds the row| trigger / click / toggle | visible QMenu QAction | — |to the invoke table, matching the code's accepted aliases.
Two minor, non-blocking observations
-
setCheckedon aQActionemitstoggled(bool), nottriggered(). Handlers wired toQAction::triggered(common for menu items) won't fire on thesetCheckedpath the way they do fortrigger/click. That's a reasonable distinction (set-state vs. activate), but worth a one-line doc note so a driver knowssetCheckedmay not run the same slot astriggerfor some actions. -
Each
QActionis emitted into bothactionsand syntheticchildrenon aQMenunode — intentional and documented, just duplicates payload indumpTree. 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
132b85d to
820ead8
Compare
ten9876
left a comment
There was a problem hiding this comment.
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 forlog 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.)
Summary
Fixes the automation bridge crash path where a client can disconnect while an
invokerequest is blocked inside nested Qt UI work such asQMenu::exec(). The server now guards sockets withQPointer, avoids holding buffer references across request handling, and drops responses when the originating client disappears.Popup menus are also easier to inspect and drive:
dumpTreenow exposesQMenuQActionentries, andinvokecan 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
cmake --build build)cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfocmake --build build --target AetherSDR --parallelinvokeis blocked in nested popup/menu UI work. The fix drops stale responses afterhandleLine()if the socket disconnected.Additional local checks:
git diff --checkAETHER_AUTOMATION=1:tools/automation_probe.py pingreturned ok; compactdumpTreecheck 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/AetherSDRChecklist
docs/COMMIT-SIGNING.md)AppSettingscalls - use nested-JSON-under-one-key(Principle V)
reverse-engineered from a proprietary binary (Principle IV)
MeterSmoother(AGENTS.md convention)