Fix UPnP close-hang, and add mnemonics to every dialog's buttons (Andre)
UPnP close hang: - RouterPortMapper.Stop()/Dispose() does a SYNCHRONOUS DeletePortMap call to the router (plus StopDiscovery). When the router is slow or unresponsive that blocks — and it ran on the UI thread in FormClosing, so enabling UPnP could make RemSound impossible to close (Andre, this morning; his logs show UPnP teardown taking several seconds even on a good run). Moved the router teardown into the same bounded background-task pattern already used for the ASIO audio dispose: UPnP + audio now tear down off the UI thread, in parallel, under one 3s cap. Anything unfinished is reclaimed on process exit, so the window always closes. Mnemonics on dialog buttons: - Andre flagged the "RemSound is already running" dialog (single-instance) having no shortcut keys. Its three TaskDialog buttons had no & mnemonics; added Alt+S / Alt+F / Alt+C. - Swept every dialog. Custom TaskDialog buttons and Form OK/Cancel/Close buttons that lacked mnemonics now have them: single-instance (Switch/Force/Cancel), save-onto-read-only (Save/Cancel), Add EQ band, Change/Manage profile password, Quick profile switch (Close), Rename peer (OK), About (Close). Cancel stays on Esc in the few dialogs where Alt+C is already taken (Rename peer's Clear, Service profile's peers list) — by design, not a miss. - The rest already had mnemonics (Keyboard-shortcut import, Update-install notice, Profile selection, Recording settings). The dialog-accessibility gate (mnemonic uniqueness + names) stays green, so nothing collides. Gate 46/46. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a433645fd4
commit
8fe52047be
@@ -1351,7 +1351,7 @@ internal sealed class AboutDialog : Form
|
|||||||
|
|
||||||
var closeButton = new Button
|
var closeButton = new Button
|
||||||
{
|
{
|
||||||
Text = "Close",
|
Text = "&Close",
|
||||||
AutoSize = true,
|
AutoSize = true,
|
||||||
DialogResult = DialogResult.OK,
|
DialogResult = DialogResult.OK,
|
||||||
TabIndex = 1,
|
TabIndex = 1,
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ internal sealed class AddBandDialog : Form
|
|||||||
AddRow(grid, 2, "&End frequency in Hz (Alt+E)", endFreq);
|
AddRow(grid, 2, "&End frequency in Hz (Alt+E)", endFreq);
|
||||||
AddRow(grid, 3, "&Gain in dB (Alt+G)", gainDb);
|
AddRow(grid, 3, "&Gain in dB (Alt+G)", gainDb);
|
||||||
|
|
||||||
var okButton = new Button { Text = "OK", AutoSize = true, DialogResult = DialogResult.None, TabIndex = 0 };
|
var okButton = new Button { Text = "&OK", AutoSize = true, DialogResult = DialogResult.None, TabIndex = 0 };
|
||||||
var cancelButton = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel, TabIndex = 1 };
|
var cancelButton = new Button { Text = "&Cancel", AutoSize = true, DialogResult = DialogResult.Cancel, TabIndex = 1 };
|
||||||
okButton.Click += (_, _) => TryAccept();
|
okButton.Click += (_, _) => TryAccept();
|
||||||
|
|
||||||
// OK before Cancel, in both tab order and left-to-right layout.
|
// OK before Cancel, in both tab order and left-to-right layout.
|
||||||
|
|||||||
@@ -1434,7 +1434,10 @@ public sealed class MainForm : Form
|
|||||||
try { processSelfMeter.Dispose(); } catch { }
|
try { processSelfMeter.Dispose(); } catch { }
|
||||||
try { deviceChangeNotifier?.Dispose(); } catch { }
|
try { deviceChangeNotifier?.Dispose(); } catch { }
|
||||||
try { powerResumeHandler?.Dispose(); } catch { }
|
try { powerResumeHandler?.Dispose(); } catch { }
|
||||||
try { routerPortMapper?.Dispose(); } catch { }
|
// NOTE: routerPortMapper.Dispose() is NOT called here — it's moved into the bounded
|
||||||
|
// background teardown below. Its Stop() does a SYNCHRONOUS DeletePortMap call to the router,
|
||||||
|
// which blocks (or hangs) when the router is slow/unresponsive; on the UI thread that froze the
|
||||||
|
// close, so enabling UPnP made RemSound impossible to shut (Andre, 2026-07-18).
|
||||||
// Reverse every Win32 lever PerformanceMode applied. The kernel would clean
|
// Reverse every Win32 lever PerformanceMode applied. The kernel would clean
|
||||||
// these up on process exit anyway, but doing it explicitly releases the power
|
// these up on process exit anyway, but doing it explicitly releases the power
|
||||||
// request handle and matches our timeBeginPeriod with a timeEndPeriod.
|
// request handle and matches our timeBeginPeriod with a timeEndPeriod.
|
||||||
@@ -1449,14 +1452,22 @@ public sealed class MainForm : Form
|
|||||||
// the form-close path run; the OS reclaims any audio resources on process exit.
|
// the form-close path run; the OS reclaims any audio resources on process exit.
|
||||||
// Worst case the user sees a brief tray-icon stutter; before this they saw a
|
// Worst case the user sees a brief tray-icon stutter; before this they saw a
|
||||||
// ~16 s frozen window before the form went away.
|
// ~16 s frozen window before the form went away.
|
||||||
var audioDispose = Task.Run(() =>
|
// The UPnP router teardown (DeletePortMap — a synchronous call to a possibly-unresponsive
|
||||||
|
// router) belongs here too: like the ASIO dispose it can block for many seconds, and on the UI
|
||||||
|
// thread it froze the close. Run both OFF the UI thread, in parallel, under one hard timeout.
|
||||||
|
var slowTeardown = Task.Run(() =>
|
||||||
{
|
{
|
||||||
try { sender.Dispose(); } catch { /* ignore */ }
|
var upnp = Task.Run(() => { try { routerPortMapper?.Dispose(); } catch { /* ignore */ } });
|
||||||
try { receiver.Dispose(); } catch { /* ignore */ }
|
var audio = Task.Run(() =>
|
||||||
|
{
|
||||||
|
try { sender.Dispose(); } catch { /* ignore */ }
|
||||||
|
try { receiver.Dispose(); } catch { /* ignore */ }
|
||||||
|
});
|
||||||
|
try { Task.WaitAll(upnp, audio); } catch { /* ignore */ }
|
||||||
});
|
});
|
||||||
if (!audioDispose.Wait(TimeSpan.FromSeconds(2)))
|
if (!slowTeardown.Wait(TimeSpan.FromSeconds(3)))
|
||||||
{
|
{
|
||||||
try { logFile.Event("close: audio dispose taking >2s; letting process exit reclaim"); } catch { }
|
try { logFile.Event("close: UPnP/audio teardown taking >3s; letting process exit reclaim"); } catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
hotkeyController.Dispose();
|
hotkeyController.Dispose();
|
||||||
@@ -2812,8 +2823,8 @@ public sealed class MainForm : Form
|
|||||||
private bool ShowSaveOnReadOnlyWarningDialog()
|
private bool ShowSaveOnReadOnlyWarningDialog()
|
||||||
{
|
{
|
||||||
var verification = new TaskDialogVerificationCheckBox("Do not show me this message again");
|
var verification = new TaskDialogVerificationCheckBox("Do not show me this message again");
|
||||||
var saveButton = new TaskDialogButton("Save anyway");
|
var saveButton = new TaskDialogButton("&Save anyway");
|
||||||
var cancelButton = new TaskDialogButton("Cancel") { AllowCloseDialog = true };
|
var cancelButton = new TaskDialogButton("&Cancel") { AllowCloseDialog = true };
|
||||||
var page = new TaskDialogPage
|
var page = new TaskDialogPage
|
||||||
{
|
{
|
||||||
Caption = AppName,
|
Caption = AppName,
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ internal static class ProfilePasswordDialog
|
|||||||
AutoSize = true,
|
AutoSize = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
var okButton = new Button { Text = "OK", AutoSize = true };
|
var okButton = new Button { Text = "&OK", AutoSize = true };
|
||||||
var cancelButton = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel };
|
var cancelButton = new Button { Text = "&Cancel", AutoSize = true, DialogResult = DialogResult.Cancel };
|
||||||
|
|
||||||
// OK is validated by hand (no auto-close DialogResult) so we can block an empty entry when
|
// OK is validated by hand (no auto-close DialogResult) so we can block an empty entry when
|
||||||
// a password is being REQUIRED. The trigger is the OK/Enter action, not typing — and
|
// a password is being REQUIRED. The trigger is the OK/Enter action, not typing — and
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ internal static class ProfilePasswordManagerDialog
|
|||||||
rows.Add((title, current, box));
|
rows.Add((title, current, box));
|
||||||
}
|
}
|
||||||
|
|
||||||
var okButton = new Button { Text = "OK", AutoSize = true, DialogResult = DialogResult.OK };
|
var okButton = new Button { Text = "&OK", AutoSize = true, DialogResult = DialogResult.OK };
|
||||||
var cancelButton = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel };
|
var cancelButton = new Button { Text = "&Cancel", AutoSize = true, DialogResult = DialogResult.Cancel };
|
||||||
var buttons = new FlowLayoutPanel { Dock = DockStyle.Bottom, FlowDirection = FlowDirection.RightToLeft, AutoSize = true, Padding = new Padding(8) };
|
var buttons = new FlowLayoutPanel { Dock = DockStyle.Bottom, FlowDirection = FlowDirection.RightToLeft, AutoSize = true, Padding = new Padding(8) };
|
||||||
buttons.Controls.Add(okButton);
|
buttons.Controls.Add(okButton);
|
||||||
buttons.Controls.Add(cancelButton);
|
buttons.Controls.Add(cancelButton);
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ internal sealed class QuickProfileSwitchDialog
|
|||||||
AutoSize = true,
|
AutoSize = true,
|
||||||
Padding = new Padding(0, 8, 0, 0),
|
Padding = new Padding(0, 8, 0, 0),
|
||||||
};
|
};
|
||||||
var closeButton = new Button { Text = "Close", AutoSize = true, DialogResult = DialogResult.Cancel, TabIndex = 1 };
|
var closeButton = new Button { Text = "&Close", AutoSize = true, DialogResult = DialogResult.Cancel, TabIndex = 1 };
|
||||||
buttons.Controls.Add(closeButton);
|
buttons.Controls.Add(closeButton);
|
||||||
root.Controls.Add(buttons, 0, 2);
|
root.Controls.Add(buttons, 0, 2);
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ internal sealed class RenamePeerDialog : Form
|
|||||||
grid.Controls.Add(nameBox, 1, 2);
|
grid.Controls.Add(nameBox, 1, 2);
|
||||||
|
|
||||||
var clearButton = new Button { Text = "&Clear custom name (Alt+C)", AutoSize = true, AccessibleName = "Clear custom name", TabIndex = 1 };
|
var clearButton = new Button { Text = "&Clear custom name (Alt+C)", AutoSize = true, AccessibleName = "Clear custom name", TabIndex = 1 };
|
||||||
var okButton = new Button { Text = "OK", AutoSize = true, DialogResult = DialogResult.OK, TabIndex = 2 };
|
var okButton = new Button { Text = "&OK", AutoSize = true, DialogResult = DialogResult.OK, TabIndex = 2 };
|
||||||
|
// Cancel keeps Esc (it's the CancelButton) rather than a mnemonic — Alt+C is the Clear button here.
|
||||||
var cancelButton = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel, TabIndex = 3 };
|
var cancelButton = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel, TabIndex = 3 };
|
||||||
|
|
||||||
clearButton.Click += (_, _) =>
|
clearButton.Click += (_, _) =>
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ internal static class SingleInstanceDialog
|
|||||||
{
|
{
|
||||||
public static SingleInstanceDecision Ask()
|
public static SingleInstanceDecision Ask()
|
||||||
{
|
{
|
||||||
var switchButton = new TaskDialogButton("Switch to the running copy");
|
var switchButton = new TaskDialogButton("&Switch to the running copy");
|
||||||
var forceButton = new TaskDialogButton("Force the running copy to close and start fresh");
|
var forceButton = new TaskDialogButton("&Force the running copy to close and start fresh");
|
||||||
var cancelButton = new TaskDialogButton("Cancel") { AllowCloseDialog = true };
|
var cancelButton = new TaskDialogButton("&Cancel") { AllowCloseDialog = true };
|
||||||
|
|
||||||
var page = new TaskDialogPage
|
var page = new TaskDialogPage
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user