mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-09-11 03:44:10 +00:00
fix(audio): P0 lifecycle executor — serialized ops, generation guard, module pinning
Three identical field crash dumps (2026-07-17, CFG fail-fast 0xc0000409 param 0xa = GUARD_ICALL_CHECK_FAILURE on the JUCE message thread, plugin WndProc frame on the stack) traced to the dispatch timeout desync: every lifecycle call site could give up after 15 s while its op was still queued, leaving two owners mutating engine/plugin lifecycle state with no ordering authority. - New LifecycleExecutor (guide §12 P0): named lifecycle ops, strictly FIFO on the message thread, engine-generation stamped at submit and no-oped when stale. The wait is unbounded with a 15 s watchdog that reports and keeps waiting; false now always means "verifiably did not run" — the "false but it may still run later" state is gone. - initialize/doShutdown bump the generation and run as executor ops; shutdown is the one bounded caller (60 s, then leak the pump — beats hanging process exit behind a wedged driver call). - runDeviceLifecycleOp and closeAllPluginEditorWindows route through the executor; the editor-teardown 15 s give-up (delayed #56 UAF) is removed. - pinPluginModuleForever: plugin modules are pinned on load so JUCE's refcount can never unload them mid-session — a queued window/timer message into an unloaded module is the CFG fail-fast in the dumps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
62dbf2c50e
commit
1359c7c24d
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "../AudioEngine.h"
|
||||
#include "../VSTHost.h"
|
||||
#include "LifecycleExecutor.h"
|
||||
|
||||
#include <juce_events/juce_events.h>
|
||||
|
||||
@@ -74,36 +75,26 @@ inline bool dispatchOnMessageThread(Func&& func)
|
||||
// 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.
|
||||
//
|
||||
// KNOWN LIMITATION (timeout desync): when the 15 s wait expires the binding
|
||||
// reports failure to JS, but the closure may still complete afterwards —
|
||||
// e.g. addSource returns -1 yet the source gets created, holding its input
|
||||
// device until the next reconfigure. Memory-safe by the capture rule above,
|
||||
// just not logically reconciled; only reachable with the message thread
|
||||
// jammed > 15 s. Callers that care can re-query engine state (listSources,
|
||||
// getCurrentDevice) after a reported failure.
|
||||
// P0 (guide §12): routed through the LifecycleExecutor. False now means the
|
||||
// op verifiably did NOT run (pump gone, or the engine generation moved while
|
||||
// it was queued) — the old "false but it may still run later" desync state
|
||||
// is gone. The wait is unbounded; a watchdog logs every 15 s while a driver
|
||||
// call blocks the message thread.
|
||||
// CAPTURE RULE unchanged: 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
|
||||
// No MessageManager means the pump is gone (pre-init or mid-shutdown):
|
||||
// report failure instead of running the mutation unserialised on the
|
||||
// caller's thread — that would reintroduce the race this helper exists
|
||||
// to prevent (CodeRabbit #113 review).
|
||||
auto* mm = juce::MessageManager::getInstanceWithoutCreating();
|
||||
if (mm == nullptr)
|
||||
return false;
|
||||
if (!mm->isThisTheMessageThread())
|
||||
return dispatchOnMessageThread(std::forward<Func>(func));
|
||||
#endif
|
||||
return runLifecycleOpOk("device-lifecycle",
|
||||
std::function<void()>(std::forward<Func>(func)));
|
||||
#else
|
||||
// macOS: dispatch runs inline (no separate pump). Linux/ALSA keeps its
|
||||
// long-standing "called from the Node main thread" contract untouched.
|
||||
func();
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Pending-async-load registry: LoadVSTWorker / LoadPresetWorker block on a
|
||||
|
||||
Reference in New Issue
Block a user