Files
Sprinter-SDCC/toolchain/sdbg/dap.py
T
2026-09-16 15:12:42 +03:00

201 lines
9.7 KiB
Python

"""Честный минимальный Debug Adapter Protocol поверх постоянной sdbg-сессии."""
from __future__ import annotations
from pathlib import Path
from .server import rpc_call
from .session import SessionError
class DapEngine:
def __init__(self, rpc=None):
self.rpc_path = None
self._rpc_override = rpc
self.attached = None
self.event_sequence = 0
def rpc(self, method, arguments=None, timeout=10):
if self._rpc_override is not None:
return self._rpc_override(method, arguments or {})
if not self.rpc_path:
raise SessionError('DAP ещё не подключён к session server')
return rpc_call(self.rpc_path, method, arguments, timeout)
def handle(self, command: str, arguments: dict) -> tuple[dict, list[tuple[str, dict]]]:
events = []
if command == 'initialize':
body = {
'supportsConfigurationDoneRequest': True,
'supportsFunctionBreakpoints': True,
'supportsInstructionBreakpoints': False,
'supportsEvaluateForHovers': True,
'supportsSetVariable': False,
'supportsStepBack': False,
'supportsRestartRequest': False,
'supportsTerminateRequest': False,
'supportsDisassembleRequest': False,
'supportsSteppingGranularity': True,
}
return body, events
if command == 'attach':
if self._rpc_override is None:
path = arguments.get('socket')
if not isinstance(path, str) or not path:
raise SessionError('В attach требуется socket session server')
self.rpc_path = path
self.attached = self.rpc('status')
self.event_sequence = self.attached['event_sequence']
events.append(('initialized', {}))
return {'buildId': self.attached['build_id']}, events
if command == 'configurationDone':
location = self.rpc('where')
events.append(('stopped', {'reason': 'entry', 'threadId': 1,
'allThreadsStopped': True,
'description': self._description(location)}))
return {}, events
if command == 'disconnect':
return {}, events
if command == 'threads':
return {'threads': [{'id': 1, 'name': 'Sprinter Z80'}]}, events
if command == 'stackTrace':
location = self.rpc('where')
return {'stackFrames': [self._frame(location)], 'totalFrames': 1}, events
if command == 'scopes':
return {'scopes': [
{'name': 'Registers', 'variablesReference': 1, 'expensive': False},
{'name': 'Globals/statics', 'variablesReference': 2, 'expensive': False},
]}, events
if command == 'variables':
reference = int(arguments.get('variablesReference', 0))
if reference == 1:
values = self.rpc('registers')['registers']
variables = [{'name': name, 'value': f'{value:#x}',
'variablesReference': 0} for name, value in sorted(values.items())]
elif reference == 2:
variables = self._global_variables()
else:
variables = []
return {'variables': variables}, events
if command == 'evaluate':
expression = arguments.get('expression', '')
if not expression.isidentifier():
raise SessionError('MVP evaluate принимает только имя переменной')
value = self.rpc('read_variable', {'name': expression})
return {'result': str(value['value']), 'type': value['type'],
'variablesReference': 0, 'memoryReference': hex(value['link_address'])}, events
if command == 'setBreakpoints':
source = arguments.get('source', {})
path = source.get('path')
if not path:
raise SessionError('Для source breakpoint нужен полный path')
specifications = [{name: item[name] for name in
('line','condition','hitCondition','logMessage') if name in item}
for item in arguments.get('breakpoints', [])]
result = self.rpc('set_source_breakpoints',
{'file': path, 'breakpoints': specifications})
return {'breakpoints': [self._breakpoint(item) for item in result['breakpoints']]}, events
if command == 'setFunctionBreakpoints':
names = [item['name'] for item in arguments.get('breakpoints', [])]
result = self.rpc('set_function_breakpoints', {'names': names})
return {'breakpoints': [self._breakpoint(item) for item in result['breakpoints']]}, events
if command == 'continue':
self.rpc('continue')
return {'allThreadsContinued': True}, events
if command == 'pause':
self.rpc('pause')
return {}, events
if command in ('next', 'stepIn'):
if arguments.get('granularity') == 'instruction':
self.rpc('step')
else:
self.rpc('source_step', {
'kind': 'over' if command == 'next' else 'into'})
return {}, events
if command == 'stepOut':
self.rpc('source_step', {'kind': 'out'})
return {}, events
raise SessionError('DAP-команда пока не поддерживается: ' + command)
def poll_events(self, timeout=1):
previous = self.event_sequence
result = self.rpc('events', {'after': self.event_sequence, 'timeout': timeout},
timeout=timeout + 2)
self.event_sequence = result['last']
translated = []
gap = max(0, result.get('first', previous + 1) - previous - 1)
if gap:
translated.append(('output', {'category': 'stderr',
'output': f'sdbg: пропущено {gap} событий журнала; '
'уменьшите частоту logpoint\n'}))
for event in result['events']:
if event['event'] == 'stopped':
translated.append(('stopped', {'reason': event['body'].get('reason', 'breakpoint'),
'threadId': 1, 'allThreadsStopped': True,
'description': self._description(
event['body'].get('location', {}))}))
elif event['event'] == 'continued':
translated.append(('continued', {'threadId': 1, 'allThreadsContinued': True}))
elif event['event'] == 'invalidated':
translated.append(('terminated', {'restart': False}))
elif event['event'] == 'output':
translated.append(('output', {'category': event['body'].get('category','console'),
'output': event['body']['output']}))
if result['closed'] and not any(name == 'terminated' for name, _ in translated):
translated.append(('terminated', {'restart': False}))
return translated, result['closed']
@staticmethod
def _description(location):
status = location.get('status', 'unknown')
bank = location.get('function', {}).get('bank') if location.get('function') else None
suffix = '' if bank is None else f', bank {bank}'
return f'{status}{suffix}, PC={location.get("pc", 0):#x}'
@staticmethod
def _frame(location):
function = location.get('function') or {}
sources = location.get('sources') or []
source = sources[0] if sources else None
frame = {
'id': 1,
'name': function.get('name', '<unknown>') +
(' [ambiguous]' if location.get('status') == 'ambiguous' else ''),
'line': source['line'] if source else 1,
'column': 1,
'instructionPointerReference': hex(location.get('link_address', location.get('pc', 0))),
}
if source:
frame['source'] = {'name': Path(source['file']).name, 'path': source['file']}
return frame
@staticmethod
def _breakpoint(item):
locations = item.get('locations', [])
first = locations[0] if locations else {}
result = {'id': item['id'], 'verified': bool(item.get('verified', locations))}
line = item.get('line', first.get('line'))
if line is not None:
result['line'] = line
if first.get('link_address') is not None:
result['instructionReference'] = hex(first['link_address'])
if len(locations) > 1:
result['message'] = f'Разрешено в {len(locations)} адресов/банков'
return result
def _global_variables(self):
result = []
for variable in self.rpc('variables'):
label = variable['name']
if variable.get('module'):
label += '@' + variable['module']
try:
value = self.rpc('read_variable', {'name': variable['name'],
'module': variable.get('module')})
text = str(value['value'])
except SessionError as error:
text = '<unavailable: ' + str(error) + '>'
result.append({'name': label, 'value': text, 'type': variable['type'],
'variablesReference': 0,
'memoryReference': hex(variable['link_address'])})
return result