Commit Graph
6 Commits
Author SHA1 Message Date
OmikronApexandClaude Fable 5 1359c7c24d 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>
2026-07-17 23:20:46 +02:00
OmikronApexandClaude Fable 5 895d0af969 fix(audio): route device probing through the message thread; document timeout desync
Review follow-up (PR #113, finding 1): ProbeDeviceOptions constructs and
destroys short-lived ASIO device objects (DeviceSetup::probeDual /
rateSupportedBy createDevice) on the Node thread - the same
create/destroy-off-the-message-thread pattern as the lifecycle ops, and a
probe ASIOAudioIODevice owns the same reset-timer machinery. Wrap it in
runDeviceLifecycleOp for consistency.

Finding 2 (timeout desync - op reported failed may still complete late)
is documented as a known limitation on the helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 23:45:29 +02:00
OmikronApexandClaude Fable 5 aa0bb9d173 fix(audio): fail runDeviceLifecycleOp when the MessageManager is gone
A null MessageManager (pre-init or mid-shutdown) previously fell through
to inline execution on the caller's thread while reporting success -
exactly the unserialised device teardown this helper prevents. Return
false instead, per the documented unavailable/timeout contract.

Addresses CodeRabbit review on #113.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 22:54:22 +02:00
OmikronApexandClaude Fable 5 354052bc7c 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>
2026-07-16 22:34:36 +02:00
OmikronApexandClaude Fable 5 ea8c6a9ccd fix(audio): address PR #107 review — close serializer gaps, editor lifetime races, dispatch failures
All 8 CodeRabbit findings verified against the code and fixed:

- ChainOps: macOS LoadVST routes its addProcessor through chainMutationMutex
  (macOS is a first-class platform; deadlock-safe — a worker holding the
  mutex never waits on the Node/main thread there). All four single-slot
  workers (LoadVST/NAM/IR/ReplaceIR) now bump chainGeneration so the
  executor's foreign-write detection sees direct loads, not just presets.
- Rebuild barrier (beginChainRebuild/endChainRebuild): LoadPreset and
  ClearChain arm it before editor teardown; OpenPluginEditor refuses to
  open while a teardown+clear/rebuild is pending (#56 window between
  closeAllPluginEditorWindows returning and the worker taking the mutex).
- EditorWindows: all slot/processor resolution in editor lambdas runs under
  a try_lock of chainMutationMutex (try_lock, never blocking — workers
  holding the mutex block-wait on the message thread). Sandbox promotion
  bumps chainGeneration. editorWindows map is now message-thread-only
  (duplicate-window check and close-erase moved into the queued lambdas).
  Null slot->processor recheck after a faulted promotion capture.
- closeAllPluginEditorWindows returns false on refused post / 15s timeout;
  ClearChain skips the clear and LoadPreset resolves {success:false}
  instead of freeing processors under a live editor.
- AddonContext: dispatchOnMessageThread reports refused-post/timeout;
  doShutdown leaves the message thread running when teardown didn't
  complete instead of unloading mid-destruction.
- RendererBus::push rejects NaN/Inf/non-positive rates and a step that
  underflows to zero; new testRejectsUnusableRates unit case.

Verified: addon builds clean, all 78 JS tests pass (storm, contracts,
executor, N-API fuzz), all 5 engine_units native tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:56:08 +02:00
OmikronApexandClaude Fable 5 d4e0bfc272 refactor(audio): extract AddonContext + NapiHelpers, guard raw N-API args (phase 6)
AddonContext (src/audio/addon/): engine/vstHost lifetime + snapshot rule,
the JUCE message thread with the macOS no-pump fork quarantined into ONE
file, the shutdown latch (exposed as isShuttingDown), doShutdown with a UI
teardown hook (NodeAddon points it at the editor-window nuke, #56), and the
pending-async-load registry. NodeAddon keeps using-declarations so the
binding bodies are unchanged. Also fixes SetBackingSpeed's bare `engine`
dereference — the one binding that dodged the file's own snapshot rule.

NapiHelpers (typed extractors argInt/argSlotId/argFiniteFloat/argBool/
argMidiChannel/argMidiByte) + rewrites of the unguarded bindings — the
deep-read §2 fix, done once: SetParameter/SetBypass/RemoveProcessor/
MoveProcessor/SetMultiBypass/SendMidiToSlot/SetGain plus the St-1 routing
quartet (SetPan/SetPostGain/SetBranch/SetBranchSrc). NaN slot ids no longer
coerce to slot 0; MIDI channel/program are range-checked before JUCE.

New gate: tests/napi-arg-fuzz.test.js — table-driven garbage (NaN/Inf/
negative/string/missing/object) against the real addon; chain state must be
byte-identical after the storm and a valid call must still apply.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 01:41:07 +02:00