Compare commits

..

4 Commits

Author SHA1 Message Date
d654e1776a Single pump for both lua and mdb outputs 2025-11-20 12:36:55 +01:00
fa0c8d5a2c Went back to single script for luadebug 2025-10-27 14:39:55 +01:00
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
92fd3cabf8 Moved luadebug to the new structure as well 2025-10-26 13:01:40 +01:00
5 changed files with 307 additions and 120 deletions

View File

@ -4,7 +4,7 @@ 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",
@ -14,7 +14,8 @@ config.plugins.jpdebug = {
type = "luadebug", type = "luadebug",
entry = "test.lua", entry = "test.lua",
cwd = ".", cwd = ".",
lua = {"lua"}, -- lua = {"lua"},
lua = {"C:\\msys64\\ucrt64\\bin\\lua.exe"},
}, },
}, },
default_target = "luadebug" default_target = "luadebug"

View File

@ -2,72 +2,137 @@ local core = require "core"
---@class runner ---@class runner
local runner = { local runner = {
new = function(self, o) end, ---@meta 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 run = function(self, target, name) end, ---@meta
wait = function(sefl, time) 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 kill = function(self) end, ---@meta
terminate = 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 = {} local debugger = {
local debugwindow = nil ---@type JPDebugView runner = runner, -- Set here as member to let runners extend this base class
local debugrunner = nil ---@type runner|nil debugwindow = nil, ---@type JPDebugView
debugrunner = nil, ---@type runner|nil
state = "idle",
}
function debugger.log(msg) function debugger.log(msg)
core.log("[jpdebug][debugger] %s", msg) core.log("[jpdebug][debugger] %s", msg)
if debugwindow then debugwindow:push("meta", "debugger] "..msg) end if debugger.debugwindow then debugger.debugwindow:push("meta", "debugger] "..msg) end
end end
function debugger.error(msg) function debugger.error(msg)
core.error("[jpdebug][debugger]"..msg) core.error("[jpdebug][debugger]"..msg)
if debugwindow then debugwindow:push("meta", "debugger] ERROR: "..msg) end if debugger.debugwindow then debugger.debugwindow:push("meta", "debugger] ERROR: "..msg) end
end end
function debugger.stdout(msg) function debugger.on_stdout(msg)
if debugwindow then debugwindow:push("stdout", msg) end if debugger.debugwindow then debugger.debugwindow:push("stdout", msg) end
end end
function debugger.stderr(msg) function debugger.on_stderr(msg)
if debugwindow then debugwindow:push("stderr", msg) end if debugger.debugwindow then debugger.debugwindow:push("stderr", msg) end
end end
function debugger.is_running() function debugger.is_running()
return debugrunner~=nil 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 end
function debugger.run(target, name, r, view) function debugger.run(target, name, r, view)
debugwindow = view if debugger.debugrunner then
debugwindow:clear() 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)) debugger.log(string.format("Running %s", name))
-- Create new runner object -- Create new runner object
debugrunner = r:new({ debugger.debugrunner = r:new({
-- Set callbacks
log = debugger.log, log = debugger.log,
error = debugger.error, error = debugger.error,
stdout = debugger.stdout, on_stdout = debugger.on_stdout,
stderr = debugger.stderr, on_stderr = debugger.on_stderr,
exited = debugger.exited, on_exit = debugger.on_exit,
on_state = debugger.on_state,
on_break = debugger.on_break,
}) })
-- And run -- And run
debugrunner:run(target, name) debugger.debugrunner:run(target, name)
end end
function debugger.stop() function debugger.stop()
if debugrunner then if debugger.debugrunner then
debugrunner:kill() debugger.debugrunner.on_exit = function() end
local exitcode = debugrunner:wait(1000) debugger.debugrunner:kill()
local exitcode = debugger.debugrunner:wait(1000)
-- TODO terminate if needed -- TODO terminate if needed
debugger.log(string.format("... Stoped: %d", exitcode)) debugger.log(string.format("... Stoped with exit code %d", exitcode))
end end
debugrunner = nil debugger.debugrunner = nil
debugger.on_state('idle')
end end
function debugger.exited() function debugger.on_exit(exitcode)
if debugrunner then debugger.log(string.format("exit: %d", exitcode))
local exitcode = debugrunner:wait(process.WAIT_INFINITE) debugger.debugrunner = nil
debugger.log(string.format("exit: %d", exitcode)) debugger.on_state('idle')
end
debugrunner = nil
end end
return debugger return debugger

View File

@ -153,19 +153,30 @@ 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 ensure_debug_view()
if active_view then
return active_view
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) local function run_target(target, name)
-- Create/get view to push text to -- Create/get view to push text to
-- TODO fix this, it throws a node is locked error once in a while -- TODO fix this, it throws a node is locked error once in a while
local view = nil local view = ensure_debug_view()
if active_view then
-- If there is already a view use that one
view = active_view
else
-- Otherwhise lets make one
view = JPDebugView()
core.root_view:get_active_node():add_view(view)
active_view = view
end
-- 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
@ -233,6 +244,11 @@ if required_toolbar_plugins and ToolbarView then
else else
table.insert(t, {symbol = "D", command = "jpdebug:stop"}) table.insert(t, {symbol = "D", command = "jpdebug:stop"})
end 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"}) table.insert(t, {symbol = "E", command = "jpdebug:reload-runners"})
self.toolbar_commands = t self.toolbar_commands = t
end end

