Files
StellaSora_DataLua/lua/vscode-debuggee.lua
T
SL1900 5e0d58cfad Initial version - 1.2.0.60
EN: 1.2.0.60
CN: 1.2.0.61
JP: 1.2.0.63
KR: 1.2.0.67
2025-12-03 01:00:00 +09:00

914 lines
21 KiB
Lua

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