Bug 673125: Implement the Debugger.Frame.prototype.onStep accessor. r=jorendorff.

This commit is contained in:
Jim Blandy 2011-08-23 14:45:36 -05:00
parent 3abd972834
commit 01972cd38f
24 changed files with 674 additions and 29 deletions

View File

@ -14,7 +14,7 @@ dbg.onDebuggerStatement = function (frame) {
var args = frame.arguments;
assertEq(args.length, argc);
for (var i = 0; i < argc; i++)
assertEq(args[i], i);
assertEq(args[i], i);
hits++;
}

View File

@ -0,0 +1,24 @@
// Simple Debugger.Frame.prototype.onStep test.
// Test that onStep fires often enough to see all four values of a.
var g = newGlobal('new-compartment');
g.a = 0;
g.eval("function f() {\n" +
" a += 2;\n" +
" a += 2;\n" +
" a += 2;\n" +
" return a;\n" +
"}\n");
var dbg = Debugger(g);
var seen = [0, 0, 0, 0, 0, 0, 0];
dbg.onEnterFrame = function (frame) {
frame.onStep = function () {
assertEq(arguments.length, 0);
assertEq(this, frame);
seen[g.a] = 1;
};
}
g.f();
assertEq(seen.join(""), "1010101");

View File

@ -0,0 +1,27 @@
// Setting frame.onStep to undefined turns off single-stepping.
var g = newGlobal('new-compartment');
g.a = 0;
g.eval("function f() {\n" +
" a++;\n" +
" a++;\n" +
" a++;\n" +
" a++;\n" +
" return a;\n" +
"}\n");
var dbg = Debugger(g);
var seen = [0, 0, 0, 0, 0];
dbg.onEnterFrame = function (frame) {
seen[g.a] = 1;
frame.onStep = function () {
seen[g.a] = 1;
if (g.a === 2) {
frame.onStep = undefined;
assertEq(frame.onStep, undefined);
}
};
}
g.f();
assertEq(seen.join(","), "1,1,1,0,0");

View File

@ -0,0 +1,28 @@
// Setting onStep does not affect later calls to the same function.
// (onStep is per-frame, not per-function.)
var g = newGlobal('new-compartment');
g.a = 1;
g.eval("function f(a) {\n" +
" var x = 2 * a;\n" +
" return x * x;\n" +
"}\n");
var dbg = Debugger(g);
var log = '';
dbg.onEnterFrame = function (frame) {
log += '+';
frame.onStep = function () {
if (log.charAt(log.length - 1) != 's')
log += 's';
};
};
g.f(1);
log += '|';
g.f(2);
log += '|';
dbg.onEnterFrame = undefined;
g.f(3);
assertEq(log, '+s|+s|');

View File

@ -0,0 +1,34 @@
// When a recursive function has many frames on the stack, onStep may be set or
// not independently on each frame.
var g = newGlobal('new-compartment');
g.eval("function f(x) {\n" +
" if (x > 0)\n" +
" f(x - 1);\n" +
" else\n" +
" debugger;\n" +
" return x;\n" +
"}");
var dbg = Debugger(g);
var seen = [0, 0, 0, 0, 0, 0, 0, 0];
function step() {
seen[this.arguments[0]] = 1;
}
dbg.onEnterFrame = function (frame) {
// Turn on stepping for even-numbered frames.
var x = frame.arguments[0];
if (x % 2 === 0)
frame.onStep = step;
};
dbg.onDebuggerStatement = function (frame) {
// This is called with 8 call frames on the stack, 7 down to 0.
// At this point we should have seen all the even-numbered frames.
assertEq(seen.join(""), "10101010");
// Now reset seen to see which frames fire onStep on the way out.
seen = [0, 0, 0, 0, 0, 0, 0, 0];
};
g.f(7);
assertEq(seen.join(""), "10101010");

View File

@ -0,0 +1,14 @@
// Upon returning to a frame with an onStep hook, the hook is called before the
// next line.
var g = newGlobal('new-compartment');
g.log = '';
g.eval("function f() { debugger; }");
var dbg = Debugger(g);
dbg.onDebuggerStatement = function (frame) {
frame.older.onStep = function () { g.log += 's'; };
};
g.eval("f();\n" +
"log += 'x';\n");
assertEq(g.log.charAt(0), 's');

View File

