Fix hard crash when toggling ASIO while sending a specific app
Local checkpoint - NOT for public release. Repro (Ed): in applications send mode with an app being captured, turning the ASIO driver off hard-crashed the process. No log and no crash-report file were written - and MixingEngine.DisposeEntry swallows managed exceptions - which points to a native access violation, not a managed throw. Cause: the ASIO toggle rebuilds the capture backend (ApplyAsioMode -> ApplyAudioRuntime -> ApplySendSources), which disposes the live ProcessLoopbackCapture. The old design released the WASAPI COM objects from the disposing thread while the capture thread could still be inside a native GetBuffer call - a classic use-after-free / AV. Fix: the capture thread now owns the ENTIRE COM lifecycle. It activates, runs, and releases every COM object itself, in its finally, only after the loop has exited. StopRecording/Dispose merely signal and join (2s) - they never touch the COM objects. If the thread ever wedges in a native call we leak it rather than free from outside (a rare bounded leak beats a hard crash). The thread is also explicitly MTA, and activation moved onto it, so the async-activation callback can't stall the UI thread. bufferReady is volatile and only disposed once the thread has genuinely exited. Also: ApplyAsioMode force-sets the WASAPI send-list visibility for the new mode, which resurrected the loopback-outputs list in applications mode; re-assert ApplySendModeVisibility at the end so the correct list stays shown after an ASIO toggle. New self-test "Per-application capture lifecycle" runs real start/stop/dispose cycles of the native capture against our own process on hardware - a bad teardown would AV and fail the gate. Gate: 16/16. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f8a42f2806
commit
ffeba4ad2b
@@ -6566,6 +6566,10 @@ public sealed class MainForm : Form
|
||||
// to re-tick to get audio back.
|
||||
ApplyAudioRuntime();
|
||||
ApplyReceiveDevices();
|
||||
// The block above force-set the WASAPI send-list visibility for the new mode. Re-assert the
|
||||
// per-application send view on top so the loopback-outputs list stays hidden (and the app list
|
||||
// shown) when we're in applications mode — ASIO toggling must not resurrect the wrong list.
|
||||
ApplySendModeVisibility();
|
||||
if (wipedSomething) logFile.Event($"audio mode change wiped now-hidden device ticks");
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ internal static class SelfTest
|
||||
RunStep(results, "Per-peer shaping DSP", PeerShapingDsp);
|
||||
RunStep(results, "Multi-output fan-out (both lanes)", FanOutToBothOutputs);
|
||||
RunStep(results, "Per-application send enumeration", AppSendEnumeration);
|
||||
RunStep(results, "Per-application capture lifecycle", AppSendCaptureLifecycle);
|
||||
RunStep(results, "v5 settings and shaping round-trip", V5ConfigRoundTrip);
|
||||
RunStep(results, "Profile save and reload", ProfileRoundTrip);
|
||||
RunStep(results, "What's-new update marker", WhatsNewMarkerRoundTrip);
|
||||
@@ -284,6 +285,31 @@ internal static class SelfTest
|
||||
return $"enumerated {apps.Count} app(s); process-loopback supported={supported}";
|
||||
}
|
||||
|
||||
/// <summary>Exercises the process-loopback capture's real start → run → teardown cycle several times
|
||||
/// against our OWN process, on hardware. This is the regression guard for the ASIO-toggle hard crash:
|
||||
/// a bad COM teardown (releasing objects from the wrong thread / mid-native-call) would take the whole
|
||||
/// test process down with an access violation, failing the gate. SKIP on Windows too old to support
|
||||
/// process loopback.</summary>
|
||||
private static string? AppSendCaptureLifecycle()
|
||||
{
|
||||
if (!RemSound.Sender.ProcessLoopbackCapture.IsSupported)
|
||||
return Skip("process loopback needs Windows 10 build 19041+");
|
||||
|
||||
var pid = Process.GetCurrentProcess().Id;
|
||||
var cycles = 0;
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
var capture = new RemSound.Sender.ProcessLoopbackCapture(pid);
|
||||
var frames = 0L;
|
||||
capture.DataAvailable += (_, e) => Interlocked.Add(ref frames, e.BytesRecorded);
|
||||
capture.StartRecording();
|
||||
Thread.Sleep(150); // let activation + the capture loop run and then be torn down
|
||||
capture.Dispose(); // teardown while the capture thread is live — the crash scenario
|
||||
cycles++;
|
||||
}
|
||||
return $"ran {cycles} start/stop/dispose cycles on pid {pid} with no crash";
|
||||
}
|
||||
|
||||
/// <summary>The v5 machine-wide settings and per-peer shaping survive a JSON save/reload: new
|
||||
/// AppConfig defaults, the named-peers book, the main tab order, per-peer shaping with parametric
|
||||
/// bands, and the new recording default. All in-memory — the real config/profiles aren't touched.</summary>
|
||||
|
||||
Reference in New Issue
Block a user