View File

@ -1,5 +1,6 @@
local core = require "core" local core = require "core"
local process = require "process" local process = require "process"
local debugger = require "plugins.jpdebug.debugger"
-- tiny helpers -- tiny helpers
local function dirname(p) return p:match("^(.*)[/\\]") or "" end local function dirname(p) return p:match("^(.*)[/\\]") or "" end
@ -14,8 +15,95 @@ local function get_plugin_root()
return dirname(here) -- …/jpdebug return dirname(here) -- …/jpdebug
end 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 -- Main lua process spawner
local function spawn_lua_process(entry, lua, cwd, env) function luadebug:spawn_lua_process(entry, lua, cwd, env)
local host = "localhost" local host = "localhost"
local port = 8172 local port = 8172
@ -26,13 +114,17 @@ local function spawn_lua_process(entry, lua, cwd, env)
local dbg_call = string.format([[ local dbg_call = string.format([[
local ok, m = pcall(require, "mobdebug"); if ok then local ok, m = pcall(require, "mobdebug"); if ok then
print("Connecting to "..%q..":"..tostring(%d)) print("Connecting to "..%q..":"..tostring(%d))
m.connecttimeout = 0.1
local connected = m.start(%q, %d) local connected = m.start(%q, %d)
if not connecten then m.off() end
end end
print("Test123")
]], host, port, host, port) ]], host, port, host, port)
local launcher = string.format([[ local launcher = string.format([[
package.path = %q .. ";" .. package.path package.path = %q .. ";" .. package.path
%s %s
dofile(%q) dofile(%q)
os.exit()
]], vendor_glob, dbg_call, entry) ]], vendor_glob, dbg_call, entry)
-- Spawn the process -- Spawn the process
@ -50,66 +142,92 @@ local function spawn_lua_process(entry, lua, cwd, env)
stdout = process.REDIRECT_PIPE, stdout = process.REDIRECT_PIPE,
stderr = process.REDIRECT_PIPE, stderr = process.REDIRECT_PIPE,
}) })
return proc return proc
end end
---@class LDB -- ---------------- Mobdebug -------------------------------------
local LDB = { function luadebug:spawn_mdb(lua)
name = "luadebug" local host = "localhost"
} local port = 8172
---@param target table Target table -- Resolve vendor/?.lua so "require('mobdebug')" finds the bundled file
---@param name string Name of the target to run local vendor_glob = join(get_plugin_root(), "vendor/?.lua")
---@praam debuginfo table Debugging information
---@return process|nil local cmd = {}
function LDB:run(target, name, debuginfo) if type(lua) == "table" then
if target.entry == nil then for i=1, #lua do table.insert(cmd, lua[i]) end
core.error("[jpdebug][luadebug] target.entry is required") 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 return
end end
local entry = target.entry
local lua = target.lua or "lua"
local cwd = target.cwd or "."
local env = target.env or {}
-- spawn the main lua process self.mdb = proc
local proc = spawn_lua_process(entry, lua, cwd, env)
if proc == nil then
core.error("[jpdebug][luadebug] Failed to start "..entry)
return nil
end
return {
luaproc=proc
}
end end
-- Wait untill it ends, possibly with timeout -- ---------------- Pumps ---------------------------------------
function LDB:wait(proc, time) function luadebug:pumps()
return proc.luaproc:wait(time) 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 end
-- Read the stdout, returns nil if process has stopped
function LDB:read_stdout(proc)
local sl = proc.luaproc:read_stdout()
return sl
end
-- Read the stderr return luadebug
function LDB:read_stderr(proc)
local sl = proc.luaproc:read_stderr()
return sl
end
-- Kill the process
function LDB:kill(proc)
proc.luaproc:kill()
end
-- Terminate the process
function LDB:terminate(proc)
proc.luaproc:terminate()
end
return LDB

View File

@ -1,30 +1,12 @@
local core = require "core" local core = require "core"
local process = require "process" local process = require "process"
local debugger = require "plugins.jpdebug.debugger"
---@class shell: runner ---@class shell: runner
local shell = { local shell = debugger.runner:new({
name = "shell", name = "shell",
proc = nil ---@type process|nil proc = nil ---@type process|nil
} })
function shell:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
---@meta
function shell.log(msg) end
---@meta
function shell.error(msg) end
---@meta
function shell.stdout(msg) end
---@meta
function shell.stderr(msg) end
---@meta
function shell.exited() end
function shell:run(target, name) function shell:run(target, name)
local opts = { local opts = {
@ -43,6 +25,8 @@ function shell:run(target, name)
self.error(string.format("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 -- output pump
core.add_thread(function() core.add_thread(function()
while true do while true do
@ -50,12 +34,15 @@ function shell:run(target, name)
coroutine.yield(0.016) -- 60FPS coroutine.yield(0.016) -- 60FPS
local sout = self.proc:read_stdout() local sout = self.proc:read_stdout()
local serr = self.proc:read_stderr() local serr = self.proc:read_stderr()
if sout == nil and serr == nil then if sout == nil or serr == nil then
self.exited() -- 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 break
end end
if sout and sout~="" then self.stdout(sout) end if sout and sout~="" then self.on_stdout(sout) end
if serr and serr~="" then self.stderr(serr) end if serr and serr~="" then self.on_stderr(serr) end
end end
end) end)
end end