@ -0,0 +1,66 @@
// After returning from an implicit toString call, the calling frame's onStep
// hook fires.
var g = newGlobal('new-compartment');
g.eval("var originalX = {toString: function () { debugger; log += 'x'; return 1; }};\n");
var dbg = Debugger(g);
dbg.onDebuggerStatement = function (frame) {
g.log += 'd';
frame.older.onStep = function () {
if (!g.log.match(/[sy]$/))
g.log += 's';
};
};
// expr is an expression that will trigger an implicit toString call.
function check(expr) {
g.log = '';
g.x = g.originalX;
g.eval(expr + ";\n" +
"log += 'y';\n");
assertEq(g.log, 'dxsy');
}
check("'' + x");
check("0 + x");
check("0 - x");
check("0 * x");
check("0 / x");
check("0 % x");
check("+x");
check("x in {}");
check("x++");
check("++x");
check("x--");
check("--x");
check("x < 0");
check("x > 0");
check("x >= 0");
check("x <= 0");
check("x == 0");
check("x != 0");
check("x & 1");
check("x | 1");
check("x ^ 1");
check("~x");
check("x << 1");
check("x >> 1");
check("x >>> 1");
g.eval("function lastStep() { throw StopIteration; }");
g.eval("function emptyIterator() { debugger; log += 'x'; return { next: lastStep }; }");
g.eval("var customEmptyIterator = { __iterator__: emptyIterator };");
g.log = '';
g.eval("for (i in customEmptyIterator);\n" +
"log += 'y';\n");
assertEq(g.log, 'dxsy');
g.eval("var getter = { get x() { debugger; return log += 'x'; } }");
check("getter.x");
g.eval("var setter = { set x(v) { debugger; return log += 'x'; } }");
check("setter.x = 1");
g.eval("Object.defineProperty(this, 'thisgetter', { get: function() { debugger; log += 'x'; }});");
check("thisgetter");

View File

@ -0,0 +1,23 @@
// The tracejit does not interfere with frame.onStep.
//
// The function f() writes 'L' to the log in a loop. If we enable stepping and
// write an 's' each time frame.onStep is called, any two Ls should have at
// least one 's' between them.
var g = newGlobal('new-compartment');
g.N = RUNLOOP + 2;
g.log = '';
g.eval("function f() {\n" +
" for (var i = 0; i <= N; i++)\n" +
" log += 'L';\n" +
"}\n");
g.f();
assertEq(/LL/.exec(g.log) !== null, true);
var dbg = Debugger(g);
dbg.onEnterFrame = function (frame) {
frame.onStep = function () { g.log += 's'; };
};
g.log = '';
g.f();
assertEq(/LL/.exec(g.log), null);

View File

@ -0,0 +1,29 @@
// frame.onStep can coexist with breakpoints.
var g = newGlobal('new-compartment');
var dbg = Debugger(g);
var log = '';
dbg.onEnterFrame = function (frame) {
var handler = {hit: function () { log += 'B'; }};
var lines = frame.script.getAllOffsets();
for (var line in lines) {
line = Number(line);
var offs = lines[line];
for (var i = 0; i < offs.length; i++)
frame.script.setBreakpoint(offs[i], handler);
}
frame.onStep = function () { log += 's'; };
};
g.eval("one = 1;\n" +
"two = 2;\n" +
"three = 3;\n" +
"four = 4;\n");
assertEq(g.four, 4);
// Breakpoints hit on all four lines.
assertEq(log.replace(/[^B]/g, ''), 'BBBB');
// onStep was called between each pair of breakpoints.
assertEq(/BB/.exec(log), null);

View File

@ -0,0 +1,24 @@
// After an implicit toString call throws an exception, the calling frame's
// onStep hook fires.
var g = newGlobal('new-compartment');
g.eval("var x = {toString: function () { debugger; log += 'x'; throw 'mud'; }};");
var dbg = Debugger(g);
dbg.onDebuggerStatement = function (frame) {
g.log += 'd';
frame.older.onStep = function () {
if (!g.log.match(/[sy]$/))
g.log += 's';
};
};
g.log = '';
g.eval("try { x + ''; } catch (x) { }\n" +
"log += 'y';\n");
assertEq(g.log, "dxsy");
g.log = '';
g.eval("try { '' + x; } catch (x) { }\n" +
"log += 'y';\n");
assertEq(g.log, "dxsy");

View File

