Завершать DAP при потере остановленного MAME
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Отладка исходников: реализация и результаты
|
# Отладка исходников: реализация и результаты
|
||||||
|
|
||||||
Дата: 2026-09-15. План: [mame-source-debug.md](mame-source-debug.md).
|
Дата: 2026-09-16. План: [mame-source-debug.md](mame-source-debug.md).
|
||||||
Реализованы сборка/карта, проверенный транспорт, базовая C-сессия,
|
Реализованы сборка/карта, проверенный транспорт, базовая C-сессия,
|
||||||
постоянный session server, DAP MVP, VS Code launch и build task. MCP C-уровня,
|
постоянный session server, DAP MVP, VS Code launch и build task. MCP C-уровня,
|
||||||
безопасный restart и расширенная отладка ещё не готовы.
|
безопасный restart и расширенная отладка ещё не готовы.
|
||||||
@@ -65,6 +65,11 @@
|
|||||||
логические группы и события. `set_source_breakpoints` и
|
логические группы и события. `set_source_breakpoints` и
|
||||||
`set_function_breakpoints` создают новый disabled-набор, при ошибке удаляют
|
`set_function_breakpoints` создают новый disabled-набор, при ошибке удаляют
|
||||||
его, затем заменяют прежний набор и активируют точки.
|
его, затем заменяют прежний набор и активируют точки.
|
||||||
|
- Session server проверяет `snapshot` и при остановленном CPU раз в 0,5 с:
|
||||||
|
reset/state load инвалидирует сессию, потеря MAME приводит к закрытию после
|
||||||
|
ограниченного timeout backend. Если выполнение продолжено из родного окна
|
||||||
|
debugger MAME, server сообщает DAP смену состояния. Закрытая сессия всегда
|
||||||
|
выдаёт DAP `terminated`, даже если событие `invalidated` выпало из журнала.
|
||||||
- DAP MVP и минимальное VS Code-расширение: attach к session server,
|
- DAP MVP и минимальное VS Code-расширение: attach к session server,
|
||||||
source/function breakpoints, один достоверный frame, registers,
|
source/function breakpoints, один достоверный frame, registers,
|
||||||
поддержанные globals/statics, evaluate одного имени, continue/pause и
|
поддержанные globals/statics, evaluate одного имени, continue/pause и
|
||||||
@@ -80,8 +85,9 @@
|
|||||||
для приложений с `app.mk`, команду Build Active Project и автоматическую
|
для приложений с `app.mk`, команду Build Active Project и автоматическую
|
||||||
debug-сборку перед F5. Она использует `make SRC_DEBUG=1` и local Python 3.12;
|
debug-сборку перед F5. Она использует `make SRC_DEBUG=1` и local Python 3.12;
|
||||||
ошибки SDCC с файлом/строкой попадают в Problems. Код сборки проверяется
|
ошибки SDCC с файлом/строкой попадают в Problems. Код сборки проверяется
|
||||||
самим расширением: при ошибке MAME не запускается. Run-команда, расширенные
|
самим расширением: при ошибке MAME не запускается. VSIX 0.2.0 собран и
|
||||||
linker/assembler diagnostics, выбор profile/EXTRA_DATA и VSIX остаются
|
проверен в отдельном workspace SprPoP. Run-команда, расширенные
|
||||||
|
linker/assembler diagnostics и выбор profile/EXTRA_DATA остаются
|
||||||
следующими шагами.
|
следующими шагами.
|
||||||
- DAP `logMessage` без пересборки: разрешены литералы, `{{`/`}}` и только
|
- DAP `logMessage` без пересборки: разрешены литералы, `{{`/`}}` и только
|
||||||
подстановки `{variable}`. Session server читает типизированное значение,
|
подстановки `{variable}`. Session server читает типизированное значение,
|
||||||
@@ -149,9 +155,8 @@ pyenv exec python toolchain/sdbg_session.py \
|
|||||||
Доступны `where`, `step`, `continue`, `break-line`, `break-function`,
|
Доступны `where`, `step`, `continue`, `break-line`, `break-function`,
|
||||||
`read-variable`, `activate-breakpoints` и `deactivate-breakpoints`.
|
`read-variable`, `activate-breakpoints` и `deactivate-breakpoints`.
|
||||||
Это диагностический однооперационный CLI: логические ID групп точек живут
|
Это диагностический однооперационный CLI: логические ID групп точек живут
|
||||||
только внутри процесса. Постоянное владение и DAP добавятся в общем session
|
только внутри процесса. Для долгой работы и DAP используйте общий session
|
||||||
server; до него для долгой ручной работы нужен один Python-процесс с
|
server ниже.
|
||||||
`DebugSession`.
|
|
||||||
|
|
||||||
Предпочтительный режим для нескольких клиентов — один server:
|
Предпочтительный режим для нескольких клиентов — один server:
|
||||||
|
|
||||||
@@ -165,7 +170,7 @@ pyenv exec python toolchain/sdbg_client.py \
|
|||||||
--socket /tmp/sprinter-sdbg.sock status
|
--socket /tmp/sprinter-sdbg.sock status
|
||||||
```
|
```
|
||||||
|
|
||||||
VS Code-расширение находится в `toolchain/vscode-sprinter-debug/`. В
|
VS Code-расширение находится в отдельном репозитории `../VSCode-Sprinter`. В
|
||||||
Extension Development Host используется attach-конфигурация:
|
Extension Development Host используется attach-конфигурация:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -177,9 +182,10 @@ Extension Development Host используется attach-конфигурац
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
В VS Code расширение `0.2.0` запускает адаптер через абсолютный путь к local
|
В VS Code расширение `0.2.0` находит SDK через `sprinterDebugger.sdkRoot`,
|
||||||
pyenv shim `~/.pyenv/shims/python` и задаёт корень workspace как `cwd`.
|
а Python 3.12 — через local pyenv shim или установленную pyenv-версию.
|
||||||
Это устраняет зависимость от `PATH` процесса VS Code, открытого из Dock.
|
Это устраняет зависимость от `PATH` процесса VS Code, открытого из Dock,
|
||||||
|
и позволяет отлаживать приложение из отдельного workspace.
|
||||||
Выбранные пути видны в Output → `Sprinter MAME Debug`; при раннем сбое
|
Выбранные пути видны в Output → `Sprinter MAME Debug`; при раннем сбое
|
||||||
launcher сообщает stderr и последние строки MAME log.
|
launcher сообщает stderr и последние строки MAME log.
|
||||||
Интегрированный запуск:
|
Интегрированный запуск:
|
||||||
@@ -213,6 +219,7 @@ pyenv exec python tests/sdbg/run_mame_probe.py --banked --launcher --native-debu
|
|||||||
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
|
||||||
pyenv exec python tests/sdbg/run_mame_probe.py --banked --launcher --step-out
|
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
|
||||||
|
pyenv exec python tests/sdbg/run_vscode_dap_probe.py --exit-while-stopped
|
||||||
pyenv exec python tests/sdbg/run_vscode_dap_probe.py --waitkey --manual-key
|
pyenv exec python tests/sdbg/run_vscode_dap_probe.py --waitkey --manual-key
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -256,8 +263,16 @@ commit `b0c4527c2edb1ee177fd09b3c412b65b385bf35b`:
|
|||||||
| Ошибочная debug-сборка, 2026-09-15 | Изолированный `fault.c` вернул код 2 от make, `file:1: error 20` сохранился, Python traceback удалён; MAME не участвует |
|
| Ошибочная debug-сборка, 2026-09-15 | Изолированный `fault.c` вернул код 2 от make, `file:1: error 20` сохранился, Python traceback удалён; MAME не участвует |
|
||||||
| Владение и generation | Второй FileBridge и чтение со stale generation отклоняются |
|
| Владение и generation | Второй FileBridge и чтение со stale generation отклоняются |
|
||||||
|
|
||||||
Полный host-набор: 29 тестов прошли, один Unix-socket тест пропущен только
|
Полный host-набор: 39 тестов (38 прошли, один Unix-socket тест пропущен только
|
||||||
из-за запрета `bind` в sandbox. Тот же socket path проверен живым DAP-запуском.
|
из-за запрета `bind` в sandbox). Тот же socket path проверен живым DAP-запуском.
|
||||||
|
После разделения репозиториев установленный VSIX в изолированном профиле
|
||||||
|
VS Code запустил сборку SprPoP и остановился в `src/sprpop.c:264` (`main`)
|
||||||
|
через API extension host. Новый живой пробник закрытия собственного MAME
|
||||||
|
при остановке в `hello/main` подтверждает DAP `terminated` после внезапной
|
||||||
|
потери процесса. Штатный DAP launch в `hello/main` также повторно прошёл
|
||||||
|
с `sdbg` и опциональным `osx` debugger provider.
|
||||||
|
Graceful-закрытие MAME при остановленном CPU и state load пока не прошли
|
||||||
|
отдельный живой тест.
|
||||||
|
|
||||||
Это репро доступности механизма, а не полный benchmark или проверка всех
|
Это репро доступности механизма, а не полный benchmark или проверка всех
|
||||||
runtime/mapping/lifecycle. Отдельная публикация debug-библиотек требует
|
runtime/mapping/lifecycle. Отдельная публикация debug-библиотек требует
|
||||||
@@ -280,9 +295,8 @@ runtime/mapping/lifecycle. Отдельная публикация debug-биб
|
|||||||
подтвердил, что при остановке в `main` окно снова открывается через
|
подтвердил, что при остановке в `main` окно снова открывается через
|
||||||
Cmd-Tab/Dock. Регрессионный DAP-проход через `getchar()` с клавишей `x`
|
Cmd-Tab/Dock. Регрессионный DAP-проход через `getchar()` с клавишей `x`
|
||||||
также завершился на следующей строке с кодом `0x78`.
|
также завершился на следующей строке с кодом `0x78`.
|
||||||
Воспроизводимый patch и идемпотентный установщик находятся в
|
Воспроизводимый patch, установщик и сборочные рецепты находятся в отдельном
|
||||||
`toolchain/mame-patches/` и `toolchain/apply-mame-sdbg-patch.sh`;
|
`../MAME/scripts/sprinter/`. Provider `osx` остаётся
|
||||||
`make mame-sdbg` собирает и устанавливает бинарник. Provider `osx` остаётся
|
|
||||||
доступной launch-опцией и проверен одновременно с DAP. Для непатченного MAME
|
доступной launch-опцией и проверен одновременно с DAP. Для непатченного MAME
|
||||||
можно использовать `auto`: штатные варианты — `osx` на macOS, `windows` в
|
можно использовать `auto`: штатные варианты — `osx` на macOS, `windows` в
|
||||||
native Windows build, `qt` или `imgui` в Linux. `qt` зависит от
|
native Windows build, `qt` или `imgui` в Linux. `qt` зависит от
|
||||||
@@ -299,12 +313,12 @@ host-моста пока не готова: FileBridge использует `fcn
|
|||||||
|
|
||||||
## Размерный регресс и оставшаяся работа
|
## Размерный регресс и оставшаяся работа
|
||||||
|
|
||||||
`make size-check` запущен: сообщает рост у 12 программ относительно текущего
|
`make size-check` теперь проходит (46 свежих карт). Прежние 12 расхождений
|
||||||
эталона, отсутствующие и новые сборки. В списке роста нет пересобранного
|
разобраны: `open()` добавил 9 байт после исправления режимов DSS;
|
||||||
hello. Дополнительно cat и hello собраны исходной обёрткой из HEAD и новой
|
`openenv` содержит ещё новый регрессионный тест; прежний размер `w3bgfx`
|
||||||
на тех же runtime/библиотеках: EXE попарно идентичны. Эталон не изменялся.
|
был снят по несвежей карте. Подробное сравнение с исходной ревизией и
|
||||||
Этот общий check пока не считается прошедшим; расхождение существующих
|
правила обновления эталона — в
|
||||||
сборок с baseline отделено от побайтовых регрессий новой debug-сборки.
|
[плане разделения](project-reorganization.md).
|
||||||
|
|
||||||
Hard reset из RPC был удалён после воспроизводимого падения MAME 0.287:
|
Hard reset из RPC был удалён после воспроизводимого падения MAME 0.287:
|
||||||
старый Lua periodic callback обращался к `debugger_manager` во время нового
|
старый Lua periodic callback обращался к `debugger_manager` во время нового
|
||||||
@@ -316,9 +330,9 @@ Crash reports в 19:22/19:23 относились к неподдержанно
|
|||||||
строит проверенное условие через I/O port. После штатных прогонов в 20:43 и
|
строит проверенное условие через I/O port. После штатных прогонов в 20:43 и
|
||||||
позже новых `mame.arm-*.ips` не появилось.
|
позже новых `mame.arm-*.ips` не появилось.
|
||||||
|
|
||||||
Далее по плану: завершить инвалидацию на exit/state load, MCP-адаптер,
|
Далее по плану: проверить graceful exit/state load живым репро, сделать
|
||||||
безопасный restart, расширенные выражения
|
MCP-адаптер, безопасный restart и расширенные выражения. VSIX уже упакован;
|
||||||
и упаковку VS Code-расширения. Для IDE ещё нужны Run-команда, выбор профиля
|
для IDE ещё нужны Run-команда, выбор профиля
|
||||||
сборки/данных и расширенная диагностика assembler/linker.
|
сборки/данных и расширенная диагностика assembler/linker.
|
||||||
Базовый attach уже проверяет принадлежность resident/current-bank к build,
|
Базовый attach уже проверяет принадлежность resident/current-bank к build,
|
||||||
но не умеет читать неотображённую physical RAM. Нет автоматического чтения
|
но не умеет читать неотображённую physical RAM. Нет автоматического чтения
|
||||||
|
|||||||
@@ -678,6 +678,11 @@ stepIn. Версионировать мост/патчи и установщик
|
|||||||
exit/reset/reload инвалидируют состояние; потеря клиента не блокирует MAME,
|
exit/reset/reload инвалидируют состояние; потеря клиента не блокирует MAME,
|
||||||
конкурентные операции не читают смешанный snapshot.
|
конкурентные операции не читают смешанный snapshot.
|
||||||
|
|
||||||
|
Текущая реализация опрашивает backend и при остановленном CPU: потеря MAME
|
||||||
|
переводит DAP в `terminated` (проверено живым пробником с внезапным выходом).
|
||||||
|
Reset/state load обработаны notifier плагина, инвалидация проверена host-тестом;
|
||||||
|
штатное закрытие MAME и загрузка state требуют отдельных живых проверок.
|
||||||
|
|
||||||
### Этап 3 — полезная CLI/MCP-отладка и логи
|
### Этап 3 — полезная CLI/MCP-отладка и логи
|
||||||
|
|
||||||
Внешние точки/условия/логи, диспетчер совпадений, базовые типы, чтение/запись
|
Внешние точки/условия/логи, диспетчер совпадений, базовые типы, чтение/запись
|
||||||
|
|||||||
@@ -99,6 +99,12 @@ service-точка `main` и вводится `a:\\HELLO.EXE`. После сов
|
|||||||
а VS Code показывает остановку на entry. Завершение debug session останавливает
|
а VS Code показывает остановку на entry. Завершение debug session останавливает
|
||||||
только созданные ей MAME/server.
|
только созданные ей MAME/server.
|
||||||
|
|
||||||
|
При reset/state load или потере MAME остановленная сессия становится
|
||||||
|
недействительной и DAP завершает её; для новой отладки запускайте F5 заново.
|
||||||
|
Проверен сценарий внезапного завершения собственного MAME во время остановки
|
||||||
|
в `main`. Отдельный живой тест штатного закрытия окна и загрузки state ещё
|
||||||
|
предстоит выполнить.
|
||||||
|
|
||||||
`dssTimeout` задаёт предельное время ожидания prompt (30 эмулируемых секунд).
|
`dssTimeout` задаёт предельное время ожидания prompt (30 эмулируемых секунд).
|
||||||
`launchAt` можно задать как необязательную нижнюю границу времени запуска;
|
`launchAt` можно задать как необязательную нижнюю границу времени запуска;
|
||||||
наличие prompt всё равно обязательно. Перед вводом сохраняется диагностический
|
наличие prompt всё равно обязательно. Перед вводом сохраняется диагностический
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import select
|
import select
|
||||||
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -90,6 +91,8 @@ def main():
|
|||||||
parser.add_argument('--build', default=None)
|
parser.add_argument('--build', default=None)
|
||||||
parser.add_argument('--app-hdd', default=None)
|
parser.add_argument('--app-hdd', default=None)
|
||||||
parser.add_argument('--launch-path', default=None)
|
parser.add_argument('--launch-path', default=None)
|
||||||
|
parser.add_argument('--exit-while-stopped', action='store_true',
|
||||||
|
help='аварийно завершить собственный MAME на main и ждать DAP terminated')
|
||||||
options, _ = parser.parse_known_args()
|
options, _ = parser.parse_known_args()
|
||||||
if not PYTHON.is_file():
|
if not PYTHON.is_file():
|
||||||
raise RuntimeError('Нет local pyenv shim: '+str(PYTHON))
|
raise RuntimeError('Нет local pyenv shim: '+str(PYTHON))
|
||||||
@@ -121,6 +124,18 @@ def main():
|
|||||||
frame = frame_response['body']['stackFrames'][0]
|
frame = frame_response['body']['stackFrames'][0]
|
||||||
if frame['name'] != 'main':
|
if frame['name'] != 'main':
|
||||||
raise RuntimeError('Не main: '+str(frame))
|
raise RuntimeError('Не main: '+str(frame))
|
||||||
|
if options.exit_while_stopped:
|
||||||
|
os.kill(launched['body']['mamePid'], signal.SIGKILL)
|
||||||
|
deadline = time.monotonic()+15
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
message, buffer = receive(process, buffer, deadline-time.monotonic())
|
||||||
|
if message.get('event') == 'terminated':
|
||||||
|
print(json.dumps({'event': 'idle_mame_exit_terminates_dap',
|
||||||
|
'frame': frame}, ensure_ascii=False), flush=True)
|
||||||
|
send(process, 5, 'disconnect')
|
||||||
|
wait_response(process, buffer, 'disconnect', 10)
|
||||||
|
return 0
|
||||||
|
raise RuntimeError('После закрытия остановленного MAME нет DAP terminated')
|
||||||
if '--waitkey' in sys.argv:
|
if '--waitkey' in sys.argv:
|
||||||
send(process, 5, 'setBreakpoints', {
|
send(process, 5, 'setBreakpoints', {
|
||||||
'source': {'path': str(ROOT/'tests/hello/hello.c')},
|
'source': {'path': str(ROOT/'tests/hello/hello.c')},
|
||||||
|
|||||||
@@ -106,6 +106,14 @@ class DapTests(unittest.TestCase):
|
|||||||
self.assertIn('пропущено 2', events[0][1]['output'])
|
self.assertIn('пропущено 2', events[0][1]['output'])
|
||||||
self.assertEqual(events[1][1]['output'], 'total=42\n')
|
self.assertEqual(events[1][1]['output'], 'total=42\n')
|
||||||
|
|
||||||
|
def test_closed_session_always_terminates_dap(self):
|
||||||
|
self.dap._rpc_override = lambda method, args: {
|
||||||
|
'events': [], 'first': 1, 'last': 0, 'closed': True,
|
||||||
|
}
|
||||||
|
events, closed = self.dap.poll_events(timeout=0)
|
||||||
|
self.assertTrue(closed)
|
||||||
|
self.assertEqual(events, [('terminated', {'restart': False})])
|
||||||
|
|
||||||
def test_stdio_framing_without_attach(self):
|
def test_stdio_framing_without_attach(self):
|
||||||
requests = [
|
requests = [
|
||||||
{'seq': 1, 'type': 'request', 'command': 'initialize', 'arguments': {}},
|
{'seq': 1, 'type': 'request', 'command': 'initialize', 'arguments': {}},
|
||||||
|
|||||||
@@ -18,10 +18,13 @@ class DummyModel:
|
|||||||
|
|
||||||
|
|
||||||
class DummyBridge:
|
class DummyBridge:
|
||||||
def __init__(self): self.calls = []
|
def __init__(self):
|
||||||
|
self.calls = []
|
||||||
|
self.state = 'stopped'
|
||||||
def close(self): pass
|
def close(self): pass
|
||||||
def request(self, command, **arguments):
|
def request(self, command, **arguments):
|
||||||
self.calls.append((command, arguments))
|
self.calls.append((command, arguments))
|
||||||
|
if command == 'snapshot': return {'state': self.state}
|
||||||
if command == 'console_print': return {'printed': True}
|
if command == 'console_print': return {'printed': True}
|
||||||
raise AssertionError(command)
|
raise AssertionError(command)
|
||||||
|
|
||||||
@@ -37,6 +40,10 @@ class DummySession:
|
|||||||
def attach(self):
|
def attach(self):
|
||||||
return {'build_id': 'test', 'location': {'status': 'mapped'}}
|
return {'build_id': 'test', 'location': {'status': 'mapped'}}
|
||||||
|
|
||||||
|
def where(self, snapshot=None):
|
||||||
|
return {'status': 'mapped', 'pc': 0x8100, 'link_address': 0x8100,
|
||||||
|
'sources': [{'file': '/src/main.c', 'line': 3}]}
|
||||||
|
|
||||||
def break_line(self, filename, line, enabled=True):
|
def break_line(self, filename, line, enabled=True):
|
||||||
if line == 99:
|
if line == 99:
|
||||||
raise SessionError('нет адреса')
|
raise SessionError('нет адреса')
|
||||||
@@ -131,6 +138,7 @@ class WaitingBridge:
|
|||||||
"""Машинный over ожидает внешний ввод, но pause должен остаться доступен."""
|
"""Машинный over ожидает внешний ввод, но pause должен остаться доступен."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.paused = False
|
self.paused = False
|
||||||
|
self.started = False
|
||||||
self.calls = []
|
self.calls = []
|
||||||
|
|
||||||
def close(self): pass
|
def close(self): pass
|
||||||
@@ -138,11 +146,12 @@ class WaitingBridge:
|
|||||||
def request(self, command, **arguments):
|
def request(self, command, **arguments):
|
||||||
self.calls.append(command)
|
self.calls.append(command)
|
||||||
if command == 'snapshot':
|
if command == 'snapshot':
|
||||||
return {'state': 'stopped' if self.paused else 'running'}
|
return {'state': 'running' if self.started and not self.paused else 'stopped'}
|
||||||
if command == 'pause':
|
if command == 'pause':
|
||||||
self.paused = True
|
self.paused = True
|
||||||
return {'accepted': True}
|
return {'accepted': True}
|
||||||
if command == 'step_over':
|
if command == 'step_over':
|
||||||
|
self.started = True
|
||||||
return {'accepted': True}
|
return {'accepted': True}
|
||||||
raise AssertionError(command)
|
raise AssertionError(command)
|
||||||
|
|
||||||
@@ -180,6 +189,37 @@ class ServerTests(unittest.TestCase):
|
|||||||
server.close()
|
server.close()
|
||||||
controller.close()
|
controller.close()
|
||||||
|
|
||||||
|
def test_idle_snapshot_detects_invalidation(self):
|
||||||
|
session = DummySession()
|
||||||
|
controller = SessionController(session)
|
||||||
|
try:
|
||||||
|
session.bridge.state = 'invalidated'
|
||||||
|
with controller.changed:
|
||||||
|
self.assertTrue(controller.changed.wait_for(lambda: controller.closed,
|
||||||
|
timeout=2))
|
||||||
|
self.assertEqual(controller.events[-1]['event'], 'invalidated')
|
||||||
|
self.assertEqual(controller.events[-1]['body']['reason'], 'reset_or_load')
|
||||||
|
finally:
|
||||||
|
controller.close()
|
||||||
|
|
||||||
|
def test_native_debugger_continue_is_reported(self):
|
||||||
|
session = DummySession()
|
||||||
|
controller = SessionController(session)
|
||||||
|
try:
|
||||||
|
session.bridge.state = 'running'
|
||||||
|
with controller.changed:
|
||||||
|
self.assertTrue(controller.changed.wait_for(lambda: controller.running,
|
||||||
|
timeout=2))
|
||||||
|
self.assertEqual(controller.events[-1]['event'], 'continued')
|
||||||
|
self.assertEqual(controller.events[-1]['body']['reason'], 'external')
|
||||||
|
session.bridge.state = 'stopped'
|
||||||
|
with controller.changed:
|
||||||
|
self.assertTrue(controller.changed.wait_for(lambda: not controller.running,
|
||||||
|
timeout=2))
|
||||||
|
self.assertEqual(controller.events[-1]['event'], 'stopped')
|
||||||
|
finally:
|
||||||
|
controller.close()
|
||||||
|
|
||||||
def test_source_breakpoints_replace_and_rollback(self):
|
def test_source_breakpoints_replace_and_rollback(self):
|
||||||
session = DummySession()
|
session = DummySession()
|
||||||
controller = SessionController(session)
|
controller = SessionController(session)
|
||||||
|
|||||||
@@ -140,6 +140,8 @@ class DapEngine:
|
|||||||
elif event['event'] == 'output':
|
elif event['event'] == 'output':
|
||||||
translated.append(('output', {'category': event['body'].get('category','console'),
|
translated.append(('output', {'category': event['body'].get('category','console'),
|
||||||
'output': event['body']['output']}))
|
'output': event['body']['output']}))
|
||||||
|
if result['closed'] and not any(name == 'terminated' for name, _ in translated):
|
||||||
|
translated.append(('terminated', {'restart': False}))
|
||||||
return translated, result['closed']
|
return translated, result['closed']
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -72,14 +72,18 @@ class SessionController:
|
|||||||
self.changed.notify_all()
|
self.changed.notify_all()
|
||||||
|
|
||||||
def _poll(self) -> None:
|
def _poll(self) -> None:
|
||||||
|
next_idle_snapshot = 0.0
|
||||||
while not self.closed:
|
while not self.closed:
|
||||||
if not self.running:
|
if not self.running:
|
||||||
time.sleep(.02)
|
delay = next_idle_snapshot - time.monotonic()
|
||||||
|
if delay > 0:
|
||||||
|
time.sleep(min(delay, .05))
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
snapshot = self.session.bridge.request('snapshot')
|
snapshot = self.session.bridge.request('snapshot')
|
||||||
if snapshot['state'] == 'stopped':
|
if snapshot['state'] == 'stopped':
|
||||||
|
if self.running:
|
||||||
location = self.session.where(snapshot)
|
location = self.session.where(snapshot)
|
||||||
if self.source_step is not None:
|
if self.source_step is not None:
|
||||||
self._source_step_stopped(location)
|
self._source_step_stopped(location)
|
||||||
@@ -89,11 +93,17 @@ class SessionController:
|
|||||||
else:
|
else:
|
||||||
self.running = False
|
self.running = False
|
||||||
self._emit('stopped', {'reason': 'breakpoint', 'location': location})
|
self._emit('stopped', {'reason': 'breakpoint', 'location': location})
|
||||||
|
elif snapshot['state'] == 'running' and not self.running:
|
||||||
|
# Команда из родного окна MAME тоже меняет состояние CPU.
|
||||||
|
self.running = True
|
||||||
|
self._emit('continued', {'reason': 'external'})
|
||||||
elif snapshot['state'] == 'invalidated':
|
elif snapshot['state'] == 'invalidated':
|
||||||
self.running = False
|
self.running = False
|
||||||
self.source_step = None
|
self.source_step = None
|
||||||
self.closed = True
|
self.closed = True
|
||||||
self._emit('invalidated', {'reason': 'reset_or_load'})
|
self._emit('invalidated', {'reason': 'reset_or_load'})
|
||||||
|
if not self.running:
|
||||||
|
next_idle_snapshot = time.monotonic() + .5
|
||||||
except (BridgeError, SessionError, ValueError, OSError) as error:
|
except (BridgeError, SessionError, ValueError, OSError) as error:
|
||||||
self.running = False
|
self.running = False
|
||||||
self.source_step = None
|
self.source_step = None
|
||||||
|
|||||||
Reference in New Issue
Block a user