Sprinter: добавить отладку C-исходников и интеграцию VS Code
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
"""Честный минимальный 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']}))
|
||||
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
|
||||
Reference in New Issue
Block a user