Sprinter: добавить отладку C-исходников и интеграцию VS Code
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
-- license:BSD-3-Clause
|
||||
-- Изолированный backend sdbg. Один каталог/процесс-владелец на сессию.
|
||||
-- Не загружать одновременно с другим мостом управления тем же CPU.
|
||||
local exports = { name="sdbgbridge", version="0.1.0", description="Sprinter source debugger",
|
||||
license="BSD-3-Clause", author={name="Sprinter C-Compiler contributors"} }
|
||||
function exports.startplugin()
|
||||
local json = require("json")
|
||||
local lfs = require("lfs")
|
||||
local directory = assert(os.getenv("SDBG_IPC_DIR"), "Нужен SDBG_IPC_DIR")
|
||||
local session = assert(os.getenv("SDBG_SESSION_ID"), "Нужен SDBG_SESSION_ID")
|
||||
local generation, sequence = 0, 0
|
||||
local events, owned = {}, {}
|
||||
local state, pending = "initializing", nil
|
||||
local invalidated=false
|
||||
local session_started=false
|
||||
local registers = {"PC","SP","AF","BC","DE","HL","IX","IY","AF2","BC2","DE2","HL2",
|
||||
"I","R","IM","IFF1","IFF2","PG0","PG1","PG2","PG3","CNF","7FFD","1FFD"}
|
||||
local function machine() return manager.machine end
|
||||
local function cpu() return machine().devices[":maincpu"] end
|
||||
local function now()
|
||||
local t=machine().time
|
||||
return t.seconds+t.attoseconds/1e18
|
||||
end
|
||||
local function event(kind, body)
|
||||
sequence=sequence+1
|
||||
events[#events+1]={seq=sequence,kind=kind,generation=generation,body=body or {}}
|
||||
if #events>512 then table.remove(events,1) end
|
||||
end
|
||||
local function atomic(path, value)
|
||||
local file=assert(io.open(path..".tmp","wb"))
|
||||
file:write(json.stringify(value));file:close()
|
||||
assert(os.rename(path..".tmp",path))
|
||||
end
|
||||
local function update()
|
||||
if invalidated then return end
|
||||
local observed=machine().debugger.execution_state=="stop" and "stopped" or "running"
|
||||
-- step() исполняется лишь после возврата periodic callback.
|
||||
if pending and pending.kind:match("^step") and now()<=pending.time then return end
|
||||
if observed~=state or (pending and observed=="stopped") then
|
||||
state=observed
|
||||
generation=generation+1
|
||||
local body={pc=cpu().state.PC.value}
|
||||
if pending then body.reason=pending.kind end
|
||||
event(state,body)
|
||||
if state=="stopped" then pending=nil end
|
||||
end
|
||||
end
|
||||
local function stopped(request)
|
||||
assert(state=="stopped" and not pending,"CPU не остановлен")
|
||||
assert(request.generation==generation,"Устаревшая generation")
|
||||
end
|
||||
local function number(value, limit)
|
||||
assert(type(value)=="number" and value%1==0 and value>=0 and value<=limit,"Недопустимое число")
|
||||
return value
|
||||
end
|
||||
local invalidate
|
||||
local function dispatch(request)
|
||||
assert(request.session==session,"Чужая сессия")
|
||||
local args=request.args or {}
|
||||
local command=request.command
|
||||
if invalidated and command~="hello" and command~="snapshot" and command~="events" then
|
||||
error("Сессия инвалидирована reset/load")
|
||||
end
|
||||
if command=="hello" then
|
||||
session_started=true
|
||||
return {protocol=1,session=session,capabilities={snapshot=true,memory=true,
|
||||
instruction_step=true,step_over=true,step_out=true,
|
||||
bank_guard=true,deferred_breakpoints=true,console_print=true},state=state}
|
||||
elseif command=="snapshot" then
|
||||
local result={state=state,time=now(),paused=machine().paused}
|
||||
result.keyboards=setmetatable({}, {__jsontype="object"})
|
||||
for tag,kbd in pairs(machine().natkeyboard.keyboards) do
|
||||
result.keyboards[tag]=kbd.enabled
|
||||
end
|
||||
if state=="stopped" and not pending then
|
||||
result.registers=setmetatable({}, {__jsontype="object"})
|
||||
for _,name in ipairs(registers) do
|
||||
if cpu().state[name] then result.registers[name]=cpu().state[name].value end
|
||||
end
|
||||
end
|
||||
return result
|
||||
elseif command=="key" then
|
||||
-- Этот путь нужен для воспроизводимых UI-тестов; обычная клавиатура
|
||||
-- хоста остаётся у MAME и не проходит через bridge.
|
||||
assert(state=="running" or (state=="stopped" and args.down==false),
|
||||
"Нажатие принимается при running CPU, отпускание также при stopped")
|
||||
local tag=args.tag
|
||||
assert(type(tag)=="string" and tag:match("^:kbd:ms_naturl:P%d+%.%d+$"),
|
||||
"Разрешены только порты PC-клавиатуры Sprinter")
|
||||
local mask=number(args.mask,0xffff)
|
||||
assert(type(args.down)=="boolean","Нужен флаг down")
|
||||
local port=machine().ioport.ports[tag]
|
||||
assert(port,"Порт клавиатуры не найден")
|
||||
for _,field in pairs(port.fields) do
|
||||
if field.mask==mask then
|
||||
field:set_value(args.down and 1 or 0)
|
||||
return {accepted=true,tag=tag,mask=mask,down=args.down}
|
||||
end
|
||||
end
|
||||
error("Поле клавиатуры не найдено")
|
||||
elseif command=="events" then
|
||||
local result={events={},last=sequence,first=events[1] and events[1].seq or sequence+1}
|
||||
for _,item in ipairs(events) do
|
||||
if item.seq>(args.after or 0) then result.events[#result.events+1]=item end
|
||||
end
|
||||
return result
|
||||
elseif command=="console_tail" then
|
||||
local log=machine().debugger.consolelog
|
||||
local total=#log
|
||||
local count=number(args.count or 40,200)
|
||||
local lines={}
|
||||
for index=math.max(1,total-count+1),total do
|
||||
lines[#lines+1]=log[index]
|
||||
end
|
||||
return {total=total,lines=lines}
|
||||
elseif command=="pause" then
|
||||
if state~="stopped" then
|
||||
pending={kind="pause",time=now()}
|
||||
machine().debugger.execution_state="stop"
|
||||
end
|
||||
return {accepted=true}
|
||||
elseif command=="continue" or command=="step" or command=="step_over" or
|
||||
command=="step_out" then
|
||||
stopped(request)
|
||||
generation=generation+1
|
||||
state="running"
|
||||
event("running",{reason=command})
|
||||
if command=="step" then
|
||||
pending={kind="step",time=now()}
|
||||
cpu().debug:step(1)
|
||||
elseif command=="step_over" then
|
||||
pending={kind="step_over",time=now()}
|
||||
machine().debugger:command("over 1")
|
||||
elseif command=="step_out" then
|
||||
pending={kind="step_out",time=now()}
|
||||
machine().debugger:command("out")
|
||||
else machine().debugger.execution_state="run" end
|
||||
return {accepted=true}
|
||||
elseif command=="memory" then
|
||||
stopped(request)
|
||||
local address=number(args.address,0xffff)
|
||||
assert(args.enabled==nil or type(args.enabled)=="boolean","Неверный enabled")
|
||||
local length=number(args.length,4096)
|
||||
assert(address+length<=0x10000,"Чтение за пределами logical memory")
|
||||
local symbols=emu.symbol_table(cpu())
|
||||
local bytes={}
|
||||
for offset=0,length-1 do
|
||||
-- Этот интерфейс отключает side effects в отличие от space:read_u8.
|
||||
bytes[#bytes+1]=string.format("%02x",symbols:memory_value(":maincpu","p",address+offset,1,true))
|
||||
end
|
||||
return {hex=table.concat(bytes)}
|
||||
elseif command=="console_print" then
|
||||
stopped(request)
|
||||
local text=args.text
|
||||
assert(type(text)=="string" and #text>=1 and #text<=2048,
|
||||
"Нужна строка журнала до 2048 байт")
|
||||
-- В debugger printf уже отформатированный текст — только данные.
|
||||
-- Убираем управляющие символы и экранируем синтаксис команды/формата.
|
||||
local safe=text:gsub("[%c]"," "):gsub("\\","\\\\")
|
||||
:gsub("%%","%%%%"):gsub('"',"'")
|
||||
machine().debugger:command('printf "'..safe..'"')
|
||||
return {printed=true}
|
||||
elseif command=="breakpoint" then
|
||||
stopped(request)
|
||||
local address=number(args.address,0xffff)
|
||||
local condition=""
|
||||
if args.window~=nil or args.page~=nil then
|
||||
local window=number(args.window,3)
|
||||
local page=number(args.page,255)
|
||||
local ports={[0]=0x82,[1]=0xa2,[2]=0xc2,[3]=0xe2}
|
||||
-- Дополнительные PG state entries драйвера Sprinter не входят в
|
||||
-- expression table CPU. Читаем штатный page-port без side effects;
|
||||
-- числа debugger expression по умолчанию шестнадцатеричные.
|
||||
condition=string.format("ib@%x==%x",ports[window],page)
|
||||
end
|
||||
local id=cpu().debug:bpset(address,condition,"")
|
||||
owned[id]={address=address,condition=condition}
|
||||
if args.enabled==false then cpu().debug:bpdisable(id) end
|
||||
return {id=id,condition=condition,enabled=args.enabled~=false}
|
||||
elseif command=="activate_breakpoints" then
|
||||
stopped(request)
|
||||
local count=0
|
||||
for id in pairs(owned) do
|
||||
if cpu().debug:bpenable(id) then count=count+1 end
|
||||
end
|
||||
return {enabled=count}
|
||||
elseif command=="deactivate_breakpoints" then
|
||||
stopped(request)
|
||||
local count=0
|
||||
for id in pairs(owned) do
|
||||
if cpu().debug:bpdisable(id) then count=count+1 end
|
||||
end
|
||||
return {disabled=count}
|
||||
elseif command=="clear" then
|
||||
stopped(request)
|
||||
local id=number(args.id,0x7fffffff)
|
||||
assert(owned[id],"Точка не принадлежит sdbg")
|
||||
cpu().debug:bpclear(id);owned[id]=nil
|
||||
return {cleared=id}
|
||||
else error("Неподдержанная команда: "..tostring(command)) end
|
||||
end
|
||||
invalidate=function(reason)
|
||||
if invalidated then return end
|
||||
if not session_started then return end
|
||||
if reason=="state_load" then
|
||||
for id in pairs(owned) do cpu().debug:bpclear(id) end
|
||||
end
|
||||
owned={}
|
||||
invalidated=true
|
||||
generation=generation+1
|
||||
pending=nil
|
||||
state="invalidated"
|
||||
event("invalidated",{reason=reason})
|
||||
end
|
||||
-- Subscription-объекты надо удерживать: иначе Lua GC снимет callback.
|
||||
exports._subscriptions={
|
||||
emu.add_machine_reset_notifier(function() invalidate("reset") end),
|
||||
emu.add_machine_post_load_notifier(function() invalidate("state_load") end)
|
||||
}
|
||||
emu.register_periodic(function()
|
||||
if not machine() or not machine().debugger then return end
|
||||
update()
|
||||
for name in lfs.dir(directory) do
|
||||
local id=name:match("^req_(%d+)%.json$")
|
||||
if id then
|
||||
local path=directory.."/"..name
|
||||
local file=io.open(path,"rb")
|
||||
if file then
|
||||
local data=file:read("*a");file:close()
|
||||
local ok,request=pcall(json.parse,data)
|
||||
local result
|
||||
if ok and type(request)=="table" then ok,result=pcall(dispatch,request)
|
||||
else result="Неверный JSON";ok=false end
|
||||
atomic(directory.."/resp_"..id..".json",{ok=ok,generation=generation,
|
||||
result=ok and result or nil,error=not ok and tostring(result) or nil,session=session})
|
||||
os.remove(path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
return exports
|
||||
Reference in New Issue
Block a user