Expand shared Sprinter MCP with managed launch, raw reads and key input

This commit is contained in:
Александр Петров
2026-09-17 22:57:27 +03:00
parent a43e7bda89
commit 5dc7998324
22 changed files with 1120 additions and 67 deletions
+40 -16
View File
@@ -19,7 +19,7 @@ ROOT = Path(__file__).resolve().parents[2]
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.server import RPC_GENERATION_MUTATIONS, rpc_call as raw_rpc_call
from sdbg.session import SessionError
@@ -98,6 +98,10 @@ def main():
help='Python 3.12 с MCP SDK 2.x для совместной живой пробы DAP/MCP')
parser.add_argument('--orphan-expiry', action='store_true',
help='проверить очистку точки владельца MCP без heartbeat через 30 с')
parser.add_argument('--screen-mcp-python', default=None,
help='Python с MCP SDK 2.x для снимка hello во время getchar')
parser.add_argument('--screen-proof', default=None,
help='сохранить PNG работающего hello вне временной сессии')
parser.add_argument('--clear-while-running', action='store_true',
help='во время WAITKEY удалить личную точку; использовать с --waitkey --emulated-key')
parser.add_argument('--exit-while-stopped', action='store_true',
@@ -128,6 +132,14 @@ def main():
launch['launchPath'] = options.launch_path
send(process, 2, 'launch', launch)
launched, buffer, events = wait_response(process, buffer, 'launch', 80)
session_identity = raw_rpc_call(launched['body']['socket'], 'status')
def session_rpc(path, method, arguments=None, timeout=10):
identity = {'session_id': session_identity['session_id'],
'build_id': session_identity['build_id']}
if method in RPC_GENERATION_MUTATIONS:
current = raw_rpc_call(path, 'status', **identity)
identity['generation'] = current['generation']
return raw_rpc_call(path, method, arguments, timeout, **identity)
if not any(item.get('event') == 'initialized' for item in events):
initialized, buffer = receive(process, buffer, 5)
if initialized.get('event') != 'initialized':
@@ -156,13 +168,13 @@ def main():
raise RuntimeError('MCP SDK probe: '+probe.stderr[-4000:])
if options.orphan_expiry:
owner = 'mcp:orphan-expiry-probe'
orphan = rpc_call(launched['body']['socket'], 'break_line', {
orphan = session_rpc(launched['body']['socket'], 'break_line', {
'file': source, 'line': 62, 'owner': owner})
rpc_call(launched['body']['socket'], 'claim_control', {'owner': owner})
session_rpc(launched['body']['socket'], 'claim_control', {'owner': owner})
print('Ожидание истечения MCP owner и очистки его точки...', flush=True)
deadline = time.monotonic()+36
while time.monotonic() < deadline:
events = rpc_call(launched['body']['socket'], 'events', {'after': 0})
events = session_rpc(launched['body']['socket'], 'events', {'after': 0})
if any(event['event'] == 'owner_expired' and
event['body']['owner'] == owner for event in events['events']):
break
@@ -170,7 +182,7 @@ def main():
else:
raise RuntimeError('MCP owner не истёк за 36 с')
try:
rpc_call(launched['body']['socket'], 'clear_breakpoint', {
session_rpc(launched['body']['socket'], 'clear_breakpoint', {
'id': orphan['id'], 'owner': owner})
except SessionError as error:
if 'Неизвестная логическая точка' not in str(error):
@@ -284,37 +296,49 @@ def main():
socket_path = launched['body']['socket']
clear_point = None
if options.clear_while_running:
clear_point = rpc_call(socket_path, 'break_line', {
clear_point = session_rpc(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 options.screen_mcp_python:
time.sleep(.6)
screen_command = [options.screen_mcp_python,
str(ROOT/'tests/sdbg/run_mcp_screen_probe.py'),
'--socket', socket_path]
if options.screen_proof:
screen_command += ['--proof', options.screen_proof]
screen_probe = subprocess.run(screen_command, capture_output=True,
text=True, timeout=20, check=False)
if screen_probe.returncode:
raise RuntimeError('MCP screen probe: '+screen_probe.stderr[-3000:])
print(screen_probe.stdout.strip(), flush=True)
if clear_point:
snapshot = rpc_call(socket_path, 'snapshot', timeout=5)
snapshot = session_rpc(socket_path, 'snapshot', timeout=5)
if snapshot['state'] != 'running':
raise RuntimeError('CPU не выполняется перед очисткой точки')
try:
rpc_call(socket_path, 'read_memory', {'address': 0, 'length': 1})
session_rpc(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', {
session_rpc(socket_path, 'clear_breakpoint', {
'id': clear_point['id'], 'owner': 'probe:running-clear'})
if '--emulated-key' in sys.argv:
first = rpc_call(socket_path, 'snapshot', timeout=5)
first = session_rpc(socket_path, 'snapshot', timeout=5)
time.sleep(1)
second = rpc_call(socket_path, 'snapshot', timeout=5)
second = session_rpc(socket_path, 'snapshot', timeout=5)
if second['paused'] or second['time'] <= first['time']:
raise RuntimeError('MAME не обновляет input frames: '+
str((first, second)))
tag, mask, _ = resolve('x')
rpc_call(socket_path, 'input_key',
session_rpc(socket_path, 'input_key',
{'tag': tag, 'mask': mask, 'down': True}, timeout=5)
time.sleep(.15)
rpc_call(socket_path, 'input_key',
session_rpc(socket_path, 'input_key',
{'tag': tag, 'mask': mask, 'down': False}, timeout=5)
key_stop, buffer = wait_event(process, buffer, 'stopped', 10)
if not key_stop:
@@ -324,7 +348,7 @@ def main():
after = after_response['body']['stackFrames'][0]
if after['line'] != 63:
raise RuntimeError('После клавиши ожидалась строка 63: '+str(after))
ascii_code = rpc_call(socket_path, 'registers', timeout=5)
ascii_code = session_rpc(socket_path, 'registers', timeout=5)
ascii_code = ascii_code['registers']['DE'] & 0xff
if ascii_code != ord('x'):
raise RuntimeError(f'WAITKEY вернул {ascii_code:#x}, ожидался x (0x78)')
@@ -340,7 +364,7 @@ def main():
return 0
if '--manual-key' in sys.argv:
socket_path = launched['body']['socket']
keyboard_state = rpc_call(socket_path, 'snapshot', timeout=5)['keyboards']
keyboard_state = session_rpc(socket_path, 'snapshot', timeout=5)['keyboards']
if keyboard_state.get(':kbd:ms_naturl') is not True:
raise RuntimeError('Физическая PC-клавиатура MAME выключена: '+
str(keyboard_state))
@@ -357,7 +381,7 @@ def main():
after = after_response['body']['stackFrames'][0]
if after['line'] != 63:
raise RuntimeError('После клавиши ожидалась строка 63: '+str(after))
ascii_code = rpc_call(socket_path, 'registers', timeout=5)
ascii_code = session_rpc(socket_path, 'registers', timeout=5)
ascii_code = ascii_code['registers']['DE'] & 0xff
if ascii_code != ord('x'):
raise RuntimeError(f'WAITKEY вернул {ascii_code:#x}, ожидался x (0x78)')