------------------------------------------------------------------------------- -- 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 6 parameters (IDEHOST, IDEPORT, IDEKEY, TRANSPORT, PLATFORM, WORKINGDIR). -- -- 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. -- ------------------------------------------------------------------------------- -- 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 "|" -- * 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 _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: -- -- -- ... -- 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 "[(...)[a][b]<...>]" -- TODO: such parsing is far from perfect if name:match("^[%w_]+%[.-%b[]%]$") == name then response.attr.type = "special" end dbgp.send_xml(self.skt, { tag = "response", attr = { command = "property_get", transaction_id = args.i, context = context}, response } ) end function M.property_value(self, args) args.m = -1 M.property_get(self, args) end function M.property_set(self, args, data) local cxt_num, name = assert(util.unb64(args.n):match("^(%-?%d+)|(.*)$")) cxt_num = tonumber(args.c or cxt_num) local cxt_id = context.Context[cxt_num] or dbgp.error(302, "No such context: "..tostring(cxt_num)) local level = tonumber(args.d or 0) local coro = get_coroutine(self, args.o) local cxt = self.stack(coro, level) -- evaluate the new value in the local context local value = select(2, dbgp.assert(206, pcall(dbgp.assert(206, util.loadin("return "..data, cxt))))) local chunk = dbgp.assert(206, util.loadin(name .. " = value", setmetatable({ value = value }, property_evaluation_environment))) dbgp.assert(206, pcall(chunk, cxt[cxt_id])) dbgp.send_xml(self.skt, { tag = "response", attr = { success = 1, transaction_id = args.i } } ) end --TODO dynamic code handling -- The DBGp specification is not clear about the line number meaning, this implementation is 1-based and numbers are inclusive function M.source(self, args) local path if args.f then path = platform.get_path(args.f) else path = self.coro:getinfo(0, "S").source assert(path:sub(1,1) == "@") path = path:sub(2) end local file, err = io.open(path) if not file then dbgp.error(100, err, { success = 0 }) end -- Try to identify compiled files if file:read(1) == "\033" then dbgp.error(100, args.f.." is bytecode", { success = 0 }) end file:seek("set", 0) local srclines = { } local beginline, endline, currentline = tonumber(args.b or 0), tonumber(args.e or math.huge), 0 for line in file:lines() do currentline = currentline + 1 if currentline >= beginline and currentline <= endline then srclines[#srclines + 1] = line elseif currentline >= endline then break end end file:close() srclines[#srclines + 1] = "" -- to add a trailing \n dbgp.send_xml(self.skt, { tag = "response", attr = { command = "source", transaction_id = args.i, success = 1}, util.b64(table.concat(srclines, "\n")) }) end -- Factory for both stdout and stderr commands, change file descriptor in io local function output_command_handler_factory(mode) return function(self, args) if args.c == "0" then -- disable io[mode] = io.base[mode] else io[mode] = setmetatable({ skt = self.skt, mode = mode }, args.c == "1" and core.copy_output or core.redirect_output) end dbgp.send_xml(self.skt, { tag = "response", attr = { command = mode, transaction_id = args.i, success = "1" } } ) end end M.stdout = output_command_handler_factory("stdout") M.stderr = output_command_handler_factory("stderr") return M 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 _VERSION == "Lua 5.1" then getglobals = function(f) return getfenv(f) end elseif _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 -i 5 -j foo will result in {i=5, j=foo} -- @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 = { ['"'] = """, ["<"] = "<", ["&"] = "&" } --- 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] = "" end end pieces[#pieces + 1] = "" 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 = '\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 . 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 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 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) return unixnormalize(path:gsub("\\","/"):lower()) 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.WARNING -- 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 _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 _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 io.base.stderr:write(string.format("DEBUGGER\t%s\t%s\n", level, msg)) 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 -- ::= :///;?# -- ::= @: -- ::= [:] -- :: = {/} -- 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 {/} is considered part of ----------------------------------------------------------------------------- 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.1.0" local debug = require "debug" -- To avoid cyclic dependency, internal state of the debugger that must be accessed -- elsewhere (in commands most likely) will be stored in a fake module "debugger.core" local core = { } package.loaded["debugger.core"] = core local util = require "debugger.util" local platform = require "debugger.platform" local dbgp = require "debugger.dbgp" local commands = require "debugger.commands" local context = require "debugger.context" local url = require "debugger.url" local log = util.log -- TODO complete the stdlib access local corunning, cocreate, cowrap, coyield, coresume, costatus = coroutine.running, coroutine.create, coroutine.wrap, coroutine.yield, coroutine.resume, coroutine.status -- register the URI of the debugger, to not jump into with redefined function or coroutine bootstrap stuff local debugger_uri = nil -- set in init function local transportmodule_uri = nil -- set in init function -- will contain the session object, and possibly a list of all sessions if a multi-threaded model is adopted -- this is only used for async commands. local active_session = nil -- tracks all active coroutines and associate an id to them, the table from_id is the id=>coro mapping, the table from_coro is the reverse core.active_coroutines = { n = 0, from_id = setmetatable({ }, { __mode = "v" }), from_coro = setmetatable({ }, { __mode = "k" }) } -- "BEGIN VERSION DEPENDENT CODE" local setbpenv -- set environment of a breakpoint (compiled function) if _VERSION == "Lua 5.1" then local setfenv = setfenv setbpenv = setfenv elseif _VERSION == "Lua 5.2" then local setupvalue = debug.setupvalue -- _ENV is the first upvalue setbpenv = function(f, t) return setupvalue(f, 1, t) end else error(_VERSION .. "is not supported.") end -- "END VERSION DEPENDENT CODE" ------------------------------------------------------------------------------- -- Output redirection handling ------------------------------------------------------------------------------- -- Override standard output functions & constants to redirect data written to these files to IDE too. -- This works only for output done in Lua, output written by C extensions is still go to system output file. -- references to native values io.base = { output = io.output, stdin = io.stdin, stdout = io.stdout, stderr = io.stderr } function _print(...) local buf = {...} for i=1, select("#", ...) do buf[i] = tostring(buf[i]) end io.stdout:write(table.concat(buf, "\t") .. "\n") end local oldPrint = print -- Actually change standard output file but still return the "fake" stdout function io.output(output) io.base.output(output) return io.stdout end local dummy = function() end -- metatable for redirecting output (not printed at all in actual output) core.redirect_output = { write = function(self, ...) local buf = {...} for i=1, select("#", ...) do buf[i] = tostring(buf[i]) end buf = table.concat(buf):gsub("\n", "\r\n") dbgp.send_xml(self.skt, { tag = "stream", attr = { type=self.mode }, util.b64(buf) } ) end, flush = dummy, close = dummy, setvbuf = dummy, seek = dummy } core.redirect_output.__index = core.redirect_output -- metatable for cloning output (outputs to actual system and send to IDE) core.copy_output = { write = function(self, ...) core.redirect_output.write(self, ...) io.base[self.mode]:write(...) end, flush = function(self, ...) return self.out:flush(...) end, close = function(self, ...) return self.out:close(...) end, setvbuf = function(self, ...) return self.out:setvbuf(...) end, seek = function(self, ...) return self.out:seek(...) end, } core.copy_output.__index = core.copy_output ------------------------------------------------------------------------------- -- Breakpoint registry ------------------------------------------------------------------------------- -- Registry of current stack levels of all running threads local stack_levels = setmetatable( { }, { __mode = "k" } ) -- File/line mapping for breakpoints (BP). For a given file/line, a list of BP is associated (DBGp specification section 7.6.1 -- require that multiple BP at same place must be handled) -- A BP is a table with all additional properties (type, condition, ...) the id is the string representation of the table. core.breakpoints = { -- functions to call to match hit conditions hit_conditions = { [">="] = function(value, target) return value >= target end, ["=="] = function(value, target) return value == target end, ["%"] = function(value, target) return (value % target) == 0 end, } } -- tracks events such as step_into or step_over core.events = { } do local file_mapping = { } local id_mapping = { } local waiting_sessions = { } -- sessions that wait for an event (over, into, out) local step_into = nil -- session that registered a step_into event, if any local sequence = 0 -- used to generate breakpoint IDs --- Inserts a new breakpoint into registry -- @param bp (table) breakpoint data -- @param uri (string, optional) Absolute file URI, for line breakpoints -- @param line (number, optional) Line where breakpoint stops, for line breakpoints -- @return breakpoint identifier function core.breakpoints.insert(bp) local bpid = sequence sequence = bpid + 1 bp.id = bpid -- re-encode the URI to avoid any mismatch (with authority for example) local uri = url.parse(bp.filename) bp.filename = url.build{ scheme=uri.scheme, authority="", path=platform.normalize(uri.path)} local filereg = file_mapping[bp.filename] if not filereg then filereg = { } file_mapping[bp.filename] = filereg end local linereg = filereg[bp.lineno] if not linereg then linereg = {} filereg[bp.lineno] = linereg end table.insert(linereg, bp) id_mapping[bpid] = bp return bpid end --- If breakpoint(s) exists for given file/line, uptates breakpoint counters -- and returns whether a breakpoint has matched (boolean) function core.breakpoints.at(file, line) local bps = file_mapping[file] and file_mapping[file][line] if not bps then return nil end local do_break = false for _, bp in pairs(bps) do if bp.state == "enabled" then local match = true if bp.condition then -- TODO: this is not the optimal solution because Context can be instantiated twice if the breakpoint matches local cxt = context.Context:new(active_session.coro, 0) setbpenv(bp.condition, cxt) local success, result = pcall(bp.condition) if not success then log("ERROR", "Condition evaluation failed for breakpoint at %s:%d: %s", file, line, result) end -- debugger always stops if an error occurs match = (not success) or result end if match then bp.hit_count = bp.hit_count + 1 if core.breakpoints.hit_conditions[bp.hit_condition](bp.hit_count, bp.hit_value) then if bp.temporary then core.breakpoints.remove(bp.id) end do_break = true -- there is no break to handle multiple breakpoints: all hit counts must be updated end end end end return do_break end function core.breakpoints.get(id) if id then return id_mapping[id] else return id_mapping end end function core.breakpoints.remove(id) local bp = id_mapping[id] if bp then id_mapping[id] = nil local linereg = file_mapping[bp.filename][bp.lineno] for i=1, #linereg do if linereg[i] == bp then table.remove(linereg, i) break end end -- cleanup file_mapping if not next(linereg) then file_mapping[bp.filename][bp.lineno] = nil end if not next(file_mapping[bp.filename]) then file_mapping[bp.filename] = nil end return true end return false end --- Returns an XML data structure that describes given breakpoint -- @param id (number) breakpoint ID -- @return Table describing a 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() print = oldPrint core.active_coroutines.from_id, core.active_coroutines.from_coro = { }, { } end --- This function handles the debugger commands while the execution is paused. This does not use coroutines because there is no -- way to get main coro in Lua 5.1 (only in 5.2) local function debugger_loop(self, async_packet) self.skt:settimeout(nil) -- set socket blocking -- in async mode, the debugger does not wait for another command before continuing and does not modify previous_context local async_mode = async_packet ~= nil if self.previous_context and not async_mode then self.state = "break" core.previous_context_response(self) end self.stack = context.ContextManager(self.coro) -- will be used to mutualize context allocation for each loop while true do -- reads packet local packet = async_packet or dbgp.read_packet(self.skt) if not packet then log("WARNING", "lost debugger connection") cleanup() break end async_packet = nil log("DEBUG", packet) local cmd, args, data = dbgp.cmd_parse(packet) -- FIXME: command such as continuations sent in async mode could lead both engine and IDE in inconsistent state : -- make a blacklist/whitelist of forbidden or allowed commands in async ? -- invoke function local func = commands[cmd] if func then local ok, cont = xpcall(function() return func(self, args, data) end, debug.traceback) if not ok then -- internal exception local code, msg, attr if type(cont) == "table" and getmetatable(cont) == dbgp.DBGP_ERR_METATABLE then code, msg, attr = cont.code, cont.message, cont.attr else code, msg, attr = 998, tostring(cont), { } end log("ERROR", "Command %s caused: (%d) %s", cmd, code, tostring(msg)) attr.command, attr.transaction_id = cmd, args.i dbgp.send_xml(self.skt, { tag = "response", attr = attr, dbgp.make_error(code, msg) } ) elseif cont then self.previous_context = { command = cmd, transaction_id = args.i } break elseif cont == nil and async_mode then break elseif cont == false then -- In case of commands that fully resumes debugger loop, the mode is sync async_mode = false end else log("Got unknown command: "..cmd) dbgp.send_xml(self.skt, { tag = "response", attr = { command = cmd, transaction_id = args.i, }, dbgp.make_error(4) } ) end end self.stack = nil -- free allocated contexts self.state = "running" self.skt:settimeout(0) -- reset socket to async end -- Stack handling can be pretty complex sometimes, especially with LuaJIT (as tail-call optimization are -- more aggressive as stock Lua). So all debugger stuff is done in another coroutine, which leave the program -- stack in a clean state and allow faster and clearer stack operations (no need to remove all debugger calls -- from stack for each operation). -- However, this does not always work with stock Lua 5.1 as the main coroutine cannot be referenced -- (coroutine.running() return nil). For this particular case, the debugger loop is started on the top of -- program stack and every stack operation is relative the the hook level (see MainThread in util.lua). local function line_hook(line) local do_break, packet = nil, nil local info = active_session.coro:getinfo(0, "S") local uri = platform.get_uri(info.source) if uri and uri ~= debugger_uri and uri ~= transportmodule_uri then -- the debugger does not break if the source is not known do_break = core.breakpoints.at(uri, line) or core.events.does_match() if do_break then core.events.discard() end -- check for async commands if not do_break then packet = dbgp.read_packet(active_session.skt) if packet then do_break = true end end end if do_break then local success, err = pcall(debugger_loop, active_session, packet) if not success then log("ERROR", "Error while debug loop: "..err) end end end local line_hook_coro = cocreate(function(line) while true do line_hook(line) line = coyield() end end) local function debugger_hook(event, line) local thread = corunning() or "main" if event == "call" then stack_levels[thread] = stack_levels[thread] + 1 elseif event == "tail call" then -- tail calls has no effects on stack handling: it is only used only for step commands but a such even does not -- interfere with any of them elseif event == "return" or event == "tail return" then stack_levels[thread] = stack_levels[thread] - 1 else -- line event: check for breakpoint active_session.coro = util.CurrentThread(corunning()) if active_session.coro[1] == "main" then line_hook(line) else -- run the debugger loop in another thread on the other cases (simplifies stack handling) assert(coresume(line_hook_coro, line)) end active_session.coro = nil end end if rawget(_G, "jit") then debugger_hook = function(event, line) local thread = corunning() or "main" if event == "call" then if debug.getinfo(2, "S").what == "C" then return end stack_levels[thread] = stack_levels[thread] + 1 elseif event == "return" or event == "tail return" then -- Return hooks are not called for tail calls in JIT (but unlike 5.2 there is no way to know whether a call is tail or not). -- So the only reliable way to know stack depth is to walk it. local depth = 2 -- TODO: find the fastest way to call getinfo ('what' parameter) while debug.getinfo(depth, "f") do depth = depth + 1 end stack_levels[thread] = depth - 2 elseif event == "line" then active_session.coro = util.CurrentThread(corunning()) if active_session.coro[1] == "main" then line_hook(line) else -- run the debugger loop in another thread on the other cases (simplifies stack handling) assert(coresume(line_hook_coro, line)) end active_session.coro = nil end end end local function init(host, port, idekey, transport, executionplatform, workingdirectory) -- 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) -- install base64 functions into util util.b64, util.rawb64, util.unb64 = transport.b64, transport.rawb64, transport.unb64 local skt = assert(transport.create()) skt:settimeout(nil) -- try to connect several times: if IDE launches both process and server at same time, first connect attempts may fail local ok, err print(string.format("Debugger v%s", DBGP_CLIENT_VERSION)) print(string.format("Debugger: Trying to connect to %s:%s ... ", host, port)) ok, err = skt:connect(host, port) --for i=1, 4 do if ok then print("Debugger: Connection succeed.") --break --else -- wait --transport.sleep(0.5) -- then retry. --print(string.format("Debugger: Retrying to connect to %s:%s ... ", host, port)) --ok, err = skt:connect(host, port) end --end --if err then error(string.format("Cannot connect to %s:%d : %s", host, port, err)) end if err then print(string.format("Cannot connect to %s:%d : %s", host, port, err)); return; end print = function(...) oldPrint(...) _print(...) end -- 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) -- 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 } }) --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