#!/usr/bin/env python3 """MCP сам запускает hello, затем DAP подключается к тому же MAME.""" from __future__ import annotations import asyncio import json import os from pathlib import Path import subprocess import sys import tempfile import time from mcp import Client, StdioServerParameters ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(Path(__file__).resolve().parent)) from run_vscode_dap_probe import send, wait_response # noqa: E402 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-managed-mcp-', dir='/tmp') as temp: socket = str(Path(temp) / 'session.sock') parameters = StdioServerParameters( command=sys.executable, args=[str(ROOT / 'toolchain/sdbg_mcp.py'), '--build', str(package), '--socket', socket, '--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 names = {tool.name for tool in (await client.list_tools()).tools} if not {'start_session', 'stop_session', 'where', 'read_variable', 'press_key'} <= names: raise RuntimeError('Нет инструментов автономного запуска') initial = await call('session_status') if initial['phase'] != 'idle': raise RuntimeError('Ожидался idle: ' + repr(initial)) started_at = time.monotonic() accepted = await call('start_session') start_ms = round((time.monotonic() - started_at) * 1000, 1) if not accepted['accepted'] or start_ms > 3000: raise RuntimeError('MCP start_session не ответил быстро: ' + repr(accepted)) duplicate = await client.call_tool('start_session', {}) if not duplicate.is_error or 'уже запущена' not in str(duplicate.content): raise RuntimeError('Повторный start_session не был отклонён') mame_pid = None try: deadline = time.monotonic() + 85 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('MCP session не дошла до main') mame_pid = status['mame_pid'] location = await call('where') if location.get('function', {}).get('name') != 'main': raise RuntimeError('MCP не остановился в main: ' + repr(location)) await call('read_variable', {'name': 'errno'}) shot = await call('screenshot') if Path(shot['path']).read_bytes()[:8] != b'\x89PNG\r\n\x1a\n': raise RuntimeError('MCP не создал PNG') dap = subprocess.Popen([sys.executable, str(ROOT / 'toolchain/sdbg_dap.py')], cwd=ROOT, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) buffer = b'' try: send(dap, 1, 'initialize') _, buffer, _ = wait_response(dap, buffer, 'initialize', 10) send(dap, 2, 'attach', {'socket': socket}) _, buffer, _ = wait_response(dap, buffer, 'attach', 10) send(dap, 3, 'configurationDone') _, buffer, _ = wait_response(dap, buffer, 'configurationDone', 10) send(dap, 4, 'stackTrace') frame, buffer, _ = wait_response(dap, buffer, 'stackTrace', 10) if frame['body']['stackFrames'][0]['name'] != 'main': raise RuntimeError('DAP attach не увидел main') send(dap, 5, 'disconnect') wait_response(dap, buffer, 'disconnect', 10) finally: if dap.poll() is None: dap.terminate() dap.communicate(timeout=8) 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') source = str(ROOT / 'tests/hello/hello.c') before = await call('set_line_breakpoint', {'file': source, 'line': 62}) await call('continue_execution') deadline = time.monotonic() + 10 while time.monotonic() < deadline: at_getchar = await call('session_status') if not at_getchar['running']: break await asyncio.sleep(.1) else: raise TimeoutError('MCP не остановился перед getchar') location = await call('where') if not any(item['line'] == 62 for item in location['sources']): raise RuntimeError('MCP остановился не перед getchar: ' + repr(location)) await call('clear_breakpoint', {'identifier': before['id']}) after = await call('set_line_breakpoint', {'file': source, 'line': 63}) await call('continue_execution') await asyncio.sleep(.4) key = await call('press_key', {'key': 'x', 'frames': 3}) deadline = time.monotonic() + 10 while time.monotonic() < deadline: after_key = await call('session_status') if not after_key['running']: break await asyncio.sleep(.1) else: raise TimeoutError('MCP press_key не завершил getchar') location = await call('where') if not any(item['line'] == 63 for item in location['sources']): raise RuntimeError('После клавиши ожидалась строка 63: ' + repr(location)) await call('clear_breakpoint', {'identifier': after['id']}) print(json.dumps({'event': 'mcp_launch_dap_attach_verified', 'tools': len(names), 'start_ms': start_ms, 'session_id': status['session_id'], 'socket': socket, 'pc': location['pc'], 'key': key['key'], 'after_getchar_line': 63}, ensure_ascii=False), flush=True) finally: await call('stop_session') deadline = time.monotonic() + 12 while time.monotonic() < deadline: ended = await call('session_status') if ended['phase'] == 'stopped': break await asyncio.sleep(.1) else: raise RuntimeError('stop_session не завершил launcher') if Path(socket).exists(): raise RuntimeError('После stop_session остался Unix socket') if mame_pid is not None: deadline = time.monotonic() + 8 while time.monotonic() < deadline: try: os.kill(mame_pid, 0) except ProcessLookupError: break await asyncio.sleep(.1) else: raise RuntimeError('После stop_session остался процесс MAME') if __name__ == '__main__': asyncio.run(probe())