856 lines
39 KiB
Rust
856 lines
39 KiB
Rust
// 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<Option<CommandChild>>);
|
|
struct BridgeProcess(Mutex<Option<CommandChild>>);
|
|
struct BridgeSampleRate(Mutex<u32>);
|
|
|
|
/// 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<CommandChild>) {
|
|
if let Some(state) = app.try_state::<BridgeProcess>() {
|
|
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::<ShmState>() {
|
|
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::<BridgeProcess>();
|
|
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<f32>,
|
|
r: Vec<f32>,
|
|
}
|
|
|
|
/// 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::<BridgeSampleRate>();
|
|
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<u8> = 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("<khong doc duoc dir>"),
|
|
}
|
|
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,
|
|
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::<ShmState>();
|
|
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;
|
|
}
|
|
// 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::<BridgeProcess>()
|
|
.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<FilePath> (enum Path(PathBuf) | Url(Url))
|
|
let picked: Option<FilePath> = 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::<EngineProcess>()
|
|
.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::<BridgeProcess>()
|
|
.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::<ShmState>();
|
|
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::<ShmState>();
|
|
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::<ShmState>();
|
|
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,
|
|
}
|
|
|
|
/// %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<PathBuf> {
|
|
let appdata = std::env::var("APPDATA").ok()?;
|
|
Some(Path::new(&appdata).join("SonicForgeDAW").join("ipc"))
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
|
|
let state = app.state::<ShmState>();
|
|
let guard = state.0.lock().map_err(|e| e.to_string())?;
|
|
let 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::<shm::SharedAudioBufferIPC>(),
|
|
sample_rate: {
|
|
let rate_state = app.state::<BridgeSampleRate>();
|
|
let g = rate_state.0.lock().unwrap_or_else(|p| p.into_inner());
|
|
*g
|
|
},
|
|
block_size: shm::AUDIO_BLOCK_SIZE as u32,
|
|
},
|
|
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,
|
|
},
|
|
};
|
|
// 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<u32>) -> Result<(), String> {
|
|
let state = app.state::<ShmState>();
|
|
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 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::<BridgeSampleRate>();
|
|
{
|
|
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())
|
|
}
|
|
}
|