Skip to content

fix(gui): let the socket layer through, and guard the views it drives - #869

Merged
got3nks merged 1 commit into
amule-org:masterfrom
got3nks:fix/notify-always-vs-gui
Aug 9, 2026
Merged

fix(gui): let the socket layer through, and guard the views it drives#869
got3nks merged 1 commit into
amule-org:masterfrom
got3nks:fix/notify-always-vs-gui

Conversation

@got3nks

@got3nks got3nks commented Aug 9, 2026

Copy link
Copy Markdown

Repairs a regression I introduced in #865, and then fixes the rest of the bug that PR was aiming at.

What #865 broke

It guarded CamuleRemoteGuiApp::OnNotifyEvent on the dialog. That is right for notifications that update the GUI and wrong for the ones carrying the EC socket: MuleNotify has two classes, and DoNotifyAlways() exists precisely because its callers must run whatever the GUI is doing. CoreNotify_LibSocketConnect and friends are how an asio callback reaches the main thread, and amulegui has no dialog until an EC connection has been made — so dropping them meant the connection could never complete, and every attempt ended at the 15-second watchdog.

The distinction now travels with the event: HandleNotificationAlways() tags the CMuleGUIEvent, Clone() carries the tag, and both dispatchers run a tagged event regardless of the dialog. The monolithic dispatcher gets the same treatment — it has always dropped socket notifications when amuledlg was null, far less exposed there since the dialog exists before networking starts, but wrong in the same way during shutdown.

The exemption wasn't enough on its own

Two GUI notifications are queued through DoNotifyAlways() as well — DownloadCtrlSort and DownloadCtrlDoItemSelectionChanged — so the exemption reached them and the original crash came straight back. Both read theApp->amuledlg->m_transferwnd, which is the 0x3c0 in the crash reports. They now test the dialog themselves. The nine socket handlers sharing that path touch no GUI at all — checked individually rather than assumed, which is the mistake that produced this iteration.

The other half of the bug

Guarding the notification path was never going to be sufficient. The remote GUI's container layer calls into the views directly from EC replies, bypassing MuleNotify entirely, and a reply can land while the dialog is gone for the same reason: it is destroyed when the link drops and rebuilt by Startup() after the next connect, with replies in flight not drained.

An audit of the tree found 26 such calls across 11 reply-driven handlers in 8 classesHandlePacket, ProcessUpdate, ProcessItemUpdate, ProcessItemUpdatePartfile, CreateItem, DeleteItem, ApplySearchProgress. CKnownFilesRem::ProcessItemUpdatePartfile calls m_transferwnd->downloadlistctrl->UpdateItem(), so a crash there is indistinguishable from the notification one in a report — same offset, same signature.

Each is guarded so the container still updates its model and only the view call is skipped; the model has to keep tracking the daemon whether or not anyone is looking at it. Where a flag already gated the work (batchDownloadList, batchSharedList) the test folded into it rather than nesting another branch.

What the audit cleared

Not exposed, with reasons rather than assumption:

  • The wxDataView viewsDownloadListCtrl, SearchListCtrl, SharedFilesCtrl, GenericClientListCtrl, FriendListCtrl, SearchListModel. A control is a child of the dialog and cannot outlive it. The port did not introduce this class of bug.
  • MuleTrayIconDlgShutDown() runs RemoveSystray() before the dialog is destroyed.
  • Dialogs (PrefsUnifiedDlg, CatDialog, ChatWnd, TransferWnd) — user-interaction entry points; the dialog exists by construction.
  • Core sources (BaseClient.cpp, amule.cpp) — monolithic only, where the dialog is built before networking starts.

Guard detection was function-scoped rather than a line window, which is what separated 223 raw matches from the 26 real ones.

Verification

Builds clean on macOS (monolithic + amulegui + amuled), no warnings from the touched files; clang-format and both clang-tidy tiers clean over the diff.

Connecting and quitting have each failed once during this work, so both want exercising against a live core, along with a core restart mid-transfer — that is when ProcessUpdate and ProcessItemUpdatePartfile are busiest.

amule-project#865 stopped amulegui connecting. It guarded CamuleRemoteGuiApp::OnNotifyEvent
on the dialog, which is right for the notifications that update the GUI and
wrong for the ones that carry the EC socket: MuleNotify has two classes, and
DoNotifyAlways() exists precisely because its callers must run whatever the
GUI is doing. CoreNotify_LibSocketConnect and friends are how an asio callback
reaches the main thread, and amulegui has no dialog until an EC connection has
been made -- so dropping them meant the connection could never complete and
every attempt ended at the 15 s watchdog.

The distinction now travels with the event. HandleNotificationAlways() tags the
CMuleGUIEvent it queues, Clone() carries the tag, and both dispatchers run a
tagged event regardless of the dialog. The monolithic one gets the same
treatment: it has always dropped socket notifications when amuledlg was null,
which is far less exposed there (the dialog exists before networking starts)
but is wrong in the same way during shutdown.

Two GUI notifications are queued through DoNotifyAlways() as well --
DownloadCtrlSort and DownloadCtrlDoItemSelectionChanged -- so the exemption
reached them too and the original crash came straight back. Both read
theApp->amuledlg->m_transferwnd, which is the 0x3c0 in the reports. They now
test the dialog themselves; the nine socket handlers on that path touch no GUI
at all, checked one by one rather than assumed.

That is still only one of the two ways a view is reached with no dialog. The
remote GUI's container layer calls into the views directly from EC replies,
bypassing MuleNotify entirely, and a reply can land while the dialog is gone
for the same reason: it is destroyed when the link drops and rebuilt by
Startup() after the next connect, and replies in flight are not drained. An
audit of the whole tree found 26 such calls across 11 reply-driven handlers in
8 classes -- HandlePacket, ProcessUpdate, ProcessItemUpdate,
ProcessItemUpdatePartfile, CreateItem, DeleteItem, ApplySearchProgress. Each is
guarded so the container still updates its model and only the view call is
skipped; the model has to keep tracking the daemon whether or not anyone is
looking at it. Where a flag already gated the work (batchDownloadList,
batchSharedList) the test folded into it rather than nesting another branch.

The views themselves are not exposed. A control is a child of the dialog and
cannot outlive it, so the derefs in DownloadListCtrl, SearchListCtrl,
SharedFilesCtrl, GenericClientListCtrl and FriendListCtrl are safe, and the
wxDataView port did not introduce this. Nor are the dialogs, the tray icon
(RemoveSystray() runs before the dialog is destroyed) or the core sources.
@got3nks
got3nks merged commit 66b9e87 into amule-org:master Aug 9, 2026
15 checks passed
@got3nks
got3nks deleted the fix/notify-always-vs-gui branch August 9, 2026 17:35
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.

1 participant