mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-09-10 23:44:11 +00:00
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>
114 lines
5.1 KiB
C++
114 lines
5.1 KiB
C++
#pragma once
|
|
|
|
// AddonContext — engine/vstHost lifetime, the JUCE message thread, the
|
|
// shutdown latch, and the pending-async-load registry (TLC plan phase 6 /
|
|
// §3.1). Moved verbatim from NodeAddon.cpp; this quarantines the JUCE_MAC
|
|
// platform fork (no dispatch loop — see startJuceMessageThread) into ONE
|
|
// file instead of a branch inside every load path.
|
|
//
|
|
// engine / vstHost — shared_ptr (not unique_ptr) so worker threads can take
|
|
// a stable snapshot that keeps the object alive for the duration of their
|
|
// work, even if the message thread reassigns the global mid-operation. This
|
|
// matters most for the async VST load: createPluginInstanceAsync's JUCE
|
|
// continuation must not have VSTHost / its formatManager torn out from
|
|
// under it mid-load.
|
|
//
|
|
// Enforced rule: every *dereference* of engine / vstHost goes through a
|
|
// local snapshot (snapshotEngine / snapshotVstHost). The only code touching
|
|
// the bare globals is the snapshot helpers and the mutex-guarded writes in
|
|
// initialize / doShutdown.
|
|
|
|
#include "../AudioEngine.h"
|
|
#include "../VSTHost.h"
|
|
#include "LifecycleExecutor.h"
|
|
|
|
#include <juce_events/juce_events.h>
|
|
|
|
#include <atomic>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <mutex>
|
|
|
|
namespace slopsmith::addon {
|
|
|
|
std::shared_ptr<AudioEngine> snapshotEngine();
|
|
std::shared_ptr<VSTHost> snapshotVstHost();
|
|
|
|
// Start the pump + create engine/vstHost on the message thread (inline on
|
|
// macOS). `uiTeardownHook` runs on the message thread at the START of
|
|
// shutdown, BEFORE engine.reset() frees the processors — NodeAddon points it
|
|
// at destroyAllPluginEditorWindowsOnMessageThread (use-after-free; #56).
|
|
void initialize(std::function<void()> uiTeardownHook);
|
|
void doShutdown();
|
|
|
|
// Dispatch `func` on the JUCE message thread and wait (bounded 15 s).
|
|
// macOS: executes inline on the caller thread — no background pump exists
|
|
// (AppKit owns the real main thread; see the fork note in the .cpp).
|
|
// Returns false when the work did not complete: the post was refused
|
|
// (message queue gone — `func` will never run) or the wait timed out
|
|
// (`func` may still run later). Lifecycle callers must treat false as
|
|
// "teardown/init did not happen" rather than continuing.
|
|
bool dispatchOnMessageThreadImpl(std::function<void()> func);
|
|
template <typename Func>
|
|
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.
|
|
//
|
|
// 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
|
|
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
|
|
// WaitableEvent until the message-thread continuation fires; doShutdown
|
|
// signals every registered event so no worker waits forever once the pump
|
|
// is gone.
|
|
// Whether doShutdown has begun (acquire). The load workers gate on this
|
|
// after registering their pending event, catching the register-vs-shutdown
|
|
// race in both directions.
|
|
bool isShuttingDown();
|
|
|
|
void registerPendingLoad(std::shared_ptr<juce::WaitableEvent> evt);
|
|
void unregisterPendingLoad(const std::shared_ptr<juce::WaitableEvent>& evt);
|
|
void cancelAllPendingLoads();
|
|
|
|
} // namespace slopsmith::addon
|