Fix the actual Win7 launch crash (issue #22): empty send-mode list
The real cause — from the tester's crash stack, not my earlier (wrong)
System.ServiceProcess theory: ApplySendModeVisibility() is called early in the
MainForm constructor (via ApplyAsioMode), BEFORE the Input/Output tab populates
the send-mode ListBox. On Windows 7 process-loopback is unsupported, so it took
the `!supported` branch and set SelectedIndex on an EMPTY list, throwing
ArgumentOutOfRangeException ("value ('0') must be less than '0'") and crashing
at launch. On Windows 10/11 that branch is skipped (process-loopback IS
supported), which hid it from the gate and from every Win10/11 test.
.NET 10 genuinely runs on the tester's Win7 SP1 box (CoreLib 10.0.826 in the
crash) — so the earlier assembly-load theory was wrong; those Win7 changes were
addressing a non-problem. This is the fault.
Fix: guard the SelectedIndex set with `sendModeList.Items.Count > 0`. When the
list is empty there's nothing to reset (it's created selecting Devices, and this
runs again once the tab is built).
Test: "Main window builds where process-loopback is unsupported (issue #22)" —
forces ProcessLoopbackCapture.IsSupported = false (new ForceSupportedForTest
seam) and constructs the headless MainForm, so a Win10/11 box exercises the Win7
path. VERIFIED it reproduces: with the guard reverted the test fails with the
exact tester exception; with the guard it passes. Gate 34/34.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
0567ea22c3
commit
c482857314
@@ -3621,7 +3621,13 @@ public sealed class MainForm : Form
|
|||||||
// The chooser row only makes sense where applications mode is possible.
|
// The chooser row only makes sense where applications mode is possible.
|
||||||
if (sendModeLabel is not null) sendModeLabel.Visible = supported;
|
if (sendModeLabel is not null) sendModeLabel.Visible = supported;
|
||||||
SetRowControlVisible(sendModeList, supported);
|
SetRowControlVisible(sendModeList, supported);
|
||||||
if (!supported && sendModeList.SelectedIndex != SendModeDevicesIndex)
|
// Items.Count > 0 guard: this method is called once EARLY in the constructor (via ApplyAsioMode)
|
||||||
|
// before the Input/Output tab has populated sendModeList, so the list can still be empty here.
|
||||||
|
// Setting SelectedIndex on an empty ListBox throws ArgumentOutOfRangeException. On Windows 10+ the
|
||||||
|
// branch is skipped anyway (process-loopback IS supported), which hid the bug — but on Windows 7
|
||||||
|
// (unsupported) it crashed the app at launch (issue #22). When the list is empty there's nothing to
|
||||||
|
// reset; it's created selecting Devices, and this runs again (line ~3611) once the list is built.
|
||||||
|
if (!supported && sendModeList.Items.Count > 0 && sendModeList.SelectedIndex != SendModeDevicesIndex)
|
||||||
{
|
{
|
||||||
suppressSendAppEvents = true;
|
suppressSendAppEvents = true;
|
||||||
sendModeList.SelectedIndex = SendModeDevicesIndex;
|
sendModeList.SelectedIndex = SendModeDevicesIndex;
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ internal static class SelfTest
|
|||||||
RunStep(results, "Auto-save non-read-only profiles (options + guard + silent timer)", AutoSaveNonReadOnlyProfiles);
|
RunStep(results, "Auto-save non-read-only profiles (options + guard + silent timer)", AutoSaveNonReadOnlyProfiles);
|
||||||
RunStep(results, "Service verb gate (normal launch stays load-safe)", ServiceVerbGate);
|
RunStep(results, "Service verb gate (normal launch stays load-safe)", ServiceVerbGate);
|
||||||
RunStep(results, "Main window builds without loading the service assembly (Win7-safe)", MainWindowServiceAssemblyFree);
|
RunStep(results, "Main window builds without loading the service assembly (Win7-safe)", MainWindowServiceAssemblyFree);
|
||||||
|
RunStep(results, "Main window builds where process-loopback is unsupported (Win7 launch, issue #22)", Win7SendModeConstruction);
|
||||||
RunStep(results, "Menu shortcuts don't clash with controls", MenuShortcutsDontClashWithControls);
|
RunStep(results, "Menu shortcuts don't clash with controls", MenuShortcutsDontClashWithControls);
|
||||||
RunStep(results, "Service log discovery (newest activity log)", ServiceLogDiscovery);
|
RunStep(results, "Service log discovery (newest activity log)", ServiceLogDiscovery);
|
||||||
|
|
||||||
@@ -1449,6 +1450,31 @@ internal static class SelfTest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Issue #22: on Windows 7 process-loopback is unsupported, so ApplySendModeVisibility — called
|
||||||
|
/// early in the constructor (via ApplyAsioMode), BEFORE the Input/Output tab populates the send-mode
|
||||||
|
/// list — set SelectedIndex on an EMPTY ListBox and threw ArgumentOutOfRangeException, crashing the app
|
||||||
|
/// at launch. On Windows 10/11 the branch is skipped (process-loopback IS supported), which hid the bug
|
||||||
|
/// from the gate. This forces the unsupported path so the crash is reproduced (and now prevented) on a
|
||||||
|
/// Win10/11 test box.</summary>
|
||||||
|
private static string? Win7SendModeConstruction()
|
||||||
|
{
|
||||||
|
var prev = RemSound.Sender.ProcessLoopbackCapture.ForceSupportedForTest;
|
||||||
|
RemSound.Sender.ProcessLoopbackCapture.ForceSupportedForTest = false; // pretend we're on Windows 7
|
||||||
|
try
|
||||||
|
{
|
||||||
|
MainForm? mf = null;
|
||||||
|
Exception? ctorEx = null;
|
||||||
|
try { mf = new MainForm(null, RemSound.Core.Profile.NewBlank(), null, null, headless: true); }
|
||||||
|
catch (Exception ex) { ctorEx = ex; }
|
||||||
|
finally { mf?.Dispose(); }
|
||||||
|
|
||||||
|
Check(ctorEx is null,
|
||||||
|
$"constructing the main window with process-loopback unsupported (the Win7 path) must not throw — got {ctorEx?.GetType().Name}: {ctorEx?.Message}");
|
||||||
|
return "the main window constructs cleanly with process-loopback unsupported (Win7 send-mode path)";
|
||||||
|
}
|
||||||
|
finally { RemSound.Sender.ProcessLoopbackCapture.ForceSupportedForTest = prev; }
|
||||||
|
}
|
||||||
|
|
||||||
private static int FreeUdpPort()
|
private static int FreeUdpPort()
|
||||||
{
|
{
|
||||||
using var s = new System.Net.Sockets.Socket(System.Net.Sockets.AddressFamily.InterNetwork,
|
using var s = new System.Net.Sockets.Socket(System.Net.Sockets.AddressFamily.InterNetwork,
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ using System.Threading;
|
|||||||
using NAudio.CoreAudioApi;
|
using NAudio.CoreAudioApi;
|
||||||
using NAudio.Wave;
|
using NAudio.Wave;
|
||||||
|
|
||||||
|
// Lets the in-app self-tests (assembly "RemSound") flip ForceSupportedForTest to exercise the Win7 paths.
|
||||||
|
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("RemSound")]
|
||||||
|
|
||||||
namespace RemSound.Sender;
|
namespace RemSound.Sender;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -59,9 +62,14 @@ public sealed class ProcessLoopbackCapture : IWaveIn
|
|||||||
includeTree = includeProcessTree;
|
includeTree = includeProcessTree;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Test-only override: when set, <see cref="IsSupported"/> returns this instead of the real OS
|
||||||
|
/// check, so a self-test running on Windows 10/11 can exercise the Windows-7 (unsupported) code paths —
|
||||||
|
/// e.g. the send-mode UI that crashed at launch on Win7 (issue #22). Null = use the real OS check.</summary>
|
||||||
|
internal static bool? ForceSupportedForTest;
|
||||||
|
|
||||||
/// <summary>True on Windows builds new enough for the process-loopback API (10.0.19041+).</summary>
|
/// <summary>True on Windows builds new enough for the process-loopback API (10.0.19041+).</summary>
|
||||||
public static bool IsSupported =>
|
public static bool IsSupported =>
|
||||||
OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041);
|
ForceSupportedForTest ?? OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041);
|
||||||
|
|
||||||
public void StartRecording()
|
public void StartRecording()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user