-- see copyright notice in wxLdb.lua local grldc = { net = require( "grldc.net" ), utilities = require( "grldc.utilities" ) } local ui = { mainWindow = require( "ui.mainWindow" ) } local lfs = require( "lfs" ) local wx = require( "wx" ) require( "coxpcall" ) local coxpcall = coxpcall local copcall = copcall local setmetatable = setmetatable local ipairs = ipairs local type = type local assert = assert local pairs = pairs local next = next local print = print local tostring = tostring local string = string local coroutine = coroutine local io = io local debug = debug local xpcall = xpcall local table = table local os = os module( "wxLdbController" ) local meta = { __index = {} } local complexValueManagerMeta = { __index = {} } local function createClientConfig(name) return { name = name, mappings = {}, breakpoints = {}, scrollPositions = {}, } end function new( engine, window ) local res = { engine = engine, window = window, clients = {}, configs = {}, activeClient = nil, threadsDirty = true, exiting = false, requests_ = {}, toListen = {}, } setmetatable( res, meta ) res.runCoroutine = coroutine.create( function() res:run_() end ) local ok, msg = coroutine.resume( res.runCoroutine ) assert( ok, debug.traceback( res.runCoroutine, msg ) ) return res end function meta.__index:ready_() return self.sleeping and #self.requests_ == 0 end function meta.__index:addListener( ip, port ) if self.engine.initialized() then self.engine.listen( ip, port ) else table.insert( self.toListen, { ip = ip, port = port } ) end end function meta.__index:update() if self:ready_() then self.sleeping = false local ok, msg = coroutine.resume( self.runCoroutine ) assert( ok, debug.traceback( self.runCoroutine, msg ) ) elseif self.currentRequest_ ~= nil then if coroutine.status( self.currentRequest_ ) ~= "suspended" then print( "Asynchronous request has terminated" ) table.remove( self.requests_, 1 ) self.currentRequest_ = nil end elseif self.requests_[1] ~= nil then print( "Starting asynchronous request..." ) local req = self.requests_[1] local co = coroutine.create( req ) self.currentRequest_ = co local ok, msg = coroutine.resume( co ) assert( ok, debug.traceback( co, msg ) ) end end function meta.__index:sleep_() self.sleeping = true coroutine.yield() end function meta.__index:queueRequest_( req ) table.insert( self.requests_, req ) end function meta.__index:run_() self:sleep_() --@GRLD_PROTECTION@ self.engine.init() for _, listener in ipairs( self.toListen ) do self.engine.listen( listener.ip, listener.port ) end self.toListen = {} self.window:show( true ) for _, cbName in ipairs( { "onNewClient", "onClientBreak", "onClientLost" } ) do self.engine.registerEvent( cbName, function( ... ) return self[cbName.."_"]( self, ... ) end ) end local wrapCb = function( cb ) return function( ... ) if self:ready_() then return cb( ... ) end end end self.window:registerEvent( ui.mainWindow.ID_BREAK, wrapCb( function() self:onDebugCommand_( "breaknow", "running" ) end ) ) self.window:registerEvent( ui.mainWindow.ID_CONTINUE, wrapCb( function() self:onDebugCommand_( "run", "break" ) end ) ) self.window:registerEvent( ui.mainWindow.ID_STEP_OVER, wrapCb( function() self:onDebugCommand_( "stepover", "break" ) end ) ) self.window:registerEvent( ui.mainWindow.ID_STEP_INTO, wrapCb( function() self:onDebugCommand_( "stepin", "break" ) end ) ) self.window:registerEvent( ui.mainWindow.ID_STEP_OUT, wrapCb( function() self:onDebugCommand_( "stepout", "break" ) end ) ) self.window:registerEvent( ui.mainWindow.ID_TOGGLE_BREAKPOINT, wrapCb( function() self:onToggleBreakpoint_() end ) ) self.window:registerEvent( "onBreakPointChanged", wrapCb( function( ... ) self:onBreakPointChanged_( ... ) end ) ) self.window:registerEvent( "onFileOpen", wrapCb( function( ... ) self:onFileOpen_( ... ) end ) ) self.window:registerEvent( "onFileClosed", wrapCb( function( ... ) self:onFileClosed_( ... ) end ) ) self.window:registerEvent( "onScrollChanged", wrapCb(function( ... ) self:onScrollChanged_( ...) end ) ) self.window:registerEvent( "onApplicationExiting", wrapCb( function( ... ) self:onApplicationExiting_( ... ) end ) ) self.window.threads:registerEvent( "onThreadClicked", wrapCb( function( ... ) self:onThreadClicked_( ... ) end ) ) self.window.threads:registerEvent( "onBreakOnConnectionChanged", wrapCb( function( ... ) self:onBreakOnConnectionChanged_( ... ) end ) ) self.window.callstack:registerEvent( "onCallstackClicked", wrapCb( function( ... ) self:onCallstackClicked_( ... ) end ) ) self.window.watch.evaluateCallback = wrapCb( function( expr ) return self:evaluateExpression_( expr ) end ) self.configs.global = createClientConfig('global') self:loadConfig_( "global" ) self:sleep_() while not self.exiting do for clientId, clientData in pairs( self.clients ) do coxpcall( function() if clientData.invalidateTimer ~= nil then if os.time() >= clientData.invalidateTimer then clientData.invalidateTimer = nil assert( clientId == self.activeClient ) self:invalidateState_( true ) end end if clientData.config.dirty and os.time() >= (clientData.config.lastConfigSave or 0) + 10 then self:saveConfig_( clientData.config.name ) end if clientData.dirty then self.window:setActive() clientData.dirty = false local client = self.engine.getClient( clientId ) if client == nil then self.threadsDirty = true elseif clientId == self.activeClient then if client:status() == "break" then client:setactivethread( clientData.activeThread ) self.threadsDirty = true local callstack = client:callstack() if type( callstack ) ~= "table" then -- if we can not get the callstack, we try to switch to the current thread clientData.activeThread = "current" client:setactivethread( clientData.activeThread ) callstack = client:callstack() end self:refreshCallstack_( callstack ) self:refreshSourceFocus_( callstack, clientData.activeLevel ) end self:refreshBreakPoints_() end elseif clientData.lastUpdate == nil or os.time() > clientData.lastUpdate + 2 then clientData.lastUpdate = os.time() self.engine.getClient( clientId ):checkConnection() end end, function( msg ) print( "Error refreshing client "..clientId ) print( msg ) print( debug.traceback() ) end ) end if self.threadsDirty then coxpcall( function() self.threadsDirty = false self:refreshThreads_() end, function( msg ) print( "Error refreshing threads" ) print( msg ) print( debug.traceback() ) end ) end self:sleep_() end end function meta.__index:getActiveClientConfig_() local activeClientId = self.activeClient if activeClientId == nil then return self.configs.global end local clientData = self.clients[activeClientId] if clientData == nil then return self.configs.global end return clientData.config end function meta.__index:evaluateExpression_( expr ) local clientId = self.activeClient local client = self.engine.getClient( clientId ) if client == nil then return { { name = "" } } end if client:status() == "running" then return { { name = "" } } end local clientData = assert( self.clients[clientId] ) local results = client:evaluate( expr, clientData.activeLevel ) complexValueManagerMeta.init( results, self.engine, clientId ) return results end function meta.__index:onCallstackClicked_( level ) local clientId = self.activeClient local clientData = self.clients[clientId] if clientData == nil then return end clientData.activeLevel = level clientData.dirty = true end function meta.__index:refreshSourcePageFocus_( remoteSource, line ) local clientId = self.activeClient local clientData = assert( self.clients[clientId] ) local sourceType = string.sub( remoteSource, 1, 1 ) if sourceType == "@" then self.window:raise() print( "Setting focus to "..remoteSource.."("..line..")" ) remoteSource = grldc.utilities.normalizePath( string.sub( remoteSource, 2 ) ) local source, remotePath, remoteFile = self:getLocalSource_( clientId, remoteSource ) if source == nil then print( "Prompting mount path..." ) local mount, path = self.window:promptMountPath( remotePath, nil, remoteFile ) print( mount, path ) if mount ~= nil then mount = grldc.utilities.normalizePath( mount ) path = grldc.utilities.normalizePath( path, lfs.currentdir() ) print( mount, path ) local mountEnd = string.sub( mount, -1 ) if mountEnd ~= "/" and mountEnd ~= "\\" then mount = mount.."/" end print( mount, path ) clientData.config.mappings[mount] = path clientData.config.dirty = true source = self:getLocalSource_( clientId, remoteSource ) assert( source ~= nil ) end end if source ~= nil then print( source ) source = grldc.utilities.normalizePath( source ) self:setSourceFocus_( "@"..source, line ) end --print( source ) return source end end function meta.__index:refreshSourceFocus_( callstack, level ) local clientId = self.activeClient local clientData = assert( self.clients[clientId] ) if type( callstack ) == "table" and callstack[level] ~= nil then local remoteSource = callstack[level].source local line = callstack[level].line local source = self:refreshSourcePageFocus_( remoteSource, line ) self:setPointers_( level, source, line ) self:refreshPointers_() self:refreshWatches_( level ) end end function meta.__index:setPointers_( level, source, line ) self.pointer = { level = level, source = source, line = line } end function meta.__index:refreshPointers_() self.window:clearMarkers() if self.pointer ~= nil then local source = self.pointer.source local level = self.pointer.level local line = self.pointer.line if source ~= nil then if level == 1 then self.window:setCurrentLine( "@"..source, line ) else self.window:getSourcePage( "@"..source ):addOtherLine( line ) end end end end function meta.__index:refreshWatches_( level ) local clientId = self.activeClient local client = self.engine.getClient( clientId ) if client == nil then return end local autoVariables = {} local locals = client:locals( level ) if type( locals ) == "string" then self.window.auto:setData( { { name = "", value = locals } } ) else for _, entry in ipairs( locals ) do table.insert( autoVariables, { name = "[local] "..entry.name, value = entry.value } ) end end local upvalues = client:upvalues( level ) if type( upvalues ) == "string" then self.window.auto:setData( { { name = "", value = locals } } ) else for _, entry in ipairs( upvalues ) do table.insert( autoVariables, { name = "[upvalue] "..entry.name, value = entry.value } ) end end complexValueManagerMeta.init( autoVariables, self.engine, clientId ) self.window.auto:setData( autoVariables ) self.window.watch:refresh() end function meta.__index:onToggleBreakpoint_() local source, line = self.window:findSourcePageFocus() self:onBreakPointChanged_( source, line ) end function meta.__index:onBreakPointChanged_( source, line ) local clientId = self.activeClient local config = nil if clientId == nil or self.clients[clientId] == nil then config = self.configs.global else local clientData = self.clients[clientId] if clientData == nil then return end config = clientData.config end if config.breakpoints[source] == nil then config.breakpoints[source] = {} end config.breakpoints[source][line] = not config.breakpoints[source][line] newValue = config.breakpoints[source][line] if not newValue then config.breakpoints[source][line] = nil end print( "Setting breakpoint at "..source.."("..line..") to "..tostring(newValue) ) assert( string.sub( source, 1, 1 ) == "@" ) source = string.sub( source, 2 ) for clientId, clientData in pairs( self.clients ) do if clientData.config == config then local client = self.engine.getClient( clientId ) if client == nil then return end local remoteSource, dir = self:getRemoteSource_( clientId, source ) self:queueRequest_( function() if remoteSource == nil then print( "Can't find remote source corresponding to "..source ) print( "Prompting mount path..." ) local mount, path = self.window:promptMountPath( nil, dir ) print( mount, path ) if mount ~= nil then mount = grldc.utilities.normalizePath( mount ) path = grldc.utilities.normalizePath( path, lfs.currentdir() ) print( mount, path ) local mountEnd = string.sub( mount, -1 ) if mountEnd ~= "/" and mountEnd ~= "\\" then mount = mount.."/" end print( mount, path ) clientData.config.mappings[mount] = path remoteSource, dir = self:getRemoteSource_( clientId, source ) assert( remoteSource ~= nil ) end end if remoteSource ~= nil then client:setbreakpoint( "@"..remoteSource, line, newValue ) clientData.config.dirty = true end self:refreshBreakPoints_() end ) end end self:refreshBreakPoints_() end function meta.__index:onApplicationExiting_() for id, clientData in pairs( self.clients ) do if clientData.config.dirty then self:saveConfig_( clientData.config.name ) end end self.window.threads:setData( nil ) end function meta.__index:refreshScrollPosition_() local config = self:getActiveClientConfig_() for source, sp in pairs( config.scrollPositions ) do local page = self.window:getSourcePage( source ) if page ~= nil then page:SetScrollPos(sp) end end end function meta.__index:refreshBreakPoints_() local clientId = self.activeClient local config = nil local client = nil if self.activeClient == nil or self.clients[clientId] == nil then config = self.configs.global else config = self.clients[clientId].config client = self.engine.getClient( clientId ) end local remoteBreakPoints = {} if client ~= nil then remoteBreakPoints = client:breakpoints() end self.window:clearBreakPoints() local goodBreakpoints = {} for remoteSource, lines in pairs( remoteBreakPoints ) do if next( lines ) ~= nil then local source = self:getLocalSource_( clientId, string.sub( remoteSource, 2 ) ) if source ~= nil then local page = self.window:findSourcePage( "@"..source ) if page ~= nil then if goodBreakpoints["@"..source] == nil then goodBreakpoints["@"..source] = {} end for line, value in pairs( lines ) do if value then page:addBreakPoint( line ) goodBreakpoints["@"..source][line] = true end end else print( "Can't find source page for breakpoint in file "..source ) end else print( "Can't find source corresponding to remote source "..remoteSource ) end end end for source, lines in pairs( config.breakpoints ) do local page = self.window:findSourcePage( source ) if page ~= nil then for line, value in pairs( lines ) do if value then if goodBreakpoints[source] == nil or not goodBreakpoints[source][line] then page:addBreakPoint( line, true ) end end end end end end function meta.__index:onFileOpen_( path ) print( "onFileOpen: "..path ) source = "@"..grldc.utilities.normalizePath( path ) print( "normalized path: "..source ) self:setSourceFocus_( source, 1 ) self:refreshPointers_() self:refreshBreakPoints_() self:refreshScrollPosition_() end function meta.__index:onFileClosed_( source ) for id, clientData in pairs( self.clients ) do clientData.config.dirty = true end end function meta.__index:onScrollChanged_( source, position ) local clientConfig = self:getActiveClientConfig_() clientConfig.scrollPositions[source] = position clientConfig.dirty = true end function meta.__index:onThreadClicked_( clientId, threadId ) print( "Thread clicked: client="..clientId..", thread="..threadId ) local clientData = self.clients[clientId] if clientData == nil then return end clientData.activeThread = threadId clientData.activeLevel = 1 self:setActiveClient_( clientId ) end function meta.__index:onBreakOnConnectionChanged_( clientId, newValue ) local clientData = self.clients[clientId] if clientData == nil then return end clientData.config.breakOnConnection = newValue clientData.config.dirty = true end function meta.__index:onDebugCommand_( command, neededState, targetClientId ) if targetClientId == nil then targetClientId = self.activeClient end if targetClientId == nil then return end local client = self.engine.getClient( targetClientId ) if client == nil then print( "No client "..targetClientId ) return end if neededState ~= nil and client:status() ~= neededState then return end self:invalidateState_( false ) local clientData = assert( self.clients[targetClientId] ) local ok, msg = xpcall( function() client[command]( client ) end, debug.traceback ) if not ok then print( msg ) end clientData.dirty = true end function meta.__index:invalidateState_( immediate ) local clientData = self.clients[self.activeClient] if not clientData or immediate then if clientData ~= nil then clientData.invalidateTimer = nil end self.threadsDirty = true self.pointer = nil self.window:clearMarkers() self.window.callstack:setData( nil ) self:refreshBreakPoints_() local client = self.engine.getClient( self.activeClient ) if client == nil then self.window.auto:clear() self.window.watch:refresh() else self.window.auto:setData( nil ) self.window.watch:refresh() end else clientData.invalidateTimer = os.time() + 1 end end function meta.__index:getLocalSource_( clientId, source ) --print( source ) local _, _, dir, file = string.find( source, "(.*[/\\])(.*)" ) assert( dir ~= nil and file ~= nil ) local clientData = assert( self.clients[clientId] ) for mount, path in pairs( clientData.config.mappings ) do if string.sub( dir, 1, #mount ) == mount then local s = string.sub( dir, #mount, #mount ) assert( s == "/" or s == "\\" ) local r = string.sub( dir, #mount + 1 ) if r ~= "" then r = r.."/" end local localPath = grldc.utilities.normalizePath( path.."/"..r..file, lfs.currentdir() ) --print( localPath ) if lfs.attributes( localPath, "mode" ) == "file" then return localPath, dir, file end end end return nil, dir, file end function meta.__index:getRemoteSource_( clientId, localSource ) print( "Searching remote source corresponding to local source "..localSource ) local _, _, dir, file = string.find( localSource, "(.*[/\\])(.*)" ) assert( dir ~= nil and file ~= nil ) local clientData = assert( self.clients[clientId] ) local bestScore = -1 local bestPath = nil for mount, path in pairs( clientData.config.mappings ) do if string.sub( dir, 1, #path ) == path then local s = string.sub( dir, #path+1, #path+1 ) if s == "/" or s == "\\" then local r = string.sub( dir, #path + 2 ) if r ~= "" then r = r.."/" end local remotePath = grldc.utilities.normalizePath( mount..r..file ) local score = 0 string.gsub( mount, "[/\\]", function() score = score + 1 end ) print( "Candidate (score="..score..") : "..remotePath ) if score > bestScore then bestScore = score bestPath = remotePath end end end end return bestPath, dir end function meta.__index:setSourceFocus_( source, line ) local exist = (self.window:findSourcePage( source ) ~= nil) local page = self.window:getSourcePage( source ) page:setFocus( line ) self.window:setSourcePageFocus( source ) if not exist then for id, clientData in pairs( self.clients ) do clientData.config.dirty = true end end end function meta.__index:refreshCallstack_( callstack ) if type( callstack ) == "string" then self.window.callstack:setData( { { name = callstack, type = "", source = "", line = "" } } ) elseif callstack[1] == nil then self.window.callstack:setData( { { name = "empty callstack", type = "", source = "", line = "" } } ) else local callstackData = {} for level, data in ipairs( callstack ) do local entry = {} if data.namewhat ~= "" then assert( data.name ~= nil ) entry.name = "["..data.namewhat.."] "..data.name else entry.name = "???" end entry.type = data.what entry.source = data.source if data.line < 0 then entry.line = "" else entry.line = tostring( data.line ) end callstackData[level] = entry end self.window.callstack:setData( callstackData ) end end function meta.__index:refreshThreads_() local data = {} for clientId, clientData in pairs( self.clients ) do local client = self.engine.getClient( clientId ) if client == nil then print( "Client does not exist anymore: "..clientId ) if self.activeClient == clientId then self.activeClient = nil self:invalidateState_( true ) end self.clients[clientId] = nil else local cdata = {} cdata.name = client:name() cdata.ip = client:ip() cdata.clientId = client.clientId cdata.coroutines = {} cdata.status = client:status() cdata.active = (clientId == self.activeClient) cdata.breakOnConnection = clientData.config.breakOnConnection if cdata.status == "break" then local current = client:getcurrentthread() local active = client:getactivethread() if active == "current" then active = current end table.insert( cdata.coroutines, { id = "main", current = (current == "main"), active = (active=="main" and cdata.clientId == self.activeClient and client:getactivethread() ~= "current") } ) local coroutines = client:coroutines() --print( coroutines ) for _, data in ipairs( coroutines ) do local codata = {} codata.id = data.id codata.current = (current == codata.id) codata.active = (active == codata.id and cdata.clientId == self.activeClient and client:getactivethread() ~= "current") table.insert( cdata.coroutines, codata ) end end table.insert( data, cdata ) end end self.window.threads:setData( data ) end function meta.__index:setActiveClient_( clientId ) self:invalidateState_( true ) self.activeClient = clientId self.clients[clientId].dirty = true self.threadsDirty = true local clientConfig = self.clients[clientId].config for source, lines in pairs( self.configs.global.breakpoints ) do for line, _ in pairs( lines ) do if clientConfig.breakpoints[source] == nil or not clientConfig.breakpoints[source][line] then self:onBreakPointChanged_( source, line ) end lines[line] = nil end end self:refreshBreakPoints_() end function meta.__index:onNewClient_( clientId ) local client = self.engine.getClient( clientId ) local name = client:name() if self.configs[name] == nil then self.configs[name] = createClientConfig(name) self:loadConfig_( name ) end self.clients[clientId] = { dirty = true, activeThread = "current", activeLevel = 1, config = self.configs[name] } for source, lines in pairs( self.configs[name].breakpoints ) do assert( string.sub( source, 1, 1 ) == "@" ) source = string.sub( source, 2 ) local remoteSource, dir = self:getRemoteSource_( clientId, source ) if remoteSource ~= nil then for line, value in pairs( lines ) do if value then client:setbreakpoint( "@"..remoteSource, line, true ) end end end end self.threadsDirty = true if self.activeClient == nil then self:setActiveClient_( clientId ) end if not self.configs[name].breakOnConnection then self.clients[clientId].ignoreNextBreak = true end end function meta.__index:onClientBreak_( clientId ) local clientData = assert( self.clients[clientId] ) clientData.invalidateTimer = nil clientData.dirty = true clientData.activeLevel = 1 self.threadsDirty = true if clientData.ignoreNextBreak then clientData.ignoreNextBreak = false self:onDebugCommand_( "run", "break", clientId ) end end function meta.__index:onClientLost_( clientId ) local clientData = assert( self.clients[clientId] ) if clientData.config.dirty then self:saveConfig_( clientData.config.name ) end self.threadsDirty = true end function meta.__index:saveConfig_( name ) local clientConfig = assert( self.configs[name] ) clientConfig.dirty = false local name = clientConfig.name local openFiles = {} for source, page in pairs( self.window:getSourcePages() ) do openFiles[page.pageIdx+1] = source end local breakpoints = clientConfig.breakpoints local scrollPositions = clientConfig.scrollPositions local path = "clients/"..name.."/config.lua" lfs.mkdir( "clients" ) lfs.mkdir( "clients/"..name ) local file = assert( io.open( path, "w" ) ) file:write( grldc.net.serialize( { mappings = clientConfig.mappings, openFiles = openFiles, breakpoints = breakpoints, scrollPositions = scrollPositions, breakOnConnection = clientConfig.breakOnConnection } ) ) file:close() print( "Saved config \""..name.."\"" ) clientConfig.lastConfigSave = os.time() end function meta.__index:loadConfig_( name ) local clientConfig = self.configs[name] local path = "clients/"..name.."/config.lua" local file = io.open( path, "r" ) if file ~= nil then local config = grldc.net.deserialize( file:read( "*a" ) ) file:close() clientConfig.mappings = config.mappings clientConfig.breakOnConnection = config.breakOnConnection if config.scrollPositions ~= nil then for source, sp in pairs( config.scrollPositions ) do clientConfig.scrollPositions[source] = sp end end for _, file in ipairs( config.openFiles ) do local page, newlyLoaded = self.window:getSourcePage( file ) if page ~= nil and newlyLoaded then local sp = clientConfig.scrollPositions[file] print("scroll to", sp) if sp ~= nil then page:SetScrollPos(sp) end end end if clientConfig.breakpoints ~= nil then for source, bp in pairs( clientConfig.breakpoints ) do if clientConfig.breakpoints[source] == nil then clientConfig.breakpoints[source] = {} end for line, _ in pairs( bp ) do clientConfig.breakpoints[source][line] = true end end end end if clientConfig.breakOnConnection == nil then clientConfig.breakOnConnection = true end clientConfig.dirty = false end function complexValueManagerMeta.init( variables, engine, clientId ) local manager = nil for _, entry in pairs( variables ) do if type( entry.value ) == "table" and entry.value.id ~= nil then if manager == nil then manager = { engine = engine, clientId = clientId } end assert( entry.value.manager == nil ) entry.value.manager = manager setmetatable( entry.value, complexValueManagerMeta ) end end end function complexValueManagerMeta.__index:release() local client = self.manager.engine.getClient( self.manager.clientId ) if client == nil then return end client:releaseValue( self.id ) end function complexValueManagerMeta.__index:get() local client = self.manager.engine.getClient( self.manager.clientId ) if client == nil then return { ERROR = "connection with client lost" } end local value = client:getValue( self.id ) complexValueManagerMeta.init( value, self.manager.engine, self.manager.clientId ) return value end