Compare commits

...

10 Commits

Author SHA1 Message Date
U-ENGINEERO\joppe.blondel
d654e1776a Single pump for both lua and mdb outputs 2025-11-20 12:36:55 +01:00
U-ENGINEERO\joppe.blondel
fa0c8d5a2c Went back to single script for luadebug 2025-10-27 14:39:55 +01:00
Joppe Blondel
349cbc8175 mdb runner/parser WIP
Do I continue like this? or just integrate the mdb runner itself into
luadebug?
2025-10-26 16:30:57 +01:00
Joppe Blondel
92fd3cabf8 Moved luadebug to the new structure as well 2025-10-26 13:01:40 +01:00
Jojojoppe
abedca2394 Added separate debugger file and fixed shell to new form 2025-10-25 20:07:49 +02:00
Jojojoppe
f6c031849f Some code movement 2025-10-25 18:25:50 +02:00
Joppe Blondel
8f5d71a6b9 Added hot reload of loaders button and did a bit of luadebug 2025-10-24 14:53:00 +02:00
Joppe Blondel
1cd7b378b6 Start with luadebug 2025-10-23 23:13:48 +02:00
Joppe Blondel
8cde56a4e8 Added better documentation for linting 2025-10-23 22:18:48 +02:00
Joppe Blondel
7b822746d3 Added toolbar with start and stop button 2025-10-23 21:27:46 +02:00
8 changed files with 617 additions and 80 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
logs.txt

View File

@@ -4,12 +4,19 @@ config.plugins.jpdebug = {
targets = { targets = {
["test - msys"] = { ["test - msys"] = {
type = "shell", type = "shell",
cmd = {"C:\\msys64\\msys2_shell.cmd", "-defterm", "-here", "-no-start", "-ucrt64", "-shell", "bash", "-c", "lua test.lua"} cmd = {"C:\\msys64\\ucrt64\\bin\\lua.exe", "test.lua"}
}, },
["test"] = { ["test"] = {
type = "shell", type = "shell",
cmd = {"lua", "test.lua"} cmd = {"lua", "test.lua"}
} },
["luadebug"] = {
type = "luadebug",
entry = "test.lua",
cwd = ".",
-- lua = {"lua"},
lua = {"C:\\msys64\\ucrt64\\bin\\lua.exe"},
},
}, },
default_target = "test" default_target = "luadebug"
} }

138
debugger.lua Normal file
View File

