Skip to content

Commit 64b9da2

Browse files
authored
Merge pull request #149 from Distributive-Network/Xmader/feat/js-debugger
Feat: pmdb, a gdb-like JavaScript debugger interface
2 parents 05309d4 + feec2c3 commit 64b9da2

File tree

5 files changed

+219
-5
lines changed

5 files changed

+219
-5
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,9 +350,34 @@ exports['today'] = date.today()
350350
## CommonJS (require)
351351
If you are having trouble with the CommonJS require function, set environment variable `DEBUG='ctx-module*'` and you can see the filenames it tries to laod.
352352

353+
## pmdb
354+
355+
PythonMonkey has a built-in gdb-like JavaScript command-line debugger called **pmdb**, which would be automatically triggered on `debugger;` statements and uncaught exceptions.
356+
357+
To enable **pmdb**, simply call `from pythonmonkey.lib import pmdb; pmdb.enable()` before doing anything on PythonMonkey.
358+
359+
```py
360+
import pythonmonkey as pm
361+
from pythonmonkey.lib import pmdb
362+
363+
pmdb.enable()
364+
365+
pm.eval("...")
366+
```
367+
368+
Run `help` command in **pmdb** to see available commands.
369+
370+
```console
371+
(pmdb) > help
372+
List of commands:
373+
...
374+
...
375+
```
376+
353377
## pmjs
354378
- there is a `.help` menu in the REPL
355379
- there is a `--help` command-line option
380+
- the `--inspect` option enables **pmdb**, a gdb-like JavaScript command-line debugger
356381
- the `-r` option can be used to load a module before your program or the REPL runs
357382
- the `-e` option can be used evaluate code -- e.g. define global variables -- before your program or the REPL runs
358383
- The REPL can evaluate Python expressions, storing them in variables named `$1`, `$2`, etc.

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 import pmdb
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", "interactive", "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+
pmdb.enable()
350355
else:
351356
assert False, "unhandled option"
352357

python/pythonmonkey/lib/pmdb.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# @file pmdb - A gdb-like JavaScript debugger interface
2+
# @author Tom Tang <[email protected]>
3+
# @date July 2023
4+
5+
import pythonmonkey as pm
6+
7+
def debuggerInput(prompt: str):
8+
try:
9+
return input(prompt) # blocking
10+
except KeyboardInterrupt:
11+
print("\b\bQuit") # to match the behaviour of gdb
12+
return ""
13+
except Exception as e:
14+
print(e)
15+
return ""
16+
17+
def enable(debuggerGlobalObject = pm.eval("debuggerGlobal")):
18+
if debuggerGlobalObject._pmdbEnabled:
19+
return # already enabled, skipping
20+
21+
debuggerGlobalObject._pmdbEnabled = True
22+
debuggerGlobalObject.eval("""(debuggerInput, _pythonPrint, _pythonExit) => {
23+
const dbg = new Debugger()
24+
const mainDebuggee = dbg.addDebuggee(mainGlobal)
25+
dbg.uncaughtExceptionHook = (e) => {
26+
_pythonPrint(e)
27+
}
28+
29+
function makeDebuggeeValue (val) {
30+
if (val instanceof Debugger.Object) {
31+
return dbg.adoptDebuggeeValue(val)
32+
} else {
33+
// See https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Object.html#makedebuggeevalue-value
34+
return mainDebuggee.makeDebuggeeValue(val)
35+
}
36+
}
37+
38+
function print (...args) {
39+
const logger = makeDebuggeeValue(mainGlobal.console.log)
40+
logger.apply(logger, args.map(makeDebuggeeValue))
41+
}
42+
43+
function printErr (...args) {
44+
const logger = makeDebuggeeValue(mainGlobal.console.error)
45+
logger.apply(logger, args.map(makeDebuggeeValue))
46+
}
47+
48+
function printSource (frame, location) {
49+
const src = frame.script.source.text
50+
const line = src.split('\\n').slice(location.lineNumber-1, location.lineNumber).join('\\n')
51+
print(line)
52+
print(" ".repeat(location.columnNumber) + "^") // indicate column position
53+
}
54+
55+
function getCommandInputs () {
56+
const input = debuggerInput("(pmdb) > ") // blocking
57+
const [_, command, rest] = input.match(/\\s*(\\w+)?(?:\\s+(.*))?/)
58+
return { command, rest }
59+
}
60+
61+
function enterDebuggerLoop (frame, checkIsBreakpoint = false) {
62+
const metadata = frame.script.getOffsetMetadata(frame.offset)
63+
if (checkIsBreakpoint && !metadata.isBreakpoint) {
64+
// This bytecode offset does not qualify as a breakpoint, skipping
65+
return
66+
}
67+
68+
blockingLoop: while (true) {
69+
const { command, rest } = getCommandInputs() // blocking
70+
switch (command) {
71+
case "b":
72+
case "break": {
73+
// Set breakpoint on specific line number
74+
const lineNum = Number(rest)
75+
if (!lineNum) {
76+
print(`"break <lineNumber>" command requires a valid line number argument.`)
77+
continue blockingLoop;
78+
}
79+
80+
// find the bytecode offset for possible breakpoint location
81+
const bp = frame.script.getPossibleBreakpoints({ line: lineNum })[0]
82+
if (!bp) {
83+
print(`No possible breakpoint location found on line ${lineNum}`)
84+
continue blockingLoop;
85+
}
86+
87+
// add handler
88+
frame.script.setBreakpoint(bp.offset, (frame) => enterDebuggerLoop(frame))
89+
90+
// print breakpoint info
91+
print(`Breakpoint set on line ${bp.lineNumber} column ${bp.columnNumber+1} in "${frame.script.url}" :`)
92+
printSource(frame, bp)
93+
94+
continue blockingLoop;
95+
}
96+
case "c":
97+
case "cont":
98+
// Continue execution until next breakpoint or `debugger` statement
99+
frame.onStep = undefined // clear step next handler
100+
break blockingLoop;
101+
case "n":
102+
case "next":
103+
// Step next
104+
frame.onStep = function () { enterDebuggerLoop(this, /*checkIsBreakpoint*/ true) } // add handler
105+
break blockingLoop;
106+
case "bt":
107+
case "backtrace":
108+
// Print backtrace of current execution frame
109+
// FIXME: we currently implement this using Error.stack
110+
print(frame.eval("(new Error).stack.split('\\\\n').slice(1).join('\\\\n')").return)
111+
continue blockingLoop;
112+
case "l":
113+
case "line": {
114+
// Print current line
115+
printSource(frame, metadata)
116+
continue blockingLoop;
117+
}
118+
case "p":
119+
case "exec":
120+
case "print": {
121+
// Execute an expression in debugging script's context and print its value
122+
if (!rest) {
123+
print(`"print <expr>" command requires an argument.`)
124+
continue blockingLoop;
125+
}
126+
const result = frame.eval(rest)
127+
if (result.throw) printErr(result.throw) // on error
128+
else print(result.return) // on success
129+
continue blockingLoop;
130+
}
131+
case "q":
132+
case "quit":
133+
case "kill":
134+
// Force exit the program
135+
_pythonExit(127)
136+
break blockingLoop;
137+
case "h":
138+
case "help":
139+
// Print help message
140+
// XXX: keep this in sync with the actual implementation
141+
print([
142+
"List of commands:",
143+
"• b <lineNumber>/break <lineNumber>: Set breakpoint on specific line",
144+
"• c/cont: Continue execution until next breakpoint or debugger statement",
145+
"• n/next: Step next",
146+
"• bt/backtrace: Print backtrace of current execution frame",
147+
"• l/line: Print current line",
148+
"• p <expr>/print <expr>/exec <expr>: Execute an expression in debugging script's context and print its value",
149+
"• q/quit/kill: Force exit the program",
150+
"• h/help: Print help message",
151+
].join("\\n"))
152+
continue blockingLoop;
153+
case "":
154+
case undefined:
155+
// no-op
156+
continue blockingLoop;
157+
default:
158+
print(`Undefined command: "${command}". Try "help".`)
159+
}
160+
}
161+
}
162+
163+
// Enter debugger on uncaught exceptions
164+
dbg.onExceptionUnwind = (frame, err) => {
165+
const isUncaught = !frame.script.isInCatchScope(frame.offset) // not in a catch block
166+
&& frame.older == null // this is the outermost frame
167+
if (isUncaught) {
168+
printErr("Uncaught exception:")
169+
printErr(err)
170+
enterDebuggerLoop(frame)
171+
}
172+
}
173+
174+
// Enter debugger on `debugger;` statement
175+
dbg.onDebuggerStatement = (frame) => enterDebuggerLoop(frame)
176+
177+
}""")(debuggerInput, print, lambda status: exit(int(status)))