@ -0,0 +1,28 @@
// Throwing and catching an error in an onStep handler shouldn't interfere
// with throwing and catching in the debuggee.
var g = newGlobal('new-compartment');
g.eval("function f() { debugger; throw 'mud'; }");
var dbg = Debugger(g);
var stepped = false;
dbg.onDebuggerStatement = function (frame) {
frame.older.onStep = function () {
stepped = true;
try {
throw 'snow';
} catch (x) {
assertEq(x, 'snow');
}
};
};
stepped = false;
g.eval("var caught;\n" +
"try {\n" +
" f();\n" +
"} catch (x) {\n" +
" caught = x;\n" +
"}\n");
assertEq(stepped, true);
assertEq(g.caught, 'mud');

View File

@ -0,0 +1,78 @@
// Test that a frame's onStep handler gets called at least once on each line of a function.
var g = newGlobal('new-compartment');
var dbg = new Debugger(g);
// When we hit a 'debugger' statement, set offsets to the frame's script's
// table of line offsets --- a sparse array indexed by line number. Begin
// single-stepping the current frame; for each source line we hit, delete
// the line's entry in offsets. Thus, at the end, offsets is an array with
// an element for each line we did not reach.
var doSingleStep = true;
var offsets;
dbg.onDebuggerStatement = function (frame) {
var script = frame.script;
offsets = script.getAllOffsets();
print("debugger line: " + script.getOffsetLine(frame.offset));
print("original lines: " + uneval(Object.keys(offsets)));
if (doSingleStep) {
frame.onStep = function onStepHandler() {
var line = script.getOffsetLine(this.offset);
delete offsets[line];
};
}
};
g.eval(
'function t(a, b, c) { \n' +
' debugger; \n' +
' var x = a; \n' +
' x += b; \n' +
' if (x < 10) \n' +
' x -= c; \n' +
' return x; \n' +
'} \n'
);
// This should stop at every line but the first of the function.
g.eval('t(1,2,3)');
assertEq(Object.keys(offsets).length, 1);
// This should stop at every line but the first of the function, and the
// body of the 'if'.
g.eval('t(10,20,30)');
assertEq(Object.keys(offsets).length, 2);
// This shouldn't stop at all. It's the frame that's in single-step mode,
// not the script, so the prior execution of t in single-step mode should
// have no effect on this one.
doSingleStep = false;
g.eval('t(0, 0, 0)');
assertEq(Object.keys(offsets).length, 6);
doSingleStep = true;
// Single-step in an eval frame. This should reach every line but the
// first.
g.eval(
'debugger; \n' +
'var a=1, b=2, c=3; \n' +
'var x = a; \n' +
'x += b; \n' +
'if (x < 10) \n' +
' x -= c; \n'
);
print("final lines: " + uneval(Object.keys(offsets)));
assertEq(Object.keys(offsets).length, 1);
// Single-step in a global code frame. This should reach every line but the
// first.
g.evaluate(
'debugger; \n' +
'var a=1, b=2, c=3; \n' +
'var x = a; \n' +
'x += b; \n' +
'if (x < 10) \n' +
' x -= c; \n'
);
print("final lines: " + uneval(Object.keys(offsets)));
assertEq(Object.keys(offsets).length, 1);

View File

@ -0,0 +1,14 @@
// If frame.onStep returns {return:val}, the frame returns.
var g = newGlobal('new-compartment');
g.eval("function f(x) {\n" +
" var a = x * x;\n" +
" return a;\n" +
"}\n");
var dbg = Debugger(g);
dbg.onEnterFrame = function (frame) {
frame.onStep = function () { return {return: "pass"}; };
};
assertEq(g.f(4), "pass");

View File

@ -0,0 +1,17 @@
// If frame.onStep returns {throw:}, an exception is thrown in the debuggee.
load(libdir + "asserts.js");
var g = newGlobal('new-compartment');
g.eval("function h() { debugger; }\n" +
"function f() {\n" +
" h();\n" +
" return 'fail';\n" +
"}\n");
var dbg = Debugger(g);
dbg.onDebuggerStatement = function (frame) {
frame.older.onStep = function () { return {throw: "pass"}; };
};
assertThrowsValue(g.f, "pass");

View File

@ -0,0 +1,19 @@
// If frame.onStep returns null, the debuggee terminates.
var g = newGlobal('new-compartment');
g.eval("function h() { debugger; }");
var dbg = Debugger(g);
var hits = 0;
dbg.onDebuggerStatement = function (frame) {
hits++;
if (hits == 1) {
var rv = frame.eval("h();\n" +
"throw 'fail';\n");
assertEq(rv, null);
} else {
frame.older.onStep = function () { return null; };
}
};
g.h();
assertEq(hits, 2);

