-- -- 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