src/JobQueue.cc

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ bool JobQueue::enqueuePromiseJob(JSContext *cx,
3939
}
4040

4141
void JobQueue::runJobs(JSContext *cx) {
42-
// TODO (Tom Tang):
43-
throw std::logic_error("JobQueue::runJobs is not implemented.");
42+
return;
4443
}
4544

4645
// is empty
@@ -50,8 +49,8 @@ bool JobQueue::empty() const {
5049
}
5150

5251
js::UniquePtr<JS::JobQueue::SavedJobQueue> JobQueue::saveJobQueue(JSContext *cx) {
53-
// TODO (Tom Tang): implement this method way later
54-
throw std::logic_error("JobQueue::saveJobQueue is not implemented\n");
52+
auto saved = js::MakeUnique<JS::JobQueue::SavedJobQueue>();
53+
return saved;
5554
}
5655

5756
bool JobQueue::init(JSContext *cx) {

src/modules/pythonmonkey/pythonmonkey.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,13 @@ PyMODINIT_FUNC PyInit_pythonmonkey(void)
431431
return NULL;
432432
}
433433

434+
JS::RootedObject debuggerGlobal(GLOBAL_CX, JS_NewGlobalObject(GLOBAL_CX, &globalClass, nullptr, JS::FireOnNewGlobalHook, options));
435+
{
436+
JSAutoRealm r(GLOBAL_CX, debuggerGlobal);
437+
JS_DefineProperty(GLOBAL_CX, debuggerGlobal, "mainGlobal", *global, JSPROP_READONLY);
438+
JS_DefineDebuggerObject(GLOBAL_CX, debuggerGlobal);
439+
}
440+
434441
autoRealm = new JSAutoRealm(GLOBAL_CX, *global);
435442

436443
if (!JS_DefineFunctions(GLOBAL_CX, *global, jsGlobalFunctions)) {
@@ -439,6 +446,7 @@ PyMODINIT_FUNC PyInit_pythonmonkey(void)
439446
}
440447

441448
JS_SetGCCallback(GLOBAL_CX, handleSharedPythonMonkeyMemory, NULL);
449+
JS_DefineProperty(GLOBAL_CX, *global, "debuggerGlobal", debuggerGlobal, JSPROP_READONLY);
442450

443451
// XXX: SpiderMonkey bug???
444452
// In https://hg.mozilla.org/releases/mozilla-esr102/file/3b574e1/js/src/jit/CacheIR.cpp#l317, trying to use the callback returned by `js::GetDOMProxyShadowsCheck()` even it's unset (nullptr)

0 commit comments

Comments
 (0)