View File

@ -0,0 +1,31 @@
// If frame.onStep returns null, debuggee catch and finally blocks are skipped.
var g = newGlobal('new-compartment');
g.eval("function h() { debugger; }");
var dbg = Debugger(g);
var hits = 0;
dbg.onDebuggerStatement = function (frame) {
hits++;
if (hits == 1) {
var rv = frame.eval("try {\n" +
" h();\n" +
" throw 'fail';\n" +
"} catch (exc) {\n" +
" caught = exc;\n" +
"} finally {\n" +
" finallyHit = true;\n" +
"}\n");
assertEq(rv, null);
} else {
frame.older.onStep = function () {
this.onStep = undefined;
return null;
};
}
};
g.h();
assertEq(hits, 2);
assertEq("caught" in g, false);
assertEq("finallyHit" in g, false);

View File

@ -4,9 +4,9 @@ var g = newGlobal('new-compartment');
var dbg = Debugger(g);
dbg.onDebuggerStatement = function (frame) {
try {
frame.arguments[0].deleteProperty("x");
frame.arguments[0].deleteProperty("x");
} catch (exc) {
return;
return;
}
throw new Error("deleteProperty should throw");
};

View File

@ -27,14 +27,14 @@ function test(code) {
var xw = gw.getOwnPropertyDescriptor("x").value;
function check() {
// The Debugger.Object seal/freeze/preventExtensions methods
// had the same effect as the corresponding ES5 Object methods.
g.compareObjects();
// The Debugger.Object seal/freeze/preventExtensions methods
// had the same effect as the corresponding ES5 Object methods.
g.compareObjects();
// The Debugger.Object introspection methods agree with the ES5 Object methods.
assertEq(xw.isExtensible(), g.Object.isExtensible(g.x), code + ' isExtensible');
assertEq(xw.isSealed(), g.Object.isSealed(g.x), code + ' isSealed');
assertEq(xw.isFrozen(), g.Object.isFrozen(g.x), code + ' isFrozen');
// The Debugger.Object introspection methods agree with the ES5 Object methods.
assertEq(xw.isExtensible(), g.Object.isExtensible(g.x), code + ' isExtensible');
assertEq(xw.isSealed(), g.Object.isSealed(g.x), code + ' isSealed');
assertEq(xw.isFrozen(), g.Object.isFrozen(g.x), code + ' isFrozen');
}
check();

View File

@ -6,9 +6,9 @@ var hits = 0;
dbg.onExceptionUnwind = function (frame, value) { hits = 'BAD'; };
dbg.onDebuggerStatement = function (frame) {
if (hits++ === 0)
assertEq(frame.eval("debugger;"), null);
assertEq(frame.eval("debugger;"), null);
else
return null;
return null;
}
assertEq(g.eval("debugger; 2"), 2);

View File

@ -370,3 +370,4 @@ MSG_DEF(JSMSG_DEBUG_BAD_LINE, 283, 0, JSEXN_TYPEERR, "invalid line numbe
MSG_DEF(JSMSG_DEBUG_NOT_DEBUGGING, 284, 0, JSEXN_ERR, "can't set breakpoint: script global is not a debuggee")
MSG_DEF(JSMSG_DEBUG_COMPARTMENT_MISMATCH, 285, 2, JSEXN_TYPEERR, "{0}: descriptor .{1} property is an object in a different compartment than the target object")
MSG_DEF(JSMSG_NOT_CALLABLE_OR_UNDEFINED, 286, 0, JSEXN_TYPEERR, "value is not a function or undefined")
MSG_DEF(JSMSG_DEBUG_NOT_SCRIPT_FRAME, 287, 0, JSEXN_ERR, "stack frame is not running JavaScript code")

View File

@ -1886,12 +1886,13 @@ Interpret(JSContext *cx, StackFrame *entryFrame, InterpMode interpMode)
#define RESTORE_INTERP_VARS() \
JS_BEGIN_MACRO \
script = regs.fp()->script(); \
SET_SCRIPT(regs.fp()->script()); \
argv = regs.fp()->maybeFormalArgs(); \
atoms = FrameAtomBase(cx, regs.fp()); \
JS_ASSERT(&cx->regs() == &regs); \
if (cx->isExceptionPending()) \
goto error; \
CHECK_INTERRUPT_HANDLER(); \
JS_END_MACRO
#define MONITOR_BRANCH() \
@ -1974,6 +1975,13 @@ Interpret(JSContext *cx, StackFrame *entryFrame, InterpMode interpMode)
DO_OP(); \
JS_END_MACRO
#define SET_SCRIPT(s) \
JS_BEGIN_MACRO \
script = (s); \
if (script->stepModeEnabled()) \
ENABLE_INTERRUPTS(); \
JS_END_MACRO
#define CHECK_INTERRUPT_HANDLER() \
JS_BEGIN_MACRO \
if (cx->debugHooks->interruptHook) \
@ -2006,7 +2014,8 @@ Interpret(JSContext *cx, StackFrame *entryFrame, InterpMode interpMode)
/* Copy in hot values that change infrequently. */
JSRuntime *const rt = cx->runtime;
JSScript *script = regs.fp()->script();
JSScript *script;
SET_SCRIPT(regs.fp()->script());
int *pcCounts = script->pcCounters.get(JSRUNMODE_INTERP);
Value *argv = regs.fp()->maybeFormalArgs();
CHECK_INTERRUPT_HANDLER();
@ -2151,18 +2160,24 @@ Interpret(JSContext *cx, StackFrame *entryFrame, InterpMode interpMode)
{
bool moreInterrupts = false;
JSInterruptHook hook = cx->debugHooks->interruptHook;
if (hook) {
if (hook || script->stepModeEnabled()) {
#ifdef JS_TRACER
if (TRACE_RECORDER(cx))
AbortRecording(cx, "interrupt hook");
AbortRecording(cx, "interrupt hook or singleStepMode");
#ifdef JS_METHODJIT
if (TRACE_PROFILER(cx))
AbortProfiling(cx);
#endif
#endif
Value rval;
switch (hook(cx, script, regs.pc, Jsvalify(&rval),
cx->debugHooks->interruptHookData)) {
JSTrapStatus status = JSTRAP_CONTINUE;
if (hook) {
status = hook(cx, script, regs.pc, Jsvalify(&rval),
cx->debugHooks->interruptHookData);
}
if (status == JSTRAP_CONTINUE && script->stepModeEnabled())
status = Debugger::onSingleStep(cx, &rval);
switch (status) {
case JSTRAP_ERROR:
goto error;
case JSTRAP_CONTINUE:
@ -2381,7 +2396,6 @@ BEGIN_CASE(JSOP_STOP)
JS_ASSERT(!regs.fp()->hasImacropc());
JS_ASSERT(!js_IsActiveWithOrBlock(cx, &regs.fp()->scopeChain(), 0));
interpReturnOK = ScriptEpilogue(cx, regs.fp(), interpReturnOK);
CHECK_INTERRUPT_HANDLER();
/* The JIT inlines ScriptEpilogue. */
#ifdef JS_METHODJIT
@ -2390,10 +2404,11 @@ BEGIN_CASE(JSOP_STOP)
cx->stack.popInlineFrame(regs);
/* Sync interpreter locals. */
script = regs.fp()->script();
SET_SCRIPT(regs.fp()->script());
pcCounts = script->pcCounters.get(JSRUNMODE_INTERP);
argv = regs.fp()->maybeFormalArgs();
atoms = FrameAtomBase(cx, regs.fp());
CHECK_INTERRUPT_HANDLER();
JS_ASSERT(*regs.pc == JSOP_TRAP || *regs.pc == JSOP_NEW || *regs.pc == JSOP_CALL ||
*regs.pc == JSOP_FUNCALL || *regs.pc == JSOP_FUNAPPLY);
@ -4060,7 +4075,7 @@ BEGIN_CASE(JSOP_FUNAPPLY)
goto error;
/* Refresh local js::Interpret state. */
script = newScript;
SET_SCRIPT(newScript);
pcCounts = script->pcCounters.get(JSRUNMODE_INTERP);
argv = regs.fp()->formalArgsEnd() - fun->nargs;
atoms = script->atomMap.vector;
@ -4083,7 +4098,6 @@ BEGIN_CASE(JSOP_FUNAPPLY)
goto error;
if (!TRACE_RECORDER(cx) && !TRACE_PROFILER(cx) && status == mjit::Compile_Okay) {
interpReturnOK = mjit::JaegerShot(cx);
CHECK_INTERRUPT_HANDLER();
goto jit_return;
}
}

View File

@ -1193,6 +1193,9 @@ stubs::Trap(VMFrame &f, uint32 trapTypes)
if (hook)
result = hook(f.cx, f.cx->fp()->script(), pc, Jsvalify(&rval),
f.cx->debugHooks->interruptHookData);
if (result == JSTRAP_CONTINUE)
result = Debugger::onSingleStep(f.cx, &rval);
}
if (result == JSTRAP_CONTINUE && (trapTypes & JSTRAP_TRAP))

View File

@ -63,6 +63,7 @@ extern Class DebuggerFrame_class;
enum {
JSSLOT_DEBUGFRAME_OWNER,
JSSLOT_DEBUGFRAME_ARGUMENTS,
JSSLOT_DEBUGFRAME_ONSTEP_HANDLER,
JSSLOT_DEBUGFRAME_COUNT
};
@ -388,11 +389,24 @@ Debugger::getHook(Hook hook) const
bool
Debugger::hasAnyLiveHooks() const
{
return enabled && (getHook(OnDebuggerStatement) ||
getHook(OnExceptionUnwind) ||
getHook(OnNewScript) ||
getHook(OnEnterFrame) ||
!JS_CLIST_IS_EMPTY(&breakpoints));
if (!enabled)
return false;
if (getHook(OnDebuggerStatement) ||
getHook(OnExceptionUnwind) ||
getHook(OnNewScript) ||
getHook(OnEnterFrame))
return true;
if (!JS_CLIST_IS_EMPTY(&breakpoints))
return true;
for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) {
if (!r.front().value->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined())
return true;
}
return false;
}
void
@ -433,8 +447,16 @@ Debugger::slowPathOnLeaveFrame(JSContext *cx)
for (Debugger **p = debuggers->begin(); p != debuggers->end(); p++) {
Debugger *dbg = *p;
if (FrameMap::Ptr p = dbg->frames.lookup(fp)) {
StackFrame *frame = p->key;
JSObject *frameobj = p->value;
frameobj->setPrivate(NULL);
/* If this frame had an onStep handler, adjust the script's count. */
if (!frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined() &&
frame->isScriptFrame()) {
frame->script()->changeStepModeCount(cx, -1);
}
dbg->frames.remove(p);
}
}
@ -878,6 +900,101 @@ Debugger::onTrap(JSContext *cx, Value *vp)
return JSTRAP_CONTINUE;
}
JSTrapStatus
Debugger::onSingleStep(JSContext *cx, Value *vp)
{
StackFrame *fp = cx->fp();
/*
* We may be stepping over a JSOP_EXCEPTION, that pushes the context's
* pending exception for a 'catch' clause to handle. Don't let the
* onStep handlers mess with that (other than by returning a resumption
* value).
*/
Value exception = UndefinedValue();
bool exceptionPending = cx->isExceptionPending();
if (exceptionPending) {
exception = cx->getPendingException();
cx->clearPendingException();
}
/* We should only receive single-step traps for scripted frames. */
JS_ASSERT(fp->isScriptFrame());
/*
* Build list of Debugger.Frame instances referring to this frame with
* onStep handlers.
*/
AutoObjectVector frames(cx);
GlobalObject *global = fp->scopeChain().getGlobal();
if (GlobalObject::DebuggerVector *debuggers = global->getDebuggers()) {
for (Debugger **d = debuggers->begin(); d != debuggers->end(); d++) {
Debugger *dbg = *d;
if (FrameMap::Ptr p = dbg->frames.lookup(fp)) {
JSObject *frame = p->value;
if (!frame->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined() &&
!frames.append(frame))
return JSTRAP_ERROR;
}
}
}
#ifdef DEBUG
/*
* Validate the single-step count on this frame's script, to ensure that
* we're not receiving traps we didn't ask for. Even when frames is
* non-empty (and thus we know this trap was requested), do the check
* anyway, to make sure the count has the correct non-zero value.
*
* The converse --- ensuring that we do receive traps when we should --- can
* be done with unit tests.
*/
{
uint32 stepperCount = 0;
JSScript *trappingScript = fp->script();
if (GlobalObject::DebuggerVector *debuggers = global->getDebuggers()) {
for (Debugger **p = debuggers->begin(); p != debuggers->end(); p++) {
Debugger *dbg = *p;
for (FrameMap::Range r = dbg->frames.all(); !r.empty(); r.popFront()) {
StackFrame *frame = r.front().key;
JSObject *frameobj = r.front().value;
if (frame->isScriptFrame() &&
frame->script() == trappingScript &&
!frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined())
{
stepperCount++;
}
}
}
}
if (trappingScript->compileAndGo)
JS_ASSERT(stepperCount == trappingScript->stepModeCount());
else
JS_ASSERT(stepperCount <= trappingScript->stepModeCount());
}
#endif
/* Call all the onStep handlers we found. */
for (JSObject **p = frames.begin(); p != frames.end(); p++) {
JSObject *frame = *p;
Debugger *dbg = Debugger::fromChildJSObject(frame);
AutoCompartment ac(cx, dbg->object);
if (!ac.enter())
return JSTRAP_ERROR;
const Value &handler = frame->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER);
Value rval;
bool ok = Invoke(cx, ObjectValue(*frame), handler, 0, NULL, &rval);
JSTrapStatus st = dbg->parseResumptionValue(ac, ok, rval, vp);
if (st != JSTRAP_CONTINUE)
return st;
}
vp->setUndefined();
if (exceptionPending)
cx->setPendingException(exception);
return JSTRAP_CONTINUE;
}
/*** Debugger JSObjects **************************************************************************/
@ -1027,8 +1144,12 @@ Debugger::trace(JSTracer *trc)
MarkObject(trc, *uncaughtExceptionHook, "hooks");
/*
* Mark Debugger.Frame objects that are reachable from JS if we look them up
* again (because the corresponding StackFrame is still on the stack).
* Mark Debugger.Frame objects. These are all reachable from JS, because the
* corresponding StackFrames are still on the stack.
*
* (Once we support generator frames properly, we will need
* weakly-referenced Debugger.Frame objects as well, for suspended generator
* frames.)
*/
for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) {
JSObject *frameobj = r.front().value;
@ -2650,6 +2771,54 @@ DebuggerFrame_getLive(JSContext *cx, uintN argc, Value *vp)
return true;
}
static bool
IsValidHook(const Value &v)
{
return v.isUndefined() || (v.isObject() && v.toObject().isCallable());
}
static JSBool
DebuggerFrame_getOnStep(JSContext *cx, uintN argc, Value *vp)
{
THIS_FRAME(cx, argc, vp, "get onStep", args, thisobj, fp);
(void) fp; // Silence GCC warning
Value handler = thisobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER);
JS_ASSERT(IsValidHook(handler));
args.rval() = handler;
return true;
}
static JSBool
DebuggerFrame_setOnStep(JSContext *cx, uintN argc, Value *vp)
{
REQUIRE_ARGC("Debugger.Frame.set onStep", 1);
THIS_FRAME(cx, argc, vp, "set onStep", args, thisobj, fp);
if (!fp->isScriptFrame()) {
JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_DEBUG_NOT_SCRIPT_FRAME);
return false;
}
if (!IsValidHook(args[0])) {
JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_NOT_CALLABLE_OR_UNDEFINED);
return false;
}
Value prior = thisobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER);
int delta = !args[0].isUndefined() - !prior.isUndefined();
if (delta != 0) {
/* Try to adjust this frame's script single-step mode count. */
AutoCompartment ac(cx, &fp->scopeChain());
if (!ac.enter())
return false;
if (!fp->script()->changeStepModeCount(cx, delta))
return false;
}
/* Now that the step mode switch has succeeded, we can install the handler. */
thisobj->setReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER, args[0]);
args.rval().setUndefined();
return true;
}
namespace js {
JSBool
@ -2795,6 +2964,7 @@ static JSPropertySpec DebuggerFrame_properties[] = {
JS_PSG("script", DebuggerFrame_getScript, 0),
JS_PSG("this", DebuggerFrame_getThis, 0),
JS_PSG("type", DebuggerFrame_getType, 0),
JS_PSGS("onStep", DebuggerFrame_getOnStep, DebuggerFrame_setOnStep, 0),
JS_PS_END
};

View File

@ -306,6 +306,7 @@ class Debugger {
NewScriptKind kind);
static inline void onDestroyScript(JSScript *script);
static JSTrapStatus onTrap(JSContext *cx, Value *vp);
static JSTrapStatus onSingleStep(JSContext *cx, Value *vp);
/************************************* Functions for use by Debugger.cpp. */