Download CommonLua/Libs/DebugAdapter/DebugAdapter.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 36 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/Libs/DebugAdapter/DebugAdapter.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/CommonLua/Libs/DebugAdapter/DebugAdapter.lua
-
curl -L -o DebugAdapter.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/Libs/DebugAdapter/DebugAdapter.lua
36 kB
| config.DebugAdapterPort = config.DebugAdapterPort or 8165 | |
| -- NOTE: for setExpression request to work: | |
| -- 1) provide evaluateName for the editable property | |
| -- 2) supportsSetExpression = true | |
| -- 3) supportsSetVariable = false/nil | |
| -- The following config variable forces the above three | |
| config.DebugAdapterUseSetExpression = false | |
| config.MaxWatchLenValue = config.MaxWatchLenValue or 512 | |
| config.MaxWatchLenKey = config.MaxWatchLenKey or 128 | |
| if FirstLoad then | |
| __tuple_meta = { __name = "tuple" } | |
| end | |
| local function IsTuple(value) | |
| return type(value) == "table" and getmetatable(value) == __tuple_meta | |
| end | |
| ----- DASocket | |
| DASocket = rawget(_G, "DASocket") or { -- simple lua table, since it needs to work before the class resolution | |
| request = false, -- the current request being processed | |
| state = false, -- false, "running", "stopped" | |
| manual_pause = false, | |
| in_break = false, | |
| debug_blacklisted = false, | |
| callstack = false, | |
| scope_frame = false, | |
| stack_vars = false, | |
| breakpoints = false, | |
| condition_env = false, | |
| var_ref_idx = false, | |
| ref_to_var = false, | |
| Capabilities = { | |
| supportsConfigurationDoneRequest = true, | |
| supportsTerminateRequest = true, | |
| supportTerminateDebuggee = true, | |
| supportsConditionalBreakpoints = true, | |
| supportsHitConditionalBreakpoints = true, | |
| supportsLogPoints = true, | |
| supportsSetVariable = not config.DebugAdapterUseSetExpression, | |
| supportsSetExpression = config.DebugAdapterUseSetExpression, | |
| supportsVariableType = true, | |
| supportsCompletionsRequest = true, | |
| completionTriggerCharacters = {".", ":", "}"}, | |
| supportsBreakpointLocationsRequest = true, -- comes after setBreakpoints request! | |
| -- NOTE: no idea how to trigger these requests: | |
| --supportsEvaluateForHovers = true, -- only evaluate request with 'watch' context comming(no 'hover') | |
| --supportsGotoTargetsRequest = true, -- no official Lua API and hacking the Lua stack seems to crash the engine | |
| --supportsModulesRequest = true, -- not received - seems it is implemented for VS but not for VSCode | |
| --additionalModuleColumns = {{attributeName = "name", label = "label"}}, | |
| --supportsDelayedStackTraceLoading = true, -- we show the whole stack anyway | |
| -- NOTE: we may have 5k threads | |
| --supportsTerminateThreadsRequest = true, | |
| --supportsSingleThreadExecutionRequests = true, | |
| --supportsValueFormattingOptions = true, -- no special formatting options | |
| --supportsLoadedSourcesRequest = true, | |
| }, | |
| } | |
| setmetatable(DASocket, JSONSocket) | |
| DASocket.__index = DASocket | |
| function DASocket:OnDisconnect(reason) | |
| ---[[]] self:Logf("OnDisconnect %s", tostring(reason) or "") | |
| table.remove_value(DAServer.debuggers, self) | |
| printf("DebugAdapter connection %d %s:%d lost%s", self.connection, self.host, self.port, reason and ("(" .. reason .. ")") or "") | |
| end | |
| function DASocket:OnMsgReceived(message, headers) | |
| local msg_type = message.type | |
| if msg_type == "event" then | |
| local func = self["Event_" .. message.event] | |
| if func then | |
| return func(self, message.body) | |
| end | |
| elseif msg_type == "request" then | |
| local func = self["Request_" .. message.command] | |
| if func then | |
| ---[[]]self:Logf("Message: %s", ValueToLuaCode(message)) | |
| self.request = message | |
| local ok, err, response = pcall(func, self, message.arguments) | |
| if not ok then | |
| print("DebugAdapter error:", err) | |
| return | |
| end | |
| assert(self.request or (not err and response == nil)) -- if a response was sent there should be no return values | |
| if self.request then -- if response not send, send it now | |
| return self:SendResponse(err, response) | |
| end | |
| return | |
| end | |
| elseif msg_type == "response" then | |
| local func = self.result_callbacks and self.result_callbacks[message.request_seq] | |
| if func then | |
| return func(self, message) | |
| end | |
| end | |
| return "Unhandled message" | |
| end | |
| function DASocket:SendEvent(event, body) | |
| self.seq_id = (self.seq_id or 0) + 1 | |
| return self:Send{ | |
| type = "event", | |
| event = event, | |
| body = body, | |
| seq = self.seq_id, | |
| } | |
| end | |
| function DASocket:SendResponse(err, response) | |
| local request = self.request | |
| self.request = nil | |
| assert(request) | |
| if not request then return end | |
| self.seq_id = (self.seq_id or 0) + 1 | |
| return self:Send{ | |
| type = "response", | |
| request_seq = request.seq, | |
| success = not err, | |
| message = err or nil, | |
| command = request.command, | |
| body = response or nil, | |
| seq = self.seq_id, | |
| } | |
| end | |
| function DASocket:SendRequest(command, arguments, callback) | |
| self.seq_id = (self.seq_id or 0) + 1 | |
| local err = self:Send{ | |
| type = "request", | |
| command = command, | |
| arguments = arguments or nil, | |
| seq = self.seq_id, | |
| } | |
| if err then return err end | |
| CreateRealTimeThread(function(self, seq) -- clear the callback in 60sec | |
| Sleep(60000) | |
| self.result_callbacks[seq] = nil | |
| end, self, self.seq_id) | |
| self.result_callbacks = self.result_callbacks or {} | |
| self.result_callbacks[self.seq_id] = callback or nil | |
| end | |
| ----- References | |
| local reference_pool_size = 100000000 | |
| local modules_start = 1 * reference_pool_size | |
| local threads_start = 2 * reference_pool_size | |
| local variables_start = 3 * reference_pool_size | |
| local reference_types = { "module", "thread", "variables"} | |
| function DASocket:GetReferenceType(id) | |
| return reference_types[id / reference_pool_size] | |
| end | |
| ----- Events | |
| function DASocket:Event_StopDAServer() | |
| -- this comes form another debugee requesting us to stop the DAServer so it can be debugged | |
| self:Logf("DebugAdapter stopped listening") | |
| if DAServer.listen_socket then | |
| sockDisconnect(DAServer.listen_socket) | |
| DAServer.listen_socket:delete() | |
| DAServer.listen_socket = nil | |
| end | |
| end | |
| ----- Requests | |
| function DASocket:Request_initialize(arguments) | |
| if arguments.clientName then | |
| self.event_source = arguments.clientName .. " " | |
| end | |
| self.linesStartAt1 = arguments.linesStartAt1 | |
| self.columnsStartAt1 = arguments.columnsStartAt1 | |
| self.client = arguments | |
| self:SendResponse(nil, self.Capabilities) | |
| DebuggerInit() | |
| DebuggerClearBreakpoints() | |
| self.condition_env = {} | |
| setmetatable(self.condition_env, { | |
| __index = DebuggerIndex | |
| }) | |
| self:SendEvent("initialized") | |
| end | |
| function DASocket:Request_configurationDone(arguments) | |
| -- marks the end of initialization | |
| self:Continue() | |
| end | |
| function DASocket:Request_attach(arguments) | |
| self:SendResponse() | |
| end | |
| function DASocket:Request_disconnect(arguments) | |
| self.state = false | |
| self:SendResponse() | |
| if arguments.restart then | |
| CreateRealTimeThread(restart, GetAppCmdLine()) | |
| elseif arguments.terminateDebuggee then | |
| CreateRealTimeThread(quit) | |
| end | |
| end | |
| function DASocket:Request_threads(arguments) | |
| local threads = { | |
| { id = threads_start + 1, name = "Global" } | |
| } | |
| return nil, { threads = threads } | |
| end | |
| -- returns table of lines with all the comments removed | |
| local function GetCleanSourceCode(filename) | |
| -- capitalize the drive letter to avoid casing mismatches | |
| filename = string.upper(filename:sub(1, 1)) .. filename:sub(2) | |
| local err, source = AsyncFileToString(filename, nil, nil, "lines") | |
| if err then return err end | |
| local clean_source = {} | |
| local in_multi_line_comment = false | |
| for line_number, line in ipairs(source) do | |
| if in_multi_line_comment then | |
| local multi_line_end = line:find("%]%]") | |
| if multi_line_end then | |
| in_multi_line_comment = false | |
| line = line:sub(multi_line_end + 2, -1) | |
| end | |
| end | |
| if in_multi_line_comment then | |
| clean_source[line_number] = "" | |
| else | |
| -- remove multiline comments on a single line(which can be several) | |
| local clean_line | |
| local string_pos = 1 | |
| repeat | |
| local multi_line_start = line:find("%-%-%[%[", string_pos) | |
| local multi_line_end = multi_line_start and line:find("%]%]", multi_line_start + 4) | |
| if multi_line_end then | |
| clean_line = clean_line or {} | |
| table.insert(clean_line, line:sub(string_pos, multi_line_start - 1)) | |
| string_pos = multi_line_end + 2 | |
| end | |
| until not multi_line_end | |
| if clean_line then | |
| line = table.concat(clean_line, "") | |
| end | |
| local multi_line_start = line:find("%-%-%[%[") | |
| if multi_line_start then | |
| in_multi_line_comment = true | |
| clean_source[line_number] = line:sub(1, multi_line_start - 1) | |
| else | |
| line = line:gsub("%-%-.*", "") -- remove comments till the end of the line | |
| clean_source[line_number] = line | |
| end | |
| end | |
| end | |
| return clean_source | |
| end | |
| function DASocket:Request_breakpointLocations(arguments) | |
| local breakpoints_locations = {} | |
| local source = GetCleanSourceCode(arguments.source.path or arguments.source.sourceReference) | |
| if source[arguments.line]:match("%S") then | |
| return nil, {breakpoints = {line = arguments.line, column = 1}} | |
| else | |
| return nil, {breakpoints = {}} | |
| end | |
| end | |
| local function get_cond_expr(cond) | |
| local cond_expr = (cond == "") and "return true" or cond | |
| if not string.match(cond_expr, "^%s*return%s") then | |
| cond_expr = "return " .. cond_expr | |
| end | |
| return cond_expr | |
| end | |
| local is_running_packed = not IsFSUnpacked() | |
| DASocket.UnpackedLuaSources = { | |
| "ModTools/Src/", | |
| } | |
| DASocket.PackedLuaMapping = { | |
| ["CommonLua/"] = "ModTools/Src/CommonLua/", | |
| ["Lua/"] = "ModTools/Src/Lua/", | |
| ["Data/"] = "ModTools/Src/Data/", | |
| } | |
| for i, dlc in pairs(rawget(_G, "DlcDefinitions")) do | |
| local dlc_path = SlashTerminate(dlc.folder) | |
| DASocket.PackedLuaMapping[dlc_path] = string.format("ModTools/Src/DLC/%s/", dlc.name) | |
| end | |
| local function PackedToUnpackedLuaPath(virtual_path) | |
| for packed, unpacked in pairs(DASocket.PackedLuaMapping) do | |
| if string.starts_with(virtual_path, packed) then | |
| local result, err = ConvertToOSPath(unpacked .. string.sub(virtual_path, #packed + 1)) | |
| if not err and io.exists(result) then | |
| return result | |
| end | |
| end | |
| end | |
| return virtual_path | |
| end | |
| local function UnpackedToPackedLuaPath(virtual_path) | |
| for packed, unpacked in pairs(DASocket.PackedLuaMapping) do | |
| if string.starts_with(virtual_path, unpacked) then | |
| return packed .. string.sub(virtual_path, #unpacked + 1) | |
| end | |
| end | |
| return virtual_path | |
| end | |
| local function FindMountedLuaPath(os_path) | |
| local lua_mount_points = { | |
| "CommonLua/", | |
| "Lua/", | |
| "Data/", | |
| } | |
| if config.Mods then | |
| if is_running_packed then | |
| table.iappend(lua_mount_points, DASocket.UnpackedLuaSources) | |
| end | |
| for i, mod in ipairs(ModsLoaded) do | |
| table.insert(lua_mount_points, mod.content_path) | |
| end | |
| for i, dlc in pairs(rawget(_G, "DlcDefinitions")) do | |
| table.insert(lua_mount_points, dlc.folder) | |
| end | |
| end | |
| local os_path_lower = os_path:lower() | |
| for i, src_virtual in ipairs(lua_mount_points) do | |
| local src_os_path, err = ConvertToOSPath(src_virtual) | |
| if not err and io.exists(src_os_path) then | |
| local src_os_path = string.lower(src_os_path) | |
| if string.starts_with(os_path_lower, src_os_path) then | |
| return src_virtual .. string.gsub(string.sub(os_path, #src_os_path + 1), "\\", "/") | |
| end | |
| end | |
| end | |
| end | |
| function DASocket:Request_setBreakpoints(arguments) | |
| if not arguments.breakpoints then return end | |
| local bp_path = arguments.source.path or arguments.source.sourceReference | |
| local source = GetCleanSourceCode(bp_path) | |
| local filename = FindMountedLuaPath(bp_path) | |
| if not filename then | |
| return "This file is not a part of the game and cannot be debugged." | |
| end | |
| if is_running_packed then | |
| filename = UnpackedToPackedLuaPath(filename) | |
| end | |
| bp_path = bp_path:lower() | |
| self.breakpoints = self.breakpoints or {} | |
| for line, bp in pairs(self.breakpoints[bp_path]) do | |
| DebuggerRemoveBreakpoint(filename, line) | |
| end | |
| self.breakpoints[bp_path] = {} | |
| local response = {} | |
| for bp_idx, bp in ipairs(arguments.breakpoints) do | |
| local bp_set = table.copy(arguments.source) | |
| bp_set.id = bp_idx | |
| bp_set.line = bp.line | |
| local condition, hitCondition | |
| if bp.condition ~= nil then | |
| bp_set.condition = bp.condition | |
| local cond_expr = get_cond_expr(bp.condition) | |
| local eval, err = load(cond_expr, nil, nil, self.condition_env) | |
| if eval then | |
| condition = eval | |
| else | |
| bp_set.message = err | |
| end | |
| if bp.hitCondition ~= nil then | |
| bp_set.hitCondition = bp.hitCondition | |
| local hit_cond_expr = get_cond_expr(bp.hitCondition) | |
| local eval, err = load(hit_cond_expr, nil, nil, self.condition_env) | |
| if eval then | |
| hitCondition = eval | |
| else | |
| bp_set.message = table.concat({bp_set.message or "", err}, "\r\n") | |
| end | |
| end | |
| end | |
| bp_set.verified = source[bp.line]:match("%S") | |
| if bp_set.verified then | |
| if bp.logMessage then | |
| DebuggerAddBreakpoint(filename, bp.line, bp.logMessage, condition, hitCondition) | |
| else | |
| DebuggerAddBreakpoint(filename, bp.line, condition, hitCondition) | |
| end | |
| end | |
| self.breakpoints[bp_path][bp.line] = bp_set | |
| table.insert(response, bp_set) | |
| end | |
| return nil, {breakpoints = response} | |
| end | |
| function DASocket:Request_pause(arguments) | |
| self:SendResponse() -- first send the response | |
| self.manual_pause = true | |
| self.debug_blacklisted = config.DebugBlacklistedSource or false | |
| config.DebugBlacklistedSource = true | |
| DebuggerBreakExecution() | |
| end | |
| -- NOTE: When "Smooth Scroll" enabled - if BP/exception occurs and VSCode is already in the file it does not jump to the line | |
| function DASocket:Request_stackTrace(arguments) | |
| self.var_ref_idx = variables_start | |
| self.ref_to_var = {} | |
| return nil, self.callstack | |
| end | |
| function DASocket:Request_continue(arguments) | |
| self:SendResponse() | |
| self:Continue() | |
| self.manual_pause = false | |
| config.DebugBlacklistedSource = self.debug_blacklisted | |
| self.debug_blacklisted = false | |
| end | |
| function DASocket:Request_step(arguments) | |
| end | |
| function DASocket:Request_stepIn(arguments) | |
| self:SendResponse() | |
| DebuggerStep("step into", self.coroutine) | |
| self:Continue() | |
| end | |
| function DASocket:Request_stepOut(arguments) | |
| self:SendResponse() | |
| DebuggerStep("step out", self.coroutine) | |
| self:Continue() | |
| end | |
| function DASocket:Request_next(arguments) | |
| self:SendResponse() | |
| DebuggerStep("step over", self.coroutine) -- this will send "stopped" event with reason "step" via hookBreakLuaDebugger | |
| self:Continue() | |
| end | |
| local function IsSimpleValue(value) | |
| local vtype = type(value) | |
| return vtype == "number" or vtype == "string" or vtype == "boolean" or vtype == "nil" | |
| end | |
| local function ValueType(value) | |
| local vtype = type(value) | |
| if vtype == "boolean" or vtype == "string" or vtype == "number" then | |
| return vtype | |
| elseif vtype == "nil" then | |
| return "boolean" | |
| else | |
| return "value" | |
| end | |
| end | |
| local function HandleExpressionResults(ok, result, ...) | |
| if not ok then | |
| return result | |
| end | |
| if select("#", ...) ~= 0 then | |
| result = setmetatable({result, ...}, __tuple_meta) | |
| end | |
| return false, result | |
| end | |
| local function GetRawG() | |
| local env = { } | |
| local env_meta = {} | |
| env_meta.__index = function(env, key) | |
| return rawget(_G, key) | |
| end | |
| env_meta.__newindex = function(env, key, value) | |
| rawset(_G, key, value) | |
| end | |
| env._G = env | |
| setmetatable(env, env_meta) | |
| return env | |
| end | |
| function DASocket:EvaluateExpression(expression, frameId) | |
| local expr, err = load("return " .. expression, nil, nil, frameId and self.stack_vars[frameId] or GetRawG()) | |
| if err then | |
| return err | |
| end | |
| return HandleExpressionResults(pcall(expr)) | |
| end | |
| local func_info = {} | |
| local class_to_name | |
| local has_CObject = false | |
| function Debug_ResolveMeta(value) | |
| local meta = getmetatable(value) | |
| if meta and LightUserDataValue(value) and not IsT(value) then | |
| return -- because LightUserDataSetMetatable(TMeta) | |
| end | |
| return meta | |
| end | |
| function Debug_ResolveObjId(obj) | |
| local id = rawget(obj, "id") or rawget(obj, "Id") or PropObjHasMember(obj, "GetId") and obj:GetId() or "" | |
| if id ~= "" and type(id) == "string" then | |
| return id | |
| end | |
| end | |
| function Debugger_ToString(value, max_len) | |
| local vtype = type(value) | |
| local meta = Debug_ResolveMeta(value) | |
| local str | |
| if vtype == "string" then | |
| str = value | |
| elseif vtype == "thread" then | |
| local str_value = tostring(value) | |
| if IsRealTimeThread(value) then | |
| str_value = "real " .. str_value | |
| elseif IsGameTimeThread(value) then | |
| str_value = "game " .. str_value | |
| end | |
| if not IsValidThread(value) then | |
| str_value = "dead " .. str_value | |
| elseif CurrentThread() == value then | |
| str_value = "current " .. str_value | |
| end | |
| return str_value | |
| elseif vtype == "function" then | |
| if IsCFunction(value) then | |
| return "C " .. tostring(value) | |
| end | |
| return "Lua " .. tostring(value) | |
| elseif IsT(value) then | |
| str = TDevModeGetEnglishText(value, "deep", "no_assert") | |
| if str == "Missing text" then | |
| str = TTranslate(value, nil, false) | |
| end | |
| elseif vtype == "table" then | |
| if rawequal(value, _G) then | |
| return "_G" | |
| end | |
| local class = meta and value.class or "" | |
| str = tostring(value) | |
| if class ~= "" and type(class) == "string" then | |
| local id = Debug_ResolveObjId(value) or "" | |
| if id ~= "" then | |
| id = ' "' .. id .. '"' | |
| end | |
| local suffix, num = string.gsub(str, "^table", "") | |
| if num == 0 then | |
| suffix = "" | |
| end | |
| str = class .. id .. suffix | |
| if not class_to_name then | |
| class_to_name = table.invert(g_Classes) | |
| has_CObject = not not g_Classes.CObject | |
| end | |
| if class_to_name[value] then | |
| return "class " .. str | |
| elseif not IsValid(value) and has_CObject and IsKindOf(value, "CObject") then | |
| return "invalid object " .. str | |
| else | |
| return "object " .. str | |
| end | |
| else | |
| local name = rawget(value, "__name") | |
| if type(name) == "string" then | |
| return name | |
| end | |
| local count = table.count(value) | |
| if count > 0 then | |
| local len = #value | |
| if len > 0 then | |
| str = str .. " #" .. len | |
| end | |
| if len ~= count then | |
| str = str .. " [" .. count .. "]" | |
| end | |
| end | |
| end | |
| elseif vtype == "userdata" then | |
| if __cobjectToCObject and __cobjectToCObject[value] then | |
| return "GameObject " .. tostring(value) | |
| end | |
| end | |
| if meta then | |
| if rawget(meta, "__tostring") ~= nil then | |
| local ok, result = pcall(meta.__tostring, value) | |
| if ok then | |
| str = result | |
| end | |
| elseif IsGrid(value) then | |
| local pid = GridGetPID(value) | |
| local w, h = value:size() | |
| return "grid " .. pid .. ' ' .. w .. 'x' .. h | |
| elseif meta == __tuple_meta then | |
| return "tuple #" .. table.count(value) .. "" | |
| end | |
| end | |
| str = str or tostring(value) | |
| max_len = max_len or config.MaxWatchLenValue | |
| if #str > max_len then | |
| str = string.sub(str, 1, max_len) .. "..." | |
| end | |
| return str | |
| end | |
| function DASocket:Request_evaluate(arguments) | |
| local context = arguments.context | |
| if context == "watch" then | |
| if not self.ref_to_var then return end | |
| local err, result = self:EvaluateExpression(arguments.expression, arguments.frameId) | |
| if err then return err end | |
| local simple_value = IsSimpleValue(result) | |
| if not simple_value then | |
| self.var_ref_idx = self.var_ref_idx + 1 | |
| self.ref_to_var[self.var_ref_idx] = result | |
| end | |
| local var_ref = simple_value and 0 or self.var_ref_idx | |
| return nil, {result = Debugger_ToString(result), variablesReference = var_ref, type = ValueType(result)} | |
| elseif context == "repl" then | |
| local err, result = self:EvaluateExpression(arguments.expression, arguments.frameId) | |
| if err then return err end | |
| local vtype = ValueType(result) | |
| if IsTuple(result) then | |
| local str = {} | |
| for i, val in ipairs(result) do | |
| str[i] = Debugger_ToString(val) | |
| end | |
| result = table.concat(str, ", ") | |
| else | |
| local str = Debugger_ToString(result) | |
| local entries = Debugger_GetWatchEntries(result) | |
| if #entries > 0 then | |
| local concat = {str, " {"} | |
| for i, entry in ipairs(entries) do | |
| concat[#concat + 1] = "\n\t" | |
| concat[#concat + 1] = Debugger_ToString(entry[1]) | |
| concat[#concat + 1] = " = " | |
| concat[#concat + 1] = Debugger_ToString(entry[2]) | |
| end | |
| concat[#concat + 1] = "\n}" | |
| str = table.concat(concat) | |
| end | |
| result = str | |
| end | |
| return nil, {result = result, type = vtype} | |
| end | |
| end | |
| function DASocket:Request_scopes(arguments) | |
| if not self.ref_to_var then return end | |
| local frame = arguments.frameId | |
| self.scope_frame = frame | |
| self.eval_env = self.stack_vars[frame] | |
| self.ref_to_var[variables_start] = self.eval_env | |
| return nil, {scopes = { | |
| { | |
| name = "Autos", | |
| variablesReference = variables_start, | |
| }, | |
| }} | |
| end | |
| function Debugger_GetWatchEntries(var) | |
| local meta = Debug_ResolveMeta(var) | |
| local vtype = type(var) | |
| local values | |
| if vtype == "thread" then | |
| local current = CurrentThread() == var | |
| local callstack = GetStack(var) or "" | |
| callstack = string.tokenize(callstack, "\n") | |
| local last_dbg_idx | |
| for i, line in ipairs(callstack) do | |
| if line:find_lower("CommonLua/Libs/DebugAdapter") then | |
| last_dbg_idx = i | |
| end | |
| end | |
| if last_dbg_idx then | |
| local clean_stack = {} | |
| for i=last_dbg_idx + 1,#callstack do | |
| clean_stack[#clean_stack + 1] = callstack[i] | |
| end | |
| callstack = clean_stack | |
| end | |
| values = { | |
| type = IsRealTimeThread(var) and "real" or IsGameTimeThread(var) and "game" or "", | |
| current = current, | |
| status = GetThreadStatus(var) or "dead", | |
| callstack = callstack, | |
| } | |
| elseif vtype == "function" then | |
| if not IsCFunction(var) then | |
| local info = func_info[var] | |
| if not info then | |
| info = debug.getinfo(var) or empty_table | |
| func_info[var] = info | |
| end | |
| if info.short_src and info.linedefined and info.linedefined ~= -1 then | |
| values = { | |
| source = string.format("%s(%d)", info.short_src, info.linedefined), | |
| } | |
| end | |
| end | |
| elseif vtype == "userdata" then | |
| if __cobjectToCObject and __cobjectToCObject[var] then | |
| return | |
| end | |
| if meta and meta.__debugview then | |
| local ok, result = pcall(meta.__debugview, var) | |
| if ok then | |
| values = result | |
| end | |
| end | |
| elseif vtype == "table" then | |
| values = var | |
| end | |
| local entries = {} | |
| if meta then | |
| table.insert(entries, {"metatable", meta}) | |
| end | |
| local biggest_number, number_keys_entries, other_keys_entries = 0 | |
| for key, value in pairs(values) do | |
| if type(key) == "number" then | |
| number_keys_entries = table.create_add(number_keys_entries, { key, value }) | |
| biggest_number = Max(biggest_number, key) | |
| else | |
| local key_str = Debugger_ToString(key, const.MaxWatchLenKey) | |
| other_keys_entries = table.create_add(other_keys_entries, { key_str, value }) | |
| end | |
| end | |
| if number_keys_entries then | |
| table.sortby_field(number_keys_entries, 1) | |
| local max_len = #tostring(biggest_number) | |
| for _, entry in ipairs(number_keys_entries) do | |
| local key, value = entry[1], entry[2] | |
| local key_str = tostring(key) | |
| key_str = string.rep(" ", max_len - #key_str) .. key_str | |
| table.insert(entries, { key_str, value }) | |
| end | |
| end | |
| if other_keys_entries then | |
| table.sort(other_keys_entries, function(e1, e2) return CmpLower(e1[1], e2[1]) end) | |
| table.iappend(entries, other_keys_entries) | |
| end | |
| return entries | |
| end | |
| function DASocket:Request_variables(arguments) | |
| if not self.var_ref_idx then return end | |
| if not arguments then return end | |
| local var_ref = arguments.variablesReference | |
| if not var_ref then return end | |
| local entries = Debugger_GetWatchEntries(self.ref_to_var[var_ref]) | |
| if #entries == 0 then return end | |
| local variables = {} | |
| for i, entry in ipairs(entries) do | |
| variables[i] = self:CreateVar(entry[1], entry[2]) | |
| end | |
| return nil, { variables = variables } | |
| end | |
| function DASocket:SetVariableValue(var_name, new_value) | |
| local vars = self.stack_vars[self.scope_frame] | |
| local var_index, up_value_func = vars:__get_value_index(var_name) | |
| rawset(vars, var_name, new_value) | |
| local result | |
| -- local variales are shadowing the upvalues with the same name | |
| if up_value_func then | |
| result = debug.setupvalue(up_value_func, var_index, new_value) | |
| else | |
| result = debug.setlocal(self.scope_frame + 8, var_index, new_value) | |
| end | |
| return vars[result] | |
| end | |
| function DASocket:Request_setVariable(arguments) | |
| if not self.ref_to_var then return end | |
| local new_value = self:SetVariableValue(arguments.name, arguments.value) | |
| return nil, {value = new_value, type = ValueType(new_value)} | |
| end | |
| function DASocket:Request_setExpression(arguments) | |
| if not self.ref_to_var then return end | |
| local var_name = arguments.expression | |
| local err, eval = self:EvaluateExpression(var_name, arguments.frameId) | |
| if err then return err end | |
| local result = self:SetVariableValue(var_name, arguments.value) | |
| return nil, {value = result, type = ValueType(result)} | |
| end | |
| function DASocket:Request_loadedSources(arguments) | |
| end | |
| function DASocket:Request_source(arguments) | |
| end | |
| function DASocket:Request_terminate(arguments) | |
| CreateRealTimeThread(quit, 1) | |
| end | |
| function DASocket:Request_modules(arguments) | |
| -- list Libs, DLCs and Mods as modules | |
| local startModule = arguments.startModule or 0 | |
| local moduleCount = arguments.moduleCount or 0 | |
| local modules = {} | |
| for _, mod in ipairs(ModsLoaded) do | |
| table.insert(modules, {id = mod.id, name = mod.name}) | |
| end | |
| return nil, {modules = modules, totalModules = #modules} | |
| end | |
| local function GetTextLine(text, line) | |
| local line_number = 1 | |
| for text_line in string.gmatch(text, "[\r\n]+") do | |
| if line_number == line then | |
| return text_line | |
| end | |
| end | |
| return text | |
| end | |
| local completion_type_remap = { | |
| ["value"] = "value", | |
| ["f"] = "function", | |
| } | |
| local function GetCompletionsList(line, column, frameId) | |
| local completions = GetAutoCompletionList(line, column) | |
| for _, completion in ipairs(completions) do | |
| completion.type = completion_type_remap[completion.kind] | |
| completion.kind = nil | |
| completion.label = completion.value | |
| completion.value = nil | |
| end | |
| return completions | |
| end | |
| function DASocket:Request_completions(arguments) | |
| local line = GetTextLine(arguments.text, arguments.line) | |
| if line then | |
| return nil, {targets = GetCompletionsList(line, arguments.column)} | |
| end | |
| end | |
| local stop_reasons_map = { | |
| step = "step", | |
| breakpoint = "breakpoint", | |
| pause = "pause", | |
| exception = "exception", | |
| } | |
| local stop_descriptions_map = { -- shown in UI | |
| step = "Step", | |
| breakpoint = "Breakpoint", | |
| pause = "Pause", | |
| exception = "Exception", | |
| } | |
| function DASocket:OnStopped(reason, bp_id) | |
| if self.state then | |
| self.state = "stopped" | |
| self:SendEvent("stopped", { | |
| reason = stop_reasons_map[reason] or "pause", | |
| description = stop_descriptions_map[reason], | |
| allThreadsStopped = true, | |
| threadId = threads_start + 1, | |
| hitBreakpointIds = bp_id and {bp_id} or nil, | |
| }) | |
| end | |
| end | |
| function DASocket:OnOutput(text, output_type) | |
| if self.state == "running" then | |
| self:SendEvent("output", { | |
| output = text, | |
| category = output_type or "console", | |
| }) | |
| end | |
| end | |
| function ForEachDebugger(method, ...) | |
| for _, debugger in ipairs(DAServer.debuggers) do | |
| debugger[method](debugger, ...) | |
| end | |
| end | |
| function OnMsg.ConsoleLine(text, bNewLine) | |
| ForEachDebugger("OnOutput", bNewLine and ("\r\n" .. text) or text) | |
| end | |
| function DASocket:OnExit() | |
| if self.state then | |
| self:SendEvent("exited", { | |
| exitCode = GetExitCode(), | |
| }) | |
| end | |
| end | |
| function DASocket:Update() | |
| while self.manual_pause do | |
| sockProcess(1) | |
| end | |
| end | |
| local function CaptureVars(co, level) | |
| local vars = {} | |
| local info | |
| if co then | |
| info = debug.getinfo(co, level, "fu") | |
| else | |
| info = debug.getinfo(level + 1, "fu") | |
| end | |
| local func = info and info.func or nil | |
| if not func then return vars end | |
| local i = 1 | |
| local local_nils = {} | |
| local upvalue_nils = {} | |
| local local_var_index = {} | |
| local upvalue_var_index = {} | |
| local function capture(var_index, index, nils, name, value) | |
| if name then | |
| if rawequal(value, nil) then | |
| nils[name] = true | |
| else | |
| vars[name] = value | |
| end | |
| var_index[name] = index | |
| return name | |
| end | |
| end | |
| -- upvalues first | |
| for i = 1, info.nups do | |
| capture(upvalue_var_index, i, upvalue_nils, debug.getupvalue(func, i)) | |
| end | |
| -- local vars can shadow upvalues and if available and edited - they should change(not shadowed upvalue) | |
| if co then | |
| while capture(local_var_index, i, local_nils, debug.getlocal(co, level, i)) do | |
| i = i + 1 | |
| end | |
| else | |
| while capture(local_var_index, i, local_nils, debug.getlocal(level + 1, i)) do | |
| i = i + 1 | |
| end | |
| end | |
| vars.__get_value_index = function(t, key) | |
| if local_var_index[key] then | |
| return local_var_index[key] | |
| else | |
| return upvalue_var_index[key], func | |
| end | |
| end | |
| return setmetatable(vars, { | |
| __index = function (t, key) | |
| if local_var_index[key] then | |
| if local_nils[key] then | |
| return nil | |
| end | |
| else | |
| if upvalue_nils[key] then | |
| return nil | |
| end | |
| end | |
| return rawget(_G, key) | |
| end, | |
| }) | |
| end | |
| local function GetStackFrames(startColumn, arguments) | |
| arguments = arguments or empty_table | |
| local co = arguments.co | |
| local level = arguments.level or 0 | |
| local max_levels = arguments.max_levels | |
| local stack_frames = {} | |
| local stack_vars = {} | |
| repeat | |
| local info | |
| if arguments.co then | |
| info = debug.getinfo(co, level, "nSl") | |
| else | |
| info = debug.getinfo(level, "nSl") | |
| end | |
| if not info then break end | |
| local vars = CaptureVars(co, level) | |
| local path = string.sub(info.source, string.match(info.source, "^@") and 2 or 1, -1) | |
| local visible = config.DebugBlacklistedSource or not string.match(path, "/DebugAdapter.lua$") | |
| local skip_frame = visible and #stack_frames == 0 and (info.name == "assert" or info.name == "error" or info.short_src == "[C]") | |
| if visible and not skip_frame then | |
| local os_path = is_running_packed and ConvertToOSPath(PackedToUnpackedLuaPath(path)) or ConvertToOSPath(path) | |
| local known_source = info.short_src ~= "[C]" and not string.starts_with(info.short_src, "[string") | |
| local default_name = known_source and "?" or info.short_src | |
| local stackFrame = {} | |
| stackFrame.id = #stack_frames + 1 | |
| stackFrame.name = string.format("%s (%s%s)", info.name or default_name, info.what, (info.namewhat or "") ~= "" and ("-" .. info.namewhat) or "") | |
| if known_source then | |
| stackFrame.source = { | |
| name = info.short_src, | |
| path = os_path, | |
| } | |
| stackFrame.line = info.currentline | |
| else | |
| stackFrame.line = 0 | |
| end | |
| stackFrame.column = 0 | |
| table.insert(stack_frames, stackFrame) | |
| table.insert(stack_vars, vars) | |
| end | |
| level = level + 1 | |
| until (level > 100) or (max_levels and #stack_frames >= max_levels) | |
| return {stackFrames = stack_frames, totalFrames = #stack_frames}, stack_vars | |
| end | |
| function DASocket:UpdateStackFrames(arguments) | |
| self.callstack, self.stack_vars = GetStackFrames(self.columnsStartAt1 and 1 or 0, arguments) | |
| end | |
| function DASocket:CreateVar(var_name, var_value) | |
| local simple_value = IsSimpleValue(var_value) | |
| if not simple_value then | |
| self.var_ref_idx = self.var_ref_idx + 1 | |
| self.ref_to_var[self.var_ref_idx] = var_value | |
| end | |
| local var = { | |
| name = var_name, | |
| value = Debugger_ToString(var_value), | |
| type = ValueType(var_value), | |
| variablesReference = simple_value and 0 or self.var_ref_idx, | |
| evaluateName = config.DebugAdapterUseSetExpression and var_name or nil, | |
| } | |
| return var | |
| end | |
| function DASocket:Continue() | |
| self.callstack = false | |
| self.scope_frame = false | |
| self.stack_vars = false | |
| self.var_ref_idx = false | |
| self.ref_to_var = false | |
| self.coroutine = false | |
| self.state = "running" | |
| end | |
| function DASocket:Break(reason, co, break_offset, level) | |
| self.coroutine = co | |
| self:UpdateStackFrames({level = level, co = co, break_offset = break_offset}) | |
| local bp_id | |
| if reason == "breakpoint" then | |
| for _, stack_frame in ipairs(self.callstack.stackFrames) do | |
| if stack_frame.source then | |
| local stack_path = stack_frame.source.path:lower() | |
| local stack_breakpoints = self.breakpoints and self.breakpoints[stack_path] | |
| local bp = stack_breakpoints and stack_breakpoints[stack_frame.line] | |
| if bp and bp.verified then | |
| bp_id = bp.id | |
| break | |
| end | |
| end | |
| end | |
| end | |
| if self.state ~= "stopped" then | |
| self:OnStopped(reason, bp_id) | |
| end | |
| if not self.in_break then | |
| self.in_break = true | |
| while not self.manual_pause and self.state == "stopped" do | |
| sockProcess(1) | |
| end | |
| self.in_break = false | |
| end | |
| end | |
| ----- DAServer | |
| DAServer = rawget(_G, "DAServer") or { -- simple lua table, since it needs to work before the class resolution | |
| host = "127.0.0.1", | |
| port = 8165, | |
| debuggers = {}, | |
| } | |
| function DAServer:Start(replace_previous, wait_debugger_time, host, port) | |
| if not self.listen_socket then | |
| self.host = host or self.host | |
| self.port = port or self.port | |
| self.listen_socket = DASocket:new{ | |
| OnAccept = function (self, ...) return DAServer:OnAccept(...) end, | |
| } | |
| local err = sockListen(self.listen_socket, self.host, self.port) | |
| if replace_previous and err == "address in use" then | |
| print("Replacing existing DebugAdapter") | |
| local timeout = GetPreciseTicks() + 2000 | |
| -- there is another debugee running, connect to it and shut it down | |
| local conn = DASocket:new() | |
| local conn_err = sockConnect(conn, timeout, self.host, self.port) | |
| if not conn_err then | |
| while GetPreciseTicks() - timeout < 0 and sockIsConnecting(conn) do | |
| sockProcess(1) | |
| end | |
| end | |
| if conn:IsConnected() then | |
| conn:SendEvent("StopDAServer") | |
| sockProcess(200) | |
| conn:delete() | |
| -- try again | |
| err = sockListen(self.listen_socket, self.host, self.port) | |
| end | |
| end | |
| if err then | |
| print("DebugAdapter listen error: ", err) | |
| self.listen_socket:delete() | |
| self.listen_socket = nil | |
| else | |
| --[[]]printf("DebugAdapter started at %s:%d", self.host, self.port) | |
| end | |
| end | |
| if self.listen_socket and wait_debugger_time then -- wait for connection | |
| local timeout = GetPreciseTicks() + wait_debugger_time | |
| while GetPreciseTicks() - timeout < 0 and #self.debuggers == 0 do | |
| sockProcess(1) | |
| end | |
| end | |
| return #self.debuggers > 0 | |
| end | |
| function DAServer:Stop() | |
| for _, da in ipairs(self.debuggers) do | |
| da:OnExit() | |
| end | |
| if self.listen_socket then | |
| self.listen_socket:delete() | |
| self.listen_socket = nil | |
| end | |
| end | |
| function DAServer:OnAccept(socket, host, port) | |
| self.connections = (self.connections or 0) + 1 | |
| local sock_obj = DASocket:new{ | |
| [true] = socket, | |
| host = host, | |
| port = port, | |
| event_source = string.format("DASocket#%d ", self.connections), | |
| connection = self.connections, | |
| } | |
| self.debuggers[#self.debuggers + 1] = sock_obj | |
| DAServer.thread = IsValidThread(DAServer.thread) or CreateRealTimeThread(function() | |
| while #DAServer.debuggers > 0 do | |
| sockProcess(0) | |
| ForEachDebugger("Update") | |
| WaitWakeup(50) | |
| end | |
| DAServer.thread = nil | |
| end) | |
| printf("DebugAdapter connection %d %s:%d", sock_obj.connection, host, port) | |
| return sock_obj | |
| end | |
| function Debug(replace_previous, wait_debugger_time, host, port) | |
| if DAServer.listen_socket then return end -- debugger already active | |
| DAServer:Start( | |
| replace_previous, | |
| wait_debugger_time, | |
| host, | |
| port or config.DebugAdapterPort) -- start without waiting for connection | |
| UpdateThreadDebugHook() -- change to the actual hook | |
| DebuggerEnableHook(true) | |
| end | |
| ----- globals | |
| function OnMsg.Autodone() | |
| DAServer:Stop() | |
| end | |
| function IsDAServerListening() | |
| return not not (rawget(_G, "DAServer") and DAServer.listen_socket) | |
| end | |
| if not Platform.ged then | |
| Debug(true) | |
| end | |
| function hookBreakLuaDebugger(reason) -- called from C so we can pass the self param | |
| ForEachDebugger("Break", reason, nil, nil, 5) | |
| if config.EnableHaerald and rawget(_G, "g_LuaDebugger") then | |
| g_LuaDebugger:Break() | |
| end | |
| end | |
| function hookLogPointLuaDebugger(log_msg) | |
| log_msg = string.gsub(log_msg, "{.-}", function(expression) | |
| expression = string.sub(expression, 2, -2) | |
| local vars = CaptureVars(nil, 7) | |
| local expr, err = load("return " .. expression, nil, nil, vars) | |
| if err then | |
| return err | |
| else | |
| local ok, result = pcall(expr) | |
| return result | |
| end | |
| end) | |
| printf("LogPoint: %s", log_msg) | |
| ForEachDebugger("OnOutput", log_msg) | |
| end | |
| local oldStartDebugger = rawget(_G, "StartDebugger") or empty_func | |
| function StartDebugger() | |
| Debug(true) | |
| if config.EnableHaerald then | |
| return oldStartDebugger() | |
| end | |
| end | |
| function _G.startdebugger(co, break_offset) | |
| Debug(true) | |
| UpdateThreadDebugHook() -- change to the actual hook | |
| StartDebugger() | |
| if IsDAServerListening() then | |
| DebuggerEnableHook(true) | |
| ForEachDebugger("Break", "exception", co, break_offset, co and 0 or 1) | |
| end | |
| if rawget(_G, "g_LuaDebugger") and config.EnableHaerald then | |
| DebuggerEnableHook(true) | |
| g_LuaDebugger:Break(co, break_offset) | |
| end | |
| end | |
| function _G.bp(...) | |
| if not (select("#", ...) == 0 or select(1, ...)) then return end | |
| Debug(true) | |
| UpdateThreadDebugHook() -- change to the actual hook | |
| StartDebugger() | |
| DebuggerEnableHook(true) | |
| local break_offset = select(2, ...) | |
| if IsDAServerListening() then | |
| ForEachDebugger("Break", "breakpoint", nil, break_offset, 5) | |
| end | |
| if rawget(_G, "g_LuaDebugger") and config.EnableHaerald then | |
| g_LuaDebugger:Break(nil, break_offset) | |
| end | |
| end |