From 53d9cda7616514841ad1684630f2e103aed6573d Mon Sep 17 00:00:00 2001 From: MCJack123 Date: Thu, 18 Aug 2022 16:28:10 -0400 Subject: [PATCH 1/2] Added CraftOS-PC debugger files --- data/computercraft/lua/debug/adapter.lua | 871 ++++++++++++++++++ data/computercraft/lua/debug/bios.lua | 258 ++++++ data/computercraft/lua/debug/console.lua | 32 + data/computercraft/lua/debug/debugger.lua | 150 +++ data/computercraft/lua/debug/profiler.lua | 150 +++ data/computercraft/lua/debug/releasenotes.lua | 74 ++ data/computercraft/lua/debug/showfile.lua | 263 ++++++ data/computercraft/lua/debug/startup.lua | 10 + 8 files changed, 1808 insertions(+) create mode 100644 data/computercraft/lua/debug/adapter.lua create mode 100644 data/computercraft/lua/debug/bios.lua create mode 100644 data/computercraft/lua/debug/console.lua create mode 100644 data/computercraft/lua/debug/debugger.lua create mode 100644 data/computercraft/lua/debug/profiler.lua create mode 100644 data/computercraft/lua/debug/releasenotes.lua create mode 100644 data/computercraft/lua/debug/showfile.lua create mode 100644 data/computercraft/lua/debug/startup.lua diff --git a/data/computercraft/lua/debug/adapter.lua b/data/computercraft/lua/debug/adapter.lua new file mode 100644 index 0000000..8dc34ac --- /dev/null +++ b/data/computercraft/lua/debug/adapter.lua @@ -0,0 +1,871 @@ +local fs = require "fs" +local parallel = require "parallel" +local term = require "term" +local textutils = require "textutils" + +print("The debug adapter is running. Please do not close this window.") +os.sleep(1) +term.setCursorBlink(false) +debugger.continue() + +local ok, err = pcall(function() + +local nextSequence = 1 + +local function sendMessage(message, headers) + message.seq = nextSequence + nextSequence = nextSequence + 1 + local data = textutils.serializeJSON(message) + local packet = "Content-Length: " .. #data .. "\r\n" + if headers then for k, v in pairs(headers) do packet = packet .. k .. ": " .. v .. "\r\n" end end + packet = packet .. "\r\n" .. data + print(packet) + debugger.sendDAPData(packet) +end + +local function copy(t) + if type(t) == "table" then + local r = {} + for k, v in pairs(t) do r[k] = copy(v) end + return r + else return t end +end + +local responseWait = {} +local initConfig = {} +local pause = false +local launchCommand +local variableRefs = {} + +local reasonMap = { + ["debug.debug() called"] = "breakpoint", + ["Pause"] = "pause", + ["Breakpoint"] = "breakpoint", + ["Function breakpoint"] = "function breakpoint", + ["Error"] = "exception", + ["Resume"] = "exception", + ["Yield"] = "exception", + ["Caught call"] = "exception", +} + +local commands, events = {}, {} + +function commands.initialize(args) + initConfig = args + return { + supportsConfigurationDoneRequest = true, + supportsFunctionBreakpoints = true, + supportsConditionalBreakpoints = false, -- TODO + supportsHitConditionalBreakpoints = false, -- TODO + supportsEvaluateForHovers = true, + exceptionBreakpointFilters = { + { + filter = "error", + label = "Any error", + description = "Breaks on any thrown error" + }, + { + filter = "load", + label = "Load code", + description = "Breaks when calling loadfile, loadAPI, require" + }, + { + filter = "run", + label = "Run program", + description = "Breaks when calling os.run, shell.run, dofile" + }, + { + filter = "resume", + label = "Resume coroutine", + description = "Breaks when resuming any coroutine" + }, + { + filter = "yield", + label = "Yield coroutine", + description = "Breaks when any coroutine yields" + } + }, + supportsStepBack = false, + supportsSetVariable = true, + supportsGotoTargetsRequest = false, + supportsStepInTargetsRequest = false, + supportsCompletionsRequest = false, + supportsModulesRequest = false, + --supportedChecksumAlgorithms = {"timestamp"}, + supportsRestartRequest = true, + supportsExceptionOptions = false, + supportsValueFormattingOptions = true, + supportsExceptionInfoRequest = true, + supportTerminateDebuggee = true, + supportSuspendDebuggee = true, + supportsDelayedStackTraceLoading = true, + supportsLoadedSourcesRequest = false, + supportsLogPoints = false, -- TODO + supportsTerminateThreadsRequest = false, + supportsSetExpression = true, + supportsTerminateRequest = true, + supportsDataBreakpoints = false, + supportsReadMemoryRequest = false, + supportsWriteMemoryRequest = false, + supportsDisassembleRequest = true, + supportsCancelRequest = false, + supportsBreakpointLocationsRequest = false, + supportsClipboardContext = false, + supportsSteppingGranularity = false, + supportsInstructionBreakpoints = false, + supportsExceptionFilterOptions = false, + supportsSingleThreadExecutionRequests = false, + } +end + +function commands.launch(args) + launchCommand = args.program + pause = true + if not debugger.status() then + debugger.step() + debugger.unblock() + print("Unblocked") + debugger.waitForBreak() + print("Done") + end + if launchCommand then debugger.setStartupCode("shell.run('" .. launchCommand .. "')") end + debugger.run("coroutine.resume(coroutine.create(os.reboot))") + debugger.continue() + print("Continuing") + debugger.waitForBreakAsync() +end + +function commands.attach() + debugger.waitForBreakAsync() +end + +function commands.restart(args) + pause = true + if not debugger.status() then + debugger.step() + debugger.unblock() + debugger.waitForBreak() + end + if launchCommand then debugger.setStartupCode("shell.run('" .. launchCommand .. "')") end + debugger.run("coroutine.resume(coroutine.create(os.reboot))") + debugger.continue() + debugger.waitForBreakAsync() +end + +function commands.disconnect(args) + if args.terminateDebuggee then + pause = true + if not debugger.status() then + debugger.step() + debugger.unblock() + debugger.waitForBreak() + end + debugger.run("coroutine.resume(coroutine.create(os.shutdown))") + debugger.continue() + debugger.waitForBreakAsync() + elseif args.suspendDebuggee then + if not debugger.status() then + debugger.step() + debugger.unblock() + end + else + if debugger.status() then + debugger.continue() + debugger.waitForBreakAsync() + end + end +end + +function commands.terminate(args) + pause = true + if not debugger.status() then + debugger.step() + debugger.unblock() + debugger.waitForBreak() + end + debugger.run("coroutine.resume(coroutine.create(os.shutdown))") + debugger.continue() + debugger.waitForBreakAsync() +end + +function commands.setBreakpoints(args) + if not args.source.adapterData then args.source.adapterData = {path = fs.combine(debugger.getInternalPath(args.source.path))} end + if not args.breakpoints or (args.source.adapterData and not args.source.adapterData.path) then return {breakpoints = textutils.empty_json_array} end + local bp = debugger.listBreakpoints() + for i, v in ipairs(bp) do if fs.combine(v.file:sub(2)) == args.source.adapterData.path then debugger.unsetBreakpoint(i) end end + local retval = {} + for _, v in ipairs(args.breakpoints) do + local id = debugger.setBreakpoint(args.source.adapterData.path, v.line) + retval[#retval+1] = {id = id, verified = true, source = copy(args.source), line = v.line} + end + if #retval == 0 then retval = textutils.empty_json_array end + return {breakpoints = retval} +end + +function commands.setFunctionBreakpoints(args) + local bp = debugger.listBreakpoints() + for i, v in ipairs(bp) do if v.line == -1 then debugger.unsetBreakpoint(i) end end + local retval = {} + for _, v in ipairs(args.breakpoints) do + local id = debugger.setFunctionBreakpoint(v.name) + retval[#retval+1] = {id = id, verified = true} + end + if #retval == 0 then retval = textutils.empty_json_array end + return {breakpoints = retval} +end + +function commands.setExceptionBreakpoints(args) + debugger.uncatch("error") + debugger.uncatch("load") + debugger.uncatch("run") + debugger.uncatch("resume") + debugger.uncatch("yield") + local retval = {} + for _, v in ipairs(args.filters) do + debugger.catch(v) + retval[#retval+1] = {verified = true} + end + if #retval == 0 then retval = textutils.empty_json_array end + return {breakpoints = retval} +end + +function commands.continue(args) + if debugger.status() then + debugger.continue() + debugger.waitForBreakAsync() + end + return {allThreadsContinued = true} +end + +function commands.next(args) + if debugger.status() then + debugger.step() + debugger.waitForBreakAsync() + end +end + +function commands.stepIn(args) + if debugger.status() then + debugger.step() + debugger.waitForBreakAsync() + end +end + +function commands.stepOut(args) + if debugger.status() then + debugger.stepOut() + debugger.waitForBreakAsync() + end +end + +function commands.pause(args) + if not debugger.status() then + debugger.step() + debugger.unblock() + end +end + +function commands.stackTrace(args) + local stack = {} + local total = 0 + while debugger.getInfo(total) do total = total + 1 end + for i = args.startFrame or 0, (args.startFrame or 1) + (args.levels or math.huge) - 1 do + local info = debugger.getInfo(i) + if not info then break end + local source = {name = info.short_src, origin = info.what} + if info.source:match("^@") then + source.path = debugger.getPath(info.source:sub(2)) + source.presentationHint = "normal" + source.adapterData = {path = info.source:sub(2)} + elseif info.source:match("^=") then + source.presentationHint = "deemphasize" + source.adapterData = {} + else + source.adapterData = {data = info.source} + end + stack[#stack+1] = { + id = i, + name = info.name, + source = source, + line = info.currentline ~= -1 and (info.currentline and info.currentline + 1) or 0, + column = 0, + instructionPointerReference = info.instruction >= 0 and tostring(i * 0x100000000 + info.instruction) or nil, + } + end + if #stack == 0 then stack = textutils.empty_json_array end + return { + stackFrames = stack, + totalFrames = total + } +end + +function commands.scopes(args) + if not debugger.status() then return {scopes = textutils.empty_json_array} end + local info = debugger.getInfo(args.frameId) + local locals = debugger.getLocals(args.frameId) + local n = 1 + for _ in pairs(locals) do n = n + 1 end + local source = {name = info.short_src, origin = info.what} + if info.source:match("^@") then + source.path = debugger.getPath(info.source:sub(2)) + source.presentationHint = "normal" + source.adapterData = {path = info.source:sub(2)} + elseif info.source:match("^=") then + source.presentationHint = "deemphasize" + source.adapterData = {} + else + source.adapterData = {data = info.source} + end + -- oof + local source2 = {} + for k, v in pairs(source) do source2[k] = v end + source2.adapterData = {path = source.adapterData.path, data = source.adapterData.data} + return {scopes = { + { + name = "Locals", + presentationHint = "locals", + variablesReference = 0x7FFFFF00 + args.frameId, + --namedVariables = n, + source = source, + line = info.linedefined, + endLine = info.lastlinedefined + }, + { + name = "Upvalues", + presentationHint = "locals", + variablesReference = 0x7FFFFF80 + args.frameId, + --namedVariables = info.nups, + source = source2, + line = info.linedefined, + endLine = info.lastlinedefined + } + }} +end + +function commands.variables(args) + if not debugger.status() then return {variables = textutils.empty_json_array} end + local retval = {} + if bit32.band(args.variablesReference, 0x7FFFFF80) == 0x7FFFFF00 then + local locals = debugger.getLocals(bit32.band(args.variablesReference, 0x7F)) + for k, v in pairs(locals) do + if args.format and args.format.hex and type(v) == "number" then v = ("%x"):format(v) end + local id = 0 + if type(v) == "table" then + id = #variableRefs + 1 + variableRefs[id] = v + end + retval[#retval+1] = { + name = k, + value = tostring(v), + type = type(v), + evaluateName = "locals." .. k, + variablesReference = id + } + end + elseif bit32.band(args.variablesReference, 0x7FFFFF80) == 0x7FFFFF80 then + local upvals = debugger.getUpvalues(bit32.band(args.variablesReference, 0x7F)) + for k, v in pairs(upvals) do + if args.format and args.format.hex and type(v) == "number" then v = ("%x"):format(v) end + local id = 0 + if type(v) == "table" then + id = #variableRefs + 1 + variableRefs[id] = v + end + retval[#retval+1] = { + name = k, + value = tostring(v), + type = type(v), + evaluateName = "upvalues." .. k, + variablesReference = id + } + end + elseif variableRefs[args.variablesReference] then + for i = (args.start or 0) + 1, (args.start or 0) + (args.count or #variableRefs[args.variablesReference]) do + local v = variableRefs[args.variablesReference][i] + if args.format and args.format.hex and type(v) == "number" then v = ("%x"):format(v) end + local id = 0 + if type(v) == "table" then + id = #variableRefs + 1 + variableRefs[id] = v + end + retval[#retval+1] = { + name = tostring(i), + value = tostring(v), + type = type(v), + variablesReference = id + } + end + end + if #retval == 0 then retval = textutils.empty_json_array end + return {variables = retval} +end + +function commands.setVariable(args, message) + if not debugger.status() then + sendMessage {type = "response", request_seq = message.seq, success = false, command = message.command, error = "Not paused"} + return false + end + if args.value == "nil" then args.value = nil + elseif args.value == "true" then args.value = true + elseif args.value == "false" then args.value = false + else args.value = tonumber(args.value) or args.value end + if bit32.band(args.variablesReference, 0x7FFFFF80) == 0x7FFFFF00 then + debugger.setLocal(bit32.band(args.variablesReference, 0x7F), args.name, args.value) + elseif bit32.band(args.variablesReference, 0x7FFFFF80) == 0x7FFFFF80 then + debugger.setUpvalue(bit32.band(args.variablesReference, 0x7F), args.name, args.value) + else + variableRefs[args.variablesReference][args.name] = args.value + end + return {value = tostring(args.value), type = type(args.value)} +end + +function commands.source(args, message) + if not debugger.status() then + sendMessage {type = "response", request_seq = message.seq, success = false, command = message.command, error = "Not paused"} + return false + end + if not args.source.adapterData then args.source.adapterData = {path = fs.combine(debugger.getInternalPath(args.source.path))} end + if args.source.adapterData.path then + local file, err = fs.open(args.source.adapterData.path, "r") + if not file then + sendMessage {type = "response", request_seq = message.seq, success = false, command = message.command, error = err} + return false + end + local data = file.readAll() + file.close() + return {content = data} + elseif args.source.adapterData.data then + return {content = args.source.adapterData.data} + else + sendMessage {type = "response", request_seq = message.seq, success = false, command = message.command, error = "No source available"} + return false + end +end + +function commands.threads(args) + return {threads = {{id = 1, name = "Computer"}}} +end + +function commands.evaluate(args, message) + if not debugger.status() then + sendMessage {type = "response", request_seq = message.seq, success = false, command = message.command, error = "Not paused"} + return false + end + local sf, func, e = args.expression, load( args.expression, "lua", "t", {} ) + local sf2, func2, e2 = "return _echo("..args.expression..");", load( "return _echo("..args.expression..");", "lua", "t", {} ) + if not func then + if func2 then + func = func2 + sf = sf2 + e = nil + end + else + if func2 then + func = func2 + sf = sf2 + end + end + if not func then + sendMessage {type = "response", request_seq = message.seq, success = false, command = message.command, error = e} + return false + end + local res = table.pack(debugger.run(sf)) + print(table.unpack(res, 1, res.n)) + if not res[1] then + sendMessage {type = "response", request_seq = message.seq, success = false, command = message.command, error = res[2]} + return false + end + if res.n > 2 then + local id = #variableRefs + 1 + for i = 1, res.n - 1 do res[i] = res[i+1] end + res[res.n] = nil + res.n = res.n - 1 + variableRefs[id] = res + return {result = tostring(res[1]) .. ", ...", variablesReference = id, indexedVariables = res.n} + elseif type(res[2]) == "table" then + local id = #variableRefs + 1 + variableRefs[id] = res[2] + return {result = tostring(res[2]), variablesReference = id} + else return {result = tostring(res[2]), variablesReference = 0} end +end + +function commands.setExpression(args, message) + if not debugger.status() then + sendMessage {type = "response", request_seq = message.seq, success = false, command = message.command, error = "Not paused"} + return false + end + local ok, err = debugger.run(args.expression .. " = " .. args.value) + if not ok then + sendMessage {type = "response", request_seq = message.seq, success = false, command = message.command, error = err} + return false + end + local _, val = debugger.run("return _echo(" .. args.expression .. ")") + if type(val) == "table" then + local id = #variableRefs + 1 + variableRefs[id] = val + return {value = tostring(val), type = "table", variablesReference = id} + else return {value = tostring(val), type = type(val)} end +end + +function commands.exceptionInfo(args) + return { + exceptionId = debugger.getReason(), + breakMode = "always", + details = debugger.getReason(), + } +end + +-- Full Lua bytecode loader! Yay! :D: +-- NOTE: This code MUST be updated when switching to Lua 5.2/5.4!!! + +local function LoadChar(S) + local x + x, S.pos = ("B"):unpack(S.str, S.pos) + return x +end + +local function LoadInt(S) + local x + x, S.pos = ("I4"):unpack(S.str, S.pos) + return x +end + +local function LoadSInt(S) + local x + x, S.pos = ("i4"):unpack(S.str, S.pos) + return x +end + +local function LoadNumber(S) + local x + x, S.pos = ("d"):unpack(S.str, S.pos) + return x +end + +local function LoadString(S) + local size = LoadInt(S) + if size == 0 then return nil + else + local s = S.str:sub(S.pos, S.pos + size - 2) + S.pos = S.pos + size + return s + end +end + +local function LoadCode(S, f) + f.code = {} + local n = LoadInt(S) + for i = 1, n do f.code[i] = LoadInt(S) end +end + +local LoadFunction + +local function LoadConstants(S, f) + local n = LoadInt(S) + f.k = {} + for i = 0, n-1 do + local t = LoadChar(S) + if t == 0 then f.k[i] = nil + elseif t == 1 then f.k[i] = LoadChar(S) ~= 0 + elseif t == 3 then f.k[i] = LoadNumber(S) + elseif t == 4 then f.k[i] = LoadString(S) + else error("bad constant") end + end + n = LoadInt(S) + f.p = {} + for i = 0, n-1 do f.p[i] = LoadFunction(S, f.source) end +end + +local function LoadDebug(S, f) + local n = LoadInt(S) + f.lineinfo = {} + for i = 1, n do f.lineinfo[i] = LoadSInt(S) end + n = LoadInt(S) + f.locvars = {} + for i = 0, n-1 do + f.locvars[i] = {} + f.locvars[i].varname = LoadString(S) + f.locvars[i].startpc = LoadInt(S) + f.locvars[i].endpc = LoadInt(S) + end + n = LoadInt(S) + f.upvalues = {} + for i = 0, n-1 do f.upvalues[i] = LoadString(S) end +end + +function LoadFunction(S, p) + local f = {} + f.source = LoadString(S) or p + f.linedefined = LoadInt(S) + f.lastlinedefined = LoadInt(S) + f.nups = LoadChar(S) + f.numparams = LoadChar(S) + f.is_vararg = LoadChar(S) == 1 + f.maxstacksize = LoadChar(S) + LoadCode(S, f) + LoadConstants(S, f) + LoadDebug(S, f) + return f +end + +local function PrintString(str) + return ("%q"):format(str) +end + +local function PrintConstant(f, i) + if type(f.k[i]) == "string" then return PrintString(f.k[i]) + else return tostring(f.k[i]) end +end + +local opnames = { + [0] = "MOVE", + "LOADK", + "LOADBOOL", + "LOADNIL", + "GETUPVAL", + "GETGLOBAL", + "GETTABLE", + "SETGLOBAL", + "SETUPVAL", + "SETTABLE", + "NEWTABLE", + "SELF", + "ADD", + "SUB", + "MUL", + "DIV", + "MOD", + "POW", + "UNM", + "NOT", + "LEN", + "CONCAT", + "JMP", + "EQ", + "LT", + "LE", + "TEST", + "TESTSET", + "CALL", + "TAILCALL", + "RETURN", + "FORLOOP", + "FORPREP", + "TFORLOOP", + "SETLIST", + "CLOSE", + "CLOSURE", + "VARARG" +} + +local OpArgN, OpArgU, OpArgR, OpArgK = 0, 1, 2, 3 +local iABC, iABx, iAsBx = 0, 1, 2 +local function opmode(t, a, b, c, m) return {t = t == 1, a = a == 1, b = b, c = c, mode = m} end + +local opmodes = { + [0] = opmode(0, 1, OpArgR, OpArgN, iABC) + ,opmode(0, 1, OpArgK, OpArgN, iABx) + ,opmode(0, 1, OpArgU, OpArgU, iABC) + ,opmode(0, 1, OpArgR, OpArgN, iABC) + ,opmode(0, 1, OpArgU, OpArgN, iABC) + ,opmode(0, 1, OpArgK, OpArgN, iABx) + ,opmode(0, 1, OpArgR, OpArgK, iABC) + ,opmode(0, 0, OpArgK, OpArgN, iABx) + ,opmode(0, 0, OpArgU, OpArgN, iABC) + ,opmode(0, 0, OpArgK, OpArgK, iABC) + ,opmode(0, 1, OpArgU, OpArgU, iABC) + ,opmode(0, 1, OpArgR, OpArgK, iABC) + ,opmode(0, 1, OpArgK, OpArgK, iABC) + ,opmode(0, 1, OpArgK, OpArgK, iABC) + ,opmode(0, 1, OpArgK, OpArgK, iABC) + ,opmode(0, 1, OpArgK, OpArgK, iABC) + ,opmode(0, 1, OpArgK, OpArgK, iABC) + ,opmode(0, 1, OpArgK, OpArgK, iABC) + ,opmode(0, 1, OpArgR, OpArgN, iABC) + ,opmode(0, 1, OpArgR, OpArgN, iABC) + ,opmode(0, 1, OpArgR, OpArgN, iABC) + ,opmode(0, 1, OpArgR, OpArgR, iABC) + ,opmode(0, 0, OpArgR, OpArgN, iAsBx) + ,opmode(1, 0, OpArgK, OpArgK, iABC) + ,opmode(1, 0, OpArgK, OpArgK, iABC) + ,opmode(1, 0, OpArgK, OpArgK, iABC) + ,opmode(1, 1, OpArgR, OpArgU, iABC) + ,opmode(1, 1, OpArgR, OpArgU, iABC) + ,opmode(0, 1, OpArgU, OpArgU, iABC) + ,opmode(0, 1, OpArgU, OpArgU, iABC) + ,opmode(0, 0, OpArgU, OpArgN, iABC) + ,opmode(0, 1, OpArgR, OpArgN, iAsBx) + ,opmode(0, 1, OpArgR, OpArgN, iAsBx) + ,opmode(1, 0, OpArgN, OpArgU, iABC) + ,opmode(0, 0, OpArgU, OpArgU, iABC) + ,opmode(0, 0, OpArgN, OpArgN, iABC) + ,opmode(0, 1, OpArgU, OpArgN, iABx) + ,opmode(0, 1, OpArgU, OpArgN, iABC) +} + +local function PrintCode(f, src) + local list = {} + local pc = 1 + while pc <= #f.code do + local i = f.code[pc] + local o = bit32.band(i, 0x3F) + local a = bit32.band(bit32.rshift(i, 6), 0xFF) + local b = bit32.band(bit32.rshift(i, 14), 0x1FF) + local c = bit32.band(bit32.rshift(i, 23), 0x1FF) + local bx = bit32.band(bit32.rshift(i, 14), 0x3FFFF) + local sbx = bx - 0x1FFFF + local inst = {source = copy(src), instructionBytes = ("%08X"):format(i), address = tostring(pc), line = f.lineinfo[pc]} + local retval = ("%-9s\t"):format(opnames[o]) + local mode = opmodes[o] + if mode.mode == iABC then + retval = retval .. a + if mode.b ~= OpArgN then retval = retval .. " " .. (bit32.btest(b, 0x100) and (-1-bit32.band(b, 0xFF)) or b) end + if mode.c ~= OpArgN then retval = retval .. " " .. (bit32.btest(c, 0x100) and (-1-bit32.band(c, 0xFF)) or c) end + elseif mode.mode == iABx then + if mode.b == OpArgK then retval = retval .. a .. " " .. (-1-bx) + else retval = retval .. a .. " " .. bx end + else + if o == 22 then retval = retval .. sbx + else retval = retval .. a .. " " .. sbx end + end + if o == 1 then retval = retval .. "\t; " .. PrintConstant(f, bx) + elseif o == 4 or o == 8 then retval = retval .. "\t; " .. (f.upvalues[b] or "-") + elseif o == 5 or o == 7 then retval = retval .. "\t; " .. f.k[bx] + elseif o == 6 or o == 11 then if bit32.btest(c, 0x100) then retval = retval .. "\t; " .. PrintConstant(f, bit32.band(c, 0xFF)) end + elseif o == 9 or o == 12 or o == 13 or o == 14 or o == 15 or o == 17 or o == 23 or o == 24 or o == 25 then + if bit32.btest(b, 0x100) or bit32.btest(c, 0x100) then + retval = retval .. "\t; " + if bit32.btest(b, 0x100) then retval = retval .. PrintConstant(f, bit32.band(b, 0xFF)) + else retval = retval .. "-" end + retval = retval .. " " + if bit32.btest(c, 0x100) then retval = retval .. PrintConstant(f, bit32.band(c, 0xFF)) + else retval = retval .. "-" end + end + elseif o == 22 or o == 31 or o == 32 then retval = retval .. "\t; to " .. (sbx+pc+2) + elseif o == 36 then retval = retval .. "\t; " .. tostring(f.p[bx]):gsub("table: ", "") + elseif o == 34 then + if c == 0 then + pc=pc+1 + retval = retval .. "\t; " .. f.code[pc] + else retval = retval .. "\t; " .. c end + end + inst.instruction = retval + list[pc] = inst + pc=pc+1 + end + return list +end + +function commands.disassemble(args) + local pc = bit32.band(tonumber(args.memoryReference), 0xFFFFFFFF) + local level = math.floor(tonumber(args.memoryReference) / 0x100000000) + local ok, code, info = debugger.run("local info = debug.getinfo(" .. (level + 2) .. ", 'Sf') local code = string.dump(info.func) info.func = nil return code, info") + if not ok then return {instructions = textutils.empty_json_array, error = code} end + local source = {name = info.short_src, origin = info.what} + if info.source:match("^@") then + source.path = debugger.getPath(info.source:sub(2)) + source.presentationHint = "normal" + source.adapterData = {path = info.source:sub(2)} + elseif info.source:match("^=") then + source.presentationHint = "deemphasize" + source.adapterData = {} + else + source.adapterData = {data = info.source} + end + local f = LoadFunction({str = code, pos = 13}, "?") + local insts = PrintCode(f, source) + local retval = {} + local start = tonumber(pc) + (args.instructionOffset or 0) + for i = start, start + args.instructionCount - 1 do + retval[#retval+1] = insts[i] or {address = tonumber(i), instruction = ""} + end + if #retval == 0 then retval = textutils.empty_json_array end + return {instructions = retval} +end + +parallel.waitForAny(function() + local buffer = "" + while true do + local _, input = os.pullEventRaw("dap_input") + print(input) + buffer = buffer .. input + while buffer:match "\r\n\r\n" do + print("Parsing") + local headers = {} + while true do + local stop = buffer:find("\r\n") + local line = buffer:sub(1, stop - 1) + buffer = buffer:sub(stop + 2) + if line == "" then break end + headers[line:match("^[^:]+")] = line:match(":%s*(.*)$") + end + if headers["Content-Length"] then + local length = tonumber(headers["Content-Length"]) + while #buffer < length do + _, input = os.pullEventRaw("dap_input") + print(input) + buffer = buffer .. input + end + local data = buffer:sub(1, length) + buffer = buffer:sub(length + 1) + local message = textutils.unserializeJSON(data) + nextSequence = message.seq + 1 + print(message.seq, message.type) + if message.type == "request" then + local body + if commands[message.command] then body = commands[message.command](message.arguments, message) end + if body ~= false then sendMessage {type = "response", request_seq = message.seq, success = true, command = message.command, body = body} end + if message.command == "initialize" then sendMessage {type = "event", event = "initialized"} + elseif message.command == "launch" or message.command == "attach" then + sendMessage {type = "event", event = "process", body = {name = "CraftOS-PC", isLocalProcess = false, startMethod = message.command}} + sendMessage {type = "event", event = "thread", body = {reason = "started", threadId = 1}} + end + elseif message.type == "response" then if responseWait[message.request_seq] then responseWait[message.request_seq](message) end + elseif message.type == "event" then if events[message.event] then events[message.event](message.body, message) end end + print("Finished command") + end + end + end +end, function() + --debugger.waitForBreakAsync() + os.sleep(0.25) -- clear queue + while true do + os.pullEventRaw("debugger_break") + debugger.confirmBreak() + print("Did break") + if not pause then + print("Sending message") + sendMessage { + type = "event", + event = "stopped", + body = { + reason = reasonMap[debugger.getReason()] or "exception", + description = debugger.getReason(), + text = debugger.getReason(), + threadId = 1, + allThreadsStopped = true, + -- TODO: hitBreakpointIds + } + } + else + os.sleep(0.25) + pause = false + end + end +end, function() + while true do + local _, text = os.pullEventRaw("debugger_print") + sendMessage { + event = "output", + body = { + category = "console", + output = text + } + } + end +end) + +end) +if not ok then io.stderr:write(err .. "\n") end +debugger.continue() +while true do coroutine.yield() end diff --git a/data/computercraft/lua/debug/bios.lua b/data/computercraft/lua/debug/bios.lua new file mode 100644 index 0000000..085c81f --- /dev/null +++ b/data/computercraft/lua/debug/bios.lua @@ -0,0 +1,258 @@ +-- Recrafted BIOS file + +-- (Most) system APIs go in here, not in _G. +-- You require `recrafted` to get access to them. +local rc = {} + +if _RC_ROM_DIR then + rc._ROM_DIR = _RC_ROM_DIR + _G._RC_ROM_DIR = nil +else + rc._ROM_DIR = "/rom" +end + +rc.platform = { + os = "Recrafted", + version = "1.01", + advanced = term.isColor(), + command = not not commands, + turtle = not not turtle, + pocket = not not pocket, + http = not not http, +} + +local function rm(api) + local tab = _G[api] + _G[api] = nil + return tab +end + +-- remove CC-specific globals +rc.peripheral = rm("peripheral") +rc.redstone = rm("redstone") +rc.commands = rm("commands") +rc.pocket = rm("pocket") +rc.turtle = rm("turtle") +rc.http = rm("http") +rc.term = rm("term") +rc.fs = rm("fs") +rc.rs = rm("rs") + +-- CraftOS-PC APIs +rc.periphemu = rm("periphemu") +rc.mounter = rm("mounter") +rc.config = rm("config") + +-- CCEmuX API +rc.ccemux = rm("ccemux") + +function rc.version() + return "Recrafted 1.0" +end + +rc.term.clear() +rc.term.setCursorPos(1,1) +rc.term.write("Starting "..rc.version()..".") +rc.term.setCursorPos(1,2) + +-- this is overwritten later +rc.expect = function(_,_,_,_) end + +function rc.write(text) + rc.expect(1, text, "string") + + local lines = 0 + local w, h = rc.term.getSize() + + local function inc_cy(cy) + lines = lines + 1 + + if cy > h - 1 then + rc.term.scroll(1) + return cy + else + return cy + 1 + end + end + + while #text > 0 do + local nl = text:find("\n") or #text + local chunk = text:sub(1, nl) + text = text:sub(#chunk + 1) + + local has_nl = chunk:sub(-1) == "\n" + if has_nl then chunk = chunk:sub(1, -2) end + + local cx, cy = rc.term.getCursorPos() + while #chunk > 0 do + if cx > w then + rc.term.setCursorPos(1, inc_cy(cy)) + cx, cy = rc.term.getCursorPos() + end + + local to_write = chunk:sub(1, w - cx + 1) + rc.term.write(to_write) + + chunk = chunk:sub(#to_write + 1) + cx, cy = rc.term.getCursorPos() + end + + if has_nl then + rc.term.setCursorPos(1, inc_cy(cy)) + end + end + + return lines +end + +-- print() gets to be global. +function _G.print(...) + local args = table.pack(...) + + for i=1, args.n, 1 do + args[i] = tostring(args[i]) + end + + return rc.write(table.concat(args, " ") .. "\n") +end + +local red = 0x4000 +function rc.printError(...) + local old = rc.term.getTextColor() + rc.term.setTextColor(red) + print(...) + rc.term.setTextColor(old) +end + +local _sd = os.shutdown +function os.shutdown() + _sd() + while true do coroutine.yield() end +end + +-- get rid of Lua 5.1 things. +if _VERSION == "Lua 5.1" then + local old_load = rm("load") + rc.lua51 = { + loadstring = rm("loadstring"), + setfenv = rm("setfenv"), + getfenv = rm("getfenv"), + unpack = rm("unpack"), + log10 = math.log10, + maxn = table.maxn + } + + math.log10 = nil + table.maxn = nil + + function _G.load(x, name, mode, env) + rc.expect(1, x, "string", "function") + rc.expect(2, name, "string", "nil") + rc.expect(3, mode, "string", "nil") + rc.expect(4, env, "table", "nil") + env = env or _G + + local result, err + if type(x) == "string" then + result, err = rc.lua51.loadstring(x, name) + else + result, err = old_load(x, name) + end + + if result then + env._ENV = env + rc.lua51.setfenv(result, env) + end + + return result, err + end + + local old_xpcall = xpcall + function _G.xpcall(call, func, ...) + local args = table.pack(...) + return old_xpcall(function() + return call(table.unpack(args, 1, args.n)) + end, func) + end +end + +function _G.loadfile(file, mode, env) + rc.expect(1, file, "string") + rc.expect(2, mode, "string", "nil") + rc.expect(3, env, "table", "nil") + + local handle, err = rc.fs.open(file, "r") + if not handle then + return nil, err + end + + local data = handle.readAll() + handle.close() + + return load(data, "="..file, mode, env) +end + +local function _assert(a, ...) + if not a then + error(..., 3) + else + return a, ... + end +end + +function _G.dofile(file) + return _assert(loadfile(file))() +end + +-- Load debugger API +if debugger then + local nativeWaitForBreak = debugger.waitForBreak + function debugger.waitForBreak() + nativeWaitForBreak() + local ev = os.pullEventRaw() + while ev ~= "debugger_break" do + ev = os.pullEventRaw() + if ev == "terminate" then + debugger.step() + debugger.unblock() + end + end + debugger.confirmBreak() + end + debugger.waitForBreakAsync = nativeWaitForBreak +end + +print("Loading initialization scripts.") + +local files = rc.fs.list(rc._ROM_DIR.."/init") +table.sort(files) + +for _, file in ipairs(files) do + print(file) + assert(loadfile(rc._ROM_DIR.."/init/"..file))(rc) +end + +local thread = require("thread") + +thread.add(function() + local sh = require("shell") + sh.init() + rc.term.at(1,1).clear() + + local ok, err + if debugger then + require("multishell").launch(nil, "debug/startup.lua") + ok = true + else + ok, err = sh.run(nil, "debug/releasenotes.lua") + end + if not ok then + rc.printError(err) + rc.sleep(1) + end +end) + +print("Starting coroutine manager.") + +rc.queueEvent("init") +thread.start() \ No newline at end of file diff --git a/data/computercraft/lua/debug/console.lua b/data/computercraft/lua/debug/console.lua new file mode 100644 index 0000000..37fb336 --- /dev/null +++ b/data/computercraft/lua/debug/console.lua @@ -0,0 +1,32 @@ +local multishell = require "multishell" +local term = require "term" +local window = require "window" + +multishell.setTitle(multishell.getCurrent(), "Console ") +local w, h = term.getSize() +local win = window.create(term.current(), 1, 1, w, 9000) +local top = 1 +local bottom = 1 +local scrolling = false +local old = term.redirect(win) +while true do + local ev, p1 = coroutine.yield() + if ev == "debugger_print" then + local lines = print(p1) + bottom = math.min(bottom + lines, 9000) + if not scrolling and bottom > h + 1 and top < 9000 - h then + top = bottom - h + win.reposition(1, 2-top) + end + elseif ev == "mouse_scroll" then + if (p1 == -1 and top > 1) or (p1 == 1 and top < 9000 - h) then + top = math.min(top + p1, 9000) + scrolling = top + h - 1 ~= bottom + multishell.setTitle(multishell.getCurrent(), scrolling and "Console \7" or "Console ") + win.reposition(1, 2-top) + end + elseif ev == "term_resize" then + w, h = old.getSize() + win.reposition(1, 2-top, old.getSize(), 9000) + end +end \ No newline at end of file diff --git a/data/computercraft/lua/debug/debugger.lua b/data/computercraft/lua/debug/debugger.lua new file mode 100644 index 0000000..8620521 --- /dev/null +++ b/data/computercraft/lua/debug/debugger.lua @@ -0,0 +1,150 @@ +local colors = require "colors" +local fs = require "fs" +local multishell = require "multishell" +local shell = require "shell" +local term = require "term" +local textutils = require "textutils" +local pretty = require "cc.pretty" + +multishell.setTitle(multishell.getCurrent(), "Debugger") +local ok, err = pcall(function() +local history = {} +local function split(inputstr, sep) + sep = sep or "%s" + local t = {} + for str in string.gmatch(inputstr, "([^"..sep.."]+)") do table.insert(t, str) end + return t +end +term.setTextColor(colors.yellow) +print("CraftOS-PC Debugger") +local advanceTemp +while true do + debugger.waitForBreak() + if advanceTemp then debugger.unsetBreakpoint(advanceTemp); advanceTemp = nil end + local info = debugger.getInfo() + if string.sub(info.source, -8) == "bios.lua" then info.source = "@/bios.lua" end + term.setTextColor(colors.blue) + print("Break at " .. (info.short_src or "?") .. ":" .. (info.currentline or "?") .. " (" .. (info.name or "?") .. "): " .. debugger.getReason()) + if info.source and info.currentline and fs.exists(string.sub(info.source, 2)) then + local file = fs.open(string.sub(info.source, 2), "r") + for i = 1, info.currentline - 1 do file.readLine() end + term.setTextColor(colors.lime) + io.write("--> ") + term.setTextColor(colors.white) + local str = string.gsub(file.readLine(), "^[ \t]+", "") + print(str) + file.close() + end + local loop = true + while loop do + term.setTextColor(colors.yellow) + io.write("(ccdb) ") + term.setTextColor(colors.white) + local cmd = io.read() + if cmd == "" then cmd = history[#history] + else table.insert(history, cmd) end + local action = split(cmd) + if action[1] == "step" or action[1] == "s" then debugger.step(action[2] and tonumber(action[2])); loop = false + elseif action[1] == "finish" or action[1] == "fin" then debugger.stepOut(); loop = false + elseif action[1] == "continue" or action[1] == "c" then debugger.continue(); loop = false + elseif action[1] == "b" or action[1] == "break" then print("Breakpoint " .. debugger.setBreakpoint(string.sub(action[2], 1, string.find(action[2], ":") - 1), tonumber(string.sub(action[2], string.find(action[2], ":") + 1))) .. " set at " .. string.sub(action[2], 1, string.find(action[2], ":") - 1) .. ":" .. string.sub(action[2], string.find(action[2], ":") + 1)) + elseif action[1] == "breakpoint" and action[2] == "set" then print("Breakpoint " .. debugger.setBreakpoint(string.sub(action[3], 1, string.find(action[3], ":") - 1), tonumber(string.sub(action[3], string.find(action[3], ":") + 1))) .. " set at " .. string.sub(action[3], 1, string.find(action[3], ":") - 1) .. ":" .. string.sub(action[3], string.find(action[3], ":") + 1)) + elseif action[1] == "catch" then + if action[2] == "catch" or action[2] == "error" or action[2] == "throw" then debugger.catch("error") + elseif action[2] == "load" then debugger.catch("load") + elseif action[2] == "exec" or action[2] == "run" then debugger.catch("run") + elseif action[2] == "resume" then debugger.catch("resume") + elseif action[2] == "yield" then debugger.catch("yield") end + elseif action[1] == "clear" then debugger.unsetBreakpoint(tonumber(action[2])) + elseif action[1] == "delete" then + if action[2] == "catch" then + if action[2] == "catch" or action[2] == "error" or action[2] == "throw" then debugger.uncatch("error") + elseif action[2] == "load" then debugger.uncatch("load") + elseif action[2] == "exec" or action[2] == "run" then debugger.uncatch("run") + elseif action[2] == "resume" then debugger.uncatch("resume") + elseif action[2] == "yield" then debugger.uncatch("yield") end + else debugger.unsetBreakpoint(tonumber(action[2])) end + elseif action[1] == "edit" and debugger.getInfo().source and fs.exists(string.sub(debugger.getInfo().source, 2)) then shell.run("edit", debugger.getInfo().source) + elseif action[1] == "advance" then + advanceTemp = debugger.setBreakpoint(string.sub(action[2], 1, string.find(action[2], ":") - 1), tonumber(string.sub(action[2], string.find(action[2], ":") + 1))) + debugger.continue() + loop = false + elseif action[1] == "info" then + if action[2] == "breakpoints" then + local breakpoints = debugger.listBreakpoints() + local keys = {} + for k,v in pairs(breakpoints) do table.insert(keys, k) end + table.sort(keys) + local lines = {} + for _,i in ipairs(keys) do table.insert(lines, {i, breakpoints[i].file, breakpoints[i].line}) end + textutils.tabulate(colors.blue, {"ID", "File", "Line"}, colors.white, table.unpack(lines)) + elseif action[2] == "frame" then + term.setTextColor(colors.blue) + print("Break at " .. (info.short_src or "?") .. ":" .. (info.currentline or "?") .. " (" .. (info.name or "?") .. "): " .. debugger.getReason()) + if info.source and info.currentline and fs.exists(string.sub(info.source, 2)) then + local file = fs.open(string.sub(info.source, 2), "r") + for i = 1, info.currentline - 1 do file.readLine() end + term.setTextColor(colors.lime) + io.write("--> ") + term.setTextColor(colors.white) + local str = string.gsub(file.readLine(), "^[ \t]+", "") + print(str) + file.close() + end + elseif action[2] == "locals" then + local lines = {} + for k,v in pairs(debugger.getLocals()) do table.insert(lines, {k, tostring(v)}) end + textutils.tabulate(colors.blue, {"Name", "Value"}, colors.white, table.unpack(lines)) + end + elseif action[1] == "print" or action[1] == "p" then + table.remove(action, 1) + local s = table.concat(action, " ") + local forcePrint = false + local sf, func, e = s, load( s, "lua", "t", {} ) + local sf2, func2, e2 = "return _echo("..s..");", load("return _echo("..s..");", "lua", "t", {}) + if not func then if func2 then func, sf, e, forcePrint = func2, sf2, nil, true end + elseif func2 then func, sf = func2, sf2 end + if func then + local res = table.pack(debugger.run(sf)) + if res[1] then + for n = 2, res.n do + local value = res[n] + pretty.pretty_print(value) + if n <= (forcePrint and 2 or 0) then break end + end + else io.stderr:write(res[2] .. "\n") end + else io.stderr:write(e .. "\n") end + elseif action[1] == "backtrace" or action[1] == "bt" then print(({debugger.run("return debug.traceback()")})[2]) + elseif action[1] == "help" then + textutils.pagedPrint([[Available commands: +advance -- Run to a position in a file in the format : +backtrace (bt) -- Show a traceback +break (b) -- Set a breakpoint in the format : +breakpoint set -- Set a breakpoint in the format : +catch -- Set a breakpoint on special calls +catch error -- Break on error +catch load -- Break on loading APIs/require +catch resume -- Break on resuming coroutine +catch run -- Break on running a program +catch yield -- Break on yielding coroutine +clear -- Clear a breakpoint +continue (c) -- Continue execution +edit -- Edit the currently running program +delete -- Clear a breakpoint +delete catch error -- Stop breaking on error +delete catch load -- Stop breaking on loading APIs/require +delete catch run -- Stop breaking on running a program +finish (fin) -- Step to the end of the current function +info -- List info about the running program +info breakpoints -- List all current breakpoints +info frame -- List the status of the program +info locals -- List all available locals +print (p) -- Run an expression and print the result a la lua.lua +step (s) -- Step a number of lines]], 4) + else io.stderr:write("Error: Invalid command\n") end + end + os.queueEvent("debugger_done") +end +end) +if not ok then io.stderr:write(err .. "\n") end +while os.pullEvent() do end \ No newline at end of file diff --git a/data/computercraft/lua/debug/profiler.lua b/data/computercraft/lua/debug/profiler.lua new file mode 100644 index 0000000..1331941 --- /dev/null +++ b/data/computercraft/lua/debug/profiler.lua @@ -0,0 +1,150 @@ +local colors = require "colors" +local fs = require "fs" +local keys = require "keys" +local multishell = require "multishell" +local term = require "term" +local window = require "window" + +multishell.setTitle(multishell.getCurrent(), "Profiler") +local ok, err = pcall(function() +local w, h = term.getSize() +local header = window.create(term.current(), 1, 1, w, 2) +local viewport = window.create(term.current(), 1, 3, w, h - 2) +local body +local widths = {{"#", 5, "count"}, {"Source", math.ceil((w - 11) / 2), "source"}, {"Function", math.floor((w - 11) / 2), "func"}, {"Time", 6, "time"}} +local profilingTime, tm +local scrollPos, scrollSize = 1, 1 + +local function formatTime(n) return string.format("%i:%02i:%02i", math.floor(n / 3600), math.floor(n / 60) % 60, n % 60) end + +local function updateHeader() + header.setBackgroundColor(colors.gray) + header.clear() + header.setCursorPos(2, 1) + header.blit(" " .. string.char(7) .. " ", profilingTime == nil and "eee" or "000", profilingTime == nil and "000" or "eee") + local timestr = "0:00:00" + if profilingTime ~= nil then timestr = formatTime((os.epoch() - profilingTime) / 1000) end + header.setBackgroundColor(colors.gray) + header.setTextColor(colors.white) + header.setCursorPos(w - #timestr, 1) + header.write(timestr) + local i = 1 + for k,v in ipairs(widths) do + header.setCursorPos(i, 2) + header.write(v[1]) + i = i + v[2] + end +end + +local sortFunctions = { + [0] = function(a, b) return a.count > b.count end, + function(a, b) return a.source > b.source end, + function(a, b) return a.func > b.func end, + function(a, b) return a.time > b.time end, + function(a, b) return a.count < b.count end, + function(a, b) return a.source < b.source end, + function(a, b) return a.func < b.func end, + function(a, b) return a.time < b.time end, +} + +local sorter = 1 + +local function parseProfile() + local lines = {} + local profile = debugger.profile() + local cw = 2 + local tw = 4 + for k,v in pairs(profile) do for l,w in pairs(v) do + table.insert(lines, {source = k, func = l, count = w.count, time = w.time}) + cw = math.max(math.floor(math.log(w.count)) + 2, cw) + tw = math.max(math.floor(math.log(w.time)) + 2, tw) + end end + widths[1][2] = cw + widths[2][2] = math.floor((w - (cw + tw)) / 2) + widths[3][2] = math.ceil((w - (cw + tw)) / 2) + widths[4][2] = tw + body = window.create(viewport, 1, 1, w, #lines) + scrollPos = 1 + scrollSize = #lines + table.sort(lines, sortFunctions[sorter]) + for k,v in ipairs(lines) do + local i = 1 + for l,w in ipairs(widths) do + body.setCursorPos(i, k) + if w[3] == "source" and #v.source > w[2]-1 then body.write(string.sub(fs.getName(v.source), 1, w[2]-1)) + else body.write(string.sub(tostring(v[w[3]]), 1, w[2]-1)) end + i = i + w[2] + end + end +end + +updateHeader() + +while true do + local ev = {os.pullEvent()} + if ev[1] == "mouse_click" and ev[2] == 1 then + if ev[4] == 1 and ev[3] > 1 and ev[3] < 5 then + if profilingTime then + os.cancelTimer(tm) + profilingTime = nil + tm = nil + debugger.startProfiling(false) + parseProfile() + else + profilingTime = os.epoch() + tm = os.startTimer(1) + debugger.startProfiling(true) + if body then body.setVisible(false) end + viewport.clear() + end + updateHeader() + elseif ev[4] == 2 then + if ev[3] <= widths[1][2] then sorter = 0 + (bit32.band(sorter, 3) == 0 and bit32.bxor(bit32.band(sorter, 4), 4) or 0) + elseif ev[3] > widths[1][2] and ev[3] <= widths[1][2] + widths[2][2] then sorter = 1 + (bit32.band(sorter, 3) == 1 and bit32.bxor(bit32.band(sorter, 4), 4) or 0) + elseif ev[3] > widths[1][2] + widths[2][2] and ev[3] <= widths[1][2] + widths[2][2] + widths[3][2] then sorter = 2 + (bit32.band(sorter, 3) == 2 and bit32.bxor(bit32.band(sorter, 4), 4) or 0) + else sorter = 3 + (bit32.band(sorter, 3) == 3 and bit32.bxor(bit32.band(sorter, 4), 4) or 0) end + parseProfile() + end + elseif ev[1] == "mouse_scroll" and ev[4] > 2 then + if ev[2] == -1 and scrollPos < 1 then scrollPos = scrollPos + 1 + elseif ev[2] == 1 and scrollPos > h - 1 - scrollSize then scrollPos = scrollPos - 1 end + if body then body.reposition(1, scrollPos) end + elseif ev[1] == "timer" and ev[2] == tm then + updateHeader() + parseProfile() + tm = os.startTimer(1) + elseif ev[1] == "term_resize" then + w, h = term.getSize() + header = window.create(term.current(), 1, 1, w, 2) + viewport = window.create(term.current(), 1, 3, w, h - 2) + widths = {{"#", 5, "count"}, {"Source", math.ceil((w - 11) / 2), "source"}, {"Function", math.floor((w - 11) / 2), "func"}, {"Time", 6, "time"}} + updateHeader() + elseif ev[1] == "key" then + if ev[2] == keys.enter then + if profilingTime then + os.cancelTimer(tm) + profilingTime = nil + tm = nil + debugger.startProfiling(false) + parseProfile() + else + profilingTime = os.epoch() + tm = os.startTimer(1) + debugger.startProfiling(true) + if body then body.setVisible(false) end + viewport.clear() + end + updateHeader() + elseif ev[2] == keys.up and scrollPos < 1 then + scrollPos = scrollPos + 1 + if body then body.reposition(1, scrollPos) end + elseif ev[2] == keys.down and scrollPos > h - 1 - scrollSize then + scrollPos = scrollPos - 1 + if body then body.reposition(1, scrollPos) end + end + end +end +end) + +if not ok then io.stderr:write(err .. "\n") end +while os.pullEvent() do end \ No newline at end of file diff --git a/data/computercraft/lua/debug/releasenotes.lua b/data/computercraft/lua/debug/releasenotes.lua new file mode 100644 index 0000000..6e316aa --- /dev/null +++ b/data/computercraft/lua/debug/releasenotes.lua @@ -0,0 +1,74 @@ +local colors = require "colors" +local http = require "http" +local keys = require "keys" +local term = require "term" +local textutils = require "textutils" +local window = require "window" + +local handle, err = http.get("https://api.github.com/repos/MCJack123/craftos2/releases/latest") +if not handle then error(err) end +local obj = textutils.unserializeJSON(handle.readAll()) +handle.close() +local w, h = term.getSize() +local oldterm = term.redirect(window.create(term.current(), 1, 1, w, h, false)) +local len = print(obj.body) +term.redirect(oldterm) +local win = window.create(term.current(), 1, 1, w, len) +local infowin = window.create(term.current(), 1, h, w, 1) +--infowin.setBackgroundColor(colors.gray) +if term.isColor() then infowin.setTextColor(colors.yellow) +else infowin.setTextColor(colors.lightGray) end +infowin.clear() +infowin.write("Release Notes") +infowin.setCursorPos(w - 14, 1) +infowin.write("Press Q to exit") +oldterm = term.redirect(win) +io.write(obj.body:gsub("(\n *)[-*]( +)", "%1\7%2")) +infowin.redraw() +local yPos = 1 +while true do + local ev = {os.pullEvent()} + if ev[1] == "key" then + if len > h then + if ev[2] == keys.up and yPos < 1 then + yPos = yPos + 1 + win.reposition(1, yPos) + infowin.redraw() + elseif ev[2] == keys.down and yPos > -len + h then + yPos = yPos - 1 + win.reposition(1, yPos) + infowin.redraw() + elseif ev[2] == keys.pageUp and yPos < 1 then + yPos = math.min(yPos + h, 1) + win.reposition(1, yPos) + infowin.redraw() + elseif ev[2] == keys.pageDown and yPos > -len + h then + yPos = math.max(yPos - h, -len + h) + win.reposition(1, yPos) + infowin.redraw() + elseif ev[2] == keys.home then + yPos = 1 + win.reposition(1, yPos) + infowin.redraw() + elseif ev[2] == keys["end"] then + yPos = -len + h + win.reposition(1, yPos) + infowin.redraw() + end + end + if ev[2] == keys.q then break end + elseif ev[1] == "mouse_scroll" and len > h then + if ev[2] == -1 and yPos < 1 then + yPos = yPos + 1 + win.reposition(1, yPos) + infowin.redraw() + elseif ev[2] == 1 and yPos > -len + h then + yPos = yPos - 1 + win.reposition(1, yPos) + infowin.redraw() + end + end +end +term.redirect(oldterm) +term.setCursorPos(1, 1) +term.clear() \ No newline at end of file diff --git a/data/computercraft/lua/debug/showfile.lua b/data/computercraft/lua/debug/showfile.lua new file mode 100644 index 0000000..a686ade --- /dev/null +++ b/data/computercraft/lua/debug/showfile.lua @@ -0,0 +1,263 @@ +local colours = require "colours" +local colors = require "colors" +local keys = require "keys" +local fs = require "fs" +local multishell = require "multishell" +local term = require "term" +local window = require "window" + +multishell.setTitle(multishell.getCurrent(), "Call Stack") +local s, e = pcall(function() +local stackWindow, viewerWindow, lines, scrollPos, infoCache + +local w, h = term.getSize() +local selectedLine + +local function getCallStack() + local i = 0 + local retval = {} + while true do + local t = debugger.getInfo(i) + if not t then return retval end + retval[i+1] = t + i=i+1 + end +end + +local function drawTraceback() + if viewerWindow then + viewerWindow.clear() + viewerWindow.setVisible(false) + viewerWindow = nil + end + local stack = getCallStack() + stackWindow = window.create(term.current(), 1, 1, w, math.max(#stack + 1, h)) + stackWindow.clear() + stackWindow.setCursorPos(1, 1) + stackWindow.setBackgroundColor(colors.black) + stackWindow.setTextColor(colors.white) + local numWidth, lineWidth = math.floor(math.log(#stack, 10)) + 3, 1 + for k,v in ipairs(stack) do lineWidth = math.max(math.floor(math.log(v.currentline or 0, 10)) + 1, lineWidth) end + local sourceWidth, nameWidth = math.ceil((w - (numWidth + lineWidth)) / 2), math.floor((w - (numWidth + lineWidth)) / 2) + stackWindow.write("#") + stackWindow.setCursorPos(numWidth, 1) + stackWindow.write("Source") + stackWindow.setCursorPos(numWidth + sourceWidth, 1) + stackWindow.write("Name") + stackWindow.setCursorPos(numWidth + sourceWidth + nameWidth, 1) + stackWindow.write("@") + for i,v in ipairs(stack) do + stackWindow.setCursorPos(1, i + 1) + stackWindow.setBackgroundColor(selectedLine == i and colors.blue or (i % 2 == 1 and colors.gray or colors.black)) + stackWindow.setTextColor((v.short_src == "[C]" or v.short_src == "(tail call)") and colors.lightGray or colors.white) + stackWindow.clearLine() + stackWindow.write(tostring(i)) + stackWindow.setCursorPos(numWidth, i + 1) + if #v.short_src > sourceWidth - 1 then stackWindow.write(string.sub(fs.getName(v.short_src), 1, sourceWidth - 1)) + else stackWindow.write(string.sub(v.short_src or "?", 1, sourceWidth - 1)) end + stackWindow.setCursorPos(numWidth + sourceWidth, i + 1) + stackWindow.write(string.sub(v.name or "?", 1, nameWidth - 1)) + stackWindow.setCursorPos(numWidth + sourceWidth + nameWidth, i + 1) + stackWindow.write(tostring(v.currentline or "")) + end + if #stack < h - 1 then for i = #stack + 1, h - 1 do + stackWindow.setCursorPos(1, i + 1) + stackWindow.setBackgroundColor(i % 2 == 1 and colors.gray or colors.black) + stackWindow.clearLine() + end end +end + +local function renderFile() + if lines == nil then return end + local info = infoCache + viewerWindow.setCursorPos(1, 2) + viewerWindow.setTextColor(colors.white) + for i = scrollPos, scrollPos + h - 2 do + if i == info.currentline then viewerWindow.setBackgroundColor(colors.blue) + else viewerWindow.setBackgroundColor(colors.black) end + viewerWindow.clearLine() + if lines[i] ~= nil then viewerWindow.write(lines[i]) end + if i ~= scrollPos + h then viewerWindow.setCursorPos(1, select(2, viewerWindow.getCursorPos()) + 1) end + end + local r = (#lines - h + 3) / (h - 1) + for i = 2, h do + viewerWindow.setCursorPos(w, i) + viewerWindow.blit(" ", "0", (scrollPos >= r * (i - 2) and scrollPos < r * (i - 1)) and "8" or "7") + end +end + +local function showFile(info) + if stackWindow then + stackWindow.clear() + stackWindow.setVisible(false) + stackWindow = nil + end + viewerWindow = window.create(term.current(), 1, 1, w, h) + viewerWindow.clear() + viewerWindow.setCursorPos(1, 1) + viewerWindow.setTextColor(colors.blue) + viewerWindow.setBackgroundColor(colors.white) + viewerWindow.clearLine() + viewerWindow.write(" " .. string.char(17) .. " File: " .. string.sub(info.source, 2)) + viewerWindow.setCursorPos(1, 2) + if string.sub(info.source, -8) == "bios.lua" then info.source = "@/bios.lua" end + if info.source and info.currentline then + if fs.exists(string.sub(info.source, 2)) then + local file = fs.open(string.sub(info.source, 2), "r") + if file ~= nil then + lines = {} + local l = file.readLine() + while l ~= nil do + l = string.gsub(l, "\t", " ") + table.insert(lines, l) + l = file.readLine() + end + file.close() + if info.currentline < h / 2 then scrollPos = 1 + elseif info.currentline > #lines - (h / 2) then scrollPos = #lines - h + else scrollPos = info.currentline - math.floor(h / 2) end + infoCache = info + renderFile() + else + lines = nil + viewerWindow.setTextColor(colors.red) + viewerWindow.write("Could not open source") + end + else + lines = nil + viewerWindow.setTextColor(colors.red) + viewerWindow.write("Could not find source") + end + else + lines = nil + viewerWindow.write("No source available") + end +end + +print("Waiting for break...") +local wait = true +local screen = false +while true do + if wait then os.pullEvent("debugger_break") end + w, h = term.getSize() + if screen then + selectedLine = 1 + local info = debugger.getInfo(selectedLine - 1) + if info and info.short_src ~= "[C]" and info.short_src ~= "(tail call)" then + showFile(info) + end + else drawTraceback() end + scrollPos = 1 + wait = true + while true do + local ev, p1, p2, p3 = os.pullEvent() + if ev == "key" then + if p1 == keys.enter then + if screen then + debugger.step() + debugger.waitForBreak() + wait = false + break + elseif selectedLine ~= nil then + local info = debugger.getInfo(selectedLine - 1) + if info and info.short_src ~= "[C]" and info.short_src ~= "(tail call)" then + screen = true + showFile(info) + end + end + elseif p1 == keys.up then + if screen then + if scrollPos > 1 then + scrollPos = scrollPos - 1 + renderFile() + end + else + if selectedLine == nil then selectedLine = 1 end + if selectedLine > 1 then + selectedLine = selectedLine - 1 + if scrollPos > selectedLine then scrollPos = selectedLine end + drawTraceback() + stackWindow.reposition(1, 2 - scrollPos) + end + end + elseif p1 == keys.down then + if screen then + if scrollPos < #lines - h + 2 then + scrollPos = scrollPos + 1 + renderFile() + end + else + if selectedLine == nil then selectedLine = 0 end + if debugger.getInfo(selectedLine) then + selectedLine = selectedLine + 1 + if scrollPos + h - 2 < selectedLine then scrollPos = scrollPos + 1 end + drawTraceback() + stackWindow.reposition(1, 2 - scrollPos) + end + end + elseif p1 == keys.left and screen then + selectedLine = nil + screen = false + scrollPos = 1 + drawTraceback() + elseif p1 == keys.right and not screen and selectedLine ~= nil then + local info = debugger.getInfo(selectedLine - 1) + if info and info.short_src ~= "[C]" and info.short_src ~= "(tail call)" then + screen = true + showFile(info) + end + end + elseif ev == "mouse_click" and p1 == 1 then + if screen then + if p2 >= 1 and p2 <= 3 and p3 == 1 then + selectedLine = nil + screen = false + scrollPos = 1 + drawTraceback() + end + else + if selectedLine == p3 - 2 + scrollPos then + local info = debugger.getInfo(selectedLine - 1) + if info and info.short_src ~= "[C]" and info.short_src ~= "(tail call)" then + screen = true + showFile(info) + end + elseif debugger.getInfo(p3 - 3 + scrollPos) then + selectedLine = p3 - 2 + scrollPos + drawTraceback() + stackWindow.reposition(1, 2 - scrollPos) + end + end + elseif ev == "mouse_scroll" then + if screen then + if p1 == 1 and scrollPos < #lines - h + 2 then + scrollPos = scrollPos + 1 + renderFile() + elseif p1 == -1 and scrollPos > 1 then + scrollPos = scrollPos - 1 + renderFile() + end + else + local _, vwh = stackWindow.getSize() + if p1 == 1 and scrollPos < vwh - h + 1 then + scrollPos = scrollPos + 1 + stackWindow.reposition(1, 2 - scrollPos) + elseif p1 == -1 and scrollPos > 1 then + scrollPos = scrollPos - 1 + stackWindow.reposition(1, 2 - scrollPos) + end + end + elseif ev == "term_resize" then + w, h = term.getSize() + if screen then renderFile() else drawTraceback() end + elseif ev == "debugger_done" then break end + end + if wait then + term.clear() + term.setCursorPos(1, 1) + print("Waiting for break...") + end +end +end) +if not s then io.stderr:write(e .. "\n") end +while true do os.pullEvent() end \ No newline at end of file diff --git a/data/computercraft/lua/debug/startup.lua b/data/computercraft/lua/debug/startup.lua new file mode 100644 index 0000000..b7e5b6d --- /dev/null +++ b/data/computercraft/lua/debug/startup.lua @@ -0,0 +1,10 @@ +local shell = require "shell" +if debugger.useDAP and debugger.useDAP() then + shell.run("debug/adapter.lua") +else + shell.openTab("debug/showfile.lua") + shell.openTab("debug/profiler.lua") + shell.openTab("debug/console.lua") + shell.run("debug/debugger.lua") +end +shell.exit() \ No newline at end of file From bc583e1b6a4bc603da4bd79ff5947b162ceb0c4c Mon Sep 17 00:00:00 2001 From: MCJack123 Date: Thu, 18 Aug 2022 16:34:31 -0400 Subject: [PATCH 2/2] Added licensing info for debugger files --- LICENSE | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE b/LICENSE index 7d23ed1..7ff1c4f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ Copyright (c) 2022 Ocawesome101 +Copyright (c) 2019-2022 JackMacWindows (/debug) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: