fix(bridge): Ozone FX GUI embed crash license check — sleep truoc attach + retry 5 spawn
Root cause (minidump x3, cung offset iZOzone11Core.dll+0x18f6b92): crash deterministic 1 instruction - memcmp std::string [rdi+0x100] vs "iLok" trong license check, doc heap ptr garbage (use-after-free/corrupt trong iZotope Product Engine license path, flaky theo timing). Fixes: - RenderFxJob.cpp: fxGuiLoadAttach giu retry 4 attempt moi instance moi (SEH-guarded dispose); them Sleep(2s) giua load va attach cho plugin iZotope/Ozone - Product Engine service het race, attempt 1 open ngay (E2E: 60-70s -> 10-22s, 8/8 run OK). - plugins.py: _spawn_fx_gui_embed process-level respawn 3->5 lan + delay 2s giua respawn (service can thoi gian hoi phuc sau crash); di chuyen BELOW_NORMAL/CREATE_NO_WINDOW sang _fx_gui_creationflags(); gui stdout bridge qua thread de khong block event loop (asyncio.to_thread). - main_fx.cpp: FxCrashDumpHandler log MOI exception (khong chi lan dau), minidump van 1 lan/process. Verified: E2E engine-from-source 8/8 run Ozone embed OK; app installed (thu cai moi installer NSIS): OPEN 49.5s, frames 10/10 jpeg, spawn.log khong con 0x7E/Fatal Error.
This commit is contained in:
@@ -455,23 +455,6 @@ static void setPluginSearchPath(const std::string& path) {
|
||||
bool vst3FxLoadInner(Vst3FxState* s, const std::string& path, double sampleRate,
|
||||
int32 maxBlockSize, std::string& err) {
|
||||
using namespace VST3::Hosting;
|
||||
#ifdef _WIN32
|
||||
// Ozone 11 / Ozone Pro (iZotope) tự __fastfail (c0000409, license/anti-
|
||||
// tamper) khi load — KHÔNG bị SEH chặn → giết cả bridge process (scan +
|
||||
// render + realtime). Không load: trả lỗi sạch cho chain. WER: Ozone
|
||||
// Pro.vst3 (v9.11.0.1955) cũng crash c0000409 trên fx_vst_bridge.
|
||||
// ponytail: muốn chạy Ozone → SandboxVst3Host (mỗi module 1 process).
|
||||
{
|
||||
std::string low = path;
|
||||
for (auto& c : low) c = (char)std::tolower((unsigned char)c);
|
||||
if (low.find("ozone 11") != std::string::npos ||
|
||||
low.find("ozone pro") != std::string::npos) {
|
||||
err = "Ozone not loadable (license fastfail) - use another module";
|
||||
std::cerr << "[RenderFx] skip (Ozone fastfail): " << path << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
setPluginSearchPath(path);
|
||||
#endif
|
||||
@@ -599,8 +582,16 @@ bool vst3FxLoadInner(Vst3FxState* s, const std::string& path, double sampleRate,
|
||||
class Vst3Fx {
|
||||
public:
|
||||
Vst3Fx() = default;
|
||||
~Vst3Fx() {
|
||||
#ifdef HAVE_VST3SDK
|
||||
~Vst3Fx() { dispose(); }
|
||||
// Full teardown of a loaded plugin (component terminate + controller
|
||||
// terminate + process data). Idempotent. Callers on the retry path wrap
|
||||
// this in SEH (fxGuiDisposeSafe) — a plugin whose GUI attach crashed may
|
||||
// be too broken to terminate cleanly; worst case it leaks instead of
|
||||
// re-crashing the process.
|
||||
void dispose() {
|
||||
#ifndef HAVE_VST3SDK
|
||||
return;
|
||||
#else
|
||||
if (!state_) return;
|
||||
auto* s = static_cast<Vst3FxState*>(state_);
|
||||
if (s->component) {
|
||||
@@ -964,6 +955,14 @@ public:
|
||||
return state_ ? static_cast<Vst3FxState*>(state_)->latencySamples : 0;
|
||||
#endif
|
||||
}
|
||||
// Retry path (fxGuiLoadAttach): take ownership of another instance's
|
||||
// loaded plugin state. other->state_ nulled so its destructor is a no-op;
|
||||
// the caller may then delete other safely. Only use on instances that
|
||||
// NEVER attached successfully (their state is clean or leaked anyway).
|
||||
void stealState(Vst3Fx* other) {
|
||||
state_ = other->state_;
|
||||
other->state_ = nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
void* state_ = nullptr;
|
||||
@@ -1604,6 +1603,32 @@ static bool fxGuiAttachSafe(void* fx, HWND hwnd) {
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
// SEH-wrapped plugin load: a load crash (rare) becomes a clean failure instead
|
||||
// of killing the bridge. locals in load() are leaked on crash — acceptable on
|
||||
// the retry path (process exits after the session anyway).
|
||||
static bool fxGuiLoadSafe(Vst3Fx* fx, const std::string& path) {
|
||||
#ifdef HAVE_VST3SDK
|
||||
__try { return fx->load(path, 44100.0, 512); }
|
||||
__except (EXCEPTION_EXECUTE_HANDLER) { return false; }
|
||||
#else
|
||||
(void)fx; (void)path;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
// SEH-guarded delete of a failed-attempt instance: dispose() (destructor) runs
|
||||
// guarded so a plugin too broken to terminate cleanly leaks instead of
|
||||
// re-crashing the process; the main goal is stopping the failed instance's
|
||||
// component/worker threads so they cannot crash the process asynchronously
|
||||
// later (a leaked crashed instance's workers kept killing the GUI session
|
||||
// seconds after a successful attach).
|
||||
static void fxGuiDisposeSafe(Vst3Fx* fx) {
|
||||
#ifdef HAVE_VST3SDK
|
||||
__try { delete fx; }
|
||||
__except (EXCEPTION_EXECUTE_HANDLER) {}
|
||||
#else
|
||||
(void)fx;
|
||||
#endif
|
||||
}
|
||||
static void fxGuiDetachSafe(void* fx) {
|
||||
#ifdef HAVE_VST3SDK
|
||||
__try { static_cast<Vst3Fx*>(fx)->closeEditor(); }
|
||||
@@ -1831,32 +1856,60 @@ static int fxGuiCreateWindow(const std::string& name, bool offscreen,
|
||||
|
||||
// 3. Load the plugin + attach the editor (SEH-guarded — plugin GUI code may
|
||||
// crash). Slow — runs on the pump thread in server mode, after HTTP is up.
|
||||
// Returns 0 ok, 2 plugin load or editor attach failure.
|
||||
// Ozone-class plugins crash intermittently INSIDE view->attached()
|
||||
// (iZOzone11Core.dll, null-vtable read — GUI-worker race), so each attempt
|
||||
// loads a FRESH instance; failed ones are torn down best-effort (guarded —
|
||||
// see fxGuiDisposeSafe). Returns 0 ok, 2 plugin load or editor attach
|
||||
// failure.
|
||||
static int fxGuiLoadAttach(Vst3Fx* fx, const std::string& path, const std::string& name, HWND hwnd, const std::string& presetB64) {
|
||||
// Fixed 44.1k/512 — GUI only, no audio processed here.
|
||||
if (!fx->load(path, 44100.0, 512)) { std::cerr << "[FxGui] plugin load failed: " << path << std::endl; return 2; }
|
||||
// Khôi phục preset GUI capture cuối (hide/show mất settings bug): áp
|
||||
// preset NGAY SAU load — trước attach editor để GUI hiện đúng cài đặt.
|
||||
if (!presetB64.empty() && !fx->applyPreset(presetB64))
|
||||
std::cerr << "[FxGui] preset apply FAILED (default kept): " << path << std::endl;
|
||||
else if (!presetB64.empty())
|
||||
std::cerr << "[FxGui] preset applied: " << path << std::endl;
|
||||
if (!fxGuiAttachSafe(fx, hwnd)) {
|
||||
std::cerr << "[FxGui] editor attach failed for " << path << std::endl;
|
||||
DestroyWindow(hwnd);
|
||||
return 2;
|
||||
const int kMaxAttempts = 4;
|
||||
for (int attempt = 1; attempt <= kMaxAttempts; ++attempt) {
|
||||
Vst3Fx* fresh = new Vst3Fx();
|
||||
std::cerr << "[FxGui] attempt " << attempt << "/" << kMaxAttempts
|
||||
<< ": load " << path << std::endl;
|
||||
// Fixed 44.1k/512 — GUI only, no audio processed here.
|
||||
if (!fxGuiLoadSafe(fresh, path)) {
|
||||
std::cerr << "[FxGui] plugin load failed: " << path << std::endl;
|
||||
delete fresh; // clean failure — state_ never set, destructor no-op
|
||||
DestroyWindow(hwnd);
|
||||
return 2;
|
||||
}
|
||||
// Khôi phục preset GUI capture cuối (hide/show mất settings bug): áp
|
||||
// preset NGAY SAU load — trước attach editor để GUI hiện đúng cài đặt.
|
||||
if (!presetB64.empty() && !fresh->applyPreset(presetB64))
|
||||
std::cerr << "[FxGui] preset apply FAILED (default kept): " << path << std::endl;
|
||||
else if (!presetB64.empty())
|
||||
std::cerr << "[FxGui] preset applied: " << path << std::endl;
|
||||
// iZotope (Ozone-class): license check races the editor attach and
|
||||
// intermittently crashes inside iZOzone11Core.dll — give the Product
|
||||
// Engine license service a beat after load before attaching.
|
||||
if (path.find("Ozone") != std::string::npos ||
|
||||
path.find("iZotope") != std::string::npos)
|
||||
Sleep(2000);
|
||||
if (fxGuiAttachSafe(fresh, hwnd)) {
|
||||
fx->stealState(fresh);
|
||||
delete fresh;
|
||||
std::cout << "[FxGui] editor open: " << (name.empty() ? path : name) << std::endl;
|
||||
return 0;
|
||||
}
|
||||
std::cerr << "[FxGui] editor attach FAILED (attempt " << attempt << "/"
|
||||
<< kMaxAttempts << ") — retrying fresh instance" << std::endl;
|
||||
// Best-effort teardown of the failed instance (guarded): stop its
|
||||
// component/worker threads so they cannot crash the process later.
|
||||
// Worst case the instance leaks (unchanged from before).
|
||||
fxGuiDisposeSafe(fresh);
|
||||
}
|
||||
std::cout << "[FxGui] editor open: " << (name.empty() ? path : name) << std::endl;
|
||||
return 0;
|
||||
DestroyWindow(hwnd);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Combined setup (legacy open mode): parse + load + window + attach, in order.
|
||||
// Combined setup (legacy open mode): parse + window + attach (load happens
|
||||
// inside fxGuiLoadAttach's retry loop), in order.
|
||||
static int fxGuiSetup(const std::string& jobPath, Vst3Fx* fx, HWND* hwndOut,
|
||||
bool offscreen, LONG_PTR userData, std::string& shmOut) {
|
||||
std::string path, name, presetB64;
|
||||
int rc = fxGuiParseJob(jobPath, path, name, shmOut, presetB64);
|
||||
if (rc) return rc;
|
||||
if (!fx->load(path, 44100.0, 512)) { std::cerr << "[FxGui] plugin load failed: " << path << std::endl; return 2; }
|
||||
rc = fxGuiCreateWindow(name, offscreen, userData, hwndOut);
|
||||
if (rc) return rc;
|
||||
return fxGuiLoadAttach(fx, path, name, *hwndOut, presetB64);
|
||||
|
||||
@@ -28,13 +28,15 @@
|
||||
|
||||
#ifdef _WIN32
|
||||
// Minidump on crash -> %TEMP%\sf_dumps (same convention as daw_vst_bridge).
|
||||
// Every qualifying exception is LOGGED (diagnosing the retry path needs to see
|
||||
// crashes after the first); only the minidump is written once per process.
|
||||
static LONG WINAPI FxCrashDumpHandler(EXCEPTION_POINTERS* ep) {
|
||||
static LONG once = 0;
|
||||
if (InterlockedCompareExchange(&once, 1, 0)) return EXCEPTION_CONTINUE_SEARCH;
|
||||
PEXCEPTION_RECORD er = ep ? ep->ExceptionRecord : nullptr;
|
||||
std::cerr << "[CRASH] fx_vst_bridge exception code=0x" << std::hex
|
||||
<< (er ? er->ExceptionCode : 0) << " addr=0x"
|
||||
<< (er ? (void*)er->ExceptionAddress : nullptr) << std::dec << std::endl;
|
||||
static LONG once = 0;
|
||||
if (InterlockedCompareExchange(&once, 1, 0)) return EXCEPTION_CONTINUE_SEARCH;
|
||||
char dumpDir[MAX_PATH];
|
||||
if (GetTempPathA(MAX_PATH, dumpDir)) {
|
||||
std::strncat(dumpDir, "sf_dumps", MAX_PATH - std::strlen(dumpDir) - 1);
|
||||
|
||||
Reference in New Issue
Block a user