From dd2a320b2ae3736549032265138937924d44337f Mon Sep 17 00:00:00 2001 From: locpham Date: Sat, 22 Aug 2026 01:53:21 +0000 Subject: [PATCH] fix(scan): fx_vst_bridge --scan timeout per-module 30s + emit UNKNOWN thay vi is_instrument=true - RenderFxJob: safeScanModule chay tren worker thread + TerminateThread neu Module::create treo >30s (Ozone 11 Clarity treo vinh vien, SEH khong bat hang) -> scan khong bao gio ket thuc -> list_fx() timeout -> fx_paths rong -> filter /available vo hieu -> nut Synth hien ca VST FX - Module crash/hang -> emit is_fx=false,is_instrument=false (UNKNOWN) thay vi is_instrument=true (truoc day xep VST FX crash vao danh sach instrument) - vst_engine: list_scan_all() tra toan bo (FX+instrument+unknown); list_fx() loc is_fx tu cache chung - plugins.py /available: loai ca fx_paths lan unknown_paths khoi vst_instruments -> nut Synth chi hien VSTi that --- app/api/v1/plugins.py | 18 ++++++++--- app/core/vst_engine.py | 50 +++++++++++++++++++++---------- native_bridge/src/RenderFxJob.cpp | 45 ++++++++++++++++++++++++---- 3 files changed, 88 insertions(+), 25 deletions(-) diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 7a12ed2..49a9fe3 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -172,20 +172,30 @@ async def list_plugins(current_user: dict = Depends(get_current_user)): # VST FX (Ozone/Sonible/...) khỏi vst_instruments theo is_fx của bridge scan. # Filter TRƯỚC merge extra: nếu không, entry FX (path .vst3) chiếm id và # setdefault chặn entry instrument (path .dll) cùng tên → mất cả hai. + # Ngoài is_fx còn loại entry UNKNOWN (is_fx=false, is_instrument=false — + # bridge không đọc được module, vd Ozone 11 Clarity crash/hang khi scan): + # không xác định được loại → KHÔNG hiện trong nút Synth. fx_paths = set() + unknown_paths = set() try: from app.core.vst_engine import get_plugin_manager - fx_paths = {f.get("path", "").lower() for f in get_plugin_manager().list_fx()} + for e in get_plugin_manager().list_scan_all(): + path = (e.get("path") or "").lower() + if e.get("is_fx"): + fx_paths.add(path) + elif e.get("is_unknown"): + unknown_paths.add(path) except Exception: pass - if fx_paths: + blocked = fx_paths | unknown_paths + if blocked: avail["vst_instruments"] = [v for v in avail["vst_instruments"] - if (v.get("path") or "").lower() not in fx_paths] + if (v.get("path") or "").lower() not in blocked] # Gộp VST từ plugin_dirs user đã scan — list_available() CHỈ quét vst_dir # env (mặc định /opt/daw_engine/vst3) → Synth dropdown không thấy VSTi mà # Plugin Manager đã scan trong thư mục user chọn (bug: nút Synth rỗng). extra = [v for v in _scan_vst_in_dirs(d["plugin_dirs"]) - if (v.get("path") or "").lower() not in fx_paths] + if (v.get("path") or "").lower() not in blocked] by_id = {v["id"]: v for v in avail["vst_instruments"]} for v in extra: by_id.setdefault(v["id"], v) diff --git a/app/core/vst_engine.py b/app/core/vst_engine.py index 85c1ff1..9196b51 100644 --- a/app/core/vst_engine.py +++ b/app/core/vst_engine.py @@ -299,6 +299,7 @@ class PluginManager: self.extra_vst_dirs = [d for d in (extra_vst_dirs or []) if d] self._sf_scan_cache = None # cache for _scan_soundfonts() self._fx_scan_cache = None # cache for list_fx() + self._fx_scan_all = None # cache for list_scan_all() def _scan_plugins(self) -> dict: plugins = {} @@ -479,18 +480,33 @@ class PluginManager: không introspect được nên bridge báo instrument — bỏ qua ở đây. Cache process-wide; refresh=True quét lại (Plugins Manager → Scan). """ - if not refresh and self._fx_scan_cache is not None: - return self._fx_scan_cache + all_entries = self.list_scan_all(refresh=refresh) + results = [e for e in all_entries if e.get("is_fx")] + results.sort(key=lambda x: x["name"].lower()) + self._fx_scan_cache = results + return results + + def list_scan_all(self, refresh=False) -> list: + """Toàn bộ kết quả `fx_vst_bridge --scan` (FX + instrument + unknown). + + Bridge scan giờ có timeout per-module (30s) và emit entry UNKNOWN + (is_fx=false, is_instrument=false) cho module crash/hang (vd Ozone 11 + Clarity) — không xếp bừa vào instrument. `/available` dùng entry + unknown để LOẠI plugin đó khỏi nút Synth (không biết loại → không + hiện là instrument), còn list_fx() chỉ lấy is_fx=true. + """ + if not refresh and self._fx_scan_all is not None: + return self._fx_scan_all from app.core.native_render import find_fx_bridge_exe exe = find_fx_bridge_exe() - results = [] + entries = [] if exe: dirs = [d for d in [self.vst_dir] + self.extra_vst_dirs if d and os.path.isdir(d)] for d in dirs: try: r = subprocess.run([exe, "--scan", d], capture_output=True, - text=True, timeout=300) + text=True, timeout=600) except Exception: continue if r.returncode != 0: @@ -500,23 +516,27 @@ class PluginManager: except Exception: continue for p in data.get("plugins", []): - if not p.get("is_fx"): - continue path = p.get("path", "") if not path: continue + is_fx = bool(p.get("is_fx")) + is_instr = bool(p.get("is_instrument")) name = p.get("name") or os.path.splitext(os.path.basename(path))[0] - results.append({"id": name, "name": name, "path": path, - "type": p.get("type", "VST3"), - "category": _classify_fx_category(name, path)}) + entries.append({ + "id": name, "name": name, "path": path, + "type": p.get("type", "VST3"), + "is_fx": is_fx, + "is_instrument": is_instr, + "is_unknown": not is_fx and not is_instr, + "category": _classify_fx_category(name, path), + }) seen, uniq = set(), [] - for fx in results: - if fx["path"] in seen: + for e in entries: + if e["path"] in seen: continue - seen.add(fx["path"]) - uniq.append(fx) - uniq.sort(key=lambda x: x["name"].lower()) - self._fx_scan_cache = uniq + seen.add(e["path"]) + uniq.append(e) + self._fx_scan_all = uniq return uniq @staticmethod diff --git a/native_bridge/src/RenderFxJob.cpp b/native_bridge/src/RenderFxJob.cpp index b01d034..325eb5a 100644 --- a/native_bridge/src/RenderFxJob.cpp +++ b/native_bridge/src/RenderFxJob.cpp @@ -1479,13 +1479,43 @@ bool scanModuleInner(const std::string& path, std::vector& out) { return added > 0; } +// Per-module scan context for the timeout worker thread. +struct ScanCtx { + const std::string* path; + std::vector* out; + bool ok = false; +}; +static DWORD WINAPI scanWorker(LPVOID p) { + auto* ctx = static_cast(p); + __try { ctx->ok = scanModuleInner(*ctx->path, *ctx->out); } + __except (EXCEPTION_EXECUTE_HANDLER) { + std::cerr << "[Scan] skip (crashed module): " << *ctx->path << std::endl; + ctx->ok = false; + } + return 0; +} + bool safeScanModule(const std::string& path, std::vector& out) { #ifdef _WIN32 - __try { return scanModuleInner(path, out); } - __except (EXCEPTION_EXECUTE_HANDLER) { - std::cerr << "[Scan] skip (crashed module): " << path << std::endl; + // Run the VST3 loader on a dedicated thread so a HANGING module (e.g. + // Ozone 11 Clarity.vst3 — Module::create blocks forever inside the SDK) + // cannot stall the whole --scan. SEH only catches crashes, not hangs, so + // a watchdog timeout + TerminateThread is required. Safe here: --scan is + // a one-shot process; the worker only touches the VST3 SDK loader and the + // process exits right after the scan completes. + ScanCtx ctx{&path, &out}; + HANDLE hThread = CreateThread(nullptr, 0, scanWorker, &ctx, 0, nullptr); + if (!hThread) { std::cerr << "[Scan] skip (thread fail): " << path << std::endl; return false; } + const DWORD timeoutMs = 30000; // 30s/module — generous for big VST3s + DWORD wait = WaitForSingleObject(hThread, timeoutMs); + if (wait == WAIT_TIMEOUT) { + TerminateThread(hThread, 0); + CloseHandle(hThread); + std::cerr << "[Scan] skip (timeout 30s): " << path << std::endl; return false; } + CloseHandle(hThread); + return ctx.ok; #else return scanModuleInner(path, out); #endif @@ -1541,9 +1571,12 @@ int run_scan_dir(const std::string& dirPath) { for (const auto& p : vst3Paths) { std::vector entries; if (!safeScanModule(p, entries)) { - // Module unreadable (or crashed): report as opaque instrument-like - // entry so the UI still lists it (VST2 default behavior). - emit(p, p.substr(p.find_last_of("/\\") + 1), false, true, "VST3"); + // Module unreadable/crashed/hung (e.g. Ozone 11 Clarity): emit + // entry UNKNOWN (is_fx=false, is_instrument=false) — trước đây + // emit is_instrument=true làm VST FX lọt vào danh sách instrument + // (nút Synth). Backend loại entry unknown khỏi vst_instruments: + // không xác định được loại → không xếp bừa vào instrument. + emit(p, p.substr(p.find_last_of("/\\") + 1), false, false, "VST3"); continue; } for (const auto& e : entries) emit(e.path, e.name, e.isFx, e.isInstrument, "VST3");