3483 lines
130 KiB
Lua

-------------------------------------------------------------------------------
-- MOOSE VERSION for DCS WORLD
-- Modified by FlightControl to understand "DCS".
-------------------------------------------------------------------------------
-- 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
-------------------------------------------------------------------------------
-- Debugger using DBGp protocol.
-------------------------------------------------------------------------------
-- The module returns a single init function which takes 7 parameters (IDEHOST, IDEPORT, IDEKEY, TRANSPORT, PLATFORM, WORKINGDIR, NBRETRY).
--
-- IDEHOST: the host name or the ip address of the DBGP server (so your ide)
-- if HOST is nil, the DBGP_IDEHOST env var is used.
-- if the env var is nil, the default value '127.0.0.1' is used.
--
-- IDEPORT: the port of the DBGP server (must be configure in the IDE)
-- if PORT is nil, the DBGP_IDEPORT env var is used.
-- if the env var is nil, the default value '10000' is used.
--
-- IDEIDEKEY: a string which is used as session key
-- if IDEKEY is nil, the DBGP_IDEKEY env var is used.
-- if the env var is nil, the default value 'luaidekey' is used.
--
-- TRANSPORT: (advanced optional parameter) the module name of which implement the transport interface used to do the connection with the server.
-- by default the debugger use an internal implementation based on luasocket, but if can not use it, you could implement or use another transport layer implementation.
-- if TRANSPORT is nil, the DBGP_TRANSPORT env var is used.
-- if the env var is nil, the default value 'debugger.transport.luasocket' is used : this is the default implementation based on luasocket.
--
-- PLATFORM: (advanced optional parameter) 'unix' or 'win32' string which define the kind of platform on which the program to debug is executed.
-- by default the debugger will try to guess it and surely success, if for some reasons it fails you could help it by precise the execution platform.
-- if PLATFORM is nil, the DBGP_PLATFORM env var is used.
-- if the env var is nil, the debugger will try to guess it.
--
-- WORKINGDIR: (advanced optional parameter) the working directory in which the program to debug is executed.
-- by default the debugger will try to guess it and surely success, if for some reasons it fails you could help it by precise the working directory.
-- if WORKINGDIR is nil, the DBGP_WORKINGDIR env var is used.
-- if the env var is nil, the debugger will try to guess it.
--
-- NBRETRY: (advanced optional parameter) the number of connection retry at start up.
-- if NBRETRY is nil, the DBGP_NBRETRY env var is used.
-- if the env var is nil, the default value is 10.
--
-------------------------------------------------------------------------------
-- Known Issues:
-- * Functions cannot be created using the debugger and then called in program because their environment is mapped directly to
-- a debugger internal structure which cannot be persisted (i.e. used outside of the debug_hook).
-- * The DLTK client implementation does not handle context for properties. As a workaround, the context is encoded into the
-- fullname attribute of each property and is used likewise in property_get commands. The syntax is "<context ID>|<full name>"
-- * Dynamic code (compiled with load or loadstring) is not handled (the debugger will step over it, like C code)
-- Design notes:
-- * The whole debugger state is kept in a (currently) unique session table in order to ease eventual adaptation to a multi-threaded
-- model, as DBGp needs one connection per thread.
-- * Full names of properties are base64 encoded because they can contain arbitrary data (spaces, escape characters, ...), this makes
-- command parsing munch easier and faster
-- * This debugger supports asynchronous commands: any command can be done at any time, but some of them (continuations) can lead to
-- inconsistent states. In addition, this have a quite big overhead (~66%), if performance is an issue, a custom command to disable
-- async mode could be done.
-- * All commands are implemented in table commands, see this comments on this table to additional details about commands implementation
-- * The environments in which are evaluated user code (property_* and eval commands, conditional breakpoints, ...) is a read/write
-- mapping of the local environment of a given stack level (can be accessed with variable names). See Context for additional details.
-- Context instantiation is pooled inside a debugging loop with ContextManager (each stack level is instantiated only once).
-- * Output redirection is done by redefining print and some values inside the io table. See "Output redirection handling" for details.
-- Todo list:
-- * Override I/O in init function instead of on module loading.
-- * Allow to break programatically (debugger.break()).
-- * Break-on-error feature (break if an error is thrown and there is no pcall in stack to handle it).
-- * Use new 5.2 facilities to provide informations about function (arguments names, vararg, ...)
-- * Allow to see ... content for vararg functions (5.2 only)
-- * Inspect LuaJIT C data (http://lua-users.org/lists/lua-l/2011-02/msg01012.html)-- /!\ This file is auto-generated. Do not alter manually /!\
--------------------------------------------------------------------------------
-- Submodules body
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Module debugger.transport.apr
package.preload["debugger.transport.apr"] = function(...)
-------------------------------------------------------------------------------
-- 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
-------------------------------------------------------------------------------
-- Apache Portable Runtime backend for DBGP debugger.
-------------------------------------------------------------------------------
local apr = require "apr"
-- base 64 wrapping
function b64_wrap(src)
local t = {}
local b64_src = mime.b64(src)
for i=1, #b64_src, 76 do t[#t+1] = b64_src:sub(i, i+75).."\r\n" end
return table.concat(t)
end
-- implements a subset of LuaSocket API using APR
local SOCKET_MT = {
connect = function(self, address, port) return self.skt:connect(address, port) end,
receive = function(self, n) return self.skt:read(n) end, -- only numeric read is used
send = function(self, data) return self.skt:write(data) end,
close = function(self) return self.skt:close() end,
settimeout = function(self, sec)
if sec == nil then self.skt:timeout_set(true)
elseif sec == 0 then self.skt:timeout_set(false)
else self.skt:timeout_set(math.floor(sec * 1000000)) end
end
}
SOCKET_MT.__index = SOCKET_MT
return {
create = function()
local skt, err = apr.socket_create('tcp')
if not skt then return nil, err end
return setmetatable({skt = skt}, SOCKET_MT)
end,
sleep = apr.sleep, -- exact same API as LuaSocket
-- Base64 related functions
--- Encodes a string into Base64 with line wrapping
-- @param data (string) data to encode
-- @return base64 encoded string
b64 = function(data)
t = {}
local b64_data = apr.base64_encode(data)
for i=1, #b64_data, 76 do t[#t+1] = b64_data:sub(i, i+75).."\r\n" end
return table.concat(t)
end,
--- Encodes a string into Base64, without any extra parsing (wrapping, ...)
-- @param data (string) data to encode
-- @return decoded string
rawb64 = apr.base64_encode,
--- Decodes base64 data
-- @param data (string) base64 encoded data
-- @return decoded string
unb64 = apr.base64_decode,
}
end
--------------------------------------------------------------------------------
-- End of moduledebugger.transport.apr
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Module debugger.transport.luasocket
package.preload["debugger.transport.luasocket"] = function(...)
-------------------------------------------------------------------------------
-- 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
-------------------------------------------------------------------------------
-- LuaSocket backend for DBGP debugger.
-------------------------------------------------------------------------------
-- in order to be as lightweight as possible with Luasocket, core API is used
-- directly (to no add yet another layer)
--FIXME: remove this hack as soon as luasocket officially support 5.2
if DBGP_CLIENT_LUA_VERSION == "Lua 5.2" then
table.getn = function(t) return t and #t end
end
local socket = require "socket"
local mime = require "mime"
local ltn12 = require "ltn12"
local reg = debug.getregistry()
return {
create = socket.tcp,
sleep = socket.sleep,
-- Base64 related functions
--- Encodes a string into Base64 with line wrapping
-- @param data (string) data to encode
-- @return base64 encoded string
b64 = function(data)
local filter = ltn12.filter.chain(mime.encode("base64"), mime.wrap("base64"))
local sink, output = ltn12.sink.table()
ltn12.pump.all(ltn12.source.string(data), ltn12.sink.chain(filter, sink))
return table.concat(output)
end,
--- Encodes a string into Base64, without any extra parsing (wrapping, ...)
-- @param data (string) data to encode
-- @return decoded string
rawb64 = function(data)
return (mime.b64(data)) -- first result of the low-level function is fine here
end,
--- Decodes base64 data
-- @param data (string) base64 encoded data
-- @return decoded string
unb64 = function(data)
return (mime.unb64(data)) -- first result of the low-level function is fine here
end,
}
end
--------------------------------------------------------------------------------
-- End of moduledebugger.transport.luasocket
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Module debugger.transport.luasocket_sched
package.preload["debugger.transport.luasocket_sched"] = function(...)
-------------------------------------------------------------------------------
-- 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
-------------------------------------------------------------------------------
-- LuaSocket with LuaSched backend for DBGP debugger.
-------------------------------------------------------------------------------
-- As LuaShed totally hides blocking functions, this module MUST be loaded on the very start of the program
-- (before loading sched) to catch references to blocking functions.
local socketcore = require"socket.core"
local debug = require "debug"
local reg = debug.getregistry()
local blockingcreate = socketcore.tcp
local blockingsleep = socketcore.sleep
local blockingconnect = reg["tcp{master}"].__index.connect
local blockingreceive = reg["tcp{client}"].__index.receive
local blockingsend = reg["tcp{client}"].__index.send
local blockingsettimeout = reg["tcp{master}"].__index.settimeout
local blockingclose = reg["tcp{master}"].__index.close
-- we cannot set a new metatable directly on socket object, so wrap it into a new table
-- and forward all calls.
local blockingtcp = {
connect = function(self, address, port) return blockingconnect(self.skt, address, port) end,
receive = function(self, n) return blockingreceive(self.skt, n) end,
send = function(self, data) return blockingsend(self.skt, data) end,
settimeout = function(self, sec) return blockingsettimeout(self.skt, sec) end,
close = function(self) return blockingclose(self.skt) end,
}
blockingtcp.__index = blockingtcp
local mime = require "mime"
local ltn12 = require "ltn12"
-- verify that the socket function are the real ones and not sched not blocking versions
assert(debug.getinfo(blockingcreate, "S").what == "C", "The debugger needs the real socket functions !")
-- cleanup the package.loaded table (socket.core adds socket field into it)
package.loaded.socket = nil
return {
create = function() return setmetatable({ skt = blockingcreate() }, blockingtcp) end,
sleep = blockingsleep,
-- Base64 related functions
--- Encodes a string into Base64 with line wrapping
-- @param data (string) data to encode
-- @return base64 encoded string
b64 = function(data)
local filter = ltn12.filter.chain(mime.encode("base64"), mime.wrap("base64"))
local sink, output = ltn12.sink.table()
ltn12.pump.all(ltn12.source.string(data), ltn12.sink.chain(filter, sink))
return table.concat(output)
end,
--- Encodes a string into Base64, without any extra parsing (wrapping, ...)
-- @param data (string) data to encode
-- @return decoded string
rawb64 = function(data)
return (mime.b64(data)) -- first result of the low-level function is fine here
end,
--- Decodes base64 data
-- @param data (string) base64 encoded data
-- @return decoded string
unb64 = function(data)
return (mime.unb64(data)) -- first result of the low-level function is fine here
end,
}
end
--------------------------------------------------------------------------------
-- End of moduledebugger.transport.luasocket_sched
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Module debugger.commands
package.preload["debugger.commands"] = function(...)
-------------------------------------------------------------------------------
-- 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
-- Commented out by FlightControl. Not working in DCS.
-- 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
end
--------------------------------------------------------------------------------
-- End of moduledebugger.commands
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Module debugger.context
package.preload["debugger.context"] = function(...)
-------------------------------------------------------------------------------
-- 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
-------------------------------------------------------------------------------
-- Context handling: allows to evaluate code snippets in the context of a function
-------------------------------------------------------------------------------
local M = { }
local dbgp = require "debugger.dbgp"
local util = require "debugger.util"
-- make unique object to access contexts
local LOCAL, UPVAL, GLOBAL, EVAL, STORE, HANDLE = {}, {}, {}, {}, {}, {}
local getglobals
if DBGP_CLIENT_LUA_VERSION == "Lua 5.1" then
getglobals = function(f) return getfenv(f) end
elseif DBGP_CLIENT_LUA_VERSION == "Lua 5.2" then
getglobals = function(f, cxt)
-- 'global' environment: this is either the local _ENV or upvalue _ENV. A special case happen when a
-- function does not reference any global variable: the upvalue _ENV may not exist at all. In this case,
-- global environment is not relevant so it is fixed to an empty table. Another solution would be to set it
-- to the environment from above stack level but it would require some overhead (especially if multiple
-- levels must be instantiated)
if cxt[LOCAL][STORE]["_ENV"] then return cxt[LOCAL]["_ENV"]
elseif cxt[UPVAL][STORE]["_ENV"] then return cxt[UPVAL]["_ENV"]
else return { } end
end
end
--- Captures variables for given stack level. The capture contains local, upvalues and global variables.
-- The capture can be seen as a proxy table to the stack level: any value can be queried or set no matter
-- it is a local or an upvalue.
-- The individual local and upvalues context are also available and can be queried and modified with indexed notation too.
-- These objects are NOT persistant and must not be used outside the debugger loop which instanciated them !
M.Context = {
-- Context identifiers can be accessed by their DBGp context ID
[0] = LOCAL,
[1] = GLOBAL, -- DLTK internal ID for globals is 1
[2] = UPVAL,
-- EVAL is used to keep results from eval in cache in order to browse or modify them, results are stored as sequence
[-1] = EVAL,
STORE = STORE,
-- gets a variable by name with correct handling of Lua scope chain
-- the or chain does not work here beacause __index metamethod would raise an error instead of returning nil
__index = function(self, k)
if self[LOCAL][STORE][k] then return self[LOCAL][k]
elseif self[UPVAL][STORE][k] then return self[UPVAL][k]
else return self[GLOBAL][k] end
end,
__newindex = function(self, k, v)
if self[LOCAL][STORE][k] then self[LOCAL][k] = v
elseif self[UPVAL][STORE][k] then self[UPVAL][k] = v
else self[GLOBAL][k] = v end
end,
-- debug only !!
__tostring = function(self)
local buf = { "Locals: \n" }
for k,v in pairs(self[LOCAL][STORE]) do
buf[#buf+1] = "\t"..tostring(k).."("..tostring(v)..")="..tostring(self[LOCAL][k]).."\n"
end
buf[#buf+1] = "Upvalues: \n"
for k,v in pairs(self[UPVAL][STORE]) do
buf[#buf+1] = "\t"..tostring(k).."("..tostring(v)..")="..tostring(self[UPVAL][k]).."\n"
end
return table.concat(buf)
end,
LocalContext = {
__index = function(self, k)
local index = self[STORE][k]
if not index then error("The local "..tostring(k).." does not exists.") end
local handle = self[HANDLE]
return select(2, handle.coro:getlocal(handle.level, index))
end,
__newindex = function(self, k, v)
local index = self[STORE][k]
if index then
local handle = self[HANDLE]
handle.coro:setlocal(handle.level, index, v)
else error("Cannot set local " .. k) end
end,
-- Lua 5.2 ready :)
--__pairs = function(self) return getmetatable(self).iterator, self, nil end,
iterator = function(self, prev)
local key, index = next(self[STORE], prev)
if key then return key, self[key] else return nil end
end,
},
UpvalContext = {
__index = function(self, k)
local index = self[STORE][k]
if not index then error("The local "..tostring(k).." does not exitsts.") end
return select(2, debug.getupvalue(self[HANDLE], index))
end,
__newindex = function(self, k, v)
local index = self[STORE][k]
if index then debug.setupvalue(self[HANDLE], index, v)
else error("Cannot set upvalue " .. k) end
end,
-- Lua 5.2 ready :)
-- __pairs = function(self) return getmetatable(self).iterator, self, nil end,
iterator = function(self, prev)
local key, index = next(self[STORE], prev)
if key then return key, self[key] else return nil end
end,
},
--- Context constructor
-- @param coro (util.*Thread instance) coroutine to map to
-- @param level (number) stack level do dump (script stack level)
new = function(cls, coro, level)
local locals, upvalues = {}, {}
if level < 0 then dbgp.error(301, "No such stack level: "..tostring(level)) end
local func = (coro:getinfo(level, "f") or dbgp.error(301, "No such stack level: "..tostring(level))).func
-- local variables
for i=1, math.huge do
local name, val = coro:getlocal(level, i)
if not name then break
elseif name:sub(1,1) ~= "(" then -- skip internal values
locals[name] = i
end
end
-- upvalues
for i=1, math.huge do
local name, val = debug.getupvalue(func, i)
if not name then break end
upvalues[name] = i
end
locals = setmetatable({ [STORE] = locals, [HANDLE] = { level = level, coro = coro } }, cls.LocalContext)
upvalues = setmetatable({ [STORE] = upvalues, [HANDLE] = func }, cls.UpvalContext)
local result = setmetatable({ [LOCAL] = locals, [UPVAL] = upvalues, [EVAL] = {} }, cls)
rawset(result, GLOBAL, getglobals(func, result))
return result
end,
}
--- Handle caching of all instantiated context.
-- Returns a function which takes 2 parameters: thread and stack level and returns the corresponding context. If this
-- context has been already queried there is no new instantiation. A ContextManager is valid only during the debug loop
-- on which it has been instantiated. References to a ContextManager must be lost after the end of debug loop (so
-- threads can be collected).
-- If a context cannot be instantiated, an 301 DBGP error is thrown.
function M.ContextManager()
local cache = { }
return function(thread, level)
-- the real coroutine is used as key (not the wrapped instance as its unicity is not guaranteed)
-- otherwise, true is used to identify current thread (as nil is not a valid table key)
local key = thread[1] or true
local thread_contexts = cache[key]
if not thread_contexts then
thread_contexts = { }
cache[key] = thread_contexts
end
local context = thread_contexts[level]
if not context then
context = M.Context:new(thread, level)
thread_contexts[level] = context
end
return context
end
end
return M
end
--------------------------------------------------------------------------------
-- End of moduledebugger.context
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Module debugger.dbgp
package.preload["debugger.dbgp"] = function(...)
-------------------------------------------------------------------------------
-- 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
-------------------------------------------------------------------------------
-- DBGp protocol utility function (parsing, error handling, XML generation).
-------------------------------------------------------------------------------
local util = require "debugger.util"
local error, setmetatable, type, pairs, ipairs, tostring, tconcat =
error, setmetatable, type, pairs, ipairs, tostring, table.concat
local M = { }
--- Parses the DBGp command arguments and returns it as a Lua table with key/value pairs.
-- For example, the sequence <code>-i 5 -j foo</code> will result in <code>{i=5, j=foo}</code>
-- @param cmd_args (string) sequence of arguments
-- @return table described above
function M.arg_parse(cmd_args)
local args = {}
for arg, val in cmd_args:gmatch("%-(%w) (%S+)") do
args[arg] = val
end
return args
end
--- Parses a command line
-- @return commande name (string)
-- @retrun arguments (table)
-- @return data (string, optional)
function M.cmd_parse(cmd)
local cmd_name, args, data
if cmd:find("--", 1, true) then -- there is a data part
cmd_name, args, data = cmd:match("^(%S+)%s+(.*)%s+%-%-%s*(.*)$")
data = util.unb64(data)
else
cmd_name, args = cmd:match("^(%S+)%s+(.*)$")
end
return cmd_name, M.arg_parse(args), data
end
--- Returns the packet read from socket, or nil followed by an error message on errors.
function M.read_packet(skt)
local size = {}
while true do
local byte, err = skt:receive(1)
if not byte then return nil, err end
if byte == "\000" then break end
size[#size+1] = byte
end
return tconcat(size)
end
M.DBGP_ERR_METATABLE = {} -- unique object used to identify DBGp errors
--- Throws a correct DBGp error which result in a fine tuned error message to the server.
-- It is intended to be called into a command to make a useful error message, a standard Lua error
-- result in a code 998 error (internal debugger error).
-- @param code numerical error code
-- @param message message string (optional)
-- @param attr extra attributes to add to the response tag (optional)
function M.error(code, message, attr)
error(setmetatable({ code = code, message = message, attr = attr or {} }, M.DBGP_ERR_METATABLE), 2)
end
--- Like core assert but throws a DBGp error if condition is not met.
-- @param code numerical error code thrown if condition is not met.
-- @param message condition to test
-- @param ... will be used as error message if test fails.
function M.assert(code, success, ...)
if not success then M.error(code, (...)) end
return success, ...
end
-- -----------------
-- Outgoing data
-- -----------------
local xmlattr_specialchars = { ['"'] = "&quot;", ["<"] = "&lt;", ["&"] = "&amp;" }
--- Very basic XML generator
-- Generates a XML string from a Lua Object Model (LOM) table.
-- See http://matthewwild.co.uk/projects/luaexpat/lom.html
function M.lom2str(xml)
local pieces = { } -- string buffer
local function generate(node)
pieces[#pieces + 1] = "<"..node.tag
pieces[#pieces + 1] = " "
-- attribute ordering is not honored here
for attr, val in pairs(node.attr or {}) do
if type(attr) == "string" then
pieces[#pieces + 1] = attr .. '="' .. tostring(val):gsub('["&<]', xmlattr_specialchars) .. '"'
pieces[#pieces + 1] = " "
end
end
pieces[#pieces] = nil -- remove the last separator (useless)
if node[1] then
pieces[#pieces + 1] = ">"
for _, child in ipairs(node) do
if type(child) == "table" then generate(child)
else pieces[#pieces + 1] = "<![CDATA[" .. tostring(child) .. "]]>" end
end
pieces[#pieces + 1] = "</" .. node.tag .. ">"
else
pieces[#pieces + 1] = "/>"
end
end
generate(xml)
return tconcat(pieces)
end
function M.send_xml(skt, resp)
if not resp.attr then resp.attr = {} end
resp.attr.xmlns = "urn:debugger_protocol_v1"
local data = '<?xml version="1.0" encoding="UTF-8" ?>\n'..M.lom2str(resp)
util.log("DEBUG", "Send " .. data)
skt:send(tostring(#data).."\000"..data.."\000")
end
--- Return an XML tag describing a debugger error, with an optional message
-- @param code (number) error code (see DBGp specification)
-- @param msg (string, optional) textual description of error
-- @return table, suitable to be converted into XML
function M.make_error(code, msg)
local elem = { tag = "error", attr = { code = code } }
if msg then
elem[1] = { tostring(msg), tag = "message" }
end
return elem
end
return M
end
--------------------------------------------------------------------------------
-- End of moduledebugger.dbgp
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Module debugger.introspection
package.preload["debugger.introspection"] = function(...)
-- ----------------------------------------------------------------------------
-- 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:
-- Julien Desgats - initial API and implementation
-- ----------------------------------------------------------------------------
-- Properties generation. Generate a LOM table with data from introspection.
-- ----------------------------------------------------------------------------
local debug = require "debug"
local platform = require "debugger.platform"
local util = require "debugger.util"
local tostring, type, assert, next, rawget, getmetatable, setmetatable, getfenv, select, coyield, cocreate, costatus, coresume, sformat, tconcat =
tostring, type, assert, next, rawget, getmetatable, setmetatable, getfenv, select, coroutine.yield, coroutine.create, coroutine.status, coroutine.resume, string.format, table.concat
local MULTIVAL_MT = { __tostring = function() return "" end }
local probes = { }
-- ---------- --
-- Public API --
-- ---------- --
---
-- Introspection logic. This module implements Lua objects introspection and
-- generates a [DBGP](http://xdebug.org/docs-dbgp.php) compatible
-- [LOM](http://matthewwild.co.uk/projects/luaexpat/lom.html) data scructure.
-- @module debugger.introspection
local M = { }
---
-- Represent the actual data to send to the debugger.
-- Full XML specification can be found in [DBGP specification](http://xdebug.org/docs-dbgp.php#properties-variables-and-values).
-- Modifying properties after their generation is possible (as actual data serialization/sending is delayed)
-- but should be used with care. The XML structure uses the [LOM](http://matthewwild.co.uk/projects/luaexpat/lom.html)
-- format, refer to these documents to get more informations about fields.
--
-- In addition to table fields, it has an array part, `[1]` being the string representation (base64 encoded),
-- possibly followed by chlid properties (@{#DBGPProperty} themselves)
--
-- @field #string tag Always "property"
-- @field #table attr XML attributes, see DBGP specification
-- @type DBGPProperty
---
-- Inpectors table, contain all inspector functions.
-- Keys are either type names (`string`, `number`, ...) or metatables
-- that have a custom inspector attached.
-- @field [parent=#debugger.introspection] #table inspectors
M.inspectors = { }
---
-- Generate a DBGP property if needed. If data is in data pagination and recursion depth ranges,
-- and send a property to the debugger, otherwise drop current property.
-- @param #string name Property name (displayed in IDE)
-- @param #string typename Type name (displayed in IDE)
-- @param #string repr Value string representation
-- @param #DBGPProperty parent Parent property
-- @param #string fullname Lua expression used to get value back in further calls
-- @return #table description
-- @function [parent=#debugger.introspection] property
M.property = coyield
---
-- Adds a probe that will be called for every unknown table/userdata.
-- @param #function probe Inspector function to call.
-- @function [parent=#debugger.introspection] add_probe
M.add_probe = function(probe) probes[#probes + 1] = probe end
---
-- Inspects a Lua value by dispatching it to correct inspector. Inspector functions have the same API.
-- @param #string name Property name (will be displayed by IDE)
-- @param value Value to inspect
-- @param #table parent Parent property (LOM table of the )
-- @param #string fullname Expression used to retrieve `value` for further debugger calls
-- @return #DBGPProperty The inspected value as returned by @{debugger.introspection#debugger.introspection.property}.
-- @return #nil If the value has not been inspected
-- @function [parent=#debugger.introspection] inspect
M.inspect = function(name, value, parent, fullname)
return (M.inspectors[type(value)] or M.inspectors.default)(name, value, parent, fullname)
end
-- ----------------- --
-- Utility functions --
-- ----------------- --
local function default_inspector(name, value, parent, fullname)
return M.property(name, type(value), tostring(value), parent, fullname)
end
-- Inspects types that can have a metatable (table and userdata). Returns
-- 1) generated property
-- 2) boolean indicating whether a custom inspector has been called (in that case, do not process value any further)
local function metatable_inspector(name, value, parent, fullname)
local mt = getmetatable(value)
do
-- find by metatable
local custom = M.inspectors[mt]
if custom then return custom(name, value, parent, fullname), true end
-- or else call probes
for i=1, #probes do
local prop = probes[i](name, value, parent, fullname)
if prop then return prop, true end
end
end
local prop = default_inspector(name, value, parent, fullname)
if mt and prop then
local mtprop = M.inspect("metatable", mt, prop, "metatable["..prop.attr.fullname.."]")
if mtprop then mtprop.attr.type = "special" end
end
return prop, false
end
local function fancy_func_repr(f, info)
local args = {}
for i=1, info.nparams do
args[i] = debug.getlocal(f, i)
end
if info.isvararg then
args[#args+1] = "..."
end
return "function(" .. tconcat(args, ", ") .. ")"
end
--- Generate a name siutable for table index syntax
-- @param name Key name
-- @return #string A table index style index
-- @usage generate_printable_key('foo') => '["foo"]'
-- @usage generate_printable_key(12) => '[12]'
-- @usage generate_printable_key({}) => '[table: 0x12345678]
-- @function [parent=#debugger.introspection] generate_printable_key
local function generate_printable_key(name)
return "[" .. (type(name) == "string" and sformat("%q", name) or tostring(name)) .. "]"
end
M.generate_printable_key = generate_printable_key
-- Used to store complex keys (other than string and number) as they cannot be passed in text
-- For these keys, the resulting expression will not be the key itself but "key_cache[...]"
-- where key_cache must be mapped to this table to resolve key correctly.
M.key_cache = setmetatable({ n=0 }, { __mode = "v" })
local function generate_key(name)
local tname = type(name)
if tname == "string" then return sformat("%q", name)
elseif tname == "number" or tname == "boolean" then return tostring(name)
else -- complex key, use key_cache for lookup
local i = M.key_cache.n
M.key_cache[i] = name
M.key_cache.n = i+1
return "key_cache["..tostring(i).."]"
end
end
--- Generate a usable fullname for a value.
-- Based on parent fullname and key value, return a valid Lua expression.
-- Key can be any value (as anything can act as table key). If it cannot
-- be serialized (only string, number and boolean can), it will be temporarly
-- stored in an internal cache to be retrieved later.
-- @param #string parent Parent fullname
-- @param key The child key to generate fullname for
-- @return #string A valid fullname expression
-- @function [parent=#debugger.introspection] make_fullname
local function make_fullname(parent, key)
return parent .. "[" .. generate_key(key) .. "]"
end
M.make_fullname = make_fullname
-- ---------- --
-- Inspectors --
-- ---------- --
M.inspectors.number = default_inspector
M.inspectors.boolean = default_inspector
M.inspectors["nil"] = default_inspector
M.inspectors.userdata = default_inspector
M.inspectors.thread = default_inspector
M.inspectors.default = default_inspector -- allows 3rd party inspectors to use the default inspector if needed
M.inspectors.userdata = function(name, value, parent, fullname)
return (metatable_inspector(name, value, parent, fullname)) -- drop second return value
end
M.inspectors.string = function(name, value, parent, fullname)
-- escape linebreaks as \n and not as \<0x0A> like %q does
return M.property(name, "string", sformat("%q", value):gsub("\\\n", "\\n"), parent, fullname)
end
M.inspectors["function"] = function(name, value, parent, fullname)
local info = debug.getinfo(value, "nSflu")
local prop
if info.what ~= "C" then
-- try to create a fancy representation if possible
local repr = info.nparams and fancy_func_repr(value, info) or tostring(value)
if info.source:sub(1,1) == "@" then
repr = repr .. "\n" .. platform.get_uri("@" .. info.source) .. "\n" .. tostring(info.linedefined)
end
prop = M.property(name, "function (Lua)", repr, parent, fullname)
else
prop = M.property(name, "function", tostring(value), parent, fullname)
end
if not prop then return nil end
-- (5.1 only) environment is dumped only if it is different from global environment
-- TODO: this is not a correct behavior: environment should be dumped if is different from current stack level one
local fenv = getfenv and getfenv(value)
if fenv and fenv ~= getfenv(0) then
local fenvprop = M.inspect("environment", fenv, prop, "environment["..prop.attr.fullname.."]")
if fenvprop then fenvprop.attr.type = "special" end
end
return prop
end
M.inspectors.table = function(name, value, parent, fullname)
local prop, iscustom = metatable_inspector(name, value, parent, fullname)
if not prop or iscustom then return prop end
-- iterate over table values and detect arrays at the same time
-- next is used to circumvent __pairs metamethod in 5.2
local isarray, i = true, 1
for k,v in next, value, nil do
M.inspect(generate_printable_key(k), v, prop, make_fullname(fullname, k))
-- array detection: keys should be accessible by 1..n keys
isarray = isarray and rawget(value, i) ~= nil
i = i + 1
end
-- empty tables are considered as tables
if isarray and i > 1 then prop.attr.type = "sequence" end
return prop
end
M.inspectors[MULTIVAL_MT] = function(name, value, parent, fullname)
if value.n == 1 then
-- return directly the value as result
return M.inspect(name, value[1], parent, fullname)
else
-- wrap values inside a multival container
local prop = M.property(name, "multival", "", parent, fullname)
if not prop then return nil end
for i=1, value.n do
M.inspect(generate_printable_key(i), value[i], prop, fullname .. "[" .. i .. "]")
end
return prop
end
end
-- ------------ --
-- Internal API --
-- ------------ --
-- Used to inspect "multival" or "vararg" values. The typical use is to pack function result(s) in a single
-- value to inspect. The Multival instances can be passed to make_property as a single value, they will be
-- correctly reported to debugger
function M.Multival(...)
return setmetatable({ n=select("#", ...), ... }, MULTIVAL_MT)
end
--- Makes a property form a name/value pair (and fullname). This is an **internal** function, and should not be used by 3rd party inspectors.
-- @param #number cxt_id Context ID in which this value resides (workaround bug 352316)
-- @param value The value to debug
-- @param name The name associated with value, passed through tostring, so it can be anything
-- @param #string fullname A Lua expression to eval to get that property again (if nil, computed automatically)
-- @param #number depth The maximum property depth (recursive calls)
-- @param #number pagesize maximum children to include
-- @param #number page The page to generate (0 based)
-- @param #number size_limit Optional, if set, the maximum size of the string representation (in bytes)
-- @param #boolean safe_name If true, does not encode the name as table key
-- @return #DBGPProperty root property
-- @function [parent=#debugger.introspection] make_property
--TODO BUG ECLIPSE TOOLSLINUX-99 352316 : as a workaround, context is encoded into the fullname property
M.make_property = function(cxt_id, value, name, fullname, depth, pagesize, page, size_limit, safe_name)
fullname = fullname or "(...)[" .. generate_key(name) .. "]"
if not safe_name then name = generate_printable_key(name) end
local generator = cocreate(function() return M.inspect(name, value, nil, fullname) end)
local propstack = { }
local rootnode
local catchthis = true
local nodestoskip = page * pagesize -- nodes to skip at root level to respect pagination
local fullname_prefix = tostring(cxt_id).."|"
while true do
local succes, name, datatype, repr, parent, fullname = assert(coresume(generator, catchthis and propstack[#propstack] or nil))
-- finalize and pop all finished properties
while propstack[#propstack] ~= parent do
local topop = propstack[#propstack]
topop.attr.fullname = util.rawb64(fullname_prefix .. topop.attr.fullname)
propstack[#propstack] = nil
end
if costatus(generator) == "dead" then break end
local prop = {
tag = "property",
attr = {
children = 0,
pagesize = pagesize,
page = parent and 0 or page,
type = datatype,
name = name,
fullname = fullname,
encoding = "base64",
size = #repr,
},
util.b64(size_limit and repr:sub(1, size_limit) or repr)
}
if parent then
parent.attr.children = 1
parent.attr.numchildren = (parent.attr.numchildren or 0) + 1
-- take pagination into accont to know if node needs to be catched
catchthis = #parent <= pagesize and #propstack <= depth
if parent == rootnode then
catchthis = catchthis and nodestoskip <= 0
nodestoskip = nodestoskip - 1
end
-- add node to tree
if catchthis then
parent[#parent + 1] = prop
propstack[#propstack + 1] = prop
end
else
rootnode = prop
catchthis = true
propstack[#propstack + 1] = prop
end
end
return rootnode
end
return M
end
--------------------------------------------------------------------------------
-- End of moduledebugger.introspection
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Module debugger.plugins.ffi
package.preload["debugger.plugins.ffi"] = function(...)
-------------------------------------------------------------------------------
-- Copyright (c) 2012-2013 Julien Desgats
-- 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:
-- Julien Desgats - initial API and implementation
-------------------------------------------------------------------------------
-- LuaJIT cdata introspection library.
-------------------------------------------------------------------------------
-- known issues:
-- * references are de-referenced event if inspect_references is unset
-- * is automatic pointer and reference de-referencing is possible ?
-- (only for first item in case of arrays). Possible leads:
-- http://stackoverflow.com/questions/7134590/how-to-test-if-an-address-is-readable-in-linux-userspace-app
-- http://www.softwareverify.com/blog/?p=319
-- * when setting a value from Eclipse, the type is sometimes changed (e.g. int => number)
local introspection = require "debugger.introspection"
local reflect = require "debugger.plugins.ffi.reflect"
local ffi = require "ffi"
local tostring, tonumber, type, assert, sformat, tconcat = tostring, tonumber, type, assert, string.format, table.concat
local M = { }
--- Whether the reference types are inspected. Usually references should be safe (at least a bit
-- safer than pointers) so they are inspected. If a reference points to unsafe memory, the whole
-- program could crash !
-- If this feature is disabled, deeply nested C types will not be displayed correctly as evaluation
-- has a recursion limit, any further evaluation is done through references.
M.inspect_references = true
local function make_typename(refct)
local t = refct.what
if t == "int" then
if refct.bool then t = "bool"
else
-- use C99 type notation to give more details about acutal type
t = (refct.unsigned and "uint" or "int") .. tostring(refct.size * 8) .. "_t"
end
elseif t == "float" then
-- assume IEEE754
if refct.size == 8 then t = "double"
elseif refct.size == 16 then t = "long double" -- not really sure this one is always true
end
elseif t == "struct" or t == "enum" or t == "union" then
t = refct.name and (t .. " " .. refct.name) or ("anonymous "..t)
elseif t == "func" then
t = "function (FFI)"
elseif t == "ptr" then
t = make_typename(refct.element_type) .. "*"
elseif t == "ref" then
t = make_typename(refct.element_type) .. "&"
elseif t == "field" then
return make_typename(refct.type)
elseif t == "bitfield" then
t = (refct.type.unsigned and "unsigned" or "signed") .. ":" .. tostring(refct.size * 8)
refct = refct.type
end
if refct.const then t = "const " .. t end
if refct.volatile then t = "volatile " .. t end
return t
end
-- if cdatakind is unknown, this one will be called
local default_inspector = introspection.inspectors.number
local inspect
-- recursion must be handled with some care: if we call regular introspection.inspect
-- we may create boxed references or Lua native objects which will be inspected as such
-- (leading to wrong type names).
local function recurse(name, value, parent, fullname, refct)
if type(value) == "cdata" then
return inspect(name, value, parent, fullname, refct)
else
local prop = introspection.inspect(name, value, parent, fullname)
if prop then
prop.attr.type = make_typename(refct)
end
return prop
end
end
-- cdata specific inspectors
local inspectors = {
struct = function(name, value, parent, fullname, refct)
local prop = introspection.property(name, make_typename(refct), tostring(value), parent, fullname)
-- inspect children, if needed
if prop then
for member in refct:members() do
local mname = member.name
recurse(mname, value[mname], prop, fullname .. sformat('[%q]', mname), member)
end
end
return prop
end,
array = function(name, value, parent, fullname, refct)
local etype = refct.element_type
-- for VLAs, reflect does not give size
local size = refct.size ~= "none" and refct.size or ffi.sizeof(value)
size = size and (size / etype.size) -- we've got the byte size, not element count
local typename = make_typename(etype)
local prop = introspection.property(name, typename .. "[" .. (tostring(size) or "") .. "]", tostring(value), parent, fullname)
if prop and size then
for i=0, size-1 do
local idx = "["..tostring(i).."]"
recurse(idx, value[i], prop, fullname .. idx, etype)
end
end
return prop
end,
func = function(name, value, parent, fullname, refct)
local args = { }
for arg in refct:arguments() do
args[#args + 1] = make_typename(arg.type) .. " " .. arg.name
end
if refct.vararg then
args[#args + 1] = "..."
end
local repr = make_typename(refct.return_type) .. " " .. refct.name .. "(" .. tconcat(args, ", ") .. ")"
return introspection.property(name, make_typename(refct), repr, parent, fullname)
end,
enum = function(name, value, parent, fullname, refct)
local repr = tonumber(value)
-- try to convert numeric value into enum name
--TODO: is there a faster method to make it ?
for val in refct:values() do
if val.value == repr then
repr = val.name
break
end
end
return introspection.property(name, make_typename(refct), tostring(repr), parent, fullname)
end,
ref = function(name, value, parent, fullname, refct)
-- this may be unsafe, see inspect_references setting
local typename = make_typename(refct)
if not M.inspect_references then
return introspection.property(name, typename, tostring(value), parent, fullname)
end
local prop = recurse(name, value, parent, fullname, refct.element_type)
if prop then
prop.attr.type = typename
end
return prop
end,
int = function(name, value, parent, fullname, refct)
return introspection.property(name, make_typename(refct), tostring(tonumber(value)), parent, fullname)
end,
-- pointers are too unsafe, do not inspect them
ptr = function(name, value, parent, fullname, refct)
return introspection.property(name, make_typename(refct), tostring(value), parent, fullname)
end,
}
inspectors.union = inspectors.struct
inspectors.float = inspectors.int
-- for struct/union fields, the actual type is nested into the refct
inspectors.field = function(name, value, parent, fullname, refct)
return inspect(name, value, parent, fullname, refct.type)
end
inspectors.bitfield = inspectors.field
inspect = function(name, value, parent, fullname, refct)
-- inspect only values, not ctypes
--FIXME: this cause references to be dereferenced and crash the process if they are wrong !
if ffi.typeof(value) ~= value then
refct = refct or reflect.typeof(value)
return (inspectors[refct.what] or default_inspector)(name, value, parent, fullname, refct)
end
-- return a simple property for ctypes
return introspection.property(name, "ctype", tostring(value), parent, fullname)
end
introspection.inspectors.cdata = inspect
return M
end
--------------------------------------------------------------------------------
-- End of moduledebugger.plugins.ffi
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Module debugger.plugins.ffi.reflect
package.preload["debugger.plugins.ffi.reflect"] = function(...)
--[[ LuaJIT FFI reflection Library ]]--
--[[ Copyright (C) 2013 Peter Cawley <lua@corsix.org>. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--]]
local ffi = require "ffi"
local bit = require "bit"
local reflect = {}
-- Relevant minimal definitions from lj_ctype.h
ffi.cdef [[
typedef struct CType {
uint32_t info;
uint32_t size;
uint16_t sib;
uint16_t next;
uint32_t name;
} CType;
typedef struct CTState {
CType *tab;
uint32_t top;
uint32_t sizetab;
void *L;
void *g;
void *finalizer;
void *miscmap;
} CTState;
]]
local function gc_str(gcref) -- Convert a GCref (to a GCstr) into a string
if gcref ~= 0 then
local ts = ffi.cast("uint32_t*", gcref)
return ffi.string(ts + 4, ts[3])
end
end
local function memptr(gcobj)
return tonumber(tostring(gcobj):match"%x*$", 16)
end
-- Acquire a pointer to this Lua universe's CTState
local CTState do
local co = coroutine.create(function()end) -- Any live coroutine will do.
local uint32_ptr = ffi.typeof("uint32_t*")
local G = ffi.cast(uint32_ptr, ffi.cast(uint32_ptr, memptr(co))[2])
-- In global_State, `MRef ctype_state` is immediately before `GCRef gcroot[GCROOT_MAX]`.
-- We first find (an entry in) gcroot by looking for a metamethod name string.
local anchor = ffi.cast("uint32_t", ffi.cast("const char*", "__index"))
local i = 0
while math.abs(tonumber(G[i] - anchor)) > 64 do
i = i + 1
end
-- We then work backwards looking for something resembling ctype_state.
repeat
i = i - 1
CTState = ffi.cast("CTState*", G[i])
until ffi.cast(uint32_ptr, CTState.g) == G
end
-- Acquire the CTState's miscmap table as a Lua variable
local miscmap do
local t = {}; t[0] = t
local tvalue = ffi.cast("uint32_t*", memptr(t))[2]
ffi.cast("uint32_t*", tvalue)[ffi.abi"le" and 0 or 1] = ffi.cast("uint32_t", ffi.cast("uintptr_t", CTState.miscmap))
miscmap = t[0]
end
-- Information for unpacking a `struct CType`.
-- One table per CT_* constant, containing:
-- * A name for that CT_
-- * Roles of the cid and size fields.
-- * Whether the sib field is meaningful.
-- * Zero or more applicable boolean flags.
local CTs = {[0] =
{"int",
"", "size", false,
{0x08000000, "bool"},
{0x04000000, "float", "subwhat"},
{0x02000000, "const"},
{0x01000000, "volatile"},
{0x00800000, "unsigned"},
{0x00400000, "long"},
},
{"struct",
"", "size", true,
{0x02000000, "const"},
{0x01000000, "volatile"},
{0x00800000, "union", "subwhat"},
{0x00100000, "vla"},
},
{"ptr",
"element_type", "size", false,
{0x02000000, "const"},
{0x01000000, "volatile"},
{0x00800000, "ref", "subwhat"},
},
{"array",
"element_type", "size", false,
{0x08000000, "vector"},
{0x04000000, "complex"},
{0x02000000, "const"},
{0x01000000, "volatile"},
{0x00100000, "vla"},
},
{"void",
"", "size", false,
{0x02000000, "const"},
{0x01000000, "volatile"},
},
{"enum",
"type", "size", true,
},
{"func",
"return_type", "nargs", true,
{0x00800000, "vararg"},
{0x00400000, "sse_reg_params"},
},
{"typedef", -- Not seen
"element_type", "", false,
},
{"attrib", -- Only seen internally
"type", "value", true,
},
{"field",
"type", "offset", true,
},
{"bitfield",
"", "offset", true,
{0x08000000, "bool"},
{0x02000000, "const"},
{0x01000000, "volatile"},
{0x00800000, "unsigned"},
},
{"constant",
"type", "value", true,
{0x02000000, "const"},
},
{"extern", -- Not seen
"CID", "", true,
},
{"kw", -- Not seen
"TOK", "size",
},
}
-- Set of CType::cid roles which are a CTypeID.
local type_keys = {
element_type = true,
return_type = true,
value_type = true,
type = true,
}
-- Create a metatable for each CT.
local metatables = {
}
for _, CT in ipairs(CTs) do
local what = CT[1]
local mt = {__index = {}}
metatables[what] = mt
end
-- Logic for merging an attribute CType onto the annotated CType.
local CTAs = {[0] =
function(a, refct) error("TODO: CTA_NONE") end,
function(a, refct) error("TODO: CTA_QUAL") end,
function(a, refct)
a = 2^a.value
refct.alignment = a
refct.attributes.align = a
end,
function(a, refct)
refct.transparent = true
refct.attributes.subtype = refct.typeid
end,
function(a, refct) refct.sym_name = a.name end,
function(a, refct) error("TODO: CTA_BAD") end,
}
-- C function calling conventions (CTCC_* constants in lj_refct.h)
local CTCCs = {[0] =
"cdecl",
"thiscall",
"fastcall",
"stdcall",
}
local function refct_from_id(id) -- refct = refct_from_id(CTypeID)
local ctype = CTState.tab[id]
local CT_code = bit.rshift(ctype.info, 28)
local CT = CTs[CT_code]
local what = CT[1]
local refct = setmetatable({
what = what,
typeid = id,
name = gc_str(ctype.name),
}, metatables[what])
-- Interpret (most of) the CType::info field
for i = 5, #CT do
if bit.band(ctype.info, CT[i][1]) ~= 0 then
if CT[i][3] == "subwhat" then
refct.what = CT[i][2]
else
refct[CT[i][2]] = true
end
end
end
if CT_code <= 5 then
refct.alignment = bit.lshift(1, bit.band(bit.rshift(ctype.info, 16), 15))
elseif what == "func" then
refct.convention = CTCCs[bit.band(bit.rshift(ctype.info, 16), 3)]
end
if CT[2] ~= "" then -- Interpret the CType::cid field
local k = CT[2]
local cid = bit.band(ctype.info, 0xffff)
if type_keys[k] then
if cid == 0 then
cid = nil
else
cid = refct_from_id(cid)
end
end
refct[k] = cid
end
if CT[3] ~= "" then -- Interpret the CType::size field
local k = CT[3]
refct[k] = ctype.size
if k == "size" and bit.bnot(refct[k]) == 0 then
refct[k] = "none"
end
end
if what == "attrib" then
-- Merge leading attributes onto the type being decorated.
local CTA = CTAs[bit.band(bit.rshift(ctype.info, 16), 0xff)]
if refct.type then
local ct = refct.type
ct.attributes = {}
CTA(refct, ct)
ct.typeid = refct.typeid
refct = ct
else
refct.CTA = CTA
end
elseif what == "bitfield" then
-- Decode extra bitfield fields, and make it look like a normal field.
refct.offset = refct.offset + bit.band(ctype.info, 127) / 8
refct.size = bit.band(bit.rshift(ctype.info, 8), 127) / 8
refct.type = {
what = "int",
bool = refct.bool,
const = refct.const,
volatile = refct.volatile,
unsigned = refct.unsigned,
size = bit.band(bit.rshift(ctype.info, 16), 127),
}
refct.bool, refct.const, refct.volatile, refct.unsigned = nil
end
if CT[4] then -- Merge sibling attributes onto this type.
while ctype.sib ~= 0 do
local entry = CTState.tab[ctype.sib]
if CTs[bit.rshift(entry.info, 28)][1] ~= "attrib" then break end
if bit.band(entry.info, 0xffff) ~= 0 then break end
local sib = refct_from_id(ctype.sib)
sib:CTA(refct)
ctype = entry
end
end
return refct
end
local function sib_iter(s, refct)
repeat
local ctype = CTState.tab[refct.typeid]
if ctype.sib == 0 then return end
refct = refct_from_id(ctype.sib)
until refct.what ~= "attrib" -- Pure attribs are skipped.
return refct
end
local function siblings(refct)
-- Follow to the end of the attrib chain, if any.
while refct.attributes do
refct = refct_from_id(refct.attributes.subtype or CTState.tab[refct.typeid].sib)
end
return sib_iter, nil, refct
end
metatables.struct.__index.members = siblings
metatables.func.__index.arguments = siblings
metatables.enum.__index.values = siblings
local function find_sibling(refct, name)
local num = tonumber(name)
if num then
for sib in siblings(refct) do
if num == 1 then
return sib
end
num = num - 1
end
else
for sib in siblings(refct) do
if sib.name == name then
return sib
end
end
end
end
metatables.struct.__index.member = find_sibling
metatables.func.__index.argument = find_sibling
metatables.enum.__index.value = find_sibling
function reflect.typeof(x) -- refct = reflect.typeof(ct)
return refct_from_id(tonumber(ffi.typeof(x)))
end
function reflect.getmetatable(x) -- mt = reflect.getmetatable(ct)
return miscmap[-tonumber(ffi.typeof(x))]
end
return reflect
end
--------------------------------------------------------------------------------
-- End of moduledebugger.plugins.ffi.reflect
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Module debugger.platform
package.preload["debugger.platform"] = function(...)
-------------------------------------------------------------------------------
-- 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
-------------------------------------------------------------------------------
-- Platform/OS specific features and path handling.
-------------------------------------------------------------------------------
local url = require "debugger.url"
local util = require "debugger.util"
local M = { }
-- Execution plaform (could be win or unix)
-- Used to manage file path difference between the 2 platform
local platform = nil
-- keep all computed URIs in cache (as they are quite long to compute)
local uri_cache = { }
-- parse a normalized path and return a table of each segment
-- you could precise the path seperator.
local function split(path,sep)
local t = {}
for w in path:gmatch("[^"..(sep or "/").."]+")do
table.insert(t, w)
end
return t
end
--- Returns a RFC2396 compliant URI for given source, or false if the mapping failed
local function get_abs_file_uri (source)
local uri
if source:sub(1,1) == "@" then -- real source file
local sourcepath = source:sub(2)
local normalizedpath = M.normalize(sourcepath)
if not M.is_path_absolute(normalizedpath) then
normalizedpath = M.normalize(M.base_dir .. "/" .. normalizedpath)
end
return M.to_file_uri(normalizedpath)
else -- dynamic code, stripped bytecode, tail return, ...
return false
end
end
--FIXME: as result is cached, changes in package.path that modify the module name are missed
-- (mostly affect main module when Lua interpreter is launched with an absolute path)
local function get_module_uri (source)
if source:sub(1,1) == "@" then -- real source file
local uri
local sourcepath = source:sub(2)
local normalizedpath = M.normalize(sourcepath)
local luapathtable = split (package.path, ";")
local is_source_absolute = M.is_path_absolute(sourcepath)
-- workarround : Add always the ?.lua entry to support
-- the case where file was loaded by : "lua myfile.lua"
table.insert(luapathtable,"?.lua")
for i,var in ipairs(luapathtable) do
-- avoid relative patterns matching absolute ones (e.g. ?.lua matches anything)
if M.is_path_absolute(var) == is_source_absolute then
local escaped = string.gsub(M.normalize(var),"[%^%$%(%)%%%.%[%]%*%+%-%?]",function(c) return "%"..c end)
local pattern = string.gsub(escaped,"%%%?","(.+)")
local modulename = string.match(normalizedpath,pattern)
if modulename then
modulename = string.gsub(modulename,"/",".");
-- if we find more than 1 possible modulename return the shorter
if not uri or string.len(uri)>string.len(modulename) then
uri = modulename
end
end
end
end
if uri then return "module:///"..uri end
end
return false
end
function M.get_uri (source)
-- search in cache
if string.sub(1,1) ~= "@" then
source = source:lower()
source = source:match( "(.*\.lua)$")
if source then
source = source:match( "scripts/(.*)") or source
source = "@" .. source
end
end
if source then
local uri = uri_cache[source]
if uri ~= nil then return uri end
-- not found, create uri
if util.features.uri == "module" then
uri = get_module_uri(source)
if not uri then uri = get_abs_file_uri (source) end
else
uri = get_abs_file_uri (source)
end
uri_cache[source] = uri
return uri
else
return nil
end
end
-- get path file from uri
function M.get_path (uri)
local parsed_path = assert(url.parse(uri))
if parsed_path.scheme == "file" then
return M.to_path(parsed_path)
else
-- search in cache
-- we should surely calculate it instead of find in cache
for k,v in pairs(uri_cache)do
if v == uri then
assert(k:sub(1,1) == "@")
return k:sub(2)
end
end
end
end
function M.normalize(path)
local parts = { }
for w in path:gmatch("[^/]+") do
if w == ".." and #parts ~=0 then table.remove(parts)
elseif w ~= "." then table.insert(parts, w)
end
end
return (path:sub(1,1) == "/" and "/" or "") .. table.concat(parts, "/")
end
function M.init(executionplatform,workingdirectory)
--------------------------
-- define current platform
--------------------------
-- check parameter
if executionplatform and executionplatform ~= "unix" and executionplatform ~="win" then
error("Unable to initialize platform module : execution platform should be 'unix' or 'win'.")
end
-- use parameter as current platform
if executionplatform then
platform = executionplatform
else
--if not define try to guess it.
local function iswindows()
local p = io.popen("echo %os%")
if p then
local result =p:read("*l")
p:close()
return result == "Windows_NT"
end
return false
end
local status, iswin = pcall(iswindows)
if status and iswin then
platform = "win"
else
platform = "unix"
end
end
--------------------------
-- platform dependent function
--------------------------
if platform == "unix" then
-- The Path separator character
M.path_sep = "/"
-- TODO the way to get the absolute path can be wrong if the program loads new source files by relative path after a cd.
-- currently, the directory is registered on start, this allows program to load any source file and then change working dir,
-- which is the most common use case.
M.base_dir = workingdirectory or os.getenv("PWD")
-- convert parsed URL table to file path for the current OS (see url.parse from luasocket)
M.to_file_uri = function (path) return url.build{scheme="file",authority="", path=path} end
-- return true is the path is absolute
-- the path must be normalized
M.is_path_absolute = function (path) return path:sub(1,1) == "/" end
-- convert absolute normalized path file to uri
M.to_path = function (parsed_url) return url.unescape(parsed_url.path) end
else
-- Implementations for Windows, see UNIX versions for documentation.
M.path_sep = "\\"
M.is_path_absolute = function (path) return path:match("^%a:/") end
M.to_file_uri = function (path) return url.build{scheme="file",authority="", path="/"..path} end
M.to_path = function (parsed_url) return url.unescape(parsed_url.path):gsub("^/", "") end
local unixnormalize = M.normalize
M.normalize = function(path)
local unixnormalpath = unixnormalize(path:gsub("\\","/"):lower())
return unixnormalpath
end
-- determine base dir
local function getworkingdirectory()
local p = io.popen("echo %cd%")
if p then
local res = p:read("*l")
p:close()
return M.normalize(res)
end
end
M.base_dir = workingdirectory or getworkingdirectory()
end
if not M.base_dir then error("Unable to determine the working directory.") end
end
return M
end
--------------------------------------------------------------------------------
-- End of moduledebugger.platform
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Module debugger.util
package.preload["debugger.util"] = function(...)
-------------------------------------------------------------------------------
-- 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
-------------------------------------------------------------------------------
-- Utility functions.
-------------------------------------------------------------------------------
local M = { }
-- log system
local LEVELS = { ERROR = 0, WARNING = 1, INFO = 2, DETAIL = 3, DEBUG = 4 }
local LOG_LEVEL = LEVELS.INFO
-- Debugger features handling. Any feature can be get like any regular table, setting features result in
-- error for unknown or read-only features.
M.features = setmetatable({ }, {
-- functions that format/validate data. If function is not provided, the feature cannot be modified.
validators = {
multiple_sessions = tonumber,
encoding = tostring,
max_children = tonumber,
max_data = tonumber,
max_depth = tonumber,
show_hidden = tonumber,
uri = tostring,
log_level = function(level_name)
-- set numerical index in internal var
LOG_LEVEL = assert(LEVELS[level_name], "No such level")
return level_name -- the displayed level is still the name
end,
},
__index = {
multiple_sessions = 0,
encoding ="UTF-8",
max_children = 32,
max_data = 0xFFFF,
max_depth = 1,
show_hidden = 1,
uri = "file",
log_level = "WARNING",
-- read only features
language_supports_threads = 0,
language_name = "Lua",
language_version = _VERSION,
protocol_version = 1,
supports_async = 1,
data_encoding = "base64",
breakpoint_languages = "Lua",
breakpoint_types = "line conditional",
},
__newindex = function(self, k, v)
local mt = getmetatable(self)
local values, validator = mt.__index, mt.validators[k]
if values[k] == nil then error("No such feature " .. tostring(k)) end
if not validator then error("The feature " .. tostring(k) .. " is read-only") end
v = assert(validator(v))
values[k] = v
end,
})
-- Wraps debug function and an attached thread
-- also handle stack & coroutine management differencies between Lua versions
local getinfo, getlocal, setlocal = debug.getinfo, debug.getlocal, debug.setlocal
-- Foreign thread is used to debug paused thread
local ForeignThreadMT = {
getinfo = function(self, level, what) return getinfo(self[1], level, what) end,
getlocal = function(self, level, idx) return getlocal(self[1], level, idx) end,
setlocal = function(self, level, idx, val) return setlocal(self[1], level, idx, val) end,
}
ForeignThreadMT.__index = ForeignThreadMT
function M.ForeignThread(coro) return setmetatable({ coro }, ForeignThreadMT) end
-- Current thread is used to debug the thread that caused the hook
-- intended to be used *ONLY* in debug loop (executed in a new thread)
local CurrentThreadMT = {
getinfo = function(self, level, what) return getinfo(self[1], level + 2, what) end,
getlocal = function(self, level, idx) return getlocal(self[1], level + 2, idx) end,
setlocal = function(self, level, idx, val) return setlocal(self[1], level + 2, idx, val) end,
}
CurrentThreadMT.__index = CurrentThreadMT
function M.CurrentThread(coro) return setmetatable({ coro }, CurrentThreadMT) end
-- Some version dependant functions
if DBGP_CLIENT_LUA_VERSION == "Lua 5.1" then
local loadstring, getfenv, setfenv, debug_getinfo, MainThread =
loadstring, getfenv, setfenv, debug.getinfo, nil
-- in 5.1 "t" flag does not exist and trigger an error so remove it from what
CurrentThreadMT.getinfo = function(self, level, what) return getinfo(self[1], level + 2, what:gsub("t", "", 1)) end
ForeignThreadMT.getinfo = function(self, level, what) return getinfo(self[1], level, what:gsub("t", "", 1)) end
-- when we're forced to start debug loop on top of program stack (when on main coroutine)
-- this requires some hackery to get right stack level
-- Fallback method to inspect running thread (only for main thread in 5.1 or for conditional breakpoints)
--- Gets a script stack level with additional debugger logic added
-- @param l (number) stack level to get for debugged script (0 based)
-- @return real Lua stack level suitable to be passed through deubg functions
local function get_script_level(l)
local hook = debug.gethook()
for i=2, math.huge do
if assert(debug.getinfo(i, "f")).func == hook then
return i + l -- the script to level is just below, but because of the extra call to this function, the level is ok for callee
end
end
end
if rawget(_G, "jit") then
MainThread = {
[1] = "main", -- as the raw thread object is used as table keys, provide a replacement.
-- LuaJIT completely eliminates tail calls from stack, so get_script_level retunrs wrong result in this case
getinfo = function(self, level, what) return getinfo(get_script_level(level) - 1, what:gsub("t", "", 1)) end,
getlocal = function(self, level, idx) return getlocal(get_script_level(level) - 1, idx) end,
setlocal = function(self, level, idx, val) return setlocal(get_script_level(level) - 1, idx, val) end,
}
else
MainThread = {
[1] = "main",
getinfo = function(self, level, what) return getinfo(get_script_level(level) , what:gsub("t", "", 1)) end,
getlocal = function(self, level, idx) return getlocal(get_script_level(level), idx) end,
setlocal = function(self, level, idx, val) return setlocal(get_script_level(level), idx, val) end,
}
end
-- If the VM is vanilla Lua 5.1 or LuaJIT 2 without 5.2 compatibility, there is no way to get a reference to
-- the main coroutine, so fall back to direct mode: the debugger loop is started on the top of main thread
-- and the actual level is recomputed each time
local oldCurrentThread = M.CurrentThread
M.CurrentThread = function(coro) return coro and oldCurrentThread(coro) or MainThread end
-- load a piece of code alog with its environment
function M.loadin(code, env)
local f,err = loadstring(code)
if not f then
return nil, err
else
return f and setfenv(f, env)
end
end
-- table that maps [gs]et environment to index
M.eval_env = setmetatable({ }, {
__index = function(self, func) return getfenv(func) end,
__newindex = function(self, func, env) return setfenv(func, env) end,
})
elseif DBGP_CLIENT_LUA_VERSION == "Lua 5.2" then
local load, debug_getinfo = load, debug.getinfo
function M.getinfo(coro, level, what)
if coro then return debug_getinfo(coro, level, what)
else return debug_getinfo(level + 1, what) end
end
function M.loadin(code, env) return load(code, nil, nil, env) end
-- no eval_env for 5.2 as functions does not have environments anymore
end
-- ----------------------------------------------------------------------------
-- Bare minimal log system.
-- ----------------------------------------------------------------------------
function M.log(level, msg, ...)
if (LEVELS[level] or -1) > LOG_LEVEL then return end
if select("#", ...) > 0 then msg = msg:format(...) end
env.info(string.format("DEBUGGER\t%s\t%s\n", level, msg))
end
function M.ser(level, ...)
local msg = ""
if (LEVELS[level] or -1) > LOG_LEVEL then return end
if select("#", ...) > 0 then msg = M.serialize(...) end
env.info(string.format("DEBUGGER\t%s\t%s\n", level, msg))
end
-- porting in Slmod's serialize_slmod2
function M.serialize(tbl) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function
lookup_table = {}
local function _Serialize( tbl )
if type(tbl) == 'table' then --function only works for tables!
if lookup_table[tbl] then
return lookup_table[object]
end
local tbl_str = {}
lookup_table[tbl] = tbl_str
tbl_str[#tbl_str + 1] = '{'
for ind,val in pairs(tbl) do -- serialize its fields
local ind_str = {}
if type(ind) == "number" then
ind_str[#ind_str + 1] = '['
ind_str[#ind_str + 1] = tostring(ind)
ind_str[#ind_str + 1] = ']='
else --must be a string
ind_str[#ind_str + 1] = '['
ind_str[#ind_str + 1] = M.basicserialize(ind)
ind_str[#ind_str + 1] = ']='
end
local val_str = {}
if ((type(val) == 'number') or (type(val) == 'boolean')) then
val_str[#val_str + 1] = tostring(val)
val_str[#val_str + 1] = ','
tbl_str[#tbl_str + 1] = table.concat(ind_str)
tbl_str[#tbl_str + 1] = table.concat(val_str)
elseif type(val) == 'string' then
val_str[#val_str + 1] = M.basicserialize(val)
val_str[#val_str + 1] = ','
tbl_str[#tbl_str + 1] = table.concat(ind_str)
tbl_str[#tbl_str + 1] = table.concat(val_str)
elseif type(val) == 'nil' then -- won't ever happen, right?
val_str[#val_str + 1] = 'nil,'
tbl_str[#tbl_str + 1] = table.concat(ind_str)
tbl_str[#tbl_str + 1] = table.concat(val_str)
elseif type(val) == 'table' then
if ind == "__index" then
-- tbl_str[#tbl_str + 1] = "__index"
-- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it
else
val_str[#val_str + 1] = _Serialize(val)
val_str[#val_str + 1] = ',' --I think this is right, I just added it
tbl_str[#tbl_str + 1] = table.concat(ind_str)
tbl_str[#tbl_str + 1] = table.concat(val_str)
end
elseif type(val) == 'function' then
-- tbl_str[#tbl_str + 1] = "function " .. tostring(ind)
-- tbl_str[#tbl_str + 1] = ',' --I think this is right, I just added it
else
-- env.info('unable to serialize value type ' .. routines.utils.basicSerialize(type(val)) .. ' at index ' .. tostring(ind))
-- env.info( debug.traceback() )
end
end
tbl_str[#tbl_str + 1] = '}'
return table.concat(tbl_str)
else
if type(tbl) == 'string' then
return tbl
else
return tostring(tbl)
end
end
end
local objectreturn = _Serialize(tbl)
return objectreturn
end
--porting in Slmod's "safestring" basic serialize
function M.basicserialize(s)
if s == nil then
return "\"\""
else
if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'table') or (type(s) == 'userdata') ) then
return tostring(s)
elseif type(s) == 'string' then
s = string.format('%s', s:gsub( "%%", "%%%%" ) )
return s
end
end
end
return M
end
--------------------------------------------------------------------------------
-- End of moduledebugger.util
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Module debugger.url
package.preload["debugger.url"] = function(...)
-----------------------------------------------------------------------------
-- URI parsing, composition and relative URL resolution
-- LuaSocket toolkit.
-- Author: Diego Nehab
-- RCS ID: $Id: url.lua,v 1.38 2006/04/03 04:45:42 diego Exp $
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Declare module
-----------------------------------------------------------------------------
local string = require("string")
local base = _G
local table = require("table")
local _ENV = { }
if setfenv then setfenv(1, _ENV) end
-----------------------------------------------------------------------------
-- Module version
-----------------------------------------------------------------------------
_VERSION = "URL 1.0.1"
-----------------------------------------------------------------------------
-- Encodes a string into its escaped hexadecimal representation
-- Input
-- s: binary string to be encoded
-- Returns
-- escaped representation of string binary
-----------------------------------------------------------------------------
function escape(s)
return string.gsub(s, "([^A-Za-z0-9_])", function(c)
return string.format("%%%02x", string.byte(c))
end)
end
-----------------------------------------------------------------------------
-- Protects a path segment, to prevent it from interfering with the
-- url parsing.
-- Input
-- s: binary string to be encoded
-- Returns
-- escaped representation of string binary
-----------------------------------------------------------------------------
local function make_set(t)
local s = {}
for i,v in base.ipairs(t) do
s[t[i]] = 1
end
return s
end
-- these are allowed withing a path segment, along with alphanum
-- other characters must be escaped
local segment_set = make_set {
"-", "_", ".", "!", "~", "*", "'", "(",
")", ":", "@", "&", "=", "+", "$", ",",
}
local function protect_segment(s)
return string.gsub(s, "([^A-Za-z0-9_])", function (c)
if segment_set[c] then return c
else return string.format("%%%02x", string.byte(c)) end
end)
end
-----------------------------------------------------------------------------
-- Encodes a string into its escaped hexadecimal representation
-- Input
-- s: binary string to be encoded
-- Returns
-- escaped representation of string binary
-----------------------------------------------------------------------------
function unescape(s)
return string.gsub(s, "%%(%x%x)", function(hex)
return string.char(base.tonumber(hex, 16))
end)
end
-----------------------------------------------------------------------------
-- Builds a path from a base path and a relative path
-- Input
-- base_path
-- relative_path
-- Returns
-- corresponding absolute path
-----------------------------------------------------------------------------
local function absolute_path(base_path, relative_path)
if string.sub(relative_path, 1, 1) == "/" then return relative_path end
local path = string.gsub(base_path, "[^/]*$", "")
path = path .. relative_path
path = string.gsub(path, "([^/]*%./)", function (s)
if s ~= "./" then return s else return "" end
end)
path = string.gsub(path, "/%.$", "/")
local reduced
while reduced ~= path do
reduced = path
path = string.gsub(reduced, "([^/]*/%.%./)", function (s)
if s ~= "../../" then return "" else return s end
end)
end
path = string.gsub(reduced, "([^/]*/%.%.)$", function (s)
if s ~= "../.." then return "" else return s end
end)
return path
end
-----------------------------------------------------------------------------
-- Parses a url and returns a table with all its parts according to RFC 2396
-- The following grammar describes the names given to the URL parts
-- <url> ::= <scheme>://<authority>/<path>;<params>?<query>#<fragment>
-- <authority> ::= <userinfo>@<host>:<port>
-- <userinfo> ::= <user>[:<password>]
-- <path> :: = {<segment>/}<segment>
-- Input
-- url: uniform resource locator of request
-- default: table with default values for each field
-- Returns
-- table with the following fields, where RFC naming conventions have
-- been preserved:
-- scheme, authority, userinfo, user, password, host, port,
-- path, params, query, fragment
-- Obs:
-- the leading '/' in {/<path>} is considered part of <path>
-----------------------------------------------------------------------------
function parse(url, default)
-- initialize default parameters
local parsed = {}
for i,v in base.pairs(default or parsed) do parsed[i] = v end
-- empty url is parsed to nil
if not url or url == "" then return nil, "invalid url" end
-- remove whitespace
-- url = string.gsub(url, "%s", "")
-- get fragment
url = string.gsub(url, "#(.*)$", function(f)
parsed.fragment = f
return ""
end)
-- get scheme
url = string.gsub(url, "^([%w][%w%+%-%.]*)%:",
function(s) parsed.scheme = s; return "" end)
-- get authority
url = string.gsub(url, "^//([^/]*)", function(n)
parsed.authority = n
return ""
end)
-- get query stringing
url = string.gsub(url, "%?(.*)", function(q)
parsed.query = q
return ""
end)
-- get params
url = string.gsub(url, "%;(.*)", function(p)
parsed.params = p
return ""
end)
-- path is whatever was left
if url ~= "" then parsed.path = url end
local authority = parsed.authority
if not authority then return parsed end
authority = string.gsub(authority,"^([^@]*)@",
function(u) parsed.userinfo = u; return "" end)
authority = string.gsub(authority, ":([^:]*)$",
function(p) parsed.port = p; return "" end)
if authority ~= "" then parsed.host = authority end
local userinfo = parsed.userinfo
if not userinfo then return parsed end
userinfo = string.gsub(userinfo, ":([^:]*)$",
function(p) parsed.password = p; return "" end)
parsed.user = userinfo
return parsed
end
-----------------------------------------------------------------------------
-- Rebuilds a parsed URL from its components.
-- Components are protected if any reserved or unallowed characters are found
-- Input
-- parsed: parsed URL, as returned by parse
-- Returns
-- a stringing with the corresponding URL
-----------------------------------------------------------------------------
function build(parsed)
local ppath = parse_path(parsed.path or "")
local url = build_path(ppath)
if parsed.params then url = url .. ";" .. parsed.params end
if parsed.query then url = url .. "?" .. parsed.query end
local authority = parsed.authority
if parsed.host then
authority = parsed.host
if parsed.port then authority = authority .. ":" .. parsed.port end
local userinfo = parsed.userinfo
if parsed.user then
userinfo = parsed.user
if parsed.password then
userinfo = userinfo .. ":" .. parsed.password
end
end
if userinfo then authority = userinfo .. "@" .. authority end
end
if authority then url = "//" .. authority .. url end
if parsed.scheme then url = parsed.scheme .. ":" .. url end
if parsed.fragment then url = url .. "#" .. parsed.fragment end
-- url = string.gsub(url, "%s", "")
return url
end
-----------------------------------------------------------------------------
-- Builds a absolute URL from a base and a relative URL according to RFC 2396
-- Input
-- base_url
-- relative_url
-- Returns
-- corresponding absolute url
-----------------------------------------------------------------------------
function absolute(base_url, relative_url)
if base.type(base_url) == "table" then
base_parsed = base_url
base_url = build(base_parsed)
else
base_parsed = parse(base_url)
end
local relative_parsed = parse(relative_url)
if not base_parsed then return relative_url
elseif not relative_parsed then return base_url
elseif relative_parsed.scheme then return relative_url
else
relative_parsed.scheme = base_parsed.scheme
if not relative_parsed.authority then
relative_parsed.authority = base_parsed.authority
if not relative_parsed.path then
relative_parsed.path = base_parsed.path
if not relative_parsed.params then
relative_parsed.params = base_parsed.params
if not relative_parsed.query then
relative_parsed.query = base_parsed.query
end
end
else
relative_parsed.path = absolute_path(base_parsed.path or "",
relative_parsed.path)
end
end
return build(relative_parsed)
end
end
-----------------------------------------------------------------------------
-- Breaks a path into its segments, unescaping the segments
-- Input
-- path
-- Returns
-- segment: a table with one entry per segment
-----------------------------------------------------------------------------
function parse_path(path)
local parsed = {}
path = path or ""
--path = string.gsub(path, "%s", "")
string.gsub(path, "([^/]+)", function (s) table.insert(parsed, s) end)
for i = 1, #parsed do
parsed[i] = unescape(parsed[i])
end
if string.sub(path, 1, 1) == "/" then parsed.is_absolute = 1 end
if string.sub(path, -1, -1) == "/" then parsed.is_directory = 1 end
return parsed
end
-----------------------------------------------------------------------------
-- Builds a path component from its segments, escaping protected characters.
-- Input
-- parsed: path segments
-- unsafe: if true, segments are not protected before path is built
-- Returns
-- path: corresponding path stringing
-----------------------------------------------------------------------------
function build_path(parsed, unsafe)
local path = ""
local n = #parsed
if unsafe then
for i = 1, n-1 do
path = path .. parsed[i]
path = path .. "/"
end
if n > 0 then
path = path .. parsed[n]
if parsed.is_directory then path = path .. "/" end
end
else
for i = 1, n-1 do
path = path .. protect_segment(parsed[i])
path = path .. "/"
end
if n > 0 then
path = path .. protect_segment(parsed[n])
if parsed.is_directory then path = path .. "/" end
end
end
if parsed.is_absolute then path = "/" .. path end
return path
end
return _ENV
end
--------------------------------------------------------------------------------
-- End of moduledebugger.url
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Main content
--------------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- 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"
local io = io
-- 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
local ser = util.ser
-- 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
env.info(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
--ser( "INFO", { bp=bp } )
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 ModifiedSource = info.source
--ModifiedSource = ModifiedSource:match( '^Scripts/Moose/(.*)' ) or ModifiedSource
--ModifiedSource = ModifiedSource
--ser( "INFO", { source = ModifiedSource } )
local uri = platform.get_uri(ModifiedSource)
--ser( "INFO", {uri=uri} )
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