diff --git a/docs/mame-source-debug-status.md b/docs/mame-source-debug-status.md index ad40303..be58844 100644 --- a/docs/mame-source-debug-status.md +++ b/docs/mame-source-debug-status.md @@ -222,6 +222,8 @@ pyenv exec python tests/sdbg/run_mame_probe.py --banked --launcher --source-step pyenv exec python tests/sdbg/run_mame_probe.py --banked --launcher --step-out pyenv exec python tests/sdbg/run_vscode_dap_probe.py pyenv exec python tests/sdbg/run_vscode_dap_probe.py --exit-while-stopped +pyenv exec python tests/sdbg/run_vscode_dap_probe.py --stop-while-stopped +pyenv exec python tests/sdbg/run_vscode_dap_probe.py --term-while-stopped pyenv exec python tests/sdbg/run_vscode_dap_probe.py --waitkey --manual-key ``` @@ -273,9 +275,16 @@ VS Code запустил сборку SprPoP и остановился в `src/s при остановке в `hello/main` подтверждает DAP `terminated` после внезапной потери процесса. Штатный DAP launch в `hello/main` также повторно прошёл с `sdbg` и опциональным `osx` debugger provider. -Отдельная проба `SIGTERM` остановленному MAME не дала DAP `terminated` за -15 секунд; причина (обработка сигнала самим MAME или путь закрытия сессии) -ещё не установлена. Это отдельный OS-signal путь, не равный закрытию окна. +Первоначально прямой `SIGTERM` остановленному MAME не давал DAP `terminated`: +через 12 секунд процесс оставался живым. Причина: SDL3 по умолчанию +преобразует `SIGTERM` в `SDL_EVENT_QUIT`, а SDL3 OSD MAME это событие +не обрабатывает. Launcher теперь выставляет `SDL_NO_SIGNAL_HANDLERS=1` +только для своего MAME, если переменная не задана пользователем. Повторный +живой прогон подтвердил `SIGTERM` → DAP `terminated`; DAP `disconnect` +(VS Code Stop) завершил MAME и адаптер с provider `sdbg` и `osx`. +Отдельный патч MAME для этого +не требуется. Закрытие окна по-прежнему проверено только через тот же +`schedule_exit()` в исходнике MAME, без автоматического UI-клика. Живой lifecycle-пробник на банковой программе теперь прошёл с `sdbg` и опциональным `osx` provider. На остановке в `main` Lua вызвал @@ -344,8 +353,8 @@ Crash reports в 19:22/19:23 относились к неподдержанно строит проверенное условие через I/O port. После штатных прогонов в 20:43 и позже новых `mame.arm-*.ips` не появилось. -Далее по плану: исследовать отдельно OS-signal путь `SIGTERM`, сделать -MCP-адаптер, безопасный restart и расширенные выражения. VSIX уже упакован; +Далее по плану: сделать MCP-адаптер, безопасный restart и расширенные +выражения. VSIX уже упакован; для IDE ещё нужны Run-команда, выбор профиля сборки/данных и расширенная диагностика assembler/linker. Базовый attach уже проверяет принадлежность resident/current-bank к build, diff --git a/docs/mame-source-debug.md b/docs/mame-source-debug.md index 0cf712e..22f17c9 100644 --- a/docs/mame-source-debug.md +++ b/docs/mame-source-debug.md @@ -684,9 +684,13 @@ exit/reset/reload инвалидируют состояние; потеря кл Lua `machine:exit()` и реальный save/load state при остановке в `main`: оба завершают DAP, post-load удаляет старые точки. SDL3-событие закрытия окна использует тот же `schedule_exit()`; прямой UI-клик не автоматизировался. -Reset по-прежнему требует безопасного отдельного репро. `SIGTERM` -остановленному процессу MAME пока не дал DAP `terminated` за 15 секунд; -это отдельный OS-signal путь, не проверка штатного выхода. +Reset по-прежнему требует безопасного отдельного репро. Для запускаемого +через toolkit MAME launcher выставляет `SDL_NO_SIGNAL_HANDLERS=1`, если +переменная не задана пользователем: SDL3 иначе превращает `SIGTERM` в +`SDL_EVENT_QUIT`, который MAME SDL3 OSD игнорирует. Живые пробы подтвердили +прямой `SIGTERM` → DAP `terminated` и DAP `disconnect` → завершение MAME и +адаптера при остановке в `main`. Изменение действует только на MAME, +запущенный launcher; системный MAME без этого окружения ведёт себя по-прежнему. ### Этап 3 — полезная CLI/MCP-отладка и логи diff --git a/docs/vscode-sprinter-debug.md b/docs/vscode-sprinter-debug.md index c9644dc..4abaa5d 100644 --- a/docs/vscode-sprinter-debug.md +++ b/docs/vscode-sprinter-debug.md @@ -162,6 +162,12 @@ Launcher монтирует временную копию CHD как `-hard2`, 6. Завершите сессию кнопкой Stop. Затем повторите профиль с `osx`: вместе с тем же VS Code-сеансом должно открыться штатное Cocoa-окно debugger MAME. +Stop отправляет DAP `disconnect`; launcher завершает запущенный им MAME. +Чтобы SDL3 не перехватывал `SIGTERM` в необрабатываемое MAME событие Quit, +launcher задаёт этому процессу `SDL_NO_SIGNAL_HANDLERS=1`, если переменная +не была задана вручную. Прямой `SIGTERM` и Stop проверены живыми пробниками +на остановке в `main`; они не требуют отдельного патча MAME. + Команда палитры `Sprinter: Build Active Project` собирает приложение по Makefile открытого C-файла. Задачи `Sprinter: Build ...` доступны и в `Tasks: Run Task`. Для launch автоматическая сборка включена по умолчанию, diff --git a/tests/sdbg/run_vscode_dap_probe.py b/tests/sdbg/run_vscode_dap_probe.py index 18ff195..a06ddc2 100644 --- a/tests/sdbg/run_vscode_dap_probe.py +++ b/tests/sdbg/run_vscode_dap_probe.py @@ -93,6 +93,10 @@ def main(): parser.add_argument('--launch-path', default=None) parser.add_argument('--exit-while-stopped', action='store_true', help='аварийно завершить собственный MAME на main и ждать DAP terminated') + parser.add_argument('--stop-while-stopped', action='store_true', + help='послать DAP disconnect на main и проверить завершение MAME') + parser.add_argument('--term-while-stopped', action='store_true', + help='послать SIGTERM собственному MAME на main и проверить DAP terminated') options, _ = parser.parse_known_args() if not PYTHON.is_file(): raise RuntimeError('Нет local pyenv shim: '+str(PYTHON)) @@ -124,6 +128,50 @@ def main(): frame = frame_response['body']['stackFrames'][0] if frame['name'] != 'main': raise RuntimeError('Не main: '+str(frame)) + if options.stop_while_stopped: + mame_pid = launched['body']['mamePid'] + send(process, 5, 'disconnect') + _, buffer, _ = wait_response(process, buffer, 'disconnect', 10) + deadline = time.monotonic()+12 + while time.monotonic() < deadline: + try: + os.kill(mame_pid, 0) + except ProcessLookupError: + break + time.sleep(.1) + else: + raise RuntimeError('После DAP disconnect MAME остался запущен') + code = process.wait(timeout=3) + if code != 0: + raise RuntimeError(f'После DAP disconnect адаптер завершился с rc={code}') + print(json.dumps({'event': 'vscode_stop_closes_mame', 'frame': frame}, + ensure_ascii=False), flush=True) + return 0 + if options.term_while_stopped: + mame_pid = launched['body']['mamePid'] + os.kill(mame_pid, signal.SIGTERM) + deadline = time.monotonic()+12 + while time.monotonic() < deadline: + try: + message, buffer = receive(process, buffer, deadline-time.monotonic()) + except TimeoutError: + break + if message.get('event') == 'terminated': + print(json.dumps({'event': 'sigterm_terminates_dap', 'frame': frame}, + ensure_ascii=False), flush=True) + send(process, 5, 'disconnect') + wait_response(process, buffer, 'disconnect', 10) + return 0 + try: + os.kill(mame_pid, 0) + status = 'alive' + except ProcessLookupError: + status = 'exited' + process_state = subprocess.run( + ['ps', '-p', str(mame_pid), '-o', 'stat=,ppid=,comm='], + capture_output=True, text=True, check=False).stdout.strip() + raise RuntimeError('SIGTERM: DAP terminated не получен за 12 с; MAME '+ + status+'; ps='+repr(process_state)) if options.exit_while_stopped: os.kill(launched['body']['mamePid'], signal.SIGKILL) deadline = time.monotonic()+15 diff --git a/toolchain/sdbg_launcher.py b/toolchain/sdbg_launcher.py index b6b2878..c4d85e3 100644 --- a/toolchain/sdbg_launcher.py +++ b/toolchain/sdbg_launcher.py @@ -222,6 +222,10 @@ def main(): args.dss_timeout) session_id=uuid.uuid4().hex environment=dict(os.environ,SDBG_IPC_DIR=str(ipc),SDBG_SESSION_ID=session_id) + # SDL3 превращает SIGTERM в SDL_EVENT_QUIT, который MAME SDL3 OSD + # не обрабатывает. Оставляем стандартное действие сигнала, чтобы + # DAP Stop не ждал принудительного kill остановленного MAME. + environment.setdefault('SDL_NO_SIGNAL_HANDLERS','1') plugin_paths=[str(ROOT/'toolchain/mcp')] installed_plugins=profile.home.parent/'plugins' if profile.home else None if installed_plugins and installed_plugins.is_dir():