Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit 306ec0c99935849881cac0a93ddd29e302190224 0 parents
@davidoshiro authored
3  .buildpath
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<buildpath>
+</buildpath>
2  .gitignore
@@ -0,0 +1,2 @@
+.DS_Store
+.project
BIN  Icon-72.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  Icon.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  Icon@2x.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 build.settings
@@ -0,0 +1,47 @@
+-- Supported values for orientation:
+-- portrait, portraitUpsideDown, landscapeLeft, landscapeRight
+
+settings = {
+
+ orientation = {
+ default = "portrait",
+ supported = { "portrait", }
+ },
+
+ iphone = {
+ plist = {
+ CFBundleIconFile = "Icon.png",
+ CFBundleIconFiles = {
+ "Icon.png",
+ "Icon@2x.png",
+ "Icon-72.png",
+ },
+ UIStatusBarHidden = false,
+ UIPrerenderedIcon = true, -- set to false for "shine" overlay
+ --UIApplicationExitsOnSuspend = true, -- uncomment to quit app on suspend
+
+ --[[
+ -- iOS app URL schemes:
+ CFBundleURLTypes =
+ {
+ {
+ CFBundleURLSchemes =
+ {
+ "fbXXXXXXXXXXXXXX", -- example scheme for facebook
+ "coronasdkapp", -- example second scheme
+ }
+ }
+ }
+ --]]
+ }
+ },
+
+ --[[
+ -- Android permissions
+
+ androidPermissions = {
+ "android.permission.INTERNET",
+ },
+
+ ]]--
+}
29 config.lua
@@ -0,0 +1,29 @@
+application = {
+ content = {
+ width = 320,
+ height = 480,
+ scale = "letterBox",
+ fps = 30,
+
+ --[[
+ imageSuffix = {
+ ["@2x"] = 2,
+ }
+ --]]
+ },
+
+ --[[
+ -- Push notifications
+
+ notification =
+ {
+ iphone =
+ {
+ types =
+ {
+ "badge", "sound", "alert", "newsstand"
+ }
+ }
+ }
+ --]]
+}
1,622 debugger.lua
@@ -0,0 +1,1622 @@
+-------------------------------------------------------------------------------
+-- Copyright (c) 2011 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 3 parameters (HOST, PORT, IDEKEY).
+
+-- HOST: 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.
+
+-- PORT: 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.
+
+-- IDEKEY: 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.
+
+-------------------------------------------------------------------------------
+-- Known Issues:
+-- * Functions cannot be created using the debugger and then called in program because their environment is mapped directly to
+-- a debugger internal structure which cannot be persisted (i.e. used outside of the debug_hook).
+-- * The DLTK client implementation does not handle context for properties. As a workaround, the context is encoded into the
+-- fullname attribute of each property and is used likewise in property_get commands. The syntax is "<context ID>|<full name>"
+-- * Dynamic code (compiled with load or loadstring) is not handled (the debugger will step over it, like C code)
+-- Design notes:
+-- * The whole debugger state is kept in a (currently) unique session table in order to ease eventual adaptation to a multi-threaded
+-- model, as DBGp needs one connection per thread.
+-- * Full names of properties are base64 encoded because they can contain arbitrary data (spaces, escape characters, ...), this makes
+-- command parsing munch easier and faster
+-- * This debugger supports asynchronous commands: any command can be done at any time, but some of them (continuations) can lead to
+-- inconsistent states. In addition, this have a quite big overhead (~66%), if performance is an issue, a custom command to disable
+-- async mode could be done.
+-- * All commands are implemented in table commands, see this comments on this table to additional details about commands implementation
+-- * The environments in which are evaluated user code (property_* and eval commands, conditional breakpoints, ...) is a read/write
+-- mapping of the local environment of a given stack level (can be accessed with variable names). See Context for additional details.
+-- Context instantiation is pooled inside a debugging loop with ContextManager (each stack level is instantiated only once).
+-- * Output redirection is done by redefining print and some values inside the io table. See "Output redirection handling" for details.
+
+-- "hack" to have access to raw socket function before their redefinition for sched
+-- This is done because once in debugger loop, the expected behavior is to block the whole Lua VM with the receive.
+-- FIXME this is fine as long as this is a PoC but an access to these functions should be available in RA socket
+local blockingtcp = {}
+do
+ local socketcore = require"socket.core"
+ local reg = debug.getregistry()
+ blockingtcp.sleep = socketcore.sleep
+ blockingtcp.connect = reg["tcp{master}"].__index.connect
+ blockingtcp.receive = reg["tcp{client}"].__index.receive
+ blockingtcp.send = reg["tcp{client}"].__index.send
+ blockingtcp.settimeout = reg["tcp{master}"].__index.settimeout
+
+ -- verify that the socket function are the real ones and not ReadyAgent not blocking versions
+ assert(debug.getinfo(blockingtcp.connect, "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
+end
+-- TODO as of 07/20/2011, the socket module packaged with ready agent requires sched to be loaded before
+pcall(require, "sched")
+local socket = require"socket"
+local url = require"socket.url"
+
+local mime = require"mime"
+local ltn12 = require"ltn12"
+
+local log
+do
+ -- make a bare minimal log system
+ local LEVELS = { ERROR = 0, WARNING = 1, INFO = 2, DETAIL = 3, DEBUG = 4 }
+ local LOG_LEVEL = 1 -- log only errors and warnings
+ log = function(mod, 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("%s\t%s\t%s\n", mod, level, msg))
+ end
+end
+
+-- TODO complete the stdlib access
+local debug_getinfo, corunning, cocreate, cowrap, coyield, coresume, costatus = debug.getinfo, coroutine.running, coroutine.create, coroutine.wrap, coroutine.yield, coroutine.resume, coroutine.status
+
+-- correct function debug.getinfo behavior regarding first argument (sometimes nil in our case)
+local function getinfo(coro, level, what)
+ if coro then return debug_getinfo(coro, level, what)
+ else return debug_getinfo(level + 1, what) end
+end
+
+-- "BEGIN PLATFORM DEPENDENT CODE"
+-- Get the execution plaform os (could be win or unix)
+-- Used to manage file path difference between the 2 platform
+local platform = "unix"
+do
+ 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 function setplatform()
+ if iswindows() then
+ platform = "win"
+ end
+ end
+ pcall(setplatform)
+end
+
+-- The Path separator character
+local path_sep = (platform == "unix" and "/" or "\\")
+
+
+-- return true is the path is absolute
+-- the path must be normalized
+-- test if it begin by / for unix and X:/ for windows
+local is_path_absolute
+if platform == "unix" then
+ is_path_absolute = function (path) return path:sub(1,1) == "/" end
+else
+ is_path_absolute = function (path) return path:match("^%a:/") end
+end
+
+-- return a clean path with / as separator (without double path separator)
+local normalize
+if platform == "unix" then
+ normalize = function (path) return path:gsub("//","/") end
+else
+ normalize = function (path)
+ return path:gsub("\\","/"):gsub("//","/"):lower()
+ end
+end
+
+-- 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.
+local base_dir
+if platform == "unix" then
+ base_dir = os.getenv("PWD")
+else
+ local p = io.popen("echo %cd%")
+ if p then
+ base_dir = p:read("*l")
+ base_dir = normalize(base_dir)
+ p:close()
+ end
+end
+if not base_dir then error("Unable to determine the working directory.")end
+
+
+-- 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 string.gfind(path, "[^"..sep.."]+")do
+ table.insert(t, w)
+ end
+ return t
+end
+
+-- merge an asbolute and a relative path in an absolute path
+-- you could precise the path seperator, '/' is the default one.
+local function merge_paths(absolutepath, relativepath,separator)
+ local sep = separator or "/"
+ local absolutetable = split(absolutepath,sep)
+ local relativetable = split(relativepath,sep)
+ for i, path in ipairs(relativetable) do
+ if path == ".." then
+ table.remove(absolutetable, table.getn(absolutetable))
+ elseif path ~= "." then
+ table.insert(absolutetable, path)
+ end
+ end
+ return sep .. table.concat(absolutetable, sep)
+end
+
+
+-- convert absolute normalized path file to uri
+local to_file_uri
+if platform == "unix" then
+ to_file_uri = function (path) return url.build{scheme="file",authority="", path=path} end
+else
+ to_file_uri = function (path) return url.build{scheme="file",authority="", path="/"..path} end
+end
+
+-- convert parsed URL table to file path for the current OS (see url.parse from luasocket)
+local to_path
+if platform == "unix" then
+ to_path = function (url) return url.path end
+else
+ to_path = function (url) return url.path:gsub("^/", "") end
+end
+-- "END PLATFORM DEPENDENT CODE"
+
+
+-- 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
+
+-- will contain the session object, and eventually 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
+local active_coroutines = { n = 0, from_id = setmetatable({ }, { __mode = "v" }), from_coro = setmetatable({ }, { __mode = "k" }) }
+
+-------------------------------------------------------------------------------
+-- Debugger features & configuration
+-------------------------------------------------------------------------------
+
+local constant_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",
+}
+
+local variable_features = setmetatable({ }, {
+ -- functions that format/validate data (defaults to tostring)
+ validators = {
+ multiple_sessions = tonumber,
+ max_children = tonumber,
+ max_data = tonumber,
+ max_depth = tonumber,
+ show_hidden = tonumber,
+ },
+ __index = {
+ multiple_sessions = 0,
+ encoding ="UTF-8",
+ max_children = 32,
+ max_data = 0xFFFF,
+ max_depth = 1,
+ show_hidden = 1,
+ uri = "file"
+ },
+ __newindex = function(self, k, v)
+ local mt = getmetatable(self)
+ local values, validators = mt.__index, mt.validators
+ if values[k] == nil then error("No such feature " .. tostring(k)) end
+ v = (validators[k] or tostring)(v)
+ values[k] = v
+ end,
+})
+
+-------------------------------------------------------------------------------
+-- Utility functions
+-------------------------------------------------------------------------------
+
+--- Encodes a string into Base64 with line wrapping
+-- @param data (string) data to encode
+-- @return base64 encoded string
+local function b64(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
+local function rawb64(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
+local function unb64(data)
+ return (mime.unb64(data)) -- first result of the low-level function is fine here
+end
+
+--- Parses the DBGp command arguments and returns it as a Lua table with key/value pairs.
+-- For example, the sequence <code>-i 5 -j foo</code> will result in <code>{i=5, j=foo}</code>
+-- @param cmd_args (string) sequence of arguments
+-- @return table described above
+local function 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)
+local function 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*(.*)$")
+ else
+ cmd_name, args, data = cmd:match("^(%S+)%s+(.*)$")
+ end
+ return cmd_name, arg_parse(args), unb64(data)
+end
+
+--- Returns the packet read from socket, or nil followed by an error message on errors.
+local function read_packet(skt)
+ local size = {}
+ while true do
+ local byte, err = blockingtcp.receive(skt, 1)
+ if not byte then return nil, err end
+ if byte == "\000" then break end
+ size[#size+1] = byte
+ end
+ return table.concat(size)
+end
+
+-- get uri from source debug info
+local get_uri
+local get_path
+do
+ local cache = { }
+ --- 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 = normalize(sourcepath)
+ if not is_path_absolute(normalizedpath) then
+ normalizedpath = merge_paths(base_dir, normalizedpath)
+ end
+ return to_file_uri(normalizedpath)
+ else -- dynamic code, stripped bytecode, tail return, ...
+ return false
+ end
+ end
+
+ local function get_module_uri (source)
+ local uri
+ if source:sub(1,1) == "@" then -- real source file
+ local sourcepath = source:sub(2)
+ local normalizedpath = normalize(sourcepath)
+ local normalizedluapath = normalize(package.path)
+ local luapathtable = split (normalizedluapath,";")
+ -- 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
+ local escaped = string.gsub(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
+ if uri then return "module:///"..uri end
+ end
+ return false
+ end
+
+ get_uri = function (source)
+ -- search in cache
+ local uri = cache[source]
+ if uri ~= nil then return uri end
+
+ -- not found, create uri
+ if variable_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
+
+ cache[source] = uri
+ return uri
+ end
+
+ -- get path file from uri
+ get_path= function (uri)
+ local parsed_path = assert(url.parse(uri))
+ if parsed_path.scheme == "file" then
+ return to_path(parsed_path)
+ else
+ -- search in cache
+ -- we should surely calculate it instead of find in cache
+ for k,v in pairs(cache)do
+ if v == uri then
+ assert(k:sub(1,1) == "@")
+ return k:sub(2)
+ end
+ end
+ end
+ end
+end
+
+-- Used to store complex keys (other than string and number) as they cannot be passed in text
+local key_cache = setmetatable({ n=0 }, { __mode = "v" })
+
+local function generate_key(name)
+ if type(name) == "string" then return string.format("%q", name)
+ elseif type(name) == "number" or type(name) == "boolean" then return tostring(name)
+ else -- complex key, use key_cache for lookup
+ local i = key_cache.n
+ key_cache[i] = name
+ key_cache.n = i+1
+ return "key_cache["..tostring(i).."]"
+ end
+end
+
+local function generate_printable_key(name)
+ return "[" .. (type(name) == "string" and string.format("%q", name) or tostring(name)) .. "]"
+end
+
+local 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 attrs extra attributes to add to the response tag (optional)
+local function dbgp_error(code, message, attrs)
+ error(setmetatable({ code = code, message = message, attrs = attrs or {} }, DBGP_ERR_METATABLE), 2)
+end
+
+local function dbgp_assert(code, success, ...)
+ if not success then dbgp_error(code, (...)) end
+ return success, ...
+end
+
+--- 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
+
+local Multival = { __tostring = function() return "" end } -- used as metatable to identify tables that are actually multival
+--- Packs a pcall results into a table with a correct nil handling
+-- @param ... what pcall returned
+-- @return (boolean) success state
+-- @return (Multival or any) In case of success, table containing results and the "n" field for size (trailing nil handling)
+-- and having Multival as metatable. In case of failure, a the error object
+local function packpcall(...)
+ local success = (...)
+ local results = success and setmetatable({ n = select("#", ...) - 1, select(2, ...) }, Multival) or (select(2, ...))
+ return success, results
+end
+
+-------------------------------------------------------------------------------
+-- XML Elements generators
+-------------------------------------------------------------------------------
+
+local xmlattr_specialchars = { ['"'] = "&quot;", ["<"] = "&lt;", ["&"] = "&amp;" }
+--- Very basic XML generator
+-- Takes a table describing the document as argument. This table has the following structure:
+-- * name: (string) tag name
+-- * attrs: (table, optional) tag attribute (key/value table)
+-- * children: (table, optional) child nodes, either sub-elements or string (text data) (sequence)
+local function generateXML(xml)
+ local pieces = { } -- string buffer
+
+ local function generate(node)
+ pieces[#pieces + 1] = "<"..node.name
+ pieces[#pieces + 1] = " "
+ for attr, val in pairs(node.attrs or {}) do
+ pieces[#pieces + 1] = attr .. '="' .. tostring(val):gsub('["&<]', xmlattr_specialchars) .. '"'
+ pieces[#pieces + 1] = " "
+ end
+ pieces[#pieces] = nil -- remove the last separator (useless)
+
+ if node.children then
+ pieces[#pieces + 1] = ">"
+ for _, child in ipairs(node.children) do
+ if type(child) == "table" then generate(child)
+ else pieces[#pieces + 1] = "<![CDATA[" .. tostring(child) .. "]]>" end
+ end
+ pieces[#pieces + 1] = "</" .. node.name .. ">"
+ else
+ pieces[#pieces + 1] = "/>"
+ end
+ end
+
+ generate(xml)
+ return table.concat(pieces)
+end
+
+local function send_xml(skt, resp)
+ if not resp.attrs then resp.attrs = {} end
+ resp.attrs.xmlns = "urn:debugger_protocol_v1"
+
+ local data = '<?xml version="1.0" encoding="UTF-8" ?>\n'..generateXML(resp)
+ log("DEBUGGER", "DEBUG", "Send " .. data)
+ blockingtcp.send(skt, 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
+local function make_error(code, msg)
+ local elem = { name = "error", attrs = { code = code } }
+ if msg then
+ elem.children = { { name = "message", children = { tostring(msg) } } }
+ end
+ return elem
+end
+
+local debugintrospection = require"debugintrospection"
+-- Provides some more informations about Lua functions
+local function new_function_introspection(...)
+ local result = debugintrospection["function"](...)
+ if result and result.file then
+ result.repr = result.repr .. "\n" .. get_uri("@" .. result.file) .. "\n" .. tostring(result.line_from)
+ result.type = "function (Lua)"
+ end
+ return result
+end
+
+
+--- Makes a property form a name/value pair (and fullname), see DBGp spec 7.11 for details
+-- It has a pretty basic handling of complex types (function, table, userdata), relying to Lua Inspector for advanced stuff.
+-- @param context (number) context ID in which this value resides (workaround bug 352316)
+-- @param value (any) the value to debug
+-- @param name (any) the name associated with value, passed through tostring
+-- @param fullname (string) a Lua expression to eval to get that property again (if nil, computed automatically)
+-- @param depth (number) the maximum property depth (recursive calls)
+-- @param pagesize (number) maximum children to include
+-- @param page (number) the page to generate (0 based)
+-- @param size_limit (number, optional) if set, the maximum size of the string representation (in bytes)
+-- @param safe_name (boolean) if true, does not encode the name as table key
+--TODO BUG ECLIPSE TOOLSLINUX-99 352316 : as a workaround, context is encoded into the fullname property
+local function make_property(context, value, name, fullname, depth, pagesize, page, size_limit, safe_name)
+ local dump = debugintrospection:new(false, false, true, false, true, true)
+ dump["function"] = new_function_introspection -- override standard definition
+
+ -- build XML
+ local function build_xml(node, name, fullname, page, depth)
+ local data = tostring(node.repr)
+
+ local specials = { }
+ if node.metatable then specials[#specials + 1] = "metatable" end
+ if node.environment then specials[#specials + 1] = "environment" end
+
+ local numchildren = #node + #specials
+ local attrs = { type = node.array and "sequence" or node.type, name=name, fullname=rawb64(tostring(context).."|"..fullname),
+ encoding="base64", children = 0, size=#data }
+ if numchildren > 0 then
+ attrs.children = 1
+ attrs.numchildren = numchildren
+ attrs.pagesize = pagesize
+ attrs.page = page
+ end
+ local children = { b64(size_limit and data:sub(1, size_limit) or data) }
+
+ if depth > 0 then
+ local from, to = page * pagesize + 1, (page + 1) * (pagesize)
+ for i = from, math.min(#node, to) do
+ local key, value = unpack(node[i])
+ key = type(key) == "number" and dump.dump[key] or key
+ value = type(value) == "number" and dump.dump[value] or value
+ children[#children + 1] = build_xml(value, "[" .. key.repr .. "]", fullname .. "[" .. generate_key(key.ref) .. "]", 0, depth - 1)
+ end
+ for i = #node + 1, math.min(to, numchildren) do
+ local special = specials[i - #node]
+ local prop = build_xml(dump.dump[node[special]], special, special .. "[" .. fullname .. "]", 0, depth - 1)
+ prop.attrs.type = "special"
+ children[#children + 1] = prop
+ end
+ end
+
+ return { name = "property", attrs = attrs, children = children }
+ end
+
+ fullname = fullname or ("(...)[" .. generate_key(name) .. "]")
+ if not safe_name then name = generate_printable_key(name) end
+
+ if getmetatable(value) == Multival then
+ local children = { }
+ for i=1, value.n do
+ local val = dump[type(value[i])](dump, value[i], depth)
+ val = type(val) == "number" and dump.dump[val] or val
+ -- Since fullname is impossible to build for multivals and they are read only,
+ -- generate_key is used to retireve reference to the object
+ children[#children + 1] = build_xml(val, "["..i.."]", generate_key(val.ref), 0, depth - 1)
+ end
+
+ -- return just the value in case of single result
+ if #children == 1 then
+ return children[1]
+ end
+
+ -- when there are multiple results, they a wrapped into a multival
+ return { attrs = { type="multival", name=name, fullname=tostring(context).."|"..fullname, encoding="base64",
+ numchildren=value.n, children=value.n > 0 and 1 or 0, size=0, pagesize=pagesize },
+ children = children,
+ name = "property" }
+ else
+ local root = dump[type(value)](dump, value, depth + 1)
+ return build_xml(type(root) == "number" and dump.dump[root] or root, name, fullname, page, depth)
+ end
+end
+
+
+-------------------------------------------------------------------------------
+-- Output redirection handling
+-------------------------------------------------------------------------------
+-- Override standard output functions & constants to redirect data written to these files to IDE too.
+-- This works only for output done in Lua, output written by C extensions is still go to system output file.
+
+-- references to native values
+io.base = { output = io.output, stdin = io.stdin, stdout = io.stdout, stderr = io.stderr }
+
+function print(...)
+ local buf = {...}
+ for i=1, select("#", ...) do
+ buf[i] = tostring(buf[i])
+ end
+ io.stdout:write(table.concat(buf, "\t") .. "\n")
+end
+
+-- Actually change standard output file but still return the "fake" stdout
+function io.output(output)
+ output = io.base.output(output)
+ return io.stdout
+end
+
+local dummy = function() end
+
+-- metatable for redirecting output (not printed at all in actual output)
+local 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")
+ send_xml(self.skt, { name="stream", attrs = { type=self.mode }, children = { b64(buf) } } )
+ end,
+ flush = dummy,
+ close = dummy,
+ setvbuf = dummy,
+ seek = dummy
+}
+redirect_output.__index = redirect_output
+
+-- metatable for cloning output (outputs to actual system and send to IDE)
+local copy_output = {
+ write = function(self, ...)
+ 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,
+}
+copy_output.__index = copy_output
+
+-- 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 copy_output or redirect_output)
+ end
+ send_xml(self.skt, { name = "response", attrs = { command = mode, transaction_id = args.i, success = "1" } } )
+ end
+end
+
+
+-------------------------------------------------------------------------------
+-- Context handling
+-------------------------------------------------------------------------------
+local Context
+do
+ -- make unique object to access contexts
+ local LOCAL, UPVAL, GLOBAL, STORE, HANDLE = {}, {}, {}, {}, {}
+
+ -- create debug functions depending on coroutine context
+ local foreign_coro_debug = { -- bare Lua functions are fine here because all parameters will be provided
+ getlocal = debug.getlocal,
+ setlocal = debug.setlocal,
+ getinfo = debug.getinfo,
+ }
+
+ local current_coro_debug = {
+ getlocal = function(_, level, index) return debug.getlocal(get_script_level(level), index) end,
+ setlocal = function(_, level, index, value) return debug.setlocal(get_script_level(level), index, value) end,
+ getinfo = function(_, level, what) return debug.getinfo(get_script_level(level), what) 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 !
+ Context = {
+ -- Context identifiers can be accessed by their DBGp context ID
+ [0] = LOCAL,
+ [1] = GLOBAL, -- DLTK internal ID for globals is 1
+ [2] = UPVAL,
+
+ -- 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,
+
+ LocalContext = {
+ __index = function(self, k)
+ local index = self[STORE][k]
+ if not index then error("The local "..tostring(k).." does not exitsts.") end
+ local handle = self[HANDLE]
+ return select(2, handle.callbacks.getlocal(handle.coro, handle.level, index))
+ end,
+ __newindex = function(self, k, v)
+ local index = self[STORE][k]
+ if index then
+ local handle = self[HANDLE]
+ handle.callbacks.setlocal(handle.coro, 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 (coroutine or nil) coroutine to map to (or nil for current coroutine)
+ -- @param level (number) stack level do dump (script stack level)
+ new = function(cls, coro, level)
+ local locals, upvalues = {}, {}
+ local debugcallbacks = coro and foreign_coro_debug or current_coro_debug
+ local func = (debugcallbacks.getinfo(coro, level, "f") or dbgp_error(301, "No such stack level: "..tostring(level))).func
+
+ -- local variables
+ for i=1, math.huge do
+ local name, val = debugcallbacks.getlocal(coro, 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] = { callbacks = debugcallbacks, level = level, coro = coro } }, cls.LocalContext)
+ upvalues = setmetatable({ [STORE] = upvalues, [HANDLE] = func }, cls.UpvalContext)
+ return setmetatable({ [LOCAL] = locals, [UPVAL] = upvalues, [GLOBAL] = debug.getfenv(func) }, cls)
+ end,
+ }
+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.
+local function ContextManager()
+ local cache = { }
+ return function(thread, level)
+ local thread_contexts = cache[thread or true] -- true is used to identify current thread (as nil is not a valid table key)
+ if not thread_contexts then
+ thread_contexts = { }
+ cache[thread or true] = thread_contexts
+ end
+
+ local context = thread_contexts[level]
+ if not context then
+ context = Context:new(thread, level)
+ thread_contexts[level] = context
+ end
+
+ return context
+ end
+end
+
+-------------------------------------------------------------------------------
+-- Property_* environment handling
+-------------------------------------------------------------------------------
+-- 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 = key_cache,
+ metatable = setmetatable({ }, {
+ __index = function(self, tbl) return getmetatable(tbl) end,
+ __newindex = function(self, tbl, mt) return setmetatable(tbl, mt) end,
+ }),
+ environment = setmetatable({ }, {
+ __index = function(self, func) return getfenv(func) end,
+ __newindex = function(self, func, env) return setfenv(func, env) end,
+ }),
+}
+-- to allows to be set as metatable
+property_evaluation_environment.__index = property_evaluation_environment
+
+-------------------------------------------------------------------------------
+-- 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.
+local 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
+local 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 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=normalize(uri.path)}
+
+ local filereg = file_mapping[bp.filename]
+ if not filereg then
+ filereg = { }
+ file_mapping[bp.filename] = filereg
+ end
+
+ local linereg = filereg[bp.lineno]
+ if not linereg then
+ linereg = {}
+ filereg[bp.lineno] = linereg
+ end
+
+ table.insert(linereg, bp)
+
+ id_mapping[bpid] = bp
+ return bpid
+ end
+
+ --- If breakpoint(s) exists for given file/line, uptates breakpoint counters
+ -- and returns whether a breakpoint has matched (boolean)
+ function breakpoints.at(file, line)
+ local bps = file_mapping[file] and file_mapping[file][line]
+ if not bps then return nil end
+
+ local do_break = false
+ for _, bp in pairs(bps) do
+ if bp.state == "enabled" then
+ local match = true
+ if bp.condition then
+ -- TODO: this is not the optimal solution because Context can be instantiated twice if the breakpoint matches
+ local success, result = pcall(setfenv(bp.condition, Context:new(nil, 0)))
+ if not success then log("DEBUGGER", "ERROR", "Condition evaluation failed for breakpoint at %s:%d", file, line) 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 breakpoints.hit_conditions[bp.hit_condition](bp.hit_count, bp.hit_value) then
+ if bp.temporary then
+ 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 breakpoints.get(id)
+ if id then return id_mapping[id]
+ else return id_mapping end
+ end
+
+ function breakpoints.remove(id)
+ local bp = id_mapping[id]
+ if bp then
+ id_mapping[id] = nil
+ local linereg = file_mapping[bp.filename][bp.lineno]
+ for i=1, #linereg do
+ if linereg[i] == bp then
+ table.remove(linereg, i)
+ break
+ end
+ end
+
+ -- cleanup file_mapping
+ if not next(linereg) then file_mapping[bp.filename][bp.lineno] = nil end
+ if not next(file_mapping[bp.filename]) then file_mapping[bp.filename] = nil end
+ return true
+ end
+ return false
+ end
+
+ --- Returns an XML data structure that describes given breakpoint
+ -- @param id (number) breakpoint ID
+ -- @return Table describing a <breakpooint> tag or nil followed by an error message
+ function breakpoints.get_xml(id)
+ local bp = id_mapping[id]
+ if not bp then return nil, "No such breakpoint: "..tostring(id) end
+
+ local response = { name = "breakpoint", attrs = { } }
+ for k,v in pairs(bp) do response.attrs[k] = v end
+ if bp.expression then
+ response.children = { { name = "expression", children = { bp.expression } } }
+ end
+
+ -- internal use only
+ response.attrs.expression = nil
+ response.attrs.condition = nil
+ response.attrs.temporary = nil -- TODO: the specification is not clear whether thsi 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 events.register(event)
+ local thread = coroutine.running() or "main"
+ log("DEBUGGER", "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 events.does_match()
+ if step_into then return true end
+
+ local thread = coroutine.running() or "main"
+ 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("DEBUGGER", "DEBUG", "Event %s matched!", event_type)
+ return true
+ end
+ end
+ return false
+ end
+
+ --- Discards event for current thread (if any)
+ function events.discard()
+ waiting_sessions[coroutine.running() or "main"] = nil
+ step_into = nil
+ end
+end
+
+-------------------------------------------------------------------------------
+-- Debugger main loop
+-------------------------------------------------------------------------------
+
+--- Send the XML response to the previous continuation command and clear the previous context
+local function previous_context_response(self, reason)
+ self.previous_context.status = self.state
+ self.previous_context.reason = reason or "ok"
+ send_xml(self.skt, { name="response", attrs = self.previous_context } )
+ self.previous_context = nil
+end
+
+--- Gets the coroutine behind an id
+-- Throws errors on unknown identifiers
+-- @param coro_id (string or nil) Coroutine identifier or nil
+-- @return Coroutine instance or nil (if coro_id was nil or if coroutine is the current coroutine)
+local function get_coroutine(coro_id)
+ if coro_id then
+ local coro = dbgp_assert(399, active_coroutines.from_id[tonumber(coro_id)], "No such coroutine")
+ dbgp_assert(399, coroutine.status(coro) ~= "dead", "Coroutine is dead")
+ return coro ~= corunning() and coro or nil
+ end
+ return nil
+end
+
+-- 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 commands
+commands = {
+ ["break"] = function(self, args)
+ self.state = "break"
+ -- send response to previous command
+ previous_context_response(self)
+ -- and then response to break command itself
+ send_xml(self.skt, { name="response", attrs = { command = "break", transaction_id = args.i, success = 1 } } )
+ return false
+ end,
+
+ status = function(self, args)
+ send_xml(self.skt, { name="response", attrs = {
+ command = "status",
+ reason = "ok",
+ status = self.state,
+ transaction_id = args.i } } )
+ end,
+
+ stop = function(self, args)
+ send_xml(self.skt, { name="response", attrs = {
+ command = "stop",
+ reason = "ok",
+ status = "stopped",
+ transaction_id = args.i } } )
+ self.skt:close()
+ os.exit(1)
+ end,
+
+ feature_get = function(self, args)
+ local name = args.n
+ local response = constant_features[name] or variable_features[name] or (not not commands[name])
+ send_xml(self.skt, { name="response", attrs = {
+ command = "feature_get",
+ feature_name = name,
+ supported = response and "1" or "0",
+ transaction_id = args.i },
+ children = { tostring(response) } } )
+ end,
+
+ feature_set = function(self, args)
+ local name, value = args.n, args.v
+ local success = 0
+ if variable_features[name] then
+ variable_features[name] = value
+ success = 1
+ end
+ send_xml(self.skt, { name="response", attrs = {
+ command = "feature_set",
+ feature = name,
+ success = success,
+ transaction_id = args.i
+ } } )
+ end,
+
+ typemap_get = function(self, args)
+ local function gentype(name, type, xsdtype)
+ return { name = "map", attrs = { name = name, type = type, ["xsi:type"] = xsdtype } }
+ end
+
+ send_xml(self.skt, { name="response", attrs = {
+ command = "typemap_get",
+ transaction_id = args.i,
+ ["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance",
+ ["xmlns:xsd"] = "http://www.w3.org/2001/XMLSchema",
+ }, children = {
+ 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,
+
+ run = function(self) return true end,
+
+ step_over = function(self)
+ events.register("over")
+ return true
+ end,
+
+ step_out = function(self)
+ events.register("out")
+ return true
+ end,
+
+ step_into = function(self)
+ events.register("into")
+ return true
+ end,
+
+ eval = function(self, args, data)
+ log("DEBUGGER", "DEBUG", "Going to eval "..data)
+ local result, err, success
+ -- first, try to load as expression
+ local func, err = loadstring("return "..data)
+
+ -- if it is not a, expression, try as statement (assignment, ...)
+ if not func then
+ func, err = loadstring(data)
+ end
+
+ if func then
+ -- DBGp does not support stack level here, see http://bugs.activestate.com/show_bug.cgi?id=81178
+ setfenv(func, self.stack(nil, 0))
+ success, result = packpcall(pcall(func))
+ if not success then err = result end
+ end
+
+ local response = { name = "response", attrs = { command = "eval", transaction_id = args.i } }
+ if not err then
+ response.attrs.success = 1
+ -- 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.children = { make_property(0, result, data, "", 1, 8000, 0, nil) }
+ else
+ response.attrs.success = 0
+ response.children = { make_error(206, err) }
+ end
+ send_xml(self.skt, response)
+ end,
+
+ stdout = output_command_handler_factory("stdout"),
+ stderr = output_command_handler_factory("stderr"),
+
+ breakpoint_set = function(self, args, data)
+ if args.o and not 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 = breakpoints.insert(bp)
+ send_xml(self.skt, { name = "response", attrs = { command = "breakpoint_set", transaction_id = args.i, state = bp.state, id = bpid } } )
+ end,
+
+ breakpoint_get = function(self, args)
+ send_xml(self.skt, { name = "response",
+ attrs = { command = "breakpoint_get", transaction_id = args.i },
+ children = { dbgp_assert(205, breakpoints.get_xml(tonumber(args.d))) } })
+ end,
+
+ breakpoint_list = function(self, args)
+ local bps = { }
+ for id, bp in pairs(breakpoints.get()) do bps[#bps + 1] = breakpoints.get_xml(id) end
+ send_xml(self.skt, { name = "response",
+ attrs = { command = "breakpoint_list", transaction_id = args.i },
+ children = bps })
+ end,
+
+ breakpoint_update = function(self, args)
+ local bp = breakpoints.get(tonumber(args.d))
+ if not bp then dbgp_error(205, "No such breakpint "..args.d) end
+ if args.o and not breakpoints.hit_conditions[args.o] then dbgp_error(200, "Invalid hit_condition operator: "..args.o) end
+
+ local response = { name = "response", attrs = { 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
+ send_xml(self.skt, response)
+ end,
+
+ breakpoint_remove = function(self, args)
+ local response = { name = "response", attrs = { command = "breakpoint_remove", transaction_id = args.i } }
+ if not breakpoints.remove(tonumber(args.d)) then dbgp_error(205, "No such breakpint "..args.d) end
+ send_xml(self.skt, response)
+ end,
+
+ stack_depth = function(self, args)
+ local depth = 0
+ local coro = get_coroutine(args.o)
+ for level = coro and 0 or get_script_level(0), math.huge do
+ local info = getinfo(coro, level, "S")
+ if not info then break end -- end of stack
+ depth = depth + 1
+ if info.what == "main" then break end -- levels below main chunk are not interesting
+ end
+ send_xml(self.skt, { name = "response", attrs = { command = "stack_depth", transaction_id = args.i, depth = depth} } )
+ end,
+
+ stack_get = function(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 attrs = { level = level, where = info.name, type="file" }
+ local uri = get_uri(info.source)
+ if uri and info.currentline then -- reachable level
+ attrs.filename = uri
+ attrs.lineno = info.currentline
+ else
+ attrs.filename = what2uri[info.what] or "unknown:/"
+ attrs.lineno = -1
+ end
+ return { name = "stack", attrs = attrs }
+ end
+
+ local children = { }
+ local coro = get_coroutine(args.o)
+ local level = coro and 0 or get_script_level(0)
+
+ if args.d then
+ local stack_level = tonumber(args.d)
+ children[#children+1] = make_level(getinfo(coro, stack_level + level, "nSl"), stack_level)
+ else
+ for i=level, math.huge do
+ local info = getinfo(coro, i, "nSl")
+ if not info then break end
+ children[#children+1] = make_level(info, i-level)
+ if info.what == "main" then break end -- levels below main chunk are not interesting
+ end
+ end
+
+ send_xml(self.skt, { name = "response", attrs = { command = "stack_get", transaction_id = args.i}, children = children } )
+ end,
+
+ --- Lists all active coroutines.
+ -- Returns a list of active coroutines with their id (an arbitrary string) to query stack and properties. The id is
+ -- guaranteed to be unique and stable for all coroutine life (they can be reused as long as coroutine exists).
+ -- Others commands such as stack_get or property_* commands takes an additional -o switch to query a particular cOroutine.
+ -- If the switch is not given, running coroutine will be used.
+ -- In case of error on coroutines (most likely coroutine not found or dead), an error 399 is thrown.
+ -- Note there is an important limitation due to Lua 5.1 coroutine implementation: you cannot query main "coroutine" from
+ -- another one, so main coroutine is not in returned list (this will change with Lua 5.2).
+ --
+ -- This is a non-standard command. The returned XML has the following strucuture:
+ -- <response command="coroutine_list" transaction_id="0">
+ -- <coroutine name="<some printtable name>" id="<coroutine id>" running="0|1" />
+ -- ...
+ -- </response>
+ coroutine_list = function(self, args)
+ local running = coroutine.running()
+ local coroutines = { }
+ -- as any operation on main coroutine will fail, it is not yet listed
+ -- coroutines[1] = { name = "coroutine", attrs = { id = 0, name = "main", running = (running == nil) and "1" or "0" } }
+ for id, coro in pairs(active_coroutines.from_id) do
+ if id ~= "n" then
+ coroutines[#coroutines + 1] = { name = "coroutine", attrs = { id = id, name = tostring(coro), running = (coro == running) and "1" or "0" } }
+ end
+ end
+ send_xml(self.skt, { name = "response", attrs = { command = "coroutine_list", transaction_id = args.i}, children = coroutines } )
+ end,
+
+ context_names = function(self, args)
+ local coro = get_coroutine(args.o)
+ local level = tonumber(args.d or 0)
+ local info = getinfo(coro, coro and level or get_script_level(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 = {
+ { name = "context", attrs = { name = "Local", id = 0 } },
+ { name = "context", attrs = { name = "Upvalue", id = 2 } },
+ { name = "context", attrs = { name = "Global", id = 1 } },
+ }
+
+ send_xml(self.skt, { name = "response", attrs = { command = "context_names", transaction_id = args.i}, children = contexts } )
+ end,
+
+ context_get = function(self, args)
+ local context = tonumber(args.c or 0)
+ local cxt_id = Context[context] or dbgp_error(302, "No such context: "..tostring(args.c))
+ local level = tonumber(args.d or 0)
+ local coro = get_coroutine(args.o)
+ local cxt = self.stack(coro, level)
+
+ local properties = { }
+ -- iteration over global is different (this could be unified in Lua 5.2 thanks to __pairs metamethod)
+ for name, val in (context == 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] = make_property(context, val, name, nil, 0, variable_features.max_children, 0,
+ variable_features.max_data, context ~= 1)
+ end
+
+ send_xml(self.skt, { name = "response",
+ attrs = { command = "context_get", transaction_id = args.i, context = context},
+ children = properties } )
+ end,
+
+ property_get = function(self, args)
+ --TODO BUG ECLIPSE TOOLSLINUX-99 352316
+ local context, name = assert(unb64(args.n):match("^(%d+)|(.*)$"))
+ context = tonumber(args.c or context)
+ local cxt_id = Context[context] or dbgp_error(302, "No such context: "..tostring(args.c))
+ local level = tonumber(args.d or 0)
+ local coro = get_coroutine(args.o)
+ local size = tonumber(args.m or variable_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, loadstring("return "..name))
+ setfenv(chunk, property_evaluation_environment)
+ local prop = select(2, dbgp_assert(300, pcall(chunk, cxt[cxt_id])))
+ local response = make_property(context, prop, name, name, variable_features.max_depth, variable_features.max_children, page, size)
+ -- make_property is not able to flag special variables as such when they are at root of property
+ -- special variables queries are in the form "<proxy name>[(...)[a][b]<...>]"
+ -- TODO: such parsing is far from perfect
+ if name:match("^[%w_]+%[.-%b[]%]$") == name then response.attrs.type = "special" end
+ send_xml(self.skt, { name = "response",
+ attrs = { command = "property_get", transaction_id = args.i, context = context},
+ children = { response } } )
+ end,
+
+ property_value = function(self, args)
+ args.m = -1
+ commands.property_get(self, args)
+ end,
+
+ property_set = function(self, args, data)
+ local context, name = assert(unb64(args.n):match("^(%d+)|(.*)$"))
+ context = tonumber(args.c or context)
+ local cxt_id = Context[context] or dbgp_error(302, "No such context: "..tostring(args.c))
+ local level = tonumber(args.d or 0)
+ local coro = get_coroutine(args.o)
+ local cxt = self.stack(coro, level)
+
+ -- evaluate the new value in the local context
+ local value = select(2, dbgp_assert(206, pcall(setfenv(dbgp_assert(206, loadstring("return "..data)), cxt))))
+
+ local chunk = dbgp_assert(206, loadstring(name .. " = value"))
+ setfenv(chunk, setmetatable({ value = value }, property_evaluation_environment))
+ dbgp_assert(206, pcall(chunk, cxt[cxt_id]))
+ send_xml(self.skt, { name = "response", attrs = { 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
+ source = function(self, args)
+ local path
+ if args.f then
+ path = get_path(args.f)
+ else
+ path = debug.getinfo(get_script_level(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 source = cowrap(function()
+ 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
+ coyield(line.."\n")
+ elseif currentline >= endline then break end
+ end
+ file:close()
+ end)
+ -- the DBGp specification does not require any specific EOL marker, but use normalize can be used here
+ local filter = ltn12.filter.chain(mime.encode("base64"), mime.wrap("base64"))
+ local output = { }
+ local sink = ltn12.sink.chain(filter, ltn12.sink.table(output))
+ assert(ltn12.pump.all(source, sink))
+
+ send_xml(self.skt, { name = "response",
+ attrs = { command = "source", transaction_id = args.i, success = 1},
+ children = { table.concat(output) } })
+ 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)
+ blockingtcp.settimeout(self.skt, 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"
+ previous_context_response(self)
+ end
+ self.stack = ContextManager() -- will be used to mutualize context allocation for each loop
+
+ while true do
+ -- reads packet
+ local packet = async_packet or assert(read_packet(self.skt))
+ async_packet = nil
+ log("DEBUGGER", "DEBUG", packet)
+ local cmd, args, data = 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 = pcall(func, self, args, data)
+ if not ok then -- internal exception
+ local code, msg, attrs
+ if type(cont) == "table" and getmetatable(cont) == DBGP_ERR_METATABLE then
+ code, msg, attrs = cont.code, cont.message, cont.attrs
+ else
+ code, msg, attrs = 998, tostring(cont), { }
+ end
+ log("DEBUGGER", "ERROR", "Command %s caused: (%d) %s", cmd, code, msg)
+ attrs.command, attrs.transaction_id = cmd, args.i
+ send_xml(self.skt, { name="response", attrs = attrs, children = { 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("DEBUGGER", "WARNING", "Got unknown command: "..cmd)
+ send_xml(self.skt, { name="response", attrs = { command = cmd, transaction_id = args.i, },
+ children = { make_error(4) } } )
+ end
+ end
+
+ self.stack = nil -- free allocated contexts
+ self.state = "running"
+ blockingtcp.settimeout(self.skt, 0) -- reset socket to async
+end
+
+local function debugger_hook(event, line)
+ local thread = corunning() or "main"
+
+ if event == "call" then
+ stack_levels[thread] = stack_levels[thread] + 1
+ elseif event == "return" or event == "tail return" then
+ stack_levels[thread] = stack_levels[thread] - 1
+ else -- line event: check for breakpoint
+ local do_break, packet = nil, nil
+ local info = debug_getinfo(2, "S")
+ local uri = get_uri(info.source)
+ if uri and uri ~= debugger_uri then -- the debugger does not break if the source is not known
+ do_break = breakpoints.at(uri, line) or events.does_match()
+ if do_break then
+ events.discard()
+ end
+
+ -- check for async commands
+ if not do_break then
+ packet = 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("DEBUGGER", "ERROR", "Error while debug loop: "..err) end
+ end
+ end
+end
+
+local function init(host, port,idekey)
+ host = host or os.getenv "DBGP_IDEHOST" or "127.0.0.1"
+ port = port or os.getenv "DBGP_IDEPORT" or "10000"
+ idekey = idekey or os.getenv("DBGP_IDEKEY") or "luaidekey"
+
+ local skt = assert(socket.tcp())
+ blockingtcp.settimeout(skt, nil)
+
+ -- try to connect several times: if IDE launches both process and server at same time, first connect attempts may fail
+ local ok, err
+ for i=1, 5 do
+ ok, err = blockingtcp.connect(skt, host, port)
+ if ok then break end
+ blockingtcp.sleep(0.5)
+ end
+ if err then error(string.format("Cannot connect to %s:%d : %s", host, port, err)) end
+
+ -- get the debugger URI
+ debugger_uri = get_uri(debug.getinfo(1).source)
+
+ -- get the root script path (the highest possible stack index)
+ local source
+ for i=2, math.huge do
+ local info = debug.getinfo(i)
+ if not info then break end
+ source = 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 = coroutine.running() or "main"
+ stack_levels[thread] = 1 -- the return event will set the counter to 0
+ local sessionid = tostring(os.time()) .. "_" .. tostring(thread)
+
+ send_xml(skt, { name="init", attrs = {
+ appid = "Lua DBGp",
+ idekey = idekey,
+ session = sessionid,
+ thread = tostring(thread),
+ parent = "",
+ language = "Lua",
+ protocol_version = "1.0",
+ fileuri = source
+ } })
+
+ local sess = { skt = skt, state = "starting", id = sessionid }
+ 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 = active_coroutines.from_coro[coro]
+ active_coroutines.from_id[coro_id] = nil
+ 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
+ active_coroutines.n = active_coroutines.n + 1
+ active_coroutines.from_id[active_coroutines.n] = coro
+ active_coroutines.from_coro[coro] = 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
256 debugintrospection.lua
@@ -0,0 +1,256 @@
+-------------------------------------------------------------------------------
+-- Copyright (c) 2011 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
+-------------------------------------------------------------------------------
+
+-- This module defines a single "class" which allows to dump arbitrary values by gathering as munch data as
+-- possible on it for debugging purposes.
+--
+-- Recursions are avoided by "flatten" all tables in the structure and referencing them by their ID.
+-- The dump format is documented in Confluence Wiki:
+-- https://confluence.anyware-tech.com/display/ECLIPSE/Lua+IDE+Internship#LuaIDEInternship-Datadumpformat
+--
+-- It has two major parts :
+-- dump : Result data structure: keys are numbers which are IDs for all dumped tables.
+-- Values are table structures.
+-- tables : Table dictionary, used to register all dumped tables, keys are tables,
+-- values are their ID. Functions upvalues are also stored here in the same way.
+--
+-- The dump itself is done by type functions, one for each Lua type. For example a complete dump of the VM is done with:
+-- local introspection = require"debugintrospection"
+-- local dump = introspection:new()
+-- dump.dump.root = dump:table(_G) -- so dump.root will be a number which point to the result of _G introspection
+--
+-- WARNING: do never keep any direct reference to internal fields (dump or tables): a dump object is not re-dumped to
+-- avoid making huge data structures (and potentially overflow l2b)
+
+-- Utility function (from ReadyAgent utils, copied here to avoid this dependency for a single function)
+local function isregulararray(T)
+ local n = 1
+ for k, v in pairs(T) do
+ if rawget(T, n) == nil then return false end
+ n = n + 1
+ end
+ return true
+end
+
+local dump_pool = {}
+dump_pool.__index = dump_pool
+
+local all_dumps = setmetatable({ }, { __mode = "k" }) -- register all dumps to avoid to re-dump them (see above warning)
+
+--- Creates a new dump pool with specified options
+-- @param dump_locales (boolean) whether local values are dumped
+-- @param dump_upvalues (boolean) whether function upvalues are dumped
+-- @param dump_metatables (boolean) whether metatables (for tables and userdata) are dumped
+-- @param dump_stacks (boolean) whether thread stacks are dumped
+-- @param dump_fenv (boolean) whether function environments are dumped
+function dump_pool:new(dump_locales, dump_upvalues, dump_metatables, dump_stacks, dump_fenv, keep_reference)
+ local dump = setmetatable({
+ current_id = 1,
+ tables = { },
+ dump = { },
+ -- set switches, force a boolean value because nil would mess with __index metamethod
+ dump_locales = dump_locales and true or false,
+ dump_upvalues = dump_upvalues and true or false,
+ dump_metatables = dump_metatables and true or false,
+ dump_stacks = dump_stacks and true or false,
+ dump_fenv = dump_fenv and true or false,
+ keep_reference = keep_reference and true or false,
+ }, self)
+ all_dumps[dump] = true
+ return dump
+end
+
+function dump_pool:_next_id()
+ local id = self.current_id
+ self.current_id = id + 1
+ return id
+end
+
+function dump_pool:_register_new(value)
+ local id = self.current_id
+ self.current_id = id + 1
+ self.tables[value] = id
+ return id
+end
+
+--- Utility function to factorize all metatable handling
+function dump_pool:_metatable(value, result, depth)
+ --TODO: add support for __pairs and __ipairs ?
+ if self.dump_metatables then
+ local mt = getmetatable(value)
+ if mt then
+ result.metatable = self[type(mt)](self, mt, depth-1)
+ if mt.__len then result.length = #value end
+ end
+ end
+ return result
+end
+
+--- Adds a field into destination table, if both key and value has been successfully dumped
+function dump_pool:_field(dest, key, value, depth)
+ local dkey, dvalue = self[type(key)](self, key, depth-1), self[type(value)](self, value, depth-1)
+ if dkey and dvalue then dest[#dest + 1] = { dkey, dvalue } end
+end
+
+--- Functions used to extract debug informations from different data types.
+-- each function takes the value to debug as parameter and returns its
+-- debugging structure (or an id, for tables), modifying the pool if needed.
+
+function dump_pool:table(value, depth)
+ depth = depth or math.huge
+ if depth < 0 then return nil end
+
+ if all_dumps[value] then return nil end
+ local id = self.tables[value]
+ if not id then
+ -- this is a new table: register it
+ id = self:_register_new(value)
+ local t = { type = "table", repr = tostring(value), ref = self.keep_reference and value or nil }
+ --Detect "arrays" (tables with 1..n keys). Empty tables are really tables
+ t.array = (not not next(value)) and isregulararray(value)
+
+ -- For arrays, make sure that keys are given in 1..n order
+ for k,v in (t.array and ipairs or pairs)(value) do
+ self:_field(t, k, v, depth)
+ end
+
+ -- The registered length refers to # result because if actual element count
+ -- can be known with dumped values
+ t.length = #value
+ self:_metatable(value, t, depth)
+ self.dump[id] = t
+ end
+ return id
+end
+
+function dump_pool:userdata(value, depth)
+ depth = depth or math.huge
+ if depth < 0 then return nil end
+
+ return self:_metatable(value, { type = "userdata", repr = tostring(value), ref = self.keep_reference and value or nil }, depth)
+end
+
+function dump_pool:thread(value, depth)
+ depth = depth or math.huge
+ if depth < 0 then return nil end
+
+ local result = { type = "thread", repr = tostring(value), status = coroutine.status(value), ref = self.keep_reference and value or nil }
+ local stack = self.tables[value]
+ if self.dump_stacks and not stack then
+ stack = self:_register_new(value)
+ local stack_table = { type="special" }
+
+ for i=1, math.huge do
+ if not debug.getinfo(value, i, "f") then break end
+ -- _filed is not used here because i is not a function and there is no risk to get a nil from number or function
+ stack_table[#stack_table+1] = { self:number(i, depth - 1), self["function"](self, i, depth - 1, value) }
+ end
+
+ stack_table.repr = tostring(#stack_table).." levels"
+ self.dump[stack] = stack_table
+ end
+ result.stack = stack
+ return result
+end
+
+dump_pool["function"] = function(self, value, depth, thread) -- function is a keyword...
+ depth = depth or math.huge
+ if depth < 0 then return nil end
+
+ local info = thread and debug.getinfo(thread, value, "nSfl") or debug.getinfo(value, "nSfl")
+ local func = info.func -- in case of value is a stack index
+ local result = { type = "function", ref = self.keep_reference and func or nil }
+ result.kind = info.what
+
+ if info.name and #info.name > 0 then result.repr = "function: "..info.name -- put natural name, if available
+ elseif func then result.repr = tostring(func) -- raw tostring otherwise
+ else result.repr = "<tail call>" end -- nothing is available for tail calls
+
+ if not func then return result end -- there is no more info to gather for tail calls
+
+ if info.what ~= "C" then
+ --TODO: do something if function is not defined in a file
+ if info.source:sub(1,1) == "@" then
+ result.file = info.source:sub(2)
+ end
+ result.line_from = info.linedefined
+ result.line_to = info.lastlinedefined
+ if info.currentline >= 0 then
+ result.line_current = info.currentline
+ end
+ end
+
+ -- Dump function env (if different from _G)
+ local env = getfenv(func)
+ if self.dump_fenv and env ~= getfenv(0) then
+ result.environment = self:table(env, depth - 1)
+ end
+
+ -- Dump function upvalues (if any), trated as a table (recursion is handled in the same way)
+ local upvalues = self.tables[func]
+ if self.dump_upvalues and not upvalues and func and debug.getupvalue(func, 1) then
+ -- Register upvalues table into result
+ local ups_table = { type="special" }
+ upvalues = self:_register_new(func)
+
+ for i=1, math.huge do
+ local name, val = debug.getupvalue(func, i)
+ if not name then break end
+ self:_field(ups_table, name, val, depth)
+ end
+
+ ups_table.repr = tostring(#ups_table)
+ self.dump[upvalues] = ups_table
+ end
+ result.upvalues = upvalues
+
+ -- Dump function locales (only for running function, recursion not handled)
+ if self.dump_locales and type(value) == "number" then
+ local getlocal = thread and function(...) return debug.getlocal(thread, ...) end or debug.getlocal
+ if getlocal(value, 1) then
+ local locales = { type="special" }
+ local locales_id = self:_next_id()
+
+ for i=1, math.huge do
+ local name, val = getlocal(value, i)
+ if not name then break
+ elseif name:sub(1,1) ~= "(" and val ~= self then -- internal values are ignored
+ self:_field(locales, name, val, depth)
+ end
+ end
+
+ locales.repr = tostring(#locales)
+ self.dump[locales_id] = locales
+ result.locales = locales_id
+ end
+ end
+ return result
+end
+
+function dump_pool:string(value, depth)
+ depth = depth or math.huge
+ if depth < 0 then return nil end
+
+ -- make the string printable (%q pattern keeps real newlines and adds quotes)
+ return { type = "string", repr = string.format("%q", value):gsub("\\\n", "\\n"), length = #value,
+ ref = self.keep_reference and value or nil }
+end
+
+-- default debug function for other types
+setmetatable(dump_pool, {
+ __index = function(cls, vtype)
+ return function(self, value, depth)
+ return (depth == nil or depth >= 0) and { repr = tostring(value), type=vtype, ref = self.keep_reference and value or nil } or nil
+ end
+ end
+})
+
+return dump_pool
BIN  icon1-down.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  icon1-down@2x.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  icon1.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  icon1@2x.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  icon2-down.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  icon2-down@2x.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  icon2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  icon2@2x.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  listItemBg.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  listItemBg_over.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 main.lua
@@ -0,0 +1,38 @@
+-- require("debugger")()
+-----------------------------------------------------------------------------------------
+--
+-- main.lua
+--
+-----------------------------------------------------------------------------------------
+
+-- show default status bar (iOS)
+display.setStatusBar( display.DefaultStatusBar )
+
+-- include Corona's "widget" library
+local widget = require "widget"
+local storyboard = require "storyboard"
+
+-- event listeners for tab buttons:
+local function onFirstView( event )
+ storyboard.gotoScene( "users" )
+end
+
+local function onSecondView( event )
+ storyboard.gotoScene( "new_user" )
+end
+
+-- create a tabBar widget with two buttons at the bottom of the screen
+
+-- table to setup buttons
+local tabButtons = {
+ { label="Users", up="icon1.png", down="icon1-down.png", width = 32, height = 32, onPress=onFirstView, selected=true },
+ { label="New User", up="icon2.png", down="icon2-down.png", width = 32, height = 32, onPress=onSecondView }
+}
+
+-- create the actual tabBar widget
+storyboard.tabBar = widget.newTabBar{
+ top = display.contentHeight - 50, -- 50 is default height for tabBar widget
+ buttons = tabButtons
+}
+
+onFirstView() -- invoke first tab button's onPress event manually
190 new_user.lua
@@ -0,0 +1,190 @@
+-----------------------------------------------------------------------------------------
+--
+-- topics.lua
+--
+-----------------------------------------------------------------------------------------
+
+local widget = require("widget")
+local storyboard = require( "storyboard" )
+local json = require("json")
+local scene = storyboard.newScene()
+local data
+local dataSaved = false
+local form = {}
+local users = {first_name=nil, last_name=nil, email=nil}
+local hasPut = false
+
+-----------------------------------------------------------------------------------------
+-- BEGINNING OF YOUR IMPLEMENTATION
+--
+-- NOTE: Code outside of listener functions (below) will only be executed once,
+-- unless storyboard.removeScene() is called.
+--
+-----------------------------------------------------------------------------------------
+function fieldHandler(name, getObj)
+ return function(e)
+ print("fieldHandler: " .. name)
+ if e.phase == "began" then
+ print("onField: began")
+ elseif e.phase == "ended" then
+ print("onField: ended")
+ elseif e.phase == "submitted" then
+ users[name] = getObj().text
+ print("onField: submit")
+ end
+ end
+end
+
+function createForm()
+ local padding = 10
+ local x = 10
+ local rowHeight = 70
+ local y = rowHeight
+ local font = native.Font
+ local fontSize = 14
+ local fieldWidth = 300
+
+ local label1 = display.newRetinaText( "First Name:", 0, 0, native.systemFont, 18)
+ label1:setTextColor( 0 ) -- black
+ label1:setReferencePoint( display.TopLeftReferencePoint )
+ label1.x = x
+ label1.y = y-25
+ form.first_name = native.newTextField(x, y, fieldWidth, 28)
+ form.first_name.size = fontSize
+ form.first_name:addEventListener('userInput', fieldHandler("first_name", function() return form.first_name end))
+ y = y + rowHeight
+
+ local label2 = display.newRetinaText( "Last Name:", x, y-10, native.systemFont, 18)
+ label2:setTextColor( 0 ) -- black
+ label2:setReferencePoint( display.TopLeftReferencePoint )
+ label2.x = x
+ label2.y = y-25
+ form.last_name = native.newTextField(x, y, fieldWidth, 28)
+ form.last_name.size = fontSize
+ form.last_name:addEventListener('userInput', fieldHandler("last_name", function() return form.last_name end))
+ y = y + rowHeight
+
+ local label3 = display.newRetinaText( "Email:", x, y-10, native.systemFont, 18)
+ label3:setTextColor( 0 ) -- black
+ label3:setReferencePoint( display.TopLeftReferencePoint )
+ label3.x = x
+ label3.y = y-25
+ form.email = native.newTextField(x, y, fieldWidth, 28)
+ form.email.size = fontSize
+ form.email:addEventListener('userInput', fieldHandler("email", function() return form.email end))
+ y = y + rowHeight
+
+end
+
+function scene:createScene( event )
+ local group = self.view
+end
+
+-- Handler that gets notified when the alert closes
+function onComplete( event )
+ if "clicked" == event.action then
+ local i = event.index
+ if 1 == i then
+ storyboard.gotoScene("new_user")
+ end
+ end
+end
+
+-- Called immediately after scene has moved onscreen:
+function scene:enterScene( event )
+ local group = self.view
+
+ createForm()
+
+ -- Network listener
+ local function networkListener(e)
+ if ( e.isError ) then
+ print( "Network error!")
+ local alert = native.showAlert( "Error", "Network Error", { "OK" }, onComplete )
+ else
+ print ( "RESPONSE: " .. tostring(event.response) )
+ -- Show alert with two buttons
+ local alert = native.showAlert( "Add User", "Success!", { "OK" }, onComplete )
+ end
+ end
+
+ -- Event listener for myButton
+ local onButtonEvent = function (event )
+ print( "You pressed and released a button!" )
+ -- Setup table to mimic rails params format
+ local results = {users=nil}
+ results.users = users
+ local url = "http://localhost:3000/users"
+ print("-------------------------------------------------------------------------------------------")
+ print(tostring(json.encode(results)))
+ print("-------------------------------------------------------------------------------------------")
+ headers = {}
+
+ headers["Content-Type"] = "application/json"
+ headers["Accept-Language"] = "en-US"
+
+ body = json.encode(users)
+
+ local params = {}
+ params.headers = headers
+ params.body = body
+ network.request(url, 'POST', networkListener, params)
+ hasPut = false
+ end
+
+ -- Create submit button
+ local myButton = widget.newButton{
+ id = "btn001",
+ left = 100,
+ top = 200,
+ label = "Widget Button",
+ width = 150, height = 28,
+ cornerRadius = 8,
+ onRelease = onButtonEvent
+ }
+ myButton.x = (display.contentWidth/2) - 75
+ myButton.y = 300
+
+ -- Change the button's label text:
+ myButton:setLabel( "My Button" )
+
+end
+
+-- Called when scene is about to move offscreen:
+function scene:exitScene( event )
+ local group = self.view
+ for k,v in pairs(form) do
+ form[k]:removeSelf()
+ form[k] = nil
+ end
+end
+
+-- If scene's view is removed, scene:destroyScene() will be called just prior to:
+function scene:destroyScene( event )