// SonicForge DAW — desktop shell: spawn/terminate daw_engine sidecar. // // QUAN TRONG (fix bản 1.1.1 — engine không chạy trên Windows): // tauri.conf.json bundle.resources giờ dùng dạng MAP: // { "resources/daw_engine": "daw_engine/" } // vì dạng ARRAY ("resources/daw_engine") copy file tới // $RESOURCE_DIR/resources/daw_engine/... (giữ tiền tố "resources/"), // trong khi code cũ tìm ở $RESOURCE_DIR/daw_engine/... -> exists=false. // Dạng map (Walk mode) giữ nguyên cây thư mục (_internal) dưới đích // "daw_engine/" — đúng layout lib.rs chờ. // Ngoài ra lib.rs còn dò THÊM các vị trí fallback (legacy/portable/dev) // và ghi đầy đủ diagnostic vào %APPDATA%/SonicForgeDAW/logs/spawn.log. use tauri::{AppHandle, Emitter, Manager}; use tauri_plugin_dialog::{DialogExt, FilePath}; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_shell::ShellExt; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Mutex; mod shm; /// App đang tắt (cửa sổ chính đã Destroyed) — pump thread phải dừng ngay, /// không respawn bridge mới sau khi sidecar đã bị giết (fix: app còn sống /// sau khi đóng window → pump thấy stall 3s → spawn bridge mới → orphan). static SHUTTING_DOWN: AtomicBool = AtomicBool::new(false); // F-PROC-2 (audit 5.2): serialize kill_bridge -> spawn_bridge across the pump // watchdog thread and the sample-rate restart command. Without it both threads // can kill+spawn concurrently and leave two daw_vst_bridge.exe on the same SHM. static BRIDGE_RESTART_LOCK: Mutex<()> = Mutex::new(()); use shm::{Shm, ShmState}; struct EngineProcess(Mutex>); struct BridgeProcess(Mutex>); struct BridgeSampleRate(Mutex); /// Overwrite the managed bridge child. `app.manage` is a NO-OP once the state /// exists (StateManager::set keeps the first value), so restarts would leave /// the state at None and the Destroyed handler would never kill the new /// bridge — orphan daw_vst_bridge.exe. First call still uses manage(). fn set_bridge_child(app: &AppHandle, child: Option) { if let Some(state) = app.try_state::() { if let Ok(mut g) = state.0.lock() { *g = child; } } else { app.manage(BridgeProcess(Mutex::new(child))); } } /// G2.2: ask the bridge to flush its state file NOW (control type=5 /// DUMP_STATE), then give the UiThread save job ~300ms to finish before /// the process is killed. Harmless when the bridge is already hung (SHM /// is owned by Rust; the control is simply never consumed). fn flush_bridge_state(app: &AppHandle) { if let Some(state) = app.try_state::() { if let Ok(guard) = state.0.lock() { if let Some(shm) = guard.as_ref() { let _ = shm.push_control(5, 0, 0, 0, ""); } } } std::thread::sleep(std::time::Duration::from_millis(300)); } /// Kill the currently managed bridge child (if any) — a restart MUST never /// leave the old bridge running, else two daw_vst_bridge.exe race on the same /// SHM (double instrument load, double OPEN_GUI, garbled control queue). fn kill_bridge(app: &AppHandle) -> bool { flush_bridge_state(app); let state = app.state::(); let mut guard = match state.0.lock() { Ok(g) => g, Err(_) => return false, }; match guard.take() { Some(child) => { let ok = kill_process_tree(&child) || child.kill().is_ok(); if ok { println!("Native Host Bridge killed (restart)"); } else { println!("Bridge kill failed"); } ok } None => false, } } /// Kill a child plus its whole process tree via taskkill /T /F. The engine /// (PyInstaller ONEDIR) and bridge are direct children, but /T guarantees no /// orphan subprocess stays behind when the DAW window closes. fn kill_process_tree(child: &CommandChild) -> bool { let pid = child.pid(); std::process::Command::new("taskkill") .args(["/PID", &pid.to_string(), "/T", "/F"]) .status() .map(|s| s.success()) .unwrap_or(false) } /// G3.2: true if a process with `pid` still exists (OpenProcess + exit code). /// Used to classify a stalled heartbeat: pid alive = HANG, pid gone = CRASH. fn process_alive(pid: u32) -> bool { use windows_sys::Win32::Foundation::STILL_ACTIVE; use windows_sys::Win32::System::Threading::{GetExitCodeProcess, OpenProcess}; let handle = unsafe { OpenProcess(windows_sys::Win32::System::Threading::PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) }; if handle.is_null() { return false; // not found or no access -> treat as gone } let mut code: u32 = 0; let ok = unsafe { GetExitCodeProcess(handle, &mut code) } != 0; unsafe { windows_sys::Win32::Foundation::CloseHandle(handle) }; ok && code == STILL_ACTIVE as u32 } /// Audio frame pushed to the WebView (bridge SHM -> `bridge-audio` event). #[derive(Clone, serde::Serialize)] struct AudioFrame { l: Vec, r: Vec, } /// Các vị trí có thể chứa daw_engine, theo thứ tự ưu tiên. fn engine_candidates(res_dir: &Path, exe_dir: &Path) -> Vec<(PathBuf, String)> { let exe_name = if cfg!(windows) { "daw_engine.exe" } else { "daw_engine" }; vec![ // 1. Layout chuẩn (resources map): $RESOURCE/daw_engine/daw_engine.exe ( res_dir.join("daw_engine").join(exe_name), "resource_dir/daw_engine (map layout)".into(), ), // 2. Legacy (resources array cũ giữ tiền tố resources/) ( res_dir.join("resources").join("daw_engine").join(exe_name), "resource_dir/resources/daw_engine (legacy array)".into(), ), // 3. Portable: engine đặt cạnh exe ( exe_dir.join("daw_engine").join(exe_name), "exe_dir/daw_engine (portable)".into(), ), // 4. Dev mode: target/{debug,release} -> src-tauri/resources ( exe_dir.join("..").join("resources").join("daw_engine").join(exe_name), "exe_dir/../resources/daw_engine (dev)".into(), ), ] } /// Các vị trí có thể chứa daw_vst_bridge (Native Host Bridge sidecar). fn bridge_candidates(res_dir: &Path, exe_dir: &Path) -> Vec<(PathBuf, String)> { let exe_name = if cfg!(windows) { "daw_vst_bridge.exe" } else { "daw_vst_bridge" }; let triple_name = if cfg!(windows) { "daw_vst_bridge-x86_64-pc-windows-msvc.exe" } else { exe_name }; vec![ (res_dir.join("binaries").join(exe_name), "resource_dir/binaries (bundle)".into()), (res_dir.join(exe_name), "resource_dir (bundle root)".into()), (res_dir.join("binaries").join(triple_name), "resource_dir/binaries (triple name)".into()), (res_dir.join(triple_name), "resource_dir (triple name, bundle)".into()), (exe_dir.join(triple_name), "exe_dir (triple name, bundle)".into()), (exe_dir.join(exe_name), "exe_dir (portable)".into()), (exe_dir.join("..").join("resources").join("binaries").join(exe_name), "exe_dir/../resources/binaries (dev)".into()), (exe_dir.join("..").join("resources").join(exe_name), "exe_dir/../resources (dev)".into()), ] } /// Spawn daw_vst_bridge.exe sidecar (B3/B8) with SHM + watchdog + audio env. /// Appends candidate diagnostics to spawn.log; manages `BridgeProcess`. /// Returns true when the child started. fn spawn_bridge( app: &AppHandle, res_dir: &Path, exe_dir: &Path, log_line: &mut String, spawn_log_path: &Path, ) -> bool { let candidates = bridge_candidates(res_dir, exe_dir); let mut found: Option<(PathBuf, String)> = None; for (path, label) in &candidates { let exists = path.exists(); log_line.push_str(&format!(" [bridge {label}] {} exists={}\n", path.display(), exists)); if found.is_none() && exists { found = Some((path.clone(), label.clone())); } } // B3 DoD: dòng tóm tắt trạng thái bridge trong spawn.log log_line.push_str(&format!("[daw_vst_bridge] exists={} ", found.is_some())); if let Ok(mut f) = std::fs::OpenOptions::new() .create(true) .append(true) .open(spawn_log_path) { use std::io::Write; let _ = f.write_all(log_line.as_bytes()); } match found { Some((bridge_exe, label)) => { let sample_rate = { let rate_state = app.state::(); let g = rate_state.0.lock().unwrap_or_else(|p| p.into_inner()); *g }; match app .shell() .command(&bridge_exe) .env("SF_SHM_NAME", shm::shm_name()) .env("SF_PARENT_PID", std::process::id().to_string()) .env("SF_SAMPLE_RATE", sample_rate.to_string()) .env("SF_BLOCK_SIZE", "256") // G2.3: point the bridge at the persisted state file so a // respawn can auto-reload instruments (StateStore::defaultPath). .env("SF_STATE_FILE", std::env::var("APPDATA") .unwrap_or_else(|_| ".".into()) + "/SonicForgeDAW/state/bridge_state.json") .spawn() { Ok((mut rx, child)) => { set_bridge_child(app, Some(child)); // B3: redirect bridge stdout/stderr → %APPDATA%/SonicForgeDAW/logs/bridge.log // (tauri-shell pipe rồi vứt rx; đọc event ghi file — E5 UI tail được). let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into()); let bridge_log = std::path::Path::new(&log_dir) .join("SonicForgeDAW").join("logs").join("bridge.log"); if let Some(parent) = bridge_log.parent() { let _ = std::fs::create_dir_all(parent); } tauri::async_runtime::spawn(async move { use std::io::Write; let mut f = match std::fs::OpenOptions::new().create(true).append(true).open(&bridge_log) { Ok(f) => f, Err(_) => return, }; while let Some(ev) = rx.recv().await { let bytes: Vec = match ev { CommandEvent::Stdout(b) | CommandEvent::Stderr(b) => b, _ => continue, }; if let Ok(text) = String::from_utf8(bytes) { let _ = writeln!(f, "{}", text.trim_end()); } } }); println!("Native Host Bridge started ({label}): {}", bridge_exe.display()); true } Err(e) => { println!("Failed to spawn daw_vst_bridge {bridge_exe:?}: {e}"); set_bridge_child(app, None); false } } } None => { println!("daw_vst_bridge binary not found — app will use WASM fallback"); set_bridge_child(app, None); false } } } fn list_dir_snippet(dir: &Path) -> String { let mut s = String::new(); match std::fs::read_dir(dir) { Ok(rd) => { let mut n = 0; for e in rd.flatten() { if n > 0 { s.push_str(", "); } s.push_str(&e.file_name().to_string_lossy()); n += 1; if n >= 15 { s.push_str(", ..."); break; } } } Err(_) => s.push_str(""), } s } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) .invoke_handler(tauri::generate_handler![ push_midi_event, load_native_instrument, open_vst_gui, bridge_status, transport_control, set_fx_chain, restart_bridge_with_sample_rate ]) .setup(|app| { // V10 (FIX_DRAG_DROP_WINDOWS): disable Tauri's built-in drag-drop // handler so WebView2 receives OS file drops (HTML5 DnD works). // Tauri default handler + SetAllowExternalDrop(false) swallows // Explorer .mid drops — app.jsx onDrop never fires on Windows. // Builder-only API, so the main window is created here instead of // tauri.conf.json (config-declared windows cannot disable it). let _ = tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::default()) .title("Sonic Forge DAW - Professional Desktop Studio") .inner_size(1440.0, 900.0) .resizable(true) .devtools(true) .disable_drag_drop_handler() .build()?; let res_dir = app .path() .resource_dir() .expect("resource dir not found"); let exe_dir = std::env::current_exe() .ok() .and_then(|p| p.parent().map(|d| d.to_path_buf())) .unwrap_or_default(); let candidates = engine_candidates(&res_dir, &exe_dir); // Ghi log spawn de chan doan — vao %APPDATA%/SonicForgeDAW/logs/ // spawn.log (thu muc luon ton tai), KHONG ghi vao engine_dir. let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into()); let spawn_log_path = std::path::Path::new(&log_dir) .join("SonicForgeDAW") .join("logs") .join("spawn.log"); if let Some(parent) = spawn_log_path.parent() { let _ = std::fs::create_dir_all(parent); } let mut log_line = format!( "resource_dir={}\nexe_dir={}\n", res_dir.display(), exe_dir.display() ); let mut found: Option<(PathBuf, String)> = None; for (path, label) in &candidates { let exists = path.exists(); log_line.push_str(&format!( " [{label}] {} exists={}\n", path.display(), exists )); if found.is_none() && exists { found = Some((path.clone(), label.clone())); } } if found.is_none() { log_line.push_str(&format!( " NOT FOUND — resource_dir contents: {}\n exe_dir contents: {}\n", list_dir_snippet(&res_dir), list_dir_snippet(&exe_dir) )); } if let Ok(mut f) = std::fs::OpenOptions::new() .create(true) .append(true) .open(&spawn_log_path) { use std::io::Write; let _ = f.write_all(log_line.as_bytes()); } match found { Some((engine_exe, label)) => { match app .shell() .command(&engine_exe) .env("SF_PARENT_PID", std::process::id().to_string()) .spawn() { Ok((_rx, child)) => { app.manage(EngineProcess(Mutex::new(Some(child)))); println!( "Python Background Engine started ({label}): {}", engine_exe.display() ); } Err(e) => { println!("Failed to spawn daw_engine {engine_exe:?}: {e}"); app.manage(EngineProcess(Mutex::new(None))); } } } None => { println!( "daw_engine binary not found in any candidate path — xem spawn.log de biet chi tiet" ); app.manage(EngineProcess(Mutex::new(None))); } } // ── Native Host Bridge (daw_vst_bridge.exe): SHM + sidecar spawn ── let shm_created = Shm::create(); println!( "SharedMemory {}: {}", shm::shm_name(), if shm_created.is_some() { "created" } else { "unavailable (non-windows or error)" } ); app.manage(ShmState(Mutex::new(shm_created))); app.manage(BridgeSampleRate(Mutex::new(48000))); // Append bridge diagnostics to spawn.log and spawn the sidecar (B3). let app_handle = app.handle().clone(); let _ = spawn_bridge(&app_handle, &res_dir, &exe_dir, &mut log_line, &spawn_log_path); // ── Audio pump: bridge SHM -> WebView `bridge-audio` events ── // G3.1: bridge liveness keys on the DEDICATED heartbeat (a bridge // thread, independent of the audio loop). A slow-but-alive audio // loop no longer looks dead. writeIndex is now only for audio // emission; a stalled writeIndex with a live heartbeat is logged // as an audio stall, never a restart. let pump_handle = app.handle().clone(); std::thread::spawn(move || { let mut last_index: u32 = 0; let mut last_hb: u32 = 0; let mut last_hb_change = std::time::Instant::now(); let mut last_audio_change = std::time::Instant::now(); let mut down_emitted = false; let mut stall_logged = false; let mut restart_attempts = 0u32; loop { // App đang tắt (window Destroyed) — dừng pump, không respawn bridge. if SHUTTING_DOWN.load(Ordering::Relaxed) { break; } let state = pump_handle.state::(); let guard = match state.0.lock() { Ok(g) => g, Err(_) => { std::thread::sleep(std::time::Duration::from_millis(10)); continue; } }; // Liveness = heartbeat advancing (~10Hz from bridge thread). let hb_advanced = guard.as_ref().map(|shm| { let hb = shm.heartbeat(); if hb != last_hb { last_hb = hb; true } else { false } }); if hb_advanced == Some(true) { last_hb_change = std::time::Instant::now(); down_emitted = false; // Bridge sống lại sau restart — đóng "sự cố" để lần // stall sau lại được hưởng 1 restart (mỗi incident 1 lần). restart_attempts = 0; } // Audio emission = writeIndex advancing (audio loop alive). // VSTi crackle fix: drain the FULL delta from the multi-slot // ring (8 slots) — a delayed pump no longer drops whole // blocks that a single-slot SHM would have overwritten. let mut audio_advanced = None; if let Some(s) = guard.as_ref() { let idx = s.write_index(); if idx != last_index { let delta = idx.wrapping_sub(last_index); // Only the last 8 blocks are still intact in the ring; // if the pump stalled longer, older blocks are lost. let start = if delta as usize > 8 { idx - 8 } else { last_index }; for k in start..idx { let (l, r) = s.read_ring_audio(k); let _ = pump_handle.emit( "bridge-audio", AudioFrame { l: l.to_vec(), r: r.to_vec() }, ); } if delta as usize > 8 { eprintln!("[tauri] audio pump lost {} blocks (stalled > ring)", delta - 8); } last_index = idx; audio_advanced = Some(true); } } if audio_advanced == Some(true) { last_audio_change = std::time::Instant::now(); stall_logged = false; } // A14: plugin MIDI output (VST2 audioMaster ProcessEvents // -> SHM midiOutQueue). Drain and forward to the page; // JS routes the notes to DAW tracks. Payload is a JSON // array of MidiEventIPC {command, channel, pitch, // velocity, data2, data3, reserved, sample_offset}. if let Some(s) = guard.as_ref() { let out = s.drain_midi_out(); if !out.is_empty() { let _ = pump_handle.emit("bridge-midi-out", out); } } // G3.1 FIX: nhả guard TRƯỚC stall log / watchdog kill+spawn. // kill_bridge -> flush_bridge_state khóa chính ShmState mutex // mà pump đang giữ -> deadlock khi watchdog trigger (không // restart, không log, bridge chết vĩnh viễn). drop(guard); if audio_advanced != Some(true) && hb_advanced == Some(true) && last_audio_change.elapsed() >= std::time::Duration::from_secs(3) && !stall_logged { // Heartbeat OK nhưng audio loop không push block — stall // thực sự (không phải bridge chết). Log, không restart. stall_logged = true; let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into()); let bridge_log = std::path::Path::new(&log_dir) .join("SonicForgeDAW").join("logs").join("bridge.log"); if let Ok(mut f) = std::fs::OpenOptions::new() .create(true).append(true).open(&bridge_log) { use std::io::Write; let _ = f.write_all(b"[tauri] audio stall (heartbeat OK)\n"); } } if hb_advanced == Some(false) && !down_emitted && last_hb_change.elapsed() >= std::time::Duration::from_secs(3) { // Check lai truoc khi respawn — Destroyed co the vua set flag. if SHUTTING_DOWN.load(Ordering::Relaxed) { break; } down_emitted = true; // FIX auto-play: bao page ngay khi bridge sap restart -- // page stop playback (isPlaying=false) de watchdog recovery // khong tu resume phat lai (auto-play) sau reconnect. let _ = pump_handle.emit("bridge-down", ()); if restart_attempts == 0 { restart_attempts += 1; let res_dir = pump_handle.path().resource_dir().unwrap_or_default(); let exe_dir = std::env::current_exe() .ok() .and_then(|p| p.parent().map(|d| d.to_path_buf())) .unwrap_or_default(); let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into()); let log_path = std::path::Path::new(&log_dir) .join("SonicForgeDAW").join("logs").join("spawn.log"); // G3.2: phân loại stall — process còn sống = HANG // (heartbeat thread kẹt), pid biến mất = CRASH. let pid = pump_handle .state::() .0 .lock() .ok() .and_then(|g| g.as_ref().map(|c| c.pid())); let kind = match pid { Some(p) if process_alive(p) => { format!("HUNG (pid {p} alive, heartbeat stalled)") } Some(p) => format!("CRASHED (pid {p} gone)"), None => "UNKNOWN (no tracked pid)".into(), }; let mut line = format!( "[tauri] bridge heartbeat stalled 3s — {kind} — restart attempt #1\n" ); // FIX: kill bridge cũ trước — không được để 2 bridge // cùng map SHM (race control queue / double load). // F-PROC-2: serialize with the sample-rate restart // command (same BRIDGE_RESTART_LOCK). let _restart_guard = match BRIDGE_RESTART_LOCK.lock() { Ok(g) => g, Err(p) => p.into_inner(), }; // F-PROC-3: re-check after acquiring the lock AND // after kill — Destroyed may have been set while we // waited/killed; never respawn during app shutdown. if SHUTTING_DOWN.load(Ordering::Relaxed) { break; } kill_bridge(&pump_handle); if SHUTTING_DOWN.load(Ordering::Relaxed) { break; } if spawn_bridge(&pump_handle, &res_dir, &exe_dir, &mut line, &log_path) { // count restarts into bridge.log (same file as stdout redirect) let bridge_log = std::path::Path::new(&log_dir) .join("SonicForgeDAW").join("logs").join("bridge.log"); if let Ok(mut f) = std::fs::OpenOptions::new() .create(true).append(true).open(&bridge_log) { use std::io::Write; let _ = f.write_all(b"[tauri] bridge restarted (attempt 1) "); } last_hb_change = std::time::Instant::now(); down_emitted = false; } else { let _ = pump_handle.emit("bridge-down", ()); } } else { let _ = pump_handle.emit("bridge-down", ()); } } std::thread::sleep(std::time::Duration::from_millis(1)); } }); // ── Native folder picker bridge (folder picker cho Plugin Manager) ── // UI chay tren http://127.0.0.1:8000 (engine) — KHONG co __TAURI__ // (WebView2 cung khong ho tro window.prompt) → engine goi qua file // IPC: engine ghi ipc/pick_dir.request → thread nay mo NATIVE dialog // (IFileDialog/Explorer — tauri-plugin-dialog) → ghi ket qua vao // ipc/pick_dir.response → engine tra ve cho frontend. let data_root = std::env::var("APPDATA").unwrap_or_else(|_| ".".into()); let ipc_dir = std::path::Path::new(&data_root) .join("SonicForgeDAW") .join("ipc"); if let Ok(()) = std::fs::create_dir_all(&ipc_dir) { // Marker: engine biet bridge ton tai (khong phai chay standalone) let _ = std::fs::write(ipc_dir.join("tauri_bridge_ready"), "1"); } let ipc_dir_for_thread = ipc_dir.clone(); let app_handle = app.handle().clone(); std::thread::spawn(move || { loop { let req = ipc_dir_for_thread.join("pick_dir.request"); if req.exists() { let _ = std::fs::remove_file(&req); let resp = ipc_dir_for_thread.join("pick_dir.response"); let _ = std::fs::remove_file(&resp); // Dialog phai chay tren main thread (GTK/Windows message loop) let handle = app_handle.clone(); let resp_for_main = resp.clone(); // Clone rieng cho closure: run_on_main_thread(&self, F) // borrow `handle`, closure (move) phai dung ban clone. let handle_for_closure = handle.clone(); let _ = handle.run_on_main_thread(move || { // tauri-plugin-dialog v2: blocking_pick_folder() tra // Option (enum Path(PathBuf) | Url(Url)) let picked: Option = handle_for_closure .dialog() .file() .blocking_pick_folder(); let val = picked .map(|p| match p { FilePath::Path(pb) => pb.to_string_lossy().to_string(), FilePath::Url(u) => u.to_string(), }) .unwrap_or_default(); let _ = std::fs::write(&resp_for_main, val); }); // Cho den khi co response (user co the de dialog mo lau) let mut waited_ms = 0u32; while !resp.exists() && waited_ms < 300_000 { std::thread::sleep(std::time::Duration::from_millis(100)); waited_ms += 100; } } std::thread::sleep(std::time::Duration::from_millis(150)); } }); Ok(()) }) .on_window_event(|window, event| { // Terminate sidecar khi DAW window dong — tranh orphan process if let tauri::WindowEvent::Destroyed = event { // Dung pump thread: khong respawn bridge sau khi sidecar da bi giet. SHUTTING_DOWN.store(true, Ordering::Relaxed); let child = window .state::() .0 .lock() .ok() .and_then(|mut lock| lock.take()); if let Some(child) = child { kill_process_tree(&child); let _ = child.kill(); println!("daw_engine sidecar terminated."); } // G2.2: flush state to disk before killing the bridge flush_bridge_state(window.app_handle()); // Terminate Native Host Bridge khi DAW window dong let bridge_child = window .state::() .0 .lock() .ok() .and_then(|mut lock| lock.take()); if let Some(child) = bridge_child { kill_process_tree(&child); let _ = child.kill(); println!("daw_vst_bridge sidecar terminated."); } // Cua so chinh da dong — ep app thoat han. Neu con window // phu/an giu event loop song, Tauri khong tu exit; exit(0) // ket thuc dut diem (pump da dung, khong con respawn bridge). if window.label() == "main" { // Orphan sweep: bat CA dau engine con song theo image name — // engine spawn thu cong (khong co SF_PARENT_PID), engine cua // lan chay truoc bi crash, hoac engine song sot khi app chet // dot ngot (watchdog graceful shutdown treo vi LAN client). // Task Manager dong app -> Destroyed khong chay -> engine tu // thoat nho watchdog; o day dong app binh thuong -> quet het. let _ = std::process::Command::new("taskkill") .args(["/IM", "daw_engine.exe", "/T", "/F"]) .status(); let _ = std::process::Command::new("taskkill") .args(["/IM", "daw_vst_bridge.exe", "/T", "/F"]) .status(); window.app_handle().exit(0); } } }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } // ── Native Host Bridge Tauri commands ────────────────────────────────────── #[tauri::command] fn push_midi_event( app: AppHandle, command: u8, channel: u8, pitch: u8, velocity: u8, data2: u8, data3: u8, sample_offset: u32, live: bool, ) -> Result<(), String> { let state = app.state::(); let guard = state.0.lock().map_err(|e| e.to_string())?; let shm = guard.as_ref().ok_or("bridge shm unavailable")?; shm.push_midi(command, channel, pitch, velocity, data2, data3, sample_offset, live as u8) .then_some(()) .ok_or_else(|| "midi queue full".to_string()) } #[tauri::command] fn load_native_instrument(app: AppHandle, path: String, instrument_type: u8, channel: u8) -> Result<(), String> { let state = app.state::(); let guard = state.0.lock().map_err(|e| e.to_string())?; let shm = guard.as_ref().ok_or("bridge shm unavailable")?; shm.push_control(2, instrument_type as u32, 0, channel as u32, &path) .then_some(()) .ok_or_else(|| "control queue full".to_string()) } #[tauri::command] fn open_vst_gui(app: AppHandle, plugin_id: String, channel: u8) -> Result<(), String> { // B9: bridge tự tạo native Win32 window cho editor VST3 (không qua // WebView2 — HTML window cũ vẽ ĐÈ lên GUI plugin). push_control(4,0,0,0) // với hwnd=0 báo bridge tạo window riêng trên ChannelWorker thread. // F5 (audit 5.2): trả Err khi lệnh không tới được bridge (SHM unavailable / // lock poisoned / queue full) — frontend openNativeGUI đã try/catch sẵn. let state = app.state::(); let guard = state.0.lock().map_err(|e| e.to_string())?; let shm = guard.as_ref().ok_or("bridge shm unavailable")?; let ok = shm.push_control(4, 0, 0, channel as u32, &plugin_id); if !ok { eprintln!("open_vst_gui: push_control type=4 hwnd=0 ch={} failed", channel); return Err("control queue full".into()); } Ok(()) } #[derive(serde::Serialize)] struct BridgeStatus { connected: bool, shm_name: String, write_index: u32, block_timestamp: u64, shm_size_bytes: usize, sample_rate: u32, block_size: u32, // V10: channel -> path mà C++ đã load/restore (bridge_state.json). JS dùng // làm nguồn sự thật khi webview không load được qua HTTP (token hết hạn / // listPlugins thiếu path) — C++ restore độc lập nên GUI vẫn mở được. loaded_channels: Vec, } #[derive(serde::Serialize)] struct BridgeLoadedChannel { channel: u32, path: String, } #[derive(serde::Deserialize)] struct BridgeStateFile { #[serde(default)] instruments: Vec, } #[derive(serde::Deserialize)] struct BridgeStateInstrument { channel: u32, path: String, } /// %APPDATA%/SonicForgeDAW/state/bridge_state.json — C++ StateStore::save ghi /// mỗi ~2s khi có instrument + khi DUMP_STATE lúc pre-kill (main.cpp G2.1). fn read_bridge_loaded_channels() -> Vec { let Some(appdata) = std::env::var("APPDATA").ok() else { return Vec::new() }; let path = Path::new(&appdata).join("SonicForgeDAW").join("state").join("bridge_state.json"); let Ok(raw) = std::fs::read_to_string(&path) else { return Vec::new() }; let Ok(state): Result = serde_json::from_str(&raw) else { return Vec::new() }; state.instruments .iter() .map(|i| BridgeLoadedChannel { channel: i.channel, path: i.path.clone() }) .collect() } /// %APPDATA%/SonicForgeDAW/ipc — cùng dir engine (plugins.py) đọc cho /// /api/v1/bridge/status; None khi APPDATA unset (non-Windows). fn bridge_ipc_dir() -> Option { let appdata = std::env::var("APPDATA").ok()?; Some(Path::new(&appdata).join("SonicForgeDAW").join("ipc")) } #[tauri::command] fn bridge_status(app: AppHandle) -> Result { let state = app.state::(); let guard = state.0.lock().map_err(|e| e.to_string())?; let mut status = match guard.as_ref() { Some(shm) => BridgeStatus { connected: true, shm_name: shm::shm_name(), write_index: shm.write_index(), block_timestamp: shm.block_timestamp(), shm_size_bytes: std::mem::size_of::(), sample_rate: { let rate_state = app.state::(); let g = rate_state.0.lock().unwrap_or_else(|p| p.into_inner()); *g }, block_size: shm::AUDIO_BLOCK_SIZE as u32, loaded_channels: Vec::new(), }, None => BridgeStatus { connected: false, shm_name: shm::shm_name(), write_index: 0, block_timestamp: 0, shm_size_bytes: 0, sample_rate: 0, block_size: 0, loaded_channels: Vec::new(), }, }; // V10: C++ loaded channels (state file) — không phụ thuộc HTTP/auth. status.loaded_channels = read_bridge_loaded_channels(); // E1: ghi JSON → engine /api/v1/bridge/status (badge Plugin Manager phải // phản ánh đúng bridge thật — trước đây file không tồn tại → luôn OFF). if let Some(dir) = bridge_ipc_dir() { let _ = std::fs::create_dir_all(&dir); if let Ok(json) = serde_json::to_string(&status) { let _ = std::fs::write(dir.join("bridge_status"), json); } } Ok(status) } #[tauri::command] fn transport_control(app: AppHandle, kind: String, playhead: Option) -> Result<(), String> { let state = app.state::(); let guard = state.0.lock().map_err(|e| e.to_string())?; let shm = guard.as_ref().ok_or("bridge shm unavailable")?; // arg0: 0=STOP (flush), 1=PLAY, 2=SET_POSITION (seek, no flush) let (arg0, arg1) = match kind.as_str() { "play" => (1, playhead.unwrap_or(0)), "set_position" => (2, playhead.unwrap_or(0)), _ => (0, playhead.unwrap_or(0)), // stop / panic }; shm.push_control(3, arg0, arg1, 0, "") .then_some(()) .ok_or_else(|| "control queue full".to_string()) } #[tauri::command] fn set_fx_chain(app: AppHandle, chain_json: String) -> Result<(), String> { if chain_json.len() >= shm::CONTROL_PATH_MAX { return Err("fx chain JSON too large".to_string()); } let state = app.state::(); let guard = state.0.lock().map_err(|e| e.to_string())?; let shm = guard.as_ref().ok_or("bridge shm unavailable")?; shm.push_control(6, 0, 0, 0, &chain_json) .then_some(()) .ok_or_else(|| "control queue full".to_string()) } #[tauri::command] fn restart_bridge_with_sample_rate(app: AppHandle, sample_rate: u32) -> Result<(), String> { println!("restart_bridge_with_sample_rate: {}", sample_rate); { let rate_state = app.state::(); { let mut g = rate_state.0.lock().unwrap_or_else(|p| p.into_inner()); *g = sample_rate; } } // F-PROC-2: serialize with the pump watchdog restart so two threads can // never both kill+spawn (two bridges on the same SHM). let _restart_guard = match BRIDGE_RESTART_LOCK.lock() { Ok(g) => g, Err(p) => p.into_inner(), }; kill_bridge(&app); let res_dir = app.path().resource_dir().unwrap_or_default(); let exe_dir = std::env::current_exe() .ok() .and_then(|p| p.parent().map(|d| d.to_path_buf())) .unwrap_or_default(); let log_dir = std::env::var("APPDATA").unwrap_or_else(|_| ".".into()); let spawn_log_path = std::path::Path::new(&log_dir) .join("SonicForgeDAW") .join("logs") .join("spawn.log"); let mut log_line = format!("[tauri] restarting bridge with sample rate {}\n", sample_rate); let ok = spawn_bridge(&app, &res_dir, &exe_dir, &mut log_line, &spawn_log_path); if ok { Ok(()) } else { Err("Failed to spawn bridge with new sample rate".into()) } }