mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-09-11 03:04:10 +00:00
fix(audio): run device lifecycle ops on the JUCE message thread (Windows)
Tester crash (dump 2026-07-15, Focusrite USB ASIO): the driver's deferred kAsioResetRequest fires a juce::Timer on the addon's JUCE message thread (ASIOAudioIODevice::timerCallback -> reloadChannelNames) while the Node thread concurrently destroys the device inside setAudioDevices/stopAudio — use-after-free, ~5 minutes after every launch. New runDeviceLifecycleOp() marshals every binding that can create or destroy a juce::AudioIODevice (setDevice, device-type switches, start/stop, stream output open/close, extra-input bind/unbind, add/removeSource) onto the message thread on Windows, serialising them with those timers. Inline on macOS (dispatch already inline) and Linux (ALSA main-thread contract unchanged), and inline when already on the message thread to avoid self-deadlock. Closures capture by value and return through shared_ptr so a timed-out dispatch that runs late can't touch the caller's dead stack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4dc68a2a28
commit
354052bc7c
@@ -54,6 +54,44 @@ inline bool dispatchOnMessageThread(Func&& func)
|
||||
return dispatchOnMessageThreadImpl(std::function<void()>(std::forward<Func>(func)));
|
||||
}
|
||||
|
||||
// Run a device-lifecycle mutation (anything that can create or destroy a
|
||||
// juce::AudioIODevice: setAudioDevices, start/stopAudio, device-type
|
||||
// switches, stream-output open/close, extra-input add/remove) on the JUCE
|
||||
// message thread.
|
||||
//
|
||||
// Why: on Windows, ASIO devices arm juce::Timers (the driver's deferred
|
||||
// kAsioResetRequest, device-change detection) that fire on the message
|
||||
// thread. Destroying the device from the Node thread while such a timer is
|
||||
// queued or mid-callback is a use-after-free — tester crash 2026-07-15:
|
||||
// ASIOAudioIODevice::timerCallback → reloadChannelNames on a freed device,
|
||||
// ~5 min after start, every run (Focusrite USB ASIO reset request).
|
||||
// Hopping the mutation onto the message thread serialises it with those
|
||||
// timer callbacks, so a timer can never observe a half-destroyed device.
|
||||
//
|
||||
// Windows-only hop, by design: macOS's dispatchOnMessageThread already runs
|
||||
// inline (no separate pump), and Linux/ALSA keeps its long-standing
|
||||
// "called from the Node main thread" contract untouched. Inline when the
|
||||
// caller already IS the message thread (engine init runs there), because
|
||||
// dispatch-and-wait from the message thread would deadlock.
|
||||
//
|
||||
// Returns false when the dispatched work did not verifiably complete (post
|
||||
// refused or 15 s timeout) — same contract as dispatchOnMessageThread.
|
||||
// CAPTURE RULE: on timeout the queued closure may still run later, so the
|
||||
// closure must own everything it touches — capture by value (engine
|
||||
// snapshot, args) and write results through a shared_ptr, never through
|
||||
// references to the caller's stack.
|
||||
template <typename Func>
|
||||
inline bool runDeviceLifecycleOp(Func&& func)
|
||||
{
|
||||
#if JUCE_WINDOWS
|
||||
if (auto* mm = juce::MessageManager::getInstanceWithoutCreating())
|
||||
if (!mm->isThisTheMessageThread())
|
||||
return dispatchOnMessageThread(std::forward<Func>(func));
|
||||
#endif
|
||||
func();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pending-async-load registry: LoadVSTWorker / LoadPresetWorker block on a
|
||||
// WaitableEvent until the message-thread continuation fires; doShutdown
|
||||
// signals every registered event so no worker waits forever once the pump
|
||||
|
||||
Reference in New Issue
Block a user