@@ -0,0 +1,138 @@
local core = require "core"
---@class runner
local runner = {
new = function(self, o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end,
caps = {
can_pause = false,
can_continue = false,
can_step_in = false,
can_step_over = false,
can_step_out = false,
can_breakpoints = false,
has_stack = false,
has_locals = false,
can_eval = false,
},
run = function(self, target, name) end, ---@meta
pause = function(self) end, --@meta
continue = function(self) end, --@meta
step_in = function(self) end, --@meta
step_over = function(self) end, --@meta
step_out = function(self) end, --@meta
wait = function(self, time) end, ---@meta
kill = function(self) end, ---@meta
terminate = function(self) end, ---@meta
-- Callbacks
log = function(msg) end, ---@meta
error = function(msg) end, ---@meta
on_stdout = function(msg) end, ---@meta
on_stderr = function(msg) end, ---@meta
on_exit = function(exitcode) end, ---@meta
on_state = function(state) end, ---@meta
on_break = function(file, line, reason) end, --@meta
on_stack = function(frames) end, ---@meta
on_locals = function(frame, vars) end, ---@meta
on_evaluated = function(expr, ok, value) end, ---@meta
}
local debugger = {
runner = runner, -- Set here as member to let runners extend this base class
debugwindow = nil, ---@type JPDebugView
debugrunner = nil, ---@type runner|nil
state = "idle",
}
function debugger.log(msg)
core.log("[jpdebug][debugger] %s", msg)
if debugger.debugwindow then debugger.debugwindow:push("meta", "debugger] "..msg) end
end
function debugger.error(msg)
core.error("[jpdebug][debugger]"..msg)
if debugger.debugwindow then debugger.debugwindow:push("meta", "debugger] ERROR: "..msg) end
end
function debugger.on_stdout(msg)
if debugger.debugwindow then debugger.debugwindow:push("stdout", msg) end
end
function debugger.on_stderr(msg)
if debugger.debugwindow then debugger.debugwindow:push("stderr", msg) end
end
function debugger.is_running()
return debugger.debugrunner~=nil
end
function debugger.on_state(state)
debugger.log(string.format("state %s -> %s", debugger.state, state))
debugger.state = state
end
function debugger.on_break(file, line, reason)
debugger.log(string.format("breakpoint hit: %s:%d", file, line))
-- Temporary continue at unknown breaks
if file=='?' and line==-1 then
if debugger.debugrunner.caps.can_continue then
debugger.debugrunner:continue()
end
end
end
function debugger.run(target, name, r, view)
if debugger.debugrunner then
if debugger.state == "paused" then
debugger.debugrunner:continue()
else
debugger.error("Already an active session")
end
return
end
debugger.debugwindow = view
debugger.debugwindow:clear()
debugger.log(string.format("Running %s", name))
-- Create new runner object
debugger.debugrunner = r:new({
-- Set callbacks
log = debugger.log,
error = debugger.error,
on_stdout = debugger.on_stdout,
on_stderr = debugger.on_stderr,
on_exit = debugger.on_exit,
on_state = debugger.on_state,
on_break = debugger.on_break,
})
-- And run
debugger.debugrunner:run(target, name)
end
function debugger.stop()
if debugger.debugrunner then
debugger.debugrunner.on_exit = function() end
debugger.debugrunner:kill()
local exitcode = debugger.debugrunner:wait(1000)
-- TODO terminate if needed
debugger.log(string.format("... Stoped with exit code %d", exitcode))
end
debugger.debugrunner = nil
debugger.on_state('idle')
end
function debugger.on_exit(exitcode)
debugger.log(string.format("exit: %d", exitcode))
debugger.debugrunner = nil
debugger.on_state('idle')
end
return debugger

237
init.lua
View File

@@ -5,24 +5,41 @@ local command = require "core.command"
local View = require "core.view" local View = require "core.view"
local process = require "process" local process = require "process"
local config = require "core.config" local config = require "core.config"
local TreeView = require "plugins.treeview"
local ToolbarView = require "plugins.toolbarview"
local debugger = require "plugins.jpdebug.debugger"
core.jpdebug = core.jpdebug or {} core.jpdebug = core.jpdebug or {}
-- Global list of all the runners -- Global list of all the runners
core.jpdebug.runners = core.jpdebug.runners or {} core.jpdebug.runners = core.jpdebug.runners or {}
local runner_shell = require("plugins.jpdebug.runners.shell")
core.jpdebug.runners[runner_shell.name] = runner_shell
-- A list of created views -- Currently used view
local active_views = {} local active_view = nil
-- The selected target -- The selected target
local selected_target = nil local selected_target = nil
-- Local helper functions for debugging -------------------------- -- Table containing all debug information (like breakpoints)
local debug_info = {}
-- Check if required plugins are installed
local required_toolbar_plugins = true
if TreeView == nil or ToolbarView == nil then
required_toolbar_plugins = false
end
-- Local helper functions ----------------------------------------
-- Dump any table to a string
---@param o any
---@param force bool|nil
---@return nil
---@diagnostic disable-next-line: unused-function ---@diagnostic disable-next-line: unused-function
local function dump(o) local function dump(o, force)
if type(o) == 'table' then force = force or false
if type(o) == 'table' or force then
local s = '{ ' local s = '{ '
for k,v in pairs(o) do for k,v in pairs(o) do
if type(k) ~= 'number' then k = '"'..k..'"' end if type(k) ~= 'number' then k = '"'..k..'"' end
@@ -30,10 +47,37 @@ local function dump(o)
end end
return s .. '} ' return s .. '} '
else else
return tostring(o) return type(o)..": "..tostring(o)
end end
end end
-- Get the plugin's path
---@return string|nil
local function get_plugin_directory()
local paths = {
USERDIR .. PATHSEP .. "plugins" .. PATHSEP .. "jpdebug",
DATADIR .. PATHSEP .. "plugins" .. PATHSEP .. "jpdebug"
}
for _, v in ipairs(paths) do
if system.get_file_info(v) then
return v
end
end
return nil
end
-- Simple function splitting strings
local function stringsplit(inputstr, sep)
if sep == nil then
sep = "%s"
end
local t = {}
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
table.insert(t, str)
end
return t
end
-- ---------- JPDebugView: a simple scrollable log view ---------- -- ---------- JPDebugView: a simple scrollable log view ----------
---@class JPDebugView : core.view ---@class JPDebugView : core.view
local JPDebugView = View:extend() local JPDebugView = View:extend()
@@ -41,6 +85,7 @@ local JPDebugView = View:extend()
function JPDebugView:new(title) function JPDebugView:new(title)
JPDebugView.super.new(self) JPDebugView.super.new(self)
self.scrollable = true self.scrollable = true
self.context = "session"
self.caption = title or "JP Debug" self.caption = title or "JP Debug"
self.lines = { "ready.\n" } self.lines = { "ready.\n" }
self.max_lines = 5000 -- keep memory bounded self.max_lines = 5000 -- keep memory bounded
@@ -57,15 +102,11 @@ function JPDebugView:get_scrollable_size()
end end
function JPDebugView:push(kind, s) function JPDebugView:push(kind, s)
--TODO do some things with kind here
if not s or s == "" then return end if not s or s == "" then return end
-- split on newlines; prefix stderr local lines = stringsplit(s, "\n")
for line in (s .. "\n"):gmatch("(.-)\n") do for _,l in pairs(lines) do
if kind == "stderr" then self.lines[#self.lines + 1] = l
line = "[stderr] " .. line
else
line = "[stdout] " .. line
end
self.lines[#self.lines + 1] = line
if #self.lines > self.max_lines then if #self.lines > self.max_lines then
local drop = #self.lines - self.max_lines local drop = #self.lines - self.max_lines
for _ = 1, drop do table.remove(self.lines, 1) end for _ = 1, drop do table.remove(self.lines, 1) end
@@ -94,6 +135,12 @@ function JPDebugView:draw()
self:draw_scrollbar() self:draw_scrollbar()
end end
function JPDebugView:try_close(do_close)
---@diagnostic disable-next-line: undefined-field
JPDebugView.super.try_close(self, do_close)
active_view = nil
end
-- ---------- helper: pick a target from project module ---------- -- ---------- helper: pick a target from project module ----------
local function get_targets() local function get_targets()
local t = (config.plugins and config.plugins.jpdebug and config.plugins.jpdebug.targets) or {} local t = (config.plugins and config.plugins.jpdebug and config.plugins.jpdebug.targets) or {}
@@ -106,61 +153,116 @@ local function get_selected_target()
end end
-- ---------- run target & pipe stdout/stderr into the view ---------- -- ---------- run target & pipe stdout/stderr into the view ----------
local function run_target(target, name) local function ensure_debug_view()
local title = ("JP Debug: %s"):format(name) if active_view then
return active_view
local view = nil
if active_views[title] then
-- If there is already a view use that one
view = active_views[title]
else
-- Otherwhise lets make one
view = JPDebugView(title)
core.root_view:get_active_node():add_view(view)
active_views[title] = view
end end
local node = core.root_view:get_active_node()
local view = JPDebugView()
-- Defer the add until the node is unlocked (next tick).
core.add_thread(function()
-- Wait until the layout is safe to mutate
while node.locked do coroutine.yield(0) end
node:add_view(view)
core.redraw = true
end)
active_view = view
return view
end
local function run_target(target, name)
-- Create/get view to push text to
-- TODO fix this, it throws a node is locked error once in a while
local view = ensure_debug_view()
-- Check if we have a runner -- Check if we have a runner
for runner_name,runner in pairs(core.jpdebug.runners) do for runner_name,runner in pairs(core.jpdebug.runners) do
if runner_name == target.type then if runner_name == target.type then
-- Found a runner debugger.run(target, name, runner, view)
local proc = runner:run(target.cmd, {
cwd = target.cwd or ".",
env = target.env or {},
stdout = process.REDIRECT_PIPE,
stderr = process.REDIRECT_PIPE
}, name)
if proc == nil then
core.error("[jpdebug] Could not run the target")
return
end
view:clear()
-- background pump (non-blocking I/O)
core.add_thread(function()
while true do
coroutine.yield(0.016) -- ~60fps
local out = proc:read_stdout()
if out == nil then
-- stdout pipe closed: try drain stderr and break when both closed
local err = proc:read_stderr()
if err ~= nil and err ~= "" then view:push("stderr", err) end
break
end
if out ~= "" then view:push("stdout", out) end
local err = proc:read_stderr()
if err ~= nil and err ~= "" then view:push("stderr", err) end
end
local code = proc:wait(process.WAIT_INFINITE)
view:push("stdout", ("\n[exit] code=%s\n"):format(tostring(code)))
end)
return view return view
end end
end end
-- No suitable runners found -- No suitable runners found
core.error("[jpdebug] No suitable runners found for target %s", name) debugger.error(string.format("No suitable runners found for target %s", name))
end
-- ---------- Reload a module on the go ------------------------------
local function hot_require(name, into)
-- blow away the cached module
package.loaded[name] = nil
local fresh = require(name)
if into then
-- wipe old table (keep the identity)
for k in pairs(into) do into[k] = nil end
-- copy new fields in
for k, v in pairs(fresh) do into[k] = v end
-- copy metatable too, if any
local mt = getmetatable(fresh)
if mt then setmetatable(into, mt) end
return into
else
return fresh
end
end
-- Load the runners table on the go
local function load_runners()
-- TODO Remove: reload debugger as well here
debugger = hot_require("plugins.jpdebug.debugger", core.jpdebug.debugger)
local runner_shell = hot_require("plugins.jpdebug.runners.shell", core.jpdebug.runners.shell)
local runner_luadebug = hot_require("plugins.jpdebug.runners.luadebug", core.jpdebug.runners.luadebug)
core.jpdebug.runners[runner_shell.name] = runner_shell
core.jpdebug.runners[runner_luadebug.name] = runner_luadebug
core.log("[jpdebug] Runners loaded")
end
-- And call it while loading the plugin
load_runners()
-- ---------- Add toolbar to treeview if plugins are installed ------
if required_toolbar_plugins and ToolbarView then
---@class Toolbar: core.view
local Toolbar = ToolbarView:extend()
function Toolbar:new()
Toolbar.super.new(self)
self.toolbar_font = renderer.font.load(get_plugin_directory() .. PATHSEP .. "toolbar_commands.ttf", style.icon_big_font:get_size())
self.toolbar_commands = {}
end
function Toolbar:_rebuild()
local t = {
{symbol = "A", command = "jpdebug:set-target"},
}
if not debugger.is_running() then
table.insert(t, {symbol = "B", command = "jpdebug:run"})
else
table.insert(t, {symbol = "D", command = "jpdebug:stop"})
end
if debugger.is_running() then
if debugger.debugrunner.caps.can_pause then table.insert(t, {symbol="C",command=""}) end
end
table.insert(t, {symbol = "E", command = "jpdebug:reload-runners"})
self.toolbar_commands = t
end
function Toolbar:update()
---@diagnostic disable-next-line: undefined-field
Toolbar.super.update(self)
self:_rebuild()
end
local toolbar_view = Toolbar()
---@diagnostic disable-next-line: unused-local
local toolbar_node = TreeView.node.b:split("up", toolbar_view, {y = true})
end end
---@diagnostic disable-next-line: param-type-mismatch ---@diagnostic disable-next-line: param-type-mismatch
@@ -184,8 +286,12 @@ command.add(nil, {
end end
end, end,
["jpdebug:stop"] = function()
debugger.stop()
end,
-- The set target command -- The set target command
["jpdebug:settarget"] = function() ["jpdebug:set-target"] = function()
core.command_view:enter("Select target", { core.command_view:enter("Select target", {
show_suggestions = true, show_suggestions = true,
submit = function(selection) submit = function(selection)
@@ -204,5 +310,10 @@ command.add(nil, {
end end
}) })
end, end,
["jpdebug:reload-runners"] = function()
load_runners()
end,
}) })

233
runners/luadebug.lua Normal file
View File

@@ -0,0 +1,233 @@
local core = require "core"
local process = require "process"
local debugger = require "plugins.jpdebug.debugger"
-- tiny helpers
local function dirname(p) return p:match("^(.*)[/\\]") or "" end
local function join(a, b) return (a:sub(-1) == "/" and a or (a .. "/")) .. b end
-- returns absolute path to the plugin root (…/jpdebug)
local function get_plugin_root()
-- debug.getinfo(1, "S").source gives "@/full/path/to/this/file.lua"
local src = debug.getinfo(1, "S").source
if src:sub(1, 1) == "@" then src = src:sub(2) end
local here = dirname(src) -- …/jpdebug/runners
return dirname(here) -- …/jpdebug
end
-- Check if string starts with a substring
local function starts_with(str, word)
return str:sub(1, #word) == word
end
-- ------------------- Runner API --------------------------------
---@class luadebug: runner
local luadebug = debugger.runner:new({
name = "luadebug",
proc = nil, ---@type process|nil
caps = {
can_pause = false,
can_continue = true,
can_step_in = true,
can_step_over = true,
can_step_out = true,
can_breakpoints = true,
has_stack = true,
has_locals = true,
can_eval = false,
},
})
function luadebug:run(target, name)
if target.entry == nil then
self.error("target.entry is required for "..name)
return
end
local entry = target.entry
local lua = target.lua or "lua"
local cwd = target.cwd or "."
local env = target.env or {}
-- spawn the debugger
self:spawn_mdb(lua)
-- TODO error checking
-- spawn the main lua process
local proc = self:spawn_lua_process(entry, lua, cwd, env)
if proc == nil then
self.error("Failed to start "..entry)
-- TODO kill mdb
return
end
self.proc = proc
self.on_state('connecting')
-- Start output pump
self:pumps()
end
function luadebug:continue()
self.mdb:write('run\n')
self.on_state('running')
end
function luadebug:wait(time)
if not self.proc then return end
return self.proc:wait(time)
end
function luadebug:read_stdout()
if not self.proc then return end
local sl = self.proc:read_stdout()
return sl
end
function luadebug:read_stderr()
if not self.proc then return end
local sl = self.proc:read_stderr()
return sl
end
function luadebug:kill()
if not self.proc then return end
self.proc:kill()
end
function luadebug:terminate()
if not self.proc then return end
self.proc:terminate()
end
-- --------------- Lua Process -----------------------------------
-- Main lua process spawner
function luadebug:spawn_lua_process(entry, lua, cwd, env)
local host = "localhost"
local port = 8172
-- Resolve vendor/?.lua so "require('mobdebug')" finds the bundled file
local vendor_glob = join(get_plugin_root(), "vendor/?.lua")
-- Build the inline lua launcher
local dbg_call = string.format([[
local ok, m = pcall(require, "mobdebug"); if ok then
print("Connecting to "..%q..":"..tostring(%d))
m.connecttimeout = 0.1
local connected = m.start(%q, %d)
if not connecten then m.off() end
end
print("Test123")
]], host, port, host, port)
local launcher = string.format([[
package.path = %q .. ";" .. package.path
%s
dofile(%q)
os.exit()
]], vendor_glob, dbg_call, entry)
-- Spawn the process
local cmd = {}
if type(lua) == "table" then
for i=1, #lua do table.insert(cmd, lua[i]) end
else
table.insert(cmd, lua)
end
table.insert(cmd, "-e")
table.insert(cmd, launcher)
local proc = process.start(cmd, {
cwd = cwd,
env = env,
stdout = process.REDIRECT_PIPE,
stderr = process.REDIRECT_PIPE,
})
return proc
end
-- ---------------- Mobdebug -------------------------------------
function luadebug:spawn_mdb(lua)
local host = "localhost"
local port = 8172
-- Resolve vendor/?.lua so "require('mobdebug')" finds the bundled file
local vendor_glob = join(get_plugin_root(), "vendor/?.lua")
local cmd = {}
if type(lua) == "table" then
for i=1, #lua do table.insert(cmd, lua[i]) end
else
table.insert(cmd, lua)
end
table.insert(cmd, "-e")
table.insert(cmd, string.format([[
package.path = %q .. ";" .. package.path
require("mobdebug").listen(%s, %d)
os.exit()
]], vendor_glob, host, port))
local proc = process.start(cmd, {
stdout = process.REDIRECT_PIPE,
stderr = process.REDIRECT_PIPE,
})
if proc == nil then
self.error("Failed to start debugger")
return
end
self.mdb = proc
end
-- ---------------- Pumps ---------------------------------------
function luadebug:pumps()
core.add_thread(function()
while true do
core.redraw = true
coroutine.yield(0.016) -- 60FPS
-- LUA PROGRAM OUTPUT
if true then
local sout = self.proc:read_stdout()
local serr = self.proc:read_stderr()
if sout == nil or serr == nil then
-- Make sure to read stderr for the last time
if serr and serr~="" then self.on_stderr(serr) end
local exitcode = self.proc:wait(process.WAIT_INFINITE)
self.on_exit(exitcode)
break
end
if sout and sout~="" then self.on_stdout(sout) end
if serr and serr~="" then self.on_stderr(serr) end
end
-- MDB OUTPUT
if true then
local sout = self.mdb:read_stdout()
local serr = self.mdb:read_stderr()
if sout == nil or serr == nil then
-- Make sure to read stderr for the last time
if serr and serr~="" then self.on_stderr(serr) end
local exitcode = self.proc:wait(process.WAIT_INFINITE)
self.on_exit(exitcode)
break
end
if sout and sout~="" then self.on_stdout("mdb> "..sout) end
if serr and serr~="" then self.on_stderr("mdb> "..serr) end
if sout and sout~="" then
-- Check output
if starts_with(sout, "Paused") then
self.on_state('paused')
self.on_break('?', -1, 'paused')
end
end
end
end
end)
end
return luadebug

View File

@@ -1,20 +1,66 @@
local core = require "core" local core = require "core"
local process = require "process" local process = require "process"
local debugger = require "plugins.jpdebug.debugger"
---@class M ---@class shell: runner
local M = { local shell = debugger.runner:new({
name = "shell" name = "shell",
} proc = nil ---@type process|nil
})
function M:run(cmd, opts, name) function shell:run(target, name)
core.log("[jpdebug] Running shell command") local opts = {
if cmd then cwd = target.cwd or ".",
local proc = process.start(cmd, opts) env = target.env or {},
return proc stdout = process.REDIRECT_PIPE,
stderr = process.REDIRECT_PIPE
}
if target.cmd then
self.proc = process.start(target.cmd, opts)
if not self.proc then
self.error("Could not start process...")
return
end
else else
core.error("[jpdebug] command not specified for target %s", name) self.error(string.format("command not specified for target %s", name))
end end
self.on_state('running')
-- output pump
core.add_thread(function()
while true do
core.redraw = true
coroutine.yield(0.016) -- 60FPS
local sout = self.proc:read_stdout()
local serr = self.proc:read_stderr()
if sout == nil or serr == nil then
-- Make sure to read stderr for the last time
if serr and serr~="" then self.on_stderr(serr) end
local exitcode = self.proc:wait(process.WAIT_INFINITE)
self.on_exit(exitcode)
break
end
if sout and sout~="" then self.on_stdout(sout) end
if serr and serr~="" then self.on_stderr(serr) end
end
end)
end end
return M function shell:wait(time)
if not self.proc then return end
return self.proc:wait(time)
end
function shell:kill()
if not self.proc then return end
self.proc:kill()
end
function shell:terminate()
if not self.proc then return end
self.proc:terminate()
end
return shell

View File

@@ -1,5 +1,6 @@
print("Starting loop: test 123") print("Starting loop: test 123")
for i = 1, 4 do for i = 1,5 do
print("i =", i) -- we'll stop here print("i = ", i) -- we'll stop here
require("socket").sleep(1)
end end
print("Yaaayyyy it works perfectly fine :)") print("Yaaayyyy it works perfectly fine :)")

BIN
toolbar_commands.ttf Normal file

Binary file not shown.