blob: 7f99724a15fee943a2cd7982f9585cf908200bb1 [file] [log] [blame]
-------------------------------------------------------------------------------
-- 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.1"
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