#!/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)