luaforwindows/SciTE/scite-debug/remDebug/remdebug/engine.lua

375 lines
9.3 KiB
Lua
Executable File

--
-- RemDebug 1.0 Beta
-- Copyright Kepler Project OK5 (http://www.keplerproject.org/remdebug)
--
local socket = require"socket"
local lfs = require"lfs"
local debug = require"debug"
module("remdebug.engine", package.seeall)
_COPYRIGHT = "2006 - Kepler Project"
_DESCRIPTION = "Remote Debugger for the Lua programming language"
_VERSION = "1.0"
local UNIX = string.sub(lfs.currentdir(),1,1) == '/'
if not UNIX then
local global_print = print
function print(...)
global_print(...)
io.stdout:flush()
end
end
-- Some 'pretty printing' code. In particular, it will try to expand tables, up to
-- a specified number of elements.
-- obviously table.concat is much more efficient, but requires that the table values
-- be strings.
function join(tbl,delim,start,finish)
local n = table.getn(tbl)
local res = ''
local k = 0
-- this is a hack to work out if a table is 'list-like' or 'map-like'
local index1 = n > 0 and tbl[1] ~= nil
local index2 = n > 1 and tbl[2] ~= nil
if index1 and index2 then
for i,v in ipairs(tbl) do
res = res..delim..tostring(v)
k = k + 1
if k > finish then
res = res.." ... "
end
end
else
for i,v in pairs(tbl) do
res = res..delim..tostring(i)..'='..tostring(v)
k = k + 1
if k > finish then
res = res.." ... "
end
end
end
return string.sub(res,2)
end
function expand_value(val)
if type(val) == 'table' then
if val.__tostring then
return tostring(val)
else
return '{'..join(val,',',1,20)..'}'
end
elseif type(val) == 'string' then
return "'"..val.."'"
else
return val
end
end
local coro_debugger
local events = { BREAK = 1, WATCH = 2 }
local breakpoints = {}
local watches = {}
local step_into = false
local step_over = false
local step_level = 0
local stack_level = 0
local controller_host = "localhost"
local controller_port = 8171
local function set_breakpoint(file, line)
if not breakpoints[file] then
breakpoints[file] = {}
end
breakpoints[file][line] = true
end
local function remove_breakpoint(file, line)
if breakpoints[file] then
breakpoints[file][line] = nil
end
end
local function has_breakpoint(file, line)
return breakpoints[file] and breakpoints[file][line]
end
local function restore_vars(vars)
if type(vars) ~= 'table' then return end
local func = debug.getinfo(3, "f").func
local i = 1
local written_vars = {}
while true do
local name = debug.getlocal(3, i)
if not name then break end
debug.setlocal(3, i, vars[name])
written_vars[name] = true
i = i + 1
end
i = 1
while true do
local name = debug.getupvalue(func, i)
if not name then break end
if not written_vars[name] then
debug.setupvalue(func, i, vars[name])
written_vars[name] = true
end
i = i + 1
end
end
local function capture_vars()
local vars = {}
local func = debug.getinfo(3, "f").func
local i = 1
while true do
local name, value = debug.getupvalue(func, i)
if not name then break end
vars[name] = value
i = i + 1
end
i = 1
while true do
local name, value = debug.getlocal(3, i)
if not name then break end
vars[name] = value
i = i + 1
end
setmetatable(vars, { __index = getfenv(func), __newindex = getfenv(func) })
return vars
end
local function break_dir(path)
local paths = {}
path = string.gsub(path, "\\", "/")
for w in string.gfind(path, "[^\/]+") do
table.insert(paths, w)
end
return paths
end
local function merge_paths(path1, path2)
-- check if path is already absolute
if UNIX then
if string.sub(path2,1,1) == '/' then
return path2
end
else
if string.sub(path2,2,2) == ':' then
return path2:gsub('\\','/')
end
end
local paths1 = break_dir(path1)
local paths2 = break_dir(path2)
for i, path in ipairs(paths2) do
if path == ".." then
table.remove(paths1, table.getn(paths1))
elseif path ~= "." then
table.insert(paths1, path)
end
end
local res = table.concat(paths1, "/")
if UNIX then
return "/"..res
else
return res
end
end
local function debug_hook(event, line)
if event == "call" then
stack_level = stack_level + 1
elseif event == "return" then
stack_level = stack_level - 1
else
local file = debug.getinfo(2, "S").source
if string.find(file, "@") == 1 then
file = string.sub(file, 2)
end
file = merge_paths(lfs.currentdir(), file)
local vars = capture_vars()
table.foreach(watches, function (index, value)
setfenv(value, vars)
local status, res = pcall(value)
if status and res then
coroutine.resume(coro_debugger, events.WATCH, vars, file, line, index)
end
end)
if step_into or (step_over and stack_level <= step_level) or has_breakpoint(file, line) then
step_into = false
step_over = false
coroutine.resume(coro_debugger, events.BREAK, vars, file, line)
restore_vars(vars)
end
end
end
--- protocol response helpers
local function bad_request(server)
server:send("400 Bad Request\n") -- check this!
end
local function OK(server,res)
if res then
if type(res) == 'string' then
server:send("200 OK "..string.len(res).."\n")
server:send(res)
else
server:send("200 OK "..res.."\n")
end
else
server:send("200 OK\n")
end
end
local function pause(server,file,line,idx_watch)
if not idx_watch then
server:send("202 Paused " .. file .. " " .. line .. "\n")
else
server:send("203 Paused " .. file .. " " .. line .. " " .. idx_watch .. "\n")
end
end
local function error(server,type,res)
server:send("401 Error in "..type.." " .. string.len(res) .. "\n")
server:send(res)
end
local function debugger_loop(server)
local command
local eval_env = {}
while true do
local line, status = server:receive()
command = string.sub(line, string.find(line, "^[A-Z]+"))
--~ print('engine',command)
if command == "SETB" then
local _, _, _, filename, line = string.find(line, "^([A-Z]+)%s+([%w%p]+)%s+(%d+)$")
if filename and line then
set_breakpoint(filename, tonumber(line))
OK(server)
else
bad_request(server)
end
elseif command == "DELB" then
local _, _, _, filename, line = string.find(line, "^([A-Z]+)%s+([%w%p]+)%s+(%d+)$")
if filename and line then
remove_breakpoint(filename, tonumber(line))
OK(server)
else
bad_request(server)
end
elseif command == "EXEC" then
local _, _, chunk = string.find(line, "^[A-Z]+%s+(.+)$")
if chunk then
local func = loadstring(chunk)
local status, res
if func then
setfenv(func, eval_env)
status, res = xpcall(func, debug.traceback)
end
res = tostring(res)
if status then
OK(server,res)
else
error(server,"Execute",res)
end
else
bad_request(server)
end
elseif command == "SETW" then
local _, _, exp = string.find(line, "^[A-Z]+%s+(.+)$")
if exp then
local func = loadstring("return(" .. exp .. ")")
local newidx = table.getn(watches) + 1
watches[newidx] = func
table.setn(watches, newidx)
OK(server,newidx)
else
bad_request(server)
end
elseif command == "DELW" then
local _, _, index = string.find(line, "^[A-Z]+%s+(%d+)$")
index = tonumber(index)
if index then
watches[index] = nil
OK(server)
else
bad_request(server)
end
elseif command == "RUN" or command == "STEP" or command == "OVER" then
OK(server)
if command == "STEP" then
step_into = true
elseif command == "OVER" then
step_over = true
step_level = stack_level
end
local ev, vars, file, line, idx_watch = coroutine.yield()
eval_env = vars
if ev == events.BREAK then
pause(server,file,line)
elseif ev == events.WATCH then
pause(server,file,line,idx_watch)
else
error(server,"Execution",file)
end
elseif command == "LOCALS" then -- new --
-- not sure why I had to hack it this way?? SJD
local tmpfile = 'remdebug-tmp.txt'
local f = io.open(tmpfile,'w')
for k,v in pairs(eval_env) do
if k:sub(1,1) ~= '(' then
f:write(k,' = ',tostring(v),'\n')
end
end
f:close()
f = io.open(tmpfile,'r')
local res = f:read("*a")
f:close()
OK(server,res)
elseif command == "DETACH" then --new--
debug.sethook()
OK(server)
else
bad_request(server)
end
end
end
coro_debugger = coroutine.create(debugger_loop)
--
-- remdebug.engine.config(tab)
-- Configures the engine
--
function config(tab)
if tab.host then
controller_host = tab.host
end
if tab.port then
controller_port = tab.port
end
end
--
-- remdebug.engine.start()
-- Tries to start the debug session by connecting with a controller
--
function start()
pcall(require, "remdebug.config")
local server = socket.connect(controller_host, controller_port)
if server then
_TRACEBACK = function (message)
local err = debug.traceback(message)
error(server,"Execute",res)
server:close()
return err
end
debug.sethook(debug_hook, "lcr")
return coroutine.resume(coro_debugger, server)
end
end