From ef7464f70def93a0c0b25fc74a0339039b10040b Mon Sep 17 00:00:00 2001 From: Ryan Ward Date: Sun, 15 Oct 2023 13:21:57 -0400 Subject: [PATCH] Working on getting pseudoThreading tests to work --- init.lua | 3 +- integration/lanesManager/extensions.lua | 10 +- integration/lanesManager/init.lua | 1 + integration/loveManager/extensions.lua | 4 +- integration/loveManager/init.lua | 2 + integration/loveManagerold/extensions.lua | 4 +- integration/loveManagerold/init.lua | 2 + integration/priorityManager/init.lua | 2 +- integration/pseudoManager/extensions.lua | 8 +- integration/pseudoManager/init.lua | 1 + integration/sharedExtensions/init.lua | 5 +- tests/main.lua | 8 +- tests/threadtests.lua | 26 +- tests/vscode-debuggee.lua | 1102 +++++++++++++++++++++ 14 files changed, 1150 insertions(+), 28 deletions(-) create mode 100644 tests/vscode-debuggee.lua diff --git a/init.lua b/init.lua index 318ff2a..928237e 100644 --- a/init.lua +++ b/init.lua @@ -1423,7 +1423,7 @@ function thread:newFunctionBase(generator, holdme, TYPE) end } t.OnDeath(function(...) temp.OnReturn:Fire(...) end) - t.OnError(function(self,err) temp.OnError:Fire(err) end) + t.OnError(function(self,err) temp.OnError:Fire(err) temp.OnError(multi.error) end) t.linkedFunction = temp t.statusconnector = temp.OnStatus return temp @@ -1885,6 +1885,7 @@ function multi:newService(func) -- Priority managed threads end) th.OnError = c.OnError -- use the threads onerror as our own + th.OnError(multi.error) function c.Destroy() th:kill() diff --git a/integration/lanesManager/extensions.lua b/integration/lanesManager/extensions.lua index 4ff3605..618762f 100644 --- a/integration/lanesManager/extensions.lua +++ b/integration/lanesManager/extensions.lua @@ -207,9 +207,9 @@ function multi:newSystemThreadedJobQueue(n) local jid = table.remove(dat, 1) local args = table.remove(dat, 1) queueReturn:push{jid, funcs[name](args[1],args[2],args[3],args[4],args[5],args[6],args[7],args[8]), queue} - end).OnError(multi.error) + end) end - end).OnError(multi.error) + end) thread:newThread("DoAllHandler",function() while true do local dat = thread.hold(function() @@ -225,7 +225,7 @@ function multi:newSystemThreadedJobQueue(n) end end end - end).OnError(multi.error) + end) thread:newThread("IdleHandler",function() while true do thread.hold(function() @@ -233,9 +233,9 @@ function multi:newSystemThreadedJobQueue(n) end) THREAD.sleep(.01) end - end).OnError(multi.error) + end) multi:mainloop() - end,i).OnError(multi.error) + end,i) end function c:Hold(opt) diff --git a/integration/lanesManager/init.lua b/integration/lanesManager/init.lua index 2f3e646..82d5610 100644 --- a/integration/lanesManager/init.lua +++ b/integration/lanesManager/init.lua @@ -130,6 +130,7 @@ function multi:newSystemThread(name, func, ...) c.OnDeath = multi:newConnection() c.OnError = multi:newConnection() GLOBAL["__THREADS__"] = livingThreads + c.OnError(multi.error) if self.isActor then self:create(c) diff --git a/integration/loveManager/extensions.lua b/integration/loveManager/extensions.lua index f1a7f27..9a22c5b 100644 --- a/integration/loveManager/extensions.lua +++ b/integration/loveManager/extensions.lua @@ -172,6 +172,7 @@ function multi:newSystemThreadedJobQueue(n) multi:newSystemThread("JobQueue_"..jqc.."_worker_"..i,function(jqc) local multi, thread = require("multi"):init() require("love.timer") + love.timer.sleep(1) local clock = os.clock local funcs = THREAD.createTable("__JobQueue_"..jqc.."_table") local queue = THREAD.waitFor("__JobQueue_"..jqc.."_queue") @@ -208,11 +209,12 @@ function multi:newSystemThreadedJobQueue(n) local id = table.remove(dat,1) local tab = {funcs[name](multi.unpack(dat))} table.insert(tab,1,id) + --local test = queueReturn.push queueReturn:push(tab) end) end end - end).OnError(multi.error) + end) thread:newThread("Idler",function() while true do thread.yield() diff --git a/integration/loveManager/init.lua b/integration/loveManager/init.lua index 92cba27..0bc062b 100644 --- a/integration/loveManager/init.lua +++ b/integration/loveManager/init.lua @@ -60,6 +60,8 @@ function multi:newSystemThread(name, func, ...) table.insert(threads, c) + c.OnError(multi.error) + if self.isActor then self:create(c) else diff --git a/integration/loveManagerold/extensions.lua b/integration/loveManagerold/extensions.lua index 36931b3..91f5819 100644 --- a/integration/loveManagerold/extensions.lua +++ b/integration/loveManagerold/extensions.lua @@ -300,7 +300,7 @@ function multi:newSystemThreadedConnection(name) -- This shouldn't be the case end end - end).OnError(multi.error) + end) return self end @@ -377,7 +377,7 @@ function multi:newSystemThreadedConnection(name) c.proxy_conn:Fire(multi.unpack(item[2])) end end - end).OnError(multi.error) + end) --- ^^^ This will only exist in the init thread THREAD.package(name,c) diff --git a/integration/loveManagerold/init.lua b/integration/loveManagerold/init.lua index f913103..2ab3061 100644 --- a/integration/loveManagerold/init.lua +++ b/integration/loveManagerold/init.lua @@ -104,6 +104,8 @@ function multi:newSystemThread(name, func, ...) c.stab.returns = nil end end) + + c.OnError(multi.error) if self.isActor then self:create(c) diff --git a/integration/priorityManager/init.lua b/integration/priorityManager/init.lua index f3fe45c..ab5e6da 100644 --- a/integration/priorityManager/init.lua +++ b/integration/priorityManager/init.lua @@ -210,7 +210,7 @@ local function init_chronos() thread.yield() priorityManager.run() end - end).OnError(multi.error) + end) end if chronos then diff --git a/integration/pseudoManager/extensions.lua b/integration/pseudoManager/extensions.lua index 5e6a8a2..fe0abd8 100644 --- a/integration/pseudoManager/extensions.lua +++ b/integration/pseudoManager/extensions.lua @@ -35,15 +35,16 @@ end function multi:newSystemThreadedQueue(name) local c = {} + c.data = {} c.Type = multi.registerType("s_queue") function c:push(v) table.insert(self,v) end function c:pop() - return table.remove(self,1) + return table.remove(self.data,1) end function c:peek() - return self[1] + return self.data[1] end function c:init() return self @@ -156,6 +157,7 @@ function multi:newSystemThreadedJobQueue(n) end) for i=1,c.cores do multi:newSystemThread("JobQueue_"..jqc.."_worker_"..i,function(jqc) + local GLOBAL, THREAD = require("multi.integration.pseudoManager"):init() local multi, thread = require("multi"):init() local clock = os.clock local funcs = THREAD.waitFor("__JobQueue_"..jqc.."_table") @@ -197,7 +199,7 @@ function multi:newSystemThreadedJobQueue(n) end) end end - end).OnError(multi.error) + end) thread:newThread("Idler",function() while true do thread.yield() diff --git a/integration/pseudoManager/init.lua b/integration/pseudoManager/init.lua index ec2f70e..e1b7b03 100644 --- a/integration/pseudoManager/init.lua +++ b/integration/pseudoManager/init.lua @@ -92,6 +92,7 @@ function multi:newSystemThread(name, func, ...) local th = thread:newISOThread(name, func, env, ...) th.Type = multi.registerType("s_thread", "pseudoThreads") + th.OnError(multi.error) id = id + 1 diff --git a/integration/sharedExtensions/init.lua b/integration/sharedExtensions/init.lua index a5b7caa..a048b5f 100644 --- a/integration/sharedExtensions/init.lua +++ b/integration/sharedExtensions/init.lua @@ -92,6 +92,8 @@ function multi:newProxy(list) local sref = table.remove(data, 1) local ret + print(_G[list[0]], func) + if sref then ret = {_G[list[0]][func](_G[list[0]], multi.unpack(data))} else @@ -115,7 +117,7 @@ function multi:newProxy(list) end) end end - end).OnError(multi.error) + end) return self else local function copy(obj) @@ -143,6 +145,7 @@ function multi:newProxy(list) setmetatable(v[2],getmetatable(multi:newConnection())) else self[v] = thread:newFunction(function(self,...) + multi.print("Pushing: " .. v) if self == me then me.send:push({v, true, ...}) else diff --git a/tests/main.lua b/tests/main.lua index fb4e2f9..a21d620 100644 --- a/tests/main.lua +++ b/tests/main.lua @@ -1,8 +1,12 @@ package.path = "../?/init.lua;../?.lua;"..package.path -require("runtests") -require("threadtests") +-- require("runtests") +-- require("threadtests") -- Allows you to run "love tests" which runs the tests +multi, thread = require("multi"):init() +GLOBAL, THREAD = require("multi.integration.loveManager"):init() + + function love.update() multi:uManager() end \ No newline at end of file diff --git a/tests/threadtests.lua b/tests/threadtests.lua index bb5754b..f3298a2 100644 --- a/tests/threadtests.lua +++ b/tests/threadtests.lua @@ -1,4 +1,5 @@ -package.path = "../?/init.lua;../?.lua;"..package.path +package.path = "D:/VSCWorkspace/?/init.lua;D:/VSCWorkspace/?.lua;"..package.path +package.cpath = "C:/luaInstalls/lua5.4/lib/lua/5.4/?/core.dll;" .. package.cpath multi, thread = require("multi"):init{error=true,warning=true,print=true}--{priority=true} proc = multi:newProcessor("Thread Test",true) local LANES, LOVE, PSEUDO = 1, 2, 3 @@ -37,12 +38,12 @@ THREAD.setENV({ }) multi:newThread("Scheduler Thread",function() - multi:newThread(function() - thread.sleep(30) - print("Timeout tests took longer than 30 seconds") - multi:Stop() - os.exit(1) - end) + -- multi:newThread(function() + -- thread.sleep(30) + -- print("Timeout tests took longer than 30 seconds") + -- multi:Stop() + -- os.exit(1) + -- end) queue = multi:newSystemThreadedQueue("Test_Queue"):init() multi:newSystemThread("Test_Thread_0", function() @@ -201,7 +202,7 @@ multi:newThread("Scheduler Thread",function() print(THREAD_NAME, "Got loop...") end) multi:mainloop() - end, tloop:getTransferable()).OnError(multi.error) + end, tloop:getTransferable()) multi.print("tloop", tloop.Type) multi.print("tloop.OnLoop", tloop.OnLoop.Type) @@ -213,12 +214,13 @@ multi:newThread("Scheduler Thread",function() thread.hold(tloop.OnLoop) multi.print("Held on proxy connection... twice") proxy_test = true - end).OnError(multi.error) + end) thread:newThread(function() + print("While Test!") while true do thread.hold(tloop.OnLoop) - print(THREAD_NAME,"Loopy") + print(THREAD_NAME,"Local Loopy") end end) @@ -228,7 +230,7 @@ multi:newThread("Scheduler Thread",function() t, val = thread.hold(function() return proxy_test - end,{sleep=5}) + end--[[,{sleep=5}]]) -- No timeouts if val == multi.TIMEOUT then multi.error("SystemThreadedProcessor/Proxies: Failed") @@ -242,7 +244,7 @@ multi:newThread("Scheduler Thread",function() we_good = true multi:Stop() -- Needed in love2d tests to stop the main runner os.exit(0) -end).OnError(multi.error) +end) multi.OnExit(function(err_or_errorcode) print("Error Code: ", err_or_errorcode) diff --git a/tests/vscode-debuggee.lua b/tests/vscode-debuggee.lua new file mode 100644 index 0000000..61b47fb --- /dev/null +++ b/tests/vscode-debuggee.lua @@ -0,0 +1,1102 @@ +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