174 lines
6.9 KiB
Python
174 lines
6.9 KiB
Python
#!/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']])
|
|
profile_arguments = {
|
|
'mameHome': '--mame-home',
|
|
'mameBin': '--mame-bin',
|
|
'mameRompath': '--mame-rompath',
|
|
'mameDssImage': '--mame-dss-image',
|
|
'mameSystemHddImage': '--mame-system-hdd-image',
|
|
'mameBios': '--mame-bios',
|
|
}
|
|
for key, option in profile_arguments.items():
|
|
if arguments.get(key):
|
|
command.extend([option, arguments[key]])
|
|
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)
|