M4: /fx-gui redirect vao session fxrt (native-window mode, embed_url=None) + watchdog auto-restart
This commit is contained in:
@@ -711,6 +711,24 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
|
||||
plugin_path = (req.path or "").strip()
|
||||
if not plugin_path:
|
||||
raise HTTPException(status_code=400, detail="Thiếu đường dẫn VST plugin.")
|
||||
# ── Phase 2 (M4): plugin là slot của session FX realtime (mastering) → mở
|
||||
# editor của CHÍNH instance DSP ngay trong fxrt_bridge (juce_fx_bridge) qua
|
||||
# SHM ctrl ring ed.open — KHÔNG spawn fx_vst_bridge instance 2 + feeder
|
||||
# (nguồn CPU 100%). GUI = cửa sổ native "FX slot N" trên desktop (không
|
||||
# HTTP embed frame → embed_url=None, native=true — frontend M6 dùng mode
|
||||
# này). Vẫn spawn fx_vst_bridge khi: không shm / session không tồn tại /
|
||||
# plugin không phải slot chain (instrument, offline, fallback an toàn).
|
||||
_sess = fx_realtime.get_session((req.shm or "").strip()) if (req.shm or "").strip() else None
|
||||
if _sess is not None and _sess.alive():
|
||||
_slot = _sess.chain_slot_for_path(plugin_path)
|
||||
if _slot is not None:
|
||||
_already = fx_realtime.editor_find(plugin_path)
|
||||
_same = _already is not None and _already[0] == _sess.name
|
||||
_sess.send_editor_cmd("open", slot=_slot)
|
||||
fx_realtime.editor_register(plugin_path, _sess.name, _slot)
|
||||
return {"success": True, "started": not _same,
|
||||
"already_running": bool(_same), "native": True,
|
||||
"embed_url": None, "session_id": _sess.name, "slot": _slot}
|
||||
# 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ũ).
|
||||
for proc in _FX_GUI_PROCESSES:
|
||||
@@ -808,6 +826,17 @@ async def close_fx_gui(req: FxGuiCloseRequest, current_user: dict = Depends(get_
|
||||
plugin_path = (req.path or "").strip()
|
||||
if not plugin_path:
|
||||
raise HTTPException(status_code=400, detail="Thiếu đường dẫn VST plugin.")
|
||||
# M4: plugin đang mở native (editor instance DSP trong session fxrt) →
|
||||
# gửi ed.close (đóng editor, GIỮ instance + state — mở lại nhanh).
|
||||
_entry = fx_realtime.editor_find(plugin_path)
|
||||
if _entry:
|
||||
_sid, _slot = _entry
|
||||
fx_realtime.editor_unregister(plugin_path)
|
||||
_sess = fx_realtime.get_session(_sid)
|
||||
if _sess is not None and _sess.alive():
|
||||
_sess.send_editor_cmd("close", slot=_slot)
|
||||
return {"success": True, "closed": True, "native": True}
|
||||
# session đã chết — rơi xuống spawn fallback (đóng bridge fx-gui cũ nếu có)
|
||||
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:
|
||||
proc.terminate()
|
||||
@@ -820,6 +849,49 @@ async def close_fx_gui(req: FxGuiCloseRequest, current_user: dict = Depends(get_
|
||||
return {"success": True, "closed": False}
|
||||
|
||||
|
||||
class FxGuiNativeRequest(BaseModel):
|
||||
"""Hide/Show GUI native (editor instance DSP trong session fxrt)."""
|
||||
path: str
|
||||
|
||||
|
||||
def _fxrt_native_editor(plugin_path):
|
||||
"""(session, slot) nếu plugin đang mở native trong session fxrt sống."""
|
||||
entry = fx_realtime.editor_find(plugin_path)
|
||||
if not entry:
|
||||
return None, None
|
||||
sess = fx_realtime.get_session(entry[0])
|
||||
if sess is None or not sess.alive():
|
||||
fx_realtime.editor_unregister(plugin_path)
|
||||
return None, None
|
||||
return sess, entry[1]
|
||||
|
||||
|
||||
@router.post("/fx-gui/hide")
|
||||
async def hide_fx_gui(req: FxGuiNativeRequest,
|
||||
current_user: dict = Depends(get_current_user)):
|
||||
"""Ẩn GUI native (ed.hide — giữ editor + instance, mở lại nhanh). Chỉ
|
||||
áp dụng editor in-process fxrt; GUI spawn mode (HTTP bridge) tự có /hide
|
||||
riêng — trả applied=False không lỗi."""
|
||||
enforce_password_changed(current_user)
|
||||
sess, slot = _fxrt_native_editor((req.path or "").strip())
|
||||
if sess is None:
|
||||
return {"success": True, "applied": False}
|
||||
sess.send_editor_cmd("hide", slot=slot)
|
||||
return {"success": True, "applied": True, "native": True}
|
||||
|
||||
|
||||
@router.post("/fx-gui/show")
|
||||
async def show_fx_gui(req: FxGuiNativeRequest,
|
||||
current_user: dict = Depends(get_current_user)):
|
||||
"""Hiện lại GUI native (ed.show)."""
|
||||
enforce_password_changed(current_user)
|
||||
sess, slot = _fxrt_native_editor((req.path or "").strip())
|
||||
if sess is None:
|
||||
return {"success": True, "applied": False}
|
||||
sess.send_editor_cmd("show", slot=slot)
|
||||
return {"success": True, "applied": True, "native": True}
|
||||
|
||||
|
||||
_FX_GUI_PROCESSES = [] # list[subprocess.Popen] — các bridge fx-gui do app spawn
|
||||
|
||||
def _register_fx_gui_process(proc):
|
||||
|
||||
@@ -16,6 +16,7 @@ Luồng dữ liệu: browser → WS → write_input() → [bridge xử lý] →
|
||||
"""
|
||||
import ctypes
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -34,6 +35,11 @@ FXRT_LAT_SLOTS = 8
|
||||
FXRT_STATE_STARTING = 0
|
||||
FXRT_STATE_READY = 1
|
||||
FXRT_STATE_ERROR = 2
|
||||
# M4.5 watchdog: số lần AUTO restart tối đa sau crash (editor in-process chết
|
||||
# không được giết session mãi — Reaper chấp nhận gap audio khi plugin crash).
|
||||
FXRT_MAX_RESTARTS = 3
|
||||
|
||||
_log = logging.getLogger("fx_realtime")
|
||||
|
||||
|
||||
class FxRTHeader(ctypes.Structure):
|
||||
@@ -131,7 +137,14 @@ class FxRealtimeSession:
|
||||
self.stats = {'publish': 0, 'drop_batch': 0, 'drop_blocks': 0,
|
||||
'stale_flush': 0, 'read_blocks': 0, 'read_empty': 0,
|
||||
'flush': 0}
|
||||
# M4.5 watchdog (chính sách FXRealtime đã duyệt): proc chết ngoài ý
|
||||
# muốn (crash view/plugin/loop) → AUTO restart; user kill / close() có
|
||||
# chủ đích → KHÔNG restart (cờ _closing / running=0 — user gọi lại).
|
||||
self._closing = False
|
||||
self._restarts = 0
|
||||
self._watch = threading.Thread(target=self._watch_loop, daemon=True)
|
||||
self._spawn()
|
||||
self._watch.start()
|
||||
|
||||
# ── process ─────────────────────────────────────────────────────────────
|
||||
def _chain_json_for_bridge(self):
|
||||
@@ -174,6 +187,13 @@ class FxRealtimeSession:
|
||||
"seq": self._chain_seq,
|
||||
"fx_chain": self._chain_json_for_bridge(),
|
||||
}
|
||||
# M4.5 respawn: job cũ không ai đọc (proc cũ đã chết) — dọn rác.
|
||||
_old_job = self._job_path
|
||||
if _old_job and os.path.exists(_old_job):
|
||||
try:
|
||||
os.remove(_old_job)
|
||||
except Exception:
|
||||
pass
|
||||
fd, self._job_path = tempfile.mkstemp(suffix=".json", prefix="fxrt_")
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
json.dump(job, f)
|
||||
@@ -235,6 +255,7 @@ class FxRealtimeSession:
|
||||
return self.proc is not None and self.proc.poll() is None
|
||||
|
||||
def close(self):
|
||||
self._closing = True # M4.5: watchdog không auto-restart khi close chủ đích
|
||||
try:
|
||||
self.h.running = 0
|
||||
except Exception:
|
||||
@@ -426,11 +447,133 @@ class FxRealtimeSession:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── editor control (Phase 2 — Reaper-style) ─────────────────────────────
|
||||
# M4: engine điều khiển editor của CHÍNH instance DSP trong juce_fx_bridge
|
||||
# qua SHM ctrl ring (key ed.*). Không spawn instance 2 / feeder.
|
||||
def send_editor_cmd(self, action, slot, w=0, h=0):
|
||||
"""Gửi lệnh editor (ed.open/close/show/hide/resize/capture) cho slot
|
||||
chain. w/h encode w*4096+h (chính xác tuyệt đối w,h<=4095 — float
|
||||
24-bit mantissa; xem M3); w,h<=0 → editor preferred size (bridge chỉ
|
||||
setSize khi w,h>0). Trả False nếu session không có SHM (fake object)."""
|
||||
key = "ed.%s" % (action or "").strip().lower()
|
||||
value = 0.0
|
||||
try:
|
||||
if int(w) > 0 and int(h) > 0:
|
||||
value = float(int(w) * 4096 + int(h))
|
||||
except Exception:
|
||||
value = 0.0
|
||||
with self._lock:
|
||||
if getattr(self, "shm", None) is None:
|
||||
return False
|
||||
try:
|
||||
self._ctrl_enqueue_locked(int(slot), key, value)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def chain_slot_for_path(self, plugin_path):
|
||||
"""Slot index (theo chain bridge ĐANG CHẠY — đã lọc active=false, giữ
|
||||
thứ tự) của plugin path; None nếu plugin không trong chain. Index khớp
|
||||
editors[] của JuceFxEngine (C++) — ed.* gửi theo index này."""
|
||||
norm = _norm_plugin_path(plugin_path or "")
|
||||
if not norm:
|
||||
return None
|
||||
for i, s in enumerate(self._chain_json_for_bridge()):
|
||||
p = (s.get("path") or "")
|
||||
if p and _norm_plugin_path(p) == norm:
|
||||
return i
|
||||
return None
|
||||
|
||||
def _watch_loop(self):
|
||||
"""M4.5 watchdog — thread riêng (ngoài audio). Proc chết ngoài ý muốn
|
||||
(crash view/plugin/loop) → AUTO restart tối đa FXRT_MAX_RESTARTS lần,
|
||||
log rc để đối chiếu CPU 100%. Không restart khi: close() chủ đích
|
||||
(_closing) / running=0 (user kill — user gọi lại). Respawn đúng cmd
|
||||
line (--juce-fx <job> --shm <name> --parent <pid>) qua _spawn()."""
|
||||
while True:
|
||||
time.sleep(0.2)
|
||||
with self._lock:
|
||||
if self._closing:
|
||||
return
|
||||
proc = self.proc
|
||||
if proc is None:
|
||||
return
|
||||
rc = proc.poll()
|
||||
if rc is None:
|
||||
continue
|
||||
if self._closing:
|
||||
return
|
||||
if getattr(self.h, "running", 1) == 0:
|
||||
_log.warning("[fxrt] bridge thoát rc=%s sau running=0 (user kill — không restart)", rc)
|
||||
return
|
||||
if self._restarts >= FXRT_MAX_RESTARTS:
|
||||
_log.error("[fxrt] bridge crash rc=%s — restart tối đa %d lần đã đạt, bỏ", rc, FXRT_MAX_RESTARTS)
|
||||
return
|
||||
self._restarts += 1
|
||||
_log.warning("[fxrt] bridge chết rc=%s — auto restart #%d", rc, self._restarts)
|
||||
try:
|
||||
self._spawn()
|
||||
except Exception as e:
|
||||
_log.error("[fxrt] respawn lỗi: %s", e)
|
||||
return
|
||||
# Chờ READY (thread riêng — không block API); crash lần nữa → vòng
|
||||
# ngoài restart tiếp (đếm _restarts đã tăng → dừng đúng giới hạn).
|
||||
deadline = time.time() + 8.0
|
||||
while time.time() < deadline:
|
||||
time.sleep(0.1)
|
||||
with self._lock:
|
||||
if self._closing or self.proc is None:
|
||||
return
|
||||
if self.proc.poll() is not None:
|
||||
break
|
||||
if self.h.state == FXRT_STATE_READY:
|
||||
break
|
||||
|
||||
# Registry toàn tiến trình (desktop app 1 user).
|
||||
_SESSIONS = {}
|
||||
_SESSIONS_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def _norm_plugin_path(p):
|
||||
"""Chuẩn hóa path plugin để so khớp (Windows case-insensitive)."""
|
||||
p = (p or "").strip()
|
||||
if not p:
|
||||
return ""
|
||||
return os.path.normcase(os.path.normpath(p))
|
||||
|
||||
# M4: mapping plugin path -> (session_id, slot) — GUI native đang mở (editor
|
||||
# instance DSP trong session fxrt). /fx-gui/{close,hide,show} chỉ nhận path →
|
||||
# tìm session qua registry này. Session chết → entry dọn khi dùng/stop.
|
||||
_FXRT_EDITORS = {}
|
||||
_FXRT_EDITORS_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def editor_register(plugin_path, session_id, slot):
|
||||
key = _norm_plugin_path(plugin_path)
|
||||
if not key:
|
||||
return
|
||||
with _FXRT_EDITORS_LOCK:
|
||||
_FXRT_EDITORS[key] = (session_id, int(slot))
|
||||
|
||||
|
||||
def editor_find(plugin_path):
|
||||
key = _norm_plugin_path(plugin_path)
|
||||
with _FXRT_EDITORS_LOCK:
|
||||
return _FXRT_EDITORS.get(key)
|
||||
|
||||
|
||||
def editor_unregister(plugin_path):
|
||||
key = _norm_plugin_path(plugin_path)
|
||||
with _FXRT_EDITORS_LOCK:
|
||||
_FXRT_EDITORS.pop(key, None)
|
||||
|
||||
|
||||
def editor_unregister_session(session_id):
|
||||
with _FXRT_EDITORS_LOCK:
|
||||
for k in [k for k, v in _FXRT_EDITORS.items() if v[0] == session_id]:
|
||||
_FXRT_EDITORS.pop(k, None)
|
||||
|
||||
|
||||
def start_session(fx_chain, sample_rate, block_size=FXRT_BLOCK):
|
||||
sess = FxRealtimeSession(fx_chain, sample_rate, block_size)
|
||||
try:
|
||||
@@ -453,6 +596,7 @@ def stop_session(session_id):
|
||||
sess = _SESSIONS.pop(session_id, None)
|
||||
if sess:
|
||||
sess.close()
|
||||
editor_unregister_session(session_id) # M4: dọn mapping GUI native
|
||||
return sess is not None
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user