chore: commit G3.2+G3.3 deploy binaries + debug scripts; ignore debug run artifacts
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
# 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"
|
||||
CDB = r"C:\Users\locpham\AppData\Local\Microsoft\WindowsApps\cdbX64.exe"
|
||||
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "driver_cdb.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(
|
||||
[CDB, "-g", "-G", "-o", "-c", ".ecxr; .dump /ma C:\\Users\\locpham\\AppData\\Local\\Temp\\procdump\\dumps\\cdb_crash.dmp; !analyze -v; k L40; q", 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 (cdb)")
|
||||
@@ -0,0 +1,175 @@
|
||||
# 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\src-tauri\binaries\daw_vst_bridge.exe"
|
||||
CDB = r"C:\Users\locpham\AppData\Local\Microsoft\WindowsApps\cdbX64.exe"
|
||||
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "driver_cdb_old.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(
|
||||
[CDB, "-g", "-G", "-o", "-c", ".ecxr; .dump /ma C:\\Users\\locpham\\AppData\\Local\\Temp\\procdump\\dumps\\cdb_crash.dmp; !analyze -v; k L40; q", 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 (cdb)")
|
||||
@@ -0,0 +1,176 @@
|
||||
# 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"
|
||||
PROCDUMP = r"C:\Users\locpham\AppData\Local\Temp\procdump\procdump64.exe"
|
||||
DUMPDIR = r"C:\Users\locpham\AppData\Local\Temp\procdump\dumps"
|
||||
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "driver_procdump.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(
|
||||
[PROCDUMP, "-accepteula", "-x", DUMPDIR, BRIDGE, "--shm", SHM_NAME], # second chance = real crash (first-chance caught a benign Nexus json throw)
|
||||
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 (procdump)")
|
||||
@@ -0,0 +1,47 @@
|
||||
p = r"C:/Users/locpham/SonicForgeStudio/native_bridge/src/Vst3Instrument.cpp"
|
||||
data = open(p, "rb").read()
|
||||
n = b"\r\n"
|
||||
|
||||
old1 = n.join([
|
||||
b" tresult queryInterface(const char* iid, void** v) override {",
|
||||
b" if (std::memcmp(iid, Steinberg::Vst::IComponentHandler::iid, 16) == 0 ||",
|
||||
b" std::memcmp(iid, Steinberg::FUnknown::iid, 16) == 0) {",
|
||||
b" *v = static_cast<Steinberg::Vst::IComponentHandler*>(this);",
|
||||
b" addRef();",
|
||||
b" return kResultOk;",
|
||||
b" }",
|
||||
b" *v = nullptr;",
|
||||
b" return kNoInterface;",
|
||||
b" }",
|
||||
])
|
||||
new1 = n.join([
|
||||
b" tresult queryInterface(const char*, void** v) override {",
|
||||
b" *v = nullptr;",
|
||||
b" return kNoInterface;",
|
||||
b" }",
|
||||
])
|
||||
|
||||
old2 = n.join([
|
||||
b" tresult queryInterface(const char* iid, void** v) override {",
|
||||
b" if (std::memcmp(iid, Steinberg::IPlugFrame::iid, 16) == 0 ||",
|
||||
b" std::memcmp(iid, Steinberg::FUnknown::iid, 16) == 0) {",
|
||||
b" *v = static_cast<Steinberg::IPlugFrame*>(this);",
|
||||
b" addRef();",
|
||||
b" return kResultOk;",
|
||||
b" }",
|
||||
b" *v = nullptr;",
|
||||
b" return kNoInterface;",
|
||||
b" }",
|
||||
])
|
||||
new2 = n.join([
|
||||
b" tresult queryInterface(const char*, void** v) override {",
|
||||
b" *v = nullptr;",
|
||||
b" return kNoInterface;",
|
||||
b" }",
|
||||
])
|
||||
|
||||
c1 = data.count(old1)
|
||||
c2 = data.count(old2)
|
||||
data = data.replace(old1, new1).replace(old2, new2)
|
||||
open(p, "wb").write(data)
|
||||
print(f"edit1={c1} edit2={c2}")
|
||||
@@ -0,0 +1,194 @@
|
||||
import subprocess
|
||||
|
||||
def load(p):
|
||||
return open(p, encoding='utf-8', newline='').read()
|
||||
|
||||
def save(p, s):
|
||||
open(p, 'w', encoding='utf-8', newline='').write(s)
|
||||
|
||||
def rep(s, needle, repl, tag):
|
||||
n = s.count(needle)
|
||||
if n != 1:
|
||||
raise SystemExit("FAIL %s: needle count=%d" % (tag, n))
|
||||
return s.replace(needle, repl)
|
||||
|
||||
cr = '\r\n'
|
||||
|
||||
# ---- main.cpp ----
|
||||
mp = load('native_bridge/src/main.cpp')
|
||||
|
||||
mp = rep(mp,
|
||||
"int main(int argc, char* argv[]) {" + cr + " std::cout << \"[NativeBridge] Starting DAW Host Bridge Engine...\" << std::endl;" + cr + "#ifdef _WIN32" + cr + " CoInitializeEx(nullptr, COINIT_MULTITHREADED);" + cr + cr + " // Disable Power Throttling (Efficiency Mode) for this background process",
|
||||
"int main(int argc, char* argv[]) {" + cr + " std::cout << \"[NativeBridge] Starting DAW Host Bridge Engine...\" << std::endl;" + cr + "#ifdef _WIN32" + cr + " // Disable Power Throttling (Efficiency Mode) for this background process",
|
||||
"coinit")
|
||||
|
||||
mp = rep(mp,
|
||||
" SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &powerState, sizeof(powerState));" + cr + cr + " // Elevate main audio processing thread to time-critical priority" + cr + " SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);",
|
||||
" SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &powerState, sizeof(powerState));",
|
||||
"timecritical")
|
||||
|
||||
mp = rep(mp,
|
||||
" instruments.setEditorOpenPredicate([](const std::string& /*lp*/) {" + cr + " return false;" + cr + " });",
|
||||
" instruments.setEditorOpenPredicate([](const std::string& lp) {" + cr + " std::lock_guard<std::mutex> lk(g_editorMutex);" + cr + " auto it = g_editorPathCount.find(lp);" + cr + " return it != g_editorPathCount.end() && it->second > 0;" + cr + " });",
|
||||
"predicate")
|
||||
|
||||
blob = subprocess.check_output(['git', 'show', '0cec3d3:native_bridge/src/main.cpp']).decode('utf-8')
|
||||
if '\r\n' not in blob and '\n' in blob:
|
||||
blob = blob.replace('\n', '\r\n')
|
||||
|
||||
def fn_start(s):
|
||||
i = s.find('static void post_close_gui(uint32_t ch, HWND hwnd) {' + cr + ' ChannelWorker* w = nullptr;')
|
||||
if i < 0:
|
||||
i = s.find('static void post_close_gui(uint32_t ch, HWND hwnd) {\n ChannelWorker* w = nullptr;')
|
||||
if i < 0:
|
||||
raise SystemExit("fn_start not found")
|
||||
return i
|
||||
|
||||
cur_start = fn_start(mp)
|
||||
cur_end = mp.find('int main(int argc, char* argv[]) {', cur_start)
|
||||
old_start = fn_start(blob)
|
||||
old_end = blob.find('int main(int argc, char* argv[]) {', old_start)
|
||||
mp = rep(mp, mp[cur_start:cur_end], blob[old_start:old_end], "post_close_gui")
|
||||
|
||||
needle5 = (
|
||||
" workers[guiCh]->post([&instruments, guiCh, arg1, arg2 = pluginId]() {" + cr +
|
||||
" if (!instruments.get(guiCh)) {" + cr +
|
||||
" std::cerr << \"[NativeBridge] GUI attach FAILED hwnd=\" << (void*)arg1" + cr +
|
||||
" << \" plugin=\" << arg2 << \" ch=\" << guiCh << \" (no instrument loaded)\" << std::endl;" + cr +
|
||||
" return;" + cr +
|
||||
" }" + cr +
|
||||
" if (auto* inst0 = instruments.get(guiCh)) {" + cr +
|
||||
" if (inst0->hasAttachedView()) {" + cr +
|
||||
" return;" + cr +
|
||||
" }" + cr +
|
||||
" }" + cr +
|
||||
" void* hwnd = (void*)arg1;" + cr +
|
||||
"#ifdef _WIN32" + cr +
|
||||
" HWND nativeHwnd = nullptr;" + cr +
|
||||
" {" + cr +
|
||||
" std::lock_guard<std::mutex> lock(g_guiMutex);" + cr +
|
||||
" auto it = g_guiWindows.find(guiCh);" + cr +
|
||||
" if (it != g_guiWindows.end()) nativeHwnd = (HWND)it->second;" + cr +
|
||||
" }" + cr +
|
||||
" if (hwnd == 0) {" + cr +
|
||||
" if (nativeHwnd && IsWindow(nativeHwnd)) {" + cr +
|
||||
" hwnd = (void*)nativeHwnd;" + cr +
|
||||
" SetWindowTextA(nativeHwnd, arg2.c_str());" + cr +
|
||||
" ShowWindow(nativeHwnd, SW_SHOW);" + cr +
|
||||
" SetForegroundWindow(nativeHwnd);" + cr +
|
||||
" // Reuse: cap nhat USERDATA (channel+1) - inst CU da bi thay" + cr +
|
||||
" // the boi assign() -> WM_DESTROY sau nay lookup inst MOI." + cr +
|
||||
" SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));" + cr +
|
||||
" } else {" + cr +
|
||||
" nativeHwnd = (HWND)create_native_vst_window(arg2.c_str());" + cr +
|
||||
" if (!nativeHwnd) {" + cr +
|
||||
" std::cerr << \"[NativeBridge] GUI create window FAILED plugin=\" << arg2 << std::endl;" + cr +
|
||||
" return;" + cr +
|
||||
" }" + cr +
|
||||
" {" + cr +
|
||||
" std::lock_guard<std::mutex> lock(g_guiMutex);" + cr +
|
||||
" g_guiWindows[guiCh] = nativeHwnd; // keep window alive" + cr +
|
||||
" g_hwndToCh[nativeHwnd] = guiCh; // WM_DESTROY cleanup" + cr +
|
||||
" }" + cr +
|
||||
" SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));" + cr +
|
||||
" hwnd = (void*)nativeHwnd;" + cr +
|
||||
" }" + cr +
|
||||
" }" + cr +
|
||||
"#endif" + cr +
|
||||
" if (auto* inst0 = instruments.get(guiCh)) {" + cr +
|
||||
" if (inst0->hasAttachedView()) {" + cr +
|
||||
" return;" + cr +
|
||||
" }" + cr +
|
||||
" }" + cr +
|
||||
"#ifdef _WIN32" + cr +
|
||||
" // Option B: chi 1 editor VST mo tai 1 thoi diem toan"
|
||||
)
|
||||
repl5 = (
|
||||
" void* hwnd = (void*)arg1;" + cr +
|
||||
"#ifdef _WIN32" + cr +
|
||||
" // Window PHAI thuoc MAIN thread (audio loop pump nay dispatch" + cr +
|
||||
" // messages cua no moi vong lap). Tao/cap nhat window ngay tai day." + cr +
|
||||
" HWND nativeHwnd = nullptr;" + cr +
|
||||
" {" + cr +
|
||||
" std::lock_guard<std::mutex> lock(g_guiMutex);" + cr +
|
||||
" auto it = g_guiWindows.find(guiCh);" + cr +
|
||||
" if (it != g_guiWindows.end()) nativeHwnd = (HWND)it->second;" + cr +
|
||||
" }" + cr +
|
||||
" if (hwnd == 0) {" + cr +
|
||||
" if (nativeHwnd && IsWindow(nativeHwnd)) {" + cr +
|
||||
" hwnd = (void*)nativeHwnd;" + cr +
|
||||
" SetWindowTextA(nativeHwnd, pluginId.c_str());" + cr +
|
||||
" ShowWindow(nativeHwnd, SW_SHOW);" + cr +
|
||||
" SetForegroundWindow(nativeHwnd);" + cr +
|
||||
" // Reuse: cap nhat USERDATA (channel+1) - inst CU da bi thay" + cr +
|
||||
" // the boi assign() -> WM_DESTROY sau nay lookup inst MOI." + cr +
|
||||
" SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));" + cr +
|
||||
" } else {" + cr +
|
||||
" nativeHwnd = (HWND)create_native_vst_window(pluginId.c_str());" + cr +
|
||||
" if (!nativeHwnd) {" + cr +
|
||||
" std::cerr << \"[NativeBridge] GUI create window FAILED plugin=\" << pluginId << std::endl;" + cr +
|
||||
" return;" + cr +
|
||||
" }" + cr +
|
||||
" {" + cr +
|
||||
" std::lock_guard<std::mutex> lock(g_guiMutex);" + cr +
|
||||
" g_guiWindows[guiCh] = nativeHwnd; // keep window alive" + cr +
|
||||
" g_hwndToCh[nativeHwnd] = guiCh; // WM_DESTROY cleanup" + cr +
|
||||
" }" + cr +
|
||||
" SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));" + cr +
|
||||
" hwnd = (void*)nativeHwnd;" + cr +
|
||||
" }" + cr +
|
||||
" }" + cr +
|
||||
"#endif" + cr +
|
||||
" workers[guiCh]->post([&instruments, guiCh, hwnd, arg2 = pluginId]() {" + cr +
|
||||
" if (!instruments.get(guiCh)) {" + cr +
|
||||
" std::cerr << \"[NativeBridge] GUI attach FAILED hwnd=\" << hwnd" + cr +
|
||||
" << \" plugin=\" << arg2 << \" ch=\" << guiCh << \" (no instrument loaded)\" << std::endl;" + cr +
|
||||
" return;" + cr +
|
||||
" }" + cr +
|
||||
" if (auto* inst0 = instruments.get(guiCh)) {" + cr +
|
||||
" if (inst0->hasAttachedView()) {" + cr +
|
||||
" return;" + cr +
|
||||
" }" + cr +
|
||||
" }" + cr +
|
||||
"#ifdef _WIN32" + cr +
|
||||
" // Option B: chi 1 editor VST mo tai 1 thoi diem toan"
|
||||
)
|
||||
mp = rep(mp, needle5, repl5, "handleOpenGui-window")
|
||||
|
||||
mp = rep(mp,
|
||||
" try {" + cr + " instruments.setBypassed(true);" + cr + " sleep_ms(10);" + cr + " ok = inst->reloadForGUI();" + cr + " instruments.setBypassed(false);" + cr + " // IME recursion fix: strip the editor's IMC before",
|
||||
" try {" + cr + " ok = inst->reloadForGUI();" + cr + " // IME recursion fix: strip the editor's IMC before",
|
||||
"bypass-open1")
|
||||
|
||||
mp = rep(mp,
|
||||
" } catch (...) {" + cr + " instruments.setBypassed(false);" + cr + " std::cerr << \"[NativeBridge] GUI attach EXCEPTION ch=\" << guiCh",
|
||||
" } catch (...) {" + cr + " std::cerr << \"[NativeBridge] GUI attach EXCEPTION ch=\" << guiCh",
|
||||
"bypass-open2")
|
||||
|
||||
mp = rep(mp,
|
||||
' << " (channel active with editor open)" << std::endl;',
|
||||
' << " (channel muted while editor open)" << std::endl;',
|
||||
"logmsg")
|
||||
|
||||
mp = rep(mp,
|
||||
" if (ch != UINT32_MAX && g_engine) {" + cr + " auto* inst = g_engine->get(ch);" + cr + " if (inst) {" + cr + " inst->resizeView(w, h);" + cr + " }" + cr + " }" + cr + " return 0;",
|
||||
" if (ch != UINT32_MAX && g_workers) {" + cr + " auto wit = g_workers->find(ch);" + cr + " if (wit != g_workers->end()) {" + cr + " // onSize() must run on the channel worker (the view was attached" + cr + " // in its COM STA apartment), not on the main pump thread." + cr + " wit->second->post([ch, w, h]() {" + cr + " if (!g_engine) return;" + cr + " auto* i = g_engine->get(ch);" + cr + " if (i) i->resizeView(w, h);" + cr + " });" + cr + " }" + cr + " }" + cr + " return 0;",
|
||||
"wmsize")
|
||||
|
||||
save('native_bridge/src/main.cpp', mp)
|
||||
|
||||
# ---- Vst3Instrument.cpp ----
|
||||
vp = load('native_bridge/src/Vst3Instrument.cpp')
|
||||
|
||||
vp = rep(vp,
|
||||
"bool Vst3Instrument::reloadForGUI() {" + cr + "#ifndef HAVE_VST3SDK" + cr + " return false;" + cr + "#else" + cr + " auto* s = static_cast<Vst3HostState*>(state_);" + cr + " if (!s || !s->controller) return false;" + cr + " return true;" + cr + "#endif" + cr + "}",
|
||||
"bool Vst3Instrument::reloadForGUI() {" + cr + "#ifndef HAVE_VST3SDK" + cr + " return false;" + cr + "#else" + cr + " auto* s = static_cast<Vst3HostState*>(state_);" + cr + " if (!s || !s->controller) return false;" + cr + " if (s->view && guiAttached_) return true;" + cr + " if (hasAttachedOnce_ && !guiAttached_) {" + cr + " // Reopen GUI sau khi dong: mot so plugin (Nexus...) hang o" + cr + " // view->attached() LAN 2 tren cung component instance. Tao lai inst" + cr + " // moi tren CUNG worker thread (COM STA con song) roi attach lai." + cr + " // PHAN NAY PHAI chay tren worker thread — apartment cua channel song" + cr + " // o day; chay tren thread tam thi apartment moi chet ngay sau do" + cr + " // (reopen lan sau -> treo nhu bridge_like)." + cr + " if (!reload()) return false;" + cr + " s = static_cast<Vst3HostState*>(state_);" + cr + " if (!s || !s->controller) return false;" + cr + " }" + cr + " return true;" + cr + "#endif" + cr + "}",
|
||||
"reloadForGUI")
|
||||
|
||||
vp = rep(vp,
|
||||
"void Vst3Instrument::closeGUI() {" + cr + "#ifndef HAVE_VST3SDK" + cr + " return;" + cr + "#else" + cr + " auto* s = static_cast<Vst3HostState*>(state_);" + cr + " if (s && s->view) {" + cr + " if (guiAttached_) {" + cr + " try {" + cr + " s->view->removed();" + cr + " } catch (...) {" + cr + " std::cerr << \"[Vst3Instrument] Exception inside view->removed()\" << std::endl;" + cr + " }" + cr + " }" + cr + " s->view = nullptr;" + cr + " }" + cr + " guiAttached_ = false;" + cr + "#endif" + cr + "}",
|
||||
"void Vst3Instrument::closeGUI() {" + cr + "#ifndef HAVE_VST3SDK" + cr + " return;" + cr + "#else" + cr + " auto* s = static_cast<Vst3HostState*>(state_);" + cr + " // Do NOT call s->view->removed(): Nexus's removed() enters an internal" + cr + " // modal wait for a message only this thread's message pump can dispatch" + cr + " // — but this thread is inside removed(), so it deadlocks forever" + cr + " // (observed: closeGUI job never returns; wdisp flood from the editor)." + cr + " // Nulling the view and letting the next terminate() (reload / destructor)" + cr + " // tear the editor windows down on this same thread is the deterministic" + cr + " // path out. Callers (post_close_gui / load job) additionally destroy the" + cr + " // editor's child windows right here on this thread so the plugin stops" + cr + " // pumping; the leaked editor object is inert once its windows are gone." + cr + " if (s) s->view = nullptr;" + cr + " guiAttached_ = false;" + cr + "#endif" + cr + "}",
|
||||
"closeGUI")
|
||||
|
||||
save('native_bridge/src/Vst3Instrument.cpp', vp)
|
||||
print("ALL EDITS OK")
|
||||
@@ -0,0 +1,50 @@
|
||||
import subprocess
|
||||
|
||||
def load(p):
|
||||
return open(p, encoding='utf-8', newline='').read()
|
||||
|
||||
def save(p, s):
|
||||
open(p, 'w', encoding='utf-8', newline='').write(s)
|
||||
|
||||
def rep(s, needle, repl, tag):
|
||||
n = s.count(needle)
|
||||
if n != 1:
|
||||
raise SystemExit("FAIL %s: needle count=%d" % (tag, n))
|
||||
return s.replace(needle, repl)
|
||||
|
||||
cr = '\r\n'
|
||||
mp = load('native_bridge/src/main.cpp')
|
||||
|
||||
# 1. forward-declare helper near post_close_gui fwd decl
|
||||
mp = rep(mp,
|
||||
"static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker",
|
||||
"static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker" + cr + "static void post_resize_view(uint32_t ch, int w, int h); // defined after ChannelWorker",
|
||||
"fwd")
|
||||
|
||||
# 2. define helper after ChannelWorker class, before same-plugin helpers
|
||||
anchor = "// --- same-plugin-DLL silence helpers"
|
||||
helper = (
|
||||
"// resizeView() must run on the channel worker (the view was attached in its" + cr +
|
||||
"// COM STA apartment), never on the main pump thread." + cr +
|
||||
"static void post_resize_view(uint32_t ch, int w, int h) {" + cr +
|
||||
" if (g_workers) {" + cr +
|
||||
" auto wit = g_workers->find(ch);" + cr +
|
||||
" if (wit != g_workers->end()) {" + cr +
|
||||
" wit->second->post([ch, w, h]() {" + cr +
|
||||
" if (!g_engine) return;" + cr +
|
||||
" auto* i = g_engine->get(ch);" + cr +
|
||||
" if (i) i->resizeView(w, h);" + cr +
|
||||
" });" + cr +
|
||||
" }" + cr +
|
||||
" }" + cr +
|
||||
"}" + cr + cr + anchor)
|
||||
mp = rep(mp, anchor, helper, "helper")
|
||||
|
||||
# 3. WM_SIZE: call helper instead of inline g_workers block
|
||||
mp = rep(mp,
|
||||
" if (ch != UINT32_MAX && g_workers) {" + cr + " auto wit = g_workers->find(ch);" + cr + " if (wit != g_workers->end()) {" + cr + " // onSize() must run on the channel worker (the view was attached" + cr + " // in its COM STA apartment), not on the main pump thread." + cr + " wit->second->post([ch, w, h]() {" + cr + " if (!g_engine) return;" + cr + " auto* i = g_engine->get(ch);" + cr + " if (i) i->resizeView(w, h);" + cr + " });" + cr + " }" + cr + " }" + cr + " return 0;",
|
||||
" if (ch != UINT32_MAX) {" + cr + " post_resize_view(ch, w, h);" + cr + " }" + cr + " return 0;",
|
||||
"wmsize")
|
||||
|
||||
save('native_bridge/src/main.cpp', mp)
|
||||
print("OK")
|
||||
@@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply owner-thread-destroy + pump-gate crash fix to main.cpp (LF file)."""
|
||||
import sys, io
|
||||
|
||||
p = r"C:/Users/locpham/SonicForgeStudio/native_bridge/src/main.cpp"
|
||||
s = open(p, "rb").read().decode("utf-8")
|
||||
orig = s
|
||||
|
||||
def rep(old, new, tag):
|
||||
global s
|
||||
n = s.count(old)
|
||||
assert n == 1, f"[{tag}] count={n}"
|
||||
s = s.replace(old, new, 1)
|
||||
print(f"ok: {tag}")
|
||||
|
||||
# 1. globals
|
||||
rep(
|
||||
"""static std::map<uint32_t, std::unique_ptr<ChannelWorker>>* g_workers = nullptr;""",
|
||||
"""static std::map<uint32_t, std::unique_ptr<ChannelWorker>>* g_workers = nullptr;
|
||||
static std::mutex g_tidMutex;
|
||||
static std::map<DWORD, ChannelWorker*> g_tidToWorker; // worker tid -> worker (owner-thread destroy)""",
|
||||
"globals")
|
||||
|
||||
# 2. fwd decl
|
||||
rep(
|
||||
"""static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker""",
|
||||
"""static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker
|
||||
static bool is_teardown_window(HWND hwnd); // fwd — defined after ChannelWorker""",
|
||||
"fwd")
|
||||
|
||||
# 3. constructor register tid
|
||||
rep(
|
||||
""" th_ = std::thread([this] {
|
||||
#ifdef _WIN32
|
||||
OleInitialize(nullptr);""",
|
||||
""" th_ = std::thread([this] {
|
||||
#ifdef _WIN32
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_tidMutex);
|
||||
g_tidToWorker[GetCurrentThreadId()] = this;
|
||||
}
|
||||
OleInitialize(nullptr);""",
|
||||
"register tid")
|
||||
|
||||
# 4. pump gate
|
||||
rep(
|
||||
""" for (int pumped = 0; pumped < 16; ++pumped) {
|
||||
if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break;
|
||||
TranslateMessage(&msg);""",
|
||||
""" for (int pumped = 0; pumped < 16; ++pumped) {
|
||||
if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break;
|
||||
// CRASH FIX (0xc000041d): JUCE editor child windows of a
|
||||
// plugin DLL are owned by the FIRST worker that ran the
|
||||
// DLL's global JUCE MessageManager - not by the channel
|
||||
// worker doing the teardown. While another worker's LOAD /
|
||||
// close job tears an instance down (reloading / close in
|
||||
// flight), dispatching a message (e.g. WM_PAINT) into the
|
||||
// plugin wndproc reads instance state being destroyed ->
|
||||
// AV in USER32 (observed: crash on the owner worker's
|
||||
// pump, right after the teardown job closed the view).
|
||||
// Drop the message instead (PeekMessageW already removed
|
||||
// it): the editor may glitch, the bridge survives.
|
||||
if (is_teardown_window(msg.hwnd)) continue;
|
||||
TranslateMessage(&msg);""",
|
||||
"pump gate")
|
||||
|
||||
# 5. unregister tid
|
||||
rep(
|
||||
"""#ifdef _WIN32
|
||||
OleUninitialize();
|
||||
#endif
|
||||
});""",
|
||||
"""#ifdef _WIN32
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_tidMutex);
|
||||
g_tidToWorker.erase(GetCurrentThreadId());
|
||||
}
|
||||
OleUninitialize();
|
||||
#endif
|
||||
});""",
|
||||
"unregister tid")
|
||||
|
||||
# 6. post_and_wait
|
||||
rep(
|
||||
""" void post(std::function<void()> job) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
jobs_.push_back(std::move(job));
|
||||
}
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
private:""",
|
||||
""" void post(std::function<void()> job) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
jobs_.push_back(std::move(job));
|
||||
}
|
||||
cv_.notify_all();
|
||||
}
|
||||
// Run job on THIS worker and wait (5s cap) until it finished. Used to
|
||||
// serialize a window destroy with the owner worker's pump. MUST NOT be
|
||||
// called from a job running on this same worker (deadlock) - callers
|
||||
// destroy directly when owner tid == current tid.
|
||||
bool post_and_wait(std::function<void()> job) {
|
||||
std::mutex doneMx;
|
||||
std::condition_variable doneCv;
|
||||
bool done = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
jobs_.push_back([&]() {
|
||||
job();
|
||||
{
|
||||
std::lock_guard<std::mutex> dk(doneMx);
|
||||
done = true;
|
||||
}
|
||||
doneCv.notify_all();
|
||||
});
|
||||
}
|
||||
cv_.notify_all();
|
||||
std::unique_lock<std::mutex> dk(doneMx);
|
||||
return doneCv.wait_for(dk, std::chrono::seconds(5), [&] { return done; });
|
||||
}
|
||||
|
||||
private:""",
|
||||
"post_and_wait")
|
||||
|
||||
# 7. helpers after close_in_flight
|
||||
rep(
|
||||
"""static bool close_in_flight(uint32_t ch) {
|
||||
std::lock_guard<std::mutex> lk(g_attachMutex);
|
||||
return g_closeInFlight[ch];
|
||||
}""",
|
||||
"""static bool close_in_flight(uint32_t ch) {
|
||||
std::lock_guard<std::mutex> lk(g_attachMutex);
|
||||
return g_closeInFlight[ch];
|
||||
}
|
||||
|
||||
// True if hwnd (or any ancestor) is a plugin editor window whose channel is
|
||||
// mid-teardown (reloading or close job in flight). Editor child windows
|
||||
// (JUCE) have the native VST window as parent; the native window is in
|
||||
// g_hwndToCh. JUCE's global message window has no such parent - walk stops
|
||||
// -> false (never dropped).
|
||||
static bool is_teardown_window(HWND hwnd) {
|
||||
for (HWND h = hwnd; h; h = GetParent(h)) {
|
||||
uint32_t ch = UINT32_MAX;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_guiMutex);
|
||||
auto it = g_hwndToCh.find(h);
|
||||
if (it != g_hwndToCh.end()) ch = it->second;
|
||||
}
|
||||
if (ch != UINT32_MAX) {
|
||||
if (close_in_flight(ch)) return true;
|
||||
return g_engine ? g_engine->isReloading(ch) : false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// CRASH FIX (0xc000041d): plugin editor child windows are owned by the
|
||||
// thread that first ran the plugin's JUCE MessageManager (a single worker),
|
||||
// NOT by the channel worker doing the teardown. Cross-thread DestroyWindow
|
||||
// races the owner's message pump. Post the destroy to the OWNER worker
|
||||
// (serialized with its pump - no wndproc entry can race) and wait. Fall
|
||||
// back to direct destroy when the owner is the current thread or unknown
|
||||
// (leak > crash). Non-trivial: destroy jobs left queued on timeout are
|
||||
// harmless - IsWindow guards them, and the pump gate already stopped
|
||||
// dispatching to teardown windows.
|
||||
static void destroy_window_on_owner(HWND hwnd) {
|
||||
if (!hwnd || !IsWindow(hwnd)) return;
|
||||
DWORD ownerTid = GetWindowThreadProcessId(hwnd, nullptr);
|
||||
DWORD curTid = GetCurrentThreadId();
|
||||
if (ownerTid == 0 || ownerTid == curTid) {
|
||||
if (IsWindow(hwnd)) DestroyWindow(hwnd);
|
||||
return;
|
||||
}
|
||||
ChannelWorker* owner = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_tidMutex);
|
||||
auto it = g_tidToWorker.find(ownerTid);
|
||||
if (it != g_tidToWorker.end()) owner = it->second;
|
||||
}
|
||||
if (!owner) {
|
||||
// Window belongs to a thread we don't control (e.g. main thread).
|
||||
// Cross-thread DestroyWindow is unsafe -> leave it (leak > crash).
|
||||
std::cerr << "[NativeBridge] destroy_window_on_owner: owner tid=" << ownerTid
|
||||
<< " not a bridge worker - leaving window " << hwnd << std::endl;
|
||||
return;
|
||||
}
|
||||
if (!owner->post_and_wait([hwnd]() {
|
||||
if (IsWindow(hwnd)) DestroyWindow(hwnd);
|
||||
}))
|
||||
std::cerr << "[NativeBridge] destroy_window_on_owner: timeout waiting owner tid="
|
||||
<< ownerTid << " to destroy " << hwnd << std::endl;
|
||||
}""",
|
||||
"helpers")
|
||||
|
||||
# 8. post_close_gui: destroy outside g_loadMutex, via owner
|
||||
rep(
|
||||
""" bool reloadOk = false;
|
||||
{
|
||||
// destroy children + reload in ONE lock: destroying the
|
||||
// editor windows runs the plugin's window proc (DLL
|
||||
// entry) on this worker; it must not race another
|
||||
// thread's createInstance/createView of the same DLL.
|
||||
// Same-thread destroy is valid (attachView created them
|
||||
// on this worker); without destroy the editor keeps
|
||||
// pumping (0x47b flood) while the audio loop processes
|
||||
// the instance (2 threads in one DLL) -> Nexus USER32 crash.
|
||||
std::lock_guard<std::mutex> lg(g_loadMutex);
|
||||
// CRASH FIX: reload() = terminate + loadPlugin
|
||||
// (createInstance) — Nexus may throw a C++ exception
|
||||
// here too; catch it, treat as failed reload, and let
|
||||
// the restore below unmute the channel (instance may be
|
||||
// broken; next load/assign rebuilds it).
|
||||
try {
|
||||
#ifdef _WIN32
|
||||
disable_ime_contexts(hwnd);
|
||||
if (hwnd && IsWindow(hwnd)) {
|
||||
while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr)) {
|
||||
std::cerr << "[dbg] closeGUI destroy child=" << c
|
||||
<< " owner_tid=" << GetWindowThreadProcessId(c, nullptr)
|
||||
<< " cur_tid=" << GetCurrentThreadId() << std::endl;
|
||||
DestroyWindow(c);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
reloadOk = i->reload();
|
||||
} catch (...) {
|
||||
std::cerr << "[NativeBridge] closeGUI reload EXCEPTION ch=" << ch
|
||||
<< " — plugin threw (createInstance)" << std::endl;
|
||||
reloadOk = false;
|
||||
}
|
||||
}""",
|
||||
""" bool reloadOk = false;
|
||||
#ifdef _WIN32
|
||||
// CRASH FIX (0xc000041d): destroy the editor's child windows
|
||||
// on the thread that OWNS them (the plugin's JUCE
|
||||
// MessageManager thread - a single worker), never
|
||||
// cross-thread. Wait for each destroy to finish (owner worker
|
||||
// serializes it with its pump) BEFORE reload() - otherwise
|
||||
// reload's createInstance enters the DLL while the owner
|
||||
// thread is still inside the plugin wndproc (2 threads in one
|
||||
// DLL -> Nexus crash). Kept OUTSIDE g_loadMutex: the wait
|
||||
// must not block other loads (the owner worker may itself be
|
||||
// waiting on that lock).
|
||||
disable_ime_contexts(hwnd);
|
||||
if (hwnd && IsWindow(hwnd)) {
|
||||
while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr)) {
|
||||
std::cerr << "[dbg] closeGUI destroy child=" << c
|
||||
<< " owner_tid=" << GetWindowThreadProcessId(c, nullptr)
|
||||
<< " cur_tid=" << GetCurrentThreadId() << std::endl;
|
||||
destroy_window_on_owner(c);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
{
|
||||
// reload() = terminate + loadPlugin (createInstance) —
|
||||
// serialized with all other plugin-DLL entry points.
|
||||
// Nexus may throw a C++ exception here too; catch it,
|
||||
// treat as failed reload, and let the restore below
|
||||
// unmute the channel (instance may be broken; next
|
||||
// load/assign rebuilds it).
|
||||
std::lock_guard<std::mutex> lg(g_loadMutex);
|
||||
try {
|
||||
reloadOk = i->reload();
|
||||
} catch (...) {
|
||||
std::cerr << "[NativeBridge] closeGUI reload EXCEPTION ch=" << ch
|
||||
<< " — plugin threw (createInstance)" << std::endl;
|
||||
reloadOk = false;
|
||||
}
|
||||
}""",
|
||||
"post_close_gui destroy")
|
||||
|
||||
# 9. LOAD close-editor destroy loop via owner
|
||||
rep(
|
||||
""" // Destroy the old editor's child windows (worker-owned,
|
||||
// created by attachView here) so they stop pumping;
|
||||
// the parent window is hidden and kept for reuse.
|
||||
// See post_close_gui for the same pattern.
|
||||
while (HWND c = FindWindowExA(hToHide, nullptr, nullptr, nullptr)) {
|
||||
std::cerr << "[dbg] load close-editor destroy child=" << c
|
||||
<< " owner_tid=" << GetWindowThreadProcessId(c, nullptr)
|
||||
<< " cur_tid=" << GetCurrentThreadId() << std::endl;
|
||||
DestroyWindow(c);
|
||||
}""",
|
||||
""" // CRASH FIX (0xc000041d): destroy the old
|
||||
// editor's child windows on the thread that OWNS
|
||||
// them (JUCE MessageManager worker), never
|
||||
// cross-thread - DestroyWindow from this LOAD
|
||||
// worker raced the owner's pump dispatching
|
||||
// WM_PAINT into the plugin wndproc -> AV. Wait
|
||||
// for each destroy before assign() below. The
|
||||
// parent window is hidden and kept for reuse.
|
||||
// See post_close_gui for the same pattern.
|
||||
while (HWND c = FindWindowExA(hToHide, nullptr, nullptr, nullptr)) {
|
||||
std::cerr << "[dbg] load close-editor destroy child=" << c
|
||||
<< " owner_tid=" << GetWindowThreadProcessId(c, nullptr)
|
||||
<< " cur_tid=" << GetCurrentThreadId() << std::endl;
|
||||
destroy_window_on_owner(c);
|
||||
}""",
|
||||
"load destroy loop")
|
||||
|
||||
assert s != orig
|
||||
open(p, "wb").write(s.encode("utf-8"))
|
||||
print("written")
|
||||
@@ -0,0 +1,108 @@
|
||||
# Drive the REAL app's bridge SHM: open GUI ch=0, screenshot, pixel analysis.
|
||||
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time, re
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
|
||||
SHM_NAME = "SonicForge_DAW_IPC"
|
||||
SIZE = 32768
|
||||
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "probe_app.log")
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
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)
|
||||
for (type_, arg0, arg1, ch, path) in events:
|
||||
write_control(type_, arg0, arg1, ch, path)
|
||||
w32(11156, 'I', len(events))
|
||||
|
||||
def wait_log(pattern, timeout):
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < timeout:
|
||||
try:
|
||||
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
|
||||
lines = f.readlines()
|
||||
for i in range(len(lines)):
|
||||
if pattern in lines[i]:
|
||||
return lines[i].strip()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
time.sleep(0.2)
|
||||
return None
|
||||
|
||||
# copy app bridge.log to local log for wait_log
|
||||
applog = os.path.join(os.environ.get('APPDATA', '.'), 'SonicForgeDAW', 'logs', 'bridge.log')
|
||||
open(LOG, 'w', encoding='utf-8').write(open(applog, encoding='utf-8', errors='replace').read())
|
||||
|
||||
print("push OPEN_GUI ch=0...")
|
||||
push_controls([(4, 0, 0, 0, "nexus")])
|
||||
r = wait_log("GUI attached", 40)
|
||||
print("[open]", r)
|
||||
hwnd = None
|
||||
if r:
|
||||
m = re.search(r'hwnd=([0-9A-Fa-f]+)', r)
|
||||
if m:
|
||||
hwnd = int(m.group(1), 16)
|
||||
print("hwnd =", hex(hwnd) if hwnd else None)
|
||||
time.sleep(6)
|
||||
|
||||
if hwnd:
|
||||
rw = w.RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(rw))
|
||||
print("window rect", rw.left, rw.top, rw.right, rw.bottom, "vis", user32.IsWindowVisible(hwnd))
|
||||
wdt, hgt = rw.right - rw.left, rw.bottom - rw.top
|
||||
ps = f"""
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$bmp = New-Object System.Drawing.Bitmap({wdt}, {hgt})
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$g.CopyFromScreen({rw.left}, {rw.top}, 0, 0, $bmp.Size)
|
||||
$bmp.Save('C:/Users/locpham/SonicForgeStudio/native_bridge/debug/probe_app.png')
|
||||
$g.Dispose(); $bmp.Dispose()
|
||||
"""
|
||||
subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=False)
|
||||
print("screenshot saved probe_app.png")
|
||||
else:
|
||||
# dump all windows with class SonicForge_Native_VST3_Class
|
||||
import ctypes
|
||||
found = []
|
||||
@ctypes.WINFUNCTYPE(w.BOOL, w.HWND, w.LPARAM)
|
||||
def cb(h, lp):
|
||||
buf = ctypes.create_unicode_buffer(64)
|
||||
user32.GetClassNameW(h, buf, 64)
|
||||
if 'SonicForge' in buf.value:
|
||||
found.append((h, buf.value))
|
||||
return True
|
||||
user32.EnumWindows(cb, 0)
|
||||
print("native vst windows:", [(hex(h), c) for h, c in found])
|
||||
|
||||
kernel32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
|
||||
kernel32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||
kernel32.UnmapViewOfFile(ptr)
|
||||
kernel32.CloseHandle(hMap)
|
||||
print("done")
|
||||
@@ -0,0 +1,130 @@
|
||||
# Drive REAL app bridge SHM (SonicForge_DAW_IPC_<daw_pid>): load Nexus ch=0, open GUI, screenshot, brightness.
|
||||
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time, re
|
||||
from PIL import Image
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
|
||||
DAW_PID = sys.argv[1] if len(sys.argv) > 1 else "21148"
|
||||
SHM_NAME = b"SonicForge_DAW_IPC"
|
||||
SIZE = 32768
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
LOG = os.path.join(HERE, "probe_app2.log")
|
||||
APP_LOG = os.path.join(os.environ.get('APPDATA', '.'), 'SonicForgeDAW', 'logs', 'bridge.log')
|
||||
NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3"
|
||||
PNG = os.path.join(HERE, "probe_app2.png")
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
kernel32.OpenFileMappingA.argtypes = [ctypes.wintypes.DWORD, w.BOOL, ctypes.c_char_p]
|
||||
kernel32.OpenFileMappingA.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
|
||||
FILE_MAP_ALL_ACCESS = 0xF001F
|
||||
|
||||
hMap = kernel32.OpenFileMappingA(FILE_MAP_ALL_ACCESS, False, SHM_NAME)
|
||||
if not hMap:
|
||||
print("OpenFileMapping 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)
|
||||
for (type_, arg0, arg1, ch, path) in events:
|
||||
write_control(type_, arg0, arg1, ch, path)
|
||||
w32(11156, 'I', len(events))
|
||||
|
||||
def log_lines():
|
||||
try:
|
||||
with open(APP_LOG, 'r', encoding='utf-8', errors='replace') as f:
|
||||
return f.readlines()
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
|
||||
def wait_log(pattern, timeout, since):
|
||||
end = time.time() + timeout
|
||||
while time.time() < end:
|
||||
lines = log_lines()
|
||||
for i in range(since, len(lines)):
|
||||
if pattern in lines[i]:
|
||||
return lines[i].strip()
|
||||
time.sleep(0.2)
|
||||
return None
|
||||
|
||||
since = len(log_lines())
|
||||
print("shm", SHM_NAME, "log lines", since)
|
||||
print("[1] load Nexus ch=0...")
|
||||
push_controls([(2, 0, 0, 0, NEXUS)])
|
||||
r = wait_log("instrument loaded ch=0", 60, since)
|
||||
print(" ", r)
|
||||
since = len(log_lines())
|
||||
print("[2] open GUI ch=0...")
|
||||
push_controls([(4, 0, 0, 0, "nexus")])
|
||||
r = wait_log("GUI attached", 40, since)
|
||||
print("[open]", r)
|
||||
hwnd = None
|
||||
if r:
|
||||
m = re.search(r'hwnd=([0-9A-Fa-f]+)', r)
|
||||
if m:
|
||||
hwnd = int(m.group(1), 16)
|
||||
print("hwnd =", hex(hwnd) if hwnd else None)
|
||||
time.sleep(8)
|
||||
|
||||
if hwnd:
|
||||
rw = w.RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(rw))
|
||||
print("window rect", rw.left, rw.top, rw.right, rw.bottom, "vis", user32.IsWindowVisible(hwnd))
|
||||
wdt, hgt = rw.right - rw.left, rw.bottom - rw.top
|
||||
if wdt <= 0 or hgt <= 0:
|
||||
print("bad rect"); sys.exit(1)
|
||||
ps = f"""
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$bmp = New-Object System.Drawing.Bitmap({wdt}, {hgt})
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$g.CopyFromScreen({rw.left}, {rw.top}, 0, 0, $bmp.Size)
|
||||
$bmp.Save('{PNG}')
|
||||
$g.Dispose(); $bmp.Dispose()
|
||||
"""
|
||||
subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=False)
|
||||
img = Image.open(PNG).convert('L')
|
||||
px = list(img.getdata())
|
||||
n = len(px)
|
||||
avg = sum(px) / n
|
||||
buckets = [0, 0, 0, 0, 0, 0, 0, 0]
|
||||
for v in px:
|
||||
buckets[v // 32] += 1
|
||||
pct = [round(100 * b / n) for b in buckets]
|
||||
print("screenshot", PNG, img.size, "avgLum", round(avg, 1), "buckets[0-31..224-255]", pct)
|
||||
else:
|
||||
found = []
|
||||
@ctypes.WINFUNCTYPE(w.BOOL, w.HWND, w.LPARAM)
|
||||
def cb(h, lp):
|
||||
buf = ctypes.create_unicode_buffer(64)
|
||||
user32.GetClassNameW(h, buf, 64)
|
||||
if 'SonicForge' in buf.value:
|
||||
found.append((h, buf.value))
|
||||
return True
|
||||
user32.EnumWindows(cb, 0)
|
||||
print("native vst windows:", [(hex(h), c) for h, c in found])
|
||||
|
||||
kernel32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
|
||||
kernel32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||
kernel32.UnmapViewOfFile(ptr)
|
||||
kernel32.CloseHandle(hMap)
|
||||
print("done")
|
||||
@@ -0,0 +1,121 @@
|
||||
# Drive REAL app bridge SHM: cycle several VST3 plugins on ch=0, screenshot each, brightness.
|
||||
# Usage: python probe_app3.py
|
||||
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time, re
|
||||
from PIL import Image
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
|
||||
SHM_NAME = b"SonicForge_DAW_IPC"
|
||||
SIZE = 32768
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
APP_LOG = os.path.join(os.environ.get('APPDATA', '.'), 'SonicForgeDAW', 'logs', 'bridge.log')
|
||||
VST = r"C:\Program Files\Common Files\VST3"
|
||||
PLUGINS = [
|
||||
("nexus", os.path.join(VST, "Nexus.vst3")),
|
||||
("kontakt", os.path.join(VST, "Kontakt.vst3")),
|
||||
("scaler2", os.path.join(VST, "Scaler2.vst3")),
|
||||
("ample", os.path.join(VST, "Ample Sound", "ACDD.vst3")),
|
||||
]
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
kernel32.OpenFileMappingA.argtypes = [ctypes.wintypes.DWORD, w.BOOL, ctypes.c_char_p]
|
||||
kernel32.OpenFileMappingA.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
|
||||
FILE_MAP_ALL_ACCESS = 0xF001F
|
||||
|
||||
hMap = kernel32.OpenFileMappingA(FILE_MAP_ALL_ACCESS, False, SHM_NAME)
|
||||
if not hMap:
|
||||
print("OpenFileMapping 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)
|
||||
for (type_, arg0, arg1, ch, path) in events:
|
||||
write_control(type_, arg0, arg1, ch, path)
|
||||
w32(11156, 'I', len(events))
|
||||
|
||||
def log_lines():
|
||||
try:
|
||||
with open(APP_LOG, 'r', encoding='utf-8', errors='replace') as f:
|
||||
return f.readlines()
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
|
||||
def wait_log(pattern, timeout, since):
|
||||
end = time.time() + timeout
|
||||
while time.time() < end:
|
||||
lines = log_lines()
|
||||
for i in range(since, len(lines)):
|
||||
if pattern in lines[i]:
|
||||
return lines[i].strip()
|
||||
time.sleep(0.2)
|
||||
return None
|
||||
|
||||
def shot(hwnd, tag):
|
||||
rw = w.RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(rw))
|
||||
wdt, hgt = rw.right - rw.left, rw.bottom - rw.top
|
||||
if wdt <= 0 or hgt <= 0:
|
||||
return None
|
||||
png = os.path.join(HERE, f"probe_app3_{tag}.png")
|
||||
ps = f"""
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$bmp = New-Object System.Drawing.Bitmap({wdt}, {hgt})
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$g.CopyFromScreen({rw.left}, {rw.top}, 0, 0, $bmp.Size)
|
||||
$bmp.Save('{png}')
|
||||
$g.Dispose(); $bmp.Dispose()
|
||||
"""
|
||||
subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=False)
|
||||
img = Image.open(png).convert('L')
|
||||
px = list(img.getdata())
|
||||
n = len(px)
|
||||
avg = sum(px) / n
|
||||
buckets = [0, 0, 0, 0, 0, 0, 0, 0]
|
||||
for v in px:
|
||||
buckets[v // 32] += 1
|
||||
pct = [round(100 * b / n) for b in buckets]
|
||||
return f"avgLum {round(avg,1)} buckets[0-31..224-255] {pct} size {img.size}"
|
||||
|
||||
for tag, path in PLUGINS:
|
||||
since = len(log_lines())
|
||||
print(f"--- {tag} ---")
|
||||
push_controls([(2, 0, 0, 0, path)])
|
||||
r = wait_log("instrument loaded ch=0", 90, since)
|
||||
print(" load:", (r or "TIMEOUT").split(" type=")[0])
|
||||
since = len(log_lines())
|
||||
push_controls([(4, 0, 0, 0, tag)])
|
||||
r = wait_log("GUI attached", 40, since)
|
||||
print(" open:", (r or "TIMEOUT")[:100])
|
||||
m = re.search(r'hwnd=([0-9A-Fa-f]+)', r or "")
|
||||
if not m:
|
||||
print(" NO HWND"); continue
|
||||
hwnd = int(m.group(1), 16)
|
||||
time.sleep(8)
|
||||
res = shot(hwnd, tag)
|
||||
print(" shot:", res)
|
||||
# close GUI via WM_CLOSE (VstWindowProc hides + detaches view)
|
||||
user32.SendMessageW(hwnd, 0x0010, 0, 0) # WM_CLOSE
|
||||
time.sleep(3)
|
||||
|
||||
print("done")
|
||||
@@ -0,0 +1,180 @@
|
||||
# Verify fix flow user: create SHM plain -> spawn bridge (no debugger) -> load Nexus ch=0
|
||||
# -> open GUI -> screenshot -> close -> reopen -> verify unmute + other plugin loads.
|
||||
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time, re
|
||||
from PIL import Image
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
|
||||
SHM_NAME = "SonicForge_DAW_IPC"
|
||||
SIZE = 32768
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
BRIDGE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe"
|
||||
LOG = os.path.join(HERE, "probe_fix_flow.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
|
||||
|
||||
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)
|
||||
for (type_, arg0, arg1, ch, path) in events:
|
||||
write_control(type_, arg0, arg1, ch, path)
|
||||
w32(11156, 'I', len(events))
|
||||
|
||||
def write_index():
|
||||
return ctypes.c_uint32.from_address(ptr + 4).value
|
||||
|
||||
def wait_log(pattern, timeout, since=0):
|
||||
end = time.time() + timeout
|
||||
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(max(0, len(lines) - 1 - since), len(lines)):
|
||||
if pattern in lines[i]:
|
||||
return lines[i].strip()
|
||||
time.sleep(0.2)
|
||||
return None
|
||||
|
||||
open(LOG, 'w', encoding='utf-8').close()
|
||||
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, 'ab'), stderr=subprocess.STDOUT,
|
||||
)
|
||||
time.sleep(1.5)
|
||||
print("bridge pid", proc.pid, "poll", proc.poll())
|
||||
if proc.poll() is not None:
|
||||
print("!!! BRIDGE EXITED EARLY"); sys.exit(1)
|
||||
|
||||
def shot(hwnd, tag):
|
||||
user32 = ctypes.windll.user32
|
||||
rw = w.RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(rw))
|
||||
wdt, hgt = rw.right - rw.left, rw.bottom - rw.top
|
||||
if wdt <= 0 or hgt <= 0:
|
||||
return f"bad rect {rw.left},{rw.top},{rw.right},{rw.bottom}"
|
||||
png = os.path.join(HERE, f"probe_fix_{tag}.png")
|
||||
ps = f"""
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$bmp = New-Object System.Drawing.Bitmap({wdt}, {hgt})
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$g.CopyFromScreen({rw.left}, {rw.top}, 0, 0, $bmp.Size)
|
||||
$bmp.Save('{png}')
|
||||
$g.Dispose(); $bmp.Dispose()
|
||||
"""
|
||||
subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=False)
|
||||
img = Image.open(png).convert('L')
|
||||
px = list(img.getdata())
|
||||
n = len(px)
|
||||
avg = sum(px) / n
|
||||
buckets = [0, 0, 0, 0, 0, 0, 0, 0]
|
||||
for v in px:
|
||||
buckets[v // 32] += 1
|
||||
pct = [round(100 * b / n) for b in buckets]
|
||||
return f"avgLum {round(avg,1)} buckets[0-31..224-255] {pct} size {img.size}"
|
||||
|
||||
def muted_now():
|
||||
# bridge log "channel muted while editor open" appears while editor open;
|
||||
# after close, next load/open should NOT show mute. We instead check writeIndex.
|
||||
return None
|
||||
|
||||
# [1] load Nexus ch=0
|
||||
since = len(open(LOG, 'rb').read().splitlines())
|
||||
print("[1] load Nexus ch=0...")
|
||||
push_controls([(2, 0, 0, 0, NEXUS)])
|
||||
r = wait_log("instrument loaded ch=0", 90)
|
||||
print(" ", r)
|
||||
|
||||
# [2] open GUI
|
||||
since = len(open(LOG, 'rb').read().splitlines())
|
||||
print("[2] open GUI ch=0...")
|
||||
push_controls([(4, 0, 0, 0, "nexus")])
|
||||
r = wait_log("GUI attached", 40)
|
||||
print(" ", r)
|
||||
m = re.search(r'hwnd=([0-9A-Fa-f]+)', r or "")
|
||||
hwnd = int(m.group(1), 16) if m else None
|
||||
time.sleep(8)
|
||||
if hwnd:
|
||||
print(" shot:", shot(hwnd, "nexus_open"))
|
||||
alive = proc.poll() is None
|
||||
print(" alive:", alive)
|
||||
wi = write_index(); time.sleep(2); wi2 = write_index()
|
||||
print(" writeIndex", wi, "->", wi2, "advancing:", wi2 > wi)
|
||||
|
||||
# [3] close GUI via WM_CLOSE
|
||||
if hwnd:
|
||||
ctypes.windll.user32.SendMessageW(hwnd, 0x0010, 0, 0)
|
||||
time.sleep(4)
|
||||
alive = proc.poll() is None
|
||||
print("[3] after close alive:", alive)
|
||||
|
||||
# [4] reopen GUI (user symptom: reopen -> mute)
|
||||
since = len(open(LOG, 'rb').read().splitlines())
|
||||
print("[4] reopen GUI ch=0...")
|
||||
push_controls([(4, 0, 0, 0, "nexus")])
|
||||
r = wait_log("GUI attached", 40)
|
||||
print(" ", r)
|
||||
m = re.search(r'hwnd=([0-9A-Fa-f]+)', r or "")
|
||||
hwnd = int(m.group(1), 16) if m else None
|
||||
time.sleep(8)
|
||||
if hwnd:
|
||||
print(" shot:", shot(hwnd, "nexus_reopen"))
|
||||
alive = proc.poll() is None
|
||||
wi = write_index(); time.sleep(2); wi2 = write_index()
|
||||
print(" alive:", alive, "writeIndex", wi, "->", wi2, "advancing:", wi2 > wi)
|
||||
|
||||
# [5] load OTHER plugin while GUI open (user symptom: not load)
|
||||
since = len(open(LOG, 'rb').read().splitlines())
|
||||
print("[5] load Ample while Nexus GUI open...")
|
||||
push_controls([(2, 0, 0, 0, AMPLE)])
|
||||
r = wait_log("instrument loaded ch=0", 60)
|
||||
print(" ", r)
|
||||
alive = proc.poll() is None
|
||||
wi = write_index(); time.sleep(2); wi2 = write_index()
|
||||
print(" alive:", alive, "writeIndex", wi, "->", wi2, "advancing:", wi2 > wi)
|
||||
|
||||
print("FINAL alive:", proc.poll() is None)
|
||||
print("--- last 25 log lines ---")
|
||||
for l in open(LOG, 'r', encoding='utf-8', errors='replace').readlines()[-25:]:
|
||||
print(l.rstrip())
|
||||
|
||||
kernel32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
|
||||
kernel32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||
kernel32.UnmapViewOfFile(ptr)
|
||||
kernel32.CloseHandle(hMap)
|
||||
proc.terminate()
|
||||
print("done")
|
||||
@@ -0,0 +1,138 @@
|
||||
# Probe GUI render: spawn bridge, load Nexus, open GUI ch=5, dump window tree + screenshot.
|
||||
# Usage: python probe_gui.py [bridge_exe]
|
||||
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time, re
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
|
||||
SHM_NAME = "SonicForge_DAW_IPC_GUIPROBE"
|
||||
SIZE = 32768
|
||||
BRIDGE = sys.argv[1] if len(sys.argv) > 1 else r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe"
|
||||
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "probe_gui.log")
|
||||
NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3"
|
||||
AMPLE = r"C:\Program Files\Common Files\VST3\Ample Sound\ACDD.vst3"
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
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)
|
||||
for (type_, arg0, arg1, ch, path) in events:
|
||||
write_control(type_, arg0, arg1, ch, path)
|
||||
w32(11156, 'I', len(events)) # controlQueueCount — bridge drains only count entries
|
||||
|
||||
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)
|
||||
|
||||
def wait_log(pattern, timeout, since=0):
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < timeout:
|
||||
try:
|
||||
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
|
||||
lines = f.readlines()
|
||||
for i in range(since, len(lines)):
|
||||
if pattern in lines[i]:
|
||||
return lines[i].strip(), i + 1
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
time.sleep(0.2)
|
||||
return None, 0
|
||||
|
||||
push_controls([(2, 0, 0, 5, NEXUS)])
|
||||
r, pos = wait_log("instrument loaded ch=5", 90)
|
||||
print("[load]", r)
|
||||
|
||||
push_controls([(4, 0, 0, 5, "nexus")])
|
||||
r, pos = wait_log("GUI attached", 30)
|
||||
print("[open]", r)
|
||||
hwnd = None
|
||||
if r:
|
||||
m = re.search(r'hwnd=([0-9A-Fa-f]+)', r)
|
||||
if m:
|
||||
hwnd = int(m.group(1), 16)
|
||||
print("hwnd =", hex(hwnd) if hwnd else None)
|
||||
|
||||
time.sleep(5) # let the editor paint
|
||||
|
||||
def dump_tree(hwnd, depth=0):
|
||||
out = []
|
||||
buf = ctypes.create_unicode_buffer(256)
|
||||
user32.GetClassNameW(hwnd, buf, 256)
|
||||
user32.GetWindowTextW(hwnd, buf, 256)
|
||||
title = buf.value
|
||||
user32.GetClassNameW(hwnd, buf, 256)
|
||||
cls = buf.value
|
||||
r = w.RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(r))
|
||||
vis = user32.IsWindowVisible(hwnd)
|
||||
out.append(f"{' '*depth}h={hwnd:08X} cls='{cls}' title='{title}' rect=({r.left},{r.top},{r.right},{r.bottom}) vis={vis}")
|
||||
children = []
|
||||
@ctypes.WINFUNCTYPE(w.BOOL, w.HWND, w.LPARAM)
|
||||
def cb(child, lp):
|
||||
children.append(child)
|
||||
return True
|
||||
user32.EnumChildWindows(hwnd, cb, 0)
|
||||
for c in children:
|
||||
out.extend(dump_tree(c, depth + 1))
|
||||
return out
|
||||
|
||||
print("--- window tree ---")
|
||||
if hwnd:
|
||||
for ln in dump_tree(hwnd):
|
||||
print(ln)
|
||||
|
||||
# screenshot
|
||||
if hwnd:
|
||||
r = w.RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(r))
|
||||
wdt, hgt = r.right - r.left, r.bottom - r.top
|
||||
ps = f"""
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$bmp = New-Object System.Drawing.Bitmap({wdt}, {hgt})
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$g.CopyFromScreen({r.left}, {r.top}, 0, 0, $bmp.Size)
|
||||
$bmp.Save('C:/Users/locpham/SonicForgeStudio/native_bridge/debug/probe_gui.png')
|
||||
$g.Dispose(); $bmp.Dispose()
|
||||
"""
|
||||
subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=False)
|
||||
print("screenshot saved probe_gui.png")
|
||||
|
||||
# cleanup
|
||||
kernel32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
|
||||
kernel32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||
kernel32.UnmapViewOfFile(ptr)
|
||||
kernel32.CloseHandle(hMap)
|
||||
try:
|
||||
proc.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
print("done")
|
||||
@@ -0,0 +1,135 @@
|
||||
# Probe Ample GUI render with current bridge exe.
|
||||
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time, re
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
|
||||
SHM_NAME = "SonicForge_DAW_IPC_GUIPROBE2"
|
||||
SIZE = 32768
|
||||
BRIDGE = sys.argv[1] if len(sys.argv) > 1 else r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe"
|
||||
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "probe_gui2.log")
|
||||
PLUGIN = sys.argv[2] if len(sys.argv) > 2 else r"C:\Program Files\Common Files\VST3\Ample Sound\ACDD.vst3"
|
||||
PID_TAG = sys.argv[3] if len(sys.argv) > 3 else "ample"
|
||||
SHOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), f"probe_gui2_{PID_TAG}.png")
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
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)
|
||||
for (type_, arg0, arg1, ch, path) in events:
|
||||
write_control(type_, arg0, arg1, ch, path)
|
||||
w32(11156, 'I', len(events))
|
||||
|
||||
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)
|
||||
|
||||
def wait_log(pattern, timeout, since=0):
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < timeout:
|
||||
try:
|
||||
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
|
||||
lines = f.readlines()
|
||||
for i in range(since, len(lines)):
|
||||
if pattern in lines[i]:
|
||||
return lines[i].strip(), i + 1
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
time.sleep(0.2)
|
||||
return None, 0
|
||||
|
||||
push_controls([(2, 0, 0, 5, PLUGIN)])
|
||||
r, pos = wait_log("instrument loaded ch=5", 90)
|
||||
print("[load]", r)
|
||||
|
||||
push_controls([(4, 0, 0, 5, PID_TAG)])
|
||||
r, pos = wait_log("GUI attached", 40)
|
||||
print("[open]", r)
|
||||
hwnd = None
|
||||
if r:
|
||||
m = re.search(r'hwnd=([0-9A-Fa-f]+)', r)
|
||||
if m:
|
||||
hwnd = int(m.group(1), 16)
|
||||
print("hwnd =", hex(hwnd) if hwnd else None)
|
||||
|
||||
time.sleep(6) # let the editor paint
|
||||
|
||||
def dump_tree(hwnd, depth=0):
|
||||
out = []
|
||||
buf = ctypes.create_unicode_buffer(256)
|
||||
user32.GetClassNameW(hwnd, buf, 256)
|
||||
cls = buf.value
|
||||
user32.GetWindowTextW(hwnd, buf, 256)
|
||||
title = buf.value
|
||||
r = w.RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(r))
|
||||
vis = user32.IsWindowVisible(hwnd)
|
||||
out.append(f"{' '*depth}h={hwnd:08X} cls='{cls}' title='{title}' rect=({r.left},{r.top},{r.right},{r.bottom}) vis={vis}")
|
||||
children = []
|
||||
@ctypes.WINFUNCTYPE(w.BOOL, w.HWND, w.LPARAM)
|
||||
def cb(child, lp):
|
||||
children.append(child)
|
||||
return True
|
||||
user32.EnumChildWindows(hwnd, cb, 0)
|
||||
for c in children:
|
||||
out.extend(dump_tree(c, depth + 1))
|
||||
return out
|
||||
|
||||
print("--- window tree ---")
|
||||
if hwnd:
|
||||
for ln in dump_tree(hwnd):
|
||||
print(ln)
|
||||
|
||||
if hwnd:
|
||||
r = w.RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(r))
|
||||
wdt, hgt = r.right - r.left, r.bottom - r.top
|
||||
ps = f"""
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$bmp = New-Object System.Drawing.Bitmap({wdt}, {hgt})
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$g.CopyFromScreen({r.left}, {r.top}, 0, 0, $bmp.Size)
|
||||
$bmp.Save('{SHOT}')
|
||||
$g.Dispose(); $bmp.Dispose()
|
||||
"""
|
||||
subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=False)
|
||||
print("screenshot saved", SHOT)
|
||||
|
||||
kernel32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
|
||||
kernel32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||
kernel32.UnmapViewOfFile(ptr)
|
||||
kernel32.CloseHandle(hMap)
|
||||
try:
|
||||
proc.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
print("done")
|
||||
@@ -0,0 +1,135 @@
|
||||
# Probe Ample GUI render with current bridge exe.
|
||||
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time, re
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
|
||||
SHM_NAME = "SonicForge_DAW_IPC_GUIPROBE2"
|
||||
SIZE = 32768
|
||||
BRIDGE = sys.argv[1] if len(sys.argv) > 1 else r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe"
|
||||
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "probe_gui2.log")
|
||||
PLUGIN = sys.argv[2] if len(sys.argv) > 2 else r"C:\Program Files\Common Files\VST3\Ample Sound\ACDD.vst3"
|
||||
PID_TAG = sys.argv[3] if len(sys.argv) > 3 else "ample"
|
||||
SHOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), f"probe_gui2_{PID_TAG}.png")
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
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)
|
||||
for (type_, arg0, arg1, ch, path) in events:
|
||||
write_control(type_, arg0, arg1, ch, path)
|
||||
w32(11156, 'I', len(events))
|
||||
|
||||
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)
|
||||
|
||||
def wait_log(pattern, timeout, since=0):
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < timeout:
|
||||
try:
|
||||
with open(LOG, 'r', encoding='utf-8', errors='replace') as f:
|
||||
lines = f.readlines()
|
||||
for i in range(since, len(lines)):
|
||||
if pattern in lines[i]:
|
||||
return lines[i].strip(), i + 1
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
time.sleep(0.2)
|
||||
return None, 0
|
||||
|
||||
push_controls([(2, 0, 0, 5, PLUGIN)])
|
||||
r, pos = wait_log("instrument loaded ch=5", 90)
|
||||
print("[load]", r)
|
||||
|
||||
push_controls([(4, 0, 0, 5, PID_TAG)])
|
||||
r, pos = wait_log("GUI attached", 40)
|
||||
print("[open]", r)
|
||||
hwnd = None
|
||||
if r:
|
||||
m = re.search(r'hwnd=([0-9A-Fa-f]+)', r)
|
||||
if m:
|
||||
hwnd = int(m.group(1), 16)
|
||||
print("hwnd =", hex(hwnd) if hwnd else None)
|
||||
|
||||
time.sleep(25) # let the editor paint
|
||||
|
||||
def dump_tree(hwnd, depth=0):
|
||||
out = []
|
||||
buf = ctypes.create_unicode_buffer(256)
|
||||
user32.GetClassNameW(hwnd, buf, 256)
|
||||
cls = buf.value
|
||||
user32.GetWindowTextW(hwnd, buf, 256)
|
||||
title = buf.value
|
||||
r = w.RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(r))
|
||||
vis = user32.IsWindowVisible(hwnd)
|
||||
out.append(f"{' '*depth}h={hwnd:08X} cls='{cls}' title='{title}' rect=({r.left},{r.top},{r.right},{r.bottom}) vis={vis}")
|
||||
children = []
|
||||
@ctypes.WINFUNCTYPE(w.BOOL, w.HWND, w.LPARAM)
|
||||
def cb(child, lp):
|
||||
children.append(child)
|
||||
return True
|
||||
user32.EnumChildWindows(hwnd, cb, 0)
|
||||
for c in children:
|
||||
out.extend(dump_tree(c, depth + 1))
|
||||
return out
|
||||
|
||||
print("--- window tree ---")
|
||||
if hwnd:
|
||||
for ln in dump_tree(hwnd):
|
||||
print(ln)
|
||||
|
||||
if hwnd:
|
||||
r = w.RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(r))
|
||||
wdt, hgt = r.right - r.left, r.bottom - r.top
|
||||
ps = f"""
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$bmp = New-Object System.Drawing.Bitmap({wdt}, {hgt})
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$g.CopyFromScreen({r.left}, {r.top}, 0, 0, $bmp.Size)
|
||||
$bmp.Save('{SHOT}')
|
||||
$g.Dispose(); $bmp.Dispose()
|
||||
"""
|
||||
subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=False)
|
||||
print("screenshot saved", SHOT)
|
||||
|
||||
kernel32.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
|
||||
kernel32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||
kernel32.UnmapViewOfFile(ptr)
|
||||
kernel32.CloseHandle(hMap)
|
||||
try:
|
||||
proc.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
print("done")
|
||||
@@ -0,0 +1,46 @@
|
||||
# Screenshot an existing window by handle (decimal), dump tree, pixel stats.
|
||||
import ctypes, ctypes.wintypes as w, subprocess, sys, os
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
user32 = ctypes.windll.user32
|
||||
hwnd = int(sys.argv[1], 16) if len(sys.argv) > 1 else 0
|
||||
out = sys.argv[2] if len(sys.argv) > 2 else "probe_win.png"
|
||||
|
||||
def dump_tree(hwnd, depth=0):
|
||||
out = []
|
||||
buf = ctypes.create_unicode_buffer(256)
|
||||
user32.GetClassNameW(hwnd, buf, 256)
|
||||
cls = buf.value
|
||||
user32.GetWindowTextW(hwnd, buf, 256)
|
||||
title = buf.value
|
||||
r = w.RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(r))
|
||||
vis = user32.IsWindowVisible(hwnd)
|
||||
out.append(f"{' '*depth}h={hwnd:08X} cls='{cls}' title='{title}' rect=({r.left},{r.top},{r.right},{r.bottom}) vis={vis}")
|
||||
children = []
|
||||
@ctypes.WINFUNCTYPE(w.BOOL, w.HWND, w.LPARAM)
|
||||
def cb(child, lp):
|
||||
children.append(child)
|
||||
return True
|
||||
user32.EnumChildWindows(hwnd, cb, 0)
|
||||
for c in children:
|
||||
out.extend(dump_tree(c, depth + 1))
|
||||
return out
|
||||
|
||||
print("--- window tree ---")
|
||||
for ln in dump_tree(hwnd):
|
||||
print(ln)
|
||||
|
||||
r = w.RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(r))
|
||||
wdt, hgt = r.right - r.left, r.bottom - r.top
|
||||
print("size", wdt, hgt)
|
||||
ps = f"""
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$bmp = New-Object System.Drawing.Bitmap({wdt}, {hgt})
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$g.CopyFromScreen({r.left}, {r.top}, 0, 0, $bmp.Size)
|
||||
$bmp.Save('C:/Users/locpham/SonicForgeStudio/native_bridge/debug/{out}')
|
||||
$g.Dispose(); $bmp.Dispose()
|
||||
"""
|
||||
subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=False)
|
||||
print("saved", out)
|
||||
@@ -0,0 +1,107 @@
|
||||
# waitscan for gui_probe bridge_exact11 (PASS case): dump threads while the
|
||||
# probe is attached, to compare against the hung bridge.
|
||||
import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "waitscan_probe.log")
|
||||
PROBE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\gui_probe.exe"
|
||||
PATH = r"C:\Program Files\Common Files\VST3\Nexus.vst3"
|
||||
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
|
||||
proc = subprocess.Popen([PROBE, PATH, "bridge_exact11", "60"],
|
||||
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)
|
||||
k32 = ctypes.windll.kernel32
|
||||
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)
|
||||
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):
|
||||
_fields_ = [("raw", ctypes.c_ubyte * 0x4d0)]
|
||||
CONTEXT_AMD64 = 0x00100000
|
||||
CONTEXT_FULL = CONTEXT_AMD64 | 0x3
|
||||
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))
|
||||
k32.GetThreadContext.argtypes = [ctypes.c_void_p, ctypes.POINTER(CONTEXT)]
|
||||
k32.GetThreadContext.restype = w.BOOL
|
||||
k32.SuspendThread.argtypes = [ctypes.c_void_p]
|
||||
k32.ResumeThread.argtypes = [ctypes.c_void_p]
|
||||
k32.OpenThread.argtypes = [w.DWORD, w.BOOL, w.DWORD]
|
||||
k32.OpenThread.restype = ctypes.c_void_p
|
||||
k32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||
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"); 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)
|
||||
print(f"tid {tid}: rip={resolve(r['rip'])} rcx=0x{r['rcx']:x} r9=0x{r['r9']:x}")
|
||||
else:
|
||||
print(f"tid {tid}: GetThreadContext fail")
|
||||
k32.ResumeThread(ht)
|
||||
k32.CloseHandle(ht)
|
||||
proc.terminate()
|
||||
Reference in New Issue
Block a user