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
This commit is contained in:
+14
-4
@@ -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)
|
||||
|
||||
+35
-15
@@ -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
|
||||
|
||||
@@ -1479,13 +1479,43 @@ bool scanModuleInner(const std::string& path, std::vector<ScanEntry>& out) {
|
||||
return added > 0;
|
||||
}
|
||||
|
||||
// Per-module scan context for the timeout worker thread.
|
||||
struct ScanCtx {
|
||||
const std::string* path;
|
||||
std::vector<ScanEntry>* out;
|
||||
bool ok = false;
|
||||
};
|
||||
static DWORD WINAPI scanWorker(LPVOID p) {
|
||||
auto* ctx = static_cast<ScanCtx*>(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<ScanEntry>& 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<ScanEntry> 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");
|
||||
|
||||
Reference in New Issue
Block a user