Files

307 lines
13 KiB
Python

#!/usr/bin/env python3
"""Apply owner-thread-destroy + pump-gate crash fix to main.cpp (LF file)."""
import sys, io
p = r"C:/Users/locpham/SonicForgeStudio/native_bridge/src/main.cpp"
s = open(p, "rb").read().decode("utf-8")
orig = s
def rep(old, new, tag):
global s
n = s.count(old)
assert n == 1, f"[{tag}] count={n}"
s = s.replace(old, new, 1)
print(f"ok: {tag}")
# 1. globals
rep(
"""static std::map<uint32_t, std::unique_ptr<ChannelWorker>>* g_workers = nullptr;""",
"""static std::map<uint32_t, std::unique_ptr<ChannelWorker>>* g_workers = nullptr;
static std::mutex g_tidMutex;
static std::map<DWORD, ChannelWorker*> g_tidToWorker; // worker tid -> worker (owner-thread destroy)""",
"globals")
# 2. fwd decl
rep(
"""static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker""",
"""static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker
static bool is_teardown_window(HWND hwnd); // fwd — defined after ChannelWorker""",
"fwd")
# 3. constructor register tid
rep(
""" th_ = std::thread([this] {
#ifdef _WIN32
OleInitialize(nullptr);""",
""" th_ = std::thread([this] {
#ifdef _WIN32
{
std::lock_guard<std::mutex> lk(g_tidMutex);
g_tidToWorker[GetCurrentThreadId()] = this;
}
OleInitialize(nullptr);""",
"register tid")
# 4. pump gate
rep(
""" for (int pumped = 0; pumped < 16; ++pumped) {
if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break;
TranslateMessage(&msg);""",
""" for (int pumped = 0; pumped < 16; ++pumped) {
if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break;
// CRASH FIX (0xc000041d): JUCE editor child windows of a
// plugin DLL are owned by the FIRST worker that ran the
// DLL's global JUCE MessageManager - not by the channel
// worker doing the teardown. While another worker's LOAD /
// close job tears an instance down (reloading / close in
// flight), dispatching a message (e.g. WM_PAINT) into the
// plugin wndproc reads instance state being destroyed ->
// AV in USER32 (observed: crash on the owner worker's
// pump, right after the teardown job closed the view).
// Drop the message instead (PeekMessageW already removed
// it): the editor may glitch, the bridge survives.
if (is_teardown_window(msg.hwnd)) continue;
TranslateMessage(&msg);""",
"pump gate")
# 5. unregister tid
rep(
"""#ifdef _WIN32
OleUninitialize();
#endif
});""",
"""#ifdef _WIN32
{
std::lock_guard<std::mutex> lk(g_tidMutex);
g_tidToWorker.erase(GetCurrentThreadId());
}
OleUninitialize();
#endif
});""",
"unregister tid")
# 6. post_and_wait
rep(
""" void post(std::function<void()> job) {
{
std::lock_guard<std::mutex> lk(mu_);
jobs_.push_back(std::move(job));
}
cv_.notify_all();
}
private:""",
""" void post(std::function<void()> job) {
{
std::lock_guard<std::mutex> lk(mu_);
jobs_.push_back(std::move(job));
}
cv_.notify_all();
}
// Run job on THIS worker and wait (5s cap) until it finished. Used to
// serialize a window destroy with the owner worker's pump. MUST NOT be
// called from a job running on this same worker (deadlock) - callers
// destroy directly when owner tid == current tid.
bool post_and_wait(std::function<void()> job) {
std::mutex doneMx;
std::condition_variable doneCv;
bool done = false;
{
std::lock_guard<std::mutex> lk(mu_);
jobs_.push_back([&]() {
job();
{
std::lock_guard<std::mutex> dk(doneMx);
done = true;
}
doneCv.notify_all();
});
}
cv_.notify_all();
std::unique_lock<std::mutex> dk(doneMx);
return doneCv.wait_for(dk, std::chrono::seconds(5), [&] { return done; });
}
private:""",
"post_and_wait")
# 7. helpers after close_in_flight
rep(
"""static bool close_in_flight(uint32_t ch) {
std::lock_guard<std::mutex> lk(g_attachMutex);
return g_closeInFlight[ch];
}""",
"""static bool close_in_flight(uint32_t ch) {
std::lock_guard<std::mutex> lk(g_attachMutex);
return g_closeInFlight[ch];
}
// True if hwnd (or any ancestor) is a plugin editor window whose channel is
// mid-teardown (reloading or close job in flight). Editor child windows
// (JUCE) have the native VST window as parent; the native window is in
// g_hwndToCh. JUCE's global message window has no such parent - walk stops
// -> false (never dropped).
static bool is_teardown_window(HWND hwnd) {
for (HWND h = hwnd; h; h = GetParent(h)) {
uint32_t ch = UINT32_MAX;
{
std::lock_guard<std::mutex> lock(g_guiMutex);
auto it = g_hwndToCh.find(h);
if (it != g_hwndToCh.end()) ch = it->second;
}
if (ch != UINT32_MAX) {
if (close_in_flight(ch)) return true;
return g_engine ? g_engine->isReloading(ch) : false;
}
}
return false;
}
// CRASH FIX (0xc000041d): plugin editor child windows are owned by the
// thread that first ran the plugin's JUCE MessageManager (a single worker),
// NOT by the channel worker doing the teardown. Cross-thread DestroyWindow
// races the owner's message pump. Post the destroy to the OWNER worker
// (serialized with its pump - no wndproc entry can race) and wait. Fall
// back to direct destroy when the owner is the current thread or unknown
// (leak > crash). Non-trivial: destroy jobs left queued on timeout are
// harmless - IsWindow guards them, and the pump gate already stopped
// dispatching to teardown windows.
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;
}
ChannelWorker* owner = nullptr;
{
std::lock_guard<std::mutex> lk(g_tidMutex);
auto it = g_tidToWorker.find(ownerTid);
if (it != g_tidToWorker.end()) owner = it->second;
}
if (!owner) {
// Window belongs to a thread we don't control (e.g. main thread).
// Cross-thread DestroyWindow is unsafe -> leave it (leak > crash).
std::cerr << "[NativeBridge] destroy_window_on_owner: owner tid=" << ownerTid
<< " not a bridge worker - leaving window " << hwnd << std::endl;
return;
}
if (!owner->post_and_wait([hwnd]() {
if (IsWindow(hwnd)) DestroyWindow(hwnd);
}))
std::cerr << "[NativeBridge] destroy_window_on_owner: timeout waiting owner tid="
<< ownerTid << " to destroy " << hwnd << std::endl;
}""",
"helpers")
# 8. post_close_gui: destroy outside g_loadMutex, via owner
rep(
""" bool reloadOk = false;
{
// destroy children + reload in ONE lock: destroying the
// editor windows runs the plugin's window proc (DLL
// entry) on this worker; it must not race another
// thread's createInstance/createView of the same DLL.
// Same-thread destroy is valid (attachView created them
// on this worker); without destroy the editor keeps
// pumping (0x47b flood) while the audio loop processes
// the instance (2 threads in one DLL) -> Nexus USER32 crash.
std::lock_guard<std::mutex> lg(g_loadMutex);
// CRASH FIX: reload() = terminate + loadPlugin
// (createInstance) — Nexus may throw a C++ exception
// here too; catch it, treat as failed reload, and let
// the restore below unmute the channel (instance may be
// broken; next load/assign rebuilds it).
try {
#ifdef _WIN32
disable_ime_contexts(hwnd);
if (hwnd && IsWindow(hwnd)) {
while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr)) {
std::cerr << "[dbg] closeGUI destroy child=" << c
<< " owner_tid=" << GetWindowThreadProcessId(c, nullptr)
<< " cur_tid=" << GetCurrentThreadId() << std::endl;
DestroyWindow(c);
}
}
#endif
reloadOk = i->reload();
} catch (...) {
std::cerr << "[NativeBridge] closeGUI reload EXCEPTION ch=" << ch
<< " — plugin threw (createInstance)" << std::endl;
reloadOk = false;
}
}""",
""" bool reloadOk = false;
#ifdef _WIN32
// CRASH FIX (0xc000041d): destroy the editor's child windows
// on the thread that OWNS them (the plugin's JUCE
// MessageManager thread - a single worker), never
// cross-thread. Wait for each destroy to finish (owner worker
// serializes it with its pump) BEFORE reload() - otherwise
// reload's createInstance enters the DLL while the owner
// thread is still inside the plugin wndproc (2 threads in one
// DLL -> Nexus crash). Kept OUTSIDE g_loadMutex: the wait
// must not block other loads (the owner worker may itself be
// waiting on that lock).
disable_ime_contexts(hwnd);
if (hwnd && IsWindow(hwnd)) {
while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr)) {
std::cerr << "[dbg] closeGUI destroy child=" << c
<< " owner_tid=" << GetWindowThreadProcessId(c, nullptr)
<< " cur_tid=" << GetCurrentThreadId() << std::endl;
destroy_window_on_owner(c);
}
}
#endif
{
// reload() = terminate + loadPlugin (createInstance) —
// serialized with all other plugin-DLL entry points.
// Nexus may throw a C++ exception here too; catch it,
// treat as failed reload, and let the restore below
// unmute the channel (instance may be broken; next
// load/assign rebuilds it).
std::lock_guard<std::mutex> lg(g_loadMutex);
try {
reloadOk = i->reload();
} catch (...) {
std::cerr << "[NativeBridge] closeGUI reload EXCEPTION ch=" << ch
<< " — plugin threw (createInstance)" << std::endl;
reloadOk = false;
}
}""",
"post_close_gui destroy")
# 9. LOAD close-editor destroy loop via owner
rep(
""" // Destroy the old editor's child windows (worker-owned,
// created by attachView here) so they stop pumping;
// the parent window is hidden and kept for reuse.
// See post_close_gui for the same pattern.
while (HWND c = FindWindowExA(hToHide, nullptr, nullptr, nullptr)) {
std::cerr << "[dbg] load close-editor destroy child=" << c
<< " owner_tid=" << GetWindowThreadProcessId(c, nullptr)
<< " cur_tid=" << GetCurrentThreadId() << std::endl;
DestroyWindow(c);
}""",
""" // CRASH FIX (0xc000041d): destroy the old
// editor's child windows on the thread that OWNS
// them (JUCE MessageManager worker), never
// cross-thread - DestroyWindow from this LOAD
// worker raced the owner's pump dispatching
// WM_PAINT into the plugin wndproc -> AV. Wait
// for each destroy before assign() below. The
// parent window is hidden and kept for reuse.
// See post_close_gui for the same pattern.
while (HWND c = FindWindowExA(hToHide, nullptr, nullptr, nullptr)) {
std::cerr << "[dbg] load close-editor destroy child=" << c
<< " owner_tid=" << GetWindowThreadProcessId(c, nullptr)
<< " cur_tid=" << GetCurrentThreadId() << std::endl;
destroy_window_on_owner(c);
}""",
"load destroy loop")
assert s != orig
open(p, "wb").write(s.encode("utf-8"))
print("written")