Проверить штатный выход 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
+15 -3
View File
@@ -214,6 +214,8 @@ pyenv exec python tests/sdbg/run_mame_probe.py
pyenv exec python tests/sdbg/run_mame_probe.py --bridge pyenv exec python tests/sdbg/run_mame_probe.py --bridge
pyenv exec python tests/sdbg/run_mame_probe.py --bridge --banked pyenv exec python tests/sdbg/run_mame_probe.py --bridge --banked
pyenv exec python tests/sdbg/run_mame_probe.py --bridge --banked --server pyenv exec python tests/sdbg/run_mame_probe.py --bridge --banked --server
pyenv exec python tests/sdbg/run_mame_probe.py --banked --bridge --server --lifecycle-exit
pyenv exec python tests/sdbg/run_mame_probe.py --banked --bridge --server --lifecycle-load
pyenv exec python tests/sdbg/run_mame_probe.py --banked --launcher pyenv exec python tests/sdbg/run_mame_probe.py --banked --launcher
pyenv exec python tests/sdbg/run_mame_probe.py --banked --launcher --native-debugger pyenv exec python tests/sdbg/run_mame_probe.py --banked --launcher --native-debugger
pyenv exec python tests/sdbg/run_mame_probe.py --banked --launcher --source-step pyenv exec python tests/sdbg/run_mame_probe.py --banked --launcher --source-step
@@ -273,8 +275,18 @@ VS Code запустил сборку SprPoP и остановился в `src/s
с `sdbg` и опциональным `osx` debugger provider. с `sdbg` и опциональным `osx` debugger provider.
Отдельная проба `SIGTERM` остановленному MAME не дала DAP `terminated` за Отдельная проба `SIGTERM` остановленному MAME не дала DAP `terminated` за
15 секунд; причина (обработка сигнала самим MAME или путь закрытия сессии) 15 секунд; причина (обработка сигнала самим MAME или путь закрытия сессии)
ещё не установлена. Штатное закрытие окна и state load не прошли отдельный ещё не установлена. Это отдельный OS-signal путь, не равный закрытию окна.
живой тест.
Живой lifecycle-пробник на банковой программе теперь прошёл с `sdbg` и
опциональным `osx` provider. На остановке в `main` Lua вызвал
`machine:exit()`: MAME вышел с кодом 0, DAP получил `terminated`, старый
session server отверг запрос. Закрытие SDL3-окна в MAME вызывает тот же
`schedule_exit()`; физический клик по кнопке окна не автоматизировался.
При save/load state пробник после сохранения поставил банковую точку,
загрузил сохранённое состояние и получил `terminated`; post-load проверка
`bplist()` показала ноль оставшихся точек, старый session server отверг
запрос. Для save/load пробник продолжает CPU после планирования операции:
эти команды MAME сами не выводят CPU из debugger stop-loop.
Это репро доступности механизма, а не полный benchmark или проверка всех Это репро доступности механизма, а не полный benchmark или проверка всех
runtime/mapping/lifecycle. Отдельная публикация debug-библиотек требует runtime/mapping/lifecycle. Отдельная публикация debug-библиотек требует
@@ -332,7 +344,7 @@ Crash reports в 19:22/19:23 относились к неподдержанно
строит проверенное условие через I/O port. После штатных прогонов в 20:43 и строит проверенное условие через I/O port. После штатных прогонов в 20:43 и
позже новых `mame.arm-*.ips` не появилось. позже новых `mame.arm-*.ips` не появилось.
Далее по плану: проверить graceful exit/state load живым репро, сделать Далее по плану: исследовать отдельно OS-signal путь `SIGTERM`, сделать
MCP-адаптер, безопасный restart и расширенные выражения. VSIX уже упакован; MCP-адаптер, безопасный restart и расширенные выражения. VSIX уже упакован;
для IDE ещё нужны Run-команда, выбор профиля для IDE ещё нужны Run-команда, выбор профиля
сборки/данных и расширенная диагностика assembler/linker. сборки/данных и расширенная диагностика assembler/linker.
+7 -2
View File
@@ -680,8 +680,13 @@ exit/reset/reload инвалидируют состояние; потеря кл
Текущая реализация опрашивает backend и при остановленном CPU: потеря MAME Текущая реализация опрашивает backend и при остановленном CPU: потеря MAME
переводит DAP в `terminated` (проверено живым пробником с внезапным выходом). переводит DAP в `terminated` (проверено живым пробником с внезапным выходом).
Reset/state load обработаны notifier плагина, инвалидация проверена host-тестом; Проверенные живые lifecycle-пробники вызывают штатный `schedule_exit()` через
штатное закрытие MAME и загрузка state требуют отдельных живых проверок. Lua `machine:exit()` и реальный save/load state при остановке в `main`:
оба завершают DAP, post-load удаляет старые точки. SDL3-событие закрытия
окна использует тот же `schedule_exit()`; прямой UI-клик не автоматизировался.
Reset по-прежнему требует безопасного отдельного репро. `SIGTERM`
остановленному процессу MAME пока не дал DAP `terminated` за 15 секунд;
это отдельный OS-signal путь, не проверка штатного выхода.
### Этап 3 — полезная CLI/MCP-отладка и логи ### Этап 3 — полезная CLI/MCP-отладка и логи
+6 -3
View File
@@ -101,9 +101,12 @@ service-точка `main` и вводится `a:\\HELLO.EXE`. После сов
При reset/state load или потере MAME остановленная сессия становится При reset/state load или потере MAME остановленная сессия становится
недействительной и DAP завершает её; для новой отладки запускайте F5 заново. недействительной и DAP завершает её; для новой отладки запускайте F5 заново.
Проверен сценарий внезапного завершения собственного MAME во время остановки Проверены внезапная потеря собственного MAME, штатный `machine:exit()` и
в `main`. Отдельный живой тест штатного закрытия окна и загрузки state ещё реальная загрузка state при остановке в `main`: каждый раз DAP получает
предстоит выполнить. `terminated`, после load старые точки удалены. Закрытие SDL3-окна MAME
вызывает тот же `schedule_exit()`, что `machine:exit()`; прямой UI-клик
в пробнике не выполнялся. Эти проверки прошли с `sdbg` и опциональным
`osx` debugger provider.
`dssTimeout` задаёт предельное время ожидания prompt (30 эмулируемых секунд). `dssTimeout` задаёт предельное время ожидания prompt (30 эмулируемых секунд).
`launchAt` можно задать как необязательную нижнюю границу времени запуска; `launchAt` можно задать как необязательную нижнюю границу времени запуска;
+88 -3
View File
@@ -23,7 +23,7 @@ os.environ['SPRINTER_PYTHON'] = sys.executable
sys.path.insert(0,str(ROOT/'toolchain')) sys.path.insert(0,str(ROOT/'toolchain'))
from sdbg.model import DebugMap from sdbg.model import DebugMap
from sdbg.image import read_ihx from sdbg.image import read_ihx
from sdbg.session import DebugSession from sdbg.session import DebugSession, SessionError
from mame_interactive import build_events from mame_interactive import build_events
from make_disk import create_floppy_image from make_disk import create_floppy_image
from mame_profile import MameProfile, write_keyboard_config from mame_profile import MameProfile, write_keyboard_config
@@ -42,6 +42,12 @@ def wait_dap_stop(engine, timeout=15):
def main(): def main():
banked_mode = '--banked' in sys.argv 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=ROOT/'build/sdbg-live'
work.mkdir(parents=True,exist_ok=True) work.mkdir(parents=True,exist_ok=True)
source=work/'probe.c' 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) ev=',\n'.join('{'+f'{t},"{tag}",{mask},{value}'+'}' for t,tag,mask,value in events)
report=work/'result.jsonl' report=work/'result.jsonl'
report.write_text('') report.write_text('')
control=work/('control-'+uuid.uuid4().hex)
state_file=work/('state-'+uuid.uuid4().hex+'.sta')
lua=work/'probe.lua' lua=work/'probe.lua'
lua.write_text('''if _G.sdbg_probe_loaded then return end lua.write_text('''if _G.sdbg_probe_loaded then return end
_G.sdbg_probe_loaded=true _G.sdbg_probe_loaded=true
@@ -180,14 +188,39 @@ local bp=nil
local false_hits=0 local false_hits=0
local launch_reported=false local launch_reported=false
local active=true local active=true
local state_loaded=false
local state_load_reported=false
_G.sdbg_probe_reset_subscription=emu.add_machine_reset_notifier(function() _G.sdbg_probe_reset_subscription=emu.add_machine_reset_notifier(function()
-- Первый notifier относится к начальному machine start. После arm это -- Первый notifier относится к начальному machine start. После arm это
-- уже настоящий reset сессии, и старый debugger object трогать нельзя. -- уже настоящий reset сессии, и старый debugger object трогать нельзя.
if phase~="booting" then active=false end if phase~="booting" then active=false end
end) end)
_G.sdbg_probe_load_subscription=emu.add_machine_post_load_notifier(function()
state_loaded=true
end)
debug.execution_state="run" debug.execution_state="run"
emu.register_periodic(function() emu.register_periodic(function()
if not active then return end 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 ticks=ticks+1
-- До окна запуска приложения DSS работает вообще без наших точек. -- До окна запуска приложения DSS работает вообще без наших точек.
-- Первая клавиша launch sequence придёт через полсекунды после arm. -- Первая клавиша launch sequence придёт через полсекунды после arm.
@@ -234,11 +267,13 @@ emu.register_periodic(function()
if now()>35 then emit('{"event":"timeout"}');report:close();machine:exit() end if now()>35 then emit('{"event":"timeout"}');report:close();machine:exit() end
end) end)
'''.replace('REPORT',json.dumps(str(report))).replace('EVENTS',ev) '''.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', command=[str(profile.binary),'sprinter','-noreadconfig',
'-rompath',str(profile.rompath),'-bios',profile.bios,'-kbd','ms_naturl,bios=sp2k', '-rompath',str(profile.rompath),'-bios',profile.bios,'-kbd','ms_naturl,bios=sp2k',
'-video','none','-sound','none','-nothrottle','-skip_gameinfo', '-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), '-flop2',str(profile.dss_image),
'-hard1',str(system),'-debug','-debugger','none', '-hard1',str(system),'-debug','-debugger','none',
'-autoboot_delay','0','-autoboot_script',str(lua)] '-autoboot_delay','0','-autoboot_script',str(lua)]
@@ -296,6 +331,56 @@ end)
dap=DapEngine() dap=DapEngine()
attached,_=dap.handle('attach',{'socket':str(socket_path)}) attached,_=dap.handle('attach',{'socket':str(socket_path)})
assert attached['buildId']==model.manifest['build_id'] 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() started=time.monotonic()
points,_=dap.handle('setFunctionBreakpoints', points,_=dap.handle('setFunctionBreakpoints',
{'breakpoints':[{'name':'worker'}]}) {'breakpoints':[{'name':'worker'}]})