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
+239
View File
@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""Живой stdio DAP-проход для проверки VS Code → launcher → MAME.
Запускает изолированный MAME и завершает только процессы этого репро.
"""
from __future__ import annotations
import json
import os
from pathlib import Path
import select
import subprocess
import sys
import time
ROOT = Path(__file__).resolve().parents[2]
PYTHON = Path.home()/'.pyenv/shims/python'
sys.path.insert(0, str(ROOT/'toolchain'))
from mame_interactive import resolve
from sdbg.server import rpc_call
def send(process, sequence, command, arguments=None):
body = json.dumps({'seq': sequence, 'type': 'request', 'command': command,
'arguments': arguments or {}}).encode()
frame = f'Content-Length: {len(body)}\r\n\r\n'.encode()+body
process.stdin.write(frame)
process.stdin.flush()
def receive(process, buffer, timeout):
deadline = time.monotonic()+timeout
while True:
separator = buffer.find(b'\r\n\r\n')
if separator >= 0:
header = buffer[:separator]
length = None
for line in header.split(b'\r\n'):
if line.lower().startswith(b'content-length:'):
length = int(line.split(b':', 1)[1].strip())
if length is None:
raise RuntimeError('DAP frame без Content-Length')
end = separator+4+length
if len(buffer) >= end:
return json.loads(buffer[separator+4:end]), buffer[end:]
remaining = deadline-time.monotonic()
if remaining <= 0:
raise TimeoutError('DAP response timeout')
readable, _, _ = select.select([process.stdout], [], [], remaining)
if not readable:
raise TimeoutError('DAP response timeout')
chunk = os.read(process.stdout.fileno(), 65536)
if not chunk:
raise RuntimeError(f'DAP завершился, rc={process.poll()}')
buffer += chunk
def wait_response(process, buffer, command, timeout):
deadline = time.monotonic()+timeout
events = []
while time.monotonic() < deadline:
message, buffer = receive(process, buffer, deadline-time.monotonic())
if message.get('type') == 'event':
events.append(message)
continue
if message.get('command') == command:
if not message.get('success'):
raise RuntimeError(f'{command}: {message.get("message")}')
return message, buffer, events
raise TimeoutError(command)
def wait_event(process, buffer, event, timeout):
deadline = time.monotonic()+timeout
while time.monotonic() < deadline:
try:
message, buffer = receive(process, buffer, deadline-time.monotonic())
except TimeoutError:
return None, buffer
if message.get('event') == 'terminated':
raise RuntimeError('DAP session завершилась во время '+event)
if message.get('event') == event:
return message, buffer
return None, buffer
def main():
if not PYTHON.is_file():
raise RuntimeError('Нет local pyenv shim: '+str(PYTHON))
build = ROOT/'tests/hello/.sprinter-cc-hello'
command = [str(PYTHON), str(ROOT/'toolchain/sdbg_dap.py')]
process = subprocess.Popen(command, cwd=ROOT, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
buffer = b''
try:
send(process, 1, 'initialize')
_, buffer, _ = wait_response(process, buffer, 'initialize', 10)
send(process, 2, 'launch', {'build': str(build), 'debugger': 'sdbg',
'dssTimeout': 30})
launched, buffer, events = wait_response(process, buffer, 'launch', 80)
if not any(item.get('event') == 'initialized' for item in events):
initialized, buffer = receive(process, buffer, 5)
if initialized.get('event') != 'initialized':
raise RuntimeError('Нет initialized: '+str(initialized))
send(process, 3, 'configurationDone')
_, buffer, _ = wait_response(process, buffer, 'configurationDone', 10)
send(process, 4, 'stackTrace')
frame_response, buffer, _ = wait_response(process, buffer, 'stackTrace', 10)
frame = frame_response['body']['stackFrames'][0]
if frame['name'] != 'main':
raise RuntimeError('Не main: '+str(frame))
if '--waitkey' in sys.argv:
send(process, 5, 'setBreakpoints', {
'source': {'path': str(ROOT/'tests/hello/hello.c')},
'breakpoints': [{'line': 62}],
})
points, buffer, _ = wait_response(process, buffer, 'setBreakpoints', 10)
if not points['body']['breakpoints'][0]['verified']:
raise RuntimeError('Не подтверждена точка getchar')
send(process, 6, 'continue')
_, buffer, _ = wait_response(process, buffer, 'continue', 10)
stopped, buffer = wait_event(process, buffer, 'stopped', 30)
if not stopped:
raise RuntimeError('Нет остановки перед getchar')
send(process, 7, 'stackTrace')
at_getchar, buffer, _ = wait_response(process, buffer, 'stackTrace', 10)
before = at_getchar['body']['stackFrames'][0]
if before['line'] != 62:
raise RuntimeError('Остановка не на getchar: '+str(before))
started = time.monotonic()
send(process, 8, 'next')
_, buffer, _ = wait_response(process, buffer, 'next', 3)
response_ms = round((time.monotonic()-started)*1000, 2)
if '--emulated-key' in sys.argv:
socket_path = launched['body']['socket']
first = rpc_call(socket_path, 'snapshot', timeout=5)
time.sleep(1)
second = rpc_call(socket_path, 'snapshot', timeout=5)
if second['paused'] or second['time'] <= first['time']:
raise RuntimeError('MAME не обновляет input frames: '+
str((first, second)))
tag, mask, _ = resolve('x')
rpc_call(socket_path, 'input_key',
{'tag': tag, 'mask': mask, 'down': True}, timeout=5)
time.sleep(.15)
rpc_call(socket_path, 'input_key',
{'tag': tag, 'mask': mask, 'down': False}, timeout=5)
key_stop, buffer = wait_event(process, buffer, 'stopped', 10)
if not key_stop:
raise RuntimeError('PC keyboard ioport не завершил WAITKEY')
send(process, 9, 'stackTrace')
after_response, buffer, _ = wait_response(process, buffer, 'stackTrace', 10)
after = after_response['body']['stackFrames'][0]
if after['line'] != 63:
raise RuntimeError('После клавиши ожидалась строка 63: '+str(after))
ascii_code = rpc_call(socket_path, 'registers', timeout=5)
ascii_code = ascii_code['registers']['DE'] & 0xff
if ascii_code != ord('x'):
raise RuntimeError(f'WAITKEY вернул {ascii_code:#x}, ожидался x (0x78)')
print(json.dumps({'event': 'mame_keyboard_port_verified',
'at': before, 'after': after,
'ascii': ascii_code,
'emulated_time_advanced': second['time']-first['time'],
'next_response_ms': response_ms},
ensure_ascii=False), flush=True)
send(process, 10, 'disconnect')
wait_response(process, buffer, 'disconnect', 10)
return 0
if '--manual-key' in sys.argv:
socket_path = launched['body']['socket']
keyboard_state = rpc_call(socket_path, 'snapshot', timeout=5)['keyboards']
if keyboard_state.get(':kbd:ms_naturl') is not True:
raise RuntimeError('Физическая PC-клавиатура MAME выключена: '+
str(keyboard_state))
print(json.dumps({'event': 'manual_key_ready',
'mame_pid': launched['body']['mamePid'],
'keyboards': keyboard_state,
'instruction': 'Click this MAME Sprinter window and press x'},
ensure_ascii=False), flush=True)
key_stop, buffer = wait_event(process, buffer, 'stopped', 45)
if not key_stop:
raise RuntimeError('Ручное нажатие x не завершило WAITKEY за 45 секунд')
send(process, 9, 'stackTrace')
after_response, buffer, _ = wait_response(process, buffer, 'stackTrace', 10)
after = after_response['body']['stackFrames'][0]
if after['line'] != 63:
raise RuntimeError('После клавиши ожидалась строка 63: '+str(after))
ascii_code = rpc_call(socket_path, 'registers', timeout=5)
ascii_code = ascii_code['registers']['DE'] & 0xff
if ascii_code != ord('x'):
raise RuntimeError(f'WAITKEY вернул {ascii_code:#x}, ожидался x (0x78)')
print(json.dumps({'event': 'physical_mame_key_verified',
'at': before, 'after': after,
'ascii': ascii_code,
'next_response_ms': response_ms},
ensure_ascii=False), flush=True)
send(process, 10, 'disconnect')
wait_response(process, buffer, 'disconnect', 10)
return 0
early, buffer = wait_event(process, buffer, 'stopped', 12)
if early:
raise RuntimeError('getchar завершился без нового ввода: '+str(early))
send(process, 9, 'pause')
_, buffer, pause_events = wait_response(process, buffer, 'pause', 6)
if not any(item.get('event') == 'stopped' for item in pause_events):
paused, buffer = wait_event(process, buffer, 'stopped', 6)
if not paused:
raise RuntimeError('Pause недоступен во время WAITKEY')
print(json.dumps({'event': 'waitkey_step_remains_responsive',
'at': before, 'next_response_ms': response_ms,
'wait_without_key_s': 12, 'pause': 'verified'},
ensure_ascii=False), flush=True)
send(process, 10, 'disconnect')
wait_response(process, buffer, 'disconnect', 10)
return 0
print(json.dumps({'event': 'vscode_dap_launch_verified',
'entry': launched['body']['entry'],
'frame': frame}, ensure_ascii=False), flush=True)
send(process, 5, 'disconnect')
wait_response(process, buffer, 'disconnect', 10)
return 0
finally:
if process.poll() is None:
process.terminate()
try:
_, stderr = process.communicate(timeout=8)
except subprocess.TimeoutExpired:
process.kill()
_, stderr = process.communicate()
if stderr:
print(stderr.decode(errors='replace')[-4000:], file=sys.stderr)
if __name__ == '__main__':
try:
sys.exit(main())
except (RuntimeError, TimeoutError, OSError, KeyError) as error:
print('vscode-dap-probe: '+str(error), file=sys.stderr)
sys.exit(1)