Sprinter: добавить отладку C-исходников и интеграцию VS Code
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Изолированный DSS→EXE→main launcher для DAP/session server."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from contextlib import redirect_stdout
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT/'toolchain'))
|
||||
from mame_interactive import basename_83, build_events
|
||||
from sdbg.image import read_ihx
|
||||
from sdbg.model import DebugMap
|
||||
|
||||
|
||||
def disk_entries(exe: Path, data) -> list[tuple[str, str]]:
|
||||
paths = [Path(exe), *(Path(name).resolve() for name in data)]
|
||||
if any(not path.is_file() for path in paths):
|
||||
missing = next(path for path in paths if not path.is_file())
|
||||
raise ValueError('Не найден файл для debug-дискеты: ' + str(missing))
|
||||
entries = [(basename_83(path.name), str(path)) for path in paths]
|
||||
names = [name.replace(' ', '') for name, _ in entries]
|
||||
if len(set(names)) != len(names):
|
||||
raise ValueError('Коллизия имён 8.3 на debug-дискете')
|
||||
clusters = sum((path.stat().st_size + 511) // 512 for path in paths)
|
||||
if len(paths) > 224 or clusters > 2847:
|
||||
raise ValueError('Файлы не помещаются на FAT12 debug-дискету')
|
||||
return entries
|
||||
|
||||
|
||||
def write_keyboard_config(cfg_directory: Path) -> None:
|
||||
"""Включить обе клавиатуры Sprinter для физического ввода в DSS."""
|
||||
(cfg_directory/'sprinter.cfg').write_text(
|
||||
'<?xml version="1.0"?>\n'
|
||||
'<mameconfig version="10">\n'
|
||||
' <system name="sprinter">\n'
|
||||
' <input>\n'
|
||||
' <keyboard tag=":" enabled="1" />\n'
|
||||
' <keyboard tag=":kbd:ms_naturl" enabled="1" />\n'
|
||||
' </input>\n'
|
||||
' </system>\n'
|
||||
'</mameconfig>\n', encoding='utf-8')
|
||||
|
||||
|
||||
def lua_script(path, ready, main_address, signature, events, launch_at,
|
||||
dss_timeout=30):
|
||||
rows = ',\n'.join('{'+f'{t},"{tag}",{mask},{value}'+'}'
|
||||
for t,tag,mask,value in events)
|
||||
text = '''if _G.sdbg_launcher_loaded then return end
|
||||
_G.sdbg_launcher_loaded=true
|
||||
local machine=manager.machine
|
||||
local cpu=machine.devices[":maincpu"]
|
||||
local debug=machine.debugger
|
||||
local events={EVENTS}
|
||||
local signature={SIGNATURE}
|
||||
local index=1
|
||||
local phase="booting"
|
||||
local active=true
|
||||
local bp=nil
|
||||
local false_hits=0
|
||||
local launch_started=nil
|
||||
local prompt_since=nil
|
||||
local prompt_row=nil
|
||||
local function now() local t=machine.time;return t.seconds+t.attoseconds/1e18 end
|
||||
local vram=nil
|
||||
for tag,share in pairs(machine.memory.shares) do
|
||||
if tag:find("vram",1,true) then vram=share;break end
|
||||
end
|
||||
local function dss_prompt_row()
|
||||
if not vram then return nil end
|
||||
local rgmod=cpu.state.RGMOD.value & 1
|
||||
local function char_at(row,col)
|
||||
return vram:read_u8((1+col+0x80*rgmod)*1024+0x300+row*4+1)
|
||||
end
|
||||
for row=0,31 do
|
||||
local drive=char_at(row,0)
|
||||
if ((drive>=65 and drive<=90) or (drive>=97 and drive<=122)) and
|
||||
char_at(row,1)==58 then
|
||||
for col=2,38 do
|
||||
local value=char_at(row,col)
|
||||
if value==62 then
|
||||
local clean=true
|
||||
for tail=col+1,math.min(col+8,79) do
|
||||
local after=char_at(row,tail)
|
||||
if after~=0 and after~=32 and after~=95 then clean=false;break end
|
||||
end
|
||||
if clean then return row end
|
||||
elseif value<32 then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
_G.sdbg_launcher_reset_subscription=emu.add_machine_reset_notifier(function()
|
||||
if phase~="booting" then active=false end
|
||||
end)
|
||||
debug.execution_state="run"
|
||||
emu.register_periodic(function()
|
||||
if not active then return end
|
||||
local current=now()
|
||||
if phase=="booting" then
|
||||
local row=nil
|
||||
if current>=LAUNCH_NOT_BEFORE then row=dss_prompt_row() end
|
||||
if row then
|
||||
if prompt_row~=row then prompt_row=row;prompt_since=current end
|
||||
if current-prompt_since>=0.25 then
|
||||
machine.video:snapshot()
|
||||
bp=cpu.debug:bpset(MAIN,"","")
|
||||
launch_started=current
|
||||
phase="loading"
|
||||
end
|
||||
else
|
||||
prompt_row=nil;prompt_since=nil
|
||||
end
|
||||
end
|
||||
while phase=="loading" and index<=#events and current>=launch_started+events[index][1] do
|
||||
local e=events[index]
|
||||
for _,field in pairs(machine.ioport.ports[e[2]].fields) do
|
||||
if field.mask==e[3] then field:set_value(e[4]);break end
|
||||
end
|
||||
index=index+1
|
||||
end
|
||||
if phase=="loading" and debug.execution_state=="stop" then
|
||||
local pc=cpu.state.PC.value
|
||||
local match=pc==MAIN
|
||||
for i,b in ipairs(signature) do
|
||||
if cpu.spaces.program:read_u8(0x10000+MAIN+i-1)~=b then match=false end
|
||||
end
|
||||
if not match then false_hits=false_hits+1;debug.execution_state="run";return end
|
||||
cpu.debug:bpclear(bp)
|
||||
local file=assert(io.open(READY..".tmp","wb"))
|
||||
file:write('{"pc":'..pc..',"false_hits":'..false_hits..
|
||||
',"dss_ready_time":'..launch_started..',"prompt_row":'..prompt_row..'}')
|
||||
file:close();assert(os.rename(READY..".tmp",READY))
|
||||
phase="attached"
|
||||
end
|
||||
if phase=="booting" and current>DSS_TIMEOUT then
|
||||
local file=io.open(READY..".error","wb")
|
||||
if file then file:write("DSS prompt timeout");file:close() end
|
||||
machine:exit()
|
||||
elseif phase=="loading" and current>launch_started+30 then
|
||||
local file=io.open(READY..".error","wb")
|
||||
if file then file:write("main timeout");file:close() end
|
||||
machine:exit()
|
||||
end
|
||||
end)
|
||||
'''
|
||||
text = (text.replace('EVENTS',rows).replace('SIGNATURE',','.join(map(str,signature)))
|
||||
.replace('LAUNCH_NOT_BEFORE',str(launch_at)).replace('MAIN',str(main_address))
|
||||
.replace('READY',json.dumps(str(ready))).replace('DSS_TIMEOUT',str(dss_timeout)))
|
||||
path.write_text(text)
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--build',required=True)
|
||||
parser.add_argument('--socket',required=True)
|
||||
parser.add_argument('--data',action='append',default=[])
|
||||
parser.add_argument('--launch-at',type=float,default=0,
|
||||
help='не начинать ввод раньше этой секунды эмуляции')
|
||||
parser.add_argument('--dss-timeout',type=float,default=30,
|
||||
help='таймаут появления стабильного prompt DSS')
|
||||
parser.add_argument('--mame',default=str(ROOT/'mame/v306/mame.arm'))
|
||||
parser.add_argument('--debugger',default='sdbg',
|
||||
help='OSD debugger provider (по умолчанию sdbg из project patch)')
|
||||
args=parser.parse_args()
|
||||
model=DebugMap(args.build)
|
||||
model.verify_executable()
|
||||
exe=Path(model.manifest['executable_path'])
|
||||
functions=[item for item in model.functions if item['name']=='main']
|
||||
function=functions[0] if len(functions)==1 else None
|
||||
if not function or function['bank'] is not None:
|
||||
raise ValueError('Нужна единственная resident-функция main')
|
||||
image=read_ihx(model.directory/(exe.stem+'.ihx'))
|
||||
signature=bytes(image[address] for address in range(
|
||||
function['start'],min(function['end'],function['start']+24)))
|
||||
if not signature:
|
||||
raise ValueError('У main нет проверяемой сигнатуры')
|
||||
mame=Path(args.mame).resolve()
|
||||
mame_dir=mame.parent
|
||||
stopping=False
|
||||
def stop(*_):
|
||||
nonlocal stopping
|
||||
stopping=True
|
||||
signal.signal(signal.SIGTERM,stop)
|
||||
signal.signal(signal.SIGINT,stop)
|
||||
with tempfile.TemporaryDirectory(prefix='sprinter-sdbg-launch-') as temporary:
|
||||
state=Path(temporary)
|
||||
ipc=state/'ipc';ipc.mkdir()
|
||||
for name in ('nvram','cfg','diff','snapshot'): (state/name).mkdir()
|
||||
write_keyboard_config(state/'cfg')
|
||||
system=state/'system.chd'
|
||||
shutil.copyfile(mame_dir/'IMG/sp_hdd_sys.chd',system)
|
||||
disk_path=state/'debug.img'
|
||||
spec=importlib.util.spec_from_file_location('make_disk',mame_dir/'make_disk.py')
|
||||
disk=importlib.util.module_from_spec(spec);spec.loader.exec_module(disk)
|
||||
files=disk_entries(exe,args.data)
|
||||
with redirect_stdout(sys.stderr):
|
||||
if not disk.create_floppy_image(str(disk_path),files):
|
||||
raise RuntimeError('Не удалось создать debug-дискету')
|
||||
command_text='a:\\'+basename_83(exe.name).replace(' ','')+'\n'
|
||||
events=build_events([(0,command_text)])
|
||||
ready=state/'main.json'
|
||||
lua=state/'launch.lua'
|
||||
lua_script(lua,ready,function['start'],signature,events,args.launch_at,
|
||||
args.dss_timeout)
|
||||
session_id=uuid.uuid4().hex
|
||||
environment=dict(os.environ,SDBG_IPC_DIR=str(ipc),SDBG_SESSION_ID=session_id)
|
||||
command=[str(mame),'sprinter','-noreadconfig','-rompath',str(mame_dir/'roms'),
|
||||
'-bios','v3.06','-kbd','ms_naturl,bios=sp2k','-video','soft','-window',
|
||||
'-sound','none','-skip_gameinfo','-beta:wd179x:0','35hd',
|
||||
'-flop1',str(disk_path),'-hard1',str(system),'-debug','-debugger',args.debugger,
|
||||
'-plugin','sdbgbridge','-pluginspath',str(ROOT/'toolchain/mcp')+';'+str(ROOT/'mame/sources/MAME/plugins'),
|
||||
'-autoboot_delay','0','-autoboot_script',str(lua)]
|
||||
for name in ('nvram','cfg','diff','snapshot'):
|
||||
command.extend(['-'+name+'_directory',str(state/name)])
|
||||
log=(state/'mame.log').open('w')
|
||||
mame_process=subprocess.Popen(command,cwd=state,env=environment,
|
||||
stdout=log,stderr=subprocess.STDOUT)
|
||||
server=None
|
||||
def mame_diagnostics():
|
||||
log.flush()
|
||||
output=(state/'mame.log').read_text(errors='replace')[-4000:].strip()
|
||||
return ('\nMAME log:\n'+output) if output else ''
|
||||
try:
|
||||
deadline=time.monotonic()+args.dss_timeout+45
|
||||
while not ready.exists():
|
||||
if mame_process.poll() is not None:
|
||||
raise RuntimeError('MAME завершился до main; лог: '+
|
||||
str(state/'mame.log')+mame_diagnostics())
|
||||
if (state/'main.json.error').exists() or time.monotonic()>deadline:
|
||||
raise RuntimeError('Таймаут ожидания main; лог: '+
|
||||
str(state/'mame.log')+mame_diagnostics())
|
||||
time.sleep(.05)
|
||||
entry=json.loads(ready.read_text())
|
||||
server=subprocess.Popen([sys.executable,str(ROOT/'toolchain/sdbg_server.py'),
|
||||
'--build',str(model.directory),'--ipc',str(ipc),'--session',session_id,
|
||||
'--socket',args.socket],stdout=subprocess.PIPE,stderr=sys.stderr,text=True)
|
||||
line=server.stdout.readline()
|
||||
if not line:
|
||||
raise RuntimeError('Session server не запустился')
|
||||
server_ready=json.loads(line)
|
||||
print(json.dumps({'ready':True,'socket':args.socket,'pid':mame_process.pid,
|
||||
'entry':entry,'build_id':server_ready['build_id']},
|
||||
ensure_ascii=False),flush=True)
|
||||
while not stopping and mame_process.poll() is None and server.poll() is None:
|
||||
if os.getppid()==1: break
|
||||
time.sleep(.1)
|
||||
finally:
|
||||
if server is not None and server.poll() is None:
|
||||
server.terminate()
|
||||
try: server.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired: server.kill();server.wait()
|
||||
if mame_process.poll() is None:
|
||||
mame_process.terminate()
|
||||
try: mame_process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired: mame_process.kill();mame_process.wait()
|
||||
log.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
try:
|
||||
sys.exit(main())
|
||||
except (ValueError,OSError,RuntimeError,KeyError,subprocess.SubprocessError) as error:
|
||||
print('sdbg-launcher: '+str(error),file=sys.stderr)
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user