1 Commits

18 changed files with 4996 additions and 129 deletions
@@ -86,12 +86,14 @@
* 1. LOAD NEW INSTRUMENT INTO BRIDGE (VST3 / VST2 / SF2 / SF3 / SFZ) * 1. LOAD NEW INSTRUMENT INTO BRIDGE (VST3 / VST2 / SF2 / SF3 / SFZ)
* instrumentType: 'VST3' | 'VST2' | 'SF2' | 'SF3' | 'SFZ' * instrumentType: 'VST3' | 'VST2' | 'SF2' | 'SF3' | 'SFZ'
* channel: MIDI channel to assign this instrument to (A10 multi-instance). * channel: MIDI channel to assign this instrument to (A10 multi-instance).
* NOTE: sends a PANIC (all notes off) first so held notes never stick across a hot-swap.
*/ */
loadInstrument: async function (filePath, instrumentType, channel) { loadInstrument: async function (filePath, instrumentType, channel) {
this.activeInstrumentType = instrumentType; this.activeInstrumentType = instrumentType;
console.log('[BridgeService] Loading ' + instrumentType + ' asset: ' + filePath + ' ch=' + channel); console.log('[BridgeService] Loading ' + instrumentType + ' asset: ' + filePath + ' ch=' + channel);
if (!this._tauri()) return false; if (!this._tauri()) return false;
try { try {
await window.__TAURI__.core.invoke('transport_control', { kind: 'panic' });
await window.__TAURI__.core.invoke('load_native_instrument', { await window.__TAURI__.core.invoke('load_native_instrument', {
path: filePath, path: filePath,
instrumentType: INSTRUMENT_TYPE[instrumentType] !== undefined ? INSTRUMENT_TYPE[instrumentType] : 0, instrumentType: INSTRUMENT_TYPE[instrumentType] !== undefined ? INSTRUMENT_TYPE[instrumentType] : 0,
+2 -2
View File
@@ -39,7 +39,7 @@
<script src="/static/js/services/storage.js?v=202608038200"></script> <script src="/static/js/services/storage.js?v=202608038200"></script>
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script> <script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608101800"></script> <script src="/static/js/services/soundfontPlayer.js?v=202608101800"></script>
<script src="/static/js/services/nativeBridgeService.js?v=202608112200"></script> <script src="/static/js/services/nativeBridgeService.js?v=202608141800"></script>
<script src="/static/js/services/bridgeAudioNode.js?v=202608121700"></script> <script src="/static/js/services/bridgeAudioNode.js?v=202608121700"></script>
<script src="/static/js/services/unifiedMidiRouter.js?v=202608112200"></script> <script src="/static/js/services/unifiedMidiRouter.js?v=202608112200"></script>
<script src="/static/js/services/audioRoutingEngine.js?v=202608112200"></script> <script src="/static/js/services/audioRoutingEngine.js?v=202608112200"></script>
@@ -50,7 +50,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script> <script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script> <script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script> <script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608141030" defer></script> <script src="/static/js/app.precompiled.js?v=202608141730" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016"> <link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style> <style>
:root { :root {
Binary file not shown.
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
# Nexus GUI crash 0xc000041d (USER32) — debug state (WIP)
Thời điểm lưu: 2026-08-14, đang chuyển sang máy Linux để tiếp tục.
## Hiện tượng
`daw_vst_bridge.exe` crash 0xc000041d khi mở GUI editor VSTi Nexus (`C:\Program Files\Common Files\VST3\Nexus.vst3`) rồi chọn instrument khác. Chạy từ `install/`.
## Kết quả đã có
- Probe `gui_probe.exe` variant `bridge_exact11` PASS (`RESULT(bridge_exact11): done=1 attached_ok=1`, log: `openGUI: attached=0` = kResultOk), kể cả khi stdout redirect ra file.
- NHƯNG bridge thật HANG tại `view->attached()` (log dừng tại `isPlatformTypeSupported=0`, không in `attached=`), kể cả SF_ONCE_PROBEWIN=1. Watchdog tick vẫn chạy = đang trong attached().
## Đã loại trừ (session 2026-08-14)
- **g_engine hypothesis DISPROVEN**: `g_engine` (main.cpp L545) KHÔNG được dùng trong path SF_ONCE (chỉ dùng ở closeGUI/main-loop). Job SF_ONCE chỉ gọi `instruments.setReloading(y,true)` → `Vst3Instrument::setReloading` (chỉ set bool) + `reloadForGUI()` (hasAttachedOnce_=false lần đầu → no-op return true).
- **Diff ChannelWorker vs PumpWorker**: giống hệt (chỉ khác tên biến + destructor + comment).
- **Diff exact11 vs SF_ONCE**: giống hệt về ngữ nghĩa — chỉ khác sampleRate/block, setEditorOpenPredicate (cả hai false), SHM driver thật vs tự tạo (đã loại bằng SF_ONCE_SHM).
- **stdout redirect ra file**: probe vẫn PASS.
- **Binary up-to-date** (build 22:09 = main.cpp 22:09).
- CMakeLists: bridge và gui_probe compile CÙNG sources, CÙNG defines (HAVE_VST3SDK=1), cùng libs — không khác.
## Stack main thread (stackscan.py v7/v8)
- Main thread chờ vô hạn trong ntdll (R9=0x7ffffffffffffffc timeout ~infinite) qua KERNELBASE+22cd8 gọi từ Nexus attached().
- 3 thread Nexus riêng chờ ntdll+164034; 1 thread (21232) là message pump (win32u+20a4).
- Export resolve thất bại (offset nội bộ không tên): ntdll+5fc6e→hàm tại 0x5fa90, ntdll+3e732→0x3dc60, KERNELBASE+22cd8→hàm tại 0x22ca0.
## Bước tiếp theo
1. **waitscan.py** (chạy dở khi lưu): dump RIP mọi thread + NtQueryObject loại/tên handle trong Rcx (wait object) — xem main thread và 3 thread Nexus có CÙNG chờ 1 object (deadlock nội bộ Nexus) hay không. Chạy: `python waitscan.py` (cần bridge đang hang).
2. Nếu waitscan không rõ: disasm KERNELBASE+22ca0 / ntdll+5fa90 xác định hàm chờ (WaitForSingleObjectEx/WaitForMultipleObjectsEx/AlertableWait), hoặc NtQuerySystemInformation wait-chain.
3. Khi tìm được trigger: REVERT toàn bộ TEST patch (watchdog, direct attach, no audio, DefWindowProcA, warm-up, pre-window, autogui, exact11, once, once_shm, probewin) — giữ fix chính thức. Đặc biệt:
- `wc.lpfnWndProc = DefWindowProcA; // TEST ISOLATION`
- bỏ `WS_VISIBLE` ở create_native_vst_window
- khôi phục VstWindowProc + WS_VISIBLE khi hết test.
4. Verify app thật từ `install/` (kill SonicForge/daw_vst trước; check `%APPDATA%\SonicForgeDAW\logs\bridge.log` + spawn.log, không "bridge stalled 3s"). Deploy exe 5 vị trí: install/, src-tauri/binaries/ (2 tên), src-tauri/target/{debug,release}/.
## Cách chạy (Windows)
- Build: `cd native_bridge && cmake --build build --config Release --target daw_vst_bridge --parallel` (nhớ `taskkill //F //IM daw_vst_bridge.exe` trước, LNK1104 nếu zombie).
- Probe: `./build/Release/gui_probe.exe "C:\Program Files\Common Files\VST3\Nexus.vst3" <variant> <secs>`; exit 127 dù PASS; watchdog tự kill sau secs+10s.
- Driver: `python driver_once.py [GATE=VAL...]` (chờ "SF_ONCE attach=" 90s + "GUI attached" 30s; attach=0 = THÀNH CÔNG).
## Scripts trong thư mục này
- `waitscan.py` — dump RIP + wait object mọi thread (đang dở, bước 1 tiếp theo).
- `stackscan.py` / `stackwalk.py` / `stack_sample*.py` — stack dump.
- `driver_once.py` / `driver_verify.py` / `driver_autogui.py` / `driver_narrow.py` — chạy bridge với gate, chờ kết quả.
- Patch scripts (workspace tmp-7d619832): `patch_once.py`, `patch_once_shm.py`, `patch_exact11_bridge.py`, `patch_gates.py`, `patch_autogui.py`, ... áp bằng python lên `native_bridge/src/main.cpp` (CRLF! ghi `io.open(encoding='utf-8', newline='\r\n')`).
## Learnings kỹ thuật (Windows ctypes)
- `GetModuleHandleW`/windll trả pointer → PHẢI set `.restype = ctypes.c_void_p` (mặc định c_int → truncate 32-bit → access violation).
- Mọi API nhận pointer/handle → set argtypes (GetThreadContext, OpenThread, DuplicateHandle...).
- `ctypes.wintypes` KHÔNG có DWORD64 — dùng ctypes.c_uint64.
- CONTEXT x64: SegCs..SegSs là WORD; struct đầy đủ 0x4d0 bytes; GetThreadContext ghi full buffer → dùng raw buffer 0x4d0 + struct.unpack_from offset cố định (rip@0xF8, rsp@0x98, rcx@0x80, rdx@0x88, r8@0xB8, r9@0xC0, ContextFlags@0x30).
- EnumProcessModulesEx: cb = bytes/8 trên x64; module handle có thể NULL → skip.
+91
View File
@@ -0,0 +1,91 @@
# Driver: LOAD ch=5 only; SF_AUTOGUI makes the bridge auto-attach.
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
SHM_NAME = "SonicForge_DAW_IPC_VERIFY"
SIZE = 32768
BRIDGE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe"
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "driver_bridge.log")
NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3"
kernel32 = ctypes.windll.kernel32
kernel32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_char_p]
kernel32.CreateFileMappingA.restype = ctypes.c_void_p
kernel32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_size_t]
kernel32.MapViewOfFile.restype = ctypes.c_void_p
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
PAGE_READWRITE = 0x04
FILE_MAP_ALL_ACCESS = 0xF001F
hMap = kernel32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, PAGE_READWRITE, 0, SIZE, SHM_NAME.encode())
ptr = kernel32.MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0)
def w32(off, fmt, *args):
ctypes.memmove(ptr + off, struct.pack(fmt, *args), struct.calcsize(fmt))
def write_control(type_, arg0, arg1, ch, path):
for i in range(8):
off = 2836 + i * 1040
t = ctypes.c_uint32.from_address(ptr + off).value
if t == 0:
w32(off, 'IIII', type_, arg0, arg1, ch)
if path:
b = path.encode('utf-8')[:1023] + b'\x00'
ctypes.memmove(ptr + off + 16, b, len(b))
return i
raise RuntimeError("control queue full")
def push_controls(events):
for i in range(8):
w32(2836 + i * 1040, 'I', 0)
n = 0
for (type_, arg0, arg1, ch, path) in events:
write_control(type_, arg0, arg1, ch, path)
n += 1
w32(11156, 'I', n)
def wait_log(pattern, timeout):
end = time.time() + timeout
last = 0
while time.time() < end:
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
except FileNotFoundError:
lines = []
for i in range(last, len(lines)):
if pattern in lines[i]:
return lines[i].strip()
last = max(0, len(lines))
time.sleep(0.2)
return None
proc = subprocess.Popen(
[BRIDGE, "--shm", SHM_NAME],
env={**os.environ, "SF_SHM_NAME": SHM_NAME, "SF_PARENT_PID": str(os.getpid()),
"SF_SAMPLE_RATE": "48000", "SF_BLOCK_SIZE": "256"},
stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT,
)
time.sleep(1.5)
print("bridge pid", proc.pid, "poll", proc.poll())
print("[1] load ch=5 Nexus...")
push_controls([(2, 0, 0, 5, NEXUS)])
r = wait_log("instrument loaded ch=5", 90)
print(" ", r)
print("[2] wait auto attach (SF_AUTOGUI)...")
r = wait_log("GUI attached", 30)
print(" ", r)
time.sleep(3)
print("FINAL alive:", proc.poll() is None)
print("--- last 25 log lines ---")
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
for l in lines[-25:]:
print(l.rstrip())
except FileNotFoundError:
pass
+107
View File
@@ -0,0 +1,107 @@
# Narrow test: load ONLY ch=5 Nexus -> open GUI ch=5. Is multi-instance the trigger?
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
SHM_NAME = "SonicForge_DAW_IPC_NARROW"
SIZE = 32768
BRIDGE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe"
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "narrow_bridge.log")
NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3"
kernel32 = ctypes.windll.kernel32
kernel32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_char_p]
kernel32.CreateFileMappingA.restype = ctypes.c_void_p
kernel32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_size_t]
kernel32.MapViewOfFile.restype = ctypes.c_void_p
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
PAGE_READWRITE = 0x04
FILE_MAP_ALL_ACCESS = 0xF001F
hMap = kernel32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, PAGE_READWRITE, 0, SIZE, SHM_NAME.encode())
if not hMap:
print("CreateFileMapping failed", ctypes.get_last_error()); sys.exit(1)
ptr = kernel32.MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0)
if not ptr:
print("MapViewOfFile failed", ctypes.get_last_error()); sys.exit(1)
def w32(off, fmt, *args):
ctypes.memmove(ptr + off, struct.pack(fmt, *args), struct.calcsize(fmt))
def write_control(type_, arg0, arg1, ch, path):
for i in range(8):
off = 2836 + i * 1040
t = ctypes.c_uint32.from_address(ptr + off).value
if t == 0:
w32(off, 'IIII', type_, arg0, arg1, ch)
if path:
b = path.encode('utf-8')[:1023] + b'\x00'
ctypes.memmove(ptr + off + 16, b, len(b))
return i
raise RuntimeError("control queue full")
def push_controls(events):
for i in range(8):
w32(2836 + i * 1040, 'I', 0)
w32(11156, 'I', 0)
n = 0
for (type_, arg0, arg1, ch, path) in events:
write_control(type_, arg0, arg1, ch, path)
n += 1
w32(11156, 'I', n)
def wait_log(pattern, timeout, since=0):
end = time.time() + timeout
lines = []
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
except FileNotFoundError:
pass
last = max(0, len(lines) - since)
while time.time() < end:
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
except FileNotFoundError:
lines = []
for i in range(last, len(lines)):
if pattern in lines[i]:
return lines[i].strip()
last = max(0, len(lines))
time.sleep(0.2)
return None
proc = subprocess.Popen(
[BRIDGE, "--shm", SHM_NAME],
env={**os.environ, "SF_SHM_NAME": SHM_NAME, "SF_PARENT_PID": str(os.getpid()),
"SF_SAMPLE_RATE": "48000", "SF_BLOCK_SIZE": "256"},
stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT,
)
time.sleep(1.5)
print("bridge pid", proc.pid, "poll", proc.poll())
print("[1] load ch=5 Nexus only...")
push_controls([(2, 0, 0, 5, NEXUS)])
r = wait_log("instrument loaded ch=5", 60)
print(" ", r)
print("[2] open GUI ch=5...")
push_controls([(4, 0, 0, 5, "nexus")])
r = wait_log("GUI attached", 30)
print(" ", r)
time.sleep(2)
print("alive:", proc.poll() is None)
print("--- last 20 log lines ---")
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
for l in f.readlines()[-20:]:
print(l.rstrip())
kernel32.UnmapViewOfFile(ptr)
kernel32.CloseHandle(hMap)
try:
proc.terminate()
except Exception:
pass
print("done")
+78
View File
@@ -0,0 +1,78 @@
# Driver: SF_ONCE test — bridge loads ch=5 itself BEFORE main loop.
# Driver only creates SHM + spawns bridge, waits for "GUI attached" (success)
# or hang (timeout while alive).
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
SHM_NAME = "SonicForge_DAW_IPC_VERIFY"
SIZE = 32768
BRIDGE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe"
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "driver_once.log")
kernel32 = ctypes.windll.kernel32
kernel32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_char_p]
kernel32.CreateFileMappingA.restype = ctypes.c_void_p
kernel32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_size_t]
kernel32.MapViewOfFile.restype = ctypes.c_void_p
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
PAGE_READWRITE = 0x04
FILE_MAP_ALL_ACCESS = 0xF001F
hMap = kernel32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, PAGE_READWRITE, 0, SIZE, SHM_NAME.encode())
ptr = kernel32.MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0)
def wait_log(pattern, timeout):
end = time.time() + timeout
last = 0
while time.time() < end:
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
except FileNotFoundError:
lines = []
for i in range(last, len(lines)):
if pattern in lines[i]:
return lines[i].strip()
last = max(0, len(lines))
time.sleep(0.2)
return None
extra = dict(os.environ)
for k in list(extra):
if k.startswith("SF_"):
del extra[k]
extra.update({
"SF_SHM_NAME": SHM_NAME,
# SF_PARENT_PID omitted: parent watchdog off (isolate)
"SF_SAMPLE_RATE": "48000",
"SF_BLOCK_SIZE": "256",
"SF_ONCE": "1",
"SF_AUTOGUI": "0", # ensure loop hook stays off
})
# Allow extra gates via argv[1]: e.g. "SF_NO_WD SF_NO_QTIMER SF_NO_AUDIO"
for gate in sys.argv[1:]:
k, v = gate.split("=", 1)
extra[k] = v
proc = subprocess.Popen(
[BRIDGE, "--shm", SHM_NAME],
env=extra,
stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT,
)
print("bridge pid", proc.pid, "poll", proc.poll())
r = wait_log("SF_ONCE attach=", 90)
print("attach line:", r)
r2 = wait_log("GUI attached", 30)
print("gui line:", r2)
time.sleep(2)
print("FINAL alive:", proc.poll() is None)
print("--- last 30 log lines ---")
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
for l in lines[-30:]:
print(l.rstrip())
except FileNotFoundError:
pass
+174
View File
@@ -0,0 +1,174 @@
# Driver verify: SHM + spawn real bridge, reproduce user flow:
# load 6x Nexus -> open GUI ch=5 -> play notes -> switch instrument ch=5 (crash point)
# -> reopen GUI -> verify alive + writeIndex advancing + no crash.
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
SHM_NAME = "SonicForge_DAW_IPC_VERIFY"
SIZE = 32768 # bigger than struct, safe
BRIDGE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe"
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "driver_bridge.log")
NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3"
AMPLE = r"C:\Program Files\Common Files\VST3\Ample Sound\ACDD.vst3"
kernel32 = ctypes.windll.kernel32
kernel32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_char_p]
kernel32.CreateFileMappingA.restype = ctypes.c_void_p
kernel32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_size_t]
kernel32.MapViewOfFile.restype = ctypes.c_void_p
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
PAGE_READWRITE = 0x04
FILE_MAP_ALL_ACCESS = 0xF001F
FILE_MAP_WRITE = 0x0002
FILE_MAP_READ = 0x0004
hMap = kernel32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, PAGE_READWRITE, 0, SIZE, SHM_NAME.encode())
if not hMap:
print("CreateFileMapping failed", ctypes.get_last_error()); sys.exit(1)
ptr = kernel32.MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0)
if not ptr:
print("MapViewOfFile failed", ctypes.get_last_error()); sys.exit(1)
def w32(off, fmt, *args):
ctypes.memmove(ptr + off, struct.pack(fmt, *args), struct.calcsize(fmt))
def write_control(type_, arg0, arg1, ch, path):
# find first free slot
for i in range(8):
off = 2836 + i * 1040
t = ctypes.c_uint32.from_address(ptr + off).value
if t == 0:
w32(off, 'IIII', type_, arg0, arg1, ch)
if path:
b = path.encode('utf-8')[:1023] + b'\x00'
ctypes.memmove(ptr + off + 16, b, len(b))
return i
raise RuntimeError("control queue full")
def submit_controls():
w32(11156, 'I', 0) # will set below
# reset type of all slots first
for i in range(8):
w32(2836 + i * 1040, 'I', 0)
def push_controls(events):
submit_controls()
n = 0
for (type_, arg0, arg1, ch, path) in events:
write_control(type_, arg0, arg1, ch, path)
n += 1
w32(11156, 'I', n)
def midi(cmd, ch, pitch, vel, data2=0, data3=0):
# bridge does not zero midiQueue after drain - always write slot 0, count 1
w32(2064, 'BBBBBB', cmd, ch, pitch, vel, data2, data3)
w32(2064 + 8, 'I', 0)
w32(2832, 'I', 1)
return True
def midi_count():
return ctypes.c_uint32.from_address(ptr + 2832).value
def write_index():
return ctypes.c_uint32.from_address(ptr + 4).value
def wait_log(pattern, timeout, since=0):
end = time.time() + timeout
lines = []
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
except FileNotFoundError:
pass
last = max(0, len(lines) - since)
while time.time() < end:
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
except FileNotFoundError:
lines = []
for i in range(last, len(lines)):
if pattern in lines[i]:
return lines[i].strip()
last = max(0, len(lines))
time.sleep(0.2)
return None
proc = subprocess.Popen(
[BRIDGE, "--shm", SHM_NAME],
env={**os.environ, "SF_SHM_NAME": SHM_NAME, "SF_PARENT_PID": str(os.getpid()),
"SF_SAMPLE_RATE": "48000", "SF_BLOCK_SIZE": "256"},
stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT,
)
time.sleep(1.5)
print("bridge pid", proc.pid, "poll", proc.poll())
# 1. Load 6 channels Nexus
print("[1] load 6x Nexus...")
push_controls([(2, 0, 0, ch, NEXUS) for ch in range(6)])
print(" wait ch=5 loaded...")
r = wait_log("instrument loaded ch=5", 90)
print(" ", r)
# 2. Open GUI ch=5
print("[2] open GUI ch=5...")
push_controls([(4, 0, 0, 5, "nexus")])
r = wait_log("GUI attached", 30)
print(" ", r)
# 3. Play notes on ch=5 and ch=2 while editor open
print("[3] play notes...")
for k in range(40):
midi(0x9, 5, 60 + (k % 12), 100)
midi(0x9, 2, 48 + (k % 7), 90)
if k % 2 == 0:
midi(0x8, 5, 60 + ((k - 1) % 12), 0)
time.sleep(0.05)
# 4. Switch instrument ch=5 while GUI open (OLD CRASH POINT)
print("[4] switch instrument ch=5 -> Ample ACDD while GUI open...")
t0 = time.time()
push_controls([(2, 0, 0, 5, AMPLE)])
r = wait_log("instrument loaded ch=5", 40)
print(" ", r, "in", round(time.time() - t0, 1), "s")
alive = proc.poll() is None
wi = write_index()
time.sleep(2)
wi2 = write_index()
print(" alive:", alive, "writeIndex", wi, "->", wi2, "advancing:", wi2 > wi)
# 5. Reopen GUI ch=5 (after switch) - attach path again
print("[5] reopen GUI ch=5...")
push_controls([(4, 0, 0, 5, "ample")])
r = wait_log("GUI attached", 40)
print(" ", r)
# 6. Final liveness check + crash check
time.sleep(3)
alive = proc.poll() is None
wi3 = write_index()
time.sleep(2)
wi4 = write_index()
print("FINAL alive:", alive, "writeIndex", wi3, "->", wi4, "advancing:", wi4 > wi3)
if not alive:
print("!!! BRIDGE CRASHED exit", proc.returncode)
print("--- last 30 log lines ---")
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
for l in lines[-30:]:
print(l.rstrip())
except FileNotFoundError:
pass
# cleanup
kernel32.UnmapViewOfFile(ptr)
kernel32.CloseHandle(hMap)
try:
proc.terminate()
except Exception:
pass
time.sleep(0.5)
print("done")
+155
View File
@@ -0,0 +1,155 @@
# Stack sampler v2: correct x64 CONTEXT layout + psapi module names.
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
SHM_NAME = "SonicForge_DAW_IPC_VERIFY"
SIZE = 32768
BRIDGE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe"
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stack_sample.log")
k32 = ctypes.windll.kernel32
k32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, w.DWORD, w.DWORD, w.DWORD, ctypes.c_char_p]
k32.CreateFileMappingA.restype = ctypes.c_void_p
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, w.DWORD, w.DWORD, w.DWORD, ctypes.c_size_t]
k32.MapViewOfFile.restype = ctypes.c_void_p
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
hMap = k32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, 0x04, 0, SIZE, SHM_NAME.encode())
ptr = k32.MapViewOfFile(hMap, 0xF001F, 0, 0, 0)
def wait_log(pattern, timeout):
end = time.time() + timeout
while time.time() < end:
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
if pattern in f.read():
return True
except FileNotFoundError:
pass
time.sleep(0.2)
return False
extra = dict(os.environ)
for k in list(extra):
if k.startswith("SF_"):
del extra[k]
extra.update({"SF_SHM_NAME": SHM_NAME, "SF_SAMPLE_RATE": "48000",
"SF_BLOCK_SIZE": "256", "SF_ONCE": "1", "SF_AUTOGUI": "0"})
for gate in sys.argv[1:]:
k, v = gate.split("=", 1)
extra[k] = v
proc = subprocess.Popen([BRIDGE, "--shm", SHM_NAME], env=extra,
stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT)
print("bridge pid", proc.pid, "poll", proc.poll())
if not wait_log("isPlatformTypeSupported=0", 120):
print("TIMEOUT waiting for hang marker")
sys.exit(1)
time.sleep(3)
pid = proc.pid
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_VM_READ = 0x0010
hProc = k32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid)
if not hProc:
print("OpenProcess failed", ctypes.get_last_error())
sys.exit(1)
psapi = ctypes.windll.psapi
psapi.EnumProcessModulesEx.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), w.DWORD, ctypes.POINTER(w.DWORD), w.DWORD]
psapi.GetModuleBaseNameA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p, w.DWORD]
psapi.GetModuleInformation.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, w.DWORD]
class MODULEINFO(ctypes.Structure):
_fields_ = [("lpBaseOfDll", ctypes.c_void_p), ("SizeOfImage", w.DWORD),
("pad", w.DWORD), ("EntryPoint", ctypes.c_void_p)]
mods = []
buf = (ctypes.c_void_p * 1024)()
needed = w.DWORD(0)
if psapi.EnumProcessModulesEx(hProc, buf, ctypes.sizeof(buf), ctypes.byref(needed), 3): # LIST_MODULES_ALL
n = needed.value // ctypes.sizeof(ctypes.c_void_p)
for i in range(min(n, 1024)):
base = buf[i]
name = ctypes.create_string_buffer(260)
psapi.GetModuleBaseNameA(hProc, base, name, 260)
info = MODULEINFO()
psapi.GetModuleInformation(hProc, base, ctypes.byref(info), ctypes.sizeof(info))
mods.append((base, info.SizeOfImage, name.value.decode('utf-8', 'replace'))) # base, size, name
mods.sort(key=lambda m: m[0])
print("modules:", [(hex(b), s, n) for b, s, n in mods])
def modname(addr):
if addr == 0:
return "null"
for b, s, n in mods:
if b <= addr < b + s:
return "%s+%x" % (n, addr - b)
return "??%x" % addr
class THREADENTRY32(ctypes.Structure):
_fields_ = [("dwSize", w.DWORD), ("cntUsage", w.DWORD), ("th32ThreadID", w.DWORD),
("th32OwnerProcessID", w.DWORD), ("tpBasePri", ctypes.c_long),
("tpDeltaPri", ctypes.c_long), ("dwFlags", w.DWORD)]
class CONTEXT64(ctypes.Structure):
_fields_ = [
("P1Home", ctypes.c_uint64), ("P2Home", ctypes.c_uint64),
("P3Home", ctypes.c_uint64), ("P4Home", ctypes.c_uint64),
("P5Home", ctypes.c_uint64), ("P6Home", ctypes.c_uint64),
("ContextFlags", w.DWORD), ("MxCsr", w.DWORD),
("SegCs", w.WORD), ("SegDs", w.WORD), ("SegEs", w.WORD),
("SegFs", w.WORD), ("SegGs", w.WORD), ("SegSs", w.WORD),
("EFlags", w.DWORD),
("Dr0", ctypes.c_uint64), ("Dr1", ctypes.c_uint64),
("Dr2", ctypes.c_uint64), ("Dr3", ctypes.c_uint64),
("Dr6", ctypes.c_uint64), ("Dr7", ctypes.c_uint64),
("Rax", ctypes.c_uint64), ("Rcx", ctypes.c_uint64),
("Rdx", ctypes.c_uint64), ("Rbx", ctypes.c_uint64),
("Rsp", ctypes.c_uint64), ("Rbp", ctypes.c_uint64),
("Rsi", ctypes.c_uint64), ("Rdi", ctypes.c_uint64),
("R8", ctypes.c_uint64), ("R9", ctypes.c_uint64),
("R10", ctypes.c_uint64), ("R11", ctypes.c_uint64),
("R12", ctypes.c_uint64), ("R13", ctypes.c_uint64),
("R14", ctypes.c_uint64), ("R15", ctypes.c_uint64),
("Rip", ctypes.c_uint64),
]
TH32CS_SNAPTHREAD = 0x4
THREAD_SUSPEND_RESUME = 0x0002
THREAD_GET_CONTEXT = 0x0008
THREAD_QUERY_INFORMATION = 0x0040
CONTEXT_CTRL_INT_SEG = 0x100001 | 0x2 | 0x20 # CONTROL | INTEGER | SEGMENTS
snap = k32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)
te = THREADENTRY32(); te.dwSize = ctypes.sizeof(THREADENTRY32)
threads = []
ok = k32.Thread32First(snap, ctypes.byref(te))
while ok:
if te.th32OwnerProcessID == pid:
threads.append(te.th32ThreadID)
ok = k32.Thread32Next(snap, ctypes.byref(te))
print("threads(%d): %s" % (len(threads), threads))
for tid in threads:
hT = k32.OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, False, tid)
if not hT:
print("tid %d: OpenThread fail" % tid)
continue
susp = k32.SuspendThread(hT)
ctx = CONTEXT64(); ctx.ContextFlags = CONTEXT_CTRL_INT_SEG
r = k32.GetThreadContext(hT, ctypes.byref(ctx))
k32.ResumeThread(hT)
k32.CloseHandle(hT)
if not r:
print("tid %d: GetThreadContext fail %d" % (tid, ctypes.get_last_error()))
continue
print("tid %d rip=%s rsp=%s" % (tid, modname(ctx.Rip), modname(ctx.Rsp)))
print("--- log tail ---")
try:
with open(LOG, 'rb') as f:
f.seek(max(0, os.path.getsize(LOG) - 2500))
print(f.read().decode('utf-8', 'replace'))
except FileNotFoundError:
pass
+217
View File
@@ -0,0 +1,217 @@
# Stack sampler v4: thread start addrs (identify main) + poor-man stack walk.
# Usage: python stack_sample2.py --bridge | --probe [secs]
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
MODE = sys.argv[1] if len(sys.argv) > 1 else "--bridge"
SECS = int(sys.argv[2]) if len(sys.argv) > 2 else 15
SHM_NAME = "SonicForge_DAW_IPC_VERIFY"
SIZE = 32768
BRIDGE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe"
PROBE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\gui_probe.exe"
NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3"
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"stack_sample.log" if MODE == "--bridge" else "stack_probe.log")
k32 = ctypes.windll.kernel32
k32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, w.DWORD, w.DWORD, w.DWORD, ctypes.c_char_p]
k32.CreateFileMappingA.restype = ctypes.c_void_p
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, w.DWORD, w.DWORD, w.DWORD, ctypes.c_size_t]
k32.MapViewOfFile.restype = ctypes.c_void_p
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
hMap = k32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, 0x04, 0, SIZE, SHM_NAME.encode())
ptr = k32.MapViewOfFile(hMap, 0xF001F, 0, 0, 0)
def wait_log(pattern, timeout):
end = time.time() + timeout
while time.time() < end:
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
if pattern in f.read():
return True
except FileNotFoundError:
pass
time.sleep(0.2)
return False
if MODE == "--bridge":
extra = dict(os.environ)
for k in list(extra):
if k.startswith("SF_"):
del extra[k]
extra.update({"SF_SHM_NAME": SHM_NAME, "SF_SAMPLE_RATE": "48000",
"SF_BLOCK_SIZE": "256", "SF_ONCE": "1", "SF_AUTOGUI": "0"})
proc = subprocess.Popen([BRIDGE, "--shm", SHM_NAME], env=extra,
stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT)
marker = "isPlatformTypeSupported=0"
wait_after = 3
else:
proc = subprocess.Popen([PROBE, NEXUS, "bridge_exact11", str(SECS)],
stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT)
marker = "openGUI: attached=0"
wait_after = 2
print("pid", proc.pid, "poll", proc.poll(), "mode", MODE)
if not wait_log(marker, 120):
print("TIMEOUT waiting for", marker)
sys.exit(1)
time.sleep(wait_after)
pid = proc.pid
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_VM_READ = 0x0010
hProc = k32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid)
if not hProc:
print("OpenProcess failed", ctypes.get_last_error())
sys.exit(1)
psapi = ctypes.windll.psapi
class MODULEINFO(ctypes.Structure):
_fields_ = [("lpBaseOfDll", ctypes.c_void_p), ("SizeOfImage", w.DWORD),
("pad", w.DWORD), ("EntryPoint", ctypes.c_void_p)]
psapi.EnumProcessModulesEx.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), w.DWORD, ctypes.POINTER(w.DWORD), w.DWORD]
psapi.GetModuleBaseNameA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p, w.DWORD]
psapi.GetModuleInformation.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(MODULEINFO), w.DWORD]
mods = []
buf = (ctypes.c_void_p * 1024)()
needed = w.DWORD(0)
if psapi.EnumProcessModulesEx(hProc, buf, ctypes.sizeof(buf), ctypes.byref(needed), 3):
n = needed.value // ctypes.sizeof(ctypes.c_void_p)
for i in range(min(n, 1024)):
base = buf[i]
name = ctypes.create_string_buffer(260)
psapi.GetModuleBaseNameA(hProc, base, name, 260)
info = MODULEINFO()
psapi.GetModuleInformation(hProc, base, ctypes.byref(info), ctypes.sizeof(info))
mods.append((base, info.SizeOfImage, name.value.decode('utf-8', 'replace')))
mods.sort(key=lambda m: m[0])
def modname(addr):
if addr == 0:
return "null"
for b, s, n in mods:
if b <= addr < b + s:
return "%s+%x" % (n, addr - b)
return "??%x" % addr
class THREADENTRY32(ctypes.Structure):
_fields_ = [("dwSize", w.DWORD), ("cntUsage", w.DWORD), ("th32ThreadID", w.DWORD),
("th32OwnerProcessID", w.DWORD), ("tpBasePri", ctypes.c_long),
("tpDeltaPri", ctypes.c_long), ("dwFlags", w.DWORD)]
class CONTEXT64(ctypes.Structure):
_fields_ = [
("P1Home", ctypes.c_uint64), ("P2Home", ctypes.c_uint64),
("P3Home", ctypes.c_uint64), ("P4Home", ctypes.c_uint64),
("P5Home", ctypes.c_uint64), ("P6Home", ctypes.c_uint64),
("ContextFlags", w.DWORD), ("MxCsr", w.DWORD),
("SegCs", w.WORD), ("SegDs", w.WORD), ("SegEs", w.WORD),
("SegFs", w.WORD), ("SegGs", w.WORD), ("SegSs", w.WORD),
("EFlags", w.DWORD),
("Dr0", ctypes.c_uint64), ("Dr1", ctypes.c_uint64),
("Dr2", ctypes.c_uint64), ("Dr3", ctypes.c_uint64),
("Dr6", ctypes.c_uint64), ("Dr7", ctypes.c_uint64),
("Rax", ctypes.c_uint64), ("Rcx", ctypes.c_uint64),
("Rdx", ctypes.c_uint64), ("Rbx", ctypes.c_uint64),
("Rsp", ctypes.c_uint64), ("Rbp", ctypes.c_uint64),
("Rsi", ctypes.c_uint64), ("Rdi", ctypes.c_uint64),
("R8", ctypes.c_uint64), ("R9", ctypes.c_uint64),
("R10", ctypes.c_uint64), ("R11", ctypes.c_uint64),
("R12", ctypes.c_uint64), ("R13", ctypes.c_uint64),
("R14", ctypes.c_uint64), ("R15", ctypes.c_uint64),
("Rip", ctypes.c_uint64),
]
TH32CS_SNAPTHREAD = 0x4
THREAD_SUSPEND_RESUME = 0x0002
THREAD_GET_CONTEXT = 0x0008
THREAD_QUERY_INFORMATION = 0x0040
CONTEXT_CTRL_INT_SEG = 0x100001 | 0x2 | 0x20
ntdll = ctypes.WinDLL("ntdll", use_last_error=True)
class THREAD_BASIC_INFORMATION(ctypes.Structure):
_fields_ = [("ExitStatus", ctypes.c_long), ("TebBaseAddress", ctypes.c_void_p),
("UniqueProcess", ctypes.c_void_p), ("UniqueThread", ctypes.c_void_p),
("AffinityMask", ctypes.c_void_p), ("Priority", ctypes.c_long),
("BasePriority", ctypes.c_long)]
def thread_start_addr(hT):
tbi = THREAD_BASIC_INFORMATION()
buf = (ctypes.c_uint64 * 4)()
try:
r = ntdll.NtQueryInformationThread(hT, 9, ctypes.byref(buf), 32, None) # ThreadQuerySetWin32StartAddress
if r == 0:
return buf[0]
except Exception:
pass
return 0
snap = k32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)
te = THREADENTRY32(); te.dwSize = ctypes.sizeof(THREADENTRY32)
threads = []
ok = k32.Thread32First(snap, ctypes.byref(te))
while ok:
if te.th32OwnerProcessID == pid:
threads.append(te.th32ThreadID)
ok = k32.Thread32Next(snap, ctypes.byref(te))
exe_name = mods[0][2] if mods else "exe"
main_tid = None
print("threads(%d): %s" % (len(threads), threads))
for tid in threads:
hT = k32.OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, False, tid)
if not hT:
continue
start = thread_start_addr(hT)
susp = k32.SuspendThread(hT)
ctx = CONTEXT64(); ctx.ContextFlags = CONTEXT_CTRL_INT_SEG
r = k32.GetThreadContext(hT, ctypes.byref(ctx))
k32.ResumeThread(hT)
k32.CloseHandle(hT)
if not r:
continue
if mods and mods[0][0] <= start < mods[0][0] + mods[0][1] and main_tid is None:
main_tid = tid
print("tid %d rip=%s rsp=%s start=%s%s" % (
tid, modname(ctx.Rip), modname(ctx.Rsp), modname(start),
" <== MAIN?" if (mods and mods[0][0] <= start < mods[0][0] + mods[0][1]) else ""))
# poor-man stack walk for MAIN thread
if main_tid:
print("--- stack walk MAIN tid=%d ---" % main_tid)
hT = k32.OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, False, main_tid)
k32.SuspendThread(hT)
ctx = CONTEXT64(); ctx.ContextFlags = CONTEXT_CTRL_INT_SEG
k32.GetThreadContext(hT, ctypes.byref(ctx))
k32.ResumeThread(hT)
k32.CloseHandle(hT)
rsp = ctx.Rsp
PAGE = 0x1000
seen = []
# scan a few pages of stack memory
base = rsp & ~0xF
data = b""
try:
buf2 = ctypes.create_string_buffer(PAGE * 64)
read = ctypes.c_size_t(0)
if k32.ReadProcessMemory(hProc, ctypes.c_void_p(base), buf2, PAGE * 64, ctypes.byref(read)):
data = buf2.raw[:read.value]
except Exception:
data = b""
for off in range(0, len(data) - 8, 8):
val = struct.unpack_from('<Q', data, off)[0]
if val >= 0x7ff000000000:
mn = modname(val)
if mn not in seen and (mn.startswith("Nexus") or mn.startswith("daw") or mn.startswith("USER32") or mn.startswith("ntdll") or mn.startswith("win32u") or mn.startswith("KERNELBASE") or mn.startswith("ole32") or mn.startswith("combase")):
seen.append(mn)
print("main stack (module+offset, dedup):")
for s in seen:
print(" ", s)
print("--- log tail ---")
try:
with open(LOG, 'rb') as f:
f.seek(max(0, os.path.getsize(LOG) - 1500))
print(f.read().decode('utf-8', 'replace'))
except FileNotFoundError:
pass
+249
View File
@@ -0,0 +1,249 @@
# Stack scan v7: read thread stack in small chunks, skip unreadable pages
# (v6 failed reading upward from rsp when the thread's stack was shallow and
# the 0x4000 chunk crossed the committed top). Also dumps full registers for
# the window-owner thread so the wait handle/params can be read.
# Usage: python stackscan.py --bridge | --probe [secs]
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
MODE = sys.argv[1] if len(sys.argv) > 1 else "--bridge"
SECS = int(sys.argv[2]) if len(sys.argv) > 2 else 15
SHM_NAME = "SonicForge_DAW_IPC_VERIFY"
SIZE = 32768
BRIDGE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe"
PROBE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\gui_probe.exe"
NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3"
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"stack_sample.log" if MODE == "--bridge" else "stack_probe.log")
k32 = ctypes.windll.kernel32
k32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, w.DWORD, w.DWORD, w.DWORD, ctypes.c_char_p]
k32.CreateFileMappingA.restype = ctypes.c_void_p
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, w.DWORD, w.DWORD, w.DWORD, ctypes.c_size_t]
k32.MapViewOfFile.restype = ctypes.c_void_p
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
hMap = k32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, 0x04, 0, SIZE, SHM_NAME.encode())
ptr = k32.MapViewOfFile(hMap, 0xF001F, 0, 0, 0)
def wait_log(pattern, timeout):
end = time.time() + timeout
while time.time() < end:
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
if pattern in f.read():
return True
except FileNotFoundError:
pass
time.sleep(0.2)
return False
if MODE == "--bridge":
extra = dict(os.environ)
for k in list(extra):
if k.startswith("SF_"):
del extra[k]
extra.update({"SF_SHM_NAME": SHM_NAME, "SF_SAMPLE_RATE": "48000",
"SF_BLOCK_SIZE": "256", "SF_ONCE": "1", "SF_AUTOGUI": "0"})
proc = subprocess.Popen([BRIDGE, "--shm", SHM_NAME], env=extra,
stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT)
marker = "isPlatformTypeSupported=0"
wait_after = 3
wnd_class = "SonicForge_Native_VST3_Class"
else:
proc = subprocess.Popen([PROBE, NEXUS, "bridge_exact11", str(SECS)],
stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT)
marker = "openGUI: attached=0"
wait_after = 2
wnd_class = "SonicForge_Native_VST3_Class"
print("pid", proc.pid, "mode", MODE)
if not wait_log(marker, 120):
print("TIMEOUT waiting for", marker)
sys.exit(1)
time.sleep(wait_after)
pid = proc.pid
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_VM_READ = 0x0010
k32.OpenProcess.argtypes = [w.DWORD, w.BOOL, w.DWORD]
k32.OpenProcess.restype = ctypes.c_void_p
hProc = k32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid)
if not hProc:
print("OpenProcess failed", ctypes.get_last_error())
sys.exit(1)
psapi = ctypes.windll.psapi
class MODULEINFO(ctypes.Structure):
_fields_ = [("lpBaseOfDll", ctypes.c_void_p), ("SizeOfImage", w.DWORD),
("pad", w.DWORD), ("EntryPoint", ctypes.c_void_p)]
psapi.EnumProcessModulesEx.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), w.DWORD, ctypes.POINTER(w.DWORD), w.DWORD]
psapi.GetModuleBaseNameA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p, w.DWORD]
psapi.GetModuleInformation.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(MODULEINFO), w.DWORD]
mods = []
buf = (ctypes.c_void_p * 1024)()
needed = w.DWORD(0)
if psapi.EnumProcessModulesEx(hProc, buf, ctypes.sizeof(buf), ctypes.byref(needed), 3):
n = needed.value // ctypes.sizeof(ctypes.c_void_p)
for i in range(min(n, 1024)):
base = buf[i]
name = ctypes.create_string_buffer(260)
psapi.GetModuleBaseNameA(hProc, base, name, 260)
info = MODULEINFO()
psapi.GetModuleInformation(hProc, base, ctypes.byref(info), ctypes.sizeof(info))
mods.append((base, info.SizeOfImage, name.value.decode('utf-8', 'replace')))
mods.sort(key=lambda m: m[0])
def modname(addr):
if addr == 0:
return "null"
for b, s, n in mods:
if b <= addr < b + s:
return "%s+%x" % (n, addr - b)
return None
# --- find the target window + its owning thread (main) ---
user32 = ctypes.windll.user32
user32.EnumWindows.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
user32.GetWindowThreadProcessId.argtypes = [ctypes.c_void_p, ctypes.POINTER(w.DWORD)]
user32.GetClassNameA.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int]
WNDENUMPROC = ctypes.WINFUNCTYPE(w.BOOL, ctypes.c_void_p, ctypes.c_void_p)
found = {"hwnd": None, "tid": None}
@WNDENUMPROC
def enumcb(hwnd, lparam):
wpid = w.DWORD(0)
tid = user32.GetWindowThreadProcessId(hwnd, ctypes.byref(wpid))
if wpid.value != pid:
return True
cls = ctypes.create_string_buffer(256)
user32.GetClassNameA(hwnd, cls, 256)
if cls.value.decode('utf-8', 'replace') == wnd_class:
found["hwnd"] = hwnd
found["tid"] = tid
return False
return True
user32.EnumWindows(enumcb, None)
print("target window hwnd=%s tid=%s" % (found["hwnd"], found["tid"]))
class THREADENTRY32(ctypes.Structure):
_fields_ = [("dwSize", w.DWORD), ("cntUsage", w.DWORD), ("th32ThreadID", w.DWORD),
("th32OwnerProcessID", w.DWORD), ("tpBasePri", ctypes.c_long),
("tpDeltaPri", ctypes.c_long), ("dwFlags", w.DWORD)]
TH32CS_SNAPTHREAD = 0x4
THREAD_SUSPEND_RESUME = 0x0002
THREAD_GET_CONTEXT = 0x0008
THREAD_QUERY_INFORMATION = 0x0040
CONTEXT_CTRL_INT_SEG = 0x100001 | 0x2 | 0x20
class CONTEXT64(ctypes.Structure):
_fields_ = [
("P1Home", ctypes.c_uint64), ("P2Home", ctypes.c_uint64),
("P3Home", ctypes.c_uint64), ("P4Home", ctypes.c_uint64),
("P5Home", ctypes.c_uint64), ("P6Home", ctypes.c_uint64),
("ContextFlags", w.DWORD), ("MxCsr", w.DWORD),
("SegCs", w.WORD), ("SegDs", w.WORD), ("SegEs", w.WORD),
("SegFs", w.WORD), ("SegGs", w.WORD), ("SegSs", w.WORD),
("EFlags", w.DWORD),
("Dr0", ctypes.c_uint64), ("Dr1", ctypes.c_uint64),
("Dr2", ctypes.c_uint64), ("Dr3", ctypes.c_uint64),
("Dr6", ctypes.c_uint64), ("Dr7", ctypes.c_uint64),
("Rax", ctypes.c_uint64), ("Rcx", ctypes.c_uint64),
("Rdx", ctypes.c_uint64), ("Rbx", ctypes.c_uint64),
("Rsp", ctypes.c_uint64), ("Rbp", ctypes.c_uint64),
("Rsi", ctypes.c_uint64), ("Rdi", ctypes.c_uint64),
("R8", ctypes.c_uint64), ("R9", ctypes.c_uint64),
("R10", ctypes.c_uint64), ("R11", ctypes.c_uint64),
("R12", ctypes.c_uint64), ("R13", ctypes.c_uint64),
("R14", ctypes.c_uint64), ("R15", ctypes.c_uint64),
("Rip", ctypes.c_uint64),
]
k32.CreateToolhelp32Snapshot.argtypes = [w.DWORD, w.DWORD]
k32.CreateToolhelp32Snapshot.restype = ctypes.c_void_p
snap = k32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)
te = THREADENTRY32(); te.dwSize = ctypes.sizeof(THREADENTRY32)
threads = []
ok = k32.Thread32First(snap, ctypes.byref(te))
while ok:
if te.th32OwnerProcessID == pid:
threads.append(te.th32ThreadID)
ok = k32.Thread32Next(snap, ctypes.byref(te))
INTEREST = ("Nexus", "daw_", "USER32", "ntdll", "win32u", "KERNELBASE", "ole32",
"combase", "uxtheme", "d2d1", "d3d11", "MSCTF", "IMM32", "SHELL32")
def read_mem(addr, size):
"""ReadProcessMemory with proper argtypes; returns bytes or None."""
chunk = ctypes.create_string_buffer(size)
read = ctypes.c_size_t(0)
k32.ReadProcessMemory.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
if not k32.ReadProcessMemory(hProc, ctypes.c_void_p(addr), chunk, size, ctypes.byref(read)):
return None
return chunk.raw[:read.value]
def scan_stack(tid, depth=0x20000):
k32.OpenThread.argtypes = [w.DWORD, w.BOOL, w.DWORD]
k32.OpenThread.restype = ctypes.c_void_p
hT = k32.OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, False, tid)
if not hT:
return None, [], None
k32.SuspendThread(hT)
ctx = CONTEXT64(); ctx.ContextFlags = CONTEXT_CTRL_INT_SEG
r = k32.GetThreadContext(hT, ctypes.byref(ctx))
k32.ResumeThread(hT)
k32.CloseHandle(hT)
if not r:
return None, [], None
rsp = ctx.Rsp
chunks = []
fails = []
# read downward a bit first (trap frame may sit below rsp), then upward
for base in (rsp - 0x1000, rsp, rsp + 0x1000, rsp + 0x2000):
if base > rsp + depth:
break
d = read_mem(base, 0x1000)
if d is None:
fails.append(hex(base))
else:
chunks.append((base, d))
data = b"".join(d for _, d in chunks)
seen = []
for addr, d in chunks:
for i in range(0, len(d) - 8, 8):
val = struct.unpack_from('<Q', d, i)[0]
mn = modname(val)
if mn:
base_mod = mn.split("+")[0]
if any(base_mod.startswith(ig) for ig in INTEREST):
seen.append((addr + i, mn))
seen.sort()
return ctx, seen, fails, rsp
regnames = ["Rax", "Rcx", "Rdx", "Rbx", "Rsp", "Rbp", "Rsi", "Rdi",
"R8", "R9", "R10", "R11", "R12", "R13", "R14", "R15", "Rip"]
for tid in threads:
ctx, stack, fails, rsp = scan_stack(tid)
tag = " <== WINDOW OWNER (main)" if tid == found["tid"] else ""
if not ctx:
print("=== tid %d (no context) ===" % tid)
continue
rip = ctx.Rip
print("=== tid %d rip=%s%s ===" % (tid, modname(rip) if modname(rip) else hex(rip), tag))
if tag:
print(" regs:", " ".join("%s=%s" % (n, hex(getattr(ctx, n))) for n in regnames))
print(" stack read fails:", fails if fails else "none")
shown = [s for s in stack if s[0] >= rsp] or stack
for a, mn in shown[-60:]:
print(" ", "%s @%x" % (mn, a))
print("--- log tail ---")
try:
with open(LOG, 'rb') as f:
f.seek(max(0, os.path.getsize(LOG) - 1000))
print(f.read().decode('utf-8', 'replace'))
except FileNotFoundError:
pass
+218
View File
@@ -0,0 +1,218 @@
# Stack walker v5: dbghelp StackWalk64 on every thread of a live process.
# Usage: python stackwalk.py --bridge | --probe [secs]
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
MODE = sys.argv[1] if len(sys.argv) > 1 else "--bridge"
SECS = int(sys.argv[2]) if len(sys.argv) > 2 else 15
SHM_NAME = "SonicForge_DAW_IPC_VERIFY"
SIZE = 32768
BRIDGE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe"
PROBE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\gui_probe.exe"
NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3"
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"stack_sample.log" if MODE == "--bridge" else "stack_probe.log")
k32 = ctypes.windll.kernel32
k32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, w.DWORD, w.DWORD, w.DWORD, ctypes.c_char_p]
k32.CreateFileMappingA.restype = ctypes.c_void_p
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, w.DWORD, w.DWORD, w.DWORD, ctypes.c_size_t]
k32.MapViewOfFile.restype = ctypes.c_void_p
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
hMap = k32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, 0x04, 0, SIZE, SHM_NAME.encode())
ptr = k32.MapViewOfFile(hMap, 0xF001F, 0, 0, 0)
def wait_log(pattern, timeout):
end = time.time() + timeout
while time.time() < end:
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
if pattern in f.read():
return True
except FileNotFoundError:
pass
time.sleep(0.2)
return False
if MODE == "--bridge":
extra = dict(os.environ)
for k in list(extra):
if k.startswith("SF_"):
del extra[k]
extra.update({"SF_SHM_NAME": SHM_NAME, "SF_SAMPLE_RATE": "48000",
"SF_BLOCK_SIZE": "256", "SF_ONCE": "1", "SF_AUTOGUI": "0"})
proc = subprocess.Popen([BRIDGE, "--shm", SHM_NAME], env=extra,
stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT)
marker = "isPlatformTypeSupported=0"
wait_after = 3
else:
proc = subprocess.Popen([PROBE, NEXUS, "bridge_exact11", str(SECS)],
stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT)
marker = "openGUI: attached=0"
wait_after = 2
print("pid", proc.pid, "mode", MODE)
if not wait_log(marker, 120):
print("TIMEOUT waiting for", marker)
sys.exit(1)
time.sleep(wait_after)
pid = proc.pid
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_VM_READ = 0x0010
hProc = k32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid)
if not hProc:
print("OpenProcess failed", ctypes.get_last_error())
sys.exit(1)
# module map
psapi = ctypes.windll.psapi
class MODULEINFO(ctypes.Structure):
_fields_ = [("lpBaseOfDll", ctypes.c_void_p), ("SizeOfImage", w.DWORD),
("pad", w.DWORD), ("EntryPoint", ctypes.c_void_p)]
psapi.EnumProcessModulesEx.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), w.DWORD, ctypes.POINTER(w.DWORD), w.DWORD]
psapi.GetModuleBaseNameA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p, w.DWORD]
psapi.GetModuleInformation.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(MODULEINFO), w.DWORD]
mods = []
buf = (ctypes.c_void_p * 1024)()
needed = w.DWORD(0)
if psapi.EnumProcessModulesEx(hProc, buf, ctypes.sizeof(buf), ctypes.byref(needed), 3):
n = needed.value // ctypes.sizeof(ctypes.c_void_p)
for i in range(min(n, 1024)):
base = buf[i]
name = ctypes.create_string_buffer(260)
psapi.GetModuleBaseNameA(hProc, base, name, 260)
info = MODULEINFO()
psapi.GetModuleInformation(hProc, base, ctypes.byref(info), ctypes.sizeof(info))
mods.append((base, info.SizeOfImage, name.value.decode('utf-8', 'replace')))
mods.sort(key=lambda m: m[0])
def modname(addr):
if addr == 0:
return "null"
for b, s, n in mods:
if b <= addr < b + s:
return "%s+%x" % (n, addr - b)
return "??%x" % addr
# dbghelp stack walk
dbg = ctypes.WinDLL("dbghelp")
dbg.SymInitializeW.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p, w.BOOL]
dbg.SymInitializeW.restype = w.BOOL
dbg.SymSetOptions.argtypes = [w.DWORD]
dbg.SymSetOptions.restype = w.DWORD
dbg.StackWalk64.argtypes = [w.DWORD, ctypes.c_void_p, ctypes.c_void_p,
ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p,
ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, w.DWORD]
dbg.StackWalk64.restype = w.BOOL
class STACKFRAME64(ctypes.Structure):
class ADDR(ctypes.Structure):
_fields_ = [("Offset", ctypes.c_uint64), ("Segment", w.DWORD),
("Mode", w.DWORD)]
_fields_ = [("AddrPC", ADDR), ("AddrReturn", ADDR), ("AddrFrame", ADDR),
("AddrStack", ADDR), ("AddrBStore", ADDR), ("FuncTableEntry", ctypes.c_void_p),
("Params", ctypes.c_uint64 * 4), ("Far", w.BOOL),
("Virtual", w.BOOL), ("Reserved", ctypes.c_uint64 * 3),
("KdHelp", ctypes.c_uint64 * 2)]
class CONTEXT64(ctypes.Structure):
_fields_ = [
("P1Home", ctypes.c_uint64), ("P2Home", ctypes.c_uint64),
("P3Home", ctypes.c_uint64), ("P4Home", ctypes.c_uint64),
("P5Home", ctypes.c_uint64), ("P6Home", ctypes.c_uint64),
("ContextFlags", w.DWORD), ("MxCsr", w.DWORD),
("SegCs", w.WORD), ("SegDs", w.WORD), ("SegEs", w.WORD),
("SegFs", w.WORD), ("SegGs", w.WORD), ("SegSs", w.WORD),
("EFlags", w.DWORD),
("Dr0", ctypes.c_uint64), ("Dr1", ctypes.c_uint64),
("Dr2", ctypes.c_uint64), ("Dr3", ctypes.c_uint64),
("Dr6", ctypes.c_uint64), ("Dr7", ctypes.c_uint64),
("Rax", ctypes.c_uint64), ("Rcx", ctypes.c_uint64),
("Rdx", ctypes.c_uint64), ("Rbx", ctypes.c_uint64),
("Rsp", ctypes.c_uint64), ("Rbp", ctypes.c_uint64),
("Rsi", ctypes.c_uint64), ("Rdi", ctypes.c_uint64),
("R8", ctypes.c_uint64), ("R9", ctypes.c_uint64),
("R10", ctypes.c_uint64), ("R11", ctypes.c_uint64),
("R12", ctypes.c_uint64), ("R13", ctypes.c_uint64),
("R14", ctypes.c_uint64), ("R15", ctypes.c_uint64),
("Rip", ctypes.c_uint64),
]
ReadProcessMemory_cb = ctypes.CFUNCTYPE(w.BOOL, ctypes.c_void_p, ctypes.c_uint64,
ctypes.c_void_p, w.DWORD, ctypes.POINTER(w.DWORD))
@ReadProcessMemory_cb
def read_mem(hProc, addr, buf, size, nread):
return k32.ReadProcessMemory(hProc, ctypes.c_void_p(addr), buf, size, nread)
class THREADENTRY32(ctypes.Structure):
_fields_ = [("dwSize", w.DWORD), ("cntUsage", w.DWORD), ("th32ThreadID", w.DWORD),
("th32OwnerProcessID", w.DWORD), ("tpBasePri", ctypes.c_long),
("tpDeltaPri", ctypes.c_long), ("dwFlags", w.DWORD)]
TH32CS_SNAPTHREAD = 0x4
THREAD_SUSPEND_RESUME = 0x0002
THREAD_GET_CONTEXT = 0x0008
THREAD_QUERY_INFORMATION = 0x0040
CONTEXT_CTRL_INT_SEG = 0x100001 | 0x2 | 0x20
IMAGE_FILE_MACHINE_AMD64 = 0x8664
dbg.SymInitializeW(hProc, None, True)
dbg.SymSetOptions(0x2) # SYMOPT_DEFERRED_LOADS
snap = k32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)
te = THREADENTRY32(); te.dwSize = ctypes.sizeof(THREADENTRY32)
threads = []
ok = k32.Thread32First(snap, ctypes.byref(te))
while ok:
if te.th32OwnerProcessID == pid:
threads.append(te.th32ThreadID)
ok = k32.Thread32Next(snap, ctypes.byref(te))
print("threads(%d): %s" % (len(threads), threads))
for tid in threads:
hT = k32.OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, False, tid)
if not hT:
continue
k32.SuspendThread(hT)
ctx = CONTEXT64(); ctx.ContextFlags = CONTEXT_CTRL_INT_SEG
r = k32.GetThreadContext(hT, ctypes.byref(ctx))
if not r:
k32.ResumeThread(hT); k32.CloseHandle(hT)
print("tid %d GetThreadContext fail" % tid)
continue
sf = STACKFRAME64()
sf.AddrPC.Offset = ctx.Rip
sf.AddrPC.Mode = 0
sf.AddrReturn.Offset = ctx.Rsp # will be set by walker
sf.AddrFrame.Offset = ctx.Rbp
sf.AddrStack.Offset = ctx.Rsp
frames = []
for i in range(48):
if not dbg.StackWalk64(IMAGE_FILE_MACHINE_AMD64, hProc, hT, ctypes.byref(sf),
ctypes.byref(ctx), read_mem,
dbg.SymFunctionTableAccess64, dbg.SymGetModuleBase64,
None, 0):
break
if sf.AddrPC.Offset == 0:
break
frames.append(modname(sf.AddrPC.Offset))
if sf.AddrPC.Offset == sf.AddrReturn.Offset:
break
if i > 0 and frames[-1] == frames[-2]:
break
k32.ResumeThread(hT)
k32.CloseHandle(hT)
print("=== tid %d (rip %s) ===" % (tid, modname(ctx.Rip)))
for f in frames:
print(" ", f)
print("--- log tail ---")
try:
with open(LOG, 'rb') as f:
f.seek(max(0, os.path.getsize(LOG) - 1200))
print(f.read().decode('utf-8', 'replace'))
except FileNotFoundError:
pass
+198
View File
@@ -0,0 +1,198 @@
# waitscan: dump all threads' registers + wait-handle object types of a hung
# bridge. Goal: find what Nexus attached() and its worker threads wait on.
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
SHM_NAME = "SonicForge_DAW_IPC_VERIFY"
SIZE = 32768
BRIDGE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe"
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "waitscan.log")
k32 = ctypes.windll.kernel32
k32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, w.DWORD, w.DWORD, w.DWORD, ctypes.c_char_p]
k32.CreateFileMappingA.restype = ctypes.c_void_p
k32.MapViewOfFile.argtypes = [ctypes.c_void_p, w.DWORD, w.DWORD, w.DWORD, ctypes.c_size_t]
k32.MapViewOfFile.restype = ctypes.c_void_p
hMap = k32.CreateFileMappingA(ctypes.c_void_p(-1).value, None, 0x04, 0, SIZE, SHM_NAME.encode())
k32.MapViewOfFile(hMap, 0xF001F, 0, 0, 0)
def wait_log(pattern, timeout):
end = time.time() + timeout
while time.time() < end:
try:
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
if pattern in f.read():
return True
except FileNotFoundError:
pass
time.sleep(0.2)
return False
extra = dict(os.environ)
for k in list(extra):
if k.startswith("SF_"):
del extra[k]
extra.update({"SF_SHM_NAME": SHM_NAME, "SF_SAMPLE_RATE": "48000",
"SF_BLOCK_SIZE": "256", "SF_ONCE": "1", "SF_AUTOGUI": "0",
"SF_ONCE_PROBEWIN": "1"})
proc = subprocess.Popen([BRIDGE, "--shm", SHM_NAME], env=extra,
stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT)
pid = proc.pid
print("pid", pid)
if not wait_log("isPlatformTypeSupported=0", 120):
print("TIMEOUT"); sys.exit(1)
time.sleep(3)
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_VM_READ = 0x0010
PROCESS_DUP_HANDLE = 0x0040
TH32CS_SNAPTHREAD = 0x4
THREAD_GET_CONTEXT = 0x0008
THREAD_QUERY_INFORMATION = 0x0040
THREAD_SUSPEND_RESUME = 0x0002
k32.OpenProcess.argtypes = [w.DWORD, w.BOOL, w.DWORD]
k32.OpenProcess.restype = ctypes.c_void_p
hProc = k32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_DUP_HANDLE, False, pid)
if not hProc:
print("OpenProcess failed", ctypes.get_last_error()); sys.exit(1)
# module list for name resolution
psapi = ctypes.windll.psapi
class MODULEINFO(ctypes.Structure):
_fields_ = [("lpBaseOfDll", ctypes.c_void_p), ("SizeOfImage", w.DWORD),
("pad", w.DWORD), ("EntryPoint", ctypes.c_void_p)]
psapi.EnumProcessModulesEx.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), w.DWORD, ctypes.POINTER(w.DWORD), w.DWORD]
psapi.GetModuleInformation.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(MODULEINFO), w.DWORD]
psapi.GetModuleBaseNameA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p, w.DWORD]
nmods = w.DWORD(0)
psapi.EnumProcessModulesEx(hProc, None, 0, ctypes.byref(nmods), 3)
arr = (ctypes.c_void_p * (nmods.value // 8))()
psapi.EnumProcessModulesEx(hProc, arr, nmods.value, ctypes.byref(nmods), 3)
mods = []
for m in arr:
if not m:
continue
mi = MODULEINFO()
psapi.GetModuleInformation(hProc, m, ctypes.byref(mi), ctypes.sizeof(MODULEINFO))
name = ctypes.create_string_buffer(260)
psapi.GetModuleBaseNameA(hProc, m, name, 260)
mods.append((mi.lpBaseOfDll, mi.SizeOfImage, name.value.decode('latin1')))
def resolve(addr):
for base, size, name in mods:
if base <= addr < base + size:
return f"{name}+0x{addr-base:x}"
return f"0x{addr:x}"
class THREADENTRY32(ctypes.Structure):
_fields_ = [("dwSize", w.DWORD), ("cntUsage", w.DWORD), ("th32ThreadID", w.DWORD),
("th32OwnerProcessID", w.DWORD), ("tpBasePri", w.LONG),
("tpDeltaPri", w.LONG), ("dwFlags", w.DWORD)]
te = THREADENTRY32()
te.dwSize = ctypes.sizeof(THREADENTRY32)
snap = k32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)
ok = k32.Thread32First(snap, ctypes.byref(te))
threads = []
while ok:
if te.th32OwnerProcessID == pid:
threads.append(te.th32ThreadID)
ok = k32.Thread32Next(snap, ctypes.byref(te))
k32.CloseHandle(snap)
print(f"threads: {threads}")
class CONTEXT(ctypes.Structure):
# full x64 CONTEXT is 0x4d0 bytes; we only parse the GPR area (0x00-0xf8).
_fields_ = [("raw", ctypes.c_ubyte * 0x4d0)]
CONTEXT_AMD64 = 0x00100000
CONTEXT_CONTROL = CONTEXT_AMD64 | 0x1
CONTEXT_INTEGER = CONTEXT_AMD64 | 0x2
CONTEXT_FULL = CONTEXT_CONTROL | CONTEXT_INTEGER
def ctx_regs(ctx):
b = bytes(ctx.raw)
u = lambda o: struct.unpack_from('<Q', b, o)[0]
return dict(rip=u(0xF8), rsp=u(0x98), rcx=u(0x80), rdx=u(0x88), r8=u(0xB8), r9=u(0xC0),
rbx=u(0x90), rax=u(0x78))
k32.GetThreadContext.argtypes = [ctypes.c_void_p, ctypes.POINTER(CONTEXT)]
k32.GetThreadContext.restype = w.BOOL
k32.SuspendThread.argtypes = [ctypes.c_void_p]
k32.SuspendThread.restype = w.DWORD
k32.ResumeThread.argtypes = [ctypes.c_void_p]
k32.ResumeThread.restype = w.DWORD
k32.OpenThread.argtypes = [w.DWORD, w.BOOL, w.DWORD]
k32.OpenThread.restype = ctypes.c_void_p
k32.CloseHandle.argtypes = [ctypes.c_void_p]
k32.CloseHandle.restype = w.BOOL
k32.DuplicateHandle.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), w.DWORD, w.BOOL, w.DWORD]
k32.DuplicateHandle.restype = w.BOOL
k32.GetCurrentProcess.restype = ctypes.c_void_p
ntdll = ctypes.windll.ntdll
class UNICODE_STRING(ctypes.Structure):
_fields_ = [("Length", w.USHORT), ("MaximumLength", w.USHORT), ("Buffer", ctypes.c_void_p)]
class OBJECT_NAME_INFORMATION(ctypes.Structure):
_fields_ = [("Name", UNICODE_STRING)]
class OBJECT_TYPE_INFORMATION(ctypes.Structure):
_fields_ = [("Name", UNICODE_STRING), ("TotalNumberOfObjects", w.ULONG), ("TotalNumberOfHandles", w.ULONG),
("TotalPagedPoolUsage", w.ULONG), ("TotalNonPagedPoolUsage", w.ULONG),
("TotalNamePoolUsage", w.ULONG), ("TotalHandleTableUsage", w.ULONG),
("HighWaterNumberOfObjects", w.ULONG), ("HighWaterNumberOfHandles", w.ULONG),
("HighWaterPagedPoolUsage", w.ULONG), ("HighWaterNonPagedPoolUsage", w.ULONG),
("HighWaterNamePoolUsage", w.ULONG), ("HighWaterHandleTableUsage", w.ULONG),
("InvalidAttributes", w.ULONG), ("GenericMapping", w.BYTE * 16), ("ValidAccess", w.ULONG),
("SecurityRequired", w.BYTE), ("MaintainHandleCount", w.BYTE),
("MaintainTypeList", w.BYTE), ("Reserved", w.BYTE * 9)]
def obj_info(h):
# duplicate to get a handle valid in OUR process for querying
dup = ctypes.c_void_p()
if not k32.DuplicateHandle(hProc, ctypes.c_void_p(h), k32.GetCurrentProcess(),
ctypes.byref(dup), 0, False, 0x2): # DUPLICATE_SAME_ACCESS
return "dup-fail"
# type
buf = ctypes.create_string_buffer(512)
sz = w.ULONG(0)
r = ntdll.NtQueryObject(dup, 2, buf, 512, ctypes.byref(sz)) # ObjectTypeInformation=2
typ = "?"
if r == 0:
ti = OBJECT_TYPE_INFORMATION.from_buffer(buf)
if ti.Name.Buffer:
typ = ctypes.wstring_at(ti.Name.Buffer)
# name
buf2 = ctypes.create_string_buffer(1024)
sz2 = w.ULONG(0)
name = ""
r = ntdll.NtQueryObject(dup, 1, buf2, 1024, ctypes.byref(sz2)) # ObjectNameInformation=1
if r == 0:
oi = OBJECT_NAME_INFORMATION.from_buffer(buf2)
if oi.Name.Buffer:
try:
name = ctypes.wstring_at(oi.Name.Buffer)
except Exception:
name = "(name-err)"
k32.CloseHandle(dup)
return f"{typ}|{name}"
for tid in threads:
ht = k32.OpenThread(THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION | THREAD_SUSPEND_RESUME, False, tid)
if not ht:
print(f"tid {tid}: open fail {ctypes.get_last_error()}"); continue
k32.SuspendThread(ht)
ctx = CONTEXT()
struct.pack_into('<I', ctx.raw, 0x30, CONTEXT_FULL)
if k32.GetThreadContext(ht, ctypes.byref(ctx)):
r = ctx_regs(ctx)
rip = r['rip']; rcx = r['rcx']
line = f"tid {tid}: rip={resolve(rip)} rcx=0x{rcx:x} rdx=0x{r['rdx']:x} r8=0x{r['r8']:x} r9=0x{r['r9']:x} rsp=0x{r['rsp']:x}"
if rip and rcx:
try:
info = obj_info(rcx)
line += f" | wait_obj: {info}"
except Exception as e:
line += f" | obj_err {e}"
print(line)
else:
print(f"tid {tid}: GetThreadContext fail {ctypes.get_last_error()}")
k32.ResumeThread(ht)
k32.CloseHandle(ht)
@@ -8,6 +8,7 @@
#include <map> #include <map>
#include <mutex> #include <mutex>
#include <functional>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -72,6 +73,19 @@ public:
INativeInstrument* get(uint32_t channel); INativeInstrument* get(uint32_t channel);
// 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
// the old instance while dispatch is inside a method on it (use-after-free
// when loading a new VSTi while others play). Events for unassigned or
// reloading channels are dropped (a mid-reload instance must not be touched
// while its worker rebuilds state_ in place).
void noteOn(uint32_t channel, uint32_t pitch, float velocity);
void noteOff(uint32_t channel, uint32_t pitch);
void controlChange(uint32_t channel, uint32_t cc, uint32_t value);
void programChange(uint32_t channel, uint32_t program);
void pitchBend(uint32_t channel, uint32_t bend14);
// Remove and destroy the instrument on `channel` (its destructor may call // Remove and destroy the instrument on `channel` (its destructor may call
// VST terminate — MUST run on the channel worker thread, caller's duty). // VST terminate — MUST run on the channel worker thread, caller's duty).
// Used by Option B: two live instances of the same plugin DLL (Nexus) // Used by Option B: two live instances of the same plugin DLL (Nexus)
@@ -87,6 +101,16 @@ public:
// teardown never races the audio thread. // teardown never races the audio thread.
void setReloading(uint32_t channel, bool on); void setReloading(uint32_t channel, bool on);
// True while a channel is being rebuilt (instance teardown/reload on its
// worker thread): real-time code drops MIDI for it. Channel-level flag —
// survives the instance swap (the per-instance flag dies with the object).
bool isReloading(uint32_t channel) const;
// Editor-open predicate: while any VST editor is attached, channels whose
// plugin DLL path has an open editor count as quiet (same-plugin channels
// too -- the worker pumps the editor's window proc inside the DLL while the
// audio loop process()es other instances of the same DLL).
void setEditorOpenPredicate(std::function<bool(const std::string& lowerPath)> fn);
// Flush every sounding note on every assigned channel. // Flush every sounding note on every assigned channel.
void allNotesOff(); void allNotesOff();
@@ -103,6 +127,13 @@ private:
std::map<uint32_t, std::string> paths_; // last assigned path per channel std::map<uint32_t, std::string> paths_; // last assigned path per channel
// Per-instrument scratch so engines that overwrite (not mix) stay additive. // Per-instrument scratch so engines that overwrite (not mix) stay additive.
std::vector<float> scratchL_, scratchR_; std::vector<float> scratchL_, scratchR_;
// Channel-level reload flag (see isReloading) — indexed by MIDI channel.
bool reloadingCh_[16] = {};
// True when the channel's plugin DLL has an attached editor (or its reload
// is in progress): real-time code drops MIDI and skips rendering. Lowercased
// compare inside -- Windows plugin paths are case-insensitive.
bool channelQuiet(uint32_t ch) const;
std::function<bool(const std::string&)> editorOpenFn_;
}; };
#endif // NATIVE_INSTRUMENT_ENGINE_H #endif // NATIVE_INSTRUMENT_ENGINE_H
@@ -9,6 +9,8 @@
#define FS_SYNTH (static_cast<fluid_synth_t*>(synth)) #define FS_SYNTH (static_cast<fluid_synth_t*>(synth))
#define FS_SETTINGS (static_cast<fluid_settings_t*>(settings)) #define FS_SETTINGS (static_cast<fluid_settings_t*>(settings))
#include <algorithm>
#include <cctype>
#include <cstring> #include <cstring>
#include <iostream> #include <iostream>
@@ -187,6 +189,10 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
if (it != channels_.end()) oldInst = std::move(it->second); if (it != channels_.end()) oldInst = std::move(it->second);
channels_[channel] = std::move(inst); channels_[channel] = std::move(inst);
paths_[channel] = path; paths_[channel] = path;
// Fresh instance is by construction not reloading — clear the channel
// flag so real-time MIDI dispatch (which drops reloading channels)
// flows to it again.
reloadingCh_[channel] = false;
} }
oldInst.reset(); oldInst.reset();
return true; return true;
@@ -203,6 +209,7 @@ void InstrumentEngineManager::unload(uint32_t channel) {
if (it != channels_.end()) oldInst = std::move(it->second); if (it != channels_.end()) oldInst = std::move(it->second);
channels_.erase(channel); channels_.erase(channel);
paths_.erase(channel); paths_.erase(channel);
reloadingCh_[channel] = false;
} }
oldInst.reset(); oldInst.reset();
} }
@@ -221,13 +228,86 @@ INativeInstrument* InstrumentEngineManager::get(uint32_t channel) {
void InstrumentEngineManager::setReloading(uint32_t channel, bool on) { void InstrumentEngineManager::setReloading(uint32_t channel, bool on) {
std::lock_guard<std::mutex> lock(mu_); std::lock_guard<std::mutex> lock(mu_);
if (channel < 16) reloadingCh_[channel] = on;
auto it = channels_.find(channel); auto it = channels_.find(channel);
if (it != channels_.end()) it->second->setReloading(on); if (it != channels_.end()) it->second->setReloading(on);
} }
bool InstrumentEngineManager::isReloading(uint32_t channel) const {
std::lock_guard<std::mutex> lock(mu_);
return channel < 16 && reloadingCh_[channel];
}
void InstrumentEngineManager::setEditorOpenPredicate(std::function<bool(const std::string&)> fn) {
std::lock_guard<std::mutex> lock(mu_);
editorOpenFn_ = std::move(fn);
}
// Quiet = reloading OR an editor attached for the channel's plugin DLL path.
// Called with mu_ held by the real-time dispatch (the predicate locks
// g_editorMutex -- lock order mu_ -> g_editorMutex; g_editorMutex is never
// held while taking mu_).
bool InstrumentEngineManager::channelQuiet(uint32_t ch) const {
if (ch >= 16 || reloadingCh_[ch]) return true;
if (!editorOpenFn_) return false;
auto it = paths_.find(ch);
if (it == paths_.end() || it->second.empty()) return false;
std::string lp = it->second;
std::transform(lp.begin(), lp.end(), lp.begin(),
[](unsigned char c) { return (char)::tolower(c); });
return editorOpenFn_(lp);
}
// Real-time MIDI dispatch: hold mu_ for the WHOLE call so assign()/unload()
// (map swap + old-instance destruction outside the lock) and reload()/reloadForGUI()
// (state_ deleted in place on the worker) can never destroy/free the instance
// while dispatch is inside a method on it — use-after-free when loading a new
// VSTi while other channels keep playing. Reloading channels are skipped: their
// instance is mid-teardown and must not be touched.
void InstrumentEngineManager::noteOn(uint32_t channel, uint32_t pitch, float velocity) {
std::lock_guard<std::mutex> lock(mu_);
if (channelQuiet(channel)) return;
auto it = channels_.find(channel);
if (it == channels_.end()) return;
it->second->noteOn(channel, pitch, velocity, 0);
}
void InstrumentEngineManager::noteOff(uint32_t channel, uint32_t pitch) {
std::lock_guard<std::mutex> lock(mu_);
if (channelQuiet(channel)) return;
auto it = channels_.find(channel);
if (it == channels_.end()) return;
it->second->noteOff(channel, pitch, 0);
}
void InstrumentEngineManager::controlChange(uint32_t channel, uint32_t cc, uint32_t value) {
std::lock_guard<std::mutex> lock(mu_);
if (channelQuiet(channel)) return;
auto it = channels_.find(channel);
if (it == channels_.end()) return;
it->second->controlChange(channel, cc, value);
}
void InstrumentEngineManager::programChange(uint32_t channel, uint32_t program) {
std::lock_guard<std::mutex> lock(mu_);
if (channelQuiet(channel)) return;
auto it = channels_.find(channel);
if (it == channels_.end()) return;
it->second->programChange(channel, program);
}
void InstrumentEngineManager::pitchBend(uint32_t channel, uint32_t bend14) {
std::lock_guard<std::mutex> lock(mu_);
if (channelQuiet(channel)) return;
auto it = channels_.find(channel);
if (it == channels_.end()) return;
it->second->pitchBend(channel, bend14);
}
void InstrumentEngineManager::allNotesOff() { void InstrumentEngineManager::allNotesOff() {
std::lock_guard<std::mutex> lock(mu_); std::lock_guard<std::mutex> lock(mu_);
for (auto& [ch, inst] : channels_) { for (auto& [ch, inst] : channels_) {
if (channelQuiet(ch)) continue; // mid-rebuild / editor open: do not touch the instance
for (uint32_t n = 0; n < 128; ++n) inst->noteOff(ch, n, 0); for (uint32_t n = 0; n < 128; ++n) inst->noteOff(ch, n, 0);
} }
} }
@@ -242,6 +322,7 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t
scratchR_.resize(numSamples); scratchR_.resize(numSamples);
} }
for (auto& [ch, inst] : channels_) { for (auto& [ch, inst] : channels_) {
if (channelQuiet(ch)) continue; // editor open on this plugin DLL: do not process
std::memset(scratchL_.data(), 0, numSamples * sizeof(float)); std::memset(scratchL_.data(), 0, numSamples * sizeof(float));
std::memset(scratchR_.data(), 0, numSamples * sizeof(float)); std::memset(scratchR_.data(), 0, numSamples * sizeof(float));
inst->processAudioBlock(scratchL_.data(), scratchR_.data(), numSamples); inst->processAudioBlock(scratchL_.data(), scratchR_.data(), numSamples);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff