Files
Sprinter-SDCC/toolchain/sdbg/mcp_adapter.py
T
2026-09-17 23:32:46 +03:00

231 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Тонкий MCP-клиент общей source-debug сессии без зависимости от MCP SDK."""
from __future__ import annotations
from pathlib import Path
import time
import uuid
from .server import RPC_GENERATION_MUTATIONS, rpc_call
from .session import SessionError
class McpSession:
def __init__(self, socket_path: str, rpc=rpc_call):
self.socket_path = str(Path(socket_path).expanduser())
self.owner = 'mcp:' + uuid.uuid4().hex
self._rpc = rpc
self.identity: dict[str, str] = {}
def call(self, method: str, arguments: dict | None = None, timeout: float = 10):
if method != 'status' and not self.identity:
self.status()
generation = None
if method in RPC_GENERATION_MUTATIONS:
generation = self.status().get('generation')
identity = dict(self.identity)
if method in RPC_GENERATION_MUTATIONS:
identity['generation'] = generation
result = self._rpc(self.socket_path, method, arguments or {},
timeout=timeout, **identity)
if method == 'status' and 'session_id' in result:
self.identity = {'session_id': result['session_id'],
'build_id': result['build_id']}
return result
def status(self):
return self.call('status')
def where(self):
return self.call('where')
def registers(self):
return self.call('registers')
def read_memory(self, address: str, length: int = 16):
return self.call('read_memory', {'address': self._address(address),
'length': length})
@staticmethod
def _address(address: str) -> int:
try:
return int(address, 16 if address.lower().startswith('0x') else 10)
except (AttributeError, ValueError) as error:
raise SessionError('Адрес памяти должен быть десятичным или 0xHEX') from error
def list_shares(self):
return self.call('list_shares')
def list_ports(self):
return self.call('list_ports')
def read_program_memory(self, address: str, length: int = 16):
return self.call('read_program_memory', {'address': self._address(address),
'length': length})
def disassemble_logical(self, address: str, length: int = 32):
return self.call('disassemble_logical', {'address': self._address(address),
'length': length})
def read_share(self, tag: str, address: str, length: int = 16):
return self.call('read_share', {'tag': tag, 'address': self._address(address),
'length': length})
def read_vram(self, address: str, length: int = 16):
return self.call('read_vram', {'address': self._address(address),
'length': length})
def read_screen_pixels(self, x: int, y: int, width: int = 1, height: int = 1):
return self.call('read_screen_pixels', {'x': x, 'y': y,
'width': width, 'height': height})
def screenshot(self):
return self.call('screenshot')
def list_breakpoints(self):
return self.call('list_breakpoints')
def press_key(self, key: str, frames: int = 3):
"""Один физический PC-key с отпусканием даже при stop/ошибке."""
from mame_interactive import SHIFT_KEY, resolve
if not isinstance(key, str) or len(key) == 0 or len(key) > 16:
raise SessionError('Нужна одна клавиша или enter/space/tab')
if not isinstance(frames, int) or frames < 1 or frames > 60:
raise SessionError('frames должен быть 1..60')
named = {'enter': '\n', 'space': ' ', 'tab': '\t'}
symbol = named.get(key.lower(), key)
if len(symbol) != 1:
raise SessionError('Пока поддерживаются один символ, enter, space, tab')
try:
tag, mask, shift = resolve(symbol)
except ValueError as error:
raise SessionError(str(error)) from error
if not self.status()['running']:
raise SessionError('Нажатие возможно только при running CPU')
held = []
try:
if shift:
self.call('input_key', {'tag': SHIFT_KEY[0], 'mask': SHIFT_KEY[1],
'down': True, 'owner': self.owner})
held.append(SHIFT_KEY)
self.call('input_key', {'tag': tag, 'mask': mask,
'down': True, 'owner': self.owner})
held.append((tag, mask))
start = self.call('snapshot')['frame']
deadline = time.monotonic() + 4
last = start
while time.monotonic() < deadline:
snapshot = self.call('snapshot')
last = snapshot['frame']
if last - start >= frames or snapshot['state'] != 'running':
break
time.sleep(.01)
else:
raise SessionError('Клавиша не получила нужное число кадров за 4 с')
return {'key': key, 'requested_frames': frames,
'observed_frames': max(0, last - start),
'stopped': snapshot['state'] != 'running'}
finally:
for release_tag, release_mask in reversed(held):
self.call('input_key', {'tag': release_tag, 'mask': release_mask,
'down': False, 'owner': self.owner})
def type_string(self, value: str):
"""Послать строку по физической клавиатуре с паузами между клавишами."""
from mame_interactive import resolve
if not isinstance(value, str) or not value or len(value) > 64:
raise SessionError('type_string принимает 1..64 символа')
try:
for char in value:
resolve(char)
except ValueError as error:
raise SessionError(str(error)) from error
if not self.status()['running']:
raise SessionError('Ввод строки возможен только при running CPU')
typed = 0
stopped = False
for char in value:
result = self.press_key(char)
typed += 1
stopped = result['stopped']
if stopped or typed == len(value):
break
snapshot = self.call('snapshot')
start = snapshot['frame']
deadline = time.monotonic() + 4
while snapshot['state'] == 'running' and snapshot['frame'] - start < 4:
if time.monotonic() >= deadline:
raise SessionError('Межклавишная пауза не получила кадры за 4 с')
time.sleep(.01)
snapshot = self.call('snapshot')
stopped = snapshot['state'] != 'running'
if stopped:
break
return {'requested': len(value), 'typed': typed,
'complete': typed == len(value), 'stopped': stopped}
def variables(self):
return self.call('variables')
def read_variable(self, name: str, module: str | None = None):
return self.call('read_variable', {'name': name, 'module': module})
def events(self, after: int = 0, timeout: float = 0):
if after < 0 or timeout < 0 or timeout > 30:
raise SessionError('after должен быть неотрицательным, timeout — от 0 до 30 с')
return self.call('events', {'after': after, 'timeout': timeout,
'owner': self.owner},
timeout=max(10, timeout + 2))
def console_tail(self, count: int = 40):
if count < 1 or count > 200:
raise SessionError('count должен быть от 1 до 200')
return self.call('mame_console_tail', {'count': count})
def break_line(self, file: str, line: int):
return self.call('break_line', {'file': file, 'line': line, 'owner': self.owner})
def break_function(self, name: str):
return self.call('break_function', {'name': name, 'owner': self.owner})
def clear_breakpoint(self, identifier: int):
return self.call('clear_breakpoint', {'id': identifier, 'owner': self.owner})
def clear_owned_breakpoints(self):
return self.call('clear_owned_breakpoints', {'owner': self.owner})
def continue_execution(self):
return self.call('continue', {'owner': self.owner})
def pause_execution(self):
return self.call('pause', {'owner': self.owner})
def step_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', {'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')
return self.call('source_step', {'kind': kind, 'owner': self.owner})
def claim_control(self):
return self.call('claim_control', {'owner': self.owner})
def renew_control(self):
return self.call('renew_control', {'owner': self.owner})
def release_control(self):
return self.call('release_control', {'owner': self.owner})