terminal based frontend for the mobdebug lua remote debugger
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

1314 lines
30 KiB

  1. #!/usr/bin/env lua
  2. --[[
  3. debug.lua
  4. standalone frontend for mobdebug
  5. Gunnar Zötl <gz@tset.de>, 2014
  6. Released under MIT/X11 license. See file LICENSE for details.
  7. --]]
  8. local config_file = "debug.lua.cfg"
  9. local user_config = os.getenv("HOME") .. "/.config/" .. config_file
  10. ---------- configure your colors here ----------------------------------
  11. local default_fg = "WHITE"
  12. local default_bg = "BLACK"
  13. local configuration = {
  14. -- default
  15. fg = "WHITE",
  16. bg = "BLACK",
  17. -- variable display
  18. var_fg = "WHITE",
  19. var_bg = "BLUE",
  20. -- misc foregrounds: breakpoint mark, current line mark, and message
  21. -- after client terminated
  22. mark_bpt_fg = "RED",
  23. mark_cur_fg = "CYAN",
  24. done_fg = "RED",
  25. -- windows (help, dialogs)
  26. widget_fg = "WHITE",
  27. widget_bg = "BLUE",
  28. -- selection
  29. sel_fg = "BLACK",
  30. sel_bg = "CYAN",
  31. -- ui elements (buttons etc). selected elements will use sel_*
  32. elem_fg = "BLUE",
  33. elem_bg = "WHITE",
  34. }
  35. ---------- end configure section ---------------------------------------
  36. -- these are necessary because the mobdebug module recklessly calls
  37. -- print and os.exit(!)
  38. local _G_print = _G.print
  39. _G.print = function() end
  40. local _os_exit = os.exit
  41. os.exit = function() coroutine.yield(_os_exit) end
  42. local mdb = require "mobdebug"
  43. local socket = require "socket"
  44. local port = tonumber((os.getenv("MOBDEBUG_PORT"))) or 8172 -- default
  45. local client
  46. local basedir, basefile
  47. local sources = {}
  48. local current_src = {}
  49. local current_file = ""
  50. local current_line = 0
  51. local selected_line
  52. local select_cmd
  53. local last_search
  54. local last_match = 0
  55. local cmd_output = {}
  56. local cmd_outlog
  57. local pinned_evals = {}
  58. local display_pinned = true
  59. -- compat
  60. local log10 = math.log10 or function(n) return math.log(n, 10) end
  61. local unpack = unpack or table.unpack
  62. ---------- modules -----------------------------------------------------
  63. local loader = require "loader"
  64. local ui = require "ui"
  65. ---------- initialization ----------------------------------------------
  66. local function init()
  67. sources = {}
  68. current_src = {}
  69. current_file = ""
  70. current_line = 0
  71. selected_line = nil
  72. select_cmd = nil
  73. last_search = nil
  74. last_match = 0
  75. cmd_output = {}
  76. pinned_evals = {}
  77. display_pinned = true
  78. end
  79. ---------- misc helpers ------------------------------------------------
  80. local function file_exists(name)
  81. local file, err = io.open(name, "r")
  82. if file then
  83. file:close()
  84. end
  85. return file ~= nil
  86. end
  87. local function output(...)
  88. local line = table.concat({...}, " ")
  89. cmd_output[#cmd_output+1] = line
  90. if cmd_outlog then
  91. cmd_outlog:write(line, "\n")
  92. end
  93. end
  94. local function output_error(...)
  95. output("Error:", ...)
  96. end
  97. local function output_debug(...)
  98. output("DBG:", ...)
  99. end
  100. -- opts: string of single char options, char followed by ':' means opt
  101. -- needs a value
  102. -- arg: table of arguments
  103. local function get_opts(opts, arg)
  104. local i = 1
  105. local opt, val
  106. local optt = {}
  107. local res = {}
  108. while i <= #opts do
  109. local ch = string.sub(opts, i, i)
  110. if string.sub(opts, i+1, i+1) == ':' then
  111. optt[ch] = true
  112. i = i + 2
  113. else
  114. optt[ch] = false
  115. i = i + 1
  116. end
  117. end
  118. i = 1
  119. while arg[i] do
  120. if string.sub(arg[i], 1, 1) == '-' then
  121. opt = string.sub(arg[i], 2, 2)
  122. if optt[opt] then
  123. if #arg[i] > 2 then
  124. val = string.sub(arg[i], 3)
  125. i = i + 1
  126. else
  127. val = arg[i+1]
  128. i = i + 2
  129. end
  130. if val == nil then
  131. return nil, "option -"..opt.." needs an argument"
  132. end
  133. elseif optt[opt] == false then
  134. if #arg[i] == 2 then
  135. val = true
  136. i = i + 1
  137. else
  138. return nil, "option -"..opt.." is a flag"
  139. end
  140. else
  141. return nil, "unknown option -"..opt
  142. end
  143. res[opt] = val
  144. else
  145. res[#res+1] = arg[i]
  146. i = i + 1
  147. end
  148. end
  149. return res
  150. end
  151. local function get_file(file)
  152. if not sources[file] then
  153. local fn = file
  154. if string.sub(file, 1, 1) ~= '/' then
  155. fn = basedir .. '/' .. file
  156. end
  157. local src, err = loader.lualoader(fn)
  158. if not src then
  159. return nil, err
  160. end
  161. sources[file] = src
  162. end
  163. return sources[file]
  164. end
  165. local function set_current_file(file)
  166. local src, err = get_file(file)
  167. if not src then
  168. output_error(err)
  169. src = loader.lualoader()
  170. end
  171. current_file = file
  172. current_src = src
  173. end
  174. ---------- configuration -----------------------------------------------
  175. local config = setmetatable({}, {
  176. __index = function(_, col)
  177. if string.find(col, "fg$") then
  178. return ui.color[default_fg]
  179. else
  180. return ui.color[default_bg]
  181. end
  182. end
  183. })
  184. local function decode_color(c)
  185. local col = ui.color[c or ""]
  186. if col then
  187. return col
  188. end
  189. local r, g, b = string.match(c, "^#([0-5])([0-5])([0-5])$")
  190. if r and g and b then
  191. col = ui.rgb2color(tonumber(r), tonumber(g), tonumber(b))
  192. return col
  193. end
  194. return nil
  195. end
  196. local function loadconfig(name)
  197. local res = {}
  198. local fn, err = loadfile(name, "t", res)
  199. print(fn, err)
  200. local ok
  201. if fn then
  202. ok, err = pcall(fn)
  203. if ok then
  204. for k, v in pairs(res) do
  205. if not configuration[k] then
  206. return nil, "Failed to load config file '" .. name .. "': " .. "invalid option '" .. k .. "'"
  207. elseif not decode_color(v) then
  208. return nil, "Failed to load config file '" .. name .. "': " .. "invalid value for option '" .. k .. "'"
  209. end
  210. end
  211. return res
  212. end
  213. end
  214. return nil, "Failed to load config file " .. err
  215. end
  216. local function readconfig()
  217. local ucfg, lcfg, err
  218. if file_exists(user_config) then
  219. ucfg, err = loadconfig(user_config)
  220. if err then return nil, err end
  221. end
  222. if file_exists(config_file) then
  223. lcfg, err = loadconfig(config_file)
  224. if err then return nil, err end
  225. end
  226. for k, v in pairs(configuration) do
  227. if ucfg and ucfg[k] then
  228. configuration[k] = ucfg[k]
  229. end
  230. if lcfg and lcfg[k] then
  231. configuration[k] = lcfg[k]
  232. end
  233. end
  234. return true
  235. end
  236. local function configure()
  237. local ok, err = readconfig()
  238. if not ok then
  239. return ok, err
  240. end
  241. for k, v in pairs(configuration) do
  242. config[k], err = decode_color(v)
  243. pcall(ui.configure, { k = v })
  244. end
  245. return true
  246. end
  247. ---------- render display ----------------------------------------------
  248. -- source display
  249. local function displaysource_renderrow(r, s, x, y, w, extra)
  250. if s == nil then return end
  251. local isbrk = extra.isbrk
  252. local linew = extra.linew
  253. local rs = string.format("%"..extra.linew.."d", r)
  254. ui.drawfield(x, y, rs, linew)
  255. if isbrk[r] then
  256. ui.setcell(x + linew, y, '*', config.mark_bpt_fg, config.bg)
  257. end
  258. if extra.cur == r then
  259. ui.setcell(x + linew + 1, y, '-', config.mark_cur_fg, config.bg)
  260. ui.setcell(x + linew + 2, y, '>', config.mark_cur_fg, config.bg)
  261. end
  262. local fg, bg = ui.attributes()
  263. if extra.sel == r then
  264. ui.attributes(config.sel_fg, config.sel_bg)
  265. end
  266. return ui.drawfield(x + linew + 3, y, tostring(s), w - linew - 3)
  267. end
  268. local function displaysource(source, x, y, w, h)
  269. local extra = {
  270. isbrk = source.breakpts,
  271. cur = current_line,
  272. sel = selected_line,
  273. linew = math.floor(log10(source.lines)) + 1
  274. }
  275. local first = (selected_line and selected_line or current_line) - math.floor(h/2)
  276. if first < 1 then
  277. first = 1
  278. elseif first + h > #source.src then
  279. first = #source.src - h + 2
  280. end
  281. ui.drawlist(source.src, first, x, y, w, h, displaysource_renderrow, extra)
  282. end
  283. -- pinned variables display
  284. local function displaypinned_renderrow(r, s, x, y, w, extra)
  285. local w1 = extra.w1
  286. local w2 = w - w1
  287. if s then
  288. ui.drawfield(x, y, string.format("%2d:", r), 3)
  289. ui.drawfield(x + 3, y, s[1], w1)
  290. ui.setcell(x + 3 + w1, y, '=')
  291. ui.drawfield(x + 3 + w1 + 1, y, s[2], w2)
  292. if cmd_outlog then
  293. cmd_outlog:write(r, ":\t", s[1], " = ", tostring(s[2]), "\n")
  294. end
  295. end
  296. end
  297. -- we could just feed the reply from the debuggee into loadstring, but
  298. -- then we would lose the already serialized data, so we parse it into a
  299. -- flat table here.
  300. local function displaypinned_readres_table(res, next_token, start)
  301. local lv = 1
  302. local t, s, e
  303. repeat
  304. t, s, e = next_token()
  305. local tv = string.sub(res, s, e)
  306. if tv == '{' then
  307. lv = lv + 1
  308. elseif tv == '}' then
  309. lv = lv - 1
  310. end
  311. until lv == 0
  312. return start, e
  313. end
  314. local function displaypinned_readres_skips(next_token)
  315. local t, s, e, tv
  316. repeat
  317. t, s, e = next_token()
  318. until t ~= 'spc' and t ~= 'com'
  319. return t, s, e
  320. end
  321. local function displaypinned_readres(res)
  322. local next_token = loader.lualexer(res)
  323. if not next_token then return nil end
  324. local rest = {}
  325. local nidx = 1
  326. local t, s, e = next_token()
  327. local tv = string.sub(res, s, e)
  328. if tv ~= '{' then return nil end
  329. repeat
  330. t, s, e = displaypinned_readres_skips(next_token)
  331. tv = string.sub(res, s, e)
  332. if tv == '{' then
  333. s, e = displaypinned_readres_table(res, next_token, s)
  334. rest[nidx] = string.sub(res, s, e)
  335. t, s, e = displaypinned_readres_skips(next_token)
  336. tv = string.sub(res, s, e)
  337. if tv ~= ',' and tv ~= '}' then output_error("unexpected token '"..tv.."'") end
  338. nidx = nidx + 1
  339. elseif tv == '[' then
  340. t, s, e = next_token()
  341. tv = string.sub(res, s, e)
  342. if t ~= 'num' then
  343. output_error("unexpected token '"..tv.."'")
  344. return {}
  345. end
  346. nidx = tonumber(tv)
  347. t, s, e = displaypinned_readres_skips(next_token)
  348. tv = string.sub(res, s, e)
  349. if tv ~= ']' then
  350. output_error("unexpected token '"..tv.."'")
  351. return {}
  352. end
  353. t, s, e = displaypinned_readres_skips(next_token)
  354. tv = string.sub(res, s, e)
  355. if tv ~= '=' then
  356. output_error("unexpected token '"..tv.."'")
  357. end
  358. elseif tv ~= '}' then
  359. local mys = s
  360. repeat
  361. t, s, e = next_token()
  362. if e then tv = string.sub(res, s, e) end
  363. until tv == ',' or tv == '}'
  364. rest[nidx] = string.sub(res, mys, e-1)
  365. nidx = nidx + 1
  366. end
  367. until tv == '}'
  368. return rest
  369. end
  370. local function displaypinned(pinned, x, y, w, h)
  371. local extra = {}
  372. local w1, dw1 = 1, math.floor((w - 3) / 2) - 1
  373. if h > 99 then h = 99 end
  374. while #pinned > h do
  375. table.remove(pinned, 1)
  376. end
  377. -- this is not strictly hygienic. should find a better solution for it.
  378. local cmd = "do local __________t, __________k, __________e = {};"
  379. for i=1, #pinned do
  380. cmd = cmd .. "__________k, __________e = pcall(function() __________t[" .. i .. "]="..pinned[i][1].." end);"
  381. end
  382. cmd = cmd .. "return __________t;end"
  383. local res, _, err = mdb.handle("exec "..cmd, client)
  384. local rt = {}
  385. if res then
  386. --rt = loadstring("return "..res)()
  387. rt = displaypinned_readres(res)
  388. end
  389. for i=1, #pinned do
  390. if #pinned[i][1] > w1 then w1 = #pinned[i][1] end
  391. pinned[i][2] = rt[i]
  392. end
  393. extra.w1 = (w1 < dw1) and w1 or dw1
  394. ui.rect(x, y, w, h)
  395. ui.drawlist(pinned, 1, x, y, w, h, displaypinned_renderrow, extra)
  396. end
  397. -- commands display
  398. local function displaycommands(cmds, x, y, w, h)
  399. local nco = #cmds
  400. local first = h > nco and 1 or nco - h + 1
  401. local y = y + (nco >= h and 1 or h - nco + 1)
  402. ui.drawtext(cmds, first, 1, y, w, h)
  403. end
  404. local function display()
  405. local w, h = ui.size()
  406. local th = h - 1
  407. local srch = math.floor(th / 3 * 2)
  408. local cmdh = th - srch
  409. local srcw = math.floor(w * 3 / 4)
  410. local pinw = w - srcw
  411. srch = srch - 1
  412. if (#pinned_evals == 0) or not display_pinned then
  413. srcw = w
  414. pinw = 0
  415. end
  416. ui.clear(config.fg, config.bg)
  417. ui.drawstatus({"Skript: "..(basefile or ""), "Dir: "..(basedir or ""), "press h for help"}, 1, ' | ')
  418. -- variables view
  419. if pinw > 0 then
  420. ui.attributes(config.var_fg, config.var_bg)
  421. displaypinned(pinned_evals, srcw + 1, 2, pinw, srch-1)
  422. end
  423. -- source view
  424. if select_cmd then
  425. selected_line = selected_line or current_line
  426. if select_cmd == ui.key.ARROW_UP then
  427. selected_line = selected_line - 1
  428. elseif select_cmd == ui.key.ARROW_DOWN then
  429. selected_line = selected_line + 1
  430. elseif select_cmd == ui.key.PGUP then
  431. selected_line = selected_line - srch
  432. elseif select_cmd == ui.key.PGDN then
  433. selected_line = selected_line + srch
  434. elseif select_cmd == ui.key.HOME then
  435. selected_line = 1
  436. elseif select_cmd == ui.key.END then
  437. selected_line = current_src.lines
  438. end
  439. select_cmd = nil
  440. end
  441. if selected_line then
  442. if selected_line < 1 then
  443. selected_line = 1
  444. elseif selected_line > current_src.lines then
  445. selected_line = current_src.lines
  446. end
  447. end
  448. ui.attributes(config.fg, config.bg)
  449. displaysource(current_src, 1, 2, srcw, srch-1)
  450. ui.drawstatus({"File: "..current_file, "Line: "..current_line.."/"..current_src.lines, #pinned_evals > 0 and "pinned: " .. #pinned_evals or ""}, srch + 1)
  451. -- commands view
  452. ui.attributes(config.fg, config.bg)
  453. displaycommands(cmd_output, 1, srch + 1, w, cmdh)
  454. -- input line
  455. ui.printat(1, h, string.rep(' ', w))
  456. ui.setcursor(1,h)
  457. -- more
  458. ui.present()
  459. end
  460. ---------- starting up the debugger ------------------------------------
  461. local function unquote(s)
  462. s = string.gsub(s, "^%s*(%S.+%S)%s*$", "%1")
  463. local ch = string.sub(s, 1, 1)
  464. if ch == "'" or ch == '"' then
  465. s = string.gsub(s, "^" .. ch .. "(.*)" .. ch .. "$", "%1")
  466. end
  467. return s
  468. end
  469. local function find_current_basedir()
  470. local res, line, err = mdb.handle("eval os.getenv('PWD')", client)
  471. if not res then
  472. output_error(err)
  473. return
  474. end
  475. local pwd = unquote(res)
  476. res, line, err = mdb.handle("eval arg[0]", client)
  477. if not res then
  478. output_error(err)
  479. return
  480. end
  481. local arg0 = unquote(res)
  482. if pwd and arg0 then
  483. basedir = basedir or pwd
  484. basefile = string.match(arg0, "/([^/]+)$") or arg0
  485. end
  486. end
  487. local function startup()
  488. init()
  489. ui.attributes(config.widget_fg, config.widget_bg)
  490. local msg = "Waiting for connections on port "..port
  491. local x, y, w, h = ui.frame(#msg, 5, "debug.lua")
  492. ui.printat(x, y+1, msg, w)
  493. ui.present()
  494. local bw, bp, bo = math.floor(#msg/2), 1, 1
  495. local bx = x + math.floor((w - bw) / 2)
  496. local server = socket.bind('*', port)
  497. if not server then
  498. return nil, "could not open server socket."
  499. end
  500. server:settimeout(0.3)
  501. local evt
  502. repeat
  503. ui.printat(bx, y+3, string.rep(' ', bw), bw)
  504. ui.setcell(bx + bp - 1, y+3, '=')
  505. if bp > 1 then ui.setcell(bx + bp - 2, y+3, '-') end
  506. if bp < bw then ui.setcell(bx + bp, y+3, '-') end
  507. bp = bp + bo
  508. if bp >= bw or bp <= 1 then bo = -bo end
  509. ui.present()
  510. client = server:accept()
  511. evt = ui.pollevent(0)
  512. if evt and (evt.key == ui.key.ESC or evt.char == 'q' or evt.char == 'Q') then
  513. server:close()
  514. return false
  515. end
  516. until client ~= nil
  517. server:close()
  518. find_current_basedir()
  519. return true
  520. end
  521. ---------- debugger commands -------------------------------------------
  522. local dbg_args = {}
  523. local function dbg_help()
  524. local t = {
  525. "n | step over next statement",
  526. "s | step into next statement",
  527. "r | run program",
  528. "o | continue until out of current function",
  529. "t [num] | trace execution",
  530. "b [file] line | set breakpoint",
  531. "c [f] ln cond | set conditional breakpoint",
  532. "db [[file] ln]| delete one or all breakpoints",
  533. "= expr | evaluate expression",
  534. "! expr | pin expression",
  535. "d! [num] | delete one or all pinned expressions",
  536. "B dir | set basedir",
  537. "L dir | set only local basedir",
  538. "P | toggle pinned expressions display",
  539. "G [file] [num]| goto line in file or current file or to file",
  540. "/ [str] | search for str in current file, or continue last search",
  541. "W[b|!] file | write setup.",
  542. "h | help",
  543. "q | quit",
  544. "[page] up/down| navigate source file",
  545. "left/right | select current line",
  546. ". | reset view",
  547. "D | stop debugging and continue execution",
  548. }
  549. ui.text(t, "Commands")
  550. end
  551. -- we're only interested in the source positions part
  552. local function dbg_stack()
  553. local res, line, err = mdb.handle("stack", client)
  554. if res then
  555. local r = {}
  556. for k, v in ipairs(res) do
  557. r[k] = v[1]
  558. end
  559. res = r
  560. end
  561. return res, err
  562. end
  563. local function update_where()
  564. local s = dbg_stack()
  565. current_line = s[1][4]
  566. set_current_file(s[1][2])
  567. output(current_file, ":", current_line)
  568. selected_line = nil
  569. end
  570. local function check_break_cond()
  571. if current_src.breakpts[current_line] then
  572. if type((current_src.breakpts[current_line])) == "string" then
  573. local res, line, err = mdb.handle("eval " .. current_src.breakpts[current_line], client)
  574. if err ~= nil then
  575. output_error("in breakpoint condition:", err)
  576. res = true
  577. else
  578. local lres = string.lower(res)
  579. if lres == "false" or lres == "nil" then
  580. res = false
  581. else
  582. res = true
  583. end
  584. if res then
  585. output("Cond:", current_src.breakpts[current_line], " is true")
  586. end
  587. end
  588. return res
  589. else
  590. return true
  591. end
  592. end
  593. return false
  594. end
  595. local function dbg_over()
  596. local res, line, err = mdb.handle("over", client)
  597. update_where()
  598. return nil, err
  599. end
  600. local function dbg_step()
  601. local res, line, err = mdb.handle("step", client)
  602. update_where()
  603. return nil, err
  604. end
  605. local function dbg_run()
  606. local res, line, err
  607. repeat
  608. res, line, err = mdb.handle("run", client)
  609. update_where()
  610. until check_break_cond()
  611. return nil, err
  612. end
  613. local function dbg_out()
  614. local res, line, err = mdb.handle("out", client)
  615. update_where()
  616. return nil, err
  617. end
  618. local function dbg_done()
  619. local res, line, err = mdb.handle("done", client)
  620. update_where()
  621. return nil, err
  622. end
  623. local function dbg_trace(num)
  624. if num and num < 1 then return nil end
  625. local res, err
  626. local steps = 1
  627. while not num or steps <= num do
  628. res, err = dbg_step()
  629. display()
  630. if check_break_cond() then return end
  631. steps = steps + 1
  632. end
  633. return res, err
  634. end
  635. dbg_args[dbg_trace] = 'N'
  636. local function dbg_eval(...)
  637. local expr = table.concat({...}, ' ')
  638. local res, line, err = mdb.handle("eval " .. expr, client)
  639. if not err then res = tostring(res) end
  640. return res, err
  641. end
  642. dbg_args[dbg_eval] = '*'
  643. local function dbg_pin_eval(...)
  644. local expr = table.concat({...}, ' ')
  645. table.insert(pinned_evals, { expr, nil })
  646. return "added pinned expression '"..expr.."'"
  647. end
  648. dbg_args[dbg_pin_eval] = '*'
  649. local function dbg_delpin(_, pin)
  650. if _ then
  651. return nil, "invalid argument #1: number expected"
  652. end
  653. if pin then
  654. if pin >= 1 and pin <= #pinned_evals then
  655. table.remove(pinned_evals, pin)
  656. return "deleted pinned expression #" .. tostring(pin)
  657. else
  658. return nil, "invalid pin number"
  659. end
  660. else
  661. pinned_evals = {}
  662. return "deleted all pinned expressions"
  663. end
  664. end
  665. local function dbg_setb(file, line)
  666. local res, _, err
  667. if file then
  668. res, err = get_file(file)
  669. if not res then
  670. return nil, err
  671. end
  672. else
  673. file = current_file
  674. end
  675. if not get_file(file).canbrk[line] then
  676. return nil, "can't set breakpoint in file '"..file.."' line "..line
  677. end
  678. if file and line then
  679. res, _, err = mdb.handle("setb " .. file .. " " .. line, client)
  680. if not err then
  681. res = "added breakpoint at " .. res .. " line " .. line
  682. get_file(file).breakpts[tonumber(line)] = true
  683. else
  684. res = nil
  685. end
  686. else
  687. err = "command requires file (optional) and line number as arguments"
  688. end
  689. return res, err
  690. end
  691. dbg_args[dbg_setb] = "Sn"
  692. local function dbg_setbcond(file, line, ...)
  693. local res, _, err
  694. local cond = table.concat({...}, ' ')
  695. if file then
  696. res, err = get_file(file)
  697. if not res then
  698. return nil, err
  699. end
  700. else
  701. file = current_file
  702. end
  703. if not get_file(file).canbrk[line] then
  704. return nil, "can't set conditional breakpoint in file '"..file.."' line "..line
  705. end
  706. if file and line then
  707. res, _, err = mdb.handle("setb " .. file .. " " .. line, client)
  708. if not err then
  709. res = "added conditional breakpoint at " .. res .. " line " .. line
  710. get_file(file).breakpts[tonumber(line)] = cond
  711. else
  712. res = nil
  713. end
  714. else
  715. err = "command requires file (optional) and line number as arguments"
  716. end
  717. return res, err
  718. end
  719. dbg_args[dbg_setbcond] = "Sn*"
  720. local function dbg_delb(file, line)
  721. local res, _, err
  722. if file then
  723. res, err = get_file(file)
  724. if not res then
  725. return nil, err
  726. end
  727. else
  728. file = current_file
  729. end
  730. if line then
  731. res, _, err = mdb.handle("delb " .. file .. " " .. line, client)
  732. if not err then
  733. res = "deleted breakpoint at " .. res .. " line " .. line
  734. get_file(file).breakpts[tonumber(line)] = nil
  735. else
  736. res = nil
  737. end
  738. else
  739. res, _, err = mdb.handle("delallb", client)
  740. if not err then
  741. for _, s in pairs(sources) do
  742. s.breakpts = {}
  743. end
  744. res = "deleted all breakpoints"
  745. else
  746. res = nil
  747. end
  748. end
  749. return res, err
  750. end
  751. local function dbg_del(ch, file, line)
  752. if ch == "b" then
  753. return dbg_delb(file, line)
  754. elseif ch == "!" then
  755. return dbg_delpin(file, line)
  756. end
  757. return nil, "unknown del function: "..ch
  758. end
  759. dbg_args[dbg_del] = "cSN"
  760. local function dbg_local_basedir(dir)
  761. basedir = dir
  762. return "local basedir is now "..basedir
  763. end
  764. dbg_args[dbg_local_basedir] = "s"
  765. local function dbg_basedir(dir)
  766. local res, err, _ = dbg_local_basedir(dir)
  767. if not err then
  768. res, _, err = mdb.handle("basedir " .. basedir, client)
  769. if not err then
  770. res = "basedir is now " .. basedir
  771. end
  772. end
  773. return res, err
  774. end
  775. dbg_args[dbg_basedir] = "s"
  776. local function dbg_gotoline(file, line)
  777. if not file and not line then
  778. return nil, "file or line number or both expected"
  779. end
  780. if file then
  781. local src, err = get_file(file)
  782. if not src then return nil, err end
  783. current_file = file
  784. current_src = src
  785. if not line then line = 1 end
  786. end
  787. if line < 1 or line > #current_src.src then
  788. return nil, "line number out of range"
  789. end
  790. selected_line = line
  791. end
  792. dbg_args[dbg_gotoline] = "SN"
  793. local function dbg_searchstr(str)
  794. local src = current_src.src
  795. local first = 1
  796. if not str and last_search then
  797. str = last_search
  798. first = last_match + 1
  799. elseif not str and not last_search then
  800. return
  801. end
  802. if str then
  803. last_search = str
  804. for line=first, #src do
  805. if string.find(src[line], str, 1, true) then
  806. selected_line = line
  807. last_match = line
  808. return "searching for " .. str
  809. end
  810. end
  811. end
  812. last_match = 0
  813. return "no match, next / wraps"
  814. end
  815. dbg_args[dbg_searchstr] = "S"
  816. local function dbg_toggle_pinned()
  817. display_pinned = not display_pinned
  818. return (display_pinned and "" or "don't ") .. "display pinned evals"
  819. end
  820. local function dbg_writesetup(what, file)
  821. local breaks, pins = true, true
  822. local res = {}
  823. if what == 'b' then
  824. pins = false
  825. elseif what == '!' then
  826. breaks = false
  827. end
  828. if breaks then
  829. for n, s in pairs(sources) do
  830. for i, c in pairs(s.breakpts) do
  831. if c == true then
  832. res[#res+1] = string.format('b %q %d', n, i)
  833. else
  834. res[#res+1] = string.format('c %q %d %s', n, i, c)
  835. end
  836. end
  837. end
  838. end
  839. if pins then
  840. for _, pin in ipairs(pinned_evals) do
  841. res[#res+1] = string.format("! %s", pin[1])
  842. end
  843. end
  844. if file then
  845. local f = io.open(file, "w")
  846. if not f then
  847. return nil, "could not open file '"..file.."' for writing"
  848. end
  849. for _, l in ipairs(res) do
  850. f:write(l, "\n")
  851. end
  852. f:close()
  853. return "wrote setup to file '"..file.."'"
  854. else
  855. for _, l in ipairs(res) do
  856. output(l)
  857. end
  858. end
  859. end
  860. dbg_args[dbg_writesetup] = "CS"
  861. local function dbg_return()
  862. update_where()
  863. end
  864. local dbg_imm = {
  865. ['h'] = dbg_help,
  866. ['s'] = dbg_step,
  867. ['n'] = dbg_over,
  868. ['r'] = dbg_run,
  869. ['o'] = dbg_out,
  870. ['P'] = dbg_toggle_pinned,
  871. ['D'] = dbg_done,
  872. ['.'] = dbg_return,
  873. }
  874. local dbg_cmdl = {
  875. ['t'] = dbg_trace,
  876. ['b'] = dbg_setb,
  877. ['c'] = dbg_setbcond,
  878. ['d'] = dbg_del,
  879. ['='] = dbg_eval,
  880. ['!'] = dbg_pin_eval,
  881. ['B'] = dbg_basedir,
  882. ['L'] = dbg_local_basedir,
  883. ['G'] = dbg_gotoline,
  884. ['/'] = dbg_searchstr,
  885. ['W'] = dbg_writesetup,
  886. }
  887. local use_selection = {
  888. ['b'] = function() return "b " .. tostring(selected_line) end,
  889. ['c'] = function() return "c " .. tostring(selected_line) .. " " end,
  890. ['d'] = function() if current_src.breakpts[selected_line] then return "db " .. tostring(selected_line) else return "d" end end,
  891. }
  892. local use_current = {
  893. ['d'] = function() if current_src.breakpts[current_line] then return "db " .. tostring(current_line) else return "d" end end,
  894. }
  895. -- argspec:
  896. -- nil any argument list
  897. -- * any argument list with at least one argument. May also be the
  898. -- last char in a spec when 1 or more args should follow.
  899. -- c,C char or optional char
  900. -- n,N number or optional number
  901. -- s,S string or optional string. A string may either be enclosed by
  902. -- quotes (' or "), or a word with no space characters in it.
  903. local function dbg_verify_args(argspec, args)
  904. local function invarg(n, t)
  905. return nil, "invalid argument #"..n..": " ..t.." expected"
  906. end
  907. if not argspec or (argspec == '*' and #args > 0) then
  908. return args
  909. end
  910. local varg = {}
  911. local nargs = 0
  912. local k = 1
  913. for i=1, #argspec do
  914. local v = args[k]
  915. local t = type(v)
  916. local spec = string.sub(argspec, i, i)
  917. local lspec = string.lower(spec)
  918. if lspec == 'n' then
  919. if t == "number" then
  920. varg[i] = v
  921. k = k + 1
  922. elseif spec == 'N' then
  923. varg[i] = nil
  924. else
  925. return invarg(k, "number")
  926. end
  927. nargs = nargs + 1
  928. elseif lspec == 'c' then
  929. if t == "string" and #v == 1 then
  930. varg[i] = v
  931. k = k + 1
  932. elseif spec == 'C' then
  933. varg[i] = nil
  934. else
  935. return invarg(k, "char")
  936. end
  937. nargs = nargs + 1
  938. elseif lspec == 's' then
  939. if t == "string" then
  940. varg[i] = v
  941. k = k + 1
  942. elseif spec == 'S' then
  943. varg[i] = nil
  944. else
  945. return invarg(k, "string")
  946. end
  947. nargs = nargs + 1
  948. elseif spec == '*' then
  949. if i ~= #argspec then
  950. return nil, "(invalid argspec for function: '"..tostring(argspec).."')"
  951. end
  952. for l = k, #args do
  953. varg[i - k + l] = args[l]
  954. nargs = nargs + 1
  955. end
  956. else
  957. return nil, "(invalid argspec for function: '"..tostring(argspec).."')"
  958. end
  959. end
  960. return varg, nargs
  961. end
  962. local function dbg_exec(cmdl)
  963. local cmd = string.sub(cmdl, 1, 1)
  964. if cmd == '' then return nil end
  965. local args = {}
  966. local s, e = string.find(cmdl, "^%s*(%S)", 2)
  967. local quote = false
  968. while s do
  969. local ch = string.sub(cmdl, e, e)
  970. if ch == "`" then
  971. quote = true
  972. elseif ch == '"' or ch == "'" then
  973. local p1 = e + 1
  974. s, e = string.find(cmdl, "[^\\]"..ch, p1)
  975. if s then
  976. args[#args+1] = quote and string.sub(cmdl, p1-1, e) or string.sub(cmdl, p1, e-1)
  977. quote = false
  978. else
  979. return nil, "unfinished string argument"
  980. end
  981. else
  982. s, e = string.find(cmdl, "%S+", s)
  983. local a = string.sub(cmdl, s, e)
  984. local n = tonumber(a)
  985. if n then
  986. args[#args+1] = n
  987. else
  988. args[#args+1] = a
  989. end
  990. end
  991. s, e = string.find(cmdl, "^%s"..(quote and '*' or '+').."(%S)", e+1)
  992. end
  993. if dbg_imm[cmd] then
  994. if #args == 0 then
  995. return dbg_imm[cmd]()
  996. else
  997. return nil, "too many arguments for command "..cmd
  998. end
  999. elseif dbg_cmdl[cmd] then
  1000. local fn = dbg_cmdl[cmd]
  1001. local argspec = dbg_args[fn]
  1002. local vargs, n = dbg_verify_args(argspec, args)
  1003. if vargs then
  1004. return fn(unpack(vargs, 1, n))
  1005. else
  1006. return nil, n
  1007. end
  1008. end
  1009. return nil, "unknown command "..cmd
  1010. end
  1011. local function dbg_execfile(file)
  1012. local f = io.open(file, "r")
  1013. if not f then
  1014. return nil, "could not open file '"..file.."' for input."
  1015. end
  1016. for l in f:lines() do
  1017. local res, err = dbg_exec(l)
  1018. if res then
  1019. output(res)
  1020. elseif err then
  1021. f:close()
  1022. return nil, err
  1023. end
  1024. end
  1025. f:close()
  1026. return "execution of commands in file '"..file.."' finished"
  1027. end
  1028. local function dbg_loop()
  1029. local w, h, evt, cmdl
  1030. local quit = false
  1031. update_where()
  1032. local evt
  1033. repeat
  1034. w, h = ui.size()
  1035. display()
  1036. evt = ui.pollevent()
  1037. if evt and evt.char ~= "" then
  1038. local ch = evt.char or ''
  1039. cmdl = nil
  1040. if dbg_imm[ch] then
  1041. cmdl = ch
  1042. elseif dbg_cmdl[ch] then
  1043. local prefill = ch
  1044. if selected_line and use_selection[ch] then
  1045. prefill = use_selection[ch]()
  1046. elseif use_current[ch] then
  1047. prefill = use_current[ch]()
  1048. end
  1049. ui.setcell(1, h, ">")
  1050. cmdl = ui.input(2, h, w, prefill)
  1051. if cmdl == "" then cmdl = nil end
  1052. end
  1053. if cmdl then
  1054. output(cmdl)
  1055. result, err = dbg_exec(cmdl)
  1056. elseif ch == "q" then
  1057. selected_line = nil
  1058. quit = ui.ask("Really quit?") == 1
  1059. end
  1060. if err then
  1061. output_error(err)
  1062. elseif result then
  1063. local resa = ui.formatwidth(result, w - 4)
  1064. output("->", resa[1])
  1065. if #resa > 1 then
  1066. for i = 2, #resa do
  1067. output(" >", resa[i])
  1068. end
  1069. end
  1070. end
  1071. result, err = nil, nil
  1072. else
  1073. local key = evt.key
  1074. if key == ui.key.ARROW_UP or key == ui.key.ARROW_DOWN or
  1075. key == ui.key.ARROW_LEFT or key == ui.key.ARROW_RIGHT or
  1076. key == ui.key.PGUP or key == ui.key.PGDN or
  1077. key == ui.key.HOME or key == ui.key.END then
  1078. select_cmd = key
  1079. end
  1080. end
  1081. until quit
  1082. return quit
  1083. end
  1084. ---------- main --------------------------------------------------------
  1085. local ok, val
  1086. if tonumber(mdb._VERSION) < 0.63 then
  1087. ok = nil
  1088. val = "debug.lua needs at least mobdebug version 0.63"
  1089. else
  1090. ok, val = pcall(function()
  1091. ui.outputmode(ui.output.COL256)
  1092. local ok, err = configure()
  1093. if not ok then
  1094. error(err)
  1095. end
  1096. local w, h = ui.size()
  1097. local quit = false
  1098. local opts, err = get_opts("p:d:x:l:h?", arg)
  1099. if not opts or opts.h or opts['?'] then
  1100. local ret = err and err .. "\n" or ""
  1101. return ret .. "usage: "..arg[0] .. " [-p port] [-d dir] [-x file] [-l file]"
  1102. end
  1103. if opts.p then
  1104. port = tonumber(opts.p)
  1105. if not port then error("argument to -p needs to be a port number") end
  1106. end
  1107. if opts.d then
  1108. basedir = opts.d
  1109. end
  1110. if opts.l then
  1111. cmd_outlog = io.open(opts.l, "w")
  1112. if not cmd_outlog then error("can't write output log '"..opts.l.."'") end
  1113. end
  1114. while not quit do
  1115. ui.clear(config.fg, config.bg)
  1116. local ok, err = startup(port)
  1117. if ok == nil then
  1118. error(err)
  1119. elseif ok == false then
  1120. return
  1121. end
  1122. local result, err
  1123. local first = 1
  1124. local cmdl
  1125. if opts.x then
  1126. local res, err = dbg_execfile(opts.x)
  1127. if res then
  1128. output(res)
  1129. else
  1130. output_error(err)
  1131. end
  1132. end
  1133. local loop = coroutine.create(dbg_loop)
  1134. local ok, val = coroutine.resume(loop)
  1135. if client then
  1136. client:close()
  1137. client = nil
  1138. end
  1139. if ok and val == _os_exit then
  1140. local w, h = ui.size()
  1141. ui.attributes(config.done_fg + ui.format.BOLD, config.bg)
  1142. ui.drawfield(1, h, "Debugged program terminated, press q to quit or any key to restart.", w)
  1143. ui.hidecursor()
  1144. ui.present()
  1145. quit = ui.waitkeypress() == 'q'
  1146. output("Debugged program terminated" .. (quit and "" or ", restarting"))
  1147. elseif ok and val == true then
  1148. quit = true
  1149. else
  1150. output_error(val)
  1151. output(debug.traceback(loop))
  1152. return nil
  1153. end
  1154. end
  1155. return
  1156. end)
  1157. end
  1158. ui.shutdown()
  1159. if outlog then outlog:close() end
  1160. if client then client:close() end
  1161. if not ok then
  1162. _G_print("Error: "..tostring(val))
  1163. elseif val then
  1164. _G_print(val)
  1165. else
  1166. _G_print("Bye.")
  1167. end