Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1964544d2d |
@@ -86,12 +86,14 @@
|
||||
* 1. LOAD NEW INSTRUMENT INTO BRIDGE (VST3 / VST2 / SF2 / SF3 / SFZ)
|
||||
* instrumentType: 'VST3' | 'VST2' | 'SF2' | 'SF3' | 'SFZ'
|
||||
* 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) {
|
||||
this.activeInstrumentType = instrumentType;
|
||||
console.log('[BridgeService] Loading ' + instrumentType + ' asset: ' + filePath + ' ch=' + channel);
|
||||
if (!this._tauri()) return false;
|
||||
try {
|
||||
await window.__TAURI__.core.invoke('transport_control', { kind: 'panic' });
|
||||
await window.__TAURI__.core.invoke('load_native_instrument', {
|
||||
path: filePath,
|
||||
instrumentType: INSTRUMENT_TYPE[instrumentType] !== undefined ? INSTRUMENT_TYPE[instrumentType] : 0,
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<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/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/unifiedMidiRouter.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/promptTemplateManager.js?v=202607281039"></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">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 <mutex>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -72,6 +73,19 @@ public:
|
||||
|
||||
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
|
||||
// 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)
|
||||
@@ -87,6 +101,16 @@ public:
|
||||
// teardown never races the audio thread.
|
||||
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.
|
||||
void allNotesOff();
|
||||
|
||||
@@ -103,6 +127,13 @@ private:
|
||||
std::map<uint32_t, std::string> paths_; // last assigned path per channel
|
||||
// Per-instrument scratch so engines that overwrite (not mix) stay additive.
|
||||
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
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#define FS_SYNTH (static_cast<fluid_synth_t*>(synth))
|
||||
#define FS_SETTINGS (static_cast<fluid_settings_t*>(settings))
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
@@ -187,6 +189,10 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
|
||||
if (it != channels_.end()) oldInst = std::move(it->second);
|
||||
channels_[channel] = std::move(inst);
|
||||
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();
|
||||
return true;
|
||||
@@ -203,6 +209,7 @@ void InstrumentEngineManager::unload(uint32_t channel) {
|
||||
if (it != channels_.end()) oldInst = std::move(it->second);
|
||||
channels_.erase(channel);
|
||||
paths_.erase(channel);
|
||||
reloadingCh_[channel] = false;
|
||||
}
|
||||
oldInst.reset();
|
||||
}
|
||||
@@ -221,13 +228,86 @@ INativeInstrument* InstrumentEngineManager::get(uint32_t channel) {
|
||||
|
||||
void InstrumentEngineManager::setReloading(uint32_t channel, bool on) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (channel < 16) reloadingCh_[channel] = on;
|
||||
auto it = channels_.find(channel);
|
||||
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() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -242,6 +322,7 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t
|
||||
scratchR_.resize(numSamples);
|
||||
}
|
||||
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(scratchR_.data(), 0, numSamples * sizeof(float));
|
||||
inst->processAudioBlock(scratchL_.data(), scratchR_.data(), numSamples);
|
||||
|
||||
+1287
-1
File diff suppressed because it is too large
Load Diff
+770
-126
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user