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:
+96
-40
@@ -737,50 +737,24 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
|
||||
raise HTTPException(status_code=500, detail="Không ghi được job file cho bridge.")
|
||||
cmd = [exe, "--fx-gui" if req.embed else "--open-fx-gui", job_path]
|
||||
try:
|
||||
# fx-gui spawn o BELOW_NORMAL: process nay load instance VST THU HAI
|
||||
# (CPU spike vai giay) - neu o NORMAL se preempt realtime audio loop
|
||||
# (fx_vst_bridge --realtime-fx, thread NORMAL) -> underrun -> am "bop
|
||||
# nghet" ngay luc mo GUI roi moi hoi phuc. BELOW_NORMAL cho load GUI
|
||||
# khong canh tranh thread audio.
|
||||
_gui_creationflags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
|
||||
if os.name == "nt":
|
||||
_gui_creationflags |= getattr(subprocess, "BELOW_NORMAL_PRIORITY_CLASS", 0)
|
||||
if req.embed:
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, text=True,
|
||||
close_fds=os.name != "nt",
|
||||
env=dict(os.environ, SF_PARENT_PID=str(os.getpid())),
|
||||
creationflags=_gui_creationflags)
|
||||
# Bridge prints SF_FXGUI_PORT=<port> after binding its HTTP server.
|
||||
embed_url = None
|
||||
try:
|
||||
for line in proc.stdout:
|
||||
line = (line or "").strip()
|
||||
if line.startswith("SF_FXGUI_PORT="):
|
||||
port = int(line.split("=", 1)[1])
|
||||
embed_url = f"http://127.0.0.1:{port}"
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
# Spawn the fx-gui bridge and WAIT for the editor attach verdict;
|
||||
# respawn a fresh process on failure (see _spawn_fx_gui_embed — an
|
||||
# in-process retry cannot recover from a crashed core DLL state).
|
||||
# Runs in a worker thread so the API event loop is not blocked for
|
||||
# the (long) load+attach+retry window.
|
||||
proc, embed_url = await asyncio.to_thread(_spawn_fx_gui_embed, cmd)
|
||||
if not embed_url:
|
||||
proc.terminate()
|
||||
_kill_fx_gui_proc(proc)
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
raise HTTPException(status_code=500, detail="Bridge không báo port fx-gui (mở GUI thất bại).")
|
||||
# Keep draining stdout in a daemon thread: the bridge logs to
|
||||
# stdout/stderr while running; if the pipe fills (64KB) the bridge
|
||||
# blocks on any later write -> GUI freezes/breaks.
|
||||
def _drain_fx_gui_stdout(pp):
|
||||
try:
|
||||
for ln in pp.stdout:
|
||||
ln = (ln or "").strip()
|
||||
if ln:
|
||||
logger.info("[fx-gui] %s", ln)
|
||||
os.remove(job_path)
|
||||
except Exception:
|
||||
pass
|
||||
threading.Thread(target=_drain_fx_gui_stdout, args=(proc,), daemon=True).start()
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Không mở được GUI embed cho plugin — bridge thất bại "
|
||||
"nhiều lần khi tải/mở editor (chi tiết trong log [fx-gui]).",
|
||||
)
|
||||
proc._sf_plugin_path = plugin_path
|
||||
proc._sf_embed_url = embed_url
|
||||
_register_fx_gui_process(proc)
|
||||
@@ -788,7 +762,7 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
|
||||
"embed_url": embed_url, "cmd": cmd}
|
||||
proc = subprocess.Popen(cmd, close_fds=os.name != "nt",
|
||||
env=dict(os.environ, SF_PARENT_PID=str(os.getpid())),
|
||||
creationflags=_gui_creationflags)
|
||||
creationflags=_fx_gui_creationflags())
|
||||
proc._sf_plugin_path = plugin_path
|
||||
_register_fx_gui_process(proc)
|
||||
return {"success": True, "started": True, "already_running": False, "cmd": cmd}
|
||||
@@ -833,6 +807,88 @@ def _prune_fx_gui_processes():
|
||||
global _FX_GUI_PROCESSES
|
||||
_FX_GUI_PROCESSES = [p for p in _FX_GUI_PROCESSES if p is not None and p.poll() is None]
|
||||
|
||||
def _fx_gui_creationflags():
|
||||
# fx-gui spawn o BELOW_NORMAL: process nay load instance VST THU HAI
|
||||
# (CPU spike vai giay) - neu o NORMAL se preempt realtime audio loop
|
||||
# (fx_vst_bridge --realtime-fx, thread NORMAL) -> underrun -> am "bop
|
||||
# nghet" ngay luc mo GUI roi moi hoi phuc. BELOW_NORMAL cho load GUI
|
||||
# khong canh tranh thread audio.
|
||||
flags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
|
||||
if os.name == "nt":
|
||||
flags |= getattr(subprocess, "BELOW_NORMAL_PRIORITY_CLASS", 0)
|
||||
return flags
|
||||
|
||||
def _kill_fx_gui_proc(proc):
|
||||
"""Terminate a bridge process best-effort (already-exited = no-op)."""
|
||||
if proc is None:
|
||||
return
|
||||
if proc.poll() is None:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
|
||||
def _spawn_fx_gui_embed(cmd):
|
||||
"""Spawn the fx-gui bridge and wait for the editor attach verdict.
|
||||
|
||||
Process-level retry: an Ozone-class plugin can crash its GUI thread
|
||||
mid-attach; a fresh instance in the SAME process still shares the crashed
|
||||
core DLL state, so respawn the whole bridge (fresh DLL state per spawn).
|
||||
Each spawn's in-process retry (4 attempts, bridge side) covers the flaky
|
||||
first attach. Returns (proc, embed_url) — embed_url is None on total
|
||||
failure (all spawns failed; proc is the last, already-dead/killed, one).
|
||||
BLOCKS: call via asyncio.to_thread from the async endpoint.
|
||||
"""
|
||||
max_spawns = 5
|
||||
respawn_delay = 2.0 # Ozone license check: Product Engine service needs
|
||||
# time between respawns — back-to-back spawns all fail while it is busy.
|
||||
proc = None
|
||||
for spawn_i in range(1, max_spawns + 1):
|
||||
proc = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||
close_fds=os.name != "nt",
|
||||
env=dict(os.environ, SF_PARENT_PID=str(os.getpid())),
|
||||
creationflags=_fx_gui_creationflags(),
|
||||
)
|
||||
holder = {"port": None, "opened": False}
|
||||
|
||||
def _watch(pp, h):
|
||||
try:
|
||||
for ln in pp.stdout:
|
||||
ln = (ln or "").strip()
|
||||
if not ln:
|
||||
continue
|
||||
logger.info("[fx-gui] %s", ln)
|
||||
if ln.startswith("SF_FXGUI_PORT="):
|
||||
h["port"] = int(ln.split("=", 1)[1])
|
||||
elif "editor open" in ln:
|
||||
h["opened"] = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
threading.Thread(target=_watch, args=(proc, holder), daemon=True).start()
|
||||
# Fast path: port prints as soon as the HTTP server binds.
|
||||
deadline = _time.time() + 20
|
||||
while holder["port"] is None and proc.poll() is None and _time.time() < deadline:
|
||||
_time.sleep(0.05)
|
||||
if holder["port"] is None:
|
||||
logger.warning("[fx-gui] spawn %d/%d: no port, respawn", spawn_i, max_spawns)
|
||||
_kill_fx_gui_proc(proc)
|
||||
_time.sleep(respawn_delay)
|
||||
continue
|
||||
# Verdict: "editor open" (success) or process exit (attach all-fail).
|
||||
deadline = _time.time() + 70
|
||||
while not holder["opened"] and proc.poll() is None and _time.time() < deadline:
|
||||
_time.sleep(0.05)
|
||||
if holder["opened"]:
|
||||
logger.info("[fx-gui] spawn %d/%d: editor open", spawn_i, max_spawns)
|
||||
return proc, "http://127.0.0.1:%d" % holder["port"]
|
||||
logger.warning("[fx-gui] spawn %d/%d: editor failed, respawn", spawn_i, max_spawns)
|
||||
_kill_fx_gui_proc(proc)
|
||||
_time.sleep(respawn_delay)
|
||||
return proc, None
|
||||
|
||||
|
||||
|
||||
@router.post("/preview")
|
||||
|
||||
@@ -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