Add shared-session MCP source debugger adapter

This commit is contained in:
Александр Петров
2026-09-16 20:43:50 +03:00
parent addc00a0f3
commit 9ad5a018a2
19 changed files with 740 additions and 22 deletions
+71 -2
View File
@@ -20,6 +20,7 @@ PYTHON = Path.home()/'.pyenv/shims/python'
sys.path.insert(0, str(ROOT/'toolchain'))
from mame_interactive import resolve
from sdbg.server import rpc_call
from sdbg.session import SessionError
def send(process, sequence, command, arguments=None):
@@ -89,8 +90,14 @@ def wait_event(process, buffer, event, timeout):
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--build', default=None)
parser.add_argument('--socket', default=None,
help='фиксированный Unix socket DAP/MCP-сеанса')
parser.add_argument('--app-hdd', default=None)
parser.add_argument('--launch-path', default=None)
parser.add_argument('--mcp-python', default=None,
help='Python 3.12 с MCP SDK 2.x для совместной живой пробы DAP/MCP')
parser.add_argument('--clear-while-running', action='store_true',
help='во время WAITKEY удалить личную точку; использовать с --waitkey --emulated-key')
parser.add_argument('--exit-while-stopped', action='store_true',
help='аварийно завершить собственный MAME на main и ждать DAP terminated')
parser.add_argument('--stop-while-stopped', action='store_true',
@@ -111,6 +118,8 @@ def main():
_, buffer, _ = wait_response(process, buffer, 'initialize', 10)
debugger = 'osx' if '--osx' in sys.argv else 'sdbg'
launch = {'build': str(build), 'debugger': debugger, 'dssTimeout': 30}
if options.socket:
launch['socket'] = options.socket
if options.app_hdd:
launch['appHdd'] = str(Path(options.app_hdd).resolve())
if options.launch_path:
@@ -128,6 +137,48 @@ def main():
frame = frame_response['body']['stackFrames'][0]
if frame['name'] != 'main':
raise RuntimeError('Не main: '+str(frame))
if options.mcp_python:
source = str(ROOT/'tests/hello/hello.c')
send(process, 5, 'setBreakpoints', {
'source': {'path': source}, 'breakpoints': [{'line': 31}]})
points, buffer, _ = wait_response(process, buffer, 'setBreakpoints', 10)
dap_point = points['body']['breakpoints'][0]
if not dap_point['verified']:
raise RuntimeError('DAP-точка строки 31 не подтверждена')
probe = subprocess.run([
options.mcp_python, str(ROOT/'tests/sdbg/run_mcp_sdk_probe.py'),
'--socket', launched['body']['socket'], '--source', source,
'--foreign-id', str(dap_point['id'])],
capture_output=True, text=True, timeout=45, check=False)
if probe.returncode:
raise RuntimeError('MCP SDK probe: '+probe.stderr[-4000:])
send(process, 6, 'continue')
_, buffer, _ = wait_response(process, buffer, 'continue', 10)
stopped, buffer = wait_event(process, buffer, 'stopped', 20)
if not stopped:
raise RuntimeError('После MCP DAP-точка строки 31 не сработала')
send(process, 7, 'stackTrace')
at_point, buffer, _ = wait_response(process, buffer, 'stackTrace', 10)
if at_point['body']['stackFrames'][0]['line'] != 31:
raise RuntimeError('После MCP ожидалась DAP-точка строки 31')
send(process, 8, 'setBreakpoints', {
'source': {'path': source}, 'breakpoints': []})
_, buffer, _ = wait_response(process, buffer, 'setBreakpoints', 10)
send(process, 9, 'continue')
_, buffer, _ = wait_response(process, buffer, 'continue', 10)
stale, buffer = wait_event(process, buffer, 'stopped', 3)
if stale:
raise RuntimeError('Точка MCP осталась после закрытия stdio-клиента')
print(probe.stdout.strip(), flush=True)
send(process, 10, 'disconnect')
wait_response(process, buffer, 'disconnect', 10)
if options.socket:
deadline = time.monotonic()+10
while Path(options.socket).exists() and time.monotonic() < deadline:
time.sleep(.1)
if Path(options.socket).exists():
raise RuntimeError('После DAP disconnect остался Unix socket сессии')
return 0
if options.stop_while_stopped:
mame_pid = launched['body']['mamePid']
send(process, 5, 'disconnect')
@@ -202,12 +253,29 @@ def main():
before = at_getchar['body']['stackFrames'][0]
if before['line'] != 62:
raise RuntimeError('Остановка не на getchar: '+str(before))
socket_path = launched['body']['socket']
clear_point = None
if options.clear_while_running:
clear_point = rpc_call(socket_path, 'break_line', {
'file': str(ROOT/'tests/hello/hello.c'), 'line': 63,
'owner': 'probe:running-clear'})
started = time.monotonic()
send(process, 8, 'next')
_, buffer, _ = wait_response(process, buffer, 'next', 3)
response_ms = round((time.monotonic()-started)*1000, 2)
if clear_point:
snapshot = rpc_call(socket_path, 'snapshot', timeout=5)
if snapshot['state'] != 'running':
raise RuntimeError('CPU не выполняется перед очисткой точки')
try:
rpc_call(socket_path, 'read_memory', {'address': 0, 'length': 1})
raise RuntimeError('Чтение памяти разрешено при running CPU')
except SessionError as error:
if 'остановленного CPU' not in str(error):
raise
rpc_call(socket_path, 'clear_breakpoint', {
'id': clear_point['id'], 'owner': 'probe:running-clear'})
if '--emulated-key' in sys.argv:
socket_path = launched['body']['socket']
first = rpc_call(socket_path, 'snapshot', timeout=5)
time.sleep(1)
second = rpc_call(socket_path, 'snapshot', timeout=5)
@@ -236,7 +304,8 @@ def main():
'at': before, 'after': after,
'ascii': ascii_code,
'emulated_time_advanced': second['time']-first['time'],
'next_response_ms': response_ms},
'next_response_ms': response_ms,
'running_clear_verified': bool(clear_point)},
ensure_ascii=False), flush=True)
send(process, 10, 'disconnect')
wait_response(process, buffer, 'disconnect', 10)