Проверить штатный выход MAME и загрузку state в DAP
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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-отладка и логи
|
||||||
|
|
||||||
|
|||||||
@@ -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` можно задать как необязательную нижнюю границу времени запуска;
|
||||||
|
|||||||
@@ -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'}]})
|
||||||
|
|||||||
Reference in New Issue
Block a user