From c48285731483b6b0c819ef41fd1151dceb56257b Mon Sep 17 00:00:00 2001 From: Ednunp <29843396+Ednunp@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:18:30 +0100 Subject: [PATCH] Fix the actual Win7 launch crash (issue #22): empty send-mode list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/RemSound.App/MainForm.cs | 8 +++++- src/RemSound.App/SelfTest.cs | 26 +++++++++++++++++++ src/RemSound.Sender/ProcessLoopbackCapture.cs | 10 ++++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/RemSound.App/MainForm.cs b/src/RemSound.App/MainForm.cs index 0b6ccd0..d087e17 100644 --- a/src/RemSound.App/MainForm.cs +++ b/src/RemSound.App/MainForm.cs @@ -3621,7 +3621,13 @@ public sealed class MainForm : Form // The chooser row only makes sense where applications mode is possible. if (sendModeLabel is not null) sendModeLabel.Visible = 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; sendModeList.SelectedIndex = SendModeDevicesIndex; diff --git a/src/RemSound.App/SelfTest.cs b/src/RemSound.App/SelfTest.cs index 4e196cf..d59279b 100644 --- a/src/RemSound.App/SelfTest.cs +++ b/src/RemSound.App/SelfTest.cs @@ -82,6 +82,7 @@ internal static class SelfTest 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, "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, "Service log discovery (newest activity log)", ServiceLogDiscovery); @@ -1449,6 +1450,31 @@ internal static class SelfTest } } + /// 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. + 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() { using var s = new System.Net.Sockets.Socket(System.Net.Sockets.AddressFamily.InterNetwork, diff --git a/src/RemSound.Sender/ProcessLoopbackCapture.cs b/src/RemSound.Sender/ProcessLoopbackCapture.cs index 4ea6f44..d4b63b1 100644 --- a/src/RemSound.Sender/ProcessLoopbackCapture.cs +++ b/src/RemSound.Sender/ProcessLoopbackCapture.cs @@ -3,6 +3,9 @@ using System.Threading; using NAudio.CoreAudioApi; 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; /// @@ -59,9 +62,14 @@ public sealed class ProcessLoopbackCapture : IWaveIn includeTree = includeProcessTree; } + /// Test-only override: when set, 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. + internal static bool? ForceSupportedForTest; + /// True on Windows builds new enough for the process-loopback API (10.0.19041+). public static bool IsSupported => - OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041); + ForceSupportedForTest ?? OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041); public void StartRecording() {