diff --git a/.gitignore b/.gitignore index 75441a5..116ce8c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ crashsave* *.lsvs config.lua *.sfc +luaenv/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7950bc0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule ".\\3rdparty\\luasocket"] + path = .\\3rdparty\\luasocket + url = https://github.com/diegonehab/luasocket diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..19663ae --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "launch-lua", + "type": "lua", + "request": "launch", + "workingDirectory": "${workspaceRoot}", + "sourceBasePath": "${workspaceRoot}", + "executable": "lsnes-bsnes.exe", + "arguments": "--lua=${workspaceFolder}/neat-donk.lua", + "listenPublicly": false, + "listenPort": 56789, + "encoding": "UTF-8", + "env": {} + } + ] +} \ No newline at end of file diff --git a/3rdparty/luasocket b/3rdparty/luasocket new file mode 160000 index 0000000..5b18e47 --- /dev/null +++ b/3rdparty/luasocket @@ -0,0 +1 @@ +Subproject commit 5b18e475f38fcf28429b1cc4b17baee3b9793a62 diff --git a/install.lua b/install.lua new file mode 100644 index 0000000..b5bc50e --- /dev/null +++ b/install.lua @@ -0,0 +1,95 @@ +local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1") + +local Promise = dofile(base.."/promise.lua") +local makeproxy = dofile(base.."/makeproxy.lua") +local util = dofile(base.."/util.lua") + +--- Echo a command, run it, and display its results +--- @param cmd string The command to execute +--- @param workdir string The working directory +local function doCmd(cmd, workdir) + local poppet = util.doCmd(cmd, workdir) + print(poppet:read("*a")) + poppet:close() +end + +--- Timer loop that triggers promises +local function timer() + Promise.update() + set_timer_timeout(1) +end + +callback.register('timer', timer) +set_timer_timeout(1) + +--- Create directory +--- @param dir string Path of directory to create +local function mkdir (dir) + local poppet = util.mkdir(dir) + print(poppet:read("*a")) + poppet:close() +end + +local waitKeyDownQueue = {} +--- Triggers on pressing a key, but only once +--- @param key string Key to monitor +--- @return Promise Promise A Promise that resolves when the key is pressed +local function waitKeyDown(key) + input.keyhook(key, true) + + local item = { + promise = Promise.new(), + key = key, + } + table.insert(waitKeyDownQueue, item) + + return item.promise +end + +local function keyhook(key, state) + for i=#waitKeyDownQueue,1,-1 do + local hook = waitKeyDownQueue[i] + if hook.key == key and state.value == 1 then + table.remove(waitKeyDownQueue, i).promise:resolve(key) + end + end +end + +callback.register('keyhook', keyhook) + + +local luabase = util.luaenv.."/lua" +local luabin = luabase.."/bin" +local lualib = luabase.."/lib" + +local luazip = util.luaenv.."/lua.zip" +local lualibzip = util.luaenv.."/lua_lib.zip" + +if util.isWin then + print("Creating luaenv directory...") + mkdir(util.luaenv) + + local xzzip = util.luaenv..'/xz.zip' + local xz = util.luaenv..'/xz' + + print("Downloading xz...") + util.downloadFile("https://tukaani.org/xz/xz-5.2.5-windows.zip", xzzip) + mkdir(xz) + util.unzip(xzzip, xz) + + print("Downloading Lua...") + util.downloadFile("https://downloads.sourceforge.net/project/luabinaries/5.2.4/Tools%20Executables/lua-5.2.4_Win32_bin.zip", luazip) + util.downloadFile("https://versaweb.dl.sourceforge.net/project/luabinaries/5.2.4/Windows%20Libraries/Dynamic/lua-5.2.4_Win32_dllw6_lib.zip", lualibzip) + mkdir(luabase) + mkdir(luabin) + util.unzip(luazip, luabin) + util.unzip(lualibzip, luabase) + mkdir(lualib) + os.rename(luabase.."/lua52.dll", lualib.."/lua52.dll") + + -- FIXME Linux will still need this tho? + makeproxy() +else + -- FIXME + print('Please install lua and xz manually...') +end \ No newline at end of file diff --git a/makeproxy.lua b/makeproxy.lua new file mode 100644 index 0000000..e54f2b0 --- /dev/null +++ b/makeproxy.lua @@ -0,0 +1,103 @@ +local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1") + +local util = dofile(base.."/util.lua") + +local CFILE = util.luaenv.."/luaproxy.c" -- Name of the C file for the proxy DLL +local SYMBOLS = util.luaenv.."/luasymbols.h" -- Name of the file of Lua symbols +local LUADLL = util.luaenv.."/lua/lib/lua52.dll" -- Name of a real Lua DLL (to get exported symbols) + +---------------------------------------------------------------------- +return function() + local cfile = assert(io.open(CFILE, "w")) + cfile:write [=[ + #include + + static struct { + #define SYMBOL(name) FARPROC name; + #include "luasymbols.h" + #undef SYMBOL + } + s_funcs; + + /* Macro for defining a proxy function. + This is a direct jump (single "jmp" assembly instruction"), + preserving stack and return address. + The following uses MSVC inline assembly which may not be + portable with other compilers. + */ + + #define SYMBOL(name) \ + void __declspec(dllexport,naked) name() { __asm { jmp s_funcs.name } } + #include "luasymbols.h" + #undef SYMBOL + + BOOL APIENTRY + DllMain(HANDLE module, DWORD reason, LPVOID reserved) + { + HANDLE h = GetModuleHandle(NULL); + #define SYMBOL(name) s_funcs.name = GetProcAddress(h, #name); + #include "luasymbols.h" + #undef SYMBOL + return TRUE; + } + ]=] + cfile:close() + + local pexportstar = util.luaenv.."/pexports.tar.xz" + + util.downloadFile('https://downloads.sourceforge.net/project/mingw/MinGW/Extension/pexports/pexports-0.47/pexports-0.47-mingw32-bin.tar.xz', pexportstar) + util.unzip(pexportstar, util.luaenv) + + local pexports = util.luaenv.."/bin/pexports.exe" + + local symbols = util.doCmd('"'..pexports..'" "'..LUADLL..'"', base) + local symfile = io.open(SYMBOLS, "w") + for sym in symbols:lines() do + -- Skip the LIBRARY and EXPORTS lines + local start = sym:sub(1,3) + if start ~= "LIB" and start ~= "EXP" then + symfile:write("SYMBOL("..sym..")\n") + end + end + symbols:close() + symfile:close() + + local hostArch = os.getenv('PROCESSOR_ARCHITECTURE'):gsub("AMD", "x") + + local arch = "" + if hostArch == 'x86' then + arch = 'x86' + else + arch = 'x64_x86' + end + + local vswhere = util.luaenv..'/vswhere.exe' + util.downloadFile('https://github.com/microsoft/vswhere/releases/download/2.8.4/vswhere.exe', vswhere) + + local poppet = util.doCmd('"'..vswhere..'" -products "*" -latest -property installationPath', util.luaenv) + local vsPath = poppet:read("*l") + poppet:read("*a") + poppet:close() + + print("Visual Studio Installation path: "..vsPath) + + local poppet = util.doCmd([[powershell "(Get-ChildItem -Recurse ']]..vsPath..[[' -Filter 'vcvarsall.bat').FullName"]]) + local vcvarsallPath = poppet:read("*l") + poppet:close() + + print("vcvarsall.bat Path: "..vcvarsallPath) + + poppet = util.doCmd( + '"'..vcvarsallPath..'" '..arch..' 2>&1 && "cl.exe" /O2 /LD /GS- "'.. + CFILE..'" /link /out:"'.. + util.luaenv..'/lua52.dll" /nodefaultlib /entry:DllMain kernel32.lib 2>&1', + base, + { + VSCMD_ARG_HOST_ARCH = hostArch, + VSCMD_ARG_TGT_ARCH = 'x86' + } + ) + + print(poppet:read("*a")) + poppet:close() +end \ No newline at end of file diff --git a/neat-donk.lua b/neat-donk.lua index e927178..61147ff 100644 --- a/neat-donk.lua +++ b/neat-donk.lua @@ -5,7 +5,11 @@ local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1") local game = dofile(base.."/game.lua") local config = dofile(base.."/config.lua") local pool = dofile(base.."/pool.lua") -local util = dofile(base.."/util.lua") + +local json = require 'dkjson' +local debuggee = dofile(base..'/vscode-debuggee.lua') +local startResult, breakerType = debuggee.start(json) +print('debuggee start ->', startResult, breakerType) local statusLine = nil local statusColor = 0x0000ff00 @@ -31,4 +35,4 @@ pool.onRenderForm(function(form) gui.text(-500, guiHeight - 20, statusLine, 0x00000000) end end) -pool.run() +pool.run() \ No newline at end of file diff --git a/pool.lua b/pool.lua index 9cb109a..8c81a56 100644 --- a/pool.lua +++ b/pool.lua @@ -770,4 +770,4 @@ function _M.run(reset) end end -return _M +return _M \ No newline at end of file diff --git a/runner-wrapper.lua b/runner-wrapper.lua index a6e1525..198d585 100644 --- a/runner-wrapper.lua +++ b/runner-wrapper.lua @@ -129,4 +129,4 @@ return function() end return _M -end +end \ No newline at end of file diff --git a/state-test.lua b/state-test.lua index 01c8514..0b914a4 100644 --- a/state-test.lua +++ b/state-test.lua @@ -1,20 +1,3 @@ -PARTY_X = 0x7e0a2a -TILE_SIZE = 32 - -print(memory.readword(PARTY_X)) - -function on_post_rewind() - print("Async?") - print(memory.readword(PARTY_X)) -end - -function movement(addr, val) - if memory.readword(addr) > TILE_SIZE * 20 then - local rew = movie.to_rewind("pool/PiratePanic.lsmv") - movie.unsafe_rewind(rew) - print("Sync?") - print(memory.readword(PARTY_X)) - end -end - -memory2.WRAM:registerwrite(0x0a2a, movement) +for k,v in pairs(_SYSTEM._G) do + print(k) +end \ No newline at end of file diff --git a/util.lua b/util.lua index d5367f5..e1252d2 100644 --- a/util.lua +++ b/util.lua @@ -1,5 +1,77 @@ +local base = string.gsub(@@LUA_SCRIPT_FILENAME@@, "(.*[/\\])(.*)", "%1") + local _M = {} +_M.isWin = package.config:sub(1,1) == "\\" +_M.luaenv = base.."/luaenv" + +--- Echo a command, run it, and return the file handle +--- @param cmd string The command to execute +--- @param workdir string The working directory +function _M.doCmd(cmd, workdir, env) + local cmdParts = {} + if workdir ~= nil then + if _M.isWin then + table.insert(cmdParts, 'cd /d "'..workdir..'" &&') + else + table.insert(cmdParts, 'cd "'..workdir..'" &&') + end + end + if env ~= nil then + for k,v in pairs(env) do + if _M.isWin then + table.insert(cmdParts, string.format("set %s=%s&&", k, v)) + else + table.insert(cmdParts, string.format("%s='%s'", k, v)) + end + end + end + table.insert(cmdParts, cmd) + local fullCmd = table.concat(cmdParts, " ") + print(fullCmd) + return io.popen(fullCmd, 'r') +end + +--- Create directory +--- @param dir string Path of directory to create +function _M.mkdir (dir) + return _M.doCmd('mkdir "'..dir..'" 2>&1') +end + +--- Unzip a ZIP file with unzip or tar +--- @param zipfile string The ZIP file path +--- @param dest string Where to unzip the ZIP file. Beware ZIP bombs. +function _M.unzip (zipfile, dest) + local xzPath = 'xz' + if _M.isWin then + xzPath = _M.luaenv..'/xz/bin_i686-sse2/xz.exe' + end + + local poppet = nil + if zipfile:sub(-3):upper() == '.XZ' then + poppet = _M.doCmd('"'..xzPath..'" -d "'..zipfile..'"', dest) + print(poppet:read("*a")) + poppet:close() + zipfile = zipfile:sub(1, -3) + print(zipfile) + end + + poppet = _M.doCmd('unzip "'..zipfile..'" -d "'..dest.. + '" 2>&1 || tar -C "'..dest..'" -xvf "'..zipfile.. + '" 2>&1', nil) + print(poppet:read("*a")) + poppet:close() +end + +--- Download a url +--- @param url string URI of resource to download +--- @param dest string File to save resource to +function _M.downloadFile (url, dest) + local poppet = _M.doCmd('curl -sL "'..url..'" > "'..dest..'" || wget -qO- "'..url..'" > "'..dest..'"') + print(poppet:read("*a")) + poppet:close() +end + function _M.table_to_string(tbl) local result = "{" local keys = {} diff --git a/vscode-debuggee.lua b/vscode-debuggee.lua new file mode 100644 index 0000000..fc12842 --- /dev/null +++ b/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 \ No newline at end of file