| [22021] | 1 | --
|
|---|
| 2 | -- MobDebug -- Lua remote debugger
|
|---|
| 3 | -- Copyright 2011-15 Paul Kulchenko
|
|---|
| 4 | -- Based on RemDebug 1.0 Copyright Kepler Project 2005
|
|---|
| 5 | --
|
|---|
| 6 |
|
|---|
| 7 | -- use loaded modules or load explicitly on those systems that require that
|
|---|
| 8 | local require = require
|
|---|
| 9 | local io = io or require "io"
|
|---|
| 10 | local table = table or require "table"
|
|---|
| 11 | local string = string or require "string"
|
|---|
| 12 | local coroutine = coroutine or require "coroutine"
|
|---|
| 13 | local debug = require "debug"
|
|---|
| 14 | -- protect require "os" as it may fail on embedded systems without os module
|
|---|
| 15 | local os = os or (function(module)
|
|---|
| 16 | local ok, res = pcall(require, module)
|
|---|
| 17 | return ok and res or nil
|
|---|
| 18 | end)("os")
|
|---|
| 19 |
|
|---|
| 20 | local mobdebug = {
|
|---|
| 21 | _NAME = "mobdebug",
|
|---|
| 22 | _VERSION = "0.702",
|
|---|
| 23 | _COPYRIGHT = "Paul Kulchenko",
|
|---|
| 24 | _DESCRIPTION = "Mobile Remote Debugger for the Lua programming language",
|
|---|
| 25 | port = os and os.getenv and tonumber((os.getenv("MOBDEBUG_PORT"))) or 8172,
|
|---|
| 26 | checkcount = 200,
|
|---|
| 27 | yieldtimeout = 0.02, -- yield timeout (s)
|
|---|
| 28 | connecttimeout = 2, -- connect timeout (s)
|
|---|
| 29 | }
|
|---|
| 30 |
|
|---|
| 31 | local HOOKMASK = "lcr"
|
|---|
| 32 | local error = error
|
|---|
| 33 | local getfenv = getfenv
|
|---|
| 34 | local setfenv = setfenv
|
|---|
| 35 | local loadstring = loadstring or load -- "load" replaced "loadstring" in Lua 5.2
|
|---|
| 36 | local pairs = pairs
|
|---|
| 37 | local setmetatable = setmetatable
|
|---|
| 38 | local tonumber = tonumber
|
|---|
| 39 | local unpack = table.unpack or unpack
|
|---|
| 40 | local rawget = rawget
|
|---|
| 41 | local gsub, sub, find = string.gsub, string.sub, string.find
|
|---|
| 42 |
|
|---|
| 43 | -- if strict.lua is used, then need to avoid referencing some global
|
|---|
| 44 | -- variables, as they can be undefined;
|
|---|
| 45 | -- use rawget to avoid complaints from strict.lua at run-time.
|
|---|
| 46 | -- it's safe to do the initialization here as all these variables
|
|---|
| 47 | -- should get defined values (if any) before the debugging starts.
|
|---|
| 48 | -- there is also global 'wx' variable, which is checked as part of
|
|---|
| 49 | -- the debug loop as 'wx' can be loaded at any time during debugging.
|
|---|
| 50 | local genv = _G or _ENV
|
|---|
| 51 | local jit = rawget(genv, "jit")
|
|---|
| 52 | local MOAICoroutine = rawget(genv, "MOAICoroutine")
|
|---|
| 53 |
|
|---|
| 54 | -- ngx_lua debugging requires a special handling as its coroutine.*
|
|---|
| 55 | -- methods use a different mechanism that doesn't allow resume calls
|
|---|
| 56 | -- from debug hook handlers.
|
|---|
| 57 | -- Instead, the "original" coroutine.* methods are used.
|
|---|
| 58 | -- `rawget` needs to be used to protect against `strict` checks, but
|
|---|
| 59 | -- ngx_lua hides those in a metatable, so need to use that.
|
|---|
| 60 | local metagindex = getmetatable(genv) and getmetatable(genv).__index
|
|---|
| 61 | local ngx = type(metagindex) == "table" and metagindex.rawget and metagindex:rawget("ngx") or nil
|
|---|
| 62 | local corocreate = ngx and coroutine._create or coroutine.create
|
|---|
| 63 | local cororesume = ngx and coroutine._resume or coroutine.resume
|
|---|
| 64 | local coroyield = ngx and coroutine._yield or coroutine.yield
|
|---|
| 65 | local corostatus = ngx and coroutine._status or coroutine.status
|
|---|
| 66 | local corowrap = coroutine.wrap
|
|---|
| 67 |
|
|---|
| 68 | if not setfenv then -- Lua 5.2+
|
|---|
| 69 | -- based on http://lua-users.org/lists/lua-l/2010-06/msg00314.html
|
|---|
| 70 | -- this assumes f is a function
|
|---|
| 71 | local function findenv(f)
|
|---|
| 72 | local level = 1
|
|---|
| 73 | repeat
|
|---|
| 74 | local name, value = debug.getupvalue(f, level)
|
|---|
| 75 | if name == '_ENV' then return level, value end
|
|---|
| 76 | level = level + 1
|
|---|
| 77 | until name == nil
|
|---|
| 78 | return nil end
|
|---|
| 79 | getfenv = function (f) return(select(2, findenv(f)) or _G) end
|
|---|
| 80 | setfenv = function (f, t)
|
|---|
| 81 | local level = findenv(f)
|
|---|
| 82 | if level then debug.setupvalue(f, level, t) end
|
|---|
| 83 | return f end
|
|---|
| 84 | end
|
|---|
| 85 |
|
|---|
| 86 | -- check for OS and convert file names to lower case on windows
|
|---|
| 87 | -- (its file system is case insensitive, but case preserving), as setting a
|
|---|
| 88 | -- breakpoint on x:\Foo.lua will not work if the file was loaded as X:\foo.lua.
|
|---|
| 89 | -- OSX and Windows behave the same way (case insensitive, but case preserving).
|
|---|
| 90 | -- OSX can be configured to be case-sensitive, so check for that. This doesn't
|
|---|
| 91 | -- handle the case of different partitions having different case-sensitivity.
|
|---|
| 92 | local win = os and os.getenv and (os.getenv('WINDIR') or (os.getenv('OS') or ''):match('[Ww]indows')) and true or false
|
|---|
| 93 | local mac = not win and (os and os.getenv and os.getenv('DYLD_LIBRARY_PATH') or not io.open("/proc")) and true or false
|
|---|
| 94 | local iscasepreserving = win or (mac and io.open('/library') ~= nil)
|
|---|
| 95 |
|
|---|
| 96 | -- turn jit off based on Mike Pall's comment in this discussion:
|
|---|
| 97 | -- http://www.freelists.org/post/luajit/Debug-hooks-and-JIT,2
|
|---|
| 98 | -- "You need to turn it off at the start if you plan to receive
|
|---|
| 99 | -- reliable hook calls at any later point in time."
|
|---|
| 100 | if jit and jit.off then jit.off() end
|
|---|
| 101 |
|
|---|
| 102 | local socket = require "socket"
|
|---|
| 103 | local coro_debugger
|
|---|
| 104 | local coro_debugee
|
|---|
| 105 | local coroutines = {}; setmetatable(coroutines, {__mode = "k"}) -- "weak" keys
|
|---|
| 106 | local events = { BREAK = 1, WATCH = 2, RESTART = 3, STACK = 4 }
|
|---|
| 107 | local breakpoints = {}
|
|---|
| 108 | local watches = {}
|
|---|
| 109 | local lastsource
|
|---|
| 110 | local lastfile
|
|---|
| 111 | local watchescnt = 0
|
|---|
| 112 | local abort -- default value is nil; this is used in start/loop distinction
|
|---|
| 113 | local seen_hook = false
|
|---|
| 114 | local checkcount = 0
|
|---|
| 115 | local step_into = false
|
|---|
| 116 | local step_over = false
|
|---|
| 117 | local step_level = 0
|
|---|
| 118 | local stack_level = 0
|
|---|
| 119 | local server
|
|---|
| 120 | local buf
|
|---|
| 121 | local outputs = {}
|
|---|
| 122 | local iobase = {print = print}
|
|---|
| 123 | local basedir = ""
|
|---|
| 124 | local deferror = "execution aborted at default debugee"
|
|---|
| 125 | local debugee = function ()
|
|---|
| 126 | local a = 1
|
|---|
| 127 | for _ = 1, 10 do a = a + 1 end
|
|---|
| 128 | error(deferror)
|
|---|
| 129 | end
|
|---|
| 130 | local function q(s) return string.gsub(s, '([%(%)%.%%%+%-%*%?%[%^%$%]])','%%%1') end
|
|---|
| 131 |
|
|---|
| 132 | local serpent = (function() ---- include Serpent module for serialization
|
|---|
| 133 | local n, v = "serpent", "0.30" -- (C) 2012-17 Paul Kulchenko; MIT License
|
|---|
| 134 | local c, d = "Paul Kulchenko", "Lua serializer and pretty printer"
|
|---|
| 135 | local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'}
|
|---|
| 136 | local badtype = {thread = true, userdata = true, cdata = true}
|
|---|
| 137 | local getmetatable = debug and debug.getmetatable or getmetatable
|
|---|
| 138 | local pairs = function(t) return next, t end -- avoid using __pairs in Lua 5.2+
|
|---|
| 139 | local keyword, globals, G = {}, {}, (_G or _ENV)
|
|---|
| 140 | for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false',
|
|---|
| 141 | 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat',
|
|---|
| 142 | 'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end
|
|---|
| 143 | for k,v in pairs(G) do globals[v] = k end -- build func to name mapping
|
|---|
| 144 | for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do
|
|---|
| 145 | for k,v in pairs(type(G[g]) == 'table' and G[g] or {}) do globals[v] = g..'.'..k end end
|
|---|
| 146 |
|
|---|
| 147 | local function s(t, opts)
|
|---|
| 148 | local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum
|
|---|
| 149 | local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge
|
|---|
| 150 | local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge)
|
|---|
| 151 | local maxlen, metatostring = tonumber(opts.maxlength), opts.metatostring
|
|---|
| 152 | local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge)
|
|---|
| 153 | local numformat = opts.numformat or "%.17g"
|
|---|
| 154 | local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0
|
|---|
| 155 | local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)",
|
|---|
| 156 | -- tostring(val) is needed because __tostring may return a non-string value
|
|---|
| 157 | function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return tostring(syms[s]) end)) end
|
|---|
| 158 | local function safestr(s) return type(s) == "number" and tostring(huge and snum[tostring(s)] or numformat:format(s))
|
|---|
| 159 | or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026
|
|---|
| 160 | or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end
|
|---|
| 161 | local function comment(s,l) return comm and (l or 0) < comm and ' --[['..select(2, pcall(tostring, s))..']]' or '' end
|
|---|
| 162 | local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal
|
|---|
| 163 | and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end
|
|---|
| 164 | local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r']
|
|---|
| 165 | local n = name == nil and '' or name
|
|---|
| 166 | local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n]
|
|---|
| 167 | local safe = plain and n or '['..safestr(n)..']'
|
|---|
| 168 | return (path or '')..(plain and path and '.' or '')..safe, safe end
|
|---|
| 169 | local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding
|
|---|
| 170 | local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'}
|
|---|
| 171 | local function padnum(d) return ("%0"..tostring(maxn).."d"):format(tonumber(d)) end
|
|---|
| 172 | table.sort(k, function(a,b)
|
|---|
| 173 | -- sort numeric keys first: k[key] is not nil for numerical keys
|
|---|
| 174 | return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum))
|
|---|
| 175 | < (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end
|
|---|
| 176 | local function val2str(t, name, indent, insref, path, plainindex, level)
|
|---|
| 177 | local ttype, level, mt = type(t), (level or 0), getmetatable(t)
|
|---|
| 178 | local spath, sname = safename(path, name)
|
|---|
| 179 | local tag = plainindex and
|
|---|
| 180 | ((type(name) == "number") and '' or name..space..'='..space) or
|
|---|
| 181 | (name ~= nil and sname..space..'='..space or '')
|
|---|
| 182 | if seen[t] then -- already seen this element
|
|---|
| 183 | sref[#sref+1] = spath..space..'='..space..seen[t]
|
|---|
| 184 | return tag..'nil'..comment('ref', level) end
|
|---|
| 185 | -- protect from those cases where __tostring may fail
|
|---|
| 186 | if type(mt) == 'table' then
|
|---|
| 187 | local to, tr = pcall(function() return mt.__tostring(t) end)
|
|---|
| 188 | local so, sr = pcall(function() return mt.__serialize(t) end)
|
|---|
| 189 | if (opts.metatostring ~= false and to or so) then -- knows how to serialize itself
|
|---|
| 190 | seen[t] = insref or spath
|
|---|
| 191 | t = so and sr or tr
|
|---|
| 192 | ttype = type(t)
|
|---|
| 193 | end -- new value falls through to be serialized
|
|---|
| 194 | end
|
|---|
| 195 | if ttype == "table" then
|
|---|
| 196 | if level >= maxl then return tag..'{}'..comment('maxlvl', level) end
|
|---|
| 197 | seen[t] = insref or spath
|
|---|
| 198 | if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty
|
|---|
| 199 | if maxlen and maxlen < 0 then return tag..'{}'..comment('maxlen', level) end
|
|---|
| 200 | local maxn, o, out = math.min(#t, maxnum or #t), {}, {}
|
|---|
| 201 | for key = 1, maxn do o[key] = key end
|
|---|
| 202 | if not maxnum or #o < maxnum then
|
|---|
| 203 | local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables
|
|---|
| 204 | for key in pairs(t) do if o[key] ~= key then n = n + 1; o[n] = key end end end
|
|---|
| 205 | if maxnum and #o > maxnum then o[maxnum+1] = nil end
|
|---|
| 206 | if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end
|
|---|
| 207 | local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output)
|
|---|
| 208 | for n, key in ipairs(o) do
|
|---|
| 209 | local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse
|
|---|
| 210 | if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing
|
|---|
| 211 | or opts.keyallow and not opts.keyallow[key]
|
|---|
| 212 | or opts.keyignore and opts.keyignore[key]
|
|---|
| 213 | or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types
|
|---|
| 214 | or sparse and value == nil then -- skipping nils; do nothing
|
|---|
| 215 | elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then
|
|---|
| 216 | if not seen[key] and not globals[key] then
|
|---|
| 217 | sref[#sref+1] = 'placeholder'
|
|---|
| 218 | local sname = safename(iname, gensym(key)) -- iname is table for local variables
|
|---|
| 219 | sref[#sref] = val2str(key,sname,indent,sname,iname,true) end
|
|---|
| 220 | sref[#sref+1] = 'placeholder'
|
|---|
| 221 | local path = seen[t]..'['..tostring(seen[key] or globals[key] or gensym(key))..']'
|
|---|
| 222 | sref[#sref] = path..space..'='..space..tostring(seen[value] or val2str(value,nil,indent,path))
|
|---|
| 223 | else
|
|---|
| 224 | out[#out+1] = val2str(value,key,indent,insref,seen[t],plainindex,level+1)
|
|---|
| 225 | if maxlen then
|
|---|
| 226 | maxlen = maxlen - #out[#out]
|
|---|
| 227 | if maxlen < 0 then break end
|
|---|
| 228 | end
|
|---|
| 229 | end
|
|---|
| 230 | end
|
|---|
| 231 | local prefix = string.rep(indent or '', level)
|
|---|
| 232 | local head = indent and '{\n'..prefix..indent or '{'
|
|---|
| 233 | local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space))
|
|---|
| 234 | local tail = indent and "\n"..prefix..'}' or '}'
|
|---|
| 235 | return (custom and custom(tag,head,body,tail,level) or tag..head..body..tail)..comment(t, level)
|
|---|
| 236 | elseif badtype[ttype] then
|
|---|
| 237 | seen[t] = insref or spath
|
|---|
| 238 | return tag..globerr(t, level)
|
|---|
| 239 | elseif ttype == 'function' then
|
|---|
| 240 | seen[t] = insref or spath
|
|---|
| 241 | if opts.nocode then return tag.."function() --[[..skipped..]] end"..comment(t, level) end
|
|---|
| 242 | local ok, res = pcall(string.dump, t)
|
|---|
| 243 | local func = ok and "((loadstring or load)("..safestr(res)..",'@serialized'))"..comment(t, level)
|
|---|
| 244 | return tag..(func or globerr(t, level))
|
|---|
| 245 | else return tag..safestr(t) end -- handle all other types
|
|---|
| 246 | end
|
|---|
| 247 | local sepr = indent and "\n" or ";"..space
|
|---|
| 248 | local body = val2str(t, name, indent) -- this call also populates sref
|
|---|
| 249 | local tail = #sref>1 and table.concat(sref, sepr)..sepr or ''
|
|---|
| 250 | local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or ''
|
|---|
| 251 | return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end"
|
|---|
| 252 | end
|
|---|
| 253 |
|
|---|
| 254 | local function deserialize(data, opts)
|
|---|
| 255 | local env = (opts and opts.safe == false) and G
|
|---|
| 256 | or setmetatable({}, {
|
|---|
| 257 | __index = function(t,k) return t end,
|
|---|
| 258 | __call = function(t,...) error("cannot call functions") end
|
|---|
| 259 | })
|
|---|
| 260 | local f, res = (loadstring or load)('return '..data, nil, nil, env)
|
|---|
| 261 | if not f then f, res = (loadstring or load)(data, nil, nil, env) end
|
|---|
| 262 | if not f then return f, res end
|
|---|
| 263 | if setfenv then setfenv(f, env) end
|
|---|
| 264 | return pcall(f)
|
|---|
| 265 | end
|
|---|
| 266 |
|
|---|
| 267 | local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end
|
|---|
| 268 | return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s,
|
|---|
| 269 | load = deserialize,
|
|---|
| 270 | dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end,
|
|---|
| 271 | line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end,
|
|---|
| 272 | block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end }
|
|---|
| 273 | end)() ---- end of Serpent module
|
|---|
| 274 |
|
|---|
| 275 | mobdebug.line = serpent.line
|
|---|
| 276 | mobdebug.dump = serpent.dump
|
|---|
| 277 | mobdebug.linemap = nil
|
|---|
| 278 | mobdebug.loadstring = loadstring
|
|---|
| 279 |
|
|---|
| 280 | local function removebasedir(path, basedir)
|
|---|
| 281 | if iscasepreserving then
|
|---|
| 282 | -- check if the lowercased path matches the basedir
|
|---|
| 283 | -- if so, return substring of the original path (to not lowercase it)
|
|---|
| 284 | return path:lower():find('^'..q(basedir:lower()))
|
|---|
| 285 | and path:sub(#basedir+1) or path
|
|---|
| 286 | else
|
|---|
| 287 | return string.gsub(path, '^'..q(basedir), '')
|
|---|
| 288 | end
|
|---|
| 289 | end
|
|---|
| 290 |
|
|---|
| 291 | local function stack(start)
|
|---|
| 292 | local function vars(f)
|
|---|
| 293 | local func = debug.getinfo(f, "f").func
|
|---|
| 294 | local i = 1
|
|---|
| 295 | local locals = {}
|
|---|
| 296 | -- get locals
|
|---|
| 297 | while true do
|
|---|
| 298 | local name, value = debug.getlocal(f, i)
|
|---|
| 299 | if not name then break end
|
|---|
| 300 | if string.sub(name, 1, 1) ~= '(' then
|
|---|
| 301 | locals[name] = {value, select(2,pcall(tostring,value))}
|
|---|
| 302 | end
|
|---|
| 303 | i = i + 1
|
|---|
| 304 | end
|
|---|
| 305 | -- get varargs (these use negative indices)
|
|---|
| 306 | i = 1
|
|---|
| 307 | while true do
|
|---|
| 308 | local name, value = debug.getlocal(f, -i)
|
|---|
| 309 | -- `not name` should be enough, but LuaJIT 2.0.0 incorrectly reports `(*temporary)` names here
|
|---|
| 310 | if not name or name ~= "(*vararg)" then break end
|
|---|
| 311 | locals[name:gsub("%)$"," "..i..")")] = {value, select(2,pcall(tostring,value))}
|
|---|
| 312 | i = i + 1
|
|---|
| 313 | end
|
|---|
| 314 | -- get upvalues
|
|---|
| 315 | i = 1
|
|---|
| 316 | local ups = {}
|
|---|
| 317 | while func do -- check for func as it may be nil for tail calls
|
|---|
| 318 | local name, value = debug.getupvalue(func, i)
|
|---|
| 319 | if not name then break end
|
|---|
| 320 | ups[name] = {value, select(2,pcall(tostring,value))}
|
|---|
| 321 | i = i + 1
|
|---|
| 322 | end
|
|---|
| 323 | return locals, ups
|
|---|
| 324 | end
|
|---|
| 325 |
|
|---|
| 326 | local stack = {}
|
|---|
| 327 | local linemap = mobdebug.linemap
|
|---|
| 328 | for i = (start or 0), 100 do
|
|---|
| 329 | local source = debug.getinfo(i, "Snl")
|
|---|
| 330 | if not source then break end
|
|---|
| 331 |
|
|---|
| 332 | local src = source.source
|
|---|
| 333 | if src:find("@") == 1 then
|
|---|
| 334 | src = src:sub(2):gsub("\\", "/")
|
|---|
| 335 | if src:find("%./") == 1 then src = src:sub(3) end
|
|---|
| 336 | end
|
|---|
| 337 |
|
|---|
| 338 | table.insert(stack, { -- remove basedir from source
|
|---|
| 339 | {source.name, removebasedir(src, basedir),
|
|---|
| 340 | linemap and linemap(source.linedefined, source.source) or source.linedefined,
|
|---|
| 341 | linemap and linemap(source.currentline, source.source) or source.currentline,
|
|---|
| 342 | source.what, source.namewhat, source.short_src},
|
|---|
| 343 | vars(i+1)})
|
|---|
| 344 | if source.what == 'main' then break end
|
|---|
| 345 | end
|
|---|
| 346 | return stack
|
|---|
| 347 | end
|
|---|
| 348 |
|
|---|
| 349 | local function set_breakpoint(file, line)
|
|---|
| 350 | if file == '-' and lastfile then file = lastfile
|
|---|
| 351 | elseif iscasepreserving then file = string.lower(file) end
|
|---|
| 352 | if not breakpoints[line] then breakpoints[line] = {} end
|
|---|
| 353 | breakpoints[line][file] = true
|
|---|
| 354 | end
|
|---|
| 355 |
|
|---|
| 356 | local function remove_breakpoint(file, line)
|
|---|
| 357 | if file == '-' and lastfile then file = lastfile
|
|---|
| 358 | elseif file == '*' and line == 0 then breakpoints = {}
|
|---|
| 359 | elseif iscasepreserving then file = string.lower(file) end
|
|---|
| 360 | if breakpoints[line] then breakpoints[line][file] = nil end
|
|---|
| 361 | end
|
|---|
| 362 |
|
|---|
| 363 | local function has_breakpoint(file, line)
|
|---|
| 364 | return breakpoints[line]
|
|---|
| 365 | and breakpoints[line][iscasepreserving and string.lower(file) or file]
|
|---|
| 366 | end
|
|---|
| 367 |
|
|---|
| 368 | local function restore_vars(vars)
|
|---|
| 369 | if type(vars) ~= 'table' then return end
|
|---|
| 370 |
|
|---|
| 371 | -- locals need to be processed in the reverse order, starting from
|
|---|
| 372 | -- the inner block out, to make sure that the localized variables
|
|---|
| 373 | -- are correctly updated with only the closest variable with
|
|---|
| 374 | -- the same name being changed
|
|---|
| 375 | -- first loop find how many local variables there is, while
|
|---|
| 376 | -- the second loop processes them from i to 1
|
|---|
| 377 | local i = 1
|
|---|
| 378 | while true do
|
|---|
| 379 | local name = debug.getlocal(3, i)
|
|---|
| 380 | if not name then break end
|
|---|
| 381 | i = i + 1
|
|---|
| 382 | end
|
|---|
| 383 | i = i - 1
|
|---|
| 384 | local written_vars = {}
|
|---|
| 385 | while i > 0 do
|
|---|
| 386 | local name = debug.getlocal(3, i)
|
|---|
| 387 | if not written_vars[name] then
|
|---|
| 388 | if string.sub(name, 1, 1) ~= '(' then
|
|---|
| 389 | debug.setlocal(3, i, rawget(vars, name))
|
|---|
| 390 | end
|
|---|
| 391 | written_vars[name] = true
|
|---|
| 392 | end
|
|---|
| 393 | i = i - 1
|
|---|
| 394 | end
|
|---|
| 395 |
|
|---|
| 396 | i = 1
|
|---|
| 397 | local func = debug.getinfo(3, "f").func
|
|---|
| 398 | while true do
|
|---|
| 399 | local name = debug.getupvalue(func, i)
|
|---|
| 400 | if not name then break end
|
|---|
| 401 | if not written_vars[name] then
|
|---|
| 402 | if string.sub(name, 1, 1) ~= '(' then
|
|---|
| 403 | debug.setupvalue(func, i, rawget(vars, name))
|
|---|
| 404 | end
|
|---|
| 405 | written_vars[name] = true
|
|---|
| 406 | end
|
|---|
| 407 | i = i + 1
|
|---|
| 408 | end
|
|---|
| 409 | end
|
|---|
| 410 |
|
|---|
| 411 | local function capture_vars(level, thread)
|
|---|
| 412 | level = (level or 0)+2 -- add two levels for this and debug calls
|
|---|
| 413 | local func = (thread and debug.getinfo(thread, level, "f") or debug.getinfo(level, "f") or {}).func
|
|---|
| 414 | if not func then return {} end
|
|---|
| 415 |
|
|---|
| 416 | local vars = {['...'] = {}}
|
|---|
| 417 | local i = 1
|
|---|
| 418 | while true do
|
|---|
| 419 | local name, value = debug.getupvalue(func, i)
|
|---|
| 420 | if not name then break end
|
|---|
| 421 | if string.sub(name, 1, 1) ~= '(' then vars[name] = value end
|
|---|
| 422 | i = i + 1
|
|---|
| 423 | end
|
|---|
| 424 | i = 1
|
|---|
| 425 | while true do
|
|---|
| 426 | local name, value
|
|---|
| 427 | if thread then
|
|---|
| 428 | name, value = debug.getlocal(thread, level, i)
|
|---|
| 429 | else
|
|---|
| 430 | name, value = debug.getlocal(level, i)
|
|---|
| 431 | end
|
|---|
| 432 | if not name then break end
|
|---|
| 433 | if string.sub(name, 1, 1) ~= '(' then vars[name] = value end
|
|---|
| 434 | i = i + 1
|
|---|
| 435 | end
|
|---|
| 436 | -- get varargs (these use negative indices)
|
|---|
| 437 | i = 1
|
|---|
| 438 | while true do
|
|---|
| 439 | local name, value
|
|---|
| 440 | if thread then
|
|---|
| 441 | name, value = debug.getlocal(thread, level, -i)
|
|---|
| 442 | else
|
|---|
| 443 | name, value = debug.getlocal(level, -i)
|
|---|
| 444 | end
|
|---|
| 445 | -- `not name` should be enough, but LuaJIT 2.0.0 incorrectly reports `(*temporary)` names here
|
|---|
| 446 | if not name or name ~= "(*vararg)" then break end
|
|---|
| 447 | vars['...'][i] = value
|
|---|
| 448 | i = i + 1
|
|---|
| 449 | end
|
|---|
| 450 | -- returned 'vars' table plays a dual role: (1) it captures local values
|
|---|
| 451 | -- and upvalues to be restored later (in case they are modified in "eval"),
|
|---|
| 452 | -- and (2) it provides an environment for evaluated chunks.
|
|---|
| 453 | -- getfenv(func) is needed to provide proper environment for functions,
|
|---|
| 454 | -- including access to globals, but this causes vars[name] to fail in
|
|---|
| 455 | -- restore_vars on local variables or upvalues with `nil` values when
|
|---|
| 456 | -- 'strict' is in effect. To avoid this `rawget` is used in restore_vars.
|
|---|
| 457 | setmetatable(vars, { __index = getfenv(func), __newindex = getfenv(func) })
|
|---|
| 458 | return vars
|
|---|
| 459 | end
|
|---|
| 460 |
|
|---|
| 461 | local function stack_depth(start_depth)
|
|---|
| 462 | for i = start_depth, 0, -1 do
|
|---|
| 463 | if debug.getinfo(i, "l") then return i+1 end
|
|---|
| 464 | end
|
|---|
| 465 | return start_depth
|
|---|
| 466 | end
|
|---|
| 467 |
|
|---|
| 468 | local function is_safe(stack_level)
|
|---|
| 469 | -- the stack grows up: 0 is getinfo, 1 is is_safe, 2 is debug_hook, 3 is user function
|
|---|
| 470 | if stack_level == 3 then return true end
|
|---|
| 471 | for i = 3, stack_level do
|
|---|
| 472 | -- return if it is not safe to abort
|
|---|
| 473 | local info = debug.getinfo(i, "S")
|
|---|
| 474 | if not info then return true end
|
|---|
| 475 | if info.what == "C" then return false end
|
|---|
| 476 | end
|
|---|
| 477 | return true
|
|---|
| 478 | end
|
|---|
| 479 |
|
|---|
| 480 | local function in_debugger()
|
|---|
| 481 | local this = debug.getinfo(1, "S").source
|
|---|
| 482 | -- only need to check few frames as mobdebug frames should be close
|
|---|
| 483 | for i = 3, 7 do
|
|---|
| 484 | local info = debug.getinfo(i, "S")
|
|---|
| 485 | if not info then return false end
|
|---|
| 486 | if info.source == this then return true end
|
|---|
| 487 | end
|
|---|
| 488 | return false
|
|---|
| 489 | end
|
|---|
| 490 |
|
|---|
| 491 | local function is_pending(peer)
|
|---|
| 492 | -- if there is something already in the buffer, skip check
|
|---|
| 493 | if not buf and checkcount >= mobdebug.checkcount then
|
|---|
| 494 | peer:settimeout(0) -- non-blocking
|
|---|
| 495 | buf = peer:receive(1)
|
|---|
| 496 | peer:settimeout() -- back to blocking
|
|---|
| 497 | checkcount = 0
|
|---|
| 498 | end
|
|---|
| 499 | return buf
|
|---|
| 500 | end
|
|---|
| 501 |
|
|---|
| 502 | local function readnext(peer, num)
|
|---|
| 503 | peer:settimeout(0) -- non-blocking
|
|---|
| 504 | local res, err, partial = peer:receive(num)
|
|---|
| 505 | peer:settimeout() -- back to blocking
|
|---|
| 506 | return res or partial or '', err
|
|---|
| 507 | end
|
|---|
| 508 |
|
|---|
| 509 | local function handle_breakpoint(peer)
|
|---|
| 510 | -- check if the buffer has the beginning of SETB/DELB command;
|
|---|
| 511 | -- this is to avoid reading the entire line for commands that
|
|---|
| 512 | -- don't need to be handled here.
|
|---|
| 513 | if not buf or not (buf:sub(1,1) == 'S' or buf:sub(1,1) == 'D') then return end
|
|---|
| 514 |
|
|---|
| 515 | -- check second character to avoid reading STEP or other S* and D* commands
|
|---|
| 516 | if #buf == 1 then buf = buf .. readnext(peer, 1) end
|
|---|
| 517 | if buf:sub(2,2) ~= 'E' then return end
|
|---|
| 518 |
|
|---|
| 519 | -- need to read few more characters
|
|---|
| 520 | buf = buf .. readnext(peer, 5-#buf)
|
|---|
| 521 | if buf ~= 'SETB ' and buf ~= 'DELB ' then return end
|
|---|
| 522 |
|
|---|
| 523 | local res, _, partial = peer:receive() -- get the rest of the line; blocking
|
|---|
| 524 | if not res then
|
|---|
| 525 | if partial then buf = buf .. partial end
|
|---|
| 526 | return
|
|---|
| 527 | end
|
|---|
| 528 |
|
|---|
| 529 | local _, _, cmd, file, line = (buf..res):find("^([A-Z]+)%s+(.-)%s+(%d+)%s*$")
|
|---|
| 530 | if cmd == 'SETB' then set_breakpoint(file, tonumber(line))
|
|---|
| 531 | elseif cmd == 'DELB' then remove_breakpoint(file, tonumber(line))
|
|---|
| 532 | else
|
|---|
| 533 | -- this looks like a breakpoint command, but something went wrong;
|
|---|
| 534 | -- return here to let the "normal" processing to handle,
|
|---|
| 535 | -- although this is likely to not go well.
|
|---|
| 536 | return
|
|---|
| 537 | end
|
|---|
| 538 |
|
|---|
| 539 | buf = nil
|
|---|
| 540 | end
|
|---|
| 541 |
|
|---|
| 542 | local function normalize_path(file)
|
|---|
| 543 | local n
|
|---|
| 544 | repeat
|
|---|
| 545 | file, n = file:gsub("/+%.?/+","/") -- remove all `//` and `/./` references
|
|---|
| 546 | until n == 0
|
|---|
| 547 | -- collapse all up-dir references: this will clobber UNC prefix (\\?\)
|
|---|
| 548 | -- and disk on Windows when there are too many up-dir references: `D:\foo\..\..\bar`;
|
|---|
| 549 | -- handle the case of multiple up-dir references: `foo/bar/baz/../../../more`;
|
|---|
| 550 | -- only remove one at a time as otherwise `../../` could be removed;
|
|---|
| 551 | repeat
|
|---|
| 552 | file, n = file:gsub("[^/]+/%.%./", "", 1)
|
|---|
| 553 | until n == 0
|
|---|
| 554 | -- there may still be a leading up-dir reference left (as `/../` or `../`); remove it
|
|---|
| 555 | return (file:gsub("^(/?)%.%./", "%1"))
|
|---|
| 556 | end
|
|---|
| 557 |
|
|---|
| 558 | local function debug_hook(event, line)
|
|---|
| 559 | -- (1) LuaJIT needs special treatment. Because debug_hook is set for
|
|---|
| 560 | -- *all* coroutines, and not just the one being debugged as in regular Lua
|
|---|
| 561 | -- (http://lua-users.org/lists/lua-l/2011-06/msg00513.html),
|
|---|
| 562 | -- need to avoid debugging mobdebug's own code as LuaJIT doesn't
|
|---|
| 563 | -- always correctly generate call/return hook events (there are more
|
|---|
| 564 | -- calls than returns, which breaks stack depth calculation and
|
|---|
| 565 | -- 'step' and 'step over' commands stop working; possibly because
|
|---|
| 566 | -- 'tail return' events are not generated by LuaJIT).
|
|---|
| 567 | -- the next line checks if the debugger is run under LuaJIT and if
|
|---|
| 568 | -- one of debugger methods is present in the stack, it simply returns.
|
|---|
| 569 | if jit then
|
|---|
| 570 | -- when luajit is compiled with LUAJIT_ENABLE_LUA52COMPAT,
|
|---|
| 571 | -- coroutine.running() returns non-nil for the main thread.
|
|---|
| 572 | local coro, main = coroutine.running()
|
|---|
| 573 | if not coro or main then coro = 'main' end
|
|---|
| 574 | local disabled = coroutines[coro] == false
|
|---|
| 575 | or coroutines[coro] == nil and coro ~= (coro_debugee or 'main')
|
|---|
| 576 | if coro_debugee and disabled or not coro_debugee and (disabled or in_debugger())
|
|---|
| 577 | then return end
|
|---|
| 578 | end
|
|---|
| 579 |
|
|---|
| 580 | -- (2) check if abort has been requested and it's safe to abort
|
|---|
| 581 | if abort and is_safe(stack_level) then error(abort) end
|
|---|
| 582 |
|
|---|
| 583 | -- (3) also check if this debug hook has not been visited for any reason.
|
|---|
| 584 | -- this check is needed to avoid stepping in too early
|
|---|
| 585 | -- (for example, when coroutine.resume() is executed inside start()).
|
|---|
| 586 | if not seen_hook and in_debugger() then return end
|
|---|
| 587 |
|
|---|
| 588 | if event == "call" then
|
|---|
| 589 | stack_level = stack_level + 1
|
|---|
| 590 | elseif event == "return" or event == "tail return" then
|
|---|
| 591 | stack_level = stack_level - 1
|
|---|
| 592 | elseif event == "line" then
|
|---|
| 593 | if mobdebug.linemap then
|
|---|
| 594 | local ok, mappedline = pcall(mobdebug.linemap, line, debug.getinfo(2, "S").source)
|
|---|
| 595 | if ok then line = mappedline end
|
|---|
| 596 | if not line then return end
|
|---|
| 597 | end
|
|---|
| 598 |
|
|---|
| 599 | -- may need to fall through because of the following:
|
|---|
| 600 | -- (1) step_into
|
|---|
| 601 | -- (2) step_over and stack_level <= step_level (need stack_level)
|
|---|
| 602 | -- (3) breakpoint; check for line first as it's known; then for file
|
|---|
| 603 | -- (4) socket call (only do every Xth check)
|
|---|
| 604 | -- (5) at least one watch is registered
|
|---|
| 605 | if not (
|
|---|
| 606 | step_into or step_over or breakpoints[line] or watchescnt > 0
|
|---|
| 607 | or is_pending(server)
|
|---|
| 608 | ) then checkcount = checkcount + 1; return end
|
|---|
| 609 |
|
|---|
| 610 | checkcount = mobdebug.checkcount -- force check on the next command
|
|---|
| 611 |
|
|---|
| 612 | -- this is needed to check if the stack got shorter or longer.
|
|---|
| 613 | -- unfortunately counting call/return calls is not reliable.
|
|---|
| 614 | -- the discrepancy may happen when "pcall(load, '')" call is made
|
|---|
| 615 | -- or when "error()" is called in a function.
|
|---|
| 616 | -- in either case there are more "call" than "return" events reported.
|
|---|
| 617 | -- this validation is done for every "line" event, but should be "cheap"
|
|---|
| 618 | -- as it checks for the stack to get shorter (or longer by one call).
|
|---|
| 619 | -- start from one level higher just in case we need to grow the stack.
|
|---|
| 620 | -- this may happen after coroutine.resume call to a function that doesn't
|
|---|
| 621 | -- have any other instructions to execute. it triggers three returns:
|
|---|
| 622 | -- "return, tail return, return", which needs to be accounted for.
|
|---|
| 623 | stack_level = stack_depth(stack_level+1)
|
|---|
| 624 |
|
|---|
| 625 | local caller = debug.getinfo(2, "S")
|
|---|
| 626 |
|
|---|
| 627 | -- grab the filename and fix it if needed
|
|---|
| 628 | local file = lastfile
|
|---|
| 629 | if (lastsource ~= caller.source) then
|
|---|
| 630 | file, lastsource = caller.source, caller.source
|
|---|
| 631 | -- technically, users can supply names that may not use '@',
|
|---|
| 632 | -- for example when they call loadstring('...', 'filename.lua').
|
|---|
| 633 | -- Unfortunately, there is no reliable/quick way to figure out
|
|---|
| 634 | -- what is the filename and what is the source code.
|
|---|
| 635 | -- If the name doesn't start with `@`, assume it's a file name if it's all on one line.
|
|---|
| 636 | if find(file, "^@") or not find(file, "[\r\n]") then
|
|---|
| 637 | file = gsub(gsub(file, "^@", ""), "\\", "/")
|
|---|
| 638 | -- normalize paths that may include up-dir or same-dir references
|
|---|
| 639 | -- if the path starts from the up-dir or reference,
|
|---|
| 640 | -- prepend `basedir` to generate absolute path to keep breakpoints working.
|
|---|
| 641 | -- ignore qualified relative path (`D:../`) and UNC paths (`\\?\`)
|
|---|
| 642 | if find(file, "^%.%./") then file = basedir..file end
|
|---|
| 643 | if find(file, "/%.%.?/") then file = normalize_path(file) end
|
|---|
| 644 | -- need this conversion to be applied to relative and absolute
|
|---|
| 645 | -- file names as you may write "require 'Foo'" to
|
|---|
| 646 | -- load "foo.lua" (on a case insensitive file system) and breakpoints
|
|---|
| 647 | -- set on foo.lua will not work if not converted to the same case.
|
|---|
| 648 | if iscasepreserving then file = string.lower(file) end
|
|---|
| 649 | if find(file, "^%./") then file = sub(file, 3)
|
|---|
| 650 | else file = gsub(file, "^"..q(basedir), "") end
|
|---|
| 651 | -- some file systems allow newlines in file names; remove these.
|
|---|
| 652 | file = gsub(file, "\n", ' ')
|
|---|
| 653 | else
|
|---|
| 654 | file = mobdebug.line(file)
|
|---|
| 655 | end
|
|---|
| 656 |
|
|---|
| 657 | -- set to true if we got here; this only needs to be done once per
|
|---|
| 658 | -- session, so do it here to at least avoid setting it for every line.
|
|---|
| 659 | seen_hook = true
|
|---|
| 660 | lastfile = file
|
|---|
| 661 | end
|
|---|
| 662 |
|
|---|
| 663 | if is_pending(server) then handle_breakpoint(server) end
|
|---|
| 664 |
|
|---|
| 665 | local vars, status, res
|
|---|
| 666 | if (watchescnt > 0) then
|
|---|
| 667 | vars = capture_vars(1)
|
|---|
| 668 | for index, value in pairs(watches) do
|
|---|
| 669 | setfenv(value, vars)
|
|---|
| 670 | local ok, fired = pcall(value)
|
|---|
| 671 | if ok and fired then
|
|---|
| 672 | status, res = cororesume(coro_debugger, events.WATCH, vars, file, line, index)
|
|---|
| 673 | break -- any one watch is enough; don't check multiple times
|
|---|
| 674 | end
|
|---|
| 675 | end
|
|---|
| 676 | end
|
|---|
| 677 |
|
|---|
| 678 | -- need to get into the "regular" debug handler, but only if there was
|
|---|
| 679 | -- no watch that was fired. If there was a watch, handle its result.
|
|---|
| 680 | local getin = (status == nil) and
|
|---|
| 681 | (step_into
|
|---|
| 682 | -- when coroutine.running() return `nil` (main thread in Lua 5.1),
|
|---|
| 683 | -- step_over will equal 'main', so need to check for that explicitly.
|
|---|
| 684 | or (step_over and step_over == (coroutine.running() or 'main') and stack_level <= step_level)
|
|---|
| 685 | or has_breakpoint(file, line)
|
|---|
| 686 | or is_pending(server))
|
|---|
| 687 |
|
|---|
| 688 | if getin then
|
|---|
| 689 | vars = vars or capture_vars(1)
|
|---|
| 690 | step_into = false
|
|---|
| 691 | step_over = false
|
|---|
| 692 | status, res = cororesume(coro_debugger, events.BREAK, vars, file, line)
|
|---|
| 693 | end
|
|---|
| 694 |
|
|---|
| 695 | -- handle 'stack' command that provides stack() information to the debugger
|
|---|
| 696 | while status and res == 'stack' do
|
|---|
| 697 | -- resume with the stack trace and variables
|
|---|
| 698 | if vars then restore_vars(vars) end -- restore vars so they are reflected in stack values
|
|---|
| 699 | status, res = cororesume(coro_debugger, events.STACK, stack(3), file, line)
|
|---|
| 700 | end
|
|---|
| 701 |
|
|---|
| 702 | -- need to recheck once more as resume after 'stack' command may
|
|---|
| 703 | -- return something else (for example, 'exit'), which needs to be handled
|
|---|
| 704 | if status and res and res ~= 'stack' then
|
|---|
| 705 | if not abort and res == "exit" then mobdebug.onexit(1, true); return end
|
|---|
| 706 | if not abort and res == "done" then mobdebug.done(); return end
|
|---|
| 707 | abort = res
|
|---|
| 708 | -- only abort if safe; if not, there is another (earlier) check inside
|
|---|
| 709 | -- debug_hook, which will abort execution at the first safe opportunity
|
|---|
| 710 | if is_safe(stack_level) then error(abort) end
|
|---|
| 711 | elseif not status and res then
|
|---|
| 712 | error(res, 2) -- report any other (internal) errors back to the application
|
|---|
| 713 | end
|
|---|
| 714 |
|
|---|
| 715 | if vars then restore_vars(vars) end
|
|---|
| 716 |
|
|---|
| 717 | -- last command requested Step Over/Out; store the current thread
|
|---|
| 718 | if step_over == true then step_over = coroutine.running() or 'main' end
|
|---|
| 719 | end
|
|---|
| 720 | end
|
|---|
| 721 |
|
|---|
| 722 | local function stringify_results(params, status, ...)
|
|---|
| 723 | if not status then return status, ... end -- on error report as it
|
|---|
| 724 |
|
|---|
| 725 | params = params or {}
|
|---|
| 726 | if params.nocode == nil then params.nocode = true end
|
|---|
| 727 | if params.comment == nil then params.comment = 1 end
|
|---|
| 728 |
|
|---|
| 729 | local t = {...}
|
|---|
| 730 | for i,v in pairs(t) do -- stringify each of the returned values
|
|---|
| 731 | local ok, res = pcall(mobdebug.line, v, params)
|
|---|
| 732 | t[i] = ok and res or ("%q"):format(res):gsub("\010","n"):gsub("\026","\\026")
|
|---|
| 733 | end
|
|---|
| 734 | -- stringify table with all returned values
|
|---|
| 735 | -- this is done to allow each returned value to be used (serialized or not)
|
|---|
| 736 | -- intependently and to preserve "original" comments
|
|---|
| 737 | return pcall(mobdebug.dump, t, {sparse = false})
|
|---|
| 738 | end
|
|---|
| 739 |
|
|---|
| 740 | local function isrunning()
|
|---|
| 741 | return coro_debugger and (corostatus(coro_debugger) == 'suspended' or corostatus(coro_debugger) == 'running')
|
|---|
| 742 | end
|
|---|
| 743 |
|
|---|
| 744 | -- this is a function that removes all hooks and closes the socket to
|
|---|
| 745 | -- report back to the controller that the debugging is done.
|
|---|
| 746 | -- the script that called `done` can still continue.
|
|---|
| 747 | local function done()
|
|---|
| 748 | if not (isrunning() and server) then return end
|
|---|
| 749 |
|
|---|
| 750 | if not jit then
|
|---|
| 751 | for co, debugged in pairs(coroutines) do
|
|---|
| 752 | if debugged then debug.sethook(co) end
|
|---|
| 753 | end
|
|---|
| 754 | end
|
|---|
| 755 |
|
|---|
| 756 | debug.sethook()
|
|---|
| 757 | server:close()
|
|---|
| 758 |
|
|---|
| 759 | coro_debugger = nil -- to make sure isrunning() returns `false`
|
|---|
| 760 | seen_hook = nil -- to make sure that the next start() call works
|
|---|
| 761 | abort = nil -- to make sure that callback calls use proper "abort" value
|
|---|
| 762 | end
|
|---|
| 763 |
|
|---|
| 764 | local function debugger_loop(sev, svars, sfile, sline)
|
|---|
| 765 | local command
|
|---|
| 766 | local app, osname
|
|---|
| 767 | local eval_env = svars or {}
|
|---|
| 768 | local function emptyWatch () return false end
|
|---|
| 769 | local loaded = {}
|
|---|
| 770 | for k in pairs(package.loaded) do loaded[k] = true end
|
|---|
| 771 |
|
|---|
| 772 | while true do
|
|---|
| 773 | local line, err
|
|---|
| 774 | local wx = rawget(genv, "wx") -- use rawread to make strict.lua happy
|
|---|
| 775 | if (wx or mobdebug.yield) and server.settimeout then server:settimeout(mobdebug.yieldtimeout) end
|
|---|
| 776 | while true do
|
|---|
| 777 | line, err = server:receive()
|
|---|
| 778 | if not line and err == "timeout" then
|
|---|
| 779 | -- yield for wx GUI applications if possible to avoid "busyness"
|
|---|
| 780 | app = app or (wx and wx.wxGetApp and wx.wxGetApp())
|
|---|
| 781 | if app then
|
|---|
| 782 | local win = app:GetTopWindow()
|
|---|
| 783 | local inloop = app:IsMainLoopRunning()
|
|---|
| 784 | osname = osname or wx.wxPlatformInfo.Get():GetOperatingSystemFamilyName()
|
|---|
| 785 | if win and not inloop then
|
|---|
| 786 | -- process messages in a regular way
|
|---|
| 787 | -- and exit as soon as the event loop is idle
|
|---|
| 788 | if osname == 'Unix' then wx.wxTimer(app):Start(10, true) end
|
|---|
| 789 | local exitLoop = function()
|
|---|
| 790 | win:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_IDLE)
|
|---|
| 791 | win:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_TIMER)
|
|---|
| 792 | app:ExitMainLoop()
|
|---|
| 793 | end
|
|---|
| 794 | win:Connect(wx.wxEVT_IDLE, exitLoop)
|
|---|
| 795 | win:Connect(wx.wxEVT_TIMER, exitLoop)
|
|---|
| 796 | app:MainLoop()
|
|---|
| 797 | end
|
|---|
| 798 | elseif mobdebug.yield then mobdebug.yield()
|
|---|
| 799 | end
|
|---|
| 800 | elseif not line and err == "closed" then
|
|---|
| 801 | error("Debugger connection closed", 0)
|
|---|
| 802 | else
|
|---|
| 803 | -- if there is something in the pending buffer, prepend it to the line
|
|---|
| 804 | if buf then line = buf .. line; buf = nil end
|
|---|
| 805 | break
|
|---|
| 806 | end
|
|---|
| 807 | end
|
|---|
| 808 | if server.settimeout then server:settimeout() end -- back to blocking
|
|---|
| 809 | command = string.sub(line, string.find(line, "^[A-Z]+"))
|
|---|
| 810 | if command == "SETB" then
|
|---|
| 811 | local _, _, _, file, line = string.find(line, "^([A-Z]+)%s+(.-)%s+(%d+)%s*$")
|
|---|
| 812 | if file and line then
|
|---|
| 813 | set_breakpoint(file, tonumber(line))
|
|---|
| 814 | server:send("200 OK\n")
|
|---|
| 815 | else
|
|---|
| 816 | server:send("400 Bad Request\n")
|
|---|
| 817 | end
|
|---|
| 818 | elseif command == "DELB" then
|
|---|
| 819 | local _, _, _, file, line = string.find(line, "^([A-Z]+)%s+(.-)%s+(%d+)%s*$")
|
|---|
| 820 | if file and line then
|
|---|
| 821 | remove_breakpoint(file, tonumber(line))
|
|---|
| 822 | server:send("200 OK\n")
|
|---|
| 823 | else
|
|---|
| 824 | server:send("400 Bad Request\n")
|
|---|
| 825 | end
|
|---|
| 826 | elseif command == "EXEC" then
|
|---|
| 827 | -- extract any optional parameters
|
|---|
| 828 | local params = string.match(line, "--%s*(%b{})%s*$")
|
|---|
| 829 | local _, _, chunk = string.find(line, "^[A-Z]+%s+(.+)$")
|
|---|
| 830 | if chunk then
|
|---|
| 831 | local func, res = mobdebug.loadstring(chunk)
|
|---|
| 832 | local status
|
|---|
| 833 | if func then
|
|---|
| 834 | local pfunc = params and loadstring("return "..params) -- use internal function
|
|---|
| 835 | params = pfunc and pfunc()
|
|---|
| 836 | params = (type(params) == "table" and params or {})
|
|---|
| 837 | local stack = tonumber(params.stack)
|
|---|
| 838 | -- if the requested stack frame is not the current one, then use a new capture
|
|---|
| 839 | -- with a specific stack frame: `capture_vars(0, coro_debugee)`
|
|---|
| 840 | local env = stack and coro_debugee and capture_vars(stack-1, coro_debugee) or eval_env
|
|---|
| 841 | setfenv(func, env)
|
|---|
| 842 | status, res = stringify_results(params, pcall(func, unpack(env['...'] or {})))
|
|---|
| 843 | end
|
|---|
| 844 | if status then
|
|---|
| 845 | if mobdebug.onscratch then mobdebug.onscratch(res) end
|
|---|
| 846 | server:send("200 OK " .. tostring(#res) .. "\n")
|
|---|
| 847 | server:send(res)
|
|---|
| 848 | else
|
|---|
| 849 | -- fix error if not set (for example, when loadstring is not present)
|
|---|
| 850 | if not res then res = "Unknown error" end
|
|---|
| 851 | server:send("401 Error in Expression " .. tostring(#res) .. "\n")
|
|---|
| 852 | server:send(res)
|
|---|
| 853 | end
|
|---|
| 854 | else
|
|---|
| 855 | server:send("400 Bad Request\n")
|
|---|
| 856 | end
|
|---|
| 857 | elseif command == "LOAD" then
|
|---|
| 858 | local _, _, size, name = string.find(line, "^[A-Z]+%s+(%d+)%s+(%S.-)%s*$")
|
|---|
| 859 | size = tonumber(size)
|
|---|
| 860 |
|
|---|
| 861 | if abort == nil then -- no LOAD/RELOAD allowed inside start()
|
|---|
| 862 | if size > 0 then server:receive(size) end
|
|---|
| 863 | if sfile and sline then
|
|---|
| 864 | server:send("201 Started " .. sfile .. " " .. tostring(sline) .. "\n")
|
|---|
| 865 | else
|
|---|
| 866 | server:send("200 OK 0\n")
|
|---|
| 867 | end
|
|---|
| 868 | else
|
|---|
| 869 | -- reset environment to allow required modules to load again
|
|---|
| 870 | -- remove those packages that weren't loaded when debugger started
|
|---|
| 871 | for k in pairs(package.loaded) do
|
|---|
| 872 | if not loaded[k] then package.loaded[k] = nil end
|
|---|
| 873 | end
|
|---|
| 874 |
|
|---|
| 875 | if size == 0 and name == '-' then -- RELOAD the current script being debugged
|
|---|
| 876 | server:send("200 OK 0\n")
|
|---|
| 877 | coroyield("load")
|
|---|
| 878 | else
|
|---|
| 879 | -- receiving 0 bytes blocks (at least in luasocket 2.0.2), so skip reading
|
|---|
| 880 | local chunk = size == 0 and "" or server:receive(size)
|
|---|
| 881 | if chunk then -- LOAD a new script for debugging
|
|---|
| 882 | local func, res = mobdebug.loadstring(chunk, "@"..name)
|
|---|
| 883 | if func then
|
|---|
| 884 | server:send("200 OK 0\n")
|
|---|
| 885 | debugee = func
|
|---|
| 886 | coroyield("load")
|
|---|
| 887 | else
|
|---|
| 888 | server:send("401 Error in Expression " .. tostring(#res) .. "\n")
|
|---|
| 889 | server:send(res)
|
|---|
| 890 | end
|
|---|
| 891 | else
|
|---|
| 892 | server:send("400 Bad Request\n")
|
|---|
| 893 | end
|
|---|
| 894 | end
|
|---|
| 895 | end
|
|---|
| 896 | elseif command == "SETW" then
|
|---|
| 897 | local _, _, exp = string.find(line, "^[A-Z]+%s+(.+)%s*$")
|
|---|
| 898 | if exp then
|
|---|
| 899 | local func, res = mobdebug.loadstring("return(" .. exp .. ")")
|
|---|
| 900 | if func then
|
|---|
| 901 | watchescnt = watchescnt + 1
|
|---|
| 902 | local newidx = #watches + 1
|
|---|
| 903 | watches[newidx] = func
|
|---|
| 904 | server:send("200 OK " .. tostring(newidx) .. "\n")
|
|---|
| 905 | else
|
|---|
| 906 | server:send("401 Error in Expression " .. tostring(#res) .. "\n")
|
|---|
| 907 | server:send(res)
|
|---|
| 908 | end
|
|---|
| 909 | else
|
|---|
| 910 | server:send("400 Bad Request\n")
|
|---|
| 911 | end
|
|---|
| 912 | elseif command == "DELW" then
|
|---|
| 913 | local _, _, index = string.find(line, "^[A-Z]+%s+(%d+)%s*$")
|
|---|
| 914 | index = tonumber(index)
|
|---|
| 915 | if index > 0 and index <= #watches then
|
|---|
| 916 | watchescnt = watchescnt - (watches[index] ~= emptyWatch and 1 or 0)
|
|---|
| 917 | watches[index] = emptyWatch
|
|---|
| 918 | server:send("200 OK\n")
|
|---|
| 919 | else
|
|---|
| 920 | server:send("400 Bad Request\n")
|
|---|
| 921 | end
|
|---|
| 922 | elseif command == "RUN" then
|
|---|
| 923 | server:send("200 OK\n")
|
|---|
| 924 |
|
|---|
| 925 | local ev, vars, file, line, idx_watch = coroyield()
|
|---|
| 926 | eval_env = vars
|
|---|
| 927 | if ev == events.BREAK then
|
|---|
| 928 | server:send("202 Paused " .. file .. " " .. tostring(line) .. "\n")
|
|---|
| 929 | elseif ev == events.WATCH then
|
|---|
| 930 | server:send("203 Paused " .. file .. " " .. tostring(line) .. " " .. tostring(idx_watch) .. "\n")
|
|---|
| 931 | elseif ev == events.RESTART then
|
|---|
| 932 | -- nothing to do
|
|---|
| 933 | else
|
|---|
| 934 | server:send("401 Error in Execution " .. tostring(#file) .. "\n")
|
|---|
| 935 | server:send(file)
|
|---|
| 936 | end
|
|---|
| 937 | elseif command == "STEP" then
|
|---|
| 938 | server:send("200 OK\n")
|
|---|
| 939 | step_into = true
|
|---|
| 940 |
|
|---|
| 941 | local ev, vars, file, line, idx_watch = coroyield()
|
|---|
| 942 | eval_env = vars
|
|---|
| 943 | if ev == events.BREAK then
|
|---|
| 944 | server:send("202 Paused " .. file .. " " .. tostring(line) .. "\n")
|
|---|
| 945 | elseif ev == events.WATCH then
|
|---|
| 946 | server:send("203 Paused " .. file .. " " .. tostring(line) .. " " .. tostring(idx_watch) .. "\n")
|
|---|
| 947 | elseif ev == events.RESTART then
|
|---|
| 948 | -- nothing to do
|
|---|
| 949 | else
|
|---|
| 950 | server:send("401 Error in Execution " .. tostring(#file) .. "\n")
|
|---|
| 951 | server:send(file)
|
|---|
| 952 | end
|
|---|
| 953 | elseif command == "OVER" or command == "OUT" then
|
|---|
| 954 | server:send("200 OK\n")
|
|---|
| 955 | step_over = true
|
|---|
| 956 |
|
|---|
| 957 | -- OVER and OUT are very similar except for
|
|---|
| 958 | -- the stack level value at which to stop
|
|---|
| 959 | if command == "OUT" then step_level = stack_level - 1
|
|---|
| 960 | else step_level = stack_level end
|
|---|
| 961 |
|
|---|
| 962 | local ev, vars, file, line, idx_watch = coroyield()
|
|---|
| 963 | eval_env = vars
|
|---|
| 964 | if ev == events.BREAK then
|
|---|
| 965 | server:send("202 Paused " .. file .. " " .. tostring(line) .. "\n")
|
|---|
| 966 | elseif ev == events.WATCH then
|
|---|
| 967 | server:send("203 Paused " .. file .. " " .. tostring(line) .. " " .. tostring(idx_watch) .. "\n")
|
|---|
| 968 | elseif ev == events.RESTART then
|
|---|
| 969 | -- nothing to do
|
|---|
| 970 | else
|
|---|
| 971 | server:send("401 Error in Execution " .. tostring(#file) .. "\n")
|
|---|
| 972 | server:send(file)
|
|---|
| 973 | end
|
|---|
| 974 | elseif command == "BASEDIR" then
|
|---|
| 975 | local _, _, dir = string.find(line, "^[A-Z]+%s+(.+)%s*$")
|
|---|
| 976 | if dir then
|
|---|
| 977 | basedir = iscasepreserving and string.lower(dir) or dir
|
|---|
| 978 | -- reset cached source as it may change with basedir
|
|---|
| 979 | lastsource = nil
|
|---|
| 980 | server:send("200 OK\n")
|
|---|
| 981 | else
|
|---|
| 982 | server:send("400 Bad Request\n")
|
|---|
| 983 | end
|
|---|
| 984 | elseif command == "SUSPEND" then
|
|---|
| 985 | -- do nothing; it already fulfilled its role
|
|---|
| 986 | elseif command == "DONE" then
|
|---|
| 987 | coroyield("done")
|
|---|
| 988 | return -- done with all the debugging
|
|---|
| 989 | elseif command == "STACK" then
|
|---|
| 990 | -- first check if we can execute the stack command
|
|---|
| 991 | -- as it requires yielding back to debug_hook it cannot be executed
|
|---|
| 992 | -- if we have not seen the hook yet as happens after start().
|
|---|
| 993 | -- in this case we simply return an empty result
|
|---|
| 994 | local vars, ev = {}
|
|---|
| 995 | if seen_hook then
|
|---|
| 996 | ev, vars = coroyield("stack")
|
|---|
| 997 | end
|
|---|
| 998 | if ev and ev ~= events.STACK then
|
|---|
| 999 | server:send("401 Error in Execution " .. tostring(#vars) .. "\n")
|
|---|
| 1000 | server:send(vars)
|
|---|
| 1001 | else
|
|---|
| 1002 | local params = string.match(line, "--%s*(%b{})%s*$")
|
|---|
| 1003 | local pfunc = params and loadstring("return "..params) -- use internal function
|
|---|
| 1004 | params = pfunc and pfunc()
|
|---|
| 1005 | params = (type(params) == "table" and params or {})
|
|---|
| 1006 | if params.nocode == nil then params.nocode = true end
|
|---|
| 1007 | if params.sparse == nil then params.sparse = false end
|
|---|
| 1008 | -- take into account additional levels for the stack frames and data management
|
|---|
| 1009 | if tonumber(params.maxlevel) then params.maxlevel = tonumber(params.maxlevel)+4 end
|
|---|
| 1010 |
|
|---|
| 1011 | local ok, res = pcall(mobdebug.dump, vars, params)
|
|---|
| 1012 | if ok then
|
|---|
| 1013 | server:send("200 OK " .. tostring(res) .. "\n")
|
|---|
| 1014 | else
|
|---|
| 1015 | server:send("401 Error in Execution " .. tostring(#res) .. "\n")
|
|---|
| 1016 | server:send(res)
|
|---|
| 1017 | end
|
|---|
| 1018 | end
|
|---|
| 1019 | elseif command == "OUTPUT" then
|
|---|
| 1020 | local _, _, stream, mode = string.find(line, "^[A-Z]+%s+(%w+)%s+([dcr])%s*$")
|
|---|
| 1021 | if stream and mode and stream == "stdout" then
|
|---|
| 1022 | -- assign "print" in the global environment
|
|---|
| 1023 | local default = mode == 'd'
|
|---|
| 1024 | genv.print = default and iobase.print or corowrap(function()
|
|---|
| 1025 | -- wrapping into coroutine.wrap protects this function from
|
|---|
| 1026 | -- being stepped through in the debugger.
|
|---|
| 1027 | -- don't use vararg (...) as it adds a reference for its values,
|
|---|
| 1028 | -- which may affect how they are garbage collected
|
|---|
| 1029 | while true do
|
|---|
| 1030 | local tbl = {coroutine.yield()}
|
|---|
| 1031 | if mode == 'c' then iobase.print(unpack(tbl)) end
|
|---|
| 1032 | for n = 1, #tbl do
|
|---|
| 1033 | tbl[n] = select(2, pcall(mobdebug.line, tbl[n], {nocode = true, comment = false})) end
|
|---|
| 1034 | local file = table.concat(tbl, "\t").."\n"
|
|---|
| 1035 | server:send("204 Output " .. stream .. " " .. tostring(#file) .. "\n" .. file)
|
|---|
| 1036 | end
|
|---|
| 1037 | end)
|
|---|
| 1038 | if not default then genv.print() end -- "fake" print to start printing loop
|
|---|
| 1039 | server:send("200 OK\n")
|
|---|
| 1040 | else
|
|---|
| 1041 | server:send("400 Bad Request\n")
|
|---|
| 1042 | end
|
|---|
| 1043 | elseif command == "EXIT" then
|
|---|
| 1044 | server:send("200 OK\n")
|
|---|
| 1045 | coroyield("exit")
|
|---|
| 1046 | else
|
|---|
| 1047 | server:send("400 Bad Request\n")
|
|---|
| 1048 | end
|
|---|
| 1049 | end
|
|---|
| 1050 | end
|
|---|
| 1051 |
|
|---|
| 1052 | local function output(stream, data)
|
|---|
| 1053 | if server then return server:send("204 Output "..stream.." "..tostring(#data).."\n"..data) end
|
|---|
| 1054 | end
|
|---|
| 1055 |
|
|---|
| 1056 | local function connect(controller_host, controller_port)
|
|---|
| 1057 | local sock, err = socket.tcp()
|
|---|
| 1058 | if not sock then return nil, err end
|
|---|
| 1059 |
|
|---|
| 1060 | if sock.settimeout then sock:settimeout(mobdebug.connecttimeout) end
|
|---|
| 1061 | local res, err = sock:connect(controller_host, tostring(controller_port))
|
|---|
| 1062 | if sock.settimeout then sock:settimeout() end
|
|---|
| 1063 |
|
|---|
| 1064 | if not res then return nil, err end
|
|---|
| 1065 | return sock
|
|---|
| 1066 | end
|
|---|
| 1067 |
|
|---|
| 1068 | local lasthost, lastport
|
|---|
| 1069 |
|
|---|
| 1070 | -- Starts a debug session by connecting to a controller
|
|---|
| 1071 | local function start(controller_host, controller_port)
|
|---|
| 1072 | -- only one debugging session can be run (as there is only one debug hook)
|
|---|
| 1073 | if isrunning() then return end
|
|---|
| 1074 |
|
|---|
| 1075 | lasthost = controller_host or lasthost
|
|---|
| 1076 | lastport = controller_port or lastport
|
|---|
| 1077 |
|
|---|
| 1078 | controller_host = lasthost or "localhost"
|
|---|
| 1079 | controller_port = lastport or mobdebug.port
|
|---|
| 1080 |
|
|---|
| 1081 | local err
|
|---|
| 1082 | server, err = mobdebug.connect(controller_host, controller_port)
|
|---|
| 1083 | if server then
|
|---|
| 1084 | -- correct stack depth which already has some calls on it
|
|---|
| 1085 | -- so it doesn't go into negative when those calls return
|
|---|
| 1086 | -- as this breaks subsequence checks in stack_depth().
|
|---|
| 1087 | -- start from 16th frame, which is sufficiently large for this check.
|
|---|
| 1088 | stack_level = stack_depth(16)
|
|---|
| 1089 |
|
|---|
| 1090 | -- provide our own traceback function to report errors remotely
|
|---|
| 1091 | -- but only under Lua 5.1/LuaJIT as it's not called under Lua 5.2+
|
|---|
| 1092 | -- (http://lua-users.org/lists/lua-l/2016-05/msg00297.html)
|
|---|
| 1093 | local function f() return function()end end
|
|---|
| 1094 | if f() ~= f() then -- Lua 5.1 or LuaJIT
|
|---|
| 1095 | local dtraceback = debug.traceback
|
|---|
| 1096 | debug.traceback = function (...)
|
|---|
| 1097 | if select('#', ...) >= 1 then
|
|---|
| 1098 | local thr, err, lvl = ...
|
|---|
| 1099 | if type(thr) ~= 'thread' then err, lvl = thr, err end
|
|---|
| 1100 | local trace = dtraceback(err, (lvl or 1)+1)
|
|---|
| 1101 | if genv.print == iobase.print then -- no remote redirect
|
|---|
| 1102 | return trace
|
|---|
| 1103 | else
|
|---|
| 1104 | genv.print(trace) -- report the error remotely
|
|---|
| 1105 | return -- don't report locally to avoid double reporting
|
|---|
| 1106 | end
|
|---|
| 1107 | end
|
|---|
| 1108 | -- direct call to debug.traceback: return the original.
|
|---|
| 1109 | -- debug.traceback(nil, level) doesn't work in Lua 5.1
|
|---|
| 1110 | -- (http://lua-users.org/lists/lua-l/2011-06/msg00574.html), so
|
|---|
| 1111 | -- simply remove first frame from the stack trace
|
|---|
| 1112 | local tb = dtraceback("", 2) -- skip debugger frames
|
|---|
| 1113 | -- if the string is returned, then remove the first new line as it's not needed
|
|---|
| 1114 | return type(tb) == "string" and tb:gsub("^\n","") or tb
|
|---|
| 1115 | end
|
|---|
| 1116 | end
|
|---|
| 1117 | coro_debugger = corocreate(debugger_loop)
|
|---|
| 1118 | debug.sethook(debug_hook, HOOKMASK)
|
|---|
| 1119 | seen_hook = nil -- reset in case the last start() call was refused
|
|---|
| 1120 | step_into = true -- start with step command
|
|---|
| 1121 | return true
|
|---|
| 1122 | else
|
|---|
| 1123 | print(("Could not connect to %s:%s: %s")
|
|---|
| 1124 | :format(controller_host, controller_port, err or "unknown error"))
|
|---|
| 1125 | end
|
|---|
| 1126 | end
|
|---|
| 1127 |
|
|---|
| 1128 | local function controller(controller_host, controller_port, scratchpad)
|
|---|
| 1129 | -- only one debugging session can be run (as there is only one debug hook)
|
|---|
| 1130 | if isrunning() then return end
|
|---|
| 1131 |
|
|---|
| 1132 | lasthost = controller_host or lasthost
|
|---|
| 1133 | lastport = controller_port or lastport
|
|---|
| 1134 |
|
|---|
| 1135 | controller_host = lasthost or "localhost"
|
|---|
| 1136 | controller_port = lastport or mobdebug.port
|
|---|
| 1137 |
|
|---|
| 1138 | local exitonerror = not scratchpad
|
|---|
| 1139 | local err
|
|---|
| 1140 | server, err = mobdebug.connect(controller_host, controller_port)
|
|---|
| 1141 | if server then
|
|---|
| 1142 | local function report(trace, err)
|
|---|
| 1143 | local msg = err .. "\n" .. trace
|
|---|
| 1144 | server:send("401 Error in Execution " .. tostring(#msg) .. "\n")
|
|---|
| 1145 | server:send(msg)
|
|---|
| 1146 | return err
|
|---|
| 1147 | end
|
|---|
| 1148 |
|
|---|
| 1149 | seen_hook = true -- allow to accept all commands
|
|---|
| 1150 | coro_debugger = corocreate(debugger_loop)
|
|---|
| 1151 |
|
|---|
| 1152 | while true do
|
|---|
| 1153 | step_into = true -- start with step command
|
|---|
| 1154 | abort = false -- reset abort flag from the previous loop
|
|---|
| 1155 | if scratchpad then checkcount = mobdebug.checkcount end -- force suspend right away
|
|---|
| 1156 |
|
|---|
| 1157 | coro_debugee = corocreate(debugee)
|
|---|
| 1158 | debug.sethook(coro_debugee, debug_hook, HOOKMASK)
|
|---|
| 1159 | local status, err = cororesume(coro_debugee, unpack(arg or {}))
|
|---|
| 1160 |
|
|---|
| 1161 | -- was there an error or is the script done?
|
|---|
| 1162 | -- 'abort' state is allowed here; ignore it
|
|---|
| 1163 | if abort then
|
|---|
| 1164 | if tostring(abort) == 'exit' then break end
|
|---|
| 1165 | else
|
|---|
| 1166 | if status then -- no errors
|
|---|
| 1167 | if corostatus(coro_debugee) == "suspended" then
|
|---|
| 1168 | -- the script called `coroutine.yield` in the "main" thread
|
|---|
| 1169 | error("attempt to yield from the main thread", 3)
|
|---|
| 1170 | end
|
|---|
| 1171 | break -- normal execution is done
|
|---|
| 1172 | elseif err and not string.find(tostring(err), deferror) then
|
|---|
| 1173 | -- report the error back
|
|---|
| 1174 | -- err is not necessarily a string, so convert to string to report
|
|---|
| 1175 | report(debug.traceback(coro_debugee), tostring(err))
|
|---|
| 1176 | if exitonerror then break end
|
|---|
| 1177 | -- check if the debugging is done (coro_debugger is nil)
|
|---|
| 1178 | if not coro_debugger then break end
|
|---|
| 1179 | -- resume once more to clear the response the debugger wants to send
|
|---|
| 1180 | -- need to use capture_vars(0) to capture only two (default) level,
|
|---|
| 1181 | -- as even though there is controller() call, because of the tail call,
|
|---|
| 1182 | -- the caller may not exist for it;
|
|---|
| 1183 | -- This is not entirely safe as the user may see the local
|
|---|
| 1184 | -- variable from console, but they will be reset anyway.
|
|---|
| 1185 | -- This functionality is used when scratchpad is paused to
|
|---|
| 1186 | -- gain access to remote console to modify global variables.
|
|---|
| 1187 | local status, err = cororesume(coro_debugger, events.RESTART, capture_vars(0))
|
|---|
| 1188 | if not status or status and err == "exit" then break end
|
|---|
| 1189 | end
|
|---|
| 1190 | end
|
|---|
| 1191 | end
|
|---|
| 1192 | else
|
|---|
| 1193 | print(("Could not connect to %s:%s: %s")
|
|---|
| 1194 | :format(controller_host, controller_port, err or "unknown error"))
|
|---|
| 1195 | return false
|
|---|
| 1196 | end
|
|---|
| 1197 | return true
|
|---|
| 1198 | end
|
|---|
| 1199 |
|
|---|
| 1200 | local function scratchpad(controller_host, controller_port)
|
|---|
| 1201 | return controller(controller_host, controller_port, true)
|
|---|
| 1202 | end
|
|---|
| 1203 |
|
|---|
| 1204 | local function loop(controller_host, controller_port)
|
|---|
| 1205 | return controller(controller_host, controller_port, false)
|
|---|
| 1206 | end
|
|---|
| 1207 |
|
|---|
| 1208 | local function on()
|
|---|
| 1209 | if not (isrunning() and server) then return end
|
|---|
| 1210 |
|
|---|
| 1211 | -- main is set to true under Lua5.2 for the "main" chunk.
|
|---|
| 1212 | -- Lua5.1 returns co as `nil` in that case.
|
|---|
| 1213 | local co, main = coroutine.running()
|
|---|
| 1214 | if main then co = nil end
|
|---|
| 1215 | if co then
|
|---|
| 1216 | coroutines[co] = true
|
|---|
| 1217 | debug.sethook(co, debug_hook, HOOKMASK)
|
|---|
| 1218 | else
|
|---|
| 1219 | if jit then coroutines.main = true end
|
|---|
| 1220 | debug.sethook(debug_hook, HOOKMASK)
|
|---|
| 1221 | end
|
|---|
| 1222 | end
|
|---|
| 1223 |
|
|---|
| 1224 | local function off()
|
|---|
| 1225 | if not (isrunning() and server) then return end
|
|---|
| 1226 |
|
|---|
| 1227 | -- main is set to true under Lua5.2 for the "main" chunk.
|
|---|
| 1228 | -- Lua5.1 returns co as `nil` in that case.
|
|---|
| 1229 | local co, main = coroutine.running()
|
|---|
| 1230 | if main then co = nil end
|
|---|
| 1231 |
|
|---|
| 1232 | -- don't remove coroutine hook under LuaJIT as there is only one (global) hook
|
|---|
| 1233 | if co then
|
|---|
| 1234 | coroutines[co] = false
|
|---|
| 1235 | if not jit then debug.sethook(co) end
|
|---|
| 1236 | else
|
|---|
| 1237 | if jit then coroutines.main = false end
|
|---|
| 1238 | if not jit then debug.sethook() end
|
|---|
| 1239 | end
|
|---|
| 1240 |
|
|---|
| 1241 | -- check if there is any thread that is still being debugged under LuaJIT;
|
|---|
| 1242 | -- if not, turn the debugging off
|
|---|
| 1243 | if jit then
|
|---|
| 1244 | local remove = true
|
|---|
| 1245 | for _, debugged in pairs(coroutines) do
|
|---|
| 1246 | if debugged then remove = false; break end
|
|---|
| 1247 | end
|
|---|
| 1248 | if remove then debug.sethook() end
|
|---|
| 1249 | end
|
|---|
| 1250 | end
|
|---|
| 1251 |
|
|---|
| 1252 | -- Handles server debugging commands
|
|---|
| 1253 | local function handle(params, client, options)
|
|---|
| 1254 | -- when `options.verbose` is not provided, use normal `print`; verbose output can be
|
|---|
| 1255 | -- disabled (`options.verbose == false`) or redirected (`options.verbose == function()...end`)
|
|---|
| 1256 | local verbose = not options or options.verbose ~= nil and options.verbose
|
|---|
| 1257 | local print = verbose and (type(verbose) == "function" and verbose or print) or function() end
|
|---|
| 1258 | local file, line, watch_idx
|
|---|
| 1259 | local _, _, command = string.find(params, "^([a-z]+)")
|
|---|
| 1260 | if command == "run" or command == "step" or command == "out"
|
|---|
| 1261 | or command == "over" or command == "exit" then
|
|---|
| 1262 | client:send(string.upper(command) .. "\n")
|
|---|
| 1263 | client:receive() -- this should consume the first '200 OK' response
|
|---|
| 1264 | while true do
|
|---|
| 1265 | local done = true
|
|---|
| 1266 | local breakpoint = client:receive()
|
|---|
| 1267 | if not breakpoint then
|
|---|
| 1268 | print("Program finished")
|
|---|
| 1269 | return nil, nil, false
|
|---|
| 1270 | end
|
|---|
| 1271 | local _, _, status = string.find(breakpoint, "^(%d+)")
|
|---|
| 1272 | if status == "200" then
|
|---|
| 1273 | -- don't need to do anything
|
|---|
| 1274 | elseif status == "202" then
|
|---|
| 1275 | _, _, file, line = string.find(breakpoint, "^202 Paused%s+(.-)%s+(%d+)%s*$")
|
|---|
| 1276 | if file and line then
|
|---|
| 1277 | print("Paused at file " .. file .. " line " .. line)
|
|---|
| 1278 | end
|
|---|
| 1279 | elseif status == "203" then
|
|---|
| 1280 | _, _, file, line, watch_idx = string.find(breakpoint, "^203 Paused%s+(.-)%s+(%d+)%s+(%d+)%s*$")
|
|---|
| 1281 | if file and line and watch_idx then
|
|---|
| 1282 | print("Paused at file " .. file .. " line " .. line .. " (watch expression " .. watch_idx .. ": [" .. watches[watch_idx] .. "])")
|
|---|
| 1283 | end
|
|---|
| 1284 | elseif status == "204" then
|
|---|
| 1285 | local _, _, stream, size = string.find(breakpoint, "^204 Output (%w+) (%d+)$")
|
|---|
| 1286 | if stream and size then
|
|---|
| 1287 | local size = tonumber(size)
|
|---|
| 1288 | local msg = size > 0 and client:receive(size) or ""
|
|---|
| 1289 | print(msg)
|
|---|
| 1290 | if outputs[stream] then outputs[stream](msg) end
|
|---|
| 1291 | -- this was just the output, so go back reading the response
|
|---|
| 1292 | done = false
|
|---|
| 1293 | end
|
|---|
| 1294 | elseif status == "401" then
|
|---|
| 1295 | local _, _, size = string.find(breakpoint, "^401 Error in Execution (%d+)$")
|
|---|
| 1296 | if size then
|
|---|
| 1297 | local msg = client:receive(tonumber(size))
|
|---|
| 1298 | print("Error in remote application: " .. msg)
|
|---|
| 1299 | return nil, nil, msg
|
|---|
| 1300 | end
|
|---|
| 1301 | else
|
|---|
| 1302 | print("Unknown error")
|
|---|
| 1303 | return nil, nil, "Debugger error: unexpected response '" .. breakpoint .. "'"
|
|---|
| 1304 | end
|
|---|
| 1305 | if done then break end
|
|---|
| 1306 | end
|
|---|
| 1307 | elseif command == "done" then
|
|---|
| 1308 | client:send(string.upper(command) .. "\n")
|
|---|
| 1309 | -- no response is expected
|
|---|
| 1310 | elseif command == "setb" or command == "asetb" then
|
|---|
| 1311 | _, _, _, file, line = string.find(params, "^([a-z]+)%s+(.-)%s+(%d+)%s*$")
|
|---|
| 1312 | if file and line then
|
|---|
| 1313 | -- if this is a file name, and not a file source
|
|---|
| 1314 | if not file:find('^".*"$') then
|
|---|
| 1315 | file = string.gsub(file, "\\", "/") -- convert slash
|
|---|
| 1316 | file = removebasedir(file, basedir)
|
|---|
| 1317 | end
|
|---|
| 1318 | client:send("SETB " .. file .. " " .. line .. "\n")
|
|---|
| 1319 | if command == "asetb" or client:receive() == "200 OK" then
|
|---|
| 1320 | set_breakpoint(file, line)
|
|---|
| 1321 | else
|
|---|
| 1322 | print("Error: breakpoint not inserted")
|
|---|
| 1323 | end
|
|---|
| 1324 | else
|
|---|
| 1325 | print("Invalid command")
|
|---|
| 1326 | end
|
|---|
| 1327 | elseif command == "setw" then
|
|---|
| 1328 | local _, _, exp = string.find(params, "^[a-z]+%s+(.+)$")
|
|---|
| 1329 | if exp then
|
|---|
| 1330 | client:send("SETW " .. exp .. "\n")
|
|---|
| 1331 | local answer = client:receive()
|
|---|
| 1332 | local _, _, watch_idx = string.find(answer, "^200 OK (%d+)%s*$")
|
|---|
| 1333 | if watch_idx then
|
|---|
| 1334 | watches[watch_idx] = exp
|
|---|
| 1335 | print("Inserted watch exp no. " .. watch_idx)
|
|---|
| 1336 | else
|
|---|
| 1337 | local _, _, size = string.find(answer, "^401 Error in Expression (%d+)$")
|
|---|
| 1338 | if size then
|
|---|
| 1339 | local err = client:receive(tonumber(size)):gsub(".-:%d+:%s*","")
|
|---|
| 1340 | print("Error: watch expression not set: " .. err)
|
|---|
| 1341 | else
|
|---|
| 1342 | print("Error: watch expression not set")
|
|---|
| 1343 | end
|
|---|
| 1344 | end
|
|---|
| 1345 | else
|
|---|
| 1346 | print("Invalid command")
|
|---|
| 1347 | end
|
|---|
| 1348 | elseif command == "delb" or command == "adelb" then
|
|---|
| 1349 | _, _, _, file, line = string.find(params, "^([a-z]+)%s+(.-)%s+(%d+)%s*$")
|
|---|
| 1350 | if file and line then
|
|---|
| 1351 | -- if this is a file name, and not a file source
|
|---|
| 1352 | if not file:find('^".*"$') then
|
|---|
| 1353 | file = string.gsub(file, "\\", "/") -- convert slash
|
|---|
| 1354 | file = removebasedir(file, basedir)
|
|---|
| 1355 | end
|
|---|
| 1356 | client:send("DELB " .. file .. " " .. line .. "\n")
|
|---|
| 1357 | if command == "adelb" or client:receive() == "200 OK" then
|
|---|
| 1358 | remove_breakpoint(file, line)
|
|---|
| 1359 | else
|
|---|
| 1360 | print("Error: breakpoint not removed")
|
|---|
| 1361 | end
|
|---|
| 1362 | else
|
|---|
| 1363 | print("Invalid command")
|
|---|
| 1364 | end
|
|---|
| 1365 | elseif command == "delallb" then
|
|---|
| 1366 | local file, line = "*", 0
|
|---|
| 1367 | client:send("DELB " .. file .. " " .. tostring(line) .. "\n")
|
|---|
| 1368 | if client:receive() == "200 OK" then
|
|---|
| 1369 | remove_breakpoint(file, line)
|
|---|
| 1370 | else
|
|---|
| 1371 | print("Error: all breakpoints not removed")
|
|---|
| 1372 | end
|
|---|
| 1373 | elseif command == "delw" then
|
|---|
| 1374 | local _, _, index = string.find(params, "^[a-z]+%s+(%d+)%s*$")
|
|---|
| 1375 | if index then
|
|---|
| 1376 | client:send("DELW " .. index .. "\n")
|
|---|
| 1377 | if client:receive() == "200 OK" then
|
|---|
| 1378 | watches[index] = nil
|
|---|
| 1379 | else
|
|---|
| 1380 | print("Error: watch expression not removed")
|
|---|
| 1381 | end
|
|---|
| 1382 | else
|
|---|
| 1383 | print("Invalid command")
|
|---|
| 1384 | end
|
|---|
| 1385 | elseif command == "delallw" then
|
|---|
| 1386 | for index, exp in pairs(watches) do
|
|---|
| 1387 | client:send("DELW " .. index .. "\n")
|
|---|
| 1388 | if client:receive() == "200 OK" then
|
|---|
| 1389 | watches[index] = nil
|
|---|
| 1390 | else
|
|---|
| 1391 | print("Error: watch expression at index " .. index .. " [" .. exp .. "] not removed")
|
|---|
| 1392 | end
|
|---|
| 1393 | end
|
|---|
| 1394 | elseif command == "eval" or command == "exec"
|
|---|
| 1395 | or command == "load" or command == "loadstring"
|
|---|
| 1396 | or command == "reload" then
|
|---|
| 1397 | local _, _, exp = string.find(params, "^[a-z]+%s+(.+)$")
|
|---|
| 1398 | if exp or (command == "reload") then
|
|---|
| 1399 | if command == "eval" or command == "exec" then
|
|---|
| 1400 | exp = (exp:gsub("%-%-%[(=*)%[.-%]%1%]", "") -- remove comments
|
|---|
| 1401 | :gsub("%-%-.-\n", " ") -- remove line comments
|
|---|
| 1402 | :gsub("\n", " ")) -- convert new lines
|
|---|
| 1403 | if command == "eval" then exp = "return " .. exp end
|
|---|
| 1404 | client:send("EXEC " .. exp .. "\n")
|
|---|
| 1405 | elseif command == "reload" then
|
|---|
| 1406 | client:send("LOAD 0 -\n")
|
|---|
| 1407 | elseif command == "loadstring" then
|
|---|
| 1408 | local _, _, _, file, lines = string.find(exp, "^([\"'])(.-)%1%s(.+)")
|
|---|
| 1409 | if not file then
|
|---|
| 1410 | _, _, file, lines = string.find(exp, "^(%S+)%s(.+)")
|
|---|
| 1411 | end
|
|---|
| 1412 | client:send("LOAD " .. tostring(#lines) .. " " .. file .. "\n")
|
|---|
| 1413 | client:send(lines)
|
|---|
| 1414 | else
|
|---|
| 1415 | local file = io.open(exp, "r")
|
|---|
| 1416 | if not file and pcall(require, "winapi") then
|
|---|
| 1417 | -- if file is not open and winapi is there, try with a short path;
|
|---|
| 1418 | -- this may be needed for unicode paths on windows
|
|---|
| 1419 | winapi.set_encoding(winapi.CP_UTF8)
|
|---|
| 1420 | local shortp = winapi.short_path(exp)
|
|---|
| 1421 | file = shortp and io.open(shortp, "r")
|
|---|
| 1422 | end
|
|---|
| 1423 | if not file then return nil, nil, "Cannot open file " .. exp end
|
|---|
| 1424 | -- read the file and remove the shebang line as it causes a compilation error
|
|---|
| 1425 | local lines = file:read("*all"):gsub("^#!.-\n", "\n")
|
|---|
| 1426 | file:close()
|
|---|
| 1427 |
|
|---|
| 1428 | local file = string.gsub(exp, "\\", "/") -- convert slash
|
|---|
| 1429 | file = removebasedir(file, basedir)
|
|---|
| 1430 | client:send("LOAD " .. tostring(#lines) .. " " .. file .. "\n")
|
|---|
| 1431 | if #lines > 0 then client:send(lines) end
|
|---|
| 1432 | end
|
|---|
| 1433 | while true do
|
|---|
| 1434 | local params, err = client:receive()
|
|---|
| 1435 | if not params then
|
|---|
| 1436 | return nil, nil, "Debugger connection " .. (err or "error")
|
|---|
| 1437 | end
|
|---|
| 1438 | local done = true
|
|---|
| 1439 | local _, _, status, len = string.find(params, "^(%d+).-%s+(%d+)%s*$")
|
|---|
| 1440 | if status == "200" then
|
|---|
| 1441 | len = tonumber(len)
|
|---|
| 1442 | if len > 0 then
|
|---|
| 1443 | local status, res
|
|---|
| 1444 | local str = client:receive(len)
|
|---|
| 1445 | -- handle serialized table with results
|
|---|
| 1446 | local func, err = loadstring(str)
|
|---|
| 1447 | if func then
|
|---|
| 1448 | status, res = pcall(func)
|
|---|
| 1449 | if not status then err = res
|
|---|
| 1450 | elseif type(res) ~= "table" then
|
|---|
| 1451 | err = "received "..type(res).." instead of expected 'table'"
|
|---|
| 1452 | end
|
|---|
| 1453 | end
|
|---|
| 1454 | if err then
|
|---|
| 1455 | print("Error in processing results: " .. err)
|
|---|
| 1456 | return nil, nil, "Error in processing results: " .. err
|
|---|
| 1457 | end
|
|---|
| 1458 | print(unpack(res))
|
|---|
| 1459 | return res[1], res
|
|---|
| 1460 | end
|
|---|
| 1461 | elseif status == "201" then
|
|---|
| 1462 | _, _, file, line = string.find(params, "^201 Started%s+(.-)%s+(%d+)%s*$")
|
|---|
| 1463 | elseif status == "202" or params == "200 OK" then
|
|---|
| 1464 | -- do nothing; this only happens when RE/LOAD command gets the response
|
|---|
| 1465 | -- that was for the original command that was aborted
|
|---|
| 1466 | elseif status == "204" then
|
|---|
| 1467 | local _, _, stream, size = string.find(params, "^204 Output (%w+) (%d+)$")
|
|---|
| 1468 | if stream and size then
|
|---|
| 1469 | local size = tonumber(size)
|
|---|
| 1470 | local msg = size > 0 and client:receive(size) or ""
|
|---|
| 1471 | print(msg)
|
|---|
| 1472 | if outputs[stream] then outputs[stream](msg) end
|
|---|
| 1473 | -- this was just the output, so go back reading the response
|
|---|
| 1474 | done = false
|
|---|
| 1475 | end
|
|---|
| 1476 | elseif status == "401" then
|
|---|
| 1477 | len = tonumber(len)
|
|---|
| 1478 | local res = client:receive(len)
|
|---|
| 1479 | print("Error in expression: " .. res)
|
|---|
| 1480 | return nil, nil, res
|
|---|
| 1481 | else
|
|---|
| 1482 | print("Unknown error")
|
|---|
| 1483 | return nil, nil, "Debugger error: unexpected response after EXEC/LOAD '" .. params .. "'"
|
|---|
| 1484 | end
|
|---|
| 1485 | if done then break end
|
|---|
| 1486 | end
|
|---|
| 1487 | else
|
|---|
| 1488 | print("Invalid command")
|
|---|
| 1489 | end
|
|---|
| 1490 | elseif command == "listb" then
|
|---|
| 1491 | for l, v in pairs(breakpoints) do
|
|---|
| 1492 | for f in pairs(v) do
|
|---|
| 1493 | print(f .. ": " .. l)
|
|---|
| 1494 | end
|
|---|
| 1495 | end
|
|---|
| 1496 | elseif command == "listw" then
|
|---|
| 1497 | for i, v in pairs(watches) do
|
|---|
| 1498 | print("Watch exp. " .. i .. ": " .. v)
|
|---|
| 1499 | end
|
|---|
| 1500 | elseif command == "suspend" then
|
|---|
| 1501 | client:send("SUSPEND\n")
|
|---|
| 1502 | elseif command == "stack" then
|
|---|
| 1503 | local opts = string.match(params, "^[a-z]+%s+(.+)$")
|
|---|
| 1504 | client:send("STACK" .. (opts and " "..opts or "") .."\n")
|
|---|
| 1505 | local resp = client:receive()
|
|---|
| 1506 | local _, _, status, res = string.find(resp, "^(%d+)%s+%w+%s+(.+)%s*$")
|
|---|
| 1507 | if status == "200" then
|
|---|
| 1508 | local func, err = loadstring(res)
|
|---|
| 1509 | if func == nil then
|
|---|
| 1510 | print("Error in stack information: " .. err)
|
|---|
| 1511 | return nil, nil, err
|
|---|
| 1512 | end
|
|---|
| 1513 | local ok, stack = pcall(func)
|
|---|
| 1514 | if not ok then
|
|---|
| 1515 | print("Error in stack information: " .. stack)
|
|---|
| 1516 | return nil, nil, stack
|
|---|
| 1517 | end
|
|---|
| 1518 | for _,frame in ipairs(stack) do
|
|---|
| 1519 | print(mobdebug.line(frame[1], {comment = false}))
|
|---|
| 1520 | end
|
|---|
| 1521 | return stack
|
|---|
| 1522 | elseif status == "401" then
|
|---|
| 1523 | local _, _, len = string.find(resp, "%s+(%d+)%s*$")
|
|---|
| 1524 | len = tonumber(len)
|
|---|
| 1525 | local res = len > 0 and client:receive(len) or "Invalid stack information."
|
|---|
| 1526 | print("Error in expression: " .. res)
|
|---|
| 1527 | return nil, nil, res
|
|---|
| 1528 | else
|
|---|
| 1529 | print("Unknown error")
|
|---|
| 1530 | return nil, nil, "Debugger error: unexpected response after STACK"
|
|---|
| 1531 | end
|
|---|
| 1532 | elseif command == "output" then
|
|---|
| 1533 | local _, _, stream, mode = string.find(params, "^[a-z]+%s+(%w+)%s+([dcr])%s*$")
|
|---|
| 1534 | if stream and mode then
|
|---|
| 1535 | client:send("OUTPUT "..stream.." "..mode.."\n")
|
|---|
| 1536 | local resp, err = client:receive()
|
|---|
| 1537 | if not resp then
|
|---|
| 1538 | print("Unknown error: "..err)
|
|---|
| 1539 | return nil, nil, "Debugger connection error: "..err
|
|---|
| 1540 | end
|
|---|
| 1541 | local _, _, status = string.find(resp, "^(%d+)%s+%w+%s*$")
|
|---|
| 1542 | if status == "200" then
|
|---|
| 1543 | print("Stream "..stream.." redirected")
|
|---|
| 1544 | outputs[stream] = type(options) == 'table' and options.handler or nil
|
|---|
| 1545 | -- the client knows when she is doing, so install the handler
|
|---|
| 1546 | elseif type(options) == 'table' and options.handler then
|
|---|
| 1547 | outputs[stream] = options.handler
|
|---|
| 1548 | else
|
|---|
| 1549 | print("Unknown error")
|
|---|
| 1550 | return nil, nil, "Debugger error: can't redirect "..stream
|
|---|
| 1551 | end
|
|---|
| 1552 | else
|
|---|
| 1553 | print("Invalid command")
|
|---|
| 1554 | end
|
|---|
| 1555 | elseif command == "basedir" then
|
|---|
| 1556 | local _, _, dir = string.find(params, "^[a-z]+%s+(.+)$")
|
|---|
| 1557 | if dir then
|
|---|
| 1558 | dir = string.gsub(dir, "\\", "/") -- convert slash
|
|---|
| 1559 | if not string.find(dir, "/$") then dir = dir .. "/" end
|
|---|
| 1560 |
|
|---|
| 1561 | local remdir = dir:match("\t(.+)")
|
|---|
| 1562 | if remdir then dir = dir:gsub("/?\t.+", "/") end
|
|---|
| 1563 | basedir = dir
|
|---|
| 1564 |
|
|---|
| 1565 | client:send("BASEDIR "..(remdir or dir).."\n")
|
|---|
| 1566 | local resp, err = client:receive()
|
|---|
| 1567 | if not resp then
|
|---|
| 1568 | print("Unknown error: "..err)
|
|---|
| 1569 | return nil, nil, "Debugger connection error: "..err
|
|---|
| 1570 | end
|
|---|
| 1571 | local _, _, status = string.find(resp, "^(%d+)%s+%w+%s*$")
|
|---|
| 1572 | if status == "200" then
|
|---|
| 1573 | print("New base directory is " .. basedir)
|
|---|
| 1574 | else
|
|---|
| 1575 | print("Unknown error")
|
|---|
| 1576 | return nil, nil, "Debugger error: unexpected response after BASEDIR"
|
|---|
| 1577 | end
|
|---|
| 1578 | else
|
|---|
| 1579 | print(basedir)
|
|---|
| 1580 | end
|
|---|
| 1581 | elseif command == "help" then
|
|---|
| 1582 | print("setb <file> <line> -- sets a breakpoint")
|
|---|
| 1583 | print("delb <file> <line> -- removes a breakpoint")
|
|---|
| 1584 | print("delallb -- removes all breakpoints")
|
|---|
| 1585 | print("setw <exp> -- adds a new watch expression")
|
|---|
| 1586 | print("delw <index> -- removes the watch expression at index")
|
|---|
| 1587 | print("delallw -- removes all watch expressions")
|
|---|
| 1588 | print("run -- runs until next breakpoint")
|
|---|
| 1589 | print("step -- runs until next line, stepping into function calls")
|
|---|
| 1590 | print("over -- runs until next line, stepping over function calls")
|
|---|
| 1591 | print("out -- runs until line after returning from current function")
|
|---|
| 1592 | print("listb -- lists breakpoints")
|
|---|
| 1593 | print("listw -- lists watch expressions")
|
|---|
| 1594 | print("eval <exp> -- evaluates expression on the current context and returns its value")
|
|---|
| 1595 | print("exec <stmt> -- executes statement on the current context")
|
|---|
| 1596 | print("load <file> -- loads a local file for debugging")
|
|---|
| 1597 | print("reload -- restarts the current debugging session")
|
|---|
| 1598 | print("stack -- reports stack trace")
|
|---|
| 1599 | print("output stdout <d|c|r> -- capture and redirect io stream (default|copy|redirect)")
|
|---|
| 1600 | print("basedir [<path>] -- sets the base path of the remote application, or shows the current one")
|
|---|
| 1601 | print("done -- stops the debugger and continues application execution")
|
|---|
| 1602 | print("exit -- exits debugger and the application")
|
|---|
| 1603 | else
|
|---|
| 1604 | local _, _, spaces = string.find(params, "^(%s*)$")
|
|---|
| 1605 | if spaces then
|
|---|
| 1606 | return nil, nil, "Empty command"
|
|---|
| 1607 | else
|
|---|
| 1608 | print("Invalid command")
|
|---|
| 1609 | return nil, nil, "Invalid command"
|
|---|
| 1610 | end
|
|---|
| 1611 | end
|
|---|
| 1612 | return file, line
|
|---|
| 1613 | end
|
|---|
| 1614 |
|
|---|
| 1615 | -- Starts debugging server
|
|---|
| 1616 | local function listen(host, port)
|
|---|
| 1617 | host = host or "*"
|
|---|
| 1618 | port = port or mobdebug.port
|
|---|
| 1619 |
|
|---|
| 1620 | local socket = require "socket"
|
|---|
| 1621 |
|
|---|
| 1622 | print("Lua Remote Debugger")
|
|---|
| 1623 | print("Run the program you wish to debug")
|
|---|
| 1624 |
|
|---|
| 1625 | local server = socket.bind(host, port)
|
|---|
| 1626 | local client = server:accept()
|
|---|
| 1627 |
|
|---|
| 1628 | client:send("STEP\n")
|
|---|
| 1629 | client:receive()
|
|---|
| 1630 |
|
|---|
| 1631 | local breakpoint = client:receive()
|
|---|
| 1632 | local _, _, file, line = string.find(breakpoint, "^202 Paused%s+(.-)%s+(%d+)%s*$")
|
|---|
| 1633 | if file and line then
|
|---|
| 1634 | print("Paused at file " .. file )
|
|---|
| 1635 | print("Type 'help' for commands")
|
|---|
| 1636 | else
|
|---|
| 1637 | local _, _, size = string.find(breakpoint, "^401 Error in Execution (%d+)%s*$")
|
|---|
| 1638 | if size then
|
|---|
| 1639 | print("Error in remote application: ")
|
|---|
| 1640 | print(client:receive(size))
|
|---|
| 1641 | end
|
|---|
| 1642 | end
|
|---|
| 1643 |
|
|---|
| 1644 | while true do
|
|---|
| 1645 | io.write("> ")
|
|---|
| 1646 | local file, line, err = handle(io.read("*line"), client)
|
|---|
| 1647 | if not file and err == false then break end -- completed debugging
|
|---|
| 1648 | end
|
|---|
| 1649 |
|
|---|
| 1650 | client:close()
|
|---|
| 1651 | end
|
|---|
| 1652 |
|
|---|
| 1653 | local cocreate
|
|---|
| 1654 | local function coro()
|
|---|
| 1655 | if cocreate then return end -- only set once
|
|---|
| 1656 | cocreate = cocreate or coroutine.create
|
|---|
| 1657 | coroutine.create = function(f, ...)
|
|---|
| 1658 | return cocreate(function(...)
|
|---|
| 1659 | mobdebug.on()
|
|---|
| 1660 | return f(...)
|
|---|
| 1661 | end, ...)
|
|---|
| 1662 | end
|
|---|
| 1663 | end
|
|---|
| 1664 |
|
|---|
| 1665 | local moconew
|
|---|
| 1666 | local function moai()
|
|---|
| 1667 | if moconew then return end -- only set once
|
|---|
| 1668 | moconew = moconew or (MOAICoroutine and MOAICoroutine.new)
|
|---|
| 1669 | if not moconew then return end
|
|---|
| 1670 | MOAICoroutine.new = function(...)
|
|---|
| 1671 | local thread = moconew(...)
|
|---|
| 1672 | -- need to support both thread.run and getmetatable(thread).run, which
|
|---|
| 1673 | -- was used in earlier MOAI versions
|
|---|
| 1674 | local mt = thread.run and thread or getmetatable(thread)
|
|---|
| 1675 | local patched = mt.run
|
|---|
| 1676 | mt.run = function(self, f, ...)
|
|---|
| 1677 | return patched(self, function(...)
|
|---|
| 1678 | mobdebug.on()
|
|---|
| 1679 | return f(...)
|
|---|
| 1680 | end, ...)
|
|---|
| 1681 | end
|
|---|
| 1682 | return thread
|
|---|
| 1683 | end
|
|---|
| 1684 | end
|
|---|
| 1685 |
|
|---|
| 1686 | -- make public functions available
|
|---|
| 1687 | mobdebug.setbreakpoint = set_breakpoint
|
|---|
| 1688 | mobdebug.removebreakpoint = remove_breakpoint
|
|---|
| 1689 | mobdebug.listen = listen
|
|---|
| 1690 | mobdebug.loop = loop
|
|---|
| 1691 | mobdebug.scratchpad = scratchpad
|
|---|
| 1692 | mobdebug.handle = handle
|
|---|
| 1693 | mobdebug.connect = connect
|
|---|
| 1694 | mobdebug.start = start
|
|---|
| 1695 | mobdebug.on = on
|
|---|
| 1696 | mobdebug.off = off
|
|---|
| 1697 | mobdebug.moai = moai
|
|---|
| 1698 | mobdebug.coro = coro
|
|---|
| 1699 | mobdebug.done = done
|
|---|
| 1700 | mobdebug.pause = function() step_into = true end
|
|---|
| 1701 | mobdebug.yield = nil -- callback
|
|---|
| 1702 | mobdebug.output = output
|
|---|
| 1703 | mobdebug.onexit = os and os.exit or done
|
|---|
| 1704 | mobdebug.onscratch = nil -- callback
|
|---|
| 1705 | mobdebug.basedir = function(b) if b then basedir = b end return basedir end
|
|---|
| 1706 |
|
|---|
| 1707 | return mobdebug
|
|---|