Проверить штатный выход MAME и загрузку state в DAP

This commit is contained in:
Александр Петров
2026-09-16 19:55:27 +03:00
parent 60762a7c0d
commit f91d296476
4 changed files with 116 additions and 11 deletions
+88 -3
View File
@@ -23,7 +23,7 @@ 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 sdbg.session import DebugSession, SessionError
from mame_interactive import build_events
from make_disk import create_floppy_image
from mame_profile import MameProfile, write_keyboard_config
@@ -42,6 +42,12 @@ def wait_dap_stop(engine, timeout=15):
def main():
banked_mode = '--banked' in sys.argv
if '--lifecycle-exit' in sys.argv and '--lifecycle-load' in sys.argv:
raise ValueError('Выберите один lifecycle-сценарий')
lifecycle = 'exit' if '--lifecycle-exit' in sys.argv else (
'load' if '--lifecycle-load' in sys.argv else None)
if lifecycle and not {'--banked', '--bridge', '--server'} <= set(sys.argv):
raise ValueError('Lifecycle-пробник требует --banked --bridge --server')
work=ROOT/'build/sdbg-live'
work.mkdir(parents=True,exist_ok=True)
source=work/'probe.c'
@@ -161,6 +167,8 @@ void worker(void) __banked {
ev=',\n'.join('{'+f'{t},"{tag}",{mask},{value}'+'}' for t,tag,mask,value in events)
report=work/'result.jsonl'
report.write_text('')
control=work/('control-'+uuid.uuid4().hex)
state_file=work/('state-'+uuid.uuid4().hex+'.sta')
lua=work/'probe.lua'
lua.write_text('''if _G.sdbg_probe_loaded then return end
_G.sdbg_probe_loaded=true
@@ -180,14 +188,39 @@ local bp=nil
local false_hits=0
local launch_reported=false
local active=true
local state_loaded=false
local state_load_reported=false
_G.sdbg_probe_reset_subscription=emu.add_machine_reset_notifier(function()
-- Первый notifier относится к начальному machine start. После arm это
-- уже настоящий reset сессии, и старый debugger object трогать нельзя.
if phase~="booting" then active=false end
end)
_G.sdbg_probe_load_subscription=emu.add_machine_post_load_notifier(function()
state_loaded=true
end)
debug.execution_state="run"
emu.register_periodic(function()
if not active then return end
if state_loaded and not state_load_reported then
local count=0
for _ in pairs(cpu.debug:bplist()) do count=count+1 end
emit('{"event":"state_loaded","breakpoints":' .. count .. '}')
state_load_reported=true
end
if phase=="bridge_wait" then
local control=io.open(CONTROL,"rb")
if control then
local action=control:read("*a");control:close();os.remove(CONTROL)
emit('{"event":"control","action":"' .. action .. '"}')
if action=="exit" then machine:exit()
-- save/load не входят в scheduled_event_pending(): остановленный
-- debugger надо отпустить, иначе операция не дойдёт до timeslice.
elseif action=="save" then machine:save(STATE);debug.execution_state="run"
elseif action=="load" then machine:load(STATE);debug.execution_state="run"
else error("Unknown lifecycle action: " .. action) end
end
return
end
ticks=ticks+1
-- До окна запуска приложения DSS работает вообще без наших точек.
-- Первая клавиша launch sequence придёт через полсекунды после arm.
@@ -234,11 +267,13 @@ emu.register_periodic(function()
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)))
.replace('SIGNATURE',','.join(map(str,signature))).replace('MAIN',str(main_address))
.replace('CONTROL',json.dumps(str(control))).replace('STATE',json.dumps(str(state_file))))
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'),
'-beta:wd179x:0','35hd','-beta:wd179x:1','35hd',
'-flop1',str(work/'probe.img'),
'-flop2',str(profile.dss_image),
'-hard1',str(system),'-debug','-debugger','none',
'-autoboot_delay','0','-autoboot_script',str(lua)]
@@ -296,6 +331,56 @@ end)
dap=DapEngine()
attached,_=dap.handle('attach',{'socket':str(socket_path)})
assert attached['buildId']==model.manifest['build_id']
if lifecycle:
def send_control(action):
pending=control.with_suffix('.tmp')
pending.write_text(action)
pending.replace(control)
if lifecycle=='load':
send_control('save')
deadline=time.monotonic()+20
while (not state_file.is_file() or state_file.stat().st_size==0):
if process.poll() is not None or time.monotonic()>deadline:
raise RuntimeError('MAME не сохранил state; лог: '+str(work/'mame.log'))
time.sleep(.05)
time.sleep(.25)
dap.handle('pause',{})
points=dap.rpc('set_function_breakpoints',
{'names':['worker']})['breakpoints']
assert points[0]['verified'] and points[0]['backend_ids'],points
send_control(lifecycle)
deadline=time.monotonic()+20
dap_events=[]
while time.monotonic()<deadline:
found,closed=dap.poll_events(.5)
dap_events.extend(found)
if closed: break
assert closed and any(name=='terminated' for name,_ in dap_events),dap_events
try:
dap.rpc('status')
raise AssertionError('Старая DAP-сессия осталась доступной')
except SessionError as error:
assert 'закрыта' in str(error) or 'инвалидирована' in str(error),error
if lifecycle=='exit':
assert process.wait(timeout=5)==0
else:
deadline=time.monotonic()+5
while '"event":"state_loaded"' not in report.read_text():
if process.poll() is not None or time.monotonic()>deadline:
raise RuntimeError('Нет post-load события; лог: '+str(work/'mame.log'))
time.sleep(.05)
loaded=next(json.loads(line) for line in report.read_text().splitlines()
if '"event":"state_loaded"' in line)
assert loaded['breakpoints']==0,loaded
with report.open('a') as file:
file.write(json.dumps({'event':'lifecycle_verified',
'action':lifecycle,'terminated':True,
'backend_breakpoints':points[0]['backend_ids']})+'\n')
print(json.dumps({'event':'lifecycle_verified',
'action':lifecycle,'dap_terminated':True,
'state_breakpoints':0 if lifecycle=='load' else None}))
state_file.unlink(missing_ok=True)
return 0
started=time.monotonic()
points,_=dap.handle('setFunctionBreakpoints',
{'breakpoints':[{'name':'worker'}]})