# 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('