|
config.DebugAdapterPort = config.DebugAdapterPort or 8165 |
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = rawget(_G, "DASocket") or { |
|
request = false, |
|
state = false, |
|
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, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}, |
|
} |
|
setmetatable(DASocket, JSONSocket) |
|
DASocket.__index = DASocket |
|
|
|
function DASocket:OnDisconnect(reason) |
|
|
|
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.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 self.request then |
|
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) |
|
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 |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
function DASocket:Event_StopDAServer() |
|
|
|
self:Logf("DebugAdapter stopped listening") |
|
if DAServer.listen_socket then |
|
sockDisconnect(DAServer.listen_socket) |
|
DAServer.listen_socket:delete() |
|
DAServer.listen_socket = nil |
|
end |
|
end |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
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 |
|
|
|
|
|
|
|
local function GetCleanSourceCode(filename) |
|
|
|
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 |
|
|
|
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("%-%-.*", "") |
|
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() |
|
self.manual_pause = true |
|
self.debug_blacklisted = config.DebugBlacklistedSource or false |
|
config.DebugBlacklistedSource = true |
|
DebuggerBreakExecution() |
|
end |
|
|
|
|
|
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) |
|
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 |
|
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 |
|
|
|
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) |
|
|
|
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 = { |
|
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 |
|
|
|
|
|
for i = 1, info.nups do |
|
capture(upvalue_var_index, i, upvalue_nils, debug.getupvalue(func, i)) |
|
end |
|
|
|
|
|
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 = rawget(_G, "DAServer") or { |
|
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 |
|
|
|
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() |
|
|
|
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 |
|
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 |
|
DAServer:Start( |
|
replace_previous, |
|
wait_debugger_time, |
|
host, |
|
port or config.DebugAdapterPort) |
|
UpdateThreadDebugHook() |
|
DebuggerEnableHook(true) |
|
end |
|
|
|
|
|
|
|
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) |
|
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() |
|
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() |
|
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 |