Sprinter: добавить отладку C-исходников и интеграцию VS Code

This commit is contained in:
2026-09-15 17:58:41 +03:00
parent 50c6e56b7b
commit e4695b8281
62 changed files with 7147 additions and 27 deletions
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""stdio DAP-адаптер Sprinter MAME; stdout содержит только DAP frames."""
from __future__ import annotations
import json
from pathlib import Path
import select
import subprocess
import sys
import tempfile
import threading
import uuid
from sdbg.dap import DapEngine
from sdbg.session import SessionError
class Protocol:
def __init__(self):
self.engine = DapEngine()
self.sequence = 1
self.write_lock = threading.Lock()
self.stopping = threading.Event()
self.poller = None
self.launcher = None
self.launcher_log = None
def send(self, message):
with self.write_lock:
message['seq'] = self.sequence
self.sequence += 1
data = json.dumps(message, ensure_ascii=False, separators=(',', ':')).encode()
sys.stdout.buffer.write(f'Content-Length: {len(data)}\r\n\r\n'.encode() + data)
sys.stdout.buffer.flush()
def event(self, name, body):
self.send({'type': 'event', 'event': name, 'body': body})
def _poll(self):
while not self.stopping.is_set():
try:
events, closed = self.engine.poll_events(1)
for name, body in events:
self.event(name, body)
if closed:
return
except (SessionError, OSError, ValueError) as error:
self.event('output', {'category': 'stderr', 'output': 'sdbg: ' + str(error) + '\n'})
self.event('terminated', {'restart': False})
return
def request(self, request):
response = {'type': 'response', 'request_seq': request.get('seq', 0),
'command': request.get('command', ''), 'success': True}
events = []
try:
if request['command'] == 'launch':
body, events = self.launch(request.get('arguments') or {})
else:
body, events = self.engine.handle(request['command'], request.get('arguments') or {})
response['body'] = body
except (SessionError, OSError, ValueError, KeyError, TypeError) as error:
response.update(success=False, message=str(error))
self.send(response)
for name, body in events:
self.event(name, body)
if request.get('command') in ('attach','launch') and response['success'] and self.poller is None:
self.poller = threading.Thread(target=self._poll, name='dap-events', daemon=True)
self.poller.start()
if request.get('command') == 'disconnect':
self.stopping.set()
self.stop_launcher()
def stop_launcher(self):
if self.launcher is not None and self.launcher.poll() is None:
self.launcher.terminate()
if self.launcher_log is not None:
self.launcher_log.close()
self.launcher_log = None
def launcher_diagnostics(self):
if self.launcher_log is None:
return ''
self.launcher_log.flush()
self.launcher_log.seek(0)
data = self.launcher_log.read()[-4000:].strip()
return '\n' + data if data else ''
def launch(self, arguments):
build = arguments.get('build')
if not isinstance(build, str) or not build:
raise SessionError('В launch требуется build с debug-пакетом')
socket_path = arguments.get('socket') or '/tmp/sprinter-sdbg-' + uuid.uuid4().hex + '.sock'
command = [sys.executable, str(Path(__file__).with_name('sdbg_launcher.py')),
'--build', build, '--socket', socket_path]
if arguments.get('mame'):
command.extend(['--mame', arguments['mame']])
if arguments.get('debugger'):
command.extend(['--debugger', arguments['debugger']])
if arguments.get('launchAt') is not None:
command.extend(['--launch-at', str(arguments['launchAt'])])
if arguments.get('dssTimeout') is not None:
command.extend(['--dss-timeout', str(arguments['dssTimeout'])])
for filename in arguments.get('data', []):
command.extend(['--data', filename])
self.launcher_log = tempfile.TemporaryFile(mode='w+t', encoding='utf-8')
try:
self.launcher = subprocess.Popen(command, stdout=subprocess.PIPE,
stderr=self.launcher_log, text=True)
except OSError:
self.launcher_log.close()
self.launcher_log = None
raise
try:
readable, _, _ = select.select([self.launcher.stdout], [], [], 75)
if not readable:
raise SessionError('Launcher не дошёл до main за 75 секунд' +
self.launcher_diagnostics())
line = self.launcher.stdout.readline()
if not line:
code = self.launcher.wait(timeout=5)
raise SessionError(f'Launcher завершился до готовности, rc={code}' +
self.launcher_diagnostics())
ready = json.loads(line)
if not ready.get('ready'):
raise SessionError('Launcher вернул неверный ready')
body, events = self.engine.handle('attach', {'socket': ready['socket']})
body['entry'] = ready['entry']
body['mamePid'] = ready['pid']
body['socket'] = ready['socket']
return body, events
except BaseException:
self.stop_launcher()
raise
def run(self):
stream = sys.stdin.buffer
while not self.stopping.is_set():
headers = {}
while True:
line = stream.readline()
if not line:
self.stopping.set()
return
if line in (b'\r\n', b'\n'):
break
name, value = line.decode('ascii').split(':', 1)
headers[name.lower()] = value.strip()
length = int(headers['content-length'])
if length > 1_048_576:
raise ValueError('DAP frame слишком велик')
request = json.loads(stream.read(length))
if request.get('type') == 'request':
self.request(request)
if __name__ == '__main__':
try:
Protocol().run()
except (ValueError, OSError, KeyError) as error:
print('sdbg-dap: ' + str(error), file=sys.stderr)
sys.exit(1)