---- MobDebug 0.561-- Copyright 2011-14 Paul Kulchenko-- Based on RemDebug 1.0 Copyright Kepler Project 2005---- use loaded modules or load explicitly on those systems that require thatlocal require = requirelocal io = io or require "io"local table = table or require "table"local string = string or require "string"local coroutine = coroutine or require "coroutine"-- protect require "os" as it may fail on embedded systems without os modulelocal os = os or (function(module)local ok, res = pcall(require, module)return ok and res or nilend)("os")local mobdebug = {_NAME = "mobdebug",_VERSION = 0.561,_COPYRIGHT = "Paul Kulchenko",_DESCRIPTION = "Mobile Remote Debugger for the Lua programming language",port = os and os.getenv and tonumber((os.getenv("MOBDEBUG_PORT"))) or 8172,checkcount = 200,yieldtimeout = 0.02,}local error = errorlocal getfenv = getfenvlocal setfenv = setfenvlocal loadstring = loadstring or load -- "load" replaced "loadstring" in Lua 5.2local pairs = pairslocal setmetatable = setmetatablelocal tonumber = tonumberlocal unpack = table.unpack or unpacklocal rawget = rawget-- if strict.lua is used, then need to avoid referencing some global-- variables, as they can be undefined;-- use rawget to avoid complaints from strict.lua at run-time.-- it's safe to do the initialization here as all these variables-- should get defined values (if any) before the debugging starts.-- there is also global 'wx' variable, which is checked as part of-- the debug loop as 'wx' can be loaded at any time during debugging.local genv = _G or _ENVlocal jit = rawget(genv, "jit")local MOAICoroutine = rawget(genv, "MOAICoroutine")if not setfenv then -- Lua 5.2-- based on http://lua-users.org/lists/lua-l/2010-06/msg00314.html-- this assumes f is a functionlocal function findenv(f)local level = 1repeatlocal name, value = debug.getupvalue(f, level)if name == '_ENV' then return level, value endlevel = level + 1until name == nilreturn nil endgetfenv = function (f) return(select(2, findenv(f)) or _G) endsetfenv = function (f, t)local level = findenv(f)if level then debug.setupvalue(f, level, t) endreturn f endend-- check for OS and convert file names to lower case on windows-- (its file system is case insensitive, but case preserving), as setting a-- breakpoint on x:\Foo.lua will not work if the file was loaded as X:\foo.lua.-- OSX and Windows behave the same way (case insensitive, but case preserving).-- OSX can be configured to be case-sensitive, so check for that. This doesn't-- handle the case of different partitions having different case-sensitivity.local win = os and os.getenv and (os.getenv('WINDIR') or (os.getenv('OS') or ''):match('[Ww]indows')) and true or falselocal mac = not win and (os and os.getenv and os.getenv('DYLD_LIBRARY_PATH') or not io.open("/proc")) and true or falselocal iscasepreserving = win or (mac and io.open('/library') ~= nil)-- turn jit off based on Mike Pall's comment in this discussion:-- http://www.freelists.org/post/luajit/Debug-hooks-and-JIT,2-- "You need to turn it off at the start if you plan to receive-- reliable hook calls at any later point in time."if jit and jit.off then jit.off() endlocal socket = require "socket"local debug = require "debug"local coro_debuggerlocal coro_debugeelocal coroutines = {}; setmetatable(coroutines, {__mode = "k"}) -- "weak" keyslocal events = { BREAK = 1, WATCH = 2, RESTART = 3, STACK = 4 }local breakpoints = {}local watches = {}local lastsourcelocal lastfilelocal watchescnt = 0local abort -- default value is nil; this is used in start/loop distinctionlocal seen_hook = falselocal checkcount = 0local step_into = falselocal step_over = falselocal step_level = 0local stack_level = 0local serverlocal buflocal outputs = {}local iobase = {print = print}local basedir = ""local deferror = "execution aborted at default debugee"local debugee = function ()local a = 1for _ = 1, 10 do a = a + 1 enderror(deferror)endlocal function q(s) return s:gsub('([%(%)%.%%%+%-%*%?%[%^%$%]])','%%%1') endlocal serpent = (function() ---- include Serpent module for serializationlocal n, v = "serpent", 0.272 -- (C) 2012-13 Paul Kulchenko; MIT Licenselocal c, d = "Paul Kulchenko", "Lua serializer and pretty printer"local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'}local badtype = {thread = true, userdata = true, cdata = true}local keyword, globals, G = {}, {}, (_G or _ENV)for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false','for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat','return', 'then', 'true', 'until', 'while'}) do keyword[k] = true endfor k,v in pairs(G) do globals[v] = k end -- build func to name mappingfor _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) dofor k,v in pairs(G[g] or {}) do globals[v] = g..'.'..k end endlocal function s(t, opts)local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnumlocal sparse, custom, huge = opts.sparse, opts.custom, not opts.nohugelocal space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge)local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge)local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)",-- tostring(val) is needed because __tostring may return a non-string valuefunction(s) if not syms[s] then symn = symn+1; syms[s] = symn end return syms[s] end)) endlocal function safestr(s) return type(s) == "number" and (huge and snum[tostring(s)] or s)or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026or ("%q"):format(s):gsub("010円","n"):gsub("026円","\026円") endlocal function comment(s,l) return comm and (l or 0) < comm and ' --[['..tostring(s)..']]' or '' endlocal function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fataland safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) endlocal function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r']local n = name == nil and '' or namelocal plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n]local safe = plain and n or '['..safestr(n)..']'return (path or '')..(plain and path and '.' or '')..safe, safe endlocal alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=paddinglocal maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'}local function padnum(d) return ("%0"..maxn.."d"):format(d) endtable.sort(k, function(a,b)-- sort numeric keys first: k[key] is not nil for numerical keysreturn (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum))< (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) endlocal function val2str(t, name, indent, insref, path, plainindex, level)local ttype, level, mt = type(t), (level or 0), getmetatable(t)local spath, sname = safename(path, name)local tag = plainindex and((type(name) == "number") and '' or name..space..'='..space) or(name ~= nil and sname..space..'='..space or '')if seen[t] then -- already seen this elementsref[#sref+1] = spath..space..'='..space..seen[t]return tag..'nil'..comment('ref', level) endif type(mt) == 'table' and (mt.__serialize or mt.__tostring) then -- knows how to serialize itselfseen[t] = insref or spathif mt.__serialize then t = mt.__serialize(t) else t = tostring(t) endttype = type(t) end -- new value falls through to be serializedif ttype == "table" thenif level >= maxl then return tag..'{}'..comment('max', level) endseen[t] = insref or spathif next(t) == nil then return tag..'{}'..comment(t, level) end -- table emptylocal maxn, o, out = math.min(#t, maxnum or #t), {}, {}for key = 1, maxn do o[key] = key endif not maxnum or #o < maxnum thenlocal n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tablesfor key in pairs(t) do if o[key] ~= key then n = n + 1; o[n] = key end end endif maxnum and #o > maxnum then o[maxnum+1] = nil endif opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) endlocal sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output)for n, key in ipairs(o) dolocal value, ktype, plainindex = t[key], type(key), n <= maxn and not sparseif opts.valignore and opts.valignore[value] -- skip ignored values; do nothingor opts.keyallow and not opts.keyallow[key]or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value typesor sparse and value == nil then -- skipping nils; do nothingelseif ktype == 'table' or ktype == 'function' or badtype[ktype] thenif not seen[key] and not globals[key] thensref[#sref+1] = 'placeholder'local sname = safename(iname, gensym(key)) -- iname is table for local variablessref[#sref] = val2str(key,sname,indent,sname,iname,true) endsref[#sref+1] = 'placeholder'local path = seen[t]..'['..(seen[key] or globals[key] or gensym(key))..']'sref[#sref] = path..space..'='..space..(seen[value] or val2str(value,nil,indent,path))elseout[#out+1] = val2str(value,key,indent,insref,seen[t],plainindex,level+1)endendlocal prefix = string.rep(indent or '', level)local head = indent and '{\n'..prefix..indent or '{'local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space))local tail = indent and "\n"..prefix..'}' or '}'return (custom and custom(tag,head,body,tail) or tag..head..body..tail)..comment(t, level)elseif badtype[ttype] thenseen[t] = insref or spathreturn tag..globerr(t, level)elseif ttype == 'function' thenseen[t] = insref or spathlocal ok, res = pcall(string.dump, t)local func = ok and ((opts.nocode and "function() --[[..skipped..]] end" or"((loadstring or load)("..safestr(res)..",'@serialized'))")..comment(t, level))return tag..(func or globerr(t, level))else return tag..safestr(t) end -- handle all other typesendlocal sepr = indent and "\n" or ";"..spacelocal body = val2str(t, name, indent) -- this call also populates sreflocal tail = #sref>1 and table.concat(sref, sepr)..sepr or ''local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or ''return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end"endlocal function deserialize(data, opts)local f, res = (loadstring or load)('return '..data)if not f then f, res = (loadstring or load)(data) endif not f then return f, res endif opts and opts.safe == false then return pcall(f) endlocal count, thread = 0, coroutine.running()local h, m, c = debug.gethook(thread)debug.sethook(function (e, l) count = count + 1if count >= 3 then error("cannot call functions") endend, "c")local res = {pcall(f)}count = 0 -- set again, otherwise it's tripped on the next sethookdebug.sethook(thread, h, m, c)return (table.unpack or unpack)(res)endlocal function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; endreturn { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s,load = deserialize,dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end,line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end,block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end }end)() ---- end of Serpent modulemobdebug.line = serpent.linemobdebug.dump = serpent.dumplocal function removebasedir(path, basedir)if iscasepreserving then-- check if the lowercased path matches the basedir-- if so, return substring of the original path (to not lowercase it)return path:lower():find('^'..q(basedir:lower()))and path:sub(#basedir+1) or pathelsereturn string.gsub(path, '^'..q(basedir), '')endendlocal function stack(start)local function vars(f)local func = debug.getinfo(f, "f").funclocal i = 1local locals = {}while true dolocal name, value = debug.getlocal(f, i)if not name then break endif string.sub(name, 1, 1) ~= '(' then locals[name] = {value, tostring(value)} endi = i + 1endi = 1local ups = {}while func and true do -- check for func as it may be nil for tail callslocal name, value = debug.getupvalue(func, i)if not name then break endups[name] = {value, tostring(value)}i = i + 1endreturn locals, upsendlocal stack = {}for i = (start or 0), 100 dolocal source = debug.getinfo(i, "Snl")if not source then break endlocal src = source.sourceif src:find("@") == 1 thensrc = src:sub(2):gsub("\\", "/")if src:find("%./") == 1 then src = src:sub(3) endendtable.insert(stack, { -- remove basedir from source{source.name, removebasedir(src, basedir), source.linedefined,source.currentline, source.what, source.namewhat, source.short_src},vars(i+1)})if source.what == 'main' then break endendreturn stackendlocal function set_breakpoint(file, line)if file == '-' and lastfile then file = lastfileelseif iscasepreserving then file = string.lower(file) endif not breakpoints[line] then breakpoints[line] = {} endbreakpoints[line][file] = trueendlocal function remove_breakpoint(file, line)if file == '-' and lastfile then file = lastfileelseif iscasepreserving then file = string.lower(file) endif breakpoints[line] then breakpoints[line][file] = nil endendlocal function has_breakpoint(file, line)return breakpoints[line]and breakpoints[line][iscasepreserving and string.lower(file) or file]endlocal function restore_vars(vars)if type(vars) ~= 'table' then return end-- locals need to be processed in the reverse order, starting from-- the inner block out, to make sure that the localized variables-- are correctly updated with only the closest variable with-- the same name being changed-- first loop find how many local variables there is, while-- the second loop processes them from i to 1local i = 1while true dolocal name = debug.getlocal(3, i)if not name then break endi = i + 1endi = i - 1local written_vars = {}while i > 0 dolocal name = debug.getlocal(3, i)if not written_vars[name] thenif string.sub(name, 1, 1) ~= '(' thendebug.setlocal(3, i, rawget(vars, name))endwritten_vars[name] = trueendi = i - 1endi = 1local func = debug.getinfo(3, "f").funcwhile true dolocal name = debug.getupvalue(func, i)if not name then break endif not written_vars[name] thenif string.sub(name, 1, 1) ~= '(' thendebug.setupvalue(func, i, rawget(vars, name))endwritten_vars[name] = trueendi = i + 1endendlocal function capture_vars(level)local vars = {}local func = debug.getinfo(level or 3, "f").funclocal i = 1while true dolocal name, value = debug.getupvalue(func, i)if not name then break endif string.sub(name, 1, 1) ~= '(' then vars[name] = value endi = i + 1endi = 1while true dolocal name, value = debug.getlocal(level or 3, i)if not name then break endif string.sub(name, 1, 1) ~= '(' then vars[name] = value endi = i + 1end-- returned 'vars' table plays a dual role: (1) it captures local values-- and upvalues to be restored later (in case they are modified in "eval"),-- and (2) it provides an environment for evaluated chunks.-- getfenv(func) is needed to provide proper environment for functions,-- including access to globals, but this causes vars[name] to fail in-- restore_vars on local variables or upvalues with `nil` values when-- 'strict' is in effect. To avoid this `rawget` is used in restore_vars.setmetatable(vars, { __index = getfenv(func), __newindex = getfenv(func) })return varsendlocal function stack_depth(start_depth)for i = start_depth, 0, -1 doif debug.getinfo(i, "l") then return i+1 endendreturn start_depthendlocal function is_safe(stack_level)-- the stack grows up: 0 is getinfo, 1 is is_safe, 2 is debug_hook, 3 is user functionif stack_level == 3 then return true endfor i = 3, stack_level do-- return if it is not safe to abortlocal info = debug.getinfo(i, "S")if not info then return true endif info.what == "C" then return false endendreturn trueendlocal function in_debugger()local this = debug.getinfo(1, "S").source-- only need to check few frames as mobdebug frames should be closefor i = 3, 7 dolocal info = debug.getinfo(i, "S")if not info then return false endif info.source == this then return true endendreturn falseendlocal function is_pending(peer)-- if there is something already in the buffer, skip checkif not buf and checkcount >= mobdebug.checkcount thenpeer:settimeout(0) -- non-blockingbuf = peer:receive(1)peer:settimeout() -- back to blockingcheckcount = 0endreturn bufendlocal function readnext(peer, num)peer:settimeout(0) -- non-blockinglocal res, err, partial = peer:receive(num)peer:settimeout() -- back to blockingreturn res or partial or '', errendlocal function handle_breakpoint(peer)-- check if the buffer has the beginning of SETB/DELB command;-- this is to avoid reading the entire line for commands that-- don't need to be handled here.if not buf or not (buf:sub(1,1) == 'S' or buf:sub(1,1) == 'D') then return end-- check second character to avoid reading STEP or other S* and D* commandsif #buf == 1 then buf = buf .. readnext(peer, 1) endif buf:sub(2,2) ~= 'E' then return end-- need to read few more charactersbuf = buf .. readnext(peer, 5-#buf)if buf ~= 'SETB ' and buf ~= 'DELB ' then return endres, err, partial = peer:receive() -- get the rest of the line; blockingif not res thenif partial then buf = buf .. partial endreturnendlocal _, _, cmd, file, line = (buf..res):find("^([A-Z]+)%s+(.-)%s+(%d+)%s*$")if cmd == 'SETB' then set_breakpoint(file, tonumber(line))elseif cmd == 'DELB' then remove_breakpoint(file, tonumber(line))else-- this looks like a breakpoint command, but something went wrong;-- return here to let the "normal" processing to handle,-- although this is likely to not go well.returnendbuf = nilendlocal function debug_hook(event, line)-- (1) LuaJIT needs special treatment. Because debug_hook is set for-- *all* coroutines, and not just the one being debugged as in regular Lua-- (http://lua-users.org/lists/lua-l/2011-06/msg00513.html),-- need to avoid debugging mobdebug's own code as LuaJIT doesn't-- always correctly generate call/return hook events (there are more-- calls than returns, which breaks stack depth calculation and-- 'step' and 'step over' commands stop working; possibly because-- 'tail return' events are not generated by LuaJIT).-- the next line checks if the debugger is run under LuaJIT and if-- one of debugger methods is present in the stack, it simply returns.if jit then-- when luajit is compiled with LUAJIT_ENABLE_LUA52COMPAT,-- coroutine.running() returns non-nil for the main thread.local coro, main = coroutine.running()if not coro or main then coro = 'main' endlocal disabled = coroutines[coro] == falseor coroutines[coro] == nil and coro ~= (coro_debugee or 'main')if coro_debugee and disabled or not coro_debugee and (disabled or in_debugger())then return endend-- (2) check if abort has been requested and it's safe to abortif abort and is_safe(stack_level) then error(abort) end-- (3) also check if this debug hook has not been visited for any reason.-- this check is needed to avoid stepping in too early-- (for example, when coroutine.resume() is executed inside start()).if not seen_hook and in_debugger() then return endif event == "call" thenstack_level = stack_level + 1elseif event == "return" or event == "tail return" thenstack_level = stack_level - 1elseif event == "line" then-- may need to fall through because of the following:-- (1) step_into-- (2) step_over and stack_level <= step_level (need stack_level)-- (3) breakpoint; check for line first as it's known; then for file-- (4) socket call (only do every Xth check)-- (5) at least one watch is registeredif not (step_into or step_over or breakpoints[line] or watchescnt > 0or is_pending(server)) then checkcount = checkcount + 1; return endcheckcount = mobdebug.checkcount -- force check on the next command-- this is needed to check if the stack got shorter or longer.-- unfortunately counting call/return calls is not reliable.-- the discrepancy may happen when "pcall(load, '')" call is made-- or when "error()" is called in a function.-- in either case there are more "call" than "return" events reported.-- this validation is done for every "line" event, but should be "cheap"-- as it checks for the stack to get shorter (or longer by one call).-- start from one level higher just in case we need to grow the stack.-- this may happen after coroutine.resume call to a function that doesn't-- have any other instructions to execute. it triggers three returns:-- "return, tail return, return", which needs to be accounted for.stack_level = stack_depth(stack_level+1)local caller = debug.getinfo(2, "S")-- grab the filename and fix it if neededlocal file = lastfileif (lastsource ~= caller.source) thenfile, lastsource = caller.source, caller.source-- technically, users can supply names that may not use '@',-- for example when they call loadstring('...', 'filename.lua').-- Unfortunately, there is no reliable/quick way to figure out-- what is the filename and what is the source code.-- The following will work if the supplied filename uses Unix path.if file:find("^@") thenfile = file:gsub("^@", ""):gsub("\\", "/")-- need this conversion to be applied to relative and absolute-- file names as you may write "require 'Foo'" to-- load "foo.lua" (on a case insensitive file system) and breakpoints-- set on foo.lua will not work if not converted to the same case.if iscasepreserving then file = string.lower(file) endif file:find("%./") == 1 then file = file:sub(3)else file = file:gsub('^'..q(basedir), '') end-- some file systems allow newlines in file names; remove these.file = file:gsub("\n", ' ')else-- this is either a file name coming from loadstring("chunk", "file"),-- or the actual source code that needs to be serialized (as it may-- include newlines); assume it's a file name if it's all on one line.file = file:find("[\r\n]") and mobdebug.line(file) or fileend-- set to true if we got here; this only needs to be done once per-- session, so do it here to at least avoid setting it for every line.seen_hook = truelastfile = fileendif is_pending(server) then handle_breakpoint(server) endlocal vars, status, resif (watchescnt > 0) thenvars = capture_vars()for index, value in pairs(watches) dosetfenv(value, vars)local ok, fired = pcall(value)if ok and fired thenstatus, res = coroutine.resume(coro_debugger, events.WATCH, vars, file, line, index)break -- any one watch is enough; don't check multiple timesendendend-- need to get into the "regular" debug handler, but only if there was-- no watch that was fired. If there was a watch, handle its result.local getin = (status == nil) and(step_into-- when coroutine.running() return `nil` (main thread in Lua 5.1),-- step_over will equal 'main', so need to check for that explicitly.or (step_over and step_over == (coroutine.running() or 'main') and stack_level <= step_level)or has_breakpoint(file, line)or is_pending(server))if getin thenvars = vars or capture_vars()step_into = falsestep_over = falsestatus, res = coroutine.resume(coro_debugger, events.BREAK, vars, file, line)end-- handle 'stack' command that provides stack() information to the debuggerif status and res == 'stack' thenwhile status and res == 'stack' do-- resume with the stack trace and variablesif vars then restore_vars(vars) end -- restore vars so they are reflected in stack values-- this may fail if __tostring method fails at run-timelocal ok, snapshot = pcall(stack, 4)status, res = coroutine.resume(coro_debugger, ok and events.STACK or events.BREAK, snapshot, file, line)endend-- need to recheck once more as resume after 'stack' command may-- return something else (for example, 'exit'), which needs to be handledif status and res and res ~= 'stack' thenif abort == nil and res == "exit" then os.exit(1, true); return endabort = res-- only abort if safe; if not, there is another (earlier) check inside-- debug_hook, which will abort execution at the first safe opportunityif is_safe(stack_level) then error(abort) endelseif not status and res thenerror(res, 2) -- report any other (internal) errors back to the applicationendif vars then restore_vars(vars) end-- last command requested Step Over/Out; store the current threadif step_over == true then step_over = coroutine.running() or 'main' endendendlocal function stringify_results(status, ...)if not status then return status, ... end -- on error report as itlocal t = {...}for i,v in pairs(t) do -- stringify each of the returned valueslocal ok, res = pcall(mobdebug.line, v, {nocode = true, comment = 1})t[i] = ok and res or ("%q"):format(res):gsub("\010","n"):gsub("\026","\\026")end-- stringify table with all returned values-- this is done to allow each returned value to be used (serialized or not)-- intependently and to preserve "original" commentsreturn pcall(mobdebug.dump, t, {sparse = false})endlocal function isrunning()return coro_debugger and coroutine.status(coro_debugger) == 'suspended'end-- this is a function that removes all hooks and closes the socket to-- report back to the controller that the debugging is done.-- the script that called `done` can still continue.local function done()if not (isrunning() and server) then return endif not jit thenfor co, debugged in pairs(coroutines) doif debugged then debug.sethook(co) endendenddebug.sethook()server:close()coro_debugger = nil -- to make sure isrunning() returns `false`seen_hook = nil -- to make sure that the next start() call worksabort = nil -- to make sure that callback calls use proper "abort" valueendlocal function debugger_loop(sev, svars, sfile, sline)local commandlocal app, osnamelocal eval_env = svars or {}local function emptyWatch () return false endlocal loaded = {}for k in pairs(package.loaded) do loaded[k] = true endwhile true dolocal line, errlocal wx = rawget(genv, "wx") -- use rawread to make strict.lua happyif (wx or mobdebug.yield) and server.settimeout then server:settimeout(mobdebug.yieldtimeout) endwhile true doline, err = server:receive()if not line and err == "timeout" then-- yield for wx GUI applications if possible to avoid "busyness"app = app or (wx and wx.wxGetApp and wx.wxGetApp())if app thenlocal win = app:GetTopWindow()local inloop = app:IsMainLoopRunning()osname = osname or wx.wxPlatformInfo.Get():GetOperatingSystemFamilyName()if win and not inloop then-- process messages in a regular way-- and exit as soon as the event loop is idleif osname == 'Unix' then wx.wxTimer(app):Start(10, true) endlocal exitLoop = function()win:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_IDLE)win:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_TIMER)app:ExitMainLoop()endwin:Connect(wx.wxEVT_IDLE, exitLoop)win:Connect(wx.wxEVT_TIMER, exitLoop)app:MainLoop()endelseif mobdebug.yield then mobdebug.yield()endelseif not line and err == "closed" thenerror("Debugger connection unexpectedly closed", 0)else-- if there is something in the pending buffer, prepend it to the lineif buf then line = buf .. line; buf = nil endbreakendendif server.settimeout then server:settimeout() end -- back to blockingcommand = string.sub(line, string.find(line, "^[A-Z]+"))if command == "SETB" thenlocal _, _, _, file, line = string.find(line, "^([A-Z]+)%s+(.-)%s+(%d+)%s*$")if file and line thenset_breakpoint(file, tonumber(line))server:send("200 OK\n")elseserver:send("400 Bad Request\n")endelseif command == "DELB" thenlocal _, _, _, file, line = string.find(line, "^([A-Z]+)%s+(.-)%s+(%d+)%s*$")if file and line thenremove_breakpoint(file, tonumber(line))server:send("200 OK\n")elseserver:send("400 Bad Request\n")endelseif command == "EXEC" thenlocal _, _, chunk = string.find(line, "^[A-Z]+%s+(.+)$")if chunk thenlocal func, res = loadstring(chunk)local statusif func thensetfenv(func, eval_env)status, res = stringify_results(pcall(func))endif status thenserver:send("200 OK " .. #res .. "\n")server:send(res)elseserver:send("401 Error in Expression " .. #res .. "\n")server:send(res)endelseserver:send("400 Bad Request\n")endelseif command == "LOAD" thenlocal _, _, size, name = string.find(line, "^[A-Z]+%s+(%d+)%s+(%S.-)%s*$")size = tonumber(size)if abort == nil then -- no LOAD/RELOAD allowed inside start()if size > 0 then server:receive(size) endif sfile and sline thenserver:send("201 Started " .. sfile .. " " .. sline .. "\n")elseserver:send("200 OK 0\n")endelse-- reset environment to allow required modules to load again-- remove those packages that weren't loaded when debugger startedfor k in pairs(package.loaded) doif not loaded[k] then package.loaded[k] = nil endendif size == 0 and name == '-' then -- RELOAD the current script being debuggedserver:send("200 OK 0\n")coroutine.yield("load")else-- receiving 0 bytes blocks (at least in luasocket 2.0.2), so skip readinglocal chunk = size == 0 and "" or server:receive(size)if chunk then -- LOAD a new script for debugginglocal func, res = loadstring(chunk, "@"..name)if func thenserver:send("200 OK 0\n")debugee = funccoroutine.yield("load")elseserver:send("401 Error in Expression " .. #res .. "\n")server:send(res)endelseserver:send("400 Bad Request\n")endendendelseif command == "SETW" thenlocal _, _, exp = string.find(line, "^[A-Z]+%s+(.+)%s*$")if exp thenlocal func, res = loadstring("return(" .. exp .. ")")if func thenwatchescnt = watchescnt + 1local newidx = #watches + 1watches[newidx] = funcserver:send("200 OK " .. newidx .. "\n")elseserver:send("401 Error in Expression " .. #res .. "\n")server:send(res)endelseserver:send("400 Bad Request\n")endelseif command == "DELW" thenlocal _, _, index = string.find(line, "^[A-Z]+%s+(%d+)%s*$")index = tonumber(index)if index > 0 and index <= #watches thenwatchescnt = watchescnt - (watches[index] ~= emptyWatch and 1 or 0)watches[index] = emptyWatchserver:send("200 OK\n")elseserver:send("400 Bad Request\n")endelseif command == "RUN" thenserver:send("200 OK\n")local ev, vars, file, line, idx_watch = coroutine.yield()eval_env = varsif ev == events.BREAK thenserver:send("202 Paused " .. file .. " " .. line .. "\n")elseif ev == events.WATCH thenserver:send("203 Paused " .. file .. " " .. line .. " " .. idx_watch .. "\n")elseif ev == events.RESTART then-- nothing to doelseserver:send("401 Error in Execution " .. #file .. "\n")server:send(file)endelseif command == "STEP" thenserver:send("200 OK\n")step_into = truelocal ev, vars, file, line, idx_watch = coroutine.yield()eval_env = varsif ev == events.BREAK thenserver:send("202 Paused " .. file .. " " .. line .. "\n")elseif ev == events.WATCH thenserver:send("203 Paused " .. file .. " " .. line .. " " .. idx_watch .. "\n")elseif ev == events.RESTART then-- nothing to doelseserver:send("401 Error in Execution " .. #file .. "\n")server:send(file)endelseif command == "OVER" or command == "OUT" thenserver:send("200 OK\n")step_over = true-- OVER and OUT are very similar except for-- the stack level value at which to stopif command == "OUT" then step_level = stack_level - 1else step_level = stack_level endlocal ev, vars, file, line, idx_watch = coroutine.yield()eval_env = varsif ev == events.BREAK thenserver:send("202 Paused " .. file .. " " .. line .. "\n")elseif ev == events.WATCH thenserver:send("203 Paused " .. file .. " " .. line .. " " .. idx_watch .. "\n")elseif ev == events.RESTART then-- nothing to doelseserver:send("401 Error in Execution " .. #file .. "\n")server:send(file)endelseif command == "BASEDIR" thenlocal _, _, dir = string.find(line, "^[A-Z]+%s+(.+)%s*$")if dir thenbasedir = iscasepreserving and string.lower(dir) or dir-- reset cached source as it may change with basedirlastsource = nilserver:send("200 OK\n")elseserver:send("400 Bad Request\n")endelseif command == "SUSPEND" then-- do nothing; it already fulfilled its roleelseif command == "DONE" thenserver:send("200 OK\n")done()return -- done with all the debuggingelseif command == "STACK" then-- first check if we can execute the stack command-- as it requires yielding back to debug_hook it cannot be executed-- if we have not seen the hook yet as happens after start().-- in this case we simply return an empty resultlocal vars, ev = {}if seen_hook thenev, vars = coroutine.yield("stack")endif ev and ev ~= events.STACK thenserver:send("401 Error in Execution " .. #vars .. "\n")server:send(vars)elselocal ok, res = pcall(mobdebug.dump, vars, {nocode = true, sparse = false})if ok thenserver:send("200 OK " .. res .. "\n")elseserver:send("401 Error in Execution " .. #res .. "\n")server:send(res)endendelseif command == "OUTPUT" thenlocal _, _, stream, mode = string.find(line, "^[A-Z]+%s+(%w+)%s+([dcr])%s*$")if stream and mode and stream == "stdout" then-- assign "print" in the global environmentlocal default = mode == 'd'genv.print = default and iobase.print or coroutine.wrap(function()-- wrapping into coroutine.wrap protects this function from-- being stepped through in the debugger.-- don't use vararg (...) as it adds a reference for its values,-- which may affect how they are garbage collectedwhile true dolocal tbl = {coroutine.yield()}if mode == 'c' then iobase.print(unpack(tbl)) endfor n = 1, #tbl dotbl[n] = select(2, pcall(mobdebug.line, tbl[n], {nocode = true, comment = false})) endlocal file = table.concat(tbl, "\t").."\n"server:send("204 Output " .. stream .. " " .. #file .. "\n" .. file)endend)if not default then genv.print() end -- "fake" print to start printing loopserver:send("200 OK\n")elseserver:send("400 Bad Request\n")endelseif command == "EXIT" thenserver:send("200 OK\n")coroutine.yield("exit")elseserver:send("400 Bad Request\n")endendendlocal function connect(controller_host, controller_port)return (socket.connect4 or socket.connect)(controller_host, controller_port)endlocal lasthost, lastport-- Starts a debug session by connecting to a controllerlocal function start(controller_host, controller_port)-- only one debugging session can be run (as there is only one debug hook)if isrunning() then return endlasthost = controller_host or lasthostlastport = controller_port or lastportcontroller_host = lasthost or "localhost"controller_port = lastport or mobdebug.portlocal errserver, err = (socket.connect4 or socket.connect)(controller_host, controller_port)if server then-- correct stack depth which already has some calls on it-- so it doesn't go into negative when those calls return-- as this breaks subsequence checks in stack_depth().-- start from 16th frame, which is sufficiently large for this check.stack_level = stack_depth(16)-- provide our own traceback function to report the error remotelydolocal dtraceback = debug.tracebackdebug.traceback = function (...)if select('#', ...) >= 1 thenlocal err, lvl = ...if err and type(err) ~= 'thread' thenlocal trace = dtraceback(err, (lvl or 2)+1)if genv.print == iobase.print then -- no remote redirectreturn traceelsegenv.print(trace) -- report the error remotelyreturn -- don't report locally to avoid double reportingendendend-- direct call to debug.traceback: return the original.-- debug.traceback(nil, level) doesn't work in Lua 5.1-- (http://lua-users.org/lists/lua-l/2011-06/msg00574.html), so-- simply remove first frame from the stack tracereturn (dtraceback(...):gsub("(stack traceback:\n)[^\n]*\n", "%1"))endendcoro_debugger = coroutine.create(debugger_loop)debug.sethook(debug_hook, "lcr")seen_hook = nil -- reset in case the last start() call was refusedstep_into = true -- start with step commandreturn trueelseprint(("Could not connect to %s:%s: %s"):format(controller_host, controller_port, err or "unknown error"))endendlocal function controller(controller_host, controller_port, scratchpad)-- only one debugging session can be run (as there is only one debug hook)if isrunning() then return endlasthost = controller_host or lasthostlastport = controller_port or lastportcontroller_host = lasthost or "localhost"controller_port = lastport or mobdebug.portlocal exitonerror = not scratchpadlocal errserver, err = (socket.connect4 or socket.connect)(controller_host, controller_port)if server thenlocal function report(trace, err)local msg = err .. "\n" .. traceserver:send("401 Error in Execution " .. #msg .. "\n")server:send(msg)return errendseen_hook = true -- allow to accept all commandscoro_debugger = coroutine.create(debugger_loop)while true dostep_into = true -- start with step commandabort = false -- reset abort flag from the previous loopif scratchpad then checkcount = mobdebug.checkcount end -- force suspend right awaycoro_debugee = coroutine.create(debugee)debug.sethook(coro_debugee, debug_hook, "lcr")local status, err = coroutine.resume(coro_debugee)-- was there an error or is the script done?-- 'abort' state is allowed here; ignore itif abort thenif tostring(abort) == 'exit' then break endelseif status then -- normal execution is donebreakelseif err and not tostring(err):find(deferror) then-- report the error back-- err is not necessarily a string, so convert to string to reportreport(debug.traceback(coro_debugee), tostring(err))if exitonerror then break end-- resume once more to clear the response the debugger wants to send-- need to use capture_vars(2) as three would be the level of-- the caller for controller(), but because of the tail call,-- the caller may not exist;-- This is not entirely safe as the user may see the local-- variable from console, but they will be reset anyway.-- This functionality is used when scratchpad is paused to-- gain access to remote console to modify global variables.local status, err = coroutine.resume(coro_debugger, events.RESTART, capture_vars(2))if not status or status and err == "exit" then break endendendendelseprint(("Could not connect to %s:%s: %s"):format(controller_host, controller_port, err or "unknown error"))return falseendreturn trueendlocal function scratchpad(controller_host, controller_port)return controller(controller_host, controller_port, true)endlocal function loop(controller_host, controller_port)return controller(controller_host, controller_port, false)endlocal function on()if not (isrunning() and server) then return end-- main is set to true under Lua5.2 for the "main" chunk.-- Lua5.1 returns co as `nil` in that case.local co, main = coroutine.running()if main then co = nil endif co thencoroutines[co] = truedebug.sethook(co, debug_hook, "lcr")elseif jit then coroutines.main = true enddebug.sethook(debug_hook, "lcr")endendlocal function off()if not (isrunning() and server) then return end-- main is set to true under Lua5.2 for the "main" chunk.-- Lua5.1 returns co as `nil` in that case.local co, main = coroutine.running()if main then co = nil end-- don't remove coroutine hook under LuaJIT as there is only one (global) hookif co thencoroutines[co] = falseif not jit then debug.sethook(co) endelseif jit then coroutines.main = false endif not jit then debug.sethook() endend-- check if there is any thread that is still being debugged under LuaJIT;-- if not, turn the debugging offif jit thenlocal remove = truefor co, debugged in pairs(coroutines) doif debugged then remove = false; break endendif remove then debug.sethook() endendend-- Handles server debugging commandslocal function handle(params, client, options)local _, _, command = string.find(params, "^([a-z]+)")local file, line, watch_idxif command == "run" or command == "step" or command == "out"or command == "over" or command == "exit" thenclient:send(string.upper(command) .. "\n")client:receive() -- this should consume the first '200 OK' responsewhile true dolocal done = truelocal breakpoint = client:receive()if not breakpoint thenprint("Program finished")os.exit(0, true)return -- use return here for those cases where os.exit() is not wantedendlocal _, _, status = string.find(breakpoint, "^(%d+)")if status == "200" then-- don't need to do anythingelseif status == "202" then_, _, file, line = string.find(breakpoint, "^202 Paused%s+(.-)%s+(%d+)%s*$")if file and line thenprint("Paused at file " .. file .. " line " .. line)endelseif status == "203" then_, _, file, line, watch_idx = string.find(breakpoint, "^203 Paused%s+(.-)%s+(%d+)%s+(%d+)%s*$")if file and line and watch_idx thenprint("Paused at file " .. file .. " line " .. line .. " (watch expression " .. watch_idx .. ": [" .. watches[watch_idx] .. "])")endelseif status == "204" thenlocal _, _, stream, size = string.find(breakpoint, "^204 Output (%w+) (%d+)$")if stream and size thenlocal msg = client:receive(tonumber(size))print(msg)if outputs[stream] then outputs[stream](msg) end-- this was just the output, so go back reading the responsedone = falseendelseif status == "401" thenlocal _, _, size = string.find(breakpoint, "^401 Error in Execution (%d+)$")if size thenlocal msg = client:receive(tonumber(size))print("Error in remote application: " .. msg)os.exit(1, true)return nil, nil, msg -- use return here for those cases where os.exit() is not wantedendelseprint("Unknown error")os.exit(1, true)-- use return here for those cases where os.exit() is not wantedreturn nil, nil, "Debugger error: unexpected response '" .. breakpoint .. "'"endif done then break endendelseif command == "done" thenclient:send(string.upper(command) .. "\n")if client:receive() ~= "200 OK" thenprint("Unknown error")os.exit(1, true)return nil, nil, "Debugger error: unexpected response after 'done'"endelseif command == "setb" or command == "asetb" then_, _, _, file, line = string.find(params, "^([a-z]+)%s+(.-)%s+(%d+)%s*$")if file and line then-- if this is a file name, and not a file sourceif not file:find('^".*"$') thenfile = string.gsub(file, "\\", "/") -- convert slashfile = removebasedir(file, basedir)endclient:send("SETB " .. file .. " " .. line .. "\n")if command == "asetb" or client:receive() == "200 OK" thenset_breakpoint(file, line)elseprint("Error: breakpoint not inserted")endelseprint("Invalid command")endelseif command == "setw" thenlocal _, _, exp = string.find(params, "^[a-z]+%s+(.+)$")if exp thenclient:send("SETW " .. exp .. "\n")local answer = client:receive()local _, _, watch_idx = string.find(answer, "^200 OK (%d+)%s*$")if watch_idx thenwatches[watch_idx] = expprint("Inserted watch exp no. " .. watch_idx)elselocal _, _, size = string.find(answer, "^401 Error in Expression (%d+)$")if size thenlocal err = client:receive(tonumber(size)):gsub(".-:%d+:%s*","")print("Error: watch expression not set: " .. err)elseprint("Error: watch expression not set")endendelseprint("Invalid command")endelseif command == "delb" or command == "adelb" then_, _, _, file, line = string.find(params, "^([a-z]+)%s+(.-)%s+(%d+)%s*$")if file and line then-- if this is a file name, and not a file sourceif not file:find('^".*"$') thenfile = string.gsub(file, "\\", "/") -- convert slashfile = removebasedir(file, basedir)endclient:send("DELB " .. file .. " " .. line .. "\n")if command == "adelb" or client:receive() == "200 OK" thenremove_breakpoint(file, line)elseprint("Error: breakpoint not removed")endelseprint("Invalid command")endelseif command == "delallb" thenfor line, breaks in pairs(breakpoints) dofor file, _ in pairs(breaks) doclient:send("DELB " .. file .. " " .. line .. "\n")if client:receive() == "200 OK" thenremove_breakpoint(file, line)elseprint("Error: breakpoint at file " .. file .. " line " .. line .. " not removed")endendendelseif command == "delw" thenlocal _, _, index = string.find(params, "^[a-z]+%s+(%d+)%s*$")if index thenclient:send("DELW " .. index .. "\n")if client:receive() == "200 OK" thenwatches[index] = nilelseprint("Error: watch expression not removed")endelseprint("Invalid command")endelseif command == "delallw" thenfor index, exp in pairs(watches) doclient:send("DELW " .. index .. "\n")if client:receive() == "200 OK" thenwatches[index] = nilelseprint("Error: watch expression at index " .. index .. " [" .. exp .. "] not removed")endendelseif command == "eval" or command == "exec"or command == "load" or command == "loadstring"or command == "reload" thenlocal _, _, exp = string.find(params, "^[a-z]+%s+(.+)$")if exp or (command == "reload") thenif command == "eval" or command == "exec" thenexp = (exp:gsub("%-%-%[(=*)%[.-%]%1%]", "") -- remove comments:gsub("%-%-.-\n", " ") -- remove line comments:gsub("\n", " ")) -- convert new linesif command == "eval" then exp = "return " .. exp endclient:send("EXEC " .. exp .. "\n")elseif command == "reload" thenclient:send("LOAD 0 -\n")elseif command == "loadstring" thenlocal _, _, _, file, lines = string.find(exp, "^([\"'])(.-)%1%s+(.+)")if not file then_, _, file, lines = string.find(exp, "^(%S+)%s+(.+)")endclient:send("LOAD " .. #lines .. " " .. file .. "\n")client:send(lines)elselocal file = io.open(exp, "r")if not file and pcall(require, "winapi") then-- if file is not open and winapi is there, try with a short path;-- this may be needed for unicode paths on windowswinapi.set_encoding(winapi.CP_UTF8)local shortp = winapi.short_path(exp)file = shortp and io.open(shortp, "r")endif not file then return nil, nil, "Cannot open file " .. exp end-- read the file and remove the shebang line as it causes a compilation errorlocal lines = file:read("*all"):gsub("^#!.-\n", "\n")file:close()local file = string.gsub(exp, "\\", "/") -- convert slashfile = removebasedir(file, basedir)client:send("LOAD " .. #lines .. " " .. file .. "\n")if #lines > 0 then client:send(lines) endendwhile true dolocal params, err = client:receive()if not params thenreturn nil, nil, "Debugger connection " .. (err or "error")endlocal done = truelocal _, _, status, len = string.find(params, "^(%d+).-%s+(%d+)%s*$")if status == "200" thenlen = tonumber(len)if len > 0 thenlocal status, reslocal str = client:receive(len)-- handle serialized table with resultslocal func, err = loadstring(str)if func thenstatus, res = pcall(func)if not status then err = reselseif type(res) ~= "table" thenerr = "received "..type(res).." instead of expected 'table'"endendif err thenprint("Error in processing results: " .. err)return nil, nil, "Error in processing results: " .. errendprint(unpack(res))return res[1], resendelseif status == "201" then_, _, file, line = string.find(params, "^201 Started%s+(.-)%s+(%d+)%s*$")elseif status == "202" or params == "200 OK" then-- do nothing; this only happens when RE/LOAD command gets the response-- that was for the original command that was abortedelseif status == "204" thenlocal _, _, stream, size = string.find(params, "^204 Output (%w+) (%d+)$")if stream and size thenlocal msg = client:receive(tonumber(size))print(msg)if outputs[stream] then outputs[stream](msg) end-- this was just the output, so go back reading the responsedone = falseendelseif status == "401" thenlen = tonumber(len)local res = client:receive(len)print("Error in expression: " .. res)return nil, nil, reselseprint("Unknown error")return nil, nil, "Debugger error: unexpected response after EXEC/LOAD '" .. params .. "'"endif done then break endendelseprint("Invalid command")endelseif command == "listb" thenfor l, v in pairs(breakpoints) dofor f in pairs(v) doprint(f .. ": " .. l)endendelseif command == "listw" thenfor i, v in pairs(watches) doprint("Watch exp. " .. i .. ": " .. v)endelseif command == "suspend" thenclient:send("SUSPEND\n")elseif command == "stack" thenclient:send("STACK\n")local resp = client:receive()local _, _, status, res = string.find(resp, "^(%d+)%s+%w+%s+(.+)%s*$")if status == "200" thenlocal func, err = loadstring(res)if func == nil thenprint("Error in stack information: " .. err)return nil, nil, errendlocal ok, stack = pcall(func)if not ok thenprint("Error in stack information: " .. stack)return nil, nil, stackendfor _,frame in ipairs(stack) doprint(mobdebug.line(frame[1], {comment = false}))endreturn stackelseif status == "401" thenlocal _, _, len = string.find(resp, "%s+(%d+)%s*$")len = tonumber(len)local res = len > 0 and client:receive(len) or "Invalid stack information."print("Error in expression: " .. res)return nil, nil, reselseprint("Unknown error")return nil, nil, "Debugger error: unexpected response after STACK"endelseif command == "output" thenlocal _, _, stream, mode = string.find(params, "^[a-z]+%s+(%w+)%s+([dcr])%s*$")if stream and mode thenclient:send("OUTPUT "..stream.." "..mode.."\n")local resp = client:receive()local _, _, status = string.find(resp, "^(%d+)%s+%w+%s*$")if status == "200" thenprint("Stream "..stream.." redirected")outputs[stream] = type(options) == 'table' and options.handler or nilelseprint("Unknown error")return nil, nil, "Debugger error: can't redirect "..streamendelseprint("Invalid command")endelseif command == "basedir" thenlocal _, _, dir = string.find(params, "^[a-z]+%s+(.+)$")if dir thendir = string.gsub(dir, "\\", "/") -- convert slashif not string.find(dir, "/$") then dir = dir .. "/" endlocal remdir = dir:match("\t(.+)")if remdir then dir = dir:gsub("/?\t.+", "/") endbasedir = dirclient:send("BASEDIR "..(remdir or dir).."\n")local resp = client:receive()local _, _, status = string.find(resp, "^(%d+)%s+%w+%s*$")if status == "200" thenprint("New base directory is " .. basedir)elseprint("Unknown error")return nil, nil, "Debugger error: unexpected response after BASEDIR"endelseprint(basedir)endelseif command == "help" thenprint("setb <file> <line> -- sets a breakpoint")print("delb <file> <line> -- removes a breakpoint")print("delallb -- removes all breakpoints")print("setw <exp> -- adds a new watch expression")print("delw <index> -- removes the watch expression at index")print("delallw -- removes all watch expressions")print("run -- runs until next breakpoint")print("step -- runs until next line, stepping into function calls")print("over -- runs until next line, stepping over function calls")print("out -- runs until line after returning from current function")print("listb -- lists breakpoints")print("listw -- lists watch expressions")print("eval <exp> -- evaluates expression on the current context and returns its value")print("exec <stmt> -- executes statement on the current context")print("load <file> -- loads a local file for debugging")print("reload -- restarts the current debugging session")print("stack -- reports stack trace")print("output stdout <d|c|r> -- capture and redirect io stream (default|copy|redirect)")print("basedir [<path>] -- sets the base path of the remote application, or shows the current one")print("done -- stops the debugger and continues application execution")print("exit -- exits debugger and the application")elselocal _, _, spaces = string.find(params, "^(%s*)$")if not spaces thenprint("Invalid command")return nil, nil, "Invalid command"endendreturn file, lineend-- Starts debugging serverlocal function listen(host, port)host = host or "*"port = port or mobdebug.portlocal socket = require "socket"print("Lua Remote Debugger")print("Run the program you wish to debug")local server = socket.bind(host, port)local client = server:accept()client:send("STEP\n")client:receive()local breakpoint = client:receive()local _, _, file, line = string.find(breakpoint, "^202 Paused%s+(.-)%s+(%d+)%s*$")if file and line thenprint("Paused at file " .. file )print("Type 'help' for commands")elselocal _, _, size = string.find(breakpoint, "^401 Error in Execution (%d+)%s*$")if size thenprint("Error in remote application: ")print(client:receive(size))endendwhile true doio.write("> ")local line = io.read("*line")handle(line, client)endendlocal cocreatelocal function coro()if cocreate then return end -- only set oncecocreate = cocreate or coroutine.createcoroutine.create = function(f, ...)return cocreate(function(...)mobdebug.on()return f(...)end, ...)endendlocal moconewlocal function moai()if moconew then return end -- only set oncemoconew = moconew or (MOAICoroutine and MOAICoroutine.new)if not moconew then return endMOAICoroutine.new = function(...)local thread = moconew(...)-- need to support both thread.run and getmetatable(thread).run, which-- was used in earlier MOAI versionslocal mt = thread.run and thread or getmetatable(thread)local patched = mt.runmt.run = function(self, f, ...)return patched(self, function(...)mobdebug.on()return f(...)end, ...)endreturn threadendend-- make public functions availablemobdebug.setbreakpoint = set_breakpointmobdebug.removebreakpoint = remove_breakpointmobdebug.listen = listenmobdebug.loop = loopmobdebug.scratchpad = scratchpadmobdebug.handle = handlemobdebug.connect = connectmobdebug.start = startmobdebug.on = onmobdebug.off = offmobdebug.moai = moaimobdebug.coro = coromobdebug.done = donemobdebug.pause = function() step_into = true endmobdebug.yield = nil -- callback-- this is needed to make "require 'modebug'" to work when mobdebug-- module is loaded manuallypackage.loaded.mobdebug = mobdebugreturn mobdebug
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。