diff --git a/docs/mame-source-debug-status.md b/docs/mame-source-debug-status.md index 8fb7a58..ad40303 100644 --- a/docs/mame-source-debug-status.md +++ b/docs/mame-source-debug-status.md @@ -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 --banked 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 --native-debugger 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. Отдельная проба `SIGTERM` остановленному MAME не дала DAP `terminated` за 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 или проверка всех runtime/mapping/lifecycle. Отдельная публикация debug-библиотек требует @@ -332,7 +344,7 @@ Crash reports в 19:22/19:23 относились к неподдержанно строит проверенное условие через I/O port. После штатных прогонов в 20:43 и позже новых `mame.arm-*.ips` не появилось. -Далее по плану: проверить graceful exit/state load живым репро, сделать +Далее по плану: исследовать отдельно OS-signal путь `SIGTERM`, сделать MCP-адаптер, безопасный restart и расширенные выражения. VSIX уже упакован; для IDE ещё нужны Run-команда, выбор профиля сборки/данных и расширенная диагностика assembler/linker. diff --git a/docs/mame-source-debug.md b/docs/mame-source-debug.md index 6ad107b..0cf712e 100644 --- a/docs/mame-source-debug.md +++ b/docs/mame-source-debug.md @@ -680,8 +680,13 @@ exit/reset/reload инвалидируют состояние; потеря кл Текущая реализация опрашивает backend и при остановленном CPU: потеря MAME переводит DAP в `terminated` (проверено живым пробником с внезапным выходом). -Reset/state load обработаны notifier плагина, инвалидация проверена host-тестом; -штатное закрытие MAME и загрузка state требуют отдельных живых проверок. +Проверенные живые lifecycle-пробники вызывают штатный `schedule_exit()` через +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-отладка и логи diff --git a/docs/vscode-sprinter-debug.md b/docs/vscode-sprinter-debug.md index a2034d2..c9644dc 100644 --- a/docs/vscode-sprinter-debug.md +++ b/docs/vscode-sprinter-debug.md @@ -101,9 +101,12 @@ service-точка `main` и вводится `a:\\HELLO.EXE`. После сов При reset/state load или потере MAME остановленная сессия становится недействительной и DAP завершает её; для новой отладки запускайте F5 заново. -Проверен сценарий внезапного завершения собственного MAME во время остановки -в `main`. Отдельный живой тест штатного закрытия окна и загрузки state ещё -предстоит выполнить. +Проверены внезапная потеря собственного MAME, штатный `machine:exit()` и +реальная загрузка state при остановке в `main`: каждый раз DAP получает +`terminated`, после load старые точки удалены. Закрытие SDL3-окна MAME +вызывает тот же `schedule_exit()`, что `machine:exit()`; прямой UI-клик +в пробнике не выполнялся. Эти проверки прошли с `sdbg` и опциональным +`osx` debugger provider. `dssTimeout` задаёт предельное время ожидания prompt (30 эмулируемых секунд). `launchAt` можно задать как необязательную нижнюю границу времени запуска; diff --git a/tests/sdbg/run_mame_probe.py b/tests/sdbg/run_mame_probe.py index 9c0ea05..0cac4bd 100644 --- a/tests/sdbg/run_mame_probe.py +++ b/tests/sdbg/run_mame_probe.py @@ -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: + 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'}]})