fix 5.2 audit findings on Windows: F-PROC-1 injection, F-MEM-5 UAF, F-PROC-2 restart race, F-PROC-3, F5, F6
This commit is contained in:
@@ -229,6 +229,7 @@ Kết quả review toàn bộ diff G2 (`git diff 1fe2d26~1..b59e416`, 15 files;
|
||||
- **G2.5** (2026-08-16, Windows): tao native_bridge/debug/test_g2_stress.py (khung test_g15_stress.py). N vong: LOAD Nexus ch5 + EZkeys ch6 (real hoac skip deu OK) -> PLAY -> taskkill daw_vst_bridge -> cho watchdog "[tauri] bridge restarted" + "restored ch=5/6" -> LOAD lai -> verify "LOAD skip" (guard G2.4) + preset giu (ch5 exact, ch6 sim>=0.7 — EZkeys nhung PlayHeadPos vao state, ratio 0.872 giua save thong thuong) + writeIndex advancing. Chay 5 + 10 iterations: PASS (20 lan kill/respawn, khong crash, bridge alive). Kem sua test_g15_stress.py: LOAD ch5 chap nhan "LOAD skip" (guard G2.4 doi log). Commit `G2.5: stress driver for persistence restart (N loop + kill/respawn)`.
|
||||
- **G2.6** (2026-08-16, Windows): verify tong.
|
||||
- **G2.0–G2.6 review + verify** (2026-08-16, Linux — máy Linux chỉ review/ghi chú, KHÔNG sửa code): đọc toàn bộ diff G2 (`1fe2d26~1..b59e416`, 15 files) — Vst3Instrument base64 + loadSerializedState (bounds-check đủ), NativeInstrumentEngine snapshot (mu_ chặn race với audio loop, skip reloading), main.cpp saveStateJob/restore/LOAD-guard/DUMP/debounce, lib.rs flush_bridge_state + env SF_STATE_FILE, test_g2_stress.py + test_state_store.cpp + test_g15_stress.py. Verify Linux: py_compile 2 stress scripts PASS; g++ compile + run test_state_store PASS (StateStore portable); node --check app.precompiled.js PASS; rustc/cargo không có default toolchain → không cargo check (Windows đã check PASS). Findings: F1 double flush_bridge_state khi window-close (minor), F2 SF_STATE_FILE thiếu Linux fallback (portability, không ảnh hưởng target Windows), F3 log "bad ctrlLen" trùng cho 2 check (cosmetic). Không sửa code (đúng phân máy). Chi tiết ở mục 5.1. G1.5 regression PASS 5/5 (LOAD/OPEN_GUI/PLAY/STOP/WM_CLOSE, writeIndex delta 307-551, bridge alive). G2.5 PASS 5+5+10 (20 lan kill/respawn, preset giu, khong EXCEPTION/FAILED). Probe G0.4 (ban cap nhat: PID dong, LOAD guard-aware, OPEN_GUI full path): PASS — LOAD skip -> OPEN_GUI hwnd -> WM_CLOSE -> "state captured ch=5 component=2628B" -> reload -> "state restored ch=5 component=2628B" -> OPEN_GUI lai attach OK, khong restart. Bug tim duoc: khong co bug san pham; 2 bug test-script (probe PID int vs list -> bao restart gia; preset EZkeys byte-diff do PlayHeadPos runtime). Commit `G2.6: verify — regression G1.5 + stress G2.5 + probe G0.4 all PASS`.
|
||||
- **Audit bảo mật/bộ nhớ/conflict child process** (2026-08-16, Linux — chỉ review/ghi chú, KHÔNG sửa code): đọc toàn bộ main.cpp/NativeInstrumentEngine/Vst3Instrument/StateStore/SandboxVst3Host/plugin_host_main/SharedMemoryIPC/shm.rs/lib.rs/nativeBridgeService.js/app.precompiled.js. Findings: F-MEM-5 UAF audio-thread get()+deref vs assign() destroy oldInst ngoài lock sau 10ms (MEDIUM, sites 794/884/909/927/953/1290); F-MEM-8 double-buffer không handshake (MEDIUM audio); F-PROC-1 command injection `--path "..."` CreateProcessA (MEDIUM); F-PROC-2 race restart×watchdog → 2 bridge cùng SHM (MEDIUM); F-PROC-3 orphan race Destroyed (LOW); F5 open_vst_gui luôn Ok (LOW); F6 comment cap 2 vs 5 (LOW). Luồng chính đúng thiết kế: drain control trước render, LOAD UiThread+g_loadMutex, OPEN_GUI defer PLAY + pending sweep, STOP flush CC64=0, DUMP_STATE trực tiếp, pump drop-guard trước kill, max 1 restart/incident, JS __bridgePlaying + __bridgeLoadedChannels reset. Verify Linux: StateStore -fanalyzer sạch + round-trip PASS; 3 file Windows-only không build được (đúng); node --check 2 JS PASS. Không sửa code (đúng phân máy). Chi tiết mục 5.2.
|
||||
- **Fix audit 5.2** (2026-08-16, Windows): xử lý toàn bộ findings 5.2. F-PROC-1: SandboxVst3Host.cpp thêm quote_arg (escape \ và " theo Windows CRT) + CreateProcessA(hostExe.c_str(), ...) + bỏ token exe khỏi cmdline -> path thù địch chứa " giữ 1 argv (probe F-PROC-1 PASS: spawn ch=7 cmdline giữ trọn path trong --path, child không map HACKED_SHM, không nhận --sr 1000, load fail như mong đợi). F-MEM-5: NativeInstrumentEngine.h + .cpp thêm has() + template withInstrument() (giữ mu_ quanh call); main.cpp đổi 6 site audio-thread: 784/794 hasAttachedView dedupe, 1073 restore guard, 1283 CC64=0, 1318 pending-GUI, 1337 state-dirty -> has()/withInstrument(); giữ nguyên 8 site UiThread (an toàn cùng thread assign). F-PROC-2: lib.rs thêm static BRIDGE_RESTART_LOCK serialize kill+spawn giữa restart_bridge_with_sample_rate và pump watchdog. F-PROC-3: pump re-check SHUTTING_DOWN sau lock + sau kill trước spawn. F5: open_vst_gui trả Err (lock poisoned / SHM unavailable / queue full), frontend try/catch sẵn. F6: comment Cap 5 -> Cap 2 (hard min(attempts,2)). F-MEM-8: ghi nhận theo dõi, không fix (đúng audit). Build: daw_vst_bridge + plugin_host PASS; cargo build --release PASS (2m53s). Deploy install/ (kill trước, copy2). Regression: G1.5 PASS 3/3 (writeIndex delta 287), G2.5 PASS 2/2 (watchdog restart + preset giữ exact), probe F-PROC-1 PASS (không injection). Commit: fix 5.2 audit findings on Windows.
|
||||
|
||||
---
|
||||
|
||||
@@ -466,7 +466,7 @@ const loadVstToBridge=async track=>{if(!window.NativeBridgeService||!window.Soni
|
||||
let knownPath=track.synth_engine&&track.synth_engine.plugin_path;if(!knownPath&&window.__bridgePluginPaths)knownPath=window.__bridgePluginPaths[instrumentId];const r=await window.SonicAPI.bridgeLoad({name:instrumentId,path:knownPath||null,instrumentType:'VST3',channel:bch});let p=r&&r.path?r.path:null;if(!p&&knownPath)p=knownPath;if(!p&&window.SonicAPI.listPlugins){try{const pl=await window.SonicAPI.listPlugins();const v=(pl&&pl.vst_instruments||[]).find(x=>x.id===instrumentId||x.name===instrumentId);if(v&&v.path){p=v.path;if(!window.__bridgePluginPaths)window.__bridgePluginPaths={};window.__bridgePluginPaths[instrumentId]=v.path;}}catch(e2){console.warn('[Bridge] listPlugins fallback fail:',e2);}}if(!p)return false;const ok=await window.NativeBridgeService.loadInstrument(p,'VST3',bch);if(ok)window.__bridgeLoadedChannels[bch]=true;console.log('[Bridge] auto-load track',track.id,'-> VST3 ch',bch,ok?'OK':'FAIL',p);return!!ok;}catch(e){console.warn('[Bridge] loadVstToBridge fail:',track.id,e);return false;}};const ensureAndOpenVstGui=async(trackId,instrumentId)=>{if(!window.SonicMidiRouter||!window.SonicMidiRouter.isBridgeActive()||!window.NativeBridgeService)return;if(!instrumentId||typeof instrumentId==='string'&&instrumentId.startsWith('sf_'))return;if(!window.__bridgeLoadedChannels)window.__bridgeLoadedChannels={};const bch=window.SonicMidiRouter.allocateChannel(trackId,false);if(!window.__bridgeLoadedChannels[bch]){const ok=await loadVstToBridge({id:trackId,instrumentId});if(!ok)return;}try{await openVstGuiRetry(instrumentId,bch,20);}catch(e){console.warn('[Bridge] openNativeGUI fail:',e);}};// V8 bug 4 + V9 bug 3/6/7: C++ load instrument ASYNC (worker thread) — OPEN_GUI
|
||||
// som → C++ defer va tu fulfil khi assign() xong (pending-GUI sweep) → khong
|
||||
// can spam. 20 lan x 500ms tao deferred storm (50+ event/moi restart) va
|
||||
// re-attach view dang attached (crash USER32 0xc000041d). Cap 5.
|
||||
// re-attach view dang attached (crash USER32 0xc000041d). Cap 2 (hard min(attempts,2)).
|
||||
const openVstGuiRetry=async(instrumentId,bch,attempts)=>{if(window.__bridgePlaying)return true;// G0.2: khong retry khi PLAY (G0.1 da defer)
|
||||
const n=Math.min(attempts&&attempts>0?attempts:2,2);for(let i=0;i<n;i++){try{await window.NativeBridgeService.openNativeGUI(instrumentId,bch);}catch(e){console.warn('[Bridge] openNativeGUI retry',i+1,e);}if(i<n-1)await new Promise(r=>setTimeout(r,800));}return true;};// Module-level so both ProfileModal (open project) and App (auto-restore) can
|
||||
// warm the FluidSynth font cache for each track's instrument. This only loads
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -79,6 +79,18 @@ public:
|
||||
|
||||
INativeInstrument* get(uint32_t channel);
|
||||
|
||||
// F-MEM-5 (audit 5.2): get() releases mu_ before the caller derefs, so a
|
||||
// worker thread's assign()/unload() can destroy the instance mid-use.
|
||||
// has() is a safe existence check; withInstrument() runs code against the
|
||||
// instance while mu_ is still held. Use these from non-Ui threads.
|
||||
bool has(uint32_t channel);
|
||||
template <typename F>
|
||||
auto withInstrument(uint32_t channel, F&& fn) -> decltype(fn((INativeInstrument*)nullptr)) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
auto it = channels_.find(channel);
|
||||
return fn(it == channels_.end() ? nullptr : it->second.get());
|
||||
}
|
||||
|
||||
// Real-time MIDI dispatch (audio loop thread). Each call holds mu_ for the
|
||||
// WHOLE call — lookup + reloading check + instrument call under one lock —
|
||||
// so a worker thread's assign()/unload() can never swap the map and destroy
|
||||
|
||||
@@ -303,6 +303,11 @@ INativeInstrument* InstrumentEngineManager::get(uint32_t channel) {
|
||||
return it == channels_.end() ? nullptr : it->second.get();
|
||||
}
|
||||
|
||||
bool InstrumentEngineManager::has(uint32_t channel) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return channels_.count(channel) != 0;
|
||||
}
|
||||
|
||||
void InstrumentEngineManager::setReloading(uint32_t channel, bool on) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (channel < 16) reloadingCh_[channel] = on;
|
||||
|
||||
@@ -7,6 +7,24 @@
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
// F-PROC-1 (audit 5.2): Windows CRT command-line parsing escape for an
|
||||
// argument inside double quotes. A path containing a double quote would
|
||||
// otherwise break out of --path "..." and inject arbitrary argv (e.g.
|
||||
// `" && calc`). Metacharacters (& | > <) are inert here: CreateProcessA
|
||||
// does not run a shell.
|
||||
static std::string quote_arg(const std::string& s) {
|
||||
std::string out = "\"";
|
||||
size_t bs = 0;
|
||||
for (char c : s) {
|
||||
if (c == '\\') { ++bs; continue; }
|
||||
if (c == '"') { out.append(bs * 2 + 1, '\\'); out += '"'; bs = 0; }
|
||||
else { out.append(bs, '\\'); out += c; bs = 0; }
|
||||
}
|
||||
out.append(bs * 2, '\\');
|
||||
out += '"';
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool proc_alive(HANDLE h) {
|
||||
#ifdef _WIN32
|
||||
if (!h) return false;
|
||||
@@ -72,8 +90,12 @@ bool SandboxVst3Host::spawnChild() {
|
||||
std::string hostExe = (slash == std::string::npos)
|
||||
? "plugin_host.exe"
|
||||
: dir.substr(0, slash + 1) + "plugin_host.exe";
|
||||
std::string cmd = "\"" + hostExe + "\" --open --shm " + shmName_ +
|
||||
" --path \"" + path_ + "\" --sr " + std::to_string((int)sampleRate_) +
|
||||
// F-PROC-1: lpApplicationName = hostExe (exe path is never parsed as
|
||||
// command line), and the VST path goes through quote_arg so a hostile
|
||||
// path cannot inject extra arguments.
|
||||
std::string cmd = "--open --shm " + shmName_ +
|
||||
" --path " + quote_arg(path_) +
|
||||
" --sr " + std::to_string((int)sampleRate_) +
|
||||
" --block " + std::to_string(block_) +
|
||||
" --parent " + std::to_string((unsigned long)GetCurrentProcessId());
|
||||
std::cerr << "[SandboxVst3Host] spawn ch=" << channel_ << " " << cmd << std::endl;
|
||||
@@ -83,7 +105,7 @@ bool SandboxVst3Host::spawnChild() {
|
||||
PROCESS_INFORMATION pi = {};
|
||||
std::vector<char> buf(cmd.begin(), cmd.end());
|
||||
buf.push_back('\0');
|
||||
if (!CreateProcessA(nullptr, buf.data(), nullptr, nullptr, FALSE,
|
||||
if (!CreateProcessA(hostExe.c_str(), buf.data(), nullptr, nullptr, FALSE,
|
||||
CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) {
|
||||
std::cerr << "[SandboxVst3Host] CreateProcessA failed err=" << (int)GetLastError()
|
||||
<< " ch=" << channel_ << std::endl;
|
||||
|
||||
+57
-55
@@ -73,11 +73,11 @@ static void disable_ime_contexts(HWND w) {
|
||||
}
|
||||
}
|
||||
|
||||
// G1.3: bo pump gate cu (g_juceOwnerTids / g_ownerTid /
|
||||
// is_teardown_window): teardown (close/LOAD job) va message pump cung chay tren
|
||||
// 1 thread (UiThread) — job chay thi pump dung, job xong thi editor window da
|
||||
// destroy nen USER32 tu huy message toi window chet. Khong con "2 thread trong
|
||||
// 1 DLL" khi teardown (multi-worker cu).
|
||||
// G1.3: bo pump gate cu (g_juceOwnerTids / g_ownerTid /
|
||||
// is_teardown_window): teardown (close/LOAD job) va message pump cung chay tren
|
||||
// 1 thread (UiThread) — job chay thi pump dung, job xong thi editor window da
|
||||
// destroy nen USER32 tu huy message toi window chet. Khong con "2 thread trong
|
||||
// 1 DLL" khi teardown (multi-worker cu).
|
||||
|
||||
// CBT hook: strip the IMC the moment any JUCE_* window is born (JUCE message
|
||||
// window AND editor child) — the post-attach disable_ime_contexts runs too
|
||||
@@ -92,8 +92,8 @@ static LRESULT CALLBACK ImeCbtHookProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
HWND w = (HWND)wParam;
|
||||
char cls[64] = {0};
|
||||
if (GetClassNameA(w, cls, 63) > 0 && std::strncmp(cls, "JUCE_", 5) == 0) {
|
||||
ImmAssociateContext(w, nullptr);
|
||||
|
||||
ImmAssociateContext(w, nullptr);
|
||||
|
||||
}
|
||||
}
|
||||
return CallNextHookEx(g_cbtHook, nCode, wParam, lParam);
|
||||
@@ -108,13 +108,13 @@ static LRESULT CALLBACK ImeCbtHookProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
class ChannelWorker; // fwd — WM_DESTROY posts closeGUI() to the channel worker
|
||||
static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker
|
||||
static void post_resize_view(uint32_t ch, int w, int h); // defined after ChannelWorker
|
||||
|
||||
|
||||
static InstrumentEngineManager* g_engine = nullptr;
|
||||
static std::mutex g_guiMutex;
|
||||
static std::map<uint32_t, void*> g_guiWindows; // channel -> HWND (keep window alive)
|
||||
static std::map<HWND, uint32_t> g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup)
|
||||
static ChannelWorker* g_uiWorker = nullptr;
|
||||
|
||||
|
||||
// Same-plugin-DLL reentrancy guards: two threads inside one VST3 DLL (Nexus)
|
||||
// crash or deadlock. g_attachPaths = lowercase plugin paths whose reload/
|
||||
// createView is running on some worker — a same-path close job must not unmute
|
||||
@@ -125,7 +125,7 @@ static ChannelWorker* g_uiWorker = nullptr;
|
||||
static std::mutex g_attachMutex;
|
||||
static std::vector<std::string> g_attachPaths;
|
||||
static bool g_closeInFlight[16] = { false };
|
||||
|
||||
|
||||
static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
|
||||
if (uMsg == WM_CLOSE) {
|
||||
uint32_t ch = UINT32_MAX;
|
||||
@@ -263,7 +263,7 @@ public:
|
||||
#endif
|
||||
th_ = std::thread([this] {
|
||||
#ifdef _WIN32
|
||||
OleInitialize(nullptr);
|
||||
OleInitialize(nullptr);
|
||||
OleInitialize(nullptr);
|
||||
// Default IMC = none on this thread: new editor windows get
|
||||
// no IME context (see disable_ime_contexts).
|
||||
@@ -295,11 +295,11 @@ public:
|
||||
for (int pumped = 0; pumped < 128; ++pumped) {
|
||||
if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break;
|
||||
hadMessages = true;
|
||||
// G1.3: bo pump gate cu — teardown (close/LOAD job)
|
||||
// va pump cung 1 thread (UiThread): job chay thi pump dung;
|
||||
// het job thi editor window da destroy -> message chet bi
|
||||
// USER32 huy tu dong. Khong can drop theo close_in_flight.
|
||||
|
||||
// G1.3: bo pump gate cu — teardown (close/LOAD job)
|
||||
// va pump cung 1 thread (UiThread): job chay thi pump dung;
|
||||
// het job thi editor window da destroy -> message chet bi
|
||||
// USER32 huy tu dong. Khong can drop theo close_in_flight.
|
||||
|
||||
TranslateMessage(&msg);
|
||||
// CRASH FIX (0xc000041d STATUS_FATAL_USER_CALLBACK_EXCEPTION):
|
||||
// a plugin window proc (Nexus throws nlohmann::json::out_of_range
|
||||
@@ -328,7 +328,7 @@ public:
|
||||
}
|
||||
}
|
||||
#ifdef _WIN32
|
||||
OleUninitialize();
|
||||
OleUninitialize();
|
||||
OleUninitialize();
|
||||
#endif
|
||||
});
|
||||
@@ -364,7 +364,7 @@ public:
|
||||
#endif
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
|
||||
|
||||
private:
|
||||
std::thread th_;
|
||||
@@ -421,24 +421,24 @@ static bool close_in_flight(uint32_t ch) {
|
||||
return g_closeInFlight[ch];
|
||||
}
|
||||
|
||||
|
||||
|
||||
// G1.3: 1 UiThread — editor windows tao trong attach job (UiThread), destroy
|
||||
// chi goi tu UiThread job (close_editor_now / LOAD) -> ownerTid luon == current
|
||||
// tid -> destroy truc tiep. Neu owner khac thread (khong con xay ra) -> bo
|
||||
// (leak > crash): cross-thread DestroyWindow khong an toan.
|
||||
static void destroy_window_on_owner(HWND hwnd) {
|
||||
if (!hwnd || !IsWindow(hwnd)) return;
|
||||
DWORD ownerTid = GetWindowThreadProcessId(hwnd, nullptr);
|
||||
DWORD curTid = GetCurrentThreadId();
|
||||
if (ownerTid == 0 || ownerTid == curTid) {
|
||||
if (IsWindow(hwnd)) DestroyWindow(hwnd);
|
||||
return;
|
||||
}
|
||||
std::cerr << "[NativeBridge] destroy_window_on_owner: owner tid=" << ownerTid
|
||||
<< " != current " << curTid << " - leaving window " << hwnd << std::endl;
|
||||
}
|
||||
|
||||
|
||||
// G1.3: 1 UiThread — editor windows tao trong attach job (UiThread), destroy
|
||||
// chi goi tu UiThread job (close_editor_now / LOAD) -> ownerTid luon == current
|
||||
// tid -> destroy truc tiep. Neu owner khac thread (khong con xay ra) -> bo
|
||||
// (leak > crash): cross-thread DestroyWindow khong an toan.
|
||||
static void destroy_window_on_owner(HWND hwnd) {
|
||||
if (!hwnd || !IsWindow(hwnd)) return;
|
||||
DWORD ownerTid = GetWindowThreadProcessId(hwnd, nullptr);
|
||||
DWORD curTid = GetCurrentThreadId();
|
||||
if (ownerTid == 0 || ownerTid == curTid) {
|
||||
if (IsWindow(hwnd)) DestroyWindow(hwnd);
|
||||
return;
|
||||
}
|
||||
std::cerr << "[NativeBridge] destroy_window_on_owner: owner tid=" << ownerTid
|
||||
<< " != current " << curTid << " - leaving window " << hwnd << std::endl;
|
||||
}
|
||||
|
||||
|
||||
// Unmute y unless its own close job is still inside createInstance — that job
|
||||
// performs the unmute once its fresh instance is loaded (or, if an attach for
|
||||
@@ -594,9 +594,9 @@ int main(int argc, char* argv[]) {
|
||||
// without an exe manifest. Ignore failure (already aware).
|
||||
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
// Default IMC = none on the main thread (IME recursion fix).
|
||||
// Default IMC = none on the main thread (IME recursion fix).
|
||||
ImmAssociateContextEx(nullptr, nullptr, IACE_DEFAULT);
|
||||
|
||||
// Default IMC = none on the main thread (IME recursion fix).
|
||||
ImmAssociateContextEx(nullptr, nullptr, IACE_DEFAULT);
|
||||
|
||||
#endif
|
||||
|
||||
// 1. Shared memory name: argv --shm <name> | env SF_SHM_NAME | default
|
||||
@@ -781,7 +781,7 @@ int main(int argc, char* argv[]) {
|
||||
~AttachInFlightGuard() { if (ipc) ipc->attachInFlight = 0; }
|
||||
};
|
||||
auto handleOpenGui = [&](uint32_t guiCh, uintptr_t arg1, const std::string& pluginId) {
|
||||
if (!instruments.get(guiCh)) {
|
||||
if (!instruments.has(guiCh)) {
|
||||
std::cerr << "[NativeBridge] GUI deferred ch=" << guiCh
|
||||
<< " plugin=" << pluginId << " (no instrument yet) - queued" << std::endl;
|
||||
std::lock_guard<std::mutex> lock(g_pendingGuiMutex);
|
||||
@@ -791,10 +791,10 @@ int main(int argc, char* argv[]) {
|
||||
// Dedupe OPEN_GUI spam (frontend openVstGuiRetry): view dang
|
||||
// attached -> GUI da mo, khong post attach job lan nua. Re-attach
|
||||
// tren view dang attached lam plugin loi (Nexus createView null).
|
||||
if (auto* i0 = instruments.get(guiCh)) {
|
||||
if (i0->hasAttachedView()) {
|
||||
return;
|
||||
}
|
||||
if (instruments.withInstrument(guiCh, [](INativeInstrument* i0) {
|
||||
return i0 && i0->hasAttachedView();
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
// G0.1 (BUG_REPORT): mo GUI khi dang PLAY -> attachView chay tren worker
|
||||
// thread trong luc audio loop process() cung DLL (2 thread 1 plugin) ->
|
||||
@@ -1022,9 +1022,9 @@ int main(int argc, char* argv[]) {
|
||||
<< " plugin=" << arg2 << std::endl;
|
||||
#ifdef _WIN32
|
||||
// Attach that — khong co view (VD: channel la SF2/SFZ hoac plugin
|
||||
// loi). Dong ngay cua so vo nghia de khong con window treo trong
|
||||
// registry; WM_CLOSE -> UiThread pump destroy (owner = UiThread).
|
||||
// PostMessage an toan cross-thread (khong nhu DestroyWindow).
|
||||
// loi). Dong ngay cua so vo nghia de khong con window treo trong
|
||||
// registry; WM_CLOSE -> UiThread pump destroy (owner = UiThread).
|
||||
// PostMessage an toan cross-thread (khong nhu DestroyWindow).
|
||||
|
||||
PostMessageA((HWND)hwnd, WM_CLOSE, 0, 0);
|
||||
#endif
|
||||
@@ -1070,7 +1070,7 @@ int main(int argc, char* argv[]) {
|
||||
std::cout << "[NativeBridge] restoring " << snap.instruments.size()
|
||||
<< " instruments from " << stateFile << std::endl;
|
||||
for (const auto& e : snap.instruments) {
|
||||
uint32_t ch = e.channel & 0xF;
|
||||
uint32_t ch = e.channel & 0xF;
|
||||
if (e.path.empty() || instruments.has(ch)) continue; // guard: empty channel only
|
||||
InstrumentType t = (InstrumentType)e.type;
|
||||
std::string path = e.path;
|
||||
@@ -1191,11 +1191,11 @@ int main(int argc, char* argv[]) {
|
||||
// DONG cua so editor dang mo cua channel TRUOC khi assign():
|
||||
// thay the inst (VST3 -> SF2/inst khac) ma editor con song ->
|
||||
// old inst destructor goi view->removed() tren HWND con hoat
|
||||
// dong -> plugin block -> treo bridge.
|
||||
// G1.2/G1.3: window tao tren UiThread (handleOpenGui post attach
|
||||
// job; create_native_vst_window chay trong job). LOAD job cung
|
||||
// chay tren UiThread -> khong cross-thread destroy. An window,
|
||||
// giu registry de reuse; editor detach + reload o duoi.
|
||||
// dong -> plugin block -> treo bridge.
|
||||
// G1.2/G1.3: window tao tren UiThread (handleOpenGui post attach
|
||||
// job; create_native_vst_window chay trong job). LOAD job cung
|
||||
// chay tren UiThread -> khong cross-thread destroy. An window,
|
||||
// giu registry de reuse; editor detach + reload o duoi.
|
||||
|
||||
HWND hToHide = nullptr;
|
||||
{
|
||||
@@ -1280,7 +1280,9 @@ int main(int argc, char* argv[]) {
|
||||
// Release sustain pedal TRUOC (CC64=0) — nhieu VSTi giu note
|
||||
// khi pedal con down -> note-off cua allNotesOff bi bo qua
|
||||
// -> am treo loop (V8 bug 3).
|
||||
for (uint32_t ch = 0; ch < 16; ++ch) {
|
||||
for (uint32_t ch = 0; ch < 16; ++ch) {
|
||||
instruments.withInstrument(ch, [&](INativeInstrument* inst) {
|
||||
if (inst) inst->controlChange(ch, 64, 0);
|
||||
});
|
||||
}
|
||||
instruments.allNotesOff();
|
||||
@@ -1315,7 +1317,7 @@ int main(int argc, char* argv[]) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_pendingGuiMutex);
|
||||
for (auto it = g_pendingGui.begin(); it != g_pendingGui.end(); ) {
|
||||
// G0.1: giu entry khi dang PLAY - handleOpenGui se defer lai
|
||||
// G0.1: giu entry khi dang PLAY - handleOpenGui se defer lai
|
||||
if (transportStopped && instruments.has(it->first)) {
|
||||
readyGui.push_back({ it->first, it->second });
|
||||
it = g_pendingGui.erase(it);
|
||||
@@ -1334,7 +1336,7 @@ int main(int argc, char* argv[]) {
|
||||
auto nowMark = std::chrono::steady_clock::now();
|
||||
if (std::chrono::duration_cast<std::chrono::milliseconds>(nowMark - lastMark).count() >= 2000) {
|
||||
lastMark = nowMark;
|
||||
for (uint32_t ch = 0; ch < 16; ++ch) {
|
||||
for (uint32_t ch = 0; ch < 16; ++ch) {
|
||||
if (instruments.has(ch)) { g_stateDirty = true; break; }
|
||||
}
|
||||
}
|
||||
|
||||
+33
-10
@@ -24,6 +24,10 @@ mod shm;
|
||||
/// 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>>);
|
||||
@@ -513,7 +517,22 @@ pub fn run() {
|
||||
);
|
||||
// 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)
|
||||
@@ -672,17 +691,15 @@ fn open_vst_gui(app: AppHandle, plugin_id: String, channel: u8) -> Result<(), St
|
||||
// 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 lock = state.0.lock();
|
||||
match lock {
|
||||
Ok(guard) => match guard.as_ref() {
|
||||
Some(shm) => {
|
||||
let ok = shm.push_control(4, 0, 0, channel as u32, &plugin_id);
|
||||
eprintln!("open_vst_gui: push_control type=4 hwnd=0 ch={} ok={}", channel, ok);
|
||||
}
|
||||
None => eprintln!("open_vst_gui: bridge shm unavailable"),
|
||||
},
|
||||
Err(e) => eprintln!("open_vst_gui: shm lock poisoned: {}", e),
|
||||
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(())
|
||||
}
|
||||
@@ -770,6 +787,12 @@ fn restart_bridge_with_sample_rate(app: AppHandle, sample_rate: u32) -> Result<(
|
||||
*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()
|
||||
|
||||
Reference in New Issue
Block a user