252 lines
12 KiB
Python
252 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""Живой stdio DAP-проход для проверки VS Code → launcher → MAME.
|
|
|
|
Запускает изолированный MAME и завершает только процессы этого репро.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
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():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--build', default=None)
|
|
parser.add_argument('--app-hdd', default=None)
|
|
parser.add_argument('--launch-path', default=None)
|
|
options, _ = parser.parse_known_args()
|
|
if not PYTHON.is_file():
|
|
raise RuntimeError('Нет local pyenv shim: '+str(PYTHON))
|
|
build = Path(options.build).resolve() if options.build else \
|
|
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)
|
|
debugger = 'osx' if '--osx' in sys.argv else 'sdbg'
|
|
launch = {'build': str(build), 'debugger': debugger, 'dssTimeout': 30}
|
|
if options.app_hdd:
|
|
launch['appHdd'] = str(Path(options.app_hdd).resolve())
|
|
if options.launch_path:
|
|
launch['launchPath'] = options.launch_path
|
|
send(process, 2, 'launch', launch)
|
|
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)
|