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")
|
||||
|
||||
Reference in New Issue
Block a user