Add asynchronous machine over and out to shared MCP
This commit is contained in:
+3
-1
@@ -280,7 +280,7 @@ Quick wins:
|
||||
## Прочий backlog
|
||||
|
||||
- [ ] **Довести MCP C-уровня до полного безопасного контракта.** Первый
|
||||
stdio-адаптер с 29 инструментами использует общую sdbg-сессию и
|
||||
stdio-адаптер с 31 инструментом использует общую sdbg-сессию и
|
||||
owner ID личных точек. Полный план и матрица всех 30 raw-возможностей —
|
||||
[mcp-convergence-plan.md](mcp-convergence-plan.md).
|
||||
control lease для DAP/MCP и cleanup точек по истечении heartbeat
|
||||
@@ -288,6 +288,8 @@ Quick wins:
|
||||
экранные API, одиночный `press_key` и читающий список C-точек уже
|
||||
добавлены; logical Z80 disassembly и ограниченный `type_string`
|
||||
проверены, машинный `step_instruction(count=1..64)` проверен на `hello`.
|
||||
Машинные `over(count)`/`out` добавлены отдельно от C source-step;
|
||||
они асинхронны, поскольку могут ждать пользовательский ввод.
|
||||
Далее natural keyboard, управляемые поля портов и мышь,
|
||||
затем доказанные
|
||||
watchpoints, запись typed values, disassemble_src и batch-загрузка
|
||||
|
||||
@@ -125,7 +125,7 @@ stdio MCP-адаптер C-уровня. Эксклюзивное владени
|
||||
проверяет сигнатуру entry и запускает session server. Закрытие DAP
|
||||
завершает только созданные им server/MAME, удаляет временный каталог и
|
||||
свой Unix socket (с проверкой inode, чтобы не удалить новый сеанс).
|
||||
- `sdbg_mcp.py` предоставляет 29 инструментов через официальный MCP SDK
|
||||
- `sdbg_mcp.py` предоставляет 31 инструмент через официальный MCP SDK
|
||||
2.x поверх того же session server: статус, C-позиция, регистры, logical
|
||||
memory, переменные, события/логи, личные точки и команды исполнения.
|
||||
MCP-точки имеют owner ID и не могут удалить точки VS Code; при обычном
|
||||
@@ -136,7 +136,7 @@ stdio MCP-адаптер C-уровня. Эксклюзивное владени
|
||||
шага после клавиши.
|
||||
Повторный запуск на том же фиксированном socket после прежнего stale-файла
|
||||
прошёл; после Stop путь удалён.
|
||||
В автономном режиме доступен 31 инструмент: официальный MCP-клиент
|
||||
В автономном режиме доступны 33 инструмента: официальный MCP-клиент
|
||||
запустил `hello`, ввёл `x` через публичный `press_key` во время `getchar()`
|
||||
и остановился на следующей C-строке. Остальные raw-возможности ввода
|
||||
перечислены в [матрице](mcp-capability-matrix.md).
|
||||
@@ -298,6 +298,7 @@ commit `b0c4527c2edb1ee177fd09b3c412b65b385bf35b`:
|
||||
| Logical Z80 disassembly MCP, 2026-09-17 | Через официальный MCP-клиент в видимом MAME прочитаны инструкции `main` с `0x8224` (`call $852C`); байты памяти до/после совпали. Адрес/длина ограничены, возвращаются bank_pages и generation. Всего 30 автономных инструментов; raw program disassembly ещё не поддержано |
|
||||
| Строковый ввод MCP, 2026-09-17 | `tests/gets` с C-картой в видимом MAME получил `Ab9` и Enter через один MCP `type_string`; скриншот показал ввод и печать строки, CPU остановился на строке 16 после `gets()`. 31 автономный инструмент; natural keyboard/мышь остаются в плане |
|
||||
| Счётчик машинных шагов MCP, 2026-09-17 | На видимом `hello` официальный MCP-клиент передал `step_instruction(count=3)`, получил следующую остановку и затем довёл приложение до `getchar()` и выхода; диапазон 1..64 проверяется клиентом, сервером и Lua backend |
|
||||
| Машинные over/out MCP, 2026-09-17 | На видимом `hello` `over 1` перешагнул call пролога, `over count` дошёл до следующего call, `out` из `_gettextmode` вернулся в caller. `out` из `___sdcc_enter_ix` ждать нельзя: helper делает tail-jump `jp (hl)`; отдельный MCP-прогон получил асинхронный ответ за 8 мс и успешно прервал его Pause. Исправлена передача десятичных count для MAME console (`#`) и длины `dasm` |
|
||||
| F10 через `getchar()`, 2026-09-15 | Асинхронный `next` ответил за 21,49 мс; при ручном `x` в окне MAME `hello.c:62` → `:63`, возврат `DE.low=0x78`; обе клавиатуры включены в изолированном cfg |
|
||||
| VS Code TaskProvider Build, 2026-09-15 | Одиннадцать Node-проверок: выбор Makefile рядом с пакетом/в `build/`, команда make с Python shim, SDCC matcher, успешный и неуспешный код задачи, pyenv вне workspace; реальный make в `tests/hello` прошёл, VS Code показал Build → MAME/DSS → `main` |
|
||||
| Ошибочная debug-сборка, 2026-09-15 | Изолированный `fault.c` вернул код 2 от make, `file:1: error 20` сохранился, Python traceback удалён; MAME не участвует |
|
||||
|
||||
@@ -536,7 +536,7 @@ console_log и загрузка набора точек. Возвращать bu
|
||||
в арбитраж, а не обходят его.
|
||||
|
||||
Первый stdio MCP-адаптер уже работает через общий session server и отдаёт
|
||||
29 проверенных C-инструментов (31 при автономном start/stop). У личных MCP-точек owner ID; удаление чужой
|
||||
31 проверенный C-инструмент (33 при автономном start/stop). У личных MCP-точек owner ID; удаление чужой
|
||||
DAP-точки отклоняется. Совместный живой прогон подтвердил чтение состояния,
|
||||
точки и последующее срабатывание DAP-точки. Добавлен control lease для
|
||||
команд CPU и ввода; передача MCP→DAP и cleanup точки владельца без heartbeat
|
||||
@@ -546,6 +546,8 @@ VRAM доступны через общий bridge, что подтвержде
|
||||
ввёл `Ab9` и Enter в `gets()` с остановкой после печати строки;
|
||||
машинный `step_instruction` принимает ограниченный `count` 1..64, не меняя
|
||||
семантику DAP source-step;
|
||||
машинные `step_over_instruction(count)` и `step_out_instruction()` выделены
|
||||
отдельно от C-шагов и возвращают принятие команды асинхронно, сохраняя Pause;
|
||||
остальные интерфейсы этого раздела ещё не реализованы.
|
||||
Автономный MCP `start_session` поднимает DSS→EXE→`main` без VS Code;
|
||||
DAP attach к созданному socket и последующий `stop_session` проверены.
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
| `set_watchpoint` | — | Нужны owner, space/access, bank guard и проверка PC при попадании |
|
||||
| `clear_watchpoint` | — | Зависит от owner-модели watchpoint |
|
||||
| `step` | `step_instruction` | Проверено на `hello` с `count=3`: 1..64 машинных инструкций при stop, общий control lease и событие остановки; при breakpoint возможно раннее завершение |
|
||||
| `step_over` | `step_source(kind="over")` | Проверен C-шаг F10; raw машинный `over count` пока отличается |
|
||||
| `step_out` | `step_source(kind="out")` | Проверен C-выход; raw выход из одного машинного frame отличается |
|
||||
| `step_over` | `step_over_instruction` | Проверено на `hello`: машинный `over count=1..64` отдельно от C-F10, асинхронное завершение через события; длинный вызов можно прервать Pause |
|
||||
| `step_out` | `step_out_instruction` | Проверено на возвращаемом `_gettextmode`: машинный `out` отдельно от C Shift+F11, асинхронное завершение. Вызов через tail-jump (`___sdcc_enter_ix`) может длиться до следующего RET; нужен Pause |
|
||||
| `resume` | `continue_execution` | Проверено с control lease и DAP-событиями |
|
||||
| `pause` | `pause_execution` | Проверено с control lease |
|
||||
| `status` | `session_status`, `where` | Проверено; C-статус содержит session/build/generation |
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
`MAME.HT/plugins/mamebridge` вместе с `src/mame_mcp.py` публикует 30
|
||||
raw-инструментов. Удалённый на этапе А `src/mame_bridge.lua` использовал тот
|
||||
же Python frontend, но обрабатывал только 16 команд и не отвечал при hard-stop.
|
||||
`toolchain/sdbg_mcp.py` публикует 29 C-инструментов через общую с DAP сессию
|
||||
(31 в автономном режиме с командами start/stop).
|
||||
`toolchain/sdbg_mcp.py` публикует 31 C-инструмент через общую с DAP сессию
|
||||
(33 в автономном режиме с командами start/stop).
|
||||
**Цель — покрыть возможности всех 30 raw-инструментов в C-сессии, а не
|
||||
механически скопировать имена и небезопасную семантику.** Полный raw MCP
|
||||
остаётся отдельным режимом для задач без C-пакета и других машин MAME.
|
||||
@@ -169,6 +169,17 @@ C-строке. Это покрывает типовой строковый вв
|
||||
сохраняет control lease и событие остановки. Живой автономный MCP-прогон
|
||||
выполнил `count=3` в `main`, затем продолжил `hello` до `getchar()` и вышел
|
||||
через клавишу. DAP source-step не менялся.
|
||||
Отдельные машинные `step_over_instruction(count=1..64)` и
|
||||
`step_out_instruction()` теперь асинхронны: MAME-команда может ждать ввод,
|
||||
поэтому RPC не держит lock до остановки; события и Pause остаются доступны.
|
||||
Живой `hello` подтвердил `over` через call, счётчик `over` до второго call и
|
||||
`out` из возвращаемой функции `_gettextmode`. Вызов `out` из
|
||||
`___sdcc_enter_ix` выявил tail-jump вместо RET и не останавливался быстро:
|
||||
именно поэтому машинные over/out не должны быть синхронными. Отдельный живой
|
||||
MCP-прогон получил ответ `out` за 8 мс и остановил долгий выход через Pause.
|
||||
Консоль MAME
|
||||
считает числа без префикса шестнадцатеричными; для `over count` и длины
|
||||
`dasm` backend теперь передаёт явный десятичный префикс `#`.
|
||||
|
||||
## Г. Сделать запуск пригодным для Codex и Claude без VS Code
|
||||
|
||||
|
||||
+11
-3
@@ -82,7 +82,8 @@ MCP-сервером MAME и удаляет socket. Закрытие самог
|
||||
`list_breakpoints`, `clear_breakpoint`, `clear_my_breakpoints`,
|
||||
`claim_control`, `release_control`,
|
||||
`continue_execution`,
|
||||
`pause_execution`, `step_instruction`, `step_source`. Ответы SDK 2.x содержат
|
||||
`pause_execution`, `step_instruction`, `step_over_instruction`,
|
||||
`step_out_instruction`, `step_source`. Ответы SDK 2.x содержат
|
||||
`structuredContent`. `step_source` принимает `into`, `over` или `out` и
|
||||
возвращает принятие команды; итоговую остановку получите через
|
||||
`recent_events` с курсором `last`. Логи `SDBG_LOG` приходят как события
|
||||
@@ -90,9 +91,16 @@ MCP-сервером MAME и удаляет socket. Закрытие самог
|
||||
`step_instruction(count)` синхронно выполняет 1..64 машинных шагов
|
||||
(`count=1` по умолчанию) и возвращает позицию остановки. Если раньше
|
||||
сработает breakpoint, выполнение может остановиться до заданного числа.
|
||||
Команды DAP F11/F10/Shift+F11 сохраняют прежнюю C-семантику.
|
||||
`step_over_instruction(count=1..64)` и `step_out_instruction()` дают именно
|
||||
машинную семантику MAME. Они отвечают сразу `accepted`; итоговую остановку
|
||||
получайте через `recent_events` или `session_status`, при долгом вызове
|
||||
доступен `pause_execution`. Например, `out` из `___sdcc_enter_ix` не
|
||||
останавливается быстро: этот helper возвращает управление через `jp (hl)`,
|
||||
а не через `ret`. Живой тест проверил `over` через такой вызов и `out` из
|
||||
возвращаемого `_gettextmode`. Команды DAP F11/F10/Shift+F11 сохраняют
|
||||
прежнюю C-семантику.
|
||||
|
||||
Число 29 в attach-режиме (31 в автономном с `start_session`/`stop_session`)
|
||||
Число 31 в attach-режиме (33 в автономном с `start_session`/`stop_session`)
|
||||
не означает полного переноса более нового `-plugin mamebridge`:
|
||||
его `src/mame_mcp.py` публикует 30 raw-инструментов, ориентированных на
|
||||
машину MAME. Здесь инструменты сгруппированы по операциям C-сессии, а
|
||||
|
||||
@@ -175,7 +175,7 @@ launcher задаёт этому процессу `SDL_NO_SIGNAL_HANDLERS=1`, е
|
||||
фиксированный `"socket": "/tmp/sprinter-sdbg-hello.sock"` и подключите
|
||||
`toolchain/sdbg_mcp.py` после остановки в `main`. MCP использует тот же
|
||||
session server; его личные точки не заменяют точки VS Code. Порядок запуска,
|
||||
29 доступных инструментов и ограничения совместного управления описаны в
|
||||
31 доступный инструмент и ограничения совместного управления описаны в
|
||||
[руководстве по MCP](sdbg-mcp.md).
|
||||
|
||||
Команда палитры `Sprinter: Build Active Project` собирает приложение по
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Живой MCP: долгий машинный out из tail-jump можно прервать Pause."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from mcp import Client, StdioServerParameters
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
async def probe() -> None:
|
||||
home = Path(os.environ['MAME_HOME']).resolve()
|
||||
binary = Path(os.environ.get('MAME_BIN', home / 'sprinter')).resolve()
|
||||
package = ROOT / 'tests/hello/.sprinter-cc-hello'
|
||||
with tempfile.TemporaryDirectory(prefix='sprinter-out-mcp-', dir='/tmp') as temp:
|
||||
parameters = StdioServerParameters(
|
||||
command=sys.executable,
|
||||
args=[str(ROOT / 'toolchain/sdbg_mcp.py'), '--build', str(package),
|
||||
'--socket', str(Path(temp) / 'session.sock'),
|
||||
'--mame-home', str(home), '--mame-bin', str(binary)])
|
||||
async with Client(parameters) as client:
|
||||
async def call(name: str, arguments: dict | None = None) -> dict:
|
||||
result = await client.call_tool(name, arguments or {})
|
||||
if result.is_error or result.structured_content is None:
|
||||
raise RuntimeError(name + ': ' + repr(result.content))
|
||||
return result.structured_content
|
||||
|
||||
await call('start_session')
|
||||
try:
|
||||
deadline = time.monotonic() + 90
|
||||
while time.monotonic() < deadline:
|
||||
status = await call('session_status')
|
||||
if status['phase'] == 'ready':
|
||||
break
|
||||
if status['phase'] in ('failed', 'stopped'):
|
||||
raise RuntimeError('Launcher: ' + repr(status))
|
||||
await asyncio.sleep(.2)
|
||||
else:
|
||||
raise TimeoutError('hello не дошёл до main')
|
||||
entered = await call('step_instruction', {'count': 1})
|
||||
code = await call('disassemble_logical',
|
||||
{'address': hex(entered['pc']), 'length': 12})
|
||||
if 'jp (hl)' not in code['text']:
|
||||
raise RuntimeError('Ожидался tail-jump helper: ' + code['text'])
|
||||
started_at = time.monotonic()
|
||||
accepted = await call('step_out_instruction')
|
||||
response_ms = round((time.monotonic() - started_at) * 1000, 1)
|
||||
if not accepted['accepted'] or response_ms > 3000:
|
||||
raise RuntimeError('Машинный out не ответил быстро: ' + repr(accepted))
|
||||
await asyncio.sleep(.3)
|
||||
if not (await call('session_status'))['running']:
|
||||
raise RuntimeError('Tail-jump out неожиданно завершился')
|
||||
paused = await call('pause_execution')
|
||||
if (await call('session_status'))['running']:
|
||||
raise RuntimeError('Pause не остановил долгий out')
|
||||
print(json.dumps({'event': 'machine_out_pause_verified',
|
||||
'response_ms': response_ms,
|
||||
'entered_pc': entered['pc'],
|
||||
'paused_pc': paused['pc']}, ensure_ascii=False),
|
||||
flush=True)
|
||||
finally:
|
||||
await call('stop_session')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(probe())
|
||||
@@ -109,8 +109,43 @@ async def probe() -> None:
|
||||
after_dap = await call('session_status')
|
||||
if after_dap['phase'] != 'ready' or after_dap['session_id'] != status['session_id']:
|
||||
raise RuntimeError('DAP disconnect завершил MCP-owned MAME')
|
||||
lines = disassembly['text'].splitlines()
|
||||
addresses = [int(line.split(':', 1)[0], 16) for line in lines]
|
||||
calls = [index for index, line in enumerate(lines)
|
||||
if ' call ' in ' ' + line.lower() + ' ']
|
||||
if len(calls) < 2 or calls[0] != 0 or calls[1] <= 1:
|
||||
raise RuntimeError('Нет двух call в main: ' + disassembly['text'])
|
||||
|
||||
async def wait_machine_stop() -> dict:
|
||||
deadline = time.monotonic() + 10
|
||||
while time.monotonic() < deadline:
|
||||
if not (await call('session_status'))['running']:
|
||||
return await call('where')
|
||||
await asyncio.sleep(.05)
|
||||
paused = await call('pause_execution')
|
||||
raise TimeoutError('Машинный шаг не завершился; Pause PC=' +
|
||||
hex(paused['pc']))
|
||||
|
||||
await call('step_over_instruction', {'count': 1})
|
||||
after_over = await wait_machine_stop()
|
||||
if after_over['pc'] != addresses[1]:
|
||||
raise RuntimeError('over call не остановился после вызова: ' +
|
||||
repr(after_over))
|
||||
await call('step_over_instruction', {'count': calls[1] - 1})
|
||||
at_call = await wait_machine_stop()
|
||||
if at_call['pc'] != addresses[calls[1]]:
|
||||
raise RuntimeError('over count не дошёл до второго call: ' +
|
||||
repr(at_call))
|
||||
entered = await call('step_instruction', {'count': 1})
|
||||
if entered['pc'] == at_call['pc']:
|
||||
raise RuntimeError('Машинный step не вошёл в возвращаемый вызов')
|
||||
await call('step_out_instruction')
|
||||
returned = await wait_machine_stop()
|
||||
if returned['pc'] != addresses[calls[1] + 1]:
|
||||
raise RuntimeError('Машинный out не вернулся после call: ' +
|
||||
repr(returned))
|
||||
stepped = await call('step_instruction', {'count': 3})
|
||||
if stepped['pc'] == location['pc'] or \
|
||||
if stepped['pc'] == returned['pc'] or \
|
||||
(await call('session_status'))['running']:
|
||||
raise RuntimeError('Три машинных шага не остановились: ' + repr(stepped))
|
||||
source = str(ROOT / 'tests/hello/hello.c')
|
||||
|
||||
@@ -64,6 +64,15 @@ class McpAdapterTests(unittest.TestCase):
|
||||
self.client.step_instruction(3)
|
||||
self.assertEqual(self.calls[-1][1], 'step')
|
||||
self.assertEqual(self.calls[-1][2]['count'], 3)
|
||||
self.client.step_over_instruction(2)
|
||||
self.assertEqual(self.calls[-1][1], 'step_over_instruction')
|
||||
self.assertEqual(self.calls[-1][2]['count'], 2)
|
||||
self.client.step_out_instruction()
|
||||
self.assertEqual(self.calls[-1][1], 'step_out_instruction')
|
||||
before = len(self.calls)
|
||||
with self.assertRaisesRegex(SessionError, '1..64'):
|
||||
self.client.step_over_instruction(0)
|
||||
self.assertEqual(len(self.calls), before)
|
||||
|
||||
def test_press_key_releases_shift_after_snapshot_error(self):
|
||||
calls = []
|
||||
|
||||
@@ -118,7 +118,7 @@ class StepBridge:
|
||||
def request(self, command, **arguments):
|
||||
self.calls.append(command)
|
||||
if command in ('step', 'step_over', 'step_out'):
|
||||
count = arguments.get('count', 1) if command == 'step' else 1
|
||||
count = arguments.get('count', 1) if command in ('step', 'step_over') else 1
|
||||
self.step_counts.append(count)
|
||||
self.owner.index = min(self.owner.index + count,
|
||||
len(self.owner.locations) - 1)
|
||||
@@ -153,7 +153,7 @@ class StepSession:
|
||||
|
||||
|
||||
class WaitingBridge:
|
||||
"""Машинный over ожидает внешний ввод, но pause должен остаться доступен."""
|
||||
"""Машинный over/out ожидает ввод, но Pause должен остаться доступен."""
|
||||
def __init__(self):
|
||||
self.paused = False
|
||||
self.started = False
|
||||
@@ -168,7 +168,7 @@ class WaitingBridge:
|
||||
if command == 'pause':
|
||||
self.paused = True
|
||||
return {'accepted': True}
|
||||
if command == 'step_over':
|
||||
if command in ('step_over', 'step_out'):
|
||||
self.started = True
|
||||
return {'accepted': True}
|
||||
raise AssertionError(command)
|
||||
@@ -500,6 +500,30 @@ class ServerTests(unittest.TestCase):
|
||||
finally:
|
||||
controller.close()
|
||||
|
||||
def test_machine_over_and_out_are_separate_from_source_steps(self):
|
||||
session = StepSession()
|
||||
controller = SessionController(session)
|
||||
try:
|
||||
for bad in (0, 65, 1.5):
|
||||
with self.assertRaisesRegex(SessionError, '1..64'):
|
||||
controller.call('step_over_instruction',
|
||||
{'count': bad, 'owner': 'mcp:test'})
|
||||
over = controller.call('step_over_instruction',
|
||||
{'count': 2, 'owner': 'mcp:test'})
|
||||
self.assertEqual(over['accepted'], True)
|
||||
self.wait_source_step(controller)
|
||||
self.assertEqual(controller.events[-1]['body']['location']['sources'][0]['line'], 4)
|
||||
self.assertEqual(session.bridge.step_counts, [2])
|
||||
session.index = 0
|
||||
out = controller.call('step_out_instruction', {'owner': 'mcp:test'})
|
||||
self.assertEqual(out['accepted'], True)
|
||||
self.wait_source_step(controller)
|
||||
self.assertEqual(controller.events[-1]['body']['location']['sources'][0]['line'], 3)
|
||||
self.assertEqual([name for name in session.bridge.calls
|
||||
if name != 'snapshot'], ['step_over', 'step_out'])
|
||||
finally:
|
||||
controller.close()
|
||||
|
||||
def test_source_step_preserves_user_breakpoint_on_same_line(self):
|
||||
session = StepSession()
|
||||
controller = SessionController(session)
|
||||
@@ -531,6 +555,23 @@ class ServerTests(unittest.TestCase):
|
||||
finally:
|
||||
controller.close()
|
||||
|
||||
def test_waiting_machine_over_out_keep_pause_available(self):
|
||||
for method in ('step_over_instruction', 'step_out_instruction'):
|
||||
with self.subTest(method=method):
|
||||
session = StepSession()
|
||||
session.bridge = WaitingBridge()
|
||||
controller = SessionController(session)
|
||||
try:
|
||||
started = controller.call(method, {'owner': 'mcp:test'})
|
||||
self.assertTrue(started['accepted'])
|
||||
self.assertTrue(controller.running)
|
||||
stopped = controller.call('pause', {'owner': 'mcp:test'})
|
||||
self.assertEqual(stopped['sources'][0]['line'], 3)
|
||||
self.assertFalse(controller.running)
|
||||
self.assertIn('pause', session.bridge.calls)
|
||||
finally:
|
||||
controller.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -168,8 +168,11 @@ function exports.startplugin()
|
||||
pending={kind="step",time=now()}
|
||||
cpu().debug:step(count)
|
||||
elseif command=="step_over" then
|
||||
local count=number(args.count or 1,64)
|
||||
assert(count>=1,"over count должен быть 1..64")
|
||||
pending={kind="step_over",time=now()}
|
||||
machine().debugger:command("over 1")
|
||||
-- В консоли MAME числа без префикса шестнадцатеричные.
|
||||
machine().debugger:command("over #"..count)
|
||||
elseif command=="step_out" then
|
||||
pending={kind="step_out",time=now()}
|
||||
machine().debugger:command("out")
|
||||
@@ -210,7 +213,7 @@ function exports.startplugin()
|
||||
"Дизассемблирование за пределами logical Z80")
|
||||
local path=directory.."/_disasm.txt"
|
||||
os.remove(path) -- Не возвращать файл от прежней команды при ошибке MAME.
|
||||
machine().debugger:command(string.format("dasm %s,0x%X,%d",path,address,length))
|
||||
machine().debugger:command(string.format("dasm %s,0x%X,#%d",path,address,length))
|
||||
local size=lfs.attributes(path,"size")
|
||||
assert(size and size>0 and size<=65536,"MAME не создал ограниченный disasm-файл")
|
||||
local file=assert(io.open(path,"rb"))
|
||||
|
||||
@@ -206,6 +206,15 @@ class McpSession:
|
||||
raise SessionError('Число машинных шагов должно быть 1..64')
|
||||
return self.call('step', {'count': count, 'owner': self.owner})
|
||||
|
||||
def step_over_instruction(self, count: int = 1):
|
||||
if not isinstance(count, int) or isinstance(count, bool) or count < 1 or count > 64:
|
||||
raise SessionError('Число машинных шагов должно быть 1..64')
|
||||
return self.call('step_over_instruction',
|
||||
{'count': count, 'owner': self.owner})
|
||||
|
||||
def step_out_instruction(self):
|
||||
return self.call('step_out_instruction', {'owner': self.owner})
|
||||
|
||||
def step_source(self, kind: str = 'into'):
|
||||
if kind not in ('into', 'over', 'out'):
|
||||
raise SessionError('kind должен быть into, over или out')
|
||||
|
||||
@@ -403,6 +403,22 @@ class SessionController:
|
||||
self.running = False
|
||||
self._emit('stopped', {'reason': 'pause', 'location': location})
|
||||
return location
|
||||
if method in ('step_over_instruction', 'step_out_instruction'):
|
||||
if self.running:
|
||||
raise SessionError('CPU уже выполняется; сначала Pause')
|
||||
command = {'step_over_instruction': 'step_over',
|
||||
'step_out_instruction': 'step_out'}[method]
|
||||
request_args = {}
|
||||
if method != 'step_out_instruction':
|
||||
count = arguments.get('count', 1)
|
||||
if type(count) is not int or count < 1 or count > 64:
|
||||
raise SessionError('Число машинных шагов должно быть 1..64')
|
||||
request_args['count'] = count
|
||||
self._control(arguments)
|
||||
self.session.bridge.request(command, **request_args)
|
||||
self.running = True
|
||||
self._emit('continued', {'reason': command})
|
||||
return {'accepted': True, 'command': command}
|
||||
if method == 'step':
|
||||
if self.running:
|
||||
raise SessionError('CPU уже выполняется; сначала Pause')
|
||||
@@ -643,7 +659,8 @@ RPC_MUTATIONS = frozenset({
|
||||
'claim_control', 'renew_control', 'release_control', 'input_key',
|
||||
'break_line', 'break_function', 'clear_breakpoint', 'clear_owned_breakpoints',
|
||||
'set_source_breakpoints', 'set_function_breakpoints',
|
||||
'continue', 'pause', 'step', 'source_step',
|
||||
'continue', 'pause', 'step', 'step_over_instruction',
|
||||
'step_out_instruction', 'source_step',
|
||||
})
|
||||
RPC_GENERATION_MUTATIONS = RPC_MUTATIONS - {
|
||||
'claim_control', 'renew_control', 'release_control',
|
||||
|
||||
@@ -193,6 +193,16 @@ def make_server(client: McpSession | ManagedMcpSession,
|
||||
"""Выполнить 1..64 машинных инструкций Z80 и вернуть текущую позицию."""
|
||||
return client.step_instruction(count)
|
||||
|
||||
@tool()
|
||||
def step_over_instruction(count: int = 1) -> dict[str, Any]:
|
||||
"""Перешагнуть 1..64 машинных инструкций Z80 (вызовы — целиком)."""
|
||||
return client.step_over_instruction(count)
|
||||
|
||||
@tool()
|
||||
def step_out_instruction() -> dict[str, Any]:
|
||||
"""Выйти из текущего машинного frame Z80; C source-step не используется."""
|
||||
return client.step_out_instruction()
|
||||
|
||||
@tool()
|
||||
def step_source(kind: str = 'into') -> dict[str, Any]:
|
||||
"""Начать асинхронный C-шаг: into, over или out; результат в recent_events."""
|
||||
|
||||
Reference in New Issue
Block a user