Skip to content

Commit e716cd0

Browse files
committed
feat(pmdb): add a gdb-like JS debugger for pmjs
Can be enabled by `pmjs --inspect`
1 parent 640485e commit e716cd0

File tree

2 files changed

+115
-1
lines changed

2 files changed

+115
-1
lines changed

python/pythonmonkey/cli/pmjs.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import sys, os, signal, getopt
77
import readline
88
import pythonmonkey as pm
9+
from pythonmonkey.lib.pmdb import pmdbEnable
10+
911
globalThis = pm.eval("globalThis")
1012
evalOpts = { 'filename': __file__, 'fromPythonFrame': True, 'strict': False } # type: pm.EvalOptions
1113

@@ -287,6 +289,7 @@ def usage():
287289
-r, --require... module to preload (option can be repeated)
288290
-v, --version print PythonMonkey version
289291
--use-strict evaluate -e, -p, and REPL code in strict mode
292+
--inspect enable pmdb, a gdb-like JavaScript debugger interface
290293
291294
Environment variables:
292295
TZ specify the timezone configuration
@@ -320,7 +323,7 @@ def main():
320323
global requirePath
321324

322325
try:
323-
opts, args = getopt.getopt(sys.argv[1:], "hie:p:r:v", ["help", "eval=", "print=", "require=", "version", "use-strict"])
326+
opts, args = getopt.getopt(sys.argv[1:], "hie:p:r:v", ["help", "eval=", "print=", "require=", "version", "use-strict", "inspect"])
324327
except getopt.GetoptError as err:
325328
# print help information and exit:
326329
print(err) # will print something like "option -a not recognized"
@@ -347,6 +350,8 @@ def main():
347350
enterRepl = False
348351
elif o in ("-r", "--require"):
349352
globalThis.require(a)
353+
elif o in ("--inspect"):
354+
pmdbEnable(pm.eval("debuggerGlobal"))
350355
else:
351356
assert False, "unhandled option"
352357

python/pythonmonkey/lib/pmdb.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# @file pmdb - A gdb-like JavaScript debugger interface for pmjs
2+
# Enabled by `pmjs --inspect`
3+
# @author Tom Tang <[email protected]>
4+
# @date July 2023
5+
6+
def debuggerInput(prompt: str):
7+
try:
8+
return input(prompt) # blocking
9+
except KeyboardInterrupt:
10+
print("\b\bQuit") # to match the behaviour of gdb
11+
return ""
12+
except Exception as e:
13+
print(e)
14+
return ""
15+
16+
def pmdbEnable(debuggerGlobalObject):
17+
debuggerGlobalObject.eval("""(debuggerInput, _pythonPrint) => {
18+
const dbg = new Debugger()
19+
const mainDebuggee = dbg.addDebuggee(mainGlobal)
20+
dbg.uncaughtExceptionHook = (e) => {
21+
_pythonPrint(e)
22+
}
23+
24+
function makeDebuggeeValue (val) {
25+
if (val instanceof Debugger.Object) {
26+
return dbg.adoptDebuggeeValue(val)
27+
} else {
28+
// See https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Object.html#makedebuggeevalue-value
29+
return mainDebuggee.makeDebuggeeValue(val)
30+
}
31+
}
32+
33+
function print (...args) {
34+
const logger = makeDebuggeeValue(mainGlobal.console.log)
35+
logger.apply(logger, args.map(makeDebuggeeValue))
36+
}
37+
38+
function printErr (...args) {
39+
const logger = makeDebuggeeValue(mainGlobal.console.error)
40+
logger.apply(logger, args.map(makeDebuggeeValue))
41+
}
42+
43+
function getCommandInputs () {
44+
const input = debuggerInput("(pmdb) > ") // blocking
45+
const [_, command, rest] = input.match(/\\s*(\\w+)?(?:\\s+(.*))?/)
46+
return { command, rest }
47+
}
48+
49+
function enterDebuggerLoop (frame) {
50+
const metadata = frame.script.getOffsetMetadata(frame.offset)
51+
if (!metadata.isBreakpoint) {
52+
// This bytecode offset does not qualify as a breakpoint, skipping
53+
return
54+
}
55+
56+
blockingLoop: while (true) {
57+
const { command, rest } = getCommandInputs() // blocking
58+
switch (command) {
59+
case "c":
60+
case "cont":
61+
// Continue execution until next breakpoint or `debugger` statement
62+
frame.onStep = undefined // clear step next handler
63+
break blockingLoop;
64+
case "n":
65+
case "next":
66+
// Step next
67+
frame.onStep = function () { enterDebuggerLoop(this) } // add handler
68+
break blockingLoop;
69+
case "bt":
70+
case "backtrace":
71+
// Print backtrace of current execution frame
72+
// FIXME: we currently implement this using Error.stack
73+
print(frame.eval("(new Error).stack.split('\\\\n').slice(1).join('\\\\n')").return)
74+
continue blockingLoop;
75+
case "l":
76+
case "line": {
77+
// Print current line
78+
const src = frame.script.source.text
79+
const line = src.split('\\n').slice(metadata.lineNumber-1, metadata.lineNumber).join('\\n')
80+
print(line)
81+
continue blockingLoop;
82+
}
83+
case "p":
84+
case "exec":
85+
case "print": {
86+
// Execute an expression in debugging script's context and print its value
87+
const result = frame.eval(rest)
88+
if (result.throw) printErr(result.throw) // on error
89+
else print(result.return) // on success
90+
continue blockingLoop;
91+
}
92+
case "q":
93+
case "quit":
94+
case "kill":
95+
// Force exit the program
96+
mainGlobal.python.exit(127)
97+
break blockingLoop;
98+
case "":
99+
case undefined:
100+
// no-op
101+
continue blockingLoop;
102+
default:
103+
print(`Undefined command: "${command}"`)
104+
}
105+
}
106+
}
107+
108+
dbg.onDebuggerStatement = (frame) => enterDebuggerLoop(frame)
109+
}""")(debuggerInput, print)

0 commit comments

Comments
 (0)