| ------------------------------------------------------------------------------- |
| -- Copyright (c) 2011-2012 Sierra Wireless and others. |
| -- All rights reserved. This program and the accompanying materials |
| -- are made available under the terms of the Eclipse Public License v1.0 |
| -- which accompanies this distribution, and is available at |
| -- http://www.eclipse.org/legal/epl-v10.html |
| -- |
| -- Contributors: |
| -- Sierra Wireless - initial API and implementation |
| ------------------------------------------------------------------------------- |
| |
| local DBGP_CLIENT_VERSION = "1.4.2" |
| DBGP_CLIENT_LUA_VERSION = os.getenv "LUA_VERSION" or _VERSION |
| if DBGP_CLIENT_LUA_VERSION ~= "Lua 5.1" and DBGP_CLIENT_LUA_VERSION ~= "Lua 5.2" then |
| print(DBGP_CLIENT_LUA_VERSION .. " is not supported. As fallback, debugger will behave as if it runs on Lua 5.2 vm. You could also try to force it to behave as a Lua 5.1 vm by setting the LUA_VERSION environment variable to 'Lua 5.1'") |
| DBGP_CLIENT_LUA_VERSION = "Lua 5.2" |
| end |
| |
| local debug = require "debug" |
| |
| -- To avoid cyclic dependency, internal state of the debugger that must be accessed |
| -- elsewhere (in commands most likely) will be stored in a fake module "debugger.core" |
| local core = { } |
| package.loaded["debugger.core"] = core |
| |
| local util = require "debugger.util" |
| local platform = require "debugger.platform" |
| local dbgp = require "debugger.dbgp" |
| local commands = require "debugger.commands" |
| local context = require "debugger.context" |
| local url = require "debugger.url" |
| |
| local log = util.log |
| |
| |
| -- TODO complete the stdlib access |
| local corunning, cocreate, cowrap, coyield, coresume, costatus = coroutine.running, coroutine.create, coroutine.wrap, coroutine.yield, coroutine.resume, coroutine.status |
| |
| |
| -- register the URI of the debugger, to not jump into with redefined function or coroutine bootstrap stuff |
| local debugger_uri = nil -- set in init function |
| local transportmodule_uri = nil -- set in init function |
| |
| -- will contain the session object, and possibly a list of all sessions if a multi-threaded model is adopted |
| -- this is only used for async commands. |
| local active_session = nil |
| |
| -- tracks all active coroutines and associate an id to them, the table from_id is the id=>coro mapping, the table from_coro is the reverse |
| core.active_coroutines = { n = 0, from_id = setmetatable({ }, { __mode = "v" }), from_coro = setmetatable({ }, { __mode = "k" }) } |
| |
| -- "BEGIN VERSION DEPENDENT CODE" |
| local setbpenv -- set environment of a breakpoint (compiled function) |
| if DBGP_CLIENT_LUA_VERSION == "Lua 5.1" then |
| local setfenv = setfenv |
| setbpenv = setfenv |
| elseif DBGP_CLIENT_LUA_VERSION == "Lua 5.2" then |
| local setupvalue = debug.setupvalue |
| -- _ENV is the first upvalue |
| setbpenv = function(f, t) return setupvalue(f, 1, t) end |
| end |
| -- "END VERSION DEPENDENT CODE" |
| |
| ------------------------------------------------------------------------------- |
| -- Output redirection handling |
| ------------------------------------------------------------------------------- |
| -- Override standard output functions & constants to redirect data written to these files to IDE too. |
| -- This works only for output done in Lua, output written by C extensions is still go to system output file. |
| |
| -- references to native values |
| io.base = { output = io.output, stdin = io.stdin, stdout = io.stdout, stderr = io.stderr } |
| |
| function print(...) |
| local buf = {...} |
| for i=1, select("#", ...) do |
| buf[i] = tostring(buf[i]) |
| end |
| io.stdout:write(table.concat(buf, "\t") .. "\n") |
| end |
| |
| -- Actually change standard output file but still return the "fake" stdout |
| function io.output(output) |
| io.base.output(output) |
| return io.stdout |
| end |
| |
| local dummy = function() end |
| |
| -- metatable for redirecting output (not printed at all in actual output) |
| core.redirect_output = { |
| write = function(self, ...) |
| local buf = {...} |
| for i=1, select("#", ...) do buf[i] = tostring(buf[i]) end |
| buf = table.concat(buf):gsub("\n", "\r\n") |
| dbgp.send_xml(self.skt, { tag = "stream", attr = { type=self.mode }, util.b64(buf) } ) |
| end, |
| flush = dummy, |
| close = dummy, |
| setvbuf = dummy, |
| seek = dummy |
| } |
| core.redirect_output.__index = core.redirect_output |
| |
| -- metatable for cloning output (outputs to actual system and send to IDE) |
| core.copy_output = { |
| write = function(self, ...) |
| core.redirect_output.write(self, ...) |
| io.base[self.mode]:write(...) |
| end, |
| flush = function(self, ...) return self.out:flush(...) end, |
| close = function(self, ...) return self.out:close(...) end, |
| setvbuf = function(self, ...) return self.out:setvbuf(...) end, |
| seek = function(self, ...) return self.out:seek(...) end, |
| } |
| core.copy_output.__index = core.copy_output |
| |
| ------------------------------------------------------------------------------- |
| -- Breakpoint registry |
| ------------------------------------------------------------------------------- |
| -- Registry of current stack levels of all running threads |
| local stack_levels = setmetatable( { }, { __mode = "k" } ) |
| |
| -- File/line mapping for breakpoints (BP). For a given file/line, a list of BP is associated (DBGp specification section 7.6.1 |
| -- require that multiple BP at same place must be handled) |
| -- A BP is a table with all additional properties (type, condition, ...) the id is the string representation of the table. |
| core.breakpoints = { |
| -- functions to call to match hit conditions |
| hit_conditions = { |
| [">="] = function(value, target) return value >= target end, |
| ["=="] = function(value, target) return value == target end, |
| ["%"] = function(value, target) return (value % target) == 0 end, |
| } |
| } |
| |
| -- tracks events such as step_into or step_over |
| core.events = { } |
| |
| do |
| local file_mapping = { } |
| local id_mapping = { } |
| local waiting_sessions = { } -- sessions that wait for an event (over, into, out) |
| local step_into = nil -- session that registered a step_into event, if any |
| local sequence = 0 -- used to generate breakpoint IDs |
| |
| --- Inserts a new breakpoint into registry |
| -- @param bp (table) breakpoint data |
| -- @param uri (string, optional) Absolute file URI, for line breakpoints |
| -- @param line (number, optional) Line where breakpoint stops, for line breakpoints |
| -- @return breakpoint identifier |
| function core.breakpoints.insert(bp) |
| local bpid = sequence |
| sequence = bpid + 1 |
| bp.id = bpid |
| -- re-encode the URI to avoid any mismatch (with authority for example) |
| local uri = url.parse(bp.filename) |
| bp.filename = url.build{ scheme=uri.scheme, authority="", path=platform.normalize(uri.path)} |
| |
| local filereg = file_mapping[bp.filename] |
| if not filereg then |
| filereg = { } |
| file_mapping[bp.filename] = filereg |
| end |
| |
| local linereg = filereg[bp.lineno] |
| if not linereg then |
| linereg = {} |
| filereg[bp.lineno] = linereg |
| end |
| |
| table.insert(linereg, bp) |
| |
| id_mapping[bpid] = bp |
| return bpid |
| end |
| |
| --- If breakpoint(s) exists for given file/line, uptates breakpoint counters |
| -- and returns whether a breakpoint has matched (boolean) |
| function core.breakpoints.at(file, line) |
| local bps = file_mapping[file] and file_mapping[file][line] |
| if not bps then return nil end |
| |
| local do_break = false |
| for _, bp in pairs(bps) do |
| if bp.state == "enabled" then |
| local match = true |
| if bp.condition then |
| -- TODO: this is not the optimal solution because Context can be instantiated twice if the breakpoint matches |
| local cxt = context.Context:new(active_session.coro, 0) |
| setbpenv(bp.condition, cxt) |
| local success, result = pcall(bp.condition) |
| if not success then log("ERROR", "Condition evaluation failed for breakpoint at %s:%d: %s", file, line, result) end |
| -- debugger always stops if an error occurs |
| match = (not success) or result |
| end |
| if match then |
| bp.hit_count = bp.hit_count + 1 |
| if core.breakpoints.hit_conditions[bp.hit_condition](bp.hit_count, bp.hit_value) then |
| if bp.temporary then |
| core.breakpoints.remove(bp.id) |
| end |
| do_break = true |
| -- there is no break to handle multiple breakpoints: all hit counts must be updated |
| end |
| end |
| end |
| end |
| return do_break |
| end |
| |
| function core.breakpoints.get(id) |
| if id then return id_mapping[id] |
| else return id_mapping end |
| end |
| |
| function core.breakpoints.remove(id) |
| local bp = id_mapping[id] |
| if bp then |
| id_mapping[id] = nil |
| local linereg = file_mapping[bp.filename][bp.lineno] |
| for i=1, #linereg do |
| if linereg[i] == bp then |
| table.remove(linereg, i) |
| break |
| end |
| end |
| |
| -- cleanup file_mapping |
| if not next(linereg) then file_mapping[bp.filename][bp.lineno] = nil end |
| if not next(file_mapping[bp.filename]) then file_mapping[bp.filename] = nil end |
| return true |
| end |
| return false |
| end |
| |
| --- Returns an XML data structure that describes given breakpoint |
| -- @param id (number) breakpoint ID |
| -- @return Table describing a <breakpooint> tag or nil followed by an error message |
| function core.breakpoints.get_xml(id) |
| local bp = id_mapping[id] |
| if not bp then return nil, "No such breakpoint: "..tostring(id) end |
| |
| local response = { tag = "breakpoint", attr = { } } |
| for k,v in pairs(bp) do response.attr[k] = v end |
| if bp.expression then |
| response[1] = { tag = "expression", bp.expression } |
| end |
| |
| -- internal use only |
| response.attr.expression = nil |
| response.attr.condition = nil |
| response.attr.temporary = nil -- TODO: the specification is not clear whether this should be provided, see other implementations |
| return response |
| end |
| |
| --- Register an event to be triggered. |
| -- @param event event name to register (must be "over", "out" or "into") |
| function core.events.register(event) |
| local thread = active_session.coro[1] |
| log("DEBUG", "Registered %s event for %s (%d)", event, tostring(thread), stack_levels[thread]) |
| if event == "into" then |
| step_into = true |
| else |
| waiting_sessions[thread] = { event, stack_levels[thread] } |
| end |
| end |
| |
| --- Returns if an event (step into, over, out) is triggered. |
| -- Does *not* discard events (even if they match) as event must be discarded manually if a breakpoint match before anyway. |
| -- @return true if an event has matched, false otherwise |
| function core.events.does_match() |
| if step_into then return true end |
| |
| local thread = active_session.coro[1] |
| local event = waiting_sessions[thread] |
| if event then |
| local event_type, target_level = unpack(event) |
| local current_level = stack_levels[thread] |
| |
| if (event_type == "over" and current_level <= target_level) or -- step over |
| (event_type == "out" and current_level < target_level) then -- step out |
| log("DEBUG", "Event %s matched!", event_type) |
| return true |
| end |
| end |
| return false |
| end |
| |
| --- Discards event for current thread (if any) |
| function core.events.discard() |
| waiting_sessions[active_session.coro[1]] = nil |
| step_into = nil |
| end |
| end |
| |
| ------------------------------------------------------------------------------- |
| -- Debugger main loop |
| ------------------------------------------------------------------------------- |
| |
| --- Send the XML response to the previous continuation command and clear the previous context |
| function core.previous_context_response(self, reason) |
| self.previous_context.status = self.state |
| self.previous_context.reason = reason or "ok" |
| dbgp.send_xml(self.skt, { tag = "response", attr = self.previous_context } ) |
| self.previous_context = nil |
| end |
| |
| local function cleanup() |
| coroutine.resume, coroutine.wrap = coresume, cowrap |
| for _, coro in pairs(core.active_coroutines.from_id) do |
| debug.sethook(coro) |
| end |
| -- to remove hook on the main coroutine, it must be the current one (otherwise, this is a no-op) and this function |
| -- have to be called adain later on the main thread to finish cleaup |
| debug.sethook() |
| core.active_coroutines.from_id, core.active_coroutines.from_coro = { }, { } |
| end |
| |
| --- This function handles the debugger commands while the execution is paused. This does not use coroutines because there is no |
| -- way to get main coro in Lua 5.1 (only in 5.2) |
| local function debugger_loop(self, async_packet) |
| self.skt:settimeout(nil) -- set socket blocking |
| |
| -- in async mode, the debugger does not wait for another command before continuing and does not modify previous_context |
| local async_mode = async_packet ~= nil |
| |
| if self.previous_context and not async_mode then |
| self.state = "break" |
| core.previous_context_response(self) |
| end |
| self.stack = context.ContextManager(self.coro) -- will be used to mutualize context allocation for each loop |
| |
| while true do |
| -- reads packet |
| local packet = async_packet or dbgp.read_packet(self.skt) |
| if not packet then |
| log("WARNING", "lost debugger connection") |
| cleanup() |
| break |
| end |
| |
| async_packet = nil |
| log("DEBUG", packet) |
| local cmd, args, data = dbgp.cmd_parse(packet) |
| |
| -- FIXME: command such as continuations sent in async mode could lead both engine and IDE in inconsistent state : |
| -- make a blacklist/whitelist of forbidden or allowed commands in async ? |
| -- invoke function |
| local func = commands[cmd] |
| if func then |
| local ok, cont = xpcall(function() return func(self, args, data) end, debug.traceback) |
| if not ok then -- internal exception |
| local code, msg, attr |
| if type(cont) == "table" and getmetatable(cont) == dbgp.DBGP_ERR_METATABLE then |
| code, msg, attr = cont.code, cont.message, cont.attr |
| else |
| code, msg, attr = 998, tostring(cont), { } |
| end |
| log("ERROR", "Command %s caused: (%d) %s", cmd, code, tostring(msg)) |
| attr.command, attr.transaction_id = cmd, args.i |
| dbgp.send_xml(self.skt, { tag = "response", attr = attr, dbgp.make_error(code, msg) } ) |
| elseif cont then |
| self.previous_context = { command = cmd, transaction_id = args.i } |
| break |
| elseif cont == nil and async_mode then |
| break |
| elseif cont == false then -- In case of commands that fully resumes debugger loop, the mode is sync |
| async_mode = false |
| end |
| else |
| log("Got unknown command: "..cmd) |
| dbgp.send_xml(self.skt, { tag = "response", attr = { command = cmd, transaction_id = args.i, }, dbgp.make_error(4) } ) |
| end |
| end |
| |
| self.stack = nil -- free allocated contexts |
| self.state = "running" |
| self.skt:settimeout(0) -- reset socket to async |
| end |
| |
| -- Stack handling can be pretty complex sometimes, especially with LuaJIT (as tail-call optimization are |
| -- more aggressive as stock Lua). So all debugger stuff is done in another coroutine, which leave the program |
| -- stack in a clean state and allow faster and clearer stack operations (no need to remove all debugger calls |
| -- from stack for each operation). |
| -- However, this does not always work with stock Lua 5.1 as the main coroutine cannot be referenced |
| -- (coroutine.running() return nil). For this particular case, the debugger loop is started on the top of |
| -- program stack and every stack operation is relative the the hook level (see MainThread in util.lua). |
| local function line_hook(line) |
| local do_break, packet = nil, nil |
| local info = active_session.coro:getinfo(0, "S") |
| local uri = platform.get_uri(info.source) |
| if uri and uri ~= debugger_uri and uri ~= transportmodule_uri then -- the debugger does not break if the source is not known |
| do_break = core.breakpoints.at(uri, line) or core.events.does_match() |
| if do_break then |
| core.events.discard() |
| end |
| |
| -- check for async commands |
| if not do_break then |
| packet = dbgp.read_packet(active_session.skt) |
| if packet then do_break = true end |
| end |
| end |
| |
| if do_break then |
| local success, err = pcall(debugger_loop, active_session, packet) |
| if not success then log("ERROR", "Error while debug loop: "..err) end |
| end |
| end |
| |
| local line_hook_coro = cocreate(function(line) |
| while true do |
| line_hook(line) |
| line = coyield() |
| end |
| end) |
| |
| local function debugger_hook(event, line) |
| local thread = corunning() or "main" |
| if event == "call" then |
| stack_levels[thread] = stack_levels[thread] + 1 |
| elseif event == "tail call" then |
| -- tail calls has no effects on stack handling: it is only used only for step commands but a such even does not |
| -- interfere with any of them |
| elseif event == "return" or event == "tail return" then |
| stack_levels[thread] = stack_levels[thread] - 1 |
| else -- line event: check for breakpoint |
| active_session.coro = util.CurrentThread(corunning()) |
| if active_session.coro[1] == "main" then |
| line_hook(line) |
| else |
| -- run the debugger loop in another thread on the other cases (simplifies stack handling) |
| assert(coresume(line_hook_coro, line)) |
| end |
| active_session.coro = nil |
| end |
| end |
| |
| if rawget(_G, "jit") then |
| debugger_hook = function(event, line) |
| local thread = corunning() or "main" |
| if event == "call" then |
| if debug.getinfo(2, "S").what == "C" then return end |
| stack_levels[thread] = stack_levels[thread] + 1 |
| elseif event == "return" or event == "tail return" then |
| -- Return hooks are not called for tail calls in JIT (but unlike 5.2 there is no way to know whether a call is tail or not). |
| -- So the only reliable way to know stack depth is to walk it. |
| local depth = 2 |
| -- TODO: find the fastest way to call getinfo ('what' parameter) |
| while debug.getinfo(depth, "f") do depth = depth + 1 end |
| stack_levels[thread] = depth - 2 |
| elseif event == "line" then |
| active_session.coro = util.CurrentThread(corunning()) |
| if active_session.coro[1] == "main" then |
| line_hook(line) |
| else |
| -- run the debugger loop in another thread on the other cases (simplifies stack handling) |
| assert(coresume(line_hook_coro, line)) |
| end |
| active_session.coro = nil |
| end |
| end |
| end |
| |
| local function sendInitPacket(skt,idekey) |
| -- get the root script path (the highest possible stack index) |
| local source |
| for i=2, math.huge do |
| local info = debug.getinfo(i) |
| if not info then break end |
| source = platform.get_uri(info.source) or source |
| end |
| if not source then source = "unknown:/" end -- when loaded before actual script (with a command line switch) |
| |
| -- generate some kind of thread identifier |
| local thread = corunning() or "main" |
| stack_levels[thread] = 1 -- the return event will set the counter to 0 |
| local sessionid = tostring(os.time()) .. "_" .. tostring(thread) |
| |
| dbgp.send_xml(skt, { tag = "init", attr = { |
| appid = "Lua DBGp", |
| idekey = idekey, |
| session = sessionid, |
| thread = tostring(thread), |
| parent = "", |
| language = "Lua", |
| protocol_version = "1.0", |
| fileuri = source |
| } }) |
| |
| return sessionid |
| end |
| |
| local function init(host, port, idekey, transport, executionplatform, workingdirectory, nbRetry) |
| -- get connection data |
| local host = host or os.getenv "DBGP_IDEHOST" or "127.0.0.1" |
| local port = port or os.getenv "DBGP_IDEPORT" or "10000" |
| local idekey = idekey or os.getenv("DBGP_IDEKEY") or "luaidekey" |
| |
| -- init plaform module |
| local executionplatform = executionplatform or os.getenv("DBGP_PLATFORM") or nil |
| local workingdirectory = workingdirectory or os.getenv("DBGP_WORKINGDIR") or nil |
| platform.init(executionplatform,workingdirectory) |
| |
| -- get transport layer |
| local transportpath = transport or os.getenv("DBGP_TRANSPORT") or "debugger.transport.luasocket" |
| local transport = require(transportpath) |
| |
| -- nb retry for connection |
| local nbRetry = nbRetry or os.getenv("DBGP_NBRETRY") or 10 |
| nbRetry = math.max(nbRetry,1) |
| |
| -- install base64 functions into util |
| util.b64, util.rawb64, util.unb64 = transport.b64, transport.rawb64, transport.unb64 |
| |
| -- get the debugger and transport layer URI |
| debugger_uri = platform.get_uri(debug.getinfo(1).source) |
| transportmodule_uri = platform.get_uri(debug.getinfo(transport.create).source) |
| |
| -- try to connect several times: if IDE launches both process and server at same time, first connect attempts may fail |
| local skt,ok, err |
| print(string.format("Debugger v%s", DBGP_CLIENT_VERSION)) |
| print(string.format("Debugger: Trying to connect to %s:%s ... ", host, port)) |
| |
| local timeelapsed = 0 |
| local sessionid = nil |
| for i=1,nbRetry do |
| -- try to connect to DBGP server |
| skt = assert(transport.create()) |
| skt:settimeout(nil) |
| ok, err = skt:connect(host, port) |
| if ok then |
| sessionid = sendInitPacket(skt,idekey) |
| -- test if socket is closed |
| ok, err = skt:receive(0) |
| if err == nil then print("Debugger: Connection succeed.") break end |
| end |
| |
| if err ~= nil then |
| -- failed to connect |
| print(string.format("Debugger: Failed to connect to %s:%s (%s)", host, port, err)) |
| skt:close() |
| |
| -- wait&retry |
| local timetowait = math.min(3, math.max(timeelapsed/2, 0.25)) |
| if i < nbRetry then |
| print(string.format("Debugger: Retrying to connect to %s:%s in %.2fs ... ", host, port,timetowait)) |
| transport.sleep(timetowait) |
| timeelapsed = timeelapsed+timetowait |
| end |
| end |
| end |
| if err then error(string.format("Cannot connect to %s:%d : %s", host, port, err)) end |
| |
| --FIXME util.CurrentThread(corunning) => util.CurrentThread(corunning()) WHAT DOES IT FIXES ?? |
| local sess = { skt = skt, state = "starting", id = sessionid, coro = util.CurrentThread(corunning) } |
| active_session = sess |
| debugger_loop(sess) |
| |
| -- set debug hooks |
| debug.sethook(debugger_hook, "rlc") |
| |
| -- install coroutine collecting functions. |
| -- TODO: maintain a list of *all* coroutines can be overkill (for example, the ones created by copcall), make a extension point to |
| -- customize debugged coroutines |
| -- coroutines are referenced during their first resume (so we are sure that they always have a stack frame) |
| local function resume_handler(coro, ...) |
| if costatus(coro) == "dead" then |
| local coro_id = core.active_coroutines.from_coro[coro] |
| core.active_coroutines.from_id[coro_id] = nil |
| core.active_coroutines.from_coro[coro] = nil |
| stack_levels[coro] = nil |
| end |
| return ... |
| end |
| |
| function coroutine.resume(coro, ...) |
| if not stack_levels[coro] then |
| -- first time referenced |
| stack_levels[coro] = 0 |
| core.active_coroutines.n = core.active_coroutines.n + 1 |
| core.active_coroutines.from_id[core.active_coroutines.n] = coro |
| core.active_coroutines.from_coro[coro] = core.active_coroutines.n |
| debug.sethook(coro, debugger_hook, "rlc") |
| end |
| return resume_handler(coro, coresume(coro, ...)) |
| end |
| |
| -- coroutine.wrap uses directly C API for coroutines and does not trigger our overridden coroutine.resume |
| -- so this is an implementation of wrap in pure Lua |
| local function wrap_handler(status, ...) |
| if not status then error((...)) end |
| return ... |
| end |
| |
| function coroutine.wrap(f) |
| local coro = coroutine.create(f) |
| return function(...) |
| return wrap_handler(coroutine.resume(coro, ...)) |
| end |
| end |
| |
| return sess |
| end |
| |
| return init |