Commit ee89551e authored by Philip Chimento's avatar Philip Chimento

debugger: GJS Debugger

This adds a simple debugger, adapted from the "jorendb" program in the
SpiderMonkey source. It has basic stepping, breaking, and printing
commands, that work like GDB. Activate it by running the GJS console
interpreter with the -d or --debugger flag _before_ the name of the JS
program on the command line.

To integrate it into programs that embed the GJS interpreter, call
gjs_context_setup_debugger_console() before executing the JS program.

It will print when Promises are launched and resolved, although it's not
yet possible to break at those points.

Closes: #110
parent 8ae00b8e
......@@ -8,24 +8,30 @@ gjsinsttestdir = $(pkglibexecdir)/installed-tests
installedtestmetadir = $(datadir)/installed-tests/gjs
jstestsdir = $(gjsinsttestdir)/js
jsscripttestsdir = $(gjsinsttestdir)/scripts
debuggertestsdir = $(gjsinsttestdir)/debugger
gjsinsttest_PROGRAMS =
gjsinsttest_DATA =
gjsinsttest_SCRIPTS =
installedtestmeta_DATA =
jstests_DATA =
jsscripttests_DATA =
debuggertests_DATA =
pkglib_LTLIBRARIES =
if BUILDOPT_INSTALL_TESTS
gjsinsttest_PROGRAMS += minijasmine
gjsinsttest_SCRIPTS += installed-tests/debugger-test.sh
gjsinsttest_DATA += $(TEST_INTROSPECTION_TYPELIBS)
installedtestmeta_DATA += \
$(jasmine_tests:.js=.test) \
$(simple_tests:%=%.test) \
installedtestmeta_DATA += \
$(jasmine_tests:.js=.test) \
$(simple_tests:%=%.test) \
$(debugger_tests:.debugger=.test) \
$(NULL)
jstests_DATA += $(jasmine_tests)
jsscripttests_DATA += $(simple_tests)
debuggertests_DATA += $(debugger_tests)
pkglib_LTLIBRARIES += libregress.la libwarnlib.la libgimarshallingtests.la
%.test: %.js installed-tests/minijasmine.test.in Makefile
......@@ -35,6 +41,13 @@ pkglib_LTLIBRARIES += libregress.la libwarnlib.la libgimarshallingtests.la
< $(srcdir)/installed-tests/minijasmine.test.in > $@.tmp && \
mv $@.tmp $@
%.test: %.debugger installed-tests/debugger.test.in Makefile
$(AM_V_GEN)$(MKDIR_P) $(@D) && \
$(SED) -e s,@pkglibexecdir\@,$(pkglibexecdir),g \
-e s,@name\@,$(notdir $<), \
< $(srcdir)/installed-tests/debugger.test.in > $@.tmp && \
mv $@.tmp $@
%.test: % installed-tests/script.test.in Makefile
$(AM_V_GEN)$(MKDIR_P) $(@D) && \
$(SED) -e s,@pkglibexecdir\@,$(pkglibexecdir), \
......
......@@ -296,13 +296,39 @@ simple_tests = \
$(NULL)
EXTRA_DIST += $(simple_tests)
debugger_tests = \
installed-tests/debugger/backtrace.debugger \
installed-tests/debugger/breakpoint.debugger \
installed-tests/debugger/continue.debugger \
installed-tests/debugger/delete.debugger \
installed-tests/debugger/detach.debugger \
installed-tests/debugger/down-up.debugger \
installed-tests/debugger/finish.debugger \
installed-tests/debugger/frame.debugger \
installed-tests/debugger/keys.debugger \
installed-tests/debugger/next.debugger \
installed-tests/debugger/print.debugger \
installed-tests/debugger/quit.debugger \
installed-tests/debugger/return.debugger \
installed-tests/debugger/set.debugger \
installed-tests/debugger/step.debugger \
installed-tests/debugger/throw.debugger \
installed-tests/debugger/until.debugger \
$(NULL)
EXTRA_DIST += \
$(debugger_tests) \
$(debugger_tests:%=%.js) \
$(debugger_tests:%=%.output) \
$(NULL)
TESTS = \
gjs-tests.gtester \
$(simple_tests) \
$(jasmine_tests) \
$(debugger_tests) \
$(NULL)
TEST_EXTENSIONS = .gtester .sh .js
TEST_EXTENSIONS = .gtester .sh .js .debugger
LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/tap-driver.sh
......@@ -314,6 +340,9 @@ SH_LOG_DRIVER = $(LOG_DRIVER)
JS_LOG_DRIVER = $(LOG_DRIVER)
JS_LOG_COMPILER = $$LOG_COMPILER $$LOG_FLAGS $(top_builddir)/minijasmine
DEBUGGER_LOG_DRIVER = $(LOG_DRIVER)
DEBUGGER_LOG_COMPILER = $(top_srcdir)/installed-tests/debugger-test.sh
CODE_COVERAGE_IGNORE_PATTERN = */{include,mfbt,gjs/test}/*
CODE_COVERAGE_GENHTML_OPTIONS = \
$(CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT) \
......
......@@ -296,6 +296,16 @@ AX_APPEND_COMPILE_FLAGS([$lt_prog_compiler_pic], [CXXFLAGS])
AX_APPEND_COMPILE_FLAGS([$lt_prog_compiler_pic], [CFLAGS])
AX_APPEND_LINK_FLAGS([$lt_prog_compiler_pic])
dnl If SpiderMonkey was compiled with this configure option, then GJS needs to
dnl be compiled with it as well, because we inherit from a SpiderMonkey class in
dnl context.cpp. See build/autoconf/compiler-opts.m4 in mozjs52.
AC_ARG_ENABLE([cpp-rtti],
[AS_HELP_STRING([--enable-cpp-rtti],
[needs to match SpiderMonkey's config option @<:@default=off@:>@])])
AS_IF([test "x$enable_cpp_rtti" != "xyes"],
[AX_APPEND_COMPILE_FLAGS([-fno-rtti])],
[AX_APPEND_COMPILE_FLAGS([-GR-])])
AC_ARG_WITH([xvfb-tests],
[AS_HELP_STRING([--with-xvfb-tests],
[Run all tests under an XVFB server @<:@default=no@:>@])])
......
......@@ -58,6 +58,7 @@ gjs_srcs = \
gjs/context.cpp \
gjs/context-private.h \
gjs/coverage.cpp \
gjs/debugger.cpp \
gjs/engine.cpp \
gjs/engine.h \
gjs/global.cpp \
......
......@@ -37,11 +37,13 @@ static char *profile_output_path = nullptr;
static char *command = NULL;
static gboolean print_version = false;
static gboolean print_js_version = false;
static gboolean debugging = false;
static bool enable_profiler = false;
static gboolean parse_profile_arg(const char *, const char *, void *, GError **);
/* Keep in sync with entries in check_script_args_for_stray_gjs_args() */
// clang-format off
static GOptionEntry entries[] = {
{ "version", 0, 0, G_OPTION_ARG_NONE, &print_version, "Print GJS version and exit" },
{ "jsversion", 0, 0, G_OPTION_ARG_NONE, &print_js_version,
......@@ -54,8 +56,10 @@ static GOptionEntry entries[] = {
G_OPTION_ARG_CALLBACK, reinterpret_cast<void *>(&parse_profile_arg),
"Enable the profiler and write output to FILE (default: gjs-$PID.syscap)",
"FILE" },
{ "debugger", 'd', 0, G_OPTION_ARG_NONE, &debugging, "Start in debug mode" },
{ NULL }
};
// clang-format on
static char **
strndupv(int n,
......@@ -248,6 +252,7 @@ main(int argc, char **argv)
command = NULL;
print_version = false;
print_js_version = false;
debugging = false;
g_option_context_set_ignore_unknown_options(context, false);
g_option_context_set_help_enabled(context, true);
if (!g_option_context_parse_strv(context, &gjs_argv, &error))
......@@ -341,6 +346,19 @@ main(int argc, char **argv)
goto out;
}
/* If we're debugging, set up the debugger and prepend a debugger statement
* to the script we're going to evaluate. The debugger statement will break
* and cause a debugger prompt to appear.
* TODO: This is not great, as it messes up column offsets on the first
* line of all scripts. It would be better to hook into a debugger event
* and interrupt on the first frame. */
if (debugging) {
gjs_context_setup_debugger_console(js_context);
char* old_script = script;
script = g_strconcat("debugger;", old_script, nullptr);
g_free(old_script);
}
/* evaluate the script */
if (!gjs_context_eval(js_context, script, len,
filename, &code, &error)) {
......@@ -364,5 +382,8 @@ main(int argc, char **argv)
g_object_unref(coverage);
g_object_unref(js_context);
g_free(script);
if (debugging)
g_print("Program exited with code %d\n", code);
exit(code);
}
......@@ -71,6 +71,26 @@ static void gjs_context_set_property (GObject *object,
const GValue *value,
GParamSpec *pspec);
/* Environment preparer needed for debugger, taken from SpiderMonkey's
* JS shell */
struct GjsEnvironmentPreparer final : public js::ScriptEnvironmentPreparer {
JSContext* m_cx;
explicit GjsEnvironmentPreparer(JSContext* cx) : m_cx(cx) {
js::SetScriptEnvironmentPreparer(m_cx, this);
}
void invoke(JS::HandleObject scope, Closure& closure) override;
};
void GjsEnvironmentPreparer::invoke(JS::HandleObject scope, Closure& closure) {
g_assert(!JS_IsExceptionPending(m_cx));
JSAutoCompartment ac(m_cx, scope);
if (!closure(m_cx))
gjs_log_exception(m_cx);
}
using JobQueue = JS::GCVector<JSObject *, 0, js::SystemAllocPolicy>;
struct _GjsContext {
......@@ -104,6 +124,8 @@ struct _GjsContext {
GjsProfiler *profiler;
bool should_profile : 1;
bool should_listen_sigusr2 : 1;
GjsEnvironmentPreparer environment_preparer;
};
/* Keep this consistent with GjsConstString */
......@@ -432,6 +454,7 @@ gjs_context_finalize(GObject *object)
js_context->global.~Heap();
js_context->const_strings.~array();
js_context->unhandled_rejection_stacks.~unordered_map();
js_context->environment_preparer.~GjsEnvironmentPreparer();
G_OBJECT_CLASS(gjs_context_parent_class)->finalize(object);
}
......@@ -467,6 +490,7 @@ gjs_context_constructed(GObject *object)
new (&js_context->unhandled_rejection_stacks) std::unordered_map<uint64_t, GjsAutoChar>;
new (&js_context->const_strings) std::array<JS::PersistentRootedId*, GJS_STRING_LAST>;
new (&js_context->environment_preparer) GjsEnvironmentPreparer(cx);
for (i = 0; i < GJS_STRING_LAST; i++) {
js_context->const_strings[i] = new JS::PersistentRootedId(cx,
gjs_intern_string_to_id(cx, const_strings[i]));
......
......@@ -105,6 +105,9 @@ void gjs_dumpstack (void);
GJS_EXPORT
const char *gjs_get_js_version(void);
GJS_EXPORT
void gjs_context_setup_debugger_console(GjsContext* gjs);
G_END_DECLS
#endif /* __GJS_CONTEXT_H__ */
/*
* Copyright (c) 2018 Philip Chimento <philip.chimento@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*
* Authored By: Philip Chimento <philip.chimento@gmail.com>
*/
#include <unistd.h>
#include <gio/gio.h>
#include "gjs/context-private.h"
#include "gjs/global.h"
#include "gjs/jsapi-util-args.h"
#ifdef HAVE_READLINE_READLINE_H
#include <readline/history.h>
#include <readline/readline.h>
#include <stdio.h>
#endif
static bool quit(JSContext* cx, unsigned argc, JS::Value* vp) {
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
JSAutoRequest ar(cx);
int32_t exitcode;
if (!gjs_parse_call_args(cx, "quit", args, "i", "exitcode", &exitcode))
return false;
auto* gjs = static_cast<GjsContext*>(JS_GetContextPrivate(cx));
_gjs_context_exit(gjs, exitcode);
return false; // without gjs_throw() == "throw uncatchable exception"
}
static bool do_readline(JSContext* cx, unsigned argc, JS::Value* vp) {
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
JSAutoRequest ar(cx);
GjsAutoJSChar prompt;
if (!gjs_parse_call_args(cx, "readline", args, "|s", "prompt", &prompt))
return false;
GjsAutoChar line;
do {
const char* real_prompt = prompt ? prompt.get() : "db> ";
#ifdef HAVE_READLINE_READLINE_H
if (isatty(STDIN_FILENO)) {
line = readline(real_prompt);
} else {
#else
{
#endif // HAVE_READLINE_READLINE_H
char buf[256];
g_print("%s", real_prompt);
fflush(stdout);
if (!fgets(buf, sizeof buf, stdin))
buf[0] = '\0';
line.reset(g_strchomp(g_strdup(buf)));
if (!isatty(STDIN_FILENO)) {
if (feof(stdin)) {
g_print("[quit due to end of input]\n");
line.reset(g_strdup("quit"));
} else {
g_print("%s\n", line.get());
}
}
}
/* EOF, return null */
if (!line) {
args.rval().setUndefined();
return true;
}
} while (line && line[0] == '\0');
/* Add line to history and convert it to a JSString so that we can pass it
* back as the return value */
#ifdef HAVE_READLINE_READLINE_H
add_history(line);
#endif
args.rval().setString(JS_NewStringCopyZ(cx, line));
return true;
}
// clang-format off
static JSFunctionSpec debugger_funcs[] = {
JS_FS("quit", quit, 1, GJS_MODULE_PROP_FLAGS),
JS_FS("readline", do_readline, 1, GJS_MODULE_PROP_FLAGS),
JS_FS_END
};
// clang-format on
void gjs_context_setup_debugger_console(GjsContext* gjs) {
auto cx = static_cast<JSContext*>(gjs_context_get_native_context(gjs));
JSAutoRequest ar(cx);
JS::RootedObject debuggee(cx, gjs_get_import_global(cx));
JS::RootedObject debugger_compartment(cx, gjs_create_global_object(cx));
/* Enter compartment of the debugger and initialize it with the debuggee */
JSAutoCompartment compartment(cx, debugger_compartment);
JS::RootedObject debuggee_wrapper(cx, debuggee);
if (!JS_WrapObject(cx, &debuggee_wrapper)) {
gjs_log_exception(cx);
return;
}
JS::RootedValue v_wrapper(cx, JS::ObjectValue(*debuggee_wrapper));
if (!JS_SetProperty(cx, debugger_compartment, "debuggee", v_wrapper) ||
!JS_DefineFunctions(cx, debugger_compartment, debugger_funcs) ||
!gjs_define_global_properties(cx, debugger_compartment, "debugger"))
gjs_log_exception(cx);
}
#!/bin/bash
if test "$GJS_USE_UNINSTALLED_FILES" = "1"; then
gjs="$TOP_BUILDDIR/gjs-console"
else
gjs=gjs-console
fi
echo 1..1
DEBUGGER_SCRIPT="$1"
JS_SCRIPT="$1.js"
EXPECTED_OUTPUT="$1.output"
THE_DIFF=$("$gjs" -d "$JS_SCRIPT" < "$DEBUGGER_SCRIPT" | sed \
-e "s#$1#$(basename $1)#g" \
-e "s/0x[0-9a-f]\{4,16\}/0xADDR/g" \
| diff -u "$EXPECTED_OUTPUT" -)
if test -n "$THE_DIFF"; then
echo "not ok 1 - $1"
echo "$THE_DIFF" | while read line; do echo "#$line"; done
else
echo "ok 1 - $1"
fi
[Test]
Type=session
Exec=@pkglibexecdir@/installed-tests/debugger-test.sh @pkglibexecdir@/installed-tests/debugger/@name@
Output=TAP
{
"rules": {
"no-debugger": "off"
}
}
debugger;
[[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]].forEach(array => {
debugger;
array.forEach(num => {
debugger;
print(num);
});
});
GJS debugger. Type "help" for help
Debugger statement, toplevel at backtrace.debugger.js:1:0
db> backtrace
#0 toplevel at backtrace.debugger.js:1:0
db> c
Debugger statement, toplevel at backtrace.debugger.js:1:9
db> bt
#0 toplevel at backtrace.debugger.js:1:9
db> c
Debugger statement, <anonymous>([object Array], 0, [object Array]) at backtrace.debugger.js:3:4
db> where
#0 <anonymous>([object Array], 0, [object Array]) at backtrace.debugger.js:3:4
#1 toplevel at backtrace.debugger.js:2:0
db> q
Program exited with code 0
breakpoint 2
break 4
b 6
c
c
c
c
print('1');
print('2');
function foo() {
print('Function foo');
}
print('3');
foo();
GJS debugger. Type "help" for help
Debugger statement, toplevel at breakpoint.debugger.js:1:0
db> breakpoint 2
Breakpoint 1 at breakpoint.debugger.js:2:0
db> break 4
Breakpoint 2 at breakpoint.debugger.js:4:4
db> b 6
Breakpoint 3 at breakpoint.debugger.js:6:0
db> c
1
Breakpoint 1, toplevel at breakpoint.debugger.js:2:0
db> c
2
Breakpoint 3, toplevel at breakpoint.debugger.js:6:0
db> c
3
Breakpoint 2, foo() at breakpoint.debugger.js:4:4
db> c
Function foo
Program exited with code 0
GJS debugger. Type "help" for help
Debugger statement, toplevel at continue.debugger.js:1:0
db> continue
Debugger statement, toplevel at continue.debugger.js:1:9
db> cont
Debugger statement, toplevel at continue.debugger.js:2:0
db> c
Program exited with code 0
b 2
b 3
b 4
b 5
# Check that breakpoint 4 still remains after deleting 1-3
delete 1
del 2
d 3
c
c
print('1');
print('2');
print('3');
print('4');
print('5');
GJS debugger. Type "help" for help
Debugger statement, toplevel at delete.debugger.js:1:0
db> b 2
Breakpoint 1 at delete.debugger.js:2:0
db> b 3
Breakpoint 2 at delete.debugger.js:3:0
db> b 4
Breakpoint 3 at delete.debugger.js:4:0
db> b 5
Breakpoint 4 at delete.debugger.js:5:0
db> # Check that breakpoint 4 still remains after deleting 1-3
db> delete 1
Breakpoint 1 at delete.debugger.js:2:0 deleted
db> del 2
Breakpoint 2 at delete.debugger.js:3:0 deleted
db> d 3
Breakpoint 3 at delete.debugger.js:4:0 deleted
db> c
1
2
3
4
Breakpoint 4, toplevel at delete.debugger.js:5:0
db> c
5
Program exited with code 0
GJS debugger. Type "help" for help
Debugger statement, toplevel at detach.debugger.js:1:0
db> detach
hi
Program exited with code 0
c
down
up
up
up
up
up
down
dn
dn
dn
c
function a() {
b();
}
function b() {
c();
}
function c() {
d();
}
function d() {
debugger;
}
a();
GJS debugger. Type "help" for help
Debugger statement, toplevel at down-up.debugger.js:1:0
db> c
Debugger statement, d() at down-up.debugger.js:14:4
db> down
Youngest frame selected; you cannot go down.
db> up
#1 c() at down-up.debugger.js:10:4
db> up
#2 b() at down-up.debugger.js:6:4
db> up
#3 a() at down-up.debugger.js:2:4
db> up
#4 toplevel at down-up.debugger.js:17:0
db> up
Initial frame selected; you cannot go up.
db> down
#3 a() at down-up.debugger.js:2:4
db> dn
#2 b() at down-up.debugger.js:6:4
db> dn
#1 c() at down-up.debugger.js:10:4
db> dn
#0 d() at down-up.debugger.js:14:4
db> c
Program exited with code 0
function foo() {
print('Print me');
debugger;
print('Print me also');
}
function bar() {
print('Print me');
debugger;
print('Print me also');
return 5;
}
foo();
bar();
print('Print me at the end');
GJS debugger. Type "help" for help
Debugger statement, toplevel at finish.debugger.js:1:0
db> c
Print me
Debugger statement, foo() at finish.debugger.js:3:4
db> finish
Run till exit from foo() at finish.debugger.js:3:4
Print me also
No value returned.
toplevel at finish.debugger.js:14:0
db> c
Print me
Debugger statement, bar() at finish.debugger.js:9:4
db> fin
Run till exit from bar() at finish.debugger.js:9:4
Print me also
Value returned is:
$1 = 5
toplevel at finish.debugger.js:15:0
db> c
Print me at the end
Program exited with code 0
function a() {
b();
}
function b() {
debugger;
}
a();
GJS debugger. Type "help" for help
Debugger statement, toplevel at frame.debugger.js:1:0
db> c
Debugger statement, b() at frame.debugger.js:6:4
db> frame 2
#2 toplevel at frame.debugger.js:9:0
db> f 1
#1 a() at frame.debugger.js:2:4
db> c
Program exited with code 0
const a = {
foo: 1,
bar: null,
tres: undefined,
};
debugger;
void a;
\ No newline at end of file
GJS debugger. Type "help" for help
Debugger statement, toplevel at keys.debugger.js:1:0
db> c
Debugger statement, toplevel at keys.debugger.js:6:0
db> keys a
$1 = [object Array]
[
"foo",
"bar",
"tres"
]
db> k a
$2 = [object Array]
[
"foo",
"bar",
"tres"
]
db> c
Program exited with code 0