From 5686f975651ca1c380f9417a814de9ac246b2aa7 Mon Sep 17 00:00:00 2001 From: FlightControl_Master Date: Sun, 5 Nov 2017 06:12:04 +0100 Subject: [PATCH] Added the debugger --- Moose Development/Debugger/debugger.lua | 3353 +++++++++++++++++++++++ 1 file changed, 3353 insertions(+) create mode 100644 Moose Development/Debugger/debugger.lua diff --git a/Moose Development/Debugger/debugger.lua b/Moose Development/Debugger/debugger.lua new file mode 100644 index 000000000..430819de3 --- /dev/null +++ b/Moose Development/Debugger/debugger.lua @@ -0,0 +1,3353 @@ +------------------------------------------------------------------------------- +-- Copyright (c) 2011-2012 Sierra Wireless and others. +-- All rights reserved. This program and the accompanying materials +-- are made available under the terms of the Eclipse Public License v1.0 +-- which accompanies this distribution, and is available at +-- http://www.eclipse.org/legal/epl-v10.html +-- +-- Contributors: +-- Sierra Wireless - initial API and implementation +------------------------------------------------------------------------------- +-- Debugger using DBGp protocol. +------------------------------------------------------------------------------- +-- The module returns a single init function which takes 7 parameters (IDEHOST, IDEPORT, IDEKEY, TRANSPORT, PLATFORM, WORKINGDIR, NBRETRY). +-- +-- IDEHOST: the host name or the ip address of the DBGP server (so your ide) +-- if HOST is nil, the DBGP_IDEHOST env var is used. +-- if the env var is nil, the default value '127.0.0.1' is used. +-- +-- IDEPORT: the port of the DBGP server (must be configure in the IDE) +-- if PORT is nil, the DBGP_IDEPORT env var is used. +-- if the env var is nil, the default value '10000' is used. +-- +-- IDEIDEKEY: a string which is used as session key +-- if IDEKEY is nil, the DBGP_IDEKEY env var is used. +-- if the env var is nil, the default value 'luaidekey' is used. +-- +-- TRANSPORT: (advanced optional parameter) the module name of which implement the transport interface used to do the connection with the server. +-- by default the debugger use an internal implementation based on luasocket, but if can not use it, you could implement or use another transport layer implementation. +-- if TRANSPORT is nil, the DBGP_TRANSPORT env var is used. +-- if the env var is nil, the default value 'debugger.transport.luasocket' is used : this is the default implementation based on luasocket. +-- +-- PLATFORM: (advanced optional parameter) 'unix' or 'win32' string which define the kind of platform on which the program to debug is executed. +-- by default the debugger will try to guess it and surely success, if for some reasons it fails you could help it by precise the execution platform. +-- if PLATFORM is nil, the DBGP_PLATFORM env var is used. +-- if the env var is nil, the debugger will try to guess it. +-- +-- WORKINGDIR: (advanced optional parameter) the working directory in which the program to debug is executed. +-- by default the debugger will try to guess it and surely success, if for some reasons it fails you could help it by precise the working directory. +-- if WORKINGDIR is nil, the DBGP_WORKINGDIR env var is used. +-- if the env var is nil, the debugger will try to guess it. +-- +-- NBRETRY: (advanced optional parameter) the number of connection retry at start up. +-- if NBRETRY is nil, the DBGP_NBRETRY env var is used. +-- if the env var is nil, the default value is 10. +-- +------------------------------------------------------------------------------- +-- Known Issues: +-- * Functions cannot be created using the debugger and then called in program because their environment is mapped directly to +-- a debugger internal structure which cannot be persisted (i.e. used outside of the debug_hook). +-- * The DLTK client implementation does not handle context for properties. As a workaround, the context is encoded into the +-- fullname attribute of each property and is used likewise in property_get commands. The syntax is "|" +-- * Dynamic code (compiled with load or loadstring) is not handled (the debugger will step over it, like C code) +-- Design notes: +-- * The whole debugger state is kept in a (currently) unique session table in order to ease eventual adaptation to a multi-threaded +-- model, as DBGp needs one connection per thread. +-- * Full names of properties are base64 encoded because they can contain arbitrary data (spaces, escape characters, ...), this makes +-- command parsing munch easier and faster +-- * This debugger supports asynchronous commands: any command can be done at any time, but some of them (continuations) can lead to +-- inconsistent states. In addition, this have a quite big overhead (~66%), if performance is an issue, a custom command to disable +-- async mode could be done. +-- * All commands are implemented in table commands, see this comments on this table to additional details about commands implementation +-- * The environments in which are evaluated user code (property_* and eval commands, conditional breakpoints, ...) is a read/write +-- mapping of the local environment of a given stack level (can be accessed with variable names). See Context for additional details. +-- Context instantiation is pooled inside a debugging loop with ContextManager (each stack level is instantiated only once). +-- * Output redirection is done by redefining print and some values inside the io table. See "Output redirection handling" for details. +-- Todo list: +-- * Override I/O in init function instead of on module loading. +-- * Allow to break programatically (debugger.break()). +-- * Break-on-error feature (break if an error is thrown and there is no pcall in stack to handle it). +-- * Use new 5.2 facilities to provide informations about function (arguments names, vararg, ...) +-- * Allow to see ... content for vararg functions (5.2 only) +-- * Inspect LuaJIT C data (http://lua-users.org/lists/lua-l/2011-02/msg01012.html)-- /!\ This file is auto-generated. Do not alter manually /!\ + +-------------------------------------------------------------------------------- +-- Submodules body +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +-- Module debugger.transport.apr +package.preload["debugger.transport.apr"] = function(...) +------------------------------------------------------------------------------- +-- Copyright (c) 2011-2012 Sierra Wireless and others. +-- All rights reserved. This program and the accompanying materials +-- are made available under the terms of the Eclipse Public License v1.0 +-- which accompanies this distribution, and is available at +-- http://www.eclipse.org/legal/epl-v10.html +-- +-- Contributors: +-- Sierra Wireless - initial API and implementation +------------------------------------------------------------------------------- +-- Apache Portable Runtime backend for DBGP debugger. +------------------------------------------------------------------------------- + +local apr = require "apr" + +-- base 64 wrapping +function b64_wrap(src) + local t = {} + local b64_src = mime.b64(src) + for i=1, #b64_src, 76 do t[#t+1] = b64_src:sub(i, i+75).."\r\n" end + return table.concat(t) +end + +-- implements a subset of LuaSocket API using APR +local SOCKET_MT = { + connect = function(self, address, port) return self.skt:connect(address, port) end, + receive = function(self, n) return self.skt:read(n) end, -- only numeric read is used + send = function(self, data) return self.skt:write(data) end, + close = function(self) return self.skt:close() end, + settimeout = function(self, sec) + if sec == nil then self.skt:timeout_set(true) + elseif sec == 0 then self.skt:timeout_set(false) + else self.skt:timeout_set(math.floor(sec * 1000000)) end + end +} +SOCKET_MT.__index = SOCKET_MT + +return { + create = function() + local skt, err = apr.socket_create('tcp') + if not skt then return nil, err end + return setmetatable({skt = skt}, SOCKET_MT) + end, + sleep = apr.sleep, -- exact same API as LuaSocket + + -- Base64 related functions + --- Encodes a string into Base64 with line wrapping + -- @param data (string) data to encode + -- @return base64 encoded string + b64 = function(data) + t = {} + local b64_data = apr.base64_encode(data) + for i=1, #b64_data, 76 do t[#t+1] = b64_data:sub(i, i+75).."\r\n" end + return table.concat(t) + end, + + --- Encodes a string into Base64, without any extra parsing (wrapping, ...) + -- @param data (string) data to encode + -- @return decoded string + rawb64 = apr.base64_encode, + + --- Decodes base64 data + -- @param data (string) base64 encoded data + -- @return decoded string + unb64 = apr.base64_decode, +} + +end +-------------------------------------------------------------------------------- +-- End of moduledebugger.transport.apr +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- +-- Module debugger.transport.luasocket +package.preload["debugger.transport.luasocket"] = function(...) +------------------------------------------------------------------------------- +-- Copyright (c) 2011-2012 Sierra Wireless and others. +-- All rights reserved. This program and the accompanying materials +-- are made available under the terms of the Eclipse Public License v1.0 +-- which accompanies this distribution, and is available at +-- http://www.eclipse.org/legal/epl-v10.html +-- +-- Contributors: +-- Sierra Wireless - initial API and implementation +------------------------------------------------------------------------------- +-- LuaSocket backend for DBGP debugger. +------------------------------------------------------------------------------- + +-- in order to be as lightweight as possible with Luasocket, core API is used +-- directly (to no add yet another layer) + +--FIXME: remove this hack as soon as luasocket officially support 5.2 +if DBGP_CLIENT_LUA_VERSION == "Lua 5.2" then + table.getn = function(t) return t and #t end +end + +local socket = require "socket" +local mime = require "mime" +local ltn12 = require "ltn12" +local reg = debug.getregistry() + + +return { + create = socket.tcp, + sleep = socket.sleep, + + -- Base64 related functions + --- Encodes a string into Base64 with line wrapping + -- @param data (string) data to encode + -- @return base64 encoded string + b64 = function(data) + local filter = ltn12.filter.chain(mime.encode("base64"), mime.wrap("base64")) + local sink, output = ltn12.sink.table() + ltn12.pump.all(ltn12.source.string(data), ltn12.sink.chain(filter, sink)) + return table.concat(output) + end, + + --- Encodes a string into Base64, without any extra parsing (wrapping, ...) + -- @param data (string) data to encode + -- @return decoded string + rawb64 = function(data) + return (mime.b64(data)) -- first result of the low-level function is fine here + end, + + --- Decodes base64 data + -- @param data (string) base64 encoded data + -- @return decoded string + unb64 = function(data) + return (mime.unb64(data)) -- first result of the low-level function is fine here + end, +} + +end +-------------------------------------------------------------------------------- +-- End of moduledebugger.transport.luasocket +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- +-- Module debugger.transport.luasocket_sched +package.preload["debugger.transport.luasocket_sched"] = function(...) +------------------------------------------------------------------------------- +-- Copyright (c) 2011-2012 Sierra Wireless and others. +-- All rights reserved. This program and the accompanying materials +-- are made available under the terms of the Eclipse Public License v1.0 +-- which accompanies this distribution, and is available at +-- http://www.eclipse.org/legal/epl-v10.html +-- +-- Contributors: +-- Sierra Wireless - initial API and implementation +------------------------------------------------------------------------------- +-- LuaSocket with LuaSched backend for DBGP debugger. +------------------------------------------------------------------------------- + +-- As LuaShed totally hides blocking functions, this module MUST be loaded on the very start of the program +-- (before loading sched) to catch references to blocking functions. + +local socketcore = require"socket.core" +local debug = require "debug" +local reg = debug.getregistry() + +local blockingcreate = socketcore.tcp +local blockingsleep = socketcore.sleep + +local blockingconnect = reg["tcp{master}"].__index.connect +local blockingreceive = reg["tcp{client}"].__index.receive +local blockingsend = reg["tcp{client}"].__index.send +local blockingsettimeout = reg["tcp{master}"].__index.settimeout +local blockingclose = reg["tcp{master}"].__index.close + +-- we cannot set a new metatable directly on socket object, so wrap it into a new table +-- and forward all calls. +local blockingtcp = { + connect = function(self, address, port) return blockingconnect(self.skt, address, port) end, + receive = function(self, n) return blockingreceive(self.skt, n) end, + send = function(self, data) return blockingsend(self.skt, data) end, + settimeout = function(self, sec) return blockingsettimeout(self.skt, sec) end, + close = function(self) return blockingclose(self.skt) end, +} + +blockingtcp.__index = blockingtcp + +local mime = require "mime" +local ltn12 = require "ltn12" + +-- verify that the socket function are the real ones and not sched not blocking versions +assert(debug.getinfo(blockingcreate, "S").what == "C", "The debugger needs the real socket functions !") +-- cleanup the package.loaded table (socket.core adds socket field into it) +package.loaded.socket = nil + +return { + create = function() return setmetatable({ skt = blockingcreate() }, blockingtcp) end, + sleep = blockingsleep, + + -- Base64 related functions + --- Encodes a string into Base64 with line wrapping + -- @param data (string) data to encode + -- @return base64 encoded string + b64 = function(data) + local filter = ltn12.filter.chain(mime.encode("base64"), mime.wrap("base64")) + local sink, output = ltn12.sink.table() + ltn12.pump.all(ltn12.source.string(data), ltn12.sink.chain(filter, sink)) + return table.concat(output) + end, + + --- Encodes a string into Base64, without any extra parsing (wrapping, ...) + -- @param data (string) data to encode + -- @return decoded string + rawb64 = function(data) + return (mime.b64(data)) -- first result of the low-level function is fine here + end, + + --- Decodes base64 data + -- @param data (string) base64 encoded data + -- @return decoded string + unb64 = function(data) + return (mime.unb64(data)) -- first result of the low-level function is fine here + end, +} + +end +-------------------------------------------------------------------------------- +-- End of moduledebugger.transport.luasocket_sched +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- +-- Module debugger.commands +package.preload["debugger.commands"] = function(...) +------------------------------------------------------------------------------- +-- Copyright (c) 2011-2012 Sierra Wireless and others. +-- All rights reserved. This program and the accompanying materials +-- are made available under the terms of the Eclipse Public License v1.0 +-- which accompanies this distribution, and is available at +-- http://www.eclipse.org/legal/epl-v10.html +-- +-- Contributors: +-- Sierra Wireless - initial API and implementation +------------------------------------------------------------------------------- +-- Commands handlers for DBGp protocol. +------------------------------------------------------------------------------- +-- Debugger command functions. Each function handle a different command. +-- A command function is called with 3 arguments +-- 1. the debug session instance +-- 2. the command arguments as table +-- 3. the command data, if any +-- The result is either : +-- * true (or any value evaluated to true) : the debugger will resume the execution of the application (continuation command) +-- * false : only in async mode, the debugger WILL wait for further commands instead of continuing (typically, break command) +-- * nil/no return : in sync mode, the debugger will wait for another command. In async mode the debugger will continue the execution + +local cowrap, coyield = coroutine.wrap, coroutine.yield +local debug = require "debug" + +local core = require "debugger.core" +local dbgp = require "debugger.dbgp" +local util = require "debugger.util" +local platform = require "debugger.platform" +local introspection = require "debugger.introspection" +local context = require "debugger.context" +local log = util.log + +local M = { } -- command handlers table + +--- Gets the coroutine behind an id +-- Throws errors on unknown identifiers +-- @param coro_id (string or nil) Coroutine identifier or nil (current coroutine) +-- @return Coroutine instance or nil (if coro_id was nil or if coroutine is the current coroutine) +local function get_coroutine(self, coro_id) + if coro_id then + local coro = dbgp.assert(399, core.active_coroutines.from_id[tonumber(coro_id)], "No such coroutine") + dbgp.assert(399, coroutine.status(coro) ~= "dead", "Coroutine is dead") + if coro ~= self.coro[1] then return util.ForeignThread(coro) end + end + return self.coro +end + +M["break"] = function(self, args) + self.state = "break" + -- send response to previous command + core.previous_context_response(self) + -- and then response to break command itself + dbgp.send_xml(self.skt, { tag = "response", attr = { command = "break", transaction_id = args.i, success = 1 } } ) + return false +end + +function M.status(self, args) + dbgp.send_xml(self.skt, { tag = "response", attr = { + command = "status", + reason = "ok", + status = self.state, + transaction_id = args.i } } ) +end + +function M.stop(self, args) + dbgp.send_xml(self.skt, { tag = "response", attr = { + command = "stop", + reason = "ok", + status = "stopped", + transaction_id = args.i } } ) + self.skt:close() + os.exit(1) +end + +function M.feature_get(self, args) + local name = args.n + local response = util.features[name] or (not not M[name]) + dbgp.send_xml(self.skt, { tag = "response", attr = { + command = "feature_get", + feature_name = name, + supported = response and "1" or "0", + transaction_id = args.i }, + tostring(response) } ) +end + +function M.feature_set(self, args) + local name, value = args.n, args.v + local success = pcall(function() util.features[name] = value end) + dbgp.send_xml(self.skt, { tag = "response", attr = { + command = "feature_set", + feature = name, + success = success and 1 or 0, + transaction_id = args.i + } } ) +end + +function M.typemap_get(self, args) + local function gentype(name, type, xsdtype) + return { tag = "map", atts = { name = name, type = type, ["xsi:type"] = xsdtype } } + end + + dbgp.send_xml(self.skt, { tag = "response", attr = { + command = "typemap_get", + transaction_id = args.i, + ["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance", + ["xmlns:xsd"] = "http://www.w3.org/2001/XMLSchema", + }, + gentype("nil", "null"), + gentype("boolean", "bool", "xsd:boolean"), + gentype("number", "float", "xsd:float"), + gentype("string", "string", "xsd:string"), + gentype("function", "resource"), + gentype("userdata", "resource"), + gentype("thread", "resource"), + gentype("table", "hash"), + gentype("sequence", "array"), -- artificial type to represent sequences (1-n continuous indexes) + gentype("multival", "array"), -- used to represent return values + } ) +end + +function M.run(self) return true end + +function M.step_over(self) + core.events.register("over") + return true +end + +function M.step_out(self) + core.events.register("out") + return true +end + +function M.step_into(self) + core.events.register("into") + return true +end + +function M.eval(self, args, data) + log("DEBUG", "Going to eval "..data) + local result, err, success + local env = self.stack(self.coro, 0) + -- first, try to load as expression + -- DBGp does not support stack level here, see http://bugs.activestate.com/show_bug.cgi?id=81178 + local func, err = util.loadin("return "..data, env) + + -- if it is not an expression, try as statement (assignment, ...) + if not func then + func, err = util.loadin(data, env) + end + + if func then + success, result = pcall(function() return introspection.Multival(func()) end) + if not success then err = result end + end + + local response = { tag = "response", attr = { command = "eval", transaction_id = args.i } } + if not err then + local nresults = result.n + if nresults == 1 then result = result[1] end + + -- store result for further use (property_*) + -- TODO: this could be optimized: this is only used for Expressions view and totally useless for interactive console, + -- so storing result or not could be set by an argument + local idx + if nresults > 0 then + local cache = env[context.Context[-1]] + idx = #cache + 1 + cache[idx] = result + end + + -- As of Lua 5.1, the maximum stack size (and result count) is 8000, this limit is used to fit all results in one page + response[1] = introspection.make_property(-1, result, idx or "", nil, 1, 8000, 0, nil) + response.attr.success = 1 + else + response.attr.success = 0 + response[1] = dbgp.make_error(206, err) + end + dbgp.send_xml(self.skt, response) +end + +function M.breakpoint_set(self, args, data) + if args.o and not core.breakpoints.hit_conditions[args.o] then dbgp.error(200, "Invalid hit_condition operator: "..args.o) end + + local filename, lineno = args.f, tonumber(args.n) + local bp = { + type = args.t, + state = args.s or "enabled", + temporary = args.r == "1", -- "0" or nil makes this property false + hit_count = 0, + filename = filename, + lineno = lineno, + hit_value = tonumber(args.h or 0), + hit_condition = args.o or ">=", + } + + if args.t == "conditional" then + bp.expression = data + -- the expression is compiled only once + bp.condition = dbgp.assert(207, loadstring("return (" .. data .. ")")) + elseif args.t ~= "line" then dbgp.error(201, "BP type " .. args.t .. " not yet supported") end + + local bpid = core.breakpoints.insert(bp) + dbgp.send_xml(self.skt, { tag = "response", attr = { command = "breakpoint_set", transaction_id = args.i, state = bp.state, id = bpid } } ) +end + +function M.breakpoint_get(self, args) + dbgp.send_xml(self.skt, { tag = "response", + attr = { command = "breakpoint_get", transaction_id = args.i }, + dbgp.assert(205, core.breakpoints.get_xml(tonumber(args.d))) }) +end + +function M.breakpoint_list(self, args) + local bps = { tag = "response", attr = { command = "breakpoint_list", transaction_id = args.i } } + for id, bp in pairs(core.breakpoints.get()) do bps[#bps + 1] = core.breakpoints.get_xml(id) end + dbgp.send_xml(self.skt, bps) +end + +function M.breakpoint_update(self, args) + local bp = core.breakpoints.get(tonumber(args.d)) + if not bp then dbgp.error(205, "No such breakpint "..args.d) end + if args.o and not core.breakpoints.hit_conditions[args.o] then dbgp.error(200, "Invalid hit_condition operator: "..args.o) end + + local response = { tag = "response", attr = { command = "breakpoint_update", transaction_id = args.i } } + bp.state = args.s or bp.state + bp.lineno = tonumber(args.n or bp.lineno) + bp.hit_value = tonumber(args.h or bp.hit_value) + bp.hit_condition = args.o or bp.hit_condition + dbgp.send_xml(self.skt, response) +end + +function M.breakpoint_remove(self, args) + local response = { tag = "response", attr = { command = "breakpoint_remove", transaction_id = args.i } } + if not core.breakpoints.remove(tonumber(args.d)) then dbgp.error(205, "No such breakpint "..args.d) end + dbgp.send_xml(self.skt, response) +end + +function M.stack_depth(self, args) + local depth = 0 + local coro = get_coroutine(self, args.o) + for level = 0, math.huge do + local info = coro:getinfo(level, "St") + if not info then break end -- end of stack + depth = depth + 1 + if info.istailcall then depth = depth + 1 end -- a 'fake' level is added in that case + if info.what == "main" then break end -- levels below main chunk are not interesting + end + dbgp.send_xml(self.skt, { tag = "response", attr = { command = "stack_depth", transaction_id = args.i, depth = depth} } ) +end + +function M.stack_get(self, args) -- TODO: dynamic code + -- special URIs to identify unreachable stack levels + local what2uri = { + tail = "tailreturn:/", + C = "ccode:/", + } + +local function make_level(info, level) + local attr = { level = level, where = info.name, type="file" } + local uri = platform.get_uri(info.source) + if uri and info.currentline then -- reachable level + attr.filename = uri + attr.lineno = info.currentline + else + attr.filename = what2uri[info.what] or "unknown:/" + attr.lineno = -1 + end + return { tag = "stack", attr = attr } +end + +local node = { tag = "response", attr = { command = "stack_get", transaction_id = args.i} } +local coro = get_coroutine(self, args.o) + +if args.d then + local stack_level = tonumber(args.d) + node[#node+1] = make_level(coro:getinfo(stack_level, "nSl"), stack_level) +else + for i=0, math.huge do + local info = coro:getinfo(i, "nSlt") + if not info then break end + node[#node+1] = make_level(info, i) + -- add a fake level of stack for tail calls (tells user that the function has not been called directly) + if info.istailcall then + node[#node+1] = { tag = "stack", attr = { level=i, type="file", filename="tailreturn:/", lineno=-1 } } + end + if info.what == "main" then break end -- levels below main chunk are not interesting + end +end + +dbgp.send_xml(self.skt, node) +end + +--- Lists all active coroutines. +-- Returns a list of active coroutines with their id (an arbitrary string) to query stack and properties. The id is +-- guaranteed to be unique and stable for all coroutine life (they can be reused as long as coroutine exists). +-- Others commands such as stack_get or property_* commands takes an additional -o switch to query a particular cOroutine. +-- If the switch is not given, running coroutine will be used. +-- In case of error on coroutines (most likely coroutine not found or dead), an error 399 is thrown. +-- Note there is an important limitation due to Lua 5.1 coroutine implementation: you cannot query main "coroutine" from +-- another one, so main coroutine is not in returned list (this will change with Lua 5.2). +-- +-- This is a non-standard command. The returned XML has the following strucuture: +-- +-- +-- ... +-- +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 DBGP_CLIENT_LUA_VERSION == "Lua 5.1" then + getglobals = function(f) return getfenv(f) end +elseif DBGP_CLIENT_LUA_VERSION == "Lua 5.2" then + getglobals = function(f, cxt) + -- 'global' environment: this is either the local _ENV or upvalue _ENV. A special case happen when a + -- function does not reference any global variable: the upvalue _ENV may not exist at all. In this case, + -- global environment is not relevant so it is fixed to an empty table. Another solution would be to set it + -- to the environment from above stack level but it would require some overhead (especially if multiple + -- levels must be instantiated) + if cxt[LOCAL][STORE]["_ENV"] then return cxt[LOCAL]["_ENV"] + elseif cxt[UPVAL][STORE]["_ENV"] then return cxt[UPVAL]["_ENV"] + else return { } end + end +end + +--- Captures variables for given stack level. The capture contains local, upvalues and global variables. +-- The capture can be seen as a proxy table to the stack level: any value can be queried or set no matter +-- it is a local or an upvalue. +-- The individual local and upvalues context are also available and can be queried and modified with indexed notation too. +-- These objects are NOT persistant and must not be used outside the debugger loop which instanciated them ! +M.Context = { + -- Context identifiers can be accessed by their DBGp context ID + [0] = LOCAL, + [1] = GLOBAL, -- DLTK internal ID for globals is 1 + [2] = UPVAL, + -- EVAL is used to keep results from eval in cache in order to browse or modify them, results are stored as sequence + [-1] = EVAL, + STORE = STORE, + + -- gets a variable by name with correct handling of Lua scope chain + -- the or chain does not work here beacause __index metamethod would raise an error instead of returning nil + __index = function(self, k) + if self[LOCAL][STORE][k] then return self[LOCAL][k] + elseif self[UPVAL][STORE][k] then return self[UPVAL][k] + else return self[GLOBAL][k] end + end, + __newindex = function(self, k, v) + if self[LOCAL][STORE][k] then self[LOCAL][k] = v + elseif self[UPVAL][STORE][k] then self[UPVAL][k] = v + else self[GLOBAL][k] = v end + end, + + -- debug only !! + __tostring = function(self) + local buf = { "Locals: \n" } + for k,v in pairs(self[LOCAL][STORE]) do + buf[#buf+1] = "\t"..tostring(k).."("..tostring(v)..")="..tostring(self[LOCAL][k]).."\n" + end + buf[#buf+1] = "Upvalues: \n" + for k,v in pairs(self[UPVAL][STORE]) do + buf[#buf+1] = "\t"..tostring(k).."("..tostring(v)..")="..tostring(self[UPVAL][k]).."\n" + end + return table.concat(buf) + end, + + LocalContext = { + __index = function(self, k) + local index = self[STORE][k] + if not index then error("The local "..tostring(k).." does not exists.") end + local handle = self[HANDLE] + return select(2, handle.coro:getlocal(handle.level, index)) + end, + __newindex = function(self, k, v) + local index = self[STORE][k] + if index then + local handle = self[HANDLE] + handle.coro:setlocal(handle.level, index, v) + else error("Cannot set local " .. k) end + end, + -- Lua 5.2 ready :) + --__pairs = function(self) return getmetatable(self).iterator, self, nil end, + iterator = function(self, prev) + local key, index = next(self[STORE], prev) + if key then return key, self[key] else return nil end + end, + }, + + UpvalContext = { + __index = function(self, k) + local index = self[STORE][k] + if not index then error("The local "..tostring(k).." does not exitsts.") end + return select(2, debug.getupvalue(self[HANDLE], index)) + end, + __newindex = function(self, k, v) + local index = self[STORE][k] + if index then debug.setupvalue(self[HANDLE], index, v) + else error("Cannot set upvalue " .. k) end + end, + -- Lua 5.2 ready :) + -- __pairs = function(self) return getmetatable(self).iterator, self, nil end, + iterator = function(self, prev) + local key, index = next(self[STORE], prev) + if key then return key, self[key] else return nil end + end, + }, + + --- Context constructor + -- @param coro (util.*Thread instance) coroutine to map to + -- @param level (number) stack level do dump (script stack level) + new = function(cls, coro, level) + local locals, upvalues = {}, {} + if level < 0 then dbgp.error(301, "No such stack level: "..tostring(level)) end + local func = (coro:getinfo(level, "f") or dbgp.error(301, "No such stack level: "..tostring(level))).func + + -- local variables + for i=1, math.huge do + local name, val = coro:getlocal(level, i) + if not name then break + elseif name:sub(1,1) ~= "(" then -- skip internal values + locals[name] = i + end + end + + -- upvalues + for i=1, math.huge do + local name, val = debug.getupvalue(func, i) + if not name then break end + upvalues[name] = i + end + + locals = setmetatable({ [STORE] = locals, [HANDLE] = { level = level, coro = coro } }, cls.LocalContext) + upvalues = setmetatable({ [STORE] = upvalues, [HANDLE] = func }, cls.UpvalContext) + + local result = setmetatable({ [LOCAL] = locals, [UPVAL] = upvalues, [EVAL] = {} }, cls) + rawset(result, GLOBAL, getglobals(func, result)) + return result + end, +} + +--- Handle caching of all instantiated context. +-- Returns a function which takes 2 parameters: thread and stack level and returns the corresponding context. If this +-- context has been already queried there is no new instantiation. A ContextManager is valid only during the debug loop +-- on which it has been instantiated. References to a ContextManager must be lost after the end of debug loop (so +-- threads can be collected). +-- If a context cannot be instantiated, an 301 DBGP error is thrown. +function M.ContextManager() + local cache = { } + return function(thread, level) + -- the real coroutine is used as key (not the wrapped instance as its unicity is not guaranteed) + -- otherwise, true is used to identify current thread (as nil is not a valid table key) + local key = thread[1] or true + local thread_contexts = cache[key] + if not thread_contexts then + thread_contexts = { } + cache[key] = thread_contexts + end + + local context = thread_contexts[level] + if not context then + context = M.Context:new(thread, level) + thread_contexts[level] = context + end + + return context + end +end + +return M + +end +-------------------------------------------------------------------------------- +-- End of moduledebugger.context +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- +-- Module debugger.dbgp +package.preload["debugger.dbgp"] = function(...) +------------------------------------------------------------------------------- +-- Copyright (c) 2011-2012 Sierra Wireless and others. +-- All rights reserved. This program and the accompanying materials +-- are made available under the terms of the Eclipse Public License v1.0 +-- which accompanies this distribution, and is available at +-- http://www.eclipse.org/legal/epl-v10.html +-- +-- Contributors: +-- Sierra Wireless - initial API and implementation +------------------------------------------------------------------------------- +-- DBGp protocol utility function (parsing, error handling, XML generation). +------------------------------------------------------------------------------- + +local util = require "debugger.util" + +local error, setmetatable, type, pairs, ipairs, tostring, tconcat = + error, setmetatable, type, pairs, ipairs, tostring, table.concat + +local M = { } + +--- Parses the DBGp command arguments and returns it as a Lua table with key/value pairs. +-- For example, the sequence -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 DBGP_CLIENT_LUA_VERSION == "Lua 5.1" then + local loadstring, getfenv, setfenv, debug_getinfo, MainThread = + loadstring, getfenv, setfenv, debug.getinfo, nil + + -- in 5.1 "t" flag does not exist and trigger an error so remove it from what + CurrentThreadMT.getinfo = function(self, level, what) return getinfo(self[1], level + 2, what:gsub("t", "", 1)) end + ForeignThreadMT.getinfo = function(self, level, what) return getinfo(self[1], level, what:gsub("t", "", 1)) end + + -- when we're forced to start debug loop on top of program stack (when on main coroutine) + -- this requires some hackery to get right stack level + + -- Fallback method to inspect running thread (only for main thread in 5.1 or for conditional breakpoints) + --- Gets a script stack level with additional debugger logic added + -- @param l (number) stack level to get for debugged script (0 based) + -- @return real Lua stack level suitable to be passed through deubg functions + local function get_script_level(l) + local hook = debug.gethook() + for i=2, math.huge do + if assert(debug.getinfo(i, "f")).func == hook then + return i + l -- the script to level is just below, but because of the extra call to this function, the level is ok for callee + end + end + end + + if rawget(_G, "jit") then + MainThread = { + [1] = "main", -- as the raw thread object is used as table keys, provide a replacement. + -- LuaJIT completely eliminates tail calls from stack, so get_script_level retunrs wrong result in this case + getinfo = function(self, level, what) return getinfo(get_script_level(level) - 1, what:gsub("t", "", 1)) end, + getlocal = function(self, level, idx) return getlocal(get_script_level(level) - 1, idx) end, + setlocal = function(self, level, idx, val) return setlocal(get_script_level(level) - 1, idx, val) end, + } + else + MainThread = { + [1] = "main", + getinfo = function(self, level, what) return getinfo(get_script_level(level) , what:gsub("t", "", 1)) end, + getlocal = function(self, level, idx) return getlocal(get_script_level(level), idx) end, + setlocal = function(self, level, idx, val) return setlocal(get_script_level(level), idx, val) end, + } + end + + + + -- If the VM is vanilla Lua 5.1 or LuaJIT 2 without 5.2 compatibility, there is no way to get a reference to + -- the main coroutine, so fall back to direct mode: the debugger loop is started on the top of main thread + -- and the actual level is recomputed each time + local oldCurrentThread = M.CurrentThread + M.CurrentThread = function(coro) return coro and oldCurrentThread(coro) or MainThread end + + -- load a piece of code alog with its environment + function M.loadin(code, env) + local f,err = loadstring(code) + if not f then + return nil, err + else + return f and setfenv(f, env) + end + end + + -- table that maps [gs]et environment to index + M.eval_env = setmetatable({ }, { + __index = function(self, func) return getfenv(func) end, + __newindex = function(self, func, env) return setfenv(func, env) end, + }) +elseif DBGP_CLIENT_LUA_VERSION == "Lua 5.2" then + local load, debug_getinfo = load, debug.getinfo + function M.getinfo(coro, level, what) + if coro then return debug_getinfo(coro, level, what) + else return debug_getinfo(level + 1, what) end + end + + function M.loadin(code, env) return load(code, nil, nil, env) end + + -- no eval_env for 5.2 as functions does not have environments anymore +end + +-- ---------------------------------------------------------------------------- +-- Bare minimal log system. +-- ---------------------------------------------------------------------------- +function M.log(level, msg, ...) + if (LEVELS[level] or -1) > LOG_LEVEL then return end + if select("#", ...) > 0 then msg = msg:format(...) end + 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.4.0" +DBGP_CLIENT_LUA_VERSION = os.getenv "LUA_VERSION" or _VERSION +if DBGP_CLIENT_LUA_VERSION ~= "Lua 5.1" and DBGP_CLIENT_LUA_VERSION ~= "Lua 5.2" then + print(DBGP_CLIENT_LUA_VERSION .. " is not supported. As fallback, debugger will behave as if it runs on Lua 5.2 vm. You could also try to force it to behave as a Lua 5.1 vm by setting the LUA_VERSION environment variable to 'Lua 5.1'") + DBGP_CLIENT_LUA_VERSION = "Lua 5.2" +end + +local debug = require "debug" + +-- To avoid cyclic dependency, internal state of the debugger that must be accessed +-- elsewhere (in commands most likely) will be stored in a fake module "debugger.core" +local core = { } +package.loaded["debugger.core"] = core + +local util = require "debugger.util" +local platform = require "debugger.platform" +local dbgp = require "debugger.dbgp" +local commands = require "debugger.commands" +local context = require "debugger.context" +local url = require "debugger.url" + +local log = util.log + + +-- TODO complete the stdlib access +local corunning, cocreate, cowrap, coyield, coresume, costatus = coroutine.running, coroutine.create, coroutine.wrap, coroutine.yield, coroutine.resume, coroutine.status + + +-- register the URI of the debugger, to not jump into with redefined function or coroutine bootstrap stuff +local debugger_uri = nil -- set in init function +local transportmodule_uri = nil -- set in init function + +-- will contain the session object, and possibly a list of all sessions if a multi-threaded model is adopted +-- this is only used for async commands. +local active_session = nil + +-- tracks all active coroutines and associate an id to them, the table from_id is the id=>coro mapping, the table from_coro is the reverse +core.active_coroutines = { n = 0, from_id = setmetatable({ }, { __mode = "v" }), from_coro = setmetatable({ }, { __mode = "k" }) } + +-- "BEGIN VERSION DEPENDENT CODE" +local setbpenv -- set environment of a breakpoint (compiled function) +if DBGP_CLIENT_LUA_VERSION == "Lua 5.1" then + local setfenv = setfenv + setbpenv = setfenv +elseif DBGP_CLIENT_LUA_VERSION == "Lua 5.2" then + local setupvalue = debug.setupvalue + -- _ENV is the first upvalue + setbpenv = function(f, t) return setupvalue(f, 1, t) end +end +-- "END VERSION DEPENDENT CODE" + +------------------------------------------------------------------------------- +-- Output redirection handling +------------------------------------------------------------------------------- +-- Override standard output functions & constants to redirect data written to these files to IDE too. +-- This works only for output done in Lua, output written by C extensions is still go to system output file. + +-- references to native values +io.base = { output = io.output, stdin = io.stdin, stdout = io.stdout, stderr = io.stderr } + +function print(...) + local buf = {...} + for i=1, select("#", ...) do + buf[i] = tostring(buf[i]) + end + env.info(table.concat(buf, "\t") .. "\n") +end + +-- Actually change standard output file but still return the "fake" stdout +function io.output(output) + io.base.output(output) + return io.stdout +end + +local dummy = function() end + +-- metatable for redirecting output (not printed at all in actual output) +core.redirect_output = { + write = function(self, ...) + local buf = {...} + for i=1, select("#", ...) do buf[i] = tostring(buf[i]) end + buf = table.concat(buf):gsub("\n", "\r\n") + dbgp.send_xml(self.skt, { tag = "stream", attr = { type=self.mode }, util.b64(buf) } ) + end, + flush = dummy, + close = dummy, + setvbuf = dummy, + seek = dummy +} +core.redirect_output.__index = core.redirect_output + +-- metatable for cloning output (outputs to actual system and send to IDE) +core.copy_output = { + write = function(self, ...) + core.redirect_output.write(self, ...) + io.base[self.mode]:write(...) + end, + flush = function(self, ...) return self.out:flush(...) end, + close = function(self, ...) return self.out:close(...) end, + setvbuf = function(self, ...) return self.out:setvbuf(...) end, + seek = function(self, ...) return self.out:seek(...) end, +} +core.copy_output.__index = core.copy_output + +------------------------------------------------------------------------------- +-- Breakpoint registry +------------------------------------------------------------------------------- +-- Registry of current stack levels of all running threads +local stack_levels = setmetatable( { }, { __mode = "k" } ) + +-- File/line mapping for breakpoints (BP). For a given file/line, a list of BP is associated (DBGp specification section 7.6.1 +-- require that multiple BP at same place must be handled) +-- A BP is a table with all additional properties (type, condition, ...) the id is the string representation of the table. +core.breakpoints = { + -- functions to call to match hit conditions + hit_conditions = { + [">="] = function(value, target) return value >= target end, + ["=="] = function(value, target) return value == target end, + ["%"] = function(value, target) return (value % target) == 0 end, + } +} + +-- tracks events such as step_into or step_over +core.events = { } + +do + local file_mapping = { } + local id_mapping = { } + local waiting_sessions = { } -- sessions that wait for an event (over, into, out) + local step_into = nil -- session that registered a step_into event, if any + local sequence = 0 -- used to generate breakpoint IDs + + --- Inserts a new breakpoint into registry + -- @param bp (table) breakpoint data + -- @param uri (string, optional) Absolute file URI, for line breakpoints + -- @param line (number, optional) Line where breakpoint stops, for line breakpoints + -- @return breakpoint identifier + function core.breakpoints.insert(bp) + local bpid = sequence + sequence = bpid + 1 + bp.id = bpid + -- re-encode the URI to avoid any mismatch (with authority for example) + local uri = url.parse(bp.filename) + bp.filename = url.build{ scheme=uri.scheme, authority="", path=platform.normalize(uri.path)} + + local filereg = file_mapping[bp.filename] + if not filereg then + filereg = { } + file_mapping[bp.filename] = filereg + end + + local linereg = filereg[bp.lineno] + if not linereg then + linereg = {} + filereg[bp.lineno] = linereg + end + + BASE:E( { "Breakpoint", bp } ) + + table.insert(linereg, bp) + + id_mapping[bpid] = bp + return bpid + end + + --- If breakpoint(s) exists for given file/line, uptates breakpoint counters + -- and returns whether a breakpoint has matched (boolean) + function core.breakpoints.at(file, line) + + BASE:E( { "File", file, "Line", 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() + 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) + --BASE:E( { "Source", info.source, debugger_uri } ) + if uri and uri ~= debugger_uri and uri ~= transportmodule_uri then -- the debugger does not break if the source is not known + do_break = core.breakpoints.at(uri, line) or core.events.does_match() + if do_break then + core.events.discard() + end + + -- check for async commands + if not do_break then + packet = dbgp.read_packet(active_session.skt) + if packet then do_break = true end + end + end + + if do_break then + local success, err = pcall(debugger_loop, active_session, packet) + if not success then log("ERROR", "Error while debug loop: "..err) end + end +end + +local line_hook_coro = cocreate(function(line) + while true do + line_hook(line) + line = coyield() + end +end) + +local function debugger_hook(event, line) + local thread = corunning() or "main" + env.info( "running" ) + 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" + env.info( "running" ) + if event == "call" then + if debug.getinfo(2, "S").what == "C" then return end + stack_levels[thread] = stack_levels[thread] + 1 + elseif event == "return" or event == "tail return" then + -- Return hooks are not called for tail calls in JIT (but unlike 5.2 there is no way to know whether a call is tail or not). + -- So the only reliable way to know stack depth is to walk it. + local depth = 2 + -- TODO: find the fastest way to call getinfo ('what' parameter) + while debug.getinfo(depth, "f") do depth = depth + 1 end + stack_levels[thread] = depth - 2 + elseif event == "line" then + active_session.coro = util.CurrentThread(corunning()) + if active_session.coro[1] == "main" then + line_hook(line) + else + -- run the debugger loop in another thread on the other cases (simplifies stack handling) + assert(coresume(line_hook_coro, line)) + end + active_session.coro = nil + end + end +end + +local function sendInitPacket(skt,idekey) + -- get the root script path (the highest possible stack index) + local source + for i=2, math.huge do + local info = debug.getinfo(i) + if not info then break end + source = platform.get_uri(info.source) or source + end + if not source then source = "unknown:/" end -- when loaded before actual script (with a command line switch) + + -- generate some kind of thread identifier + local thread = corunning() or "main" + stack_levels[thread] = 1 -- the return event will set the counter to 0 + local sessionid = tostring(os.time()) .. "_" .. tostring(thread) + + dbgp.send_xml(skt, { tag = "init", attr = { + appid = "Lua DBGp", + idekey = idekey, + session = sessionid, + thread = tostring(thread), + parent = "", + language = "Lua", + protocol_version = "1.0", + fileuri = source + } }) + + return sessionid +end + +local function init(host, port, idekey, transport, executionplatform, workingdirectory, nbRetry) + -- get connection data + local host = host or os.getenv "DBGP_IDEHOST" or "127.0.0.1" + local port = port or os.getenv "DBGP_IDEPORT" or "10000" + local idekey = idekey or os.getenv("DBGP_IDEKEY") or "luaidekey" + + -- init plaform module + local executionplatform = executionplatform or os.getenv("DBGP_PLATFORM") or nil + local workingdirectory = workingdirectory or os.getenv("DBGP_WORKINGDIR") or nil + platform.init(executionplatform,workingdirectory) + + -- get transport layer + local transportpath = transport or os.getenv("DBGP_TRANSPORT") or "debugger.transport.luasocket" + local transport = require(transportpath) + + -- nb retry for connection + local nbRetry = nbRetry or os.getenv("DBGP_NBRETRY") or 10 + nbRetry = math.max(nbRetry,1) + + -- install base64 functions into util + util.b64, util.rawb64, util.unb64 = transport.b64, transport.rawb64, transport.unb64 + + -- get the debugger and transport layer URI + debugger_uri = platform.get_uri(debug.getinfo(1).source) + transportmodule_uri = platform.get_uri(debug.getinfo(transport.create).source) + + -- try to connect several times: if IDE launches both process and server at same time, first connect attempts may fail + local skt,ok, err + print(string.format("Debugger v%s", DBGP_CLIENT_VERSION)) + print(string.format("Debugger: Trying to connect to %s:%s ... ", host, port)) + + local timeelapsed = 0 + local sessionid = nil + for i=1,nbRetry do + -- try to connect to DBGP server + skt = assert(transport.create()) + skt:settimeout(nil) + ok, err = skt:connect(host, port) + if ok then + sessionid = sendInitPacket(skt,idekey) + -- test if socket is closed + ok, err = skt:receive(0) + if err == nil then print("Debugger: Connection succeed.") break end + end + + if err ~= nil then + -- failed to connect + print(string.format("Debugger: Failed to connect to %s:%s (%s)", host, port, err)) + skt:close() + + -- wait&retry + local timetowait = math.min(3, math.max(timeelapsed/2, 0.25)) + if i < nbRetry then + print(string.format("Debugger: Retrying to connect to %s:%s in %.2fs ... ", host, port,timetowait)) + transport.sleep(timetowait) + timeelapsed = timeelapsed+timetowait + end + end + end + if err then error(string.format("Cannot connect to %s:%d : %s", host, port, err)) end + + --FIXME util.CurrentThread(corunning) => util.CurrentThread(corunning()) WHAT DOES IT FIXES ?? + local sess = { skt = skt, state = "starting", id = sessionid, coro = util.CurrentThread(corunning) } + active_session = sess + debugger_loop(sess) + + -- set debug hooks + debug.sethook(debugger_hook, "rlc") + + -- install coroutine collecting functions. + -- TODO: maintain a list of *all* coroutines can be overkill (for example, the ones created by copcall), make a extension point to + -- customize debugged coroutines + -- coroutines are referenced during their first resume (so we are sure that they always have a stack frame) + local function resume_handler(coro, ...) + if costatus(coro) == "dead" then + local coro_id = core.active_coroutines.from_coro[coro] + core.active_coroutines.from_id[coro_id] = nil + core.active_coroutines.from_coro[coro] = nil + stack_levels[coro] = nil + end + return ... + end + + function coroutine.resume(coro, ...) + if not stack_levels[coro] then + -- first time referenced + stack_levels[coro] = 0 + core.active_coroutines.n = core.active_coroutines.n + 1 + core.active_coroutines.from_id[core.active_coroutines.n] = coro + core.active_coroutines.from_coro[coro] = core.active_coroutines.n + debug.sethook(coro, debugger_hook, "rlc") + end + return resume_handler(coro, coresume(coro, ...)) + end + + -- coroutine.wrap uses directly C API for coroutines and does not trigger our overridden coroutine.resume + -- so this is an implementation of wrap in pure Lua + local function wrap_handler(status, ...) + if not status then error((...)) end + return ... + end + + function coroutine.wrap(f) + local coro = coroutine.create(f) + return function(...) + return wrap_handler(coroutine.resume(coro, ...)) + end + end + + return sess +end + +return init