fix(api,ui): chan double-spawn GUI embed master chain - register bridge ngay sau Popen + inflight guard + frontend busy ref (crash 0xc0000005 2 instance Ozone)
This commit is contained in:
+36
-20
@@ -719,10 +719,17 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
|
|||||||
if not plugin_path:
|
if not plugin_path:
|
||||||
raise HTTPException(status_code=400, detail="Thiếu đường dẫn VST plugin.")
|
raise HTTPException(status_code=400, detail="Thiếu đường dẫn VST plugin.")
|
||||||
# Tránh mở 2 GUI cùng plugin: nếu tiến trình bridge fx-gui cho plugin này
|
# Tránh mở 2 GUI cùng plugin: nếu tiến trình bridge fx-gui cho plugin này
|
||||||
# đang chạy → trả started=False (frontend có thể focus window cũ).
|
# đang chạy (đã register sau Popen, kể cả khi còn đang chờ attach verdict)
|
||||||
|
# → trả started=False (frontend có thể focus window cũ).
|
||||||
for proc in _FX_GUI_PROCESSES:
|
for proc in _FX_GUI_PROCESSES:
|
||||||
if proc is not None and proc.poll() is None and getattr(proc, "_sf_plugin_path", None) == plugin_path:
|
if proc is not None and proc.poll() is None and getattr(proc, "_sf_plugin_path", None) == plugin_path:
|
||||||
return {"success": True, "started": False, "already_running": True, "cmd": [], "embed_url": getattr(proc, "_sf_embed_url", None) or None}
|
return {"success": True, "started": False, "already_running": True, "cmd": [], "embed_url": getattr(proc, "_sf_embed_url", None) or None}
|
||||||
|
# Request thứ 2 cho cùng plugin trong lúc spawn đầu ĐANG CHỜ verdict (có thể
|
||||||
|
# 20-70s cho Ozone-class) chưa thấy process nào register → phải chặn bằng
|
||||||
|
# inflight set, nếu không sẽ spawn bridge thứ 2 → 2 GUI instance Ozone cùng
|
||||||
|
# lúc → crash (0xc0000005 iZOzone11Core.dll) như bug đã gặp.
|
||||||
|
if req.embed and plugin_path in _FX_GUI_INFLIGHT:
|
||||||
|
return {"success": True, "started": False, "already_running": True, "cmd": [], "embed_url": None}
|
||||||
job = {"path": plugin_path, "name": req.name or os.path.basename(plugin_path)}
|
job = {"path": plugin_path, "name": req.name or os.path.basename(plugin_path)}
|
||||||
if req.shm:
|
if req.shm:
|
||||||
job["shm"] = req.shm
|
job["shm"] = req.shm
|
||||||
@@ -743,23 +750,25 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
|
|||||||
# in-process retry cannot recover from a crashed core DLL state).
|
# 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
|
# Runs in a worker thread so the API event loop is not blocked for
|
||||||
# the (long) load+attach+retry window.
|
# the (long) load+attach+retry window.
|
||||||
proc, embed_url = await asyncio.to_thread(_spawn_fx_gui_embed, cmd)
|
_FX_GUI_INFLIGHT.add(plugin_path)
|
||||||
if not embed_url:
|
try:
|
||||||
_kill_fx_gui_proc(proc)
|
proc, embed_url = await asyncio.to_thread(_spawn_fx_gui_embed, cmd, plugin_path)
|
||||||
try:
|
if not embed_url:
|
||||||
os.remove(job_path)
|
_kill_fx_gui_proc(proc)
|
||||||
except Exception:
|
try:
|
||||||
pass
|
os.remove(job_path)
|
||||||
raise HTTPException(
|
except Exception:
|
||||||
status_code=500,
|
pass
|
||||||
detail="Không mở được GUI embed cho plugin — bridge thất bại "
|
raise HTTPException(
|
||||||
"nhiều lần khi tải/mở editor (chi tiết trong log [fx-gui]).",
|
status_code=500,
|
||||||
)
|
detail="Không mở được GUI embed cho plugin — bridge thất bại "
|
||||||
proc._sf_plugin_path = plugin_path
|
"nhiều lần khi tải/mở editor (chi tiết trong log [fx-gui]).",
|
||||||
proc._sf_embed_url = embed_url
|
)
|
||||||
_register_fx_gui_process(proc)
|
proc._sf_embed_url = embed_url
|
||||||
return {"success": True, "started": True, "already_running": False,
|
return {"success": True, "started": True, "already_running": False,
|
||||||
"embed_url": embed_url, "cmd": cmd}
|
"embed_url": embed_url, "cmd": cmd}
|
||||||
|
finally:
|
||||||
|
_FX_GUI_INFLIGHT.discard(plugin_path)
|
||||||
proc = subprocess.Popen(cmd, close_fds=os.name != "nt",
|
proc = subprocess.Popen(cmd, close_fds=os.name != "nt",
|
||||||
env=dict(os.environ, SF_PARENT_PID=str(os.getpid())),
|
env=dict(os.environ, SF_PARENT_PID=str(os.getpid())),
|
||||||
creationflags=_fx_gui_creationflags())
|
creationflags=_fx_gui_creationflags())
|
||||||
@@ -797,11 +806,13 @@ async def close_fx_gui(req: FxGuiCloseRequest, current_user: dict = Depends(get_
|
|||||||
|
|
||||||
|
|
||||||
_FX_GUI_PROCESSES = [] # list[subprocess.Popen] — các bridge fx-gui do app spawn
|
_FX_GUI_PROCESSES = [] # list[subprocess.Popen] — các bridge fx-gui do app spawn
|
||||||
|
_FX_GUI_INFLIGHT = set() # set[str] plugin_path — spawn embed đang chờ verdict
|
||||||
|
|
||||||
def _register_fx_gui_process(proc):
|
def _register_fx_gui_process(proc):
|
||||||
global _FX_GUI_PROCESSES
|
global _FX_GUI_PROCESSES
|
||||||
_prune_fx_gui_processes()
|
_prune_fx_gui_processes()
|
||||||
_FX_GUI_PROCESSES.append(proc)
|
if proc not in _FX_GUI_PROCESSES:
|
||||||
|
_FX_GUI_PROCESSES.append(proc)
|
||||||
|
|
||||||
def _prune_fx_gui_processes():
|
def _prune_fx_gui_processes():
|
||||||
global _FX_GUI_PROCESSES
|
global _FX_GUI_PROCESSES
|
||||||
@@ -829,7 +840,7 @@ def _kill_fx_gui_proc(proc):
|
|||||||
except Exception:
|
except Exception:
|
||||||
proc.kill()
|
proc.kill()
|
||||||
|
|
||||||
def _spawn_fx_gui_embed(cmd):
|
def _spawn_fx_gui_embed(cmd, plugin_path=None):
|
||||||
"""Spawn the fx-gui bridge and wait for the editor attach verdict.
|
"""Spawn the fx-gui bridge and wait for the editor attach verdict.
|
||||||
|
|
||||||
Process-level retry: an Ozone-class plugin can crash its GUI thread
|
Process-level retry: an Ozone-class plugin can crash its GUI thread
|
||||||
@@ -851,6 +862,11 @@ def _spawn_fx_gui_embed(cmd):
|
|||||||
env=dict(os.environ, SF_PARENT_PID=str(os.getpid())),
|
env=dict(os.environ, SF_PARENT_PID=str(os.getpid())),
|
||||||
creationflags=_fx_gui_creationflags(),
|
creationflags=_fx_gui_creationflags(),
|
||||||
)
|
)
|
||||||
|
# Register NGAY sau Popen (trước vòng chờ verdict 20-70s): request thứ 2
|
||||||
|
# cho cùng plugin thấy process đang chạy → không spawn thêm instance GUI.
|
||||||
|
if plugin_path is not None:
|
||||||
|
proc._sf_plugin_path = plugin_path
|
||||||
|
_register_fx_gui_process(proc)
|
||||||
holder = {"port": None, "opened": False}
|
holder = {"port": None, "opened": False}
|
||||||
|
|
||||||
def _watch(pp, h):
|
def _watch(pp, h):
|
||||||
|
|||||||
+23
-3
@@ -14228,6 +14228,7 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
|||||||
const [activeMasterVstPath, setActiveMasterVstPath] = React.useState(null);
|
const [activeMasterVstPath, setActiveMasterVstPath] = React.useState(null);
|
||||||
const masterGuiOpenRef = React.useRef(null);
|
const masterGuiOpenRef = React.useRef(null);
|
||||||
masterGuiOpenRef.current = masterGuiOpen;
|
masterGuiOpenRef.current = masterGuiOpen;
|
||||||
|
const masterGuiBusyRef = React.useRef(false); // request openFxGui đang bay (chưa có verdict 20-70s)
|
||||||
const closeMasterGui = () => {
|
const closeMasterGui = () => {
|
||||||
const g = masterGuiOpenRef.current;
|
const g = masterGuiOpenRef.current;
|
||||||
if (g) {
|
if (g) {
|
||||||
@@ -14255,12 +14256,14 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
|||||||
}, [masterGuiOpen]);
|
}, [masterGuiOpen]);
|
||||||
// Reopen the saved GUI when the panel is opened after a project restore.
|
// Reopen the saved GUI when the panel is opened after a project restore.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isOpen || !ozState || !ozState.gui_open_path || masterGuiOpen) return;
|
if (!isOpen || !ozState || !ozState.gui_open_path || masterGuiOpen || masterGuiBusyRef.current) return;
|
||||||
const s = (ozState.vstFxChain || []).find(x => x.path === ozState.gui_open_path);
|
const s = (ozState.vstFxChain || []).find(x => x.path === ozState.gui_open_path);
|
||||||
if (!s || !window.SonicAPI || !window.SonicAPI.openFxGui) return;
|
if (!s || !window.SonicAPI || !window.SonicAPI.openFxGui) return;
|
||||||
|
masterGuiBusyRef.current = true;
|
||||||
window.SonicAPI.openFxGui({ path: s.path, name: ozState.gui_open_name || s.name || s.path, embed: true, shm: fxRt.sessionId || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' }).then(r => {
|
window.SonicAPI.openFxGui({ path: s.path, name: ozState.gui_open_name || s.name || s.path, embed: true, shm: fxRt.sessionId || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' }).then(r => {
|
||||||
|
masterGuiBusyRef.current = false;
|
||||||
if (r && r.embed_url) setMasterGuiOpen({ path: s.path, url: r.embed_url });
|
if (r && r.embed_url) setMasterGuiOpen({ path: s.path, url: r.embed_url });
|
||||||
}).catch(() => {});
|
}).catch(() => { masterGuiBusyRef.current = false; });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isOpen, ozState && ozState.gui_open_path]);
|
}, [isOpen, ozState && ozState.gui_open_path]);
|
||||||
const setMasterVstChain = (next) => setOzState(prev => {
|
const setMasterVstChain = (next) => setOzState(prev => {
|
||||||
@@ -14284,7 +14287,24 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
|||||||
const toggleMasterVst = (idx) => setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
|
const toggleMasterVst = (idx) => setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
|
||||||
const removeMasterVst = (idx) => { const rem = masterVstChain[idx]; if (rem) delete _capturedPresetByPath[rem.path]; setMasterVstChain(masterVstChain.filter((_, i) => i !== idx)); };
|
const removeMasterVst = (idx) => { const rem = masterVstChain[idx]; if (rem) delete _capturedPresetByPath[rem.path]; setMasterVstChain(masterVstChain.filter((_, i) => i !== idx)); };
|
||||||
const moveMasterVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= masterVstChain.length) return; const next = [...masterVstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setMasterVstChain(next); };
|
const moveMasterVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= masterVstChain.length) return; const next = [...masterVstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setMasterVstChain(next); };
|
||||||
const openMasterVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (masterGuiOpen && masterGuiOpen.path === s.path) { setActiveMasterVstPath(s.path); return; } if (masterGuiOpen) closeMasterGui(); if (s.path) setActiveMasterVstPath(s.path); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true, shm: fxRt.sessionId || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' }).then(r => { if (r && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
|
const openMasterVstGui = (s) => {
|
||||||
|
if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; }
|
||||||
|
if (masterGuiOpen && masterGuiOpen.path === s.path) { setActiveMasterVstPath(s.path); return; }
|
||||||
|
// Request trước cho GUI embed còn đang bay (verdict 20-70s với Ozone-class):
|
||||||
|
// chặn click lặp — nếu không sẽ spawn bridge GUI thứ 2 → crash (0xc0000005).
|
||||||
|
if (masterGuiBusyRef.current) { window.showToast && window.showToast('GUI đang mở, chờ chút...', 'info'); return; }
|
||||||
|
if (masterGuiOpen) closeMasterGui();
|
||||||
|
if (s.path) setActiveMasterVstPath(s.path);
|
||||||
|
if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; }
|
||||||
|
masterGuiBusyRef.current = true;
|
||||||
|
window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true, shm: fxRt.sessionId || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' }).then(r => {
|
||||||
|
masterGuiBusyRef.current = false;
|
||||||
|
if (r && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); }
|
||||||
|
else if (r && r.already_running && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); }
|
||||||
|
else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info');
|
||||||
|
else window.showToast && window.showToast('Không mở được GUI embed', 'error');
|
||||||
|
}).catch(err => { masterGuiBusyRef.current = false; window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error'); });
|
||||||
|
};
|
||||||
const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.path === path); if (!fx) return; setMasterVstChain([...masterVstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]);
|
const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.path === path); if (!fx) return; setMasterVstChain([...masterVstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]);
|
||||||
openMasterVstGui({ path: fx.path, name: fx.name || fx.id }); };
|
openMasterVstGui({ path: fx.path, name: fx.name || fx.id }); };
|
||||||
const changeMasterVstPlugin = (idx, path) => { const fx = (masterFxList || []).find(f => f.path === path); delete _capturedPresetByPath[path]; setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
|
const changeMasterVstPlugin = (idx, path) => { const fx = (masterFxList || []).find(f => f.path === path); delete _capturedPresetByPath[path]; setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
|
||||||
|
|||||||
Reference in New Issue
Block a user