Skip to content

Dispose every dialog in a finally, not on the last line - #33

Merged
MarcelInTO merged 1 commit into
mainfrom
dialog-dispose
Aug 23, 2026
Merged

MarcelInTO merged 1 commit into
mainfrom
dialog-dispose

Conversation

@MarcelInTO

Copy link
Copy Markdown
Owner

Closes #25.

All six modal sites constructed a view, ran it, and disposed it on the last line of the method,
with nothing in between protected. If anything there threw — building the view, adding a button,
taking focus, running the modal — the Dispose was skipped and the whole view subtree leaked for
the life of the session.

Not purely theoretical. Dialog layout is known to throw in this codebase: a MessageBox dies
with ArgumentOutOfRangeException: width ('-3') when the driver has not learned the terminal
size, recorded in the PTY harness notes after it was mistaken for an app bug. A terminal that
fails to answer a size query is exactly the situation where fux would then also leak every dialog
the user opens.

method dialog
StartRename Dialog
StartInsert Dialog
StartSnippets Dialog ← the alert
StartFind Dialog
StartOpen OpenDialog
StartSaveAs SaveDialog

Code scanning raised this as cs/dispose-not-called-on-throw (alert 1050) against
StartSnippets. That alert is correct — unlike the eleven false positives dismissed alongside it
during the #17 triage — but it names one site of six, because the scanner only sees changed
lines. All six are fixed in one pass, as the issue asked: fixing only the flagged one would leave
the codebase less consistent than it was.

The convention is written down once on RunModal, where whoever adds a seventh dialog will be
looking, rather than repeated six times.

Verification

Behaviour is unchanged by construction — the results are still read into locals inside the try,
before the view is torn down. Every fixture drills to the same check count as before:
405 / 409 / 396 / 383 / 388, all PASS. That the numbers did not move is the point.

No new drill check comes with this. There is nothing observable to assert without fabricating
a throw, and a check that cannot fail would be worse than none. The fix is structural: verified
by inspection, by the build, and by CodeQL on this PR, which is where alert 1050 should clear.

🤖 Generated with Claude Code

https://claude.ai/code/session_016X4pgrsMBcsXjqWDo2gBBt

All six modal sites constructed a view, ran it, and disposed it on the last
line of the method, with nothing in between protected. If anything there threw
— building the view, adding a button, taking focus, running the modal — the
Dispose was skipped and the whole view subtree leaked for the life of the
session.

Not purely theoretical: dialog layout is known to throw here. A MessageBox
dies with ArgumentOutOfRangeException: width ('-3') when the driver has not
learned the terminal size, recorded in the PTY harness notes after it was
mistaken for an app bug. A terminal that fails to answer a size query is
exactly the situation where fux would then also leak every dialog opened.

Code scanning raised this as cs/dispose-not-called-on-throw (alert 1050)
against StartSnippets. That alert is correct — unlike the eleven false
positives dismissed alongside it during the #17 triage — but it names one site
of six, because the scanner only sees changed lines. All six are fixed in one
pass: fixing only the flagged one would leave the codebase less consistent
than it was, which is why it was left alone at the time rather than patched
piecemeal.

The convention is written down on RunModal, where whoever adds a seventh
dialog will be looking, rather than repeated six times.

Behaviour is unchanged by construction: the results are still read into locals
inside the try, before the view is torn down, and every fixture drills to the
same check count as before (405/409/396/383/388, all green). No new drill
check comes with this — there is nothing observable to assert without
fabricating a throw, and a check that cannot fail would be worse than none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X4pgrsMBcsXjqWDo2gBBt
Comment thread src/Fux/Program.cs
try
{
d.Add(new Label { X = 1, Y = 0, Text = "New name:" }, field);
d.AddButton(new Button { Text = "Cancel" }); // Result 0
Comment thread src/Fux/Program.cs
{
d.Add(new Label { X = 1, Y = 0, Text = "New name:" }, field);
d.AddButton(new Button { Text = "Cancel" }); // Result 0
d.AddButton(new Button { Text = "OK" }); // Result 1; last added = default (Enter)
Comment thread src/Fux/Program.cs
{
d.HotKeySpecifier = NoHotKey; // the title carries a node name — see NoHotKey
d.Add(new Label { X = 1, Y = 0, Text = "Name (element/attribute/PI):" }, field, kindSel, posSel);
d.AddButton(new Button { Text = "Cancel" }); // Result 0
Comment thread src/Fux/Program.cs
d.HotKeySpecifier = NoHotKey; // the title carries a node name — see NoHotKey
d.Add(new Label { X = 1, Y = 0, Text = "Name (element/attribute/PI):" }, field, kindSel, posSel);
d.AddButton(new Button { Text = "Cancel" }); // Result 0
d.AddButton(new Button { Text = "OK" }); // Result 1; last added = default (Enter)
Comment thread src/Fux/Program.cs

// Enter on the list commits, so the whole gesture is ^B, arrow, Enter — no Tab,
// no button, no mouse. Bound explicitly: 2.4.17 has no OpenSelectedItem event, and
// Accept does NOT bubble from a ListView to the dialog's default button (drilled —
Comment thread src/Fux/Program.cs
// Enter on the list commits, so the whole gesture is ^B, arrow, Enter — no Tab,
// no button, no mouse. Bound explicitly: 2.4.17 has no OpenSelectedItem event, and
// Accept does NOT bubble from a ListView to the dialog's default button (drilled —
// the assumption that it did failed the check that pins this).
Comment thread src/Fux/Program.cs
field.MoveEnd();
RunModal(ui, d);

ok = d.Result is 1;
Comment thread src/Fux/Program.cs
RunModal(ui, d);

ok = d.Result is 1;
expr = field.Text;
@MarcelInTO
MarcelInTO merged commit 7e6632b into main Aug 23, 2026
5 checks passed
@MarcelInTO
MarcelInTO deleted the dialog-dispose branch August 23, 2026 05:43
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.

Dialogs leak if anything throws between construction and Dispose (six sites)

2 participants