#!/usr/bin/env python3 """Изолированный MAME-репро: загрузка EXE, проверка main и async step. Использует копию системного HDD и собственную дискету/каталоги состояния. Ничего не меняет в общих media или уже запущенном MAME. """ import json import os from pathlib import Path import select import shutil import subprocess import sys import time import uuid # Запуск через системный python3 тоже подхватывает local pyenv проекта. if sys.version_info < (3, 12): os.execvp('pyenv', ['pyenv', 'exec', 'python', __file__, *sys.argv[1:]]) ROOT=Path(__file__).resolve().parents[2] os.environ['SPRINTER_PYTHON'] = sys.executable sys.path.insert(0,str(ROOT/'toolchain')) from sdbg.model import DebugMap from sdbg.image import read_ihx from sdbg.session import DebugSession from mame_interactive import build_events from make_disk import create_floppy_image from mame_profile import MameProfile, write_keyboard_config def wait_dap_stop(engine, timeout=15): deadline=time.monotonic()+timeout seen=[] while time.monotonic()=events[1][1]-0.5 then bp=cpu.debug:bpset(MAIN,"", "") phase="loading" machine.video:snapshot() emit('{"event":"dss_wait_complete","time":' .. now() .. '}') emit('{"event":"entry_armed","time":' .. now() .. '}') end if not launch_reported and now()>=events[1][1] then launch_reported=true emit('{"event":"launch_input_started","time":' .. now() .. '}') end while index<=#events and now()>=events[index][1] do local e=events[index] for _,field in pairs(machine.ioport.ports[e[2]].fields) do if field.mask==e[3] then field:set_value(e[4]);break end end index=index+1 end if debug.execution_state=="stop" then local pc=cpu.state.PC.value if phase=="loading" then local match=pc==MAIN for i,b in ipairs(signature) do if cpu.spaces.program:read_u8(0x10000+MAIN+i-1)~=b then match=false end end if not match then false_hits=false_hits+1;debug.execution_state="run";return end emit('{"event":"main","pc":' .. pc .. ',"ticks":' .. ticks .. ',"false_hits":' .. false_hits .. '}') cpu.debug:bpclear(bp) if os.getenv("SDBG_SESSION_ID") then phase="bridge_wait";return end phase="stepping" last_time=now() cpu.debug:step(1) emit('{"event":"step_requested","pc":' .. cpu.state.PC.value .. '}') elseif phase=="stepping" and now()>last_time then emit('{"event":"step_completed","pc":' .. pc .. ',"ticks":' .. ticks .. '}') report:close() machine:exit() end end if now()>35 then emit('{"event":"timeout"}');report:close();machine:exit() end end) '''.replace('REPORT',json.dumps(str(report))).replace('EVENTS',ev) .replace('SIGNATURE',','.join(map(str,signature))).replace('MAIN',str(main_address))) command=[str(profile.binary),'sprinter','-noreadconfig', '-rompath',str(profile.rompath),'-bios',profile.bios,'-kbd','ms_naturl,bios=sp2k', '-video','none','-sound','none','-nothrottle','-skip_gameinfo', '-beta:wd179x:0','35hd','-flop1',str(work/'probe.img'), '-flop2',str(profile.dss_image), '-hard1',str(system),'-debug','-debugger','none', '-autoboot_delay','0','-autoboot_script',str(lua)] for key in ['nvram','cfg','diff','snapshot']: (work/key).mkdir(exist_ok=True) command.extend(['-'+key+'_directory',str(work/key)]) write_keyboard_config(work/'cfg') bridge_mode = '--bridge' in sys.argv server_mode = '--server' in sys.argv env = dict(os.environ) if bridge_mode: command[command.index('-debugger')+1] = ( 'osx' if '--native-debugger' in sys.argv else 'sdbg') command[command.index('-video')+1]='soft' command.append('-window') ipc=work/('ipc-'+uuid.uuid4().hex) ipc.mkdir() env.update(SDBG_IPC_DIR=str(ipc),SDBG_SESSION_ID=uuid.uuid4().hex) plugin_paths = [ROOT / 'toolchain/mcp'] if profile.home: installed = profile.home.parent / 'plugins' if installed.is_dir(): plugin_paths.append(installed) command.extend(['-verbose','-plugin','sdbgbridge','-pluginspath', ';'.join(map(str, plugin_paths))]) with (work/'mame.log').open('w') as log: if not bridge_mode: result=subprocess.run(command,cwd=work,env=env,stdout=log,stderr=subprocess.STDOUT,timeout=55) else: from sdbg.transport import FileBridge, BridgeError process=subprocess.Popen(command,cwd=work,env=env,stdout=log,stderr=subprocess.STDOUT) bridge=None server_process=None try: deadline=time.monotonic()+50 while '"event":"main"' not in report.read_text(): if process.poll() is not None or time.monotonic()>deadline: raise RuntimeError('Нет остановки main; см. '+str(work/'mame.log')) time.sleep(.05) if server_mode: assert banked_mode,'--server live probe пока требует --banked' from sdbg.dap import DapEngine socket_path=Path('/tmp')/('sprinter-sdbg-'+uuid.uuid4().hex+'.sock') server_process=subprocess.Popen([ sys.executable,str(ROOT/'toolchain/sdbg_server.py'), '--build',str(model.directory),'--ipc',str(ipc), '--session',env['SDBG_SESSION_ID'],'--socket',str(socket_path)], stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True) ready=server_process.stdout.readline() assert ready and json.loads(ready)['ready'],server_process.stderr.read() try: FileBridge(ipc,env['SDBG_SESSION_ID']) raise AssertionError('Session server не удержал owner lock') except BridgeError: pass dap=DapEngine() attached,_=dap.handle('attach',{'socket':str(socket_path)}) assert attached['buildId']==model.manifest['build_id'] started=time.monotonic() points,_=dap.handle('setFunctionBreakpoints', {'breakpoints':[{'name':'worker'}]}) assert points['breakpoints'][0]['verified'] dap.handle('continue',{}) deadline=time.monotonic()+5 dap_events=[] while time.monotonic()=1 bridge.request('continue') stopped=bridge.wait_stopped() location=session.where(stopped) assert location['status'] in ('mapped','ambiguous'), (location,stopped,attached) assert location['function']['name']=='worker' assert location['function']['bank']==1 session.clear_breakpoint(breakpoint['id']) after={'pc':stopped['registers']['PC']} else: bridge.request('step') after=session.where(bridge.wait_stopped()) assert after['pc']==main_address+3 bridge.request('step') session.where(bridge.wait_stopped()) assert session.read_variable('total')['value']==42 breakpoint=session.break_line(str(source),5) bridge.request('continue') stopped=bridge.wait_stopped() location=session.where(stopped) assert location['status']=='mapped' assert location['sources']==[{'file':str(source.resolve()),'line':5}] session.clear_breakpoint(breakpoint['id']) events=bridge.request('events',after=0) assert any(event['kind']=='stopped' for event in events['events']) final_total=session.read_variable('total')['value'] if not banked_mode else None debug_wall_ms=round((time.monotonic()-started)*1000,2) with report.open('a') as file: file.write(json.dumps({'event':'bridge_verified','pc_before':main_address, 'pc_after_step':after['pc'], 'line_break_pc':stopped['registers']['PC'], 'total':final_total, 'bank':location['function'].get('bank'), 'step_wall_ms':debug_wall_ms, 'generation':bridge.generation})+'\n') finally: if bridge: bridge.close() if server_process and server_process.poll() is None: server_process.terminate() try: server_process.wait(timeout=3) except subprocess.TimeoutExpired: server_process.kill();server_process.wait() if process.poll() is None: process.terminate() try: process.wait(timeout=5) except subprocess.TimeoutExpired: process.kill();process.wait() print(report.read_text()) if not bridge_mode and (result.returncode or 'step_completed' not in report.read_text()): print((work/'mame.log').read_text());return 1 return 0 if __name__=='__main__':sys.exit(main())