Graphical Remote Lua Debugger, a debugger for the lua programming language.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

808 lines
22 KiB

-- see copyright notice in grldc.h
local debug = require( "debug" )
local net = require( "grldc.net" )
local socket = require( "grldc.socket" )
local utilities = require( "grldc.utilities" )
local assert = assert
local print = print
local type = type
local debug = debug
local xpcall = xpcall
local pcall = pcall
local tostring = tostring
local string = string
local table = table
local unpack = unpack
local error = error
local setmetatable = setmetatable
local getmetatable = getmetatable
local coroutine = coroutine
local pairs = pairs
local ipairs = ipairs
local loadstring = loadstring
local originalCoroutineCreate = coroutine.create
local globals = _G
local getfenv = getfenv
local setfenv = setfenv
local findfenv = getfenv
-- luabind functions
local class_info = class_info
if getfenv == nil then
-- lua 5.2: no get/set environment
findfenv = function( f )
local idx = 1
while true do
local name, value = debug.getupvalue( f, idx )
if name == nil then break end
--print( "\""..name.."\"" )
if name == "_ENV" then return value end
idx = idx + 1
end
return nil
end
getfenv = function( f )
local env = findfenv( f )
if env == nil then
error( "Can't find function environment" )
end
return env
end
assert( setfenv == nil )
setfenv = function( f, env )
local idx = 1
while true do
local name, value = debug.getupvalue( f, idx )
if name == nil then break end
--print( "\""..name.."\"" )
if name == "_ENV" then
debug.setupvalue( f, idx, env )
return
end
idx = idx + 1
end
error( "Can't find function environment" )
end
end
module( "grldc" )
local server = nil
local status = "running"
local hookActiveCount = 0
local callstack
local coroutines = {}
setmetatable( coroutines, { __mode = "k" } )
local commands = {}
local runningCommands = {} -- commands that can be issued even when the debugged code is running
local breakPoints = {}
local breakPointAliases = {}
internal_.init( breakPointAliases )
local values = {}
local proxyMeta = {}
local envMeta = {}
local function releaseValue( id )
--print( "Releasing value with ID "..id )
assert( values[id] ~= nil )
values[id] = nil
end
local function splitValue( value )
local t = type( value )
if t == "nil" then
return { type = t, short = tostring( value ) }
elseif t == "number" then
return value
elseif t == "string" then
local maxStringLength = 48
if #value > maxStringLength then
local id = #values + 1 -- table 'values' can have holes, but this will always give a free id
--print( "Created string value with ID "..id )
values[id] = value
return { type = t, short = "\""..string.sub( value, 1, maxStringLength-3 ).."\"...", id = id }
else
return value
end
elseif t == "boolean" then
return value
elseif t == "table" then
local id = #values + 1 -- table 'values' can have holes, but this will always give a free id
--print( "Created table value with ID "..id )
values[id] = value
local res = { type = t, short = tostring( value ), id = id }
if getmetatable( value ) == proxyMeta then
res.type = "proxy"
res.short = value.short
end
return res
elseif t == "function" then
local id = #values + 1 -- table 'values' can have holes, but this will always give a free id
--print( "Created function value with ID "..id )
values[id] = value
return { type = t, short = tostring( value ), id = id }
elseif t == "thread" then
return { type = t, short = tostring( value ) }
elseif t == "userdata" then
local m = getmetatable(value)
if m and m.__luabind_class then
local tostr = m.__tostring
m.__tostring = nil -- temporarily disable tostring, so that we can get native lua info
local ok, ptr = pcall(tostring,value)
m.__tostring = tostr
_, _, ptr = string.find( ptr, "userdata: (.+)" )
local info = class_info( value )
local id = #values + 1 -- table 'values' can have holes, but this will always give a free id
values[id] = value
return { type = t, short = "[luabind] "..info.name..": "..ptr, id = id }
end
return { type = t, short = tostring(value) }
end
end
local function getValue( id )
local value = assert( values[id], "No value associated to ID "..tostring(id) )
local t = type( value )
local res
if t == "table" and getmetatable( value ) == proxyMeta then
res = {}
for _, entry in ipairs( value ) do
table.insert( res, { name = entry.name, value = splitValue( entry.value ) } )
end
elseif t == "table" then
res = {}
local meta = getmetatable( value )
if meta ~= nil then
table.insert( res, { name = "<metatable>", value = splitValue( meta ) } )
end
for k, v in pairs( value ) do
local key = splitValue(k)
local val = splitValue(v)
if type( key ) == "table" and key.id ~= nil then -- the key is a complex value
local proxy = { { name = "<key>", value = k }, { name = "<value>", value = v } }
if type( val ) == "table" then
proxy.short = val.short
else
if type( val ) == "string" then
proxy.short = "\""..val.."\""
else
proxy.short = tostring( val )
end
end
setmetatable( proxy, proxyMeta )
proxy = splitValue( proxy )
table.insert( res, { name = "["..key.short.."]", value = proxy } )
releaseValue( key.id )
if type( val ) == "table" and val.id ~= nil then
releaseValue( val.id )
end
else
local name
if type( key ) == "table" then
name = "["..key.short.."]"
else
local simpleKey = false
if type( key ) == "string" then
simpleKey = (string.find( key, "^[%a_][%a%d_]*$" ) ~= nil)
end
if simpleKey then
name = key
else
local keyStr = tostring( key )
if type( key ) == "string" then
keyStr = "\""..keyStr.."\""
end
name = "["..keyStr.."]"
end
end
table.insert( res, { name = name, value = val } )
end
end
elseif t == "function" then
res = {}
local upvaluesProxy = {}
setmetatable( upvaluesProxy, proxyMeta )
local upIdx = 1
while true do
local upName, upValue = debug.getupvalue( value, upIdx )
if upName == nil then break end
table.insert( upvaluesProxy, { name = upIdx..": "..upName, value = upValue } )
upIdx = upIdx + 1
end
local info = debug.getinfo( value, "S" )
table.insert( res, { name = "<what>", value = splitValue( info.what ) } )
if string.sub( info.source, 1, 1 ) == "@" then
table.insert( res, { name = "<source>", value = splitValue( info.source.."("..info.linedefined..")" ) } )
else
table.insert( res, { name = "<source>", value = splitValue( info.source ) } )
end
table.insert( res, { name = "<environment>", value = splitValue( findfenv( value ) ) } )
table.insert( res, { name = "<upvalues>", value = splitValue( upvaluesProxy ) } )
elseif t == "string" then
res = { { name = "<value>", value = value } }
elseif t == "userdata" then
local m = getmetatable( value )
if m and m.__luabind_class then
local info = class_info( value )
local res = {}
table.insert( res, { name = "<class methods>", value = splitValue( info.methods ) } )
for _, attrName in pairs( info.attributes ) do
table.insert( res, { name = attrName, value = splitValue( value[attrName] ) } )
end
return res
else
error( "Unknown value type: "..t.." (value = "..tostring( value )..")" )
end
else
error( "Unknown value type: "..t.." (value = "..tostring( value )..")" )
end
if res[1] == nil then
res[1] = { name = "<empty>" }
end
return res
end
local function checkClosed( f, ... )
local results = { xpcall( f, function( msg ) if msg == "closed" then return msg end return debug.traceback( msg ) end ) }
if not results[1] then
if results[2] == "closed" then
print( "Connection with debugger lost" )
server = nil
else
error( results[2], 0 )
end
return ...
end
return unpack( results, 2 )
end
local function synchronize()
print( "sending synchronization request..." )
server:send( "synchronize" )
print( "receiving breakpoints..." )
local numBreakpoints = server:receive()
print( tostring(numBreakpoints).." breakpoint(s)" )
assert( numBreakpoints == 0 ) -- not yet implemented
local breakOnConnection = server:receive()
return breakOnConnection
end
function updateRunningRequests_()
while true do
if server == nil then break end
local command = server:tryReceive( "running" )
if command == nil then break end
local func = runningCommands[command]
assert( func ~= nil, "Unknown running command: "..tostring( command ) )
local ok, msg = xpcall( func, debug.traceback )
if not ok then
print( "Error processing running command "..command..": "..tostring( msg ) )
end
end
end
function registerSourceFile_( fileName )
local nsource = "@"..utilities.normalizePath( string.sub( fileName, 2 ) )
s = breakPoints[nsource]
if s == nil then
s = {}
breakPoints[nsource] = s
end
breakPointAliases[fileName] = s
return s
end
local function getinfo( thread, level, what )
if type(level) ~= "function" then
level = level + 1 -- do not count ourself
end
if thread == nil then
thread = getmainthread()
end
return debug.getinfo( thread, level, what )
end
local function getlocal( thread, level, idx )
level = level + 1 -- do not count ourself
if thread == nil then
thread = getmainthread()
end
return debug.getlocal( thread, level, idx )
end
local function setlocal( thread, level, idx, value )
level = level + 1 -- do not count ourself
if thread == nil then
thread = getmainthread()
end
return debug.setlocal( thread, level, idx, value )
end
function getAppLevel_( thread, fromLevel )
-- find where the application code starts in the callstack (we want to ignore grldc functions)
local level = (fromLevel or 1) + 1
local appLevel
local grldcFunction = { [breakNow] = true, [connect] = true, [globals.coroutine.create] = true, [updateRunningRequests_] = true }
--[[print( "GRLDC functions:" )
for f in pairs( grldcFunction ) do
print( "\t"..tostring(f).." ("..tostring(getinfo(thread,f,"nf").name)..")" )
end
print( "Current stack:" )]]
while true do
local info = getinfo( thread, level, "f" )
if info == nil then break end
--print( "\t"..level.." "..tostring(info.func).." ("..tostring(info.name)..")" )
if grldcFunction[info.func] then appLevel = level end -- actual appLevel is level + 1, but we don't count ourself
level = level + 1
end
return appLevel
end
local function getCallstack( thread )
local appLevel = getAppLevel_( thread )
if appLevel == nil then appLevel = 0 end
local callstack = {}
local level = appLevel
while true do
local info = getinfo( thread, level, "nSl" )
if info == nil then break end
level = level + 1
local data =
{
name = info.name,
namewhat = info.namewhat,
what = info.what,
source = info.source,
line = info.currentline,
}
table.insert( callstack, data )
end
return callstack
end
local function setHook()
hookActiveCount = hookActiveCount + 1
if hookActiveCount == 1 then
internal_.setHookActive( true )
end
end
local function removeHook()
hookActiveCount = hookActiveCount - 1
if hookActiveCount == 0 then
internal_.setHookActive( false )
end
--debug.sethook( nil )
end
function suspendHook()
removeHook()
end
function resumeHook()
setHook()
end
local function registerCoroutine( co )
--debug.sethook( co, hook, "crl" )
internal_.setHook( co )
coroutines[co] = {}
end
globals.coroutine.create = function( f )
local co = originalCoroutineCreate( f )
registerCoroutine( co )
return co
end
local function setBreakPoint( source, line, value )
assert( string.sub( source, 1, 1 ) == "@" )
local nsource = "@"..utilities.normalizePath( string.sub( source, 2 ) )
assert( nsource == source, "Source must be normalized before setting a breakpoint, but source "..source.." is not normalized to "..nsource )
print( "Setting breakpoint at "..source.."("..line..") to "..tostring( value ) )
local s = breakPoints[source]
if s == nil then s = {} breakPoints[source] = s end
if value then
s[line] = true
else
s[line] = nil
end
end
function connect( address, port, name, maxRetry )
local retryCount = maxRetry
assert( name ~= nil )
assert( server == nil, "Already connected" )
print( "grldc: connecting to GRLD server..." )
while true do
local ok, msg = pcall( function()
server = net.connect( address, port )
end )
if ok then break end
if not ok and msg ~= "connection refused" then
error( msg )
end
if maxRetry ~= nil then
retryCount = retryCount - 1
if retryCount < 0 then
print( "grldc: can't connect to GRLD server after "..(maxRetry+1).." attempt(s) ; debugging disabled" )
return false
end
end
end
print( "grldc module connected to the GRLD server" )
checkClosed( function()
print( "sending client name..." )
server:send( name )
print( "synchronizing with server..." )
local breakOnConnection = synchronize()
local co, mainthread = coroutine.running()
assert( co == nil or mainthread, "Connection to the debugger must be done from the main thread" )
if mainthread then
-- lua 5.2: we can access the main thread directly
getmainthread = function() return co end
end
print( "setting debug hook..." )
internal_.setHook( getmainthread() )
print( "hook set" )
setHook()
if breakOnConnection then
breakNow()
end
end )
return true
end
local function breakNowImpl()
assert( status == "running" )
status = "break"
internal_.setStepMode( 0, nil )
server:send( "break" )
--assert( server:receive() == "ack_break" )
callstack = getCallstack( coroutine.running() )
server:send( callstack[1].source )
server:send( callstack[1].line )
while status == "break" do
--print( "waiting data..." )
server:waitData()
--print( "received data" )
updateRunningRequests_()
local command = server:tryReceive()
if command ~= nil then
assert( commands[command] ~= nil, "Received unknown command: "..tostring(command) )
commands[command]()
end
end
callstack = nil
end
local function getCoroutineId( co )
if co == nil then
return "main"
else
local _, _, id = string.find( tostring( co ), "thread: (.*)" )
assert( id ~= nil )
return id
end
end
local function getCoroutineFromId( id )
if id == "current" then
return coroutine.running()
else
local co = nil
if id ~= "main" then
for c, info in pairs( coroutines ) do
if coroutine.status( c ) ~= "dead" and id == getCoroutineId( c ) then
co = c
break
end
end
if co == nil then
return "no such coroutine"
end
end
return co
end
end
function commands.run()
--server:send( "ack_run" )
status = "running"
--assert( stepMode == nil )
end
function commands.stepover()
--server:send( "ack_stepover" )
status = "running"
internal_.setStepMode( 2, coroutine.running() or getmainthread() )
end
function commands.stepin()
--server:send( "ack_stepin" )
status = "running"
internal_.setStepMode( 1, nil )
end
function commands.stepout()
--server:send( "ack_stepout" )
status = "running"
internal_.setStepMode( 3, coroutine.running() or getmainthread() )
end
function commands.callstack()
local thread = server:receive()
if thread == "current" then
server:send( callstack )
else
local co = getCoroutineFromId( thread )
if type( co ) ~= "string" then
server:send( getCallstack( co ) )
else
server:send( co )
end
end
end
function commands.coroutines()
local res = {}
for co, info in pairs( coroutines ) do
if coroutine.status( co ) ~= "dead" then
local id = getCoroutineId( co )
table.insert( res, { id = id } )
end
end
server:send( res )
end
function commands.currentthread()
server:send( getCoroutineId( coroutine.running() ) )
end
function commands.breakpoints()
server:send( breakPoints )
end
function commands.locals()
local res = {}
local thread = server:receive()
local level = server:receive()
local co = getCoroutineFromId( thread )
if type( co ) ~= "string" then
local idx = 1
local appLevel = getAppLevel_( co, 1 )
if appLevel == nil then
appLevel = 0
end
level = level + appLevel - 1
while true do
local name, value = getlocal( co, level, idx )
if name == nil then break end
if name ~= "(*temporary)" then
table.insert( res, { name = name, value = splitValue( value ) } )
end
idx = idx + 1
end
server:send( res )
else
server:send( "no such coroutine" )
end
end
function commands.upvalues()
local res = {}
local thread = server:receive()
local level = server:receive()
local co = getCoroutineFromId( thread )
if type( co ) ~= "string" then
local idx = 1
local appLevel = getAppLevel_( co, 1 )
if appLevel == nil then
appLevel = 0
end
level = level + appLevel - 1
local info = getinfo( co, level, "f" )
while true do
local name, value = debug.getupvalue( info.func, idx )
if name == nil then break end
table.insert( res, { name = name, value = splitValue( value ) } )
idx = idx + 1
end
server:send( res )
else
server:send( "no such coroutine" )
end
end
function commands.evaluate()
local expr = server:receive()
local thread = server:receive()
local level = server:receive()
local co = getCoroutineFromId( thread )
if type( co ) ~= "string" then
if string.sub( expr, 1, 1 ) == "=" then
expr = "return "..string.sub( expr, 2 )
end
local ok, results = pcall( function()
local f = assert( loadstring( expr ) )
local appLevel = getAppLevel_( co, 1 )
if appLevel == nil then
appLevel = 0
end
local orgLevel = level
level = level + appLevel - 1
local info = getinfo( co, level, "f" )
local upvalues = {}
local idx = 1
while true do
local name, value = debug.getupvalue( info.func, idx )
if name == nil then break end
upvalues[name] = idx
idx = idx + 1
end
local locals = {}
idx = 1
while true do
local name, value = getlocal( co, level, idx )
if name == nil then break end
locals[name] = idx
idx = idx + 1
end
local env = setmetatable( { func = info.func, thread = co, level = orgLevel, locals = locals, upvalues = upvalues, environment = getfenv( info.func ) }, envMeta )
setfenv( f, env )
return { f() }
end )
if ok then
local res = {}
local lastResult = 0 -- TODO : check if there is a way to know the actual number of results, even if the last ones are nil values
for idx, value in pairs( results ) do
if idx > lastResult then lastResult = idx end
res[idx] = { name = "result #"..tostring(idx), value = splitValue( value ) }
end
for idx = 1, lastResult - 1 do
if res[idx] == nil then
res[idx] = { name = "result #"..tostring(idx), value = splitValue( nil ) }
end
end
if res[1] == nil then
res[1] = { name = "<no result>" }
end
server:send( res )
else
server:send( { { name = "<error>", value = splitValue( results ) } } )
end
else
server:send( { { name = "<error>", value = "no such coroutine" } } )
end
end
envMeta.__index = function( self, key )
if key == "__globals__" then
return globals
elseif key == "_G" then
return self
end
local lv = self.locals[key]
if lv ~= nil then
local appLevel = getAppLevel_( self.thread, 1 )
if appLevel == nil then
appLevel = 0
end
level = self.level + appLevel - 1
local k, v = getlocal( self.thread, level, lv )
return v
end
local uv = self.upvalues[key]
if uv ~= nil then
local k, v = debug.getupvalue( self.func, uv )
return v
end
return self.environment[key]
end
envMeta.__newindex = function( self, key, value )
if key == "__globals__" then
globals[key] = value
return
elseif key == "_G" then
error( "Can't override _G when remotely evaluating an expression" )
end
local lv = self.locals[key]
if lv ~= nil then
local appLevel = getAppLevel_( self.thread, 1 )
if appLevel == nil then
appLevel = 0
end
level = self.level + appLevel - 1
setlocal( self.thread, level, lv, value )
return
end
local uv = self.upvalues[key]
if uv ~= nil then
debug.setupvalue( self.func, uv, value )
return
end
self.environment[key] = value
end
function commands.getValue()
local id = server:receive()
server:send( getValue( id ) )
end
function runningCommands.releaseValue()
local id = server:receive( "running" )
releaseValue( id )
end
runningCommands["break"] = function()
if status == "running" then
breakNow()
else
print( "Break command ignored: already breaked" )
end
end
runningCommands.setbreakpoint = function()
local data = server:receive( "running" )
setBreakPoint( data.source, data.line, data.value )
end
function breakNow()
removeHook()
print( "Breaking execution..." )
while true do
if server == nil then
print( "Can't break execution: not connected to a debugger" )
return
end
local ok, msg = xpcall( breakNowImpl,
function( msg )
if msg == "closed" then return msg end
return debug.traceback( msg )
end
)
if ok then
break
else
if msg == "closed" then
print( "Connection with debugger lost" )
server = nil
break
else
print( "Error during break: "..msg )
end
end
socket.sleep( 0.1 )
status = "running"
end
print( "Resuming execution..." )
internal_.setStepDepth( 0 )
setHook()
end