local debuggee = {} local socket = require("socket.core") local json local handlers = {} local sock local directorySeperator = package.config:sub(1, 1) local sourceBasePath = "." local storedVariables = {} local nextVarRef = 1 local baseDepth, breaker, sendEvent local dumpCommunication = false local ignoreFirstFrameInC = false local debugTargetCo, redirectedPrintFunction, onError, addUserdataVar local defaultOnError = function(e) print("****************************************************") print(e) print("****************************************************") end local function valueToString(value, depth) local str = "" depth = depth or 0 local t = type(value) if t == "table" then str = str .. "{\n" for k, v in pairs(value) do str = str .. string.rep(" ", depth + 1) .. "[" .. valueToString(k) .. "]" .. " = " .. valueToString(v, depth + 1) .. ",\n" end str = str .. string.rep(" ", depth) .. "}" elseif t == "string" then str = str .. "\"" .. tostring(value) .. "\"" else str = str .. tostring(value) end return str end local sethook = debug.sethook debug.sethook = nil local cocreate = coroutine.create function coroutine.create(f) local c = cocreate(f) debuggee.addCoroutine(c) return c end local debug_getinfo = function(depth, what) if debugTargetCo then return debug.getinfo(debugTargetCo, depth, what) else return debug.getinfo(depth + 1, what) end end local debug_getlocal = function(depth, i) if debugTargetCo then return debug.getlocal(debugTargetCo, depth, i) else return debug.getlocal(depth + 1, i) end end local DO_TEST = false local getMatchCount = function(a, b) local n = math.min(#a, #b) for i = 0, n - 1 do if a[#a - i] == b[#b - i] then else return i end end return n end if DO_TEST then assert(getMatchCount({ "a", "b", "c" }, { "a", "b", "c" }) == 3) assert(getMatchCount({"b", "c"}, { "a", "b", "c" }) == 2) assert(getMatchCount({ "a", "b", "c" }, {"b", "c"}) == 2) assert(getMatchCount({}, { "a", "b", "c" }) == 0) assert(getMatchCount({ "a", "b", "c" }, {}) == 0) assert(getMatchCount({ "a", "b", "c" }, { "a", "b", "c", "d" }) == 0) end local splitChunkName = function(s) if string.sub(s, 1, 1) == "@" then s = string.sub(s, 2) end local a = {} for word in string.gmatch(s, "[^/\\]+") do a[#a + 1] = string.lower(word) end return a end if DO_TEST then local a = splitChunkName("@.\\vscode-debuggee.lua") assert(#a == 2) assert(a[1] == ".") assert(a[2] == "vscode-debuggee.lua") local a = splitChunkName("@C:\\dev\\VSCodeLuaDebug\\debuggee/lua\\socket.lua") assert(#a == 6) assert(a[1] == "c:") assert(a[2] == "dev") assert(a[3] == "vscodeluadebug") assert(a[4] == "debuggee") assert(a[5] == "lua") assert(a[6] == "socket.lua") local a = splitChunkName("@main.lua") assert(#a == 1) assert(a[1] == "main.lua") end local Path = {} function Path.isAbsolute(a) local firstChar = string.sub(a, 1, 1) if firstChar == "/" or firstChar == "\\" then return true end if string.match(a, "^%a%:[/\\]") then return true end return false end local np_pat1, np_pat2 = ("[^SEP:]+SEP%.%.SEP?"):gsub("SEP", directorySeperator), ("SEP+%.?SEP"):gsub("SEP", directorySeperator) function Path.normpath(path) path = path:gsub("[/\\]", directorySeperator) if directorySeperator == "\\" then local unc = ("SEPSEP"):gsub("SEP", directorySeperator) if path:match("^" .. unc) then return unc .. Path.normpath(path:sub(3)) end end local k repeat path, k = path:gsub(np_pat2, directorySeperator) until k == 0 repeat path, k = path:gsub(np_pat1, "", 1) until k == 0 if path == "" then path = "." end return path end function Path.concat(a, b) local lastChar = string.sub(a, #a, #a) if lastChar ~= "/" and lastChar ~= "\\" then a = a .. directorySeperator end if string.match(b, "^%.%\\") or string.match(b, "^%.%/") then b = string.sub(b, 3) end return a .. b end function Path.toAbsolute(base, sub) if Path.isAbsolute(sub) then return Path.normpath(sub) else return Path.normpath(Path.concat(base, sub)) end end if DO_TEST then assert(Path.isAbsolute("c:\\asdf\\afsd")) assert(Path.isAbsolute("c:/asdf/afsd")) if directorySeperator == "\\" then assert(Path.toAbsolute("c:\\asdf", "fdsf") == "c:\\asdf\\fdsf") assert(Path.toAbsolute("c:\\asdf", ".\\fdsf") == "c:\\asdf\\fdsf") assert(Path.toAbsolute("c:\\asdf", "..\\fdsf") == "c:\\fdsf") assert(Path.toAbsolute("c:\\asdf", "c:\\fdsf") == "c:\\fdsf") assert(Path.toAbsolute("c:/asdf", "../fdsf") == "c:\\fdsf") assert(Path.toAbsolute("\\\\HOST\\asdf", "..\\fdsf") == "\\\\HOST\\fdsf") elseif directorySeperator == "/" then assert(Path.toAbsolute("/usr/bin/asdf", "fdsf") == "/usr/bin/asdf/fdsf") assert(Path.toAbsolute("/usr/bin/asdf", "./fdsf") == "/usr/bin/asdf/fdsf") assert(Path.toAbsolute("/usr/bin/asdf", "../fdsf") == "/usr/bin/fdsf") assert(Path.toAbsolute("/usr/bin/asdf", "/usr/bin/fdsf") == "/usr/bin/fdsf") assert(Path.toAbsolute("\\usr\\bin\\asdf", "..\\fdsf") == "/usr/bin/fdsf") end end local coroutineSet = {} setmetatable(coroutineSet, {__mode = "v"}) local sendFully = function(str) local first = 1 while first <= #str do local sent = sock:send(str, first) if sent and 0 < sent then first = first + sent else error("sock:send() returned < 0") end end end local logToDebugConsole = function(output, category) local dumpMsg = { event = "output", type = "event", body = { category = category or "console", output = output } } local dumpBody = json.encode(dumpMsg) sendFully("#" .. #dumpBody .. "\n" .. dumpBody) end local createHaltBreaker = function() local loadedChunkNameMap = {} for chunkname, _ in pairs(debug.getchunknames()) do loadedChunkNameMap[chunkname] = splitChunkName(chunkname) end local findMostSimilarChunkName = function(path) local splitedReqPath = splitChunkName(path) local maxMatchCount = 0 local foundChunkName for chunkName, splitted in pairs(loadedChunkNameMap) do local count = getMatchCount(splitedReqPath, splitted) if maxMatchCount < count then maxMatchCount = count foundChunkName = chunkName end end return foundChunkName end local lineBreakCallback local updateCoroutineHook = function(c) if lineBreakCallback then sethook(c, lineBreakCallback, "l") else sethook(c) end end local sethalt = function(cname, ln) for i = ln, ln + 10 do if debug.sethalt(cname, i) then return i end end return nil end return { setBreakpoints = function(path, lines) local foundChunkName = findMostSimilarChunkName(path) local verifiedLines = {} if foundChunkName then debug.clearhalt(foundChunkName) for _, ln in ipairs(lines) do verifiedLines[ln] = sethalt(foundChunkName, ln) end end return verifiedLines end, setLineBreak = function(callback) if callback then sethook(callback, "l") else sethook() end lineBreakCallback = callback for cid, c in pairs(coroutineSet) do updateCoroutineHook(c) end end, coroutineAdded = function(c) updateCoroutineHook(c) end, stackOffset = { enterDebugLoop = 6, halt = 6, step = 4, stepDebugLoop = 6 } } end local createPureBreaker = function() local lineBreakCallback local breakpointsPerPath = {} local chunknameToPathCache = {} local chunkNameToPath = function(chunkname) local cached = chunknameToPathCache[chunkname] if cached then return cached end local splitedReqPath = splitChunkName(chunkname) local maxMatchCount = 0 local foundPath for path, _ in pairs(breakpointsPerPath) do local splitted = splitChunkName(path) local count = getMatchCount(splitedReqPath, splitted) if maxMatchCount < count then maxMatchCount = count foundPath = path end end if foundPath then chunknameToPathCache[chunkname] = foundPath end return foundPath end local entered = false local hookfunc = function() if entered then return false end entered = true if lineBreakCallback then lineBreakCallback() end local info = debug_getinfo(2, "Sl") if info then local path = chunkNameToPath(info.source) path = path and string.lower(path) local bpSet = breakpointsPerPath[path] if bpSet and bpSet[info.currentline] then _G.__halt__() end end entered = false end sethook(hookfunc, "l") return { setBreakpoints = function(path, lines) local t = {} local verifiedLines = {} for _, ln in ipairs(lines) do t[ln] = true verifiedLines[ln] = ln end path = path and string.lower(path) breakpointsPerPath[path] = t return verifiedLines end, setLineBreak = function(callback) lineBreakCallback = callback end, coroutineAdded = function(c) sethook(c, hookfunc, "l") end, stackOffset = { enterDebugLoop = 6, halt = 7, step = 4, stepDebugLoop = 7 } } end local sendMessage = function(msg) local body = json.encode(msg) if dumpCommunication then logToDebugConsole("[SENDING] " .. valueToString(msg)) end sendFully("#" .. #body .. "\n" .. body) end local recvMessage = function() local header = sock:receive("*l") if header == nil then return nil end if string.sub(header, 1, 1) ~= "#" then error("헤더 이상함:" .. header) end local bodySize = tonumber(header:sub(2)) local body = sock:receive(bodySize) return json.decode(body) end local debugLoop = function() storedVariables = {} nextVarRef = 1 while true do local msg = recvMessage() if msg then if dumpCommunication then logToDebugConsole("[RECEIVED] " .. valueToString(msg), "stderr") end local fn = handlers[msg.command] if not fn then goto lbl_38 end do local rv = fn(msg) if rv == "CONTINUE" then break end end goto lbl_38 else if redirectedPrintFunction then _G.print = redirectedPrintFunction end break end ::lbl_38:: end storedVariables = {} nextVarRef = 1 end local sockArray = {} function debuggee.start(jsonLib, config) json = jsonLib assert(jsonLib) config = config or {} local connectTimeout = config.connectTimeout or 5.0 local controllerHost = config.controllerHost or "localhost" local controllerPort = config.controllerPort or 56789 onError = config.onError or defaultOnError addUserdataVar = config.addUserdataVar or function() return end local redirectPrint = config.redirectPrint or false dumpCommunication = config.dumpCommunication or false ignoreFirstFrameInC = config.ignoreFirstFrameInC or false if not config.luaStyleLog then function valueToString(value) return json.encode(value) end end local breakerType if debug.sethalt then breaker = createHaltBreaker() breakerType = "halt" else breaker = createPureBreaker() breakerType = "pure" end local err sock, err = socket.tcp() if not sock then error(err) end sockArray = {sock} if sock.settimeout then sock:settimeout(connectTimeout) end local res, err = sock:connect(controllerHost, tostring(controllerPort)) if not res then sock:close() sock = nil return false, breakerType end if sock.settimeout then sock:settimeout() end sock:setoption("tcp-nodelay", true) local initMessage = recvMessage() assert(initMessage and initMessage.command == "welcome") sourceBasePath = initMessage.sourceBasePath directorySeperator = initMessage.directorySeperator if redirectPrint then redirectedPrintFunction = _G.print function _G.print(...) local t = { n = select("#", ...), ... } for i = 1, #t do t[i] = tostring(t[i]) end sendEvent("output", { category = "stdout", output = table.concat(t, "\t") .. "\n" }) end end debugLoop() return true, breakerType end function debuggee.poll() if not sock then return end while true do local r, w, e = socket.select(sockArray, nil, 0) if e == "timeout" then break end local msg = recvMessage() if msg then if dumpCommunication then logToDebugConsole("[POLL-RECEIVED] " .. valueToString(msg), "stderr") end if msg.command == "pause" then debuggee.enterDebugLoop(1) return end local fn = handlers[msg.command] if fn then local rv = fn(msg) else end else break end end end local getCoroutineId = function(c) local threadIdHex = string.sub(tostring(c), 9) return tonumber(threadIdHex, 16) end function debuggee.addCoroutine(c) local cid = getCoroutineId(c) coroutineSet[cid] = c breaker.coroutineAdded(c) end local sendSuccess = function(req, body) sendMessage({ command = req.command, success = true, request_seq = req.seq, type = "response", body = body }) end local sendFailure = function(req, msg) sendMessage({ command = req.command, success = false, request_seq = req.seq, type = "response", message = msg }) end function sendEvent(eventName, body) sendMessage({ event = eventName, type = "event", body = body }) end local currentThreadId = function() return 0 end local startDebugLoop = function() sendEvent("stopped", { reason = "breakpoint", threadId = currentThreadId(), allThreadsStopped = true }) local status, err = pcall(debugLoop) if not status then onError(err) end end function _G.__halt__() baseDepth = breaker.stackOffset.halt startDebugLoop() end function debuggee.enterDebugLoop(depthOrCo, what) if sock == nil then return false end if what then sendEvent("output", {category = "stderr", output = what}) end if type(depthOrCo) == "thread" then baseDepth = 0 debugTargetCo = depthOrCo elseif type(depthOrCo) == "table" then baseDepth = depthOrCo.depth or 0 debugTargetCo = depthOrCo.co else baseDepth = (depthOrCo or 0) + breaker.stackOffset.enterDebugLoop debugTargetCo = nil end startDebugLoop() return true end function debuggee.print(category, ...) if sock == nil then return false end local t = { ... } for i = 1, #t do t[i] = tostring(t[i]) end local categoryVscodeConsole = "stdout" if category == "warning" then categoryVscodeConsole = "console" elseif category == "error" then categoryVscodeConsole = "stderr" elseif category == "log" then categoryVscodeConsole = "stdout" end sendEvent("output", { category = categoryVscodeConsole, output = table.concat(t, "\t") .. "\n" }) end function handlers.setBreakpoints(req) local bpLines = {} for _, bp in ipairs(req.arguments.breakpoints) do bpLines[#bpLines + 1] = bp.line end local verifiedLines = breaker.setBreakpoints(req.arguments.source.path, bpLines) local breakpoints = {} for i, ln in ipairs(bpLines) do breakpoints[i] = { verified = verifiedLines[ln] ~= nil, line = verifiedLines[ln] } end sendSuccess(req, {breakpoints = breakpoints}) end function handlers.configurationDone(req) sendSuccess(req, {}) return "CONTINUE" end function handlers.threads(req) local c = coroutine.running() local mainThread = { id = currentThreadId(), name = c and tostring(c) or "main" } sendSuccess(req, { threads = {mainThread} }) end function handlers.stackTrace(req) assert(req.arguments.threadId == 0) local stackFrames = {} local firstFrame = (req.arguments.startFrame or 0) + baseDepth local lastFrame = req.arguments.levels and req.arguments.levels ~= 0 and firstFrame + req.arguments.levels - 1 or 9999 if ignoreFirstFrameInC then local info = debug_getinfo(firstFrame, "lnS") if info and info.what == "C" then firstFrame = firstFrame + 1 end end for i = firstFrame, lastFrame do local info = debug_getinfo(i, "lnS") if info == nil then break end local src = info.source if string.sub(src, 1, 1) == "@" then src = string.sub(src, 2) end local name if info.name then name = info.name .. " (" .. (info.namewhat or "?") .. ")" else name = "?" end local sframe = { name = name, source = { name = nil, path = Path.toAbsolute(sourceBasePath, src) }, column = 1, line = info.currentline or 1, id = i } stackFrames[#stackFrames + 1] = sframe end sendSuccess(req, {stackFrames = stackFrames}) end local scopeTypes = { Locals = 1, Upvalues = 2, Globals = 3 } function handlers.scopes(req) local depth = req.arguments.frameId local scopes = {} local addScope = function(name) scopes[#scopes + 1] = { name = name, expensive = false, variablesReference = depth * 1000000 + scopeTypes[name] } end addScope("Locals") addScope("Upvalues") addScope("Globals") sendSuccess(req, {scopes = scopes}) end local registerVar = function(varNameCount, name_, value, noQuote) local ty = type(value) local name if type(name_) == "number" then name = "[" .. name_ .. "]" else name = tostring(name_) end if varNameCount[name] then varNameCount[name] = varNameCount[name] + 1 name = name .. " (" .. varNameCount[name] .. ")" else varNameCount[name] = 1 end local item = {name = name, type = ty} if ty == "string" and not noQuote then item.value = "\"" .. value .. "\"" else item.value = tostring(value) end if ty == "table" or ty == "function" or ty == "userdata" then storedVariables[nextVarRef] = value item.variablesReference = nextVarRef nextVarRef = nextVarRef + 1 else item.variablesReference = -1 end return item end function handlers.variables(req) local varRef = req.arguments.variablesReference local variables = {} local varNameCount = {} local addVar = function(name, value, noQuote) variables[#variables + 1] = registerVar(varNameCount, name, value, noQuote) end if 1000000 <= varRef then local depth = math.floor(varRef / 1000000) local scopeType = varRef % 1000000 if scopeType == scopeTypes.Locals then for i = 1, 9999 do local name, value = debug_getlocal(depth, i) if name == nil then break end addVar(name, value, nil) end elseif scopeType == scopeTypes.Upvalues then local info = debug_getinfo(depth, "f") if info and info.func then for i = 1, 9999 do local name, value = debug.getupvalue(info.func, i) if name == nil then break end addVar(name, value, nil) end end elseif scopeType == scopeTypes.Globals then for name, value in pairs(_G) do addVar(name, value) end table.sort(variables, function(a, b) return a.name < b.name end) end else local var = storedVariables[varRef] if type(var) == "table" then for k, v in pairs(var) do addVar(k, v) end table.sort(variables, function(a, b) local aNum, aMatched = string.gsub(a.name, "^%[(%d+)%]$", "%1") local bNum, bMatched = string.gsub(b.name, "^%[(%d+)%]$", "%1") if aMatched == 1 and bMatched == 1 then return tonumber(aNum) < tonumber(bNum) elseif aMatched == bMatched then return a.name < b.name else return aMatched < bMatched end end) elseif type(var) == "function" then local info = debug.getinfo(var, "S") addVar("(source)", tostring(info.short_src), true) addVar("(line)", info.linedefined) for i = 1, 9999 do local name, value = debug.getupvalue(var, i) if name == nil then break end addVar(name, value) end elseif type(var) == "userdata" then addUserdataVar(var, addVar) end local mt = getmetatable(var) if mt then addVar("(metatable)", mt) end end sendSuccess(req, {variables = variables}) end function handlers.continue(req) sendSuccess(req, {}) return "CONTINUE" end local stackHeight = function() for i = 1, 9999999 do if debug_getinfo(i, "") == nil then return i end end end local stepTargetHeight local step = function() if stepTargetHeight == nil or stackHeight() <= stepTargetHeight then breaker.setLineBreak(nil) baseDepth = breaker.stackOffset.stepDebugLoop startDebugLoop() end end function handlers.next(req) stepTargetHeight = stackHeight() - breaker.stackOffset.step breaker.setLineBreak(step) sendSuccess(req, {}) return "CONTINUE" end function handlers.stepIn(req) stepTargetHeight = nil breaker.setLineBreak(step) sendSuccess(req, {}) return "CONTINUE" end function handlers.stepOut(req) stepTargetHeight = stackHeight() - (breaker.stackOffset.step + 1) breaker.setLineBreak(step) sendSuccess(req, {}) return "CONTINUE" end function handlers.evaluate(req) local sourceCode = req.arguments.expression if string.sub(sourceCode, 1, 1) == "!" then sourceCode = string.sub(sourceCode, 2) else sourceCode = "return (" .. sourceCode .. ")" end local depth = req.arguments.frameId local tempG = {} local declared = {} local set = function(k, v) tempG[k] = v declared[k] = true end for name, value in pairs(_G) do set(name, value) end if depth then local info = debug_getinfo(depth, "f") if info and info.func then for i = 1, 9999 do local name, value = debug.getupvalue(info.func, i) if name == nil then break end set(name, value) end end for i = 1, 9999 do local name, value = debug_getlocal(depth, i) if name == nil then goto lbl_80 end set(name, value) end else end ::lbl_80:: local mt = { __newindex = function() error("assignment not allowed", 2) end, __index = function(t, k) if not declared[k] then error("not declared", 2) end end } setmetatable(tempG, mt) local fn, err = (loadstring or load)(sourceCode, "X", nil, tempG) if fn == nil then sendFailure(req, string.gsub(err, "^%[string %\"X%\"%]%:%d+%: ", "")) return end if setfenv ~= nil then setfenv(fn, tempG) end local success, aux = pcall(fn) if not success then aux = aux or "" sendFailure(req, string.gsub(aux, "^%[string %\"X%\"%]%:%d+%: ", "")) return end local varNameCount = {} local item = registerVar(varNameCount, "", aux) sendSuccess(req, { result = item.value, type = item.type, variablesReference = item.variablesReference }) end return debuggee