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 local breaker local sendEvent local dumpCommunication = false local ignoreFirstFrameInC = false local debugTargetCo = nil local redirectedPrintFunction = nil local onError = nil local addUserdataVar = nil local function defaultOnError(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 coroutine.create = function(f) local c = cocreate(f) debuggee.addCoroutine(c) return c end ------------------------------------------------------------------------------- local function debug_getinfo(depth, what) if debugTargetCo then return debug.getinfo(debugTargetCo, depth, what) else return debug.getinfo(depth + 1, what) end end ------------------------------------------------------------------------------- local function debug_getlocal(depth, i) if debugTargetCo then return debug.getlocal(debugTargetCo, depth, i) else return debug.getlocal(depth + 1, i) end end ------------------------------------------------------------------------------- local DO_TEST = false ------------------------------------------------------------------------------- -- chunkname matching {{{ local function getMatchCount(a, b) local n = math.min(#a, #b) for i = 0, n - 1 do if a[#a - i] == b[#b - i] then -- pass 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 function splitChunkName(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 -- chunkname matching }}} -- path control {{{ 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) -- UNC 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 -- A/../ -> (empty) path,k = path:gsub(np_pat1, '', 1) until k == 0 if path == '' then path = '.' end return path end function Path.concat(a, b) -- normalize a local lastChar = string.sub(a, #a, #a) if not (lastChar == '/' or lastChar == '\\') then a = a .. directorySeperator end -- normalize b 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 -- path control }}} local coroutineSet = {} setmetatable(coroutineSet, { __mode = 'v' }) ------------------------------------------------------------------------------- -- network utility {{{ local function sendFully(str) local first = 1 while first <= #str do local sent = sock:send(str, first) if sent and sent > 0 then first = first + sent; else error('sock:send() returned < 0') end end end -- send log to debug console local function logToDebugConsole(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 -- pure mode {{{ local function createHaltBreaker() -- chunkname matching { local loadedChunkNameMap = {} for chunkname, _ in pairs(debug.getchunknames()) do loadedChunkNameMap[chunkname] = splitChunkName(chunkname) end local function findMostSimilarChunkName(path) local splitedReqPath = splitChunkName(path) local maxMatchCount = 0 local foundChunkName = nil for chunkName, splitted in pairs(loadedChunkNameMap) do local count = getMatchCount(splitedReqPath, splitted) if (count > maxMatchCount) then maxMatchCount = count foundChunkName = chunkName end end return foundChunkName end -- chunkname matching } local lineBreakCallback = nil local function updateCoroutineHook(c) if lineBreakCallback then sethook(c, lineBreakCallback, 'l') else sethook(c) end end local function sethalt(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 function createPureBreaker() local lineBreakCallback = nil local breakpointsPerPath = {} local chunknameToPathCache = {} local function chunkNameToPath(chunkname) local cached = chunknameToPathCache[chunkname] if cached then return cached end local splitedReqPath = splitChunkName(chunkname) local maxMatchCount = 0 local foundPath = nil for path, _ in pairs(breakpointsPerPath) do local splitted = splitChunkName(path) local count = getMatchCount(splitedReqPath, splitted) if (count > maxMatchCount) then maxMatchCount = count foundPath = path end end if foundPath then chunknameToPathCache[chunkname] = foundPath end return foundPath end local entered = false local function hookfunc() 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) if path then path = string.lower(path) end 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 if path then path = string.lower(path) end 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 -- pure mode }}} -- 센드는 블럭이어도 됨. local function sendMessage(msg) local body = json.encode(msg) if dumpCommunication then logToDebugConsole('[SENDING] ' .. valueToString(msg)) end sendFully('#' .. #body .. '\n' .. body) end -- 리시브는 블럭이 아니어야 할 거 같은데... 음... 블럭이어도 괜찮나? local function recvMessage() 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 -- network utility }}} ------------------------------------------------------------------------------- local function debugLoop() 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 fn then local rv = fn(msg) -- continue인데 break하는 게 역설적으로 느껴지지만 -- 디버그 루프를 탈출(break)해야 정상 실행 흐름을 계속(continue)할 수 있지.. if (rv == 'CONTINUE') then break; end else --print('UNKNOWN DEBUG COMMAND: ' .. tostring(msg.command)) end else -- 디버그 중에 디버거가 떨어졌다. -- print펑션을 리다이렉트 한경우에는 원래대로 돌려놓는다 if redirectedPrintFunction then _G.print = redirectedPrintFunction end break end 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 valueToString = function(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 -- 디버거가 떨어질때를 대비해서 보관한다 _G.print = function(...) 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' -- Same as default "print" output end new line. }) end end debugLoop() return true, breakerType end ------------------------------------------------------------------------------- function debuggee.poll() if not sock then return end -- Processes commands in the queue. -- Immediately returns when the queue is/became empty. 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) -- Ignores rv, because this loop never blocks except explicit pause command. else --print('POLL-UNKNOWN DEBUG COMMAND: ' .. tostring(msg.command)) end else break end end end ------------------------------------------------------------------------------- local function getCoroutineId(c) -- 'thread: 011DD5B0' -- 12345678^ 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 function sendSuccess(req, body) sendMessage({ command = req.command, success = true, request_seq = req.seq, type = "response", body = body }) end ------------------------------------------------------------------------------- local function sendFailure(req, msg) sendMessage({ command = req.command, success = false, request_seq = req.seq, type = "response", message = msg }) end ------------------------------------------------------------------------------- sendEvent = function(eventName, body) sendMessage({ event = eventName, type = "event", body = body }) end ------------------------------------------------------------------------------- local function currentThreadId() --[[ local threadId = 0 if coroutine.running() then end return threadId ]] return 0 end ------------------------------------------------------------------------------- local function startDebugLoop() sendEvent( 'stopped', { reason = 'breakpoint', threadId = currentThreadId(), allThreadsStopped = true }) local status, err = pcall(debugLoop) if not status then onError(err) end end ------------------------------------------------------------------------------- _G.__halt__ = function() 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 for printing on vscode debug console -- First parameter 'category' can colorizes print text 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' -- yellow elseif category == 'error' then categoryVscodeConsole = 'stderr' -- red elseif category == 'log' then categoryVscodeConsole = 'stdout' -- white end sendEvent( 'output', { category = categoryVscodeConsole, output = table.concat(t, '\t') .. '\n' -- Same as default "print" output end new line. }) end ------------------------------------------------------------------------------- -- ★★★ https://github.com/Microsoft/vscode-debugadapter-node/blob/master/protocol/src/debugProtocol.ts ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- 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 firstframe function of stack is C function, ignore it. 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 --print(json.encode(info)) 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 function addScope(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 function registerVar(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 function addVar(name, value, noQuote) variables[#variables + 1] = registerVar(varNameCount, name, value, noQuote) end if (varRef >= 1000000) then -- Scope. 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 -- Expansion. 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 -- both are numbers. compare numerically. return tonumber(aNum) < tonumber(bNum) elseif aMatched == bMatched then -- both are strings. compare alphabetically. return a.name < b.name else -- string comes first. 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 function stackHeight() for i = 1, 9999999 do if (debug_getinfo(i, '') == nil) then return i end end end ------------------------------------------------------------------------------- local stepTargetHeight = nil local function step() 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 function set(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 break end set(name, value) end else -- VSCode가 depth를 안 보낼 수도 있다. -- 특정 스택 프레임을 선택하지 않은, 전역 이름만 조회하는 경우이다. end 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) -- 파싱 -- loadstring for Lua 5.1 -- load for Lua 5.2 and 5.3(supports the private environment's load function) 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 -- Only for Lua 5.1 setfenv(fn, tempG) end local success, aux = pcall(fn) if not success then aux = aux or '' -- Execution of 'error()' returns nil as aux 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