blob: 2471d2c96094d3db2c28c6f28e267b4bb9d00346 [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
-------------------------------------------------------------------------------
-- Commands handlers for DBGp protocol.
-------------------------------------------------------------------------------
-- Debugger command functions. Each function handle a different command.
-- A command function is called with 3 arguments
-- 1. the debug session instance
-- 2. the command arguments as table
-- 3. the command data, if any
-- The result is either :
-- * true (or any value evaluated to true) : the debugger will resume the execution of the application (continuation command)
-- * false : only in async mode, the debugger WILL wait for further commands instead of continuing (typically, break command)
-- * nil/no return : in sync mode, the debugger will wait for another command. In async mode the debugger will continue the execution
local cowrap, coyield = coroutine.wrap, coroutine.yield
local debug = require "debug"
local core = require "debugger.core"
local dbgp = require "debugger.dbgp"
local util = require "debugger.util"
local platform = require "debugger.platform"
local introspection = require "debugger.introspection"
local context = require "debugger.context"
local log = util.log
local M = { } -- command handlers table
--- Gets the coroutine behind an id
-- Throws errors on unknown identifiers
-- @param coro_id (string or nil) Coroutine identifier or nil (current coroutine)
-- @return Coroutine instance or nil (if coro_id was nil or if coroutine is the current coroutine)
local function get_coroutine(self, coro_id)
if coro_id then
local coro = dbgp.assert(399, core.active_coroutines.from_id[tonumber(coro_id)], "No such coroutine")
dbgp.assert(399, coroutine.status(coro) ~= "dead", "Coroutine is dead")
if coro ~= self.coro[1] then return util.ForeignThread(coro) end
end
return self.coro
end
M["break"] = function(self, args)
self.state = "break"
-- send response to previous command
core.previous_context_response(self)
-- and then response to break command itself
dbgp.send_xml(self.skt, { tag = "response", attr = { command = "break", transaction_id = args.i, success = 1 } } )
return false
end
function M.status(self, args)
dbgp.send_xml(self.skt, { tag = "response", attr = {
command = "status",
reason = "ok",
status = self.state,
transaction_id = args.i } } )
end
function M.stop(self, args)
dbgp.send_xml(self.skt, { tag = "response", attr = {
command = "stop",
reason = "ok",
status = "stopped",
transaction_id = args.i } } )
self.skt:close()
os.exit(1)
end
function M.feature_get(self, args)
local name = args.n
local response = util.features[name] or (not not M[name])
dbgp.send_xml(self.skt, { tag = "response", attr = {
command = "feature_get",
feature_name = name,
supported = response and "1" or "0",
transaction_id = args.i },
tostring(response) } )
end
function M.feature_set(self, args)
local name, value = args.n, args.v
local success = pcall(function() util.features[name] = value end)
dbgp.send_xml(self.skt, { tag = "response", attr = {
command = "feature_set",
feature = name,
success = success and 1 or 0,
transaction_id = args.i
} } )
end
function M.typemap_get(self, args)
local function gentype(name, type, xsdtype)
return { tag = "map", atts = { name = name, type = type, ["xsi:type"] = xsdtype } }
end
dbgp.send_xml(self.skt, { tag = "response", attr = {
command = "typemap_get",
transaction_id = args.i,
["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance",
["xmlns:xsd"] = "http://www.w3.org/2001/XMLSchema",
},
gentype("nil", "null"),
gentype("boolean", "bool", "xsd:boolean"),
gentype("number", "float", "xsd:float"),
gentype("string", "string", "xsd:string"),
gentype("function", "resource"),
gentype("userdata", "resource"),
gentype("thread", "resource"),
gentype("table", "hash"),
gentype("sequence", "array"), -- artificial type to represent sequences (1-n continuous indexes)
gentype("multival", "array"), -- used to represent return values
} )
end
function M.run(self) return true end
function M.step_over(self)
core.events.register("over")
return true
end
function M.step_out(self)
core.events.register("out")
return true
end
function M.step_into(self)
core.events.register("into")
return true
end
function M.eval(self, args, data)
log("DEBUG", "Going to eval "..data)
local result, err, success
local env = self.stack(self.coro, 0)
-- first, try to load as expression
-- DBGp does not support stack level here, see http://bugs.activestate.com/show_bug.cgi?id=81178
local func, err = util.loadin("return "..data, env)
-- if it is not an expression, try as statement (assignment, ...)
if not func then
func, err = util.loadin(data, env)
end
if func then
success, result = pcall(function() return introspection.Multival(func()) end)
if not success then err = result end
end
local response = { tag = "response", attr = { command = "eval", transaction_id = args.i } }
if not err then
local nresults = result.n
if nresults == 1 then result = result[1] end
-- store result for further use (property_*)
-- TODO: this could be optimized: this is only used for Expressions view and totally useless for interactive console,
-- so storing result or not could be set by an argument
local idx
if nresults > 0 then
local cache = env[context.Context[-1]]
idx = #cache + 1
cache[idx] = result
end
-- As of Lua 5.1, the maximum stack size (and result count) is 8000, this limit is used to fit all results in one page
response[1] = introspection.make_property(-1, result, idx or "", nil, 1, 8000, 0, nil)
response.attr.success = 1
else
response.attr.success = 0
response[1] = dbgp.make_error(206, err)
end
dbgp.send_xml(self.skt, response)
end
function M.breakpoint_set(self, args, data)
if args.o and not core.breakpoints.hit_conditions[args.o] then dbgp.error(200, "Invalid hit_condition operator: "..args.o) end
local filename, lineno = args.f, tonumber(args.n)
local bp = {
type = args.t,
state = args.s or "enabled",
temporary = args.r == "1", -- "0" or nil makes this property false
hit_count = 0,
filename = filename,
lineno = lineno,
hit_value = tonumber(args.h or 0),
hit_condition = args.o or ">=",
}
if args.t == "conditional" then
bp.expression = data
-- the expression is compiled only once
bp.condition = dbgp.assert(207, loadstring("return (" .. data .. ")"))
elseif args.t ~= "line" then dbgp.error(201, "BP type " .. args.t .. " not yet supported") end
local bpid = core.breakpoints.insert(bp)
dbgp.send_xml(self.skt, { tag = "response", attr = { command = "breakpoint_set", transaction_id = args.i, state = bp.state, id = bpid } } )
end
function M.breakpoint_get(self, args)
dbgp.send_xml(self.skt, { tag = "response",
attr = { command = "breakpoint_get", transaction_id = args.i },
dbgp.assert(205, core.breakpoints.get_xml(tonumber(args.d))) })
end
function M.breakpoint_list(self, args)
local bps = { tag = "response", attr = { command = "breakpoint_list", transaction_id = args.i } }
for id, bp in pairs(core.breakpoints.get()) do bps[#bps + 1] = core.breakpoints.get_xml(id) end
dbgp.send_xml(self.skt, bps)
end
function M.breakpoint_update(self, args)
local bp = core.breakpoints.get(tonumber(args.d))
if not bp then dbgp.error(205, "No such breakpint "..args.d) end
if args.o and not core.breakpoints.hit_conditions[args.o] then dbgp.error(200, "Invalid hit_condition operator: "..args.o) end
local response = { tag = "response", attr = { command = "breakpoint_update", transaction_id = args.i } }
bp.state = args.s or bp.state
bp.lineno = tonumber(args.n or bp.lineno)
bp.hit_value = tonumber(args.h or bp.hit_value)
bp.hit_condition = args.o or bp.hit_condition
dbgp.send_xml(self.skt, response)
end
function M.breakpoint_remove(self, args)
local response = { tag = "response", attr = { command = "breakpoint_remove", transaction_id = args.i } }
if not core.breakpoints.remove(tonumber(args.d)) then dbgp.error(205, "No such breakpint "..args.d) end
dbgp.send_xml(self.skt, response)
end
function M.stack_depth(self, args)
local depth = 0
local coro = get_coroutine(self, args.o)
for level = 0, math.huge do
local info = coro:getinfo(level, "St")
if not info then break end -- end of stack
depth = depth + 1
if info.istailcall then depth = depth + 1 end -- a 'fake' level is added in that case
if info.what == "main" then break end -- levels below main chunk are not interesting
end
dbgp.send_xml(self.skt, { tag = "response", attr = { command = "stack_depth", transaction_id = args.i, depth = depth} } )
end
function M.stack_get(self, args) -- TODO: dynamic code
-- special URIs to identify unreachable stack levels
local what2uri = {
tail = "tailreturn:/",
C = "ccode:/",
}
local function make_level(info, level)
local attr = { level = level, where = info.name, type="file" }
local uri = platform.get_uri(info.source)
if uri and info.currentline then -- reachable level
attr.filename = uri
attr.lineno = info.currentline
else
attr.filename = what2uri[info.what] or "unknown:/"
attr.lineno = -1
end
return { tag = "stack", attr = attr }
end
local node = { tag = "response", attr = { command = "stack_get", transaction_id = args.i} }
local coro = get_coroutine(self, args.o)
if args.d then
local stack_level = tonumber(args.d)
node[#node+1] = make_level(coro:getinfo(stack_level, "nSl"), stack_level)
else
for i=0, math.huge do
local info = coro:getinfo(i, "nSlt")
if not info then break end
node[#node+1] = make_level(info, i)
-- add a fake level of stack for tail calls (tells user that the function has not been called directly)
if info.istailcall then
node[#node+1] = { tag = "stack", attr = { level=i, type="file", filename="tailreturn:/", lineno=-1 } }
end
if info.what == "main" then break end -- levels below main chunk are not interesting
end
end
dbgp.send_xml(self.skt, node)
end
--- Lists all active coroutines.
-- Returns a list of active coroutines with their id (an arbitrary string) to query stack and properties. The id is
-- guaranteed to be unique and stable for all coroutine life (they can be reused as long as coroutine exists).
-- Others commands such as stack_get or property_* commands takes an additional -o switch to query a particular cOroutine.
-- If the switch is not given, running coroutine will be used.
-- In case of error on coroutines (most likely coroutine not found or dead), an error 399 is thrown.
-- Note there is an important limitation due to Lua 5.1 coroutine implementation: you cannot query main "coroutine" from
-- another one, so main coroutine is not in returned list (this will change with Lua 5.2).
--
-- This is a non-standard command. The returned XML has the following strucuture:
-- <response command="coroutine_list" transaction_id="0">
-- <coroutine name="<some printtable name>" id="<coroutine id>" running="0|1" />
-- ...
-- </response>
function M.coroutine_list(self, args)
local running = self.coro[1]
local coroutines = { tag = "response", attr = { command = "coroutine_list", transaction_id = args.i } }
-- as any operation on main coroutine will fail, it is not yet listed
-- coroutines[1] = { name = "coroutine", attr = { id = 0, name = "main", running = (running == nil) and "1" or "0" } }
for id, coro in pairs(core.active_coroutines.from_id) do
if id ~= "n" then
coroutines[#coroutines + 1] = { tag = "coroutine", attr = { id = id, name = tostring(coro), running = (coro == running) and "1" or "0" } }
end
end
dbgp.send_xml(self.skt, coroutines)
end
function M.context_names(self, args)
local coro = get_coroutine(self, args.o)
local level = tonumber(args.d or 0)
local info = coro:getinfo(level, "f") or dbgp.error(301, "No such stack level "..tostring(level))
-- All contexts are always passed, even if empty. This is how DLTK expect context, what about others ?
local contexts = {
tag = "response", attr = { command = "context_names", transaction_id = args.i },
{ tag = "context", attr = { name = "Local", id = 0 } },
{ tag = "context", attr = { name = "Upvalue", id = 2 } },
{ tag = "context", attr = { name = "Global", id = 1 } },
}
dbgp.send_xml(self.skt, contexts)
end
function M.context_get(self, args)
local cxt_num = tonumber(args.c or 0)
local cxt_id = context.Context[cxt_num] or dbgp.error(302, "No such context: "..tostring(cxt_num))
local level = tonumber(args.d or 0)
local coro = get_coroutine(self, args.o)
local cxt = self.stack(coro, level)
local properties = { tag = "response", attr = { command = "context_get", transaction_id = args.i, context = context} }
-- iteration over global is different (this could be unified in Lua 5.2 thanks to __pairs metamethod)
for name, val in (cxt_num == 1 and next or getmetatable(cxt[cxt_id]).iterator), cxt[cxt_id], nil do
-- the DBGp specification is not clear about the depth of a context_get, but a recursive get could be *really* slow in Lua
properties[#properties + 1] = introspection.make_property(cxt_num, val, name, nil, 0, util.features.max_children, 0,
util.features.max_data, cxt_num ~= 1)
end
dbgp.send_xml(self.skt, properties)
end
-------------------------------------------------------------------------------
-- Property_* commands
-------------------------------------------------------------------------------
-- This in the environment in which properties are get or set.
-- It notably contain a collection of proxy table which handle transparentely get/set operations on special fields
-- and the cache of complex keys.
local property_evaluation_environment = {
key_cache = introspection.key_cache,
metatable = setmetatable({ }, {
__index = function(self, tbl) return getmetatable(tbl) end,
__newindex = function(self, tbl, mt) return setmetatable(tbl, mt) end,
}),
environment = util.eval_env,
}
-- to allows to be set as metatable
property_evaluation_environment.__index = property_evaluation_environment
function M.property_get(self, args)
--TODO BUG ECLIPSE TOOLSLINUX-99 352316
local cxt_num, name = assert(util.unb64(args.n):match("^(%-?%d+)|(.*)$"))
cxt_num = tonumber(args.c or cxt_num)
local cxt_id = context.Context[cxt_num] or dbgp.error(302, "No such context: "..tostring(cxt_num))
local level = tonumber(args.d or 0)
local coro = get_coroutine(self, args.o)
local size = tonumber(args.m or util.features.max_data)
if size < 0 then size = nil end -- call from property_value
local page = tonumber(args.p or 0)
local cxt = self.stack(coro, level)
local chunk = dbgp.assert(206, util.loadin("return "..name, property_evaluation_environment))
local prop = select(2, dbgp.assert(300, pcall(chunk, cxt[cxt_id])))
local response = introspection.make_property(cxt_num, prop, name, name, util.features.max_depth, util.features.max_children, page, size)
-- make_property is not able to flag special variables as such when they are at root of property
-- special variables queries are in the form "<proxy name>[(...)[a][b]<...>]"
-- TODO: such parsing is far from perfect
if name:match("^[%w_]+%[.-%b[]%]$") == name then response.attr.type = "special" end
dbgp.send_xml(self.skt, { tag = "response",
attr = { command = "property_get", transaction_id = args.i, context = context},
response } )
end
function M.property_value(self, args)
args.m = -1
M.property_get(self, args)
end
function M.property_set(self, args, data)
local cxt_num, name = assert(util.unb64(args.n):match("^(%-?%d+)|(.*)$"))
cxt_num = tonumber(args.c or cxt_num)
local cxt_id = context.Context[cxt_num] or dbgp.error(302, "No such context: "..tostring(cxt_num))
local level = tonumber(args.d or 0)
local coro = get_coroutine(self, args.o)
local cxt = self.stack(coro, level)
-- evaluate the new value in the local context
local value = select(2, dbgp.assert(206, pcall(dbgp.assert(206, util.loadin("return "..data, cxt)))))
local chunk = dbgp.assert(206, util.loadin(name .. " = value", setmetatable({ value = value }, property_evaluation_environment)))
dbgp.assert(206, pcall(chunk, cxt[cxt_id]))
dbgp.send_xml(self.skt, { tag = "response", attr = { success = 1, transaction_id = args.i } } )
end
--TODO dynamic code handling
-- The DBGp specification is not clear about the line number meaning, this implementation is 1-based and numbers are inclusive
function M.source(self, args)
local path
if args.f then
path = platform.get_path(args.f)
else
path = self.coro:getinfo(0, "S").source
assert(path:sub(1,1) == "@")
path = path:sub(2)
end
local file, err = io.open(path)
if not file then dbgp.error(100, err, { success = 0 }) end
-- Try to identify compiled files
if file:read(1) == "\033" then dbgp.error(100, args.f.." is bytecode", { success = 0 }) end
file:seek("set", 0)
local srclines = { }
local beginline, endline, currentline = tonumber(args.b or 0), tonumber(args.e or math.huge), 0
for line in file:lines() do
currentline = currentline + 1
if currentline >= beginline and currentline <= endline then
srclines[#srclines + 1] = line
elseif currentline >= endline then break end
end
file:close()
srclines[#srclines + 1] = "" -- to add a trailing \n
dbgp.send_xml(self.skt, { tag = "response",
attr = { command = "source", transaction_id = args.i, success = 1},
util.b64(table.concat(srclines, "\n")) })
end
-- Factory for both stdout and stderr commands, change file descriptor in io
local function output_command_handler_factory(mode)
return function(self, args)
if args.c == "0" then -- disable
io[mode] = io.base[mode]
else
io[mode] = setmetatable({ skt = self.skt, mode = mode }, args.c == "1" and core.copy_output or core.redirect_output)
end
dbgp.send_xml(self.skt, { tag = "response", attr = { command = mode, transaction_id = args.i, success = "1" } } )
end
end
M.stdout = output_command_handler_factory("stdout")
M.stderr = output_command_handler_factory("stderr")
return M