Add support for dbg.hooks.throw.
authorJason Orendorff <jorendorff@mozilla.com>
Fri, 13 May 2011 17:56:17 -0500
changeset 74423 5842d0285cdafaa4ae329214f0b46953d0c3d5e7
parent 74422 ddab2f5eb8df82b549b4c94c925569c9f3b57c0f
child 74424 8ef6dc591b933ffa08b4403bab0f5f311f7b8a7e
push id2
push userbsmedberg@mozilla.com
push dateFri, 19 Aug 2011 14:38:13 +0000
milestone6.0a1
Add support for dbg.hooks.throw.
js/src/jit-test/tests/debug/hooks-throw-01.js
js/src/jit-test/tests/debug/hooks-throw-02.js
js/src/jit-test/tests/debug/hooks-throw-03.js
js/src/jsdbg.cpp
js/src/jsdbg.h
js/src/jsinterp.cpp
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/hooks-throw-01.js
@@ -0,0 +1,27 @@
+// |jit-test| debug
+// Basic throw hook test.
+
+load(libdir + "asserts.js");
+
+var g = newGlobal('new-compartment');
+var dbg = Debug(g);
+var hit = false;
+dbg.hooks = {
+    throw: function (frame, exc) {
+        // hooks.throw is called multiple times as the stack is unwound.
+        // Only check the first hit.
+        assertEq(arguments.length, 2);
+        assertEq(frame instanceof Debug.Frame, true);
+        if (!hit) {
+            assertEq(exc, 101);
+            assertEq(frame.type, "call");
+            assertEq(frame.callee.name, "f");
+            assertEq(frame.older.type, "eval");
+            hit = true;
+        }
+    }
+};
+
+g.eval("function f() { throw 101; }");
+assertThrowsValue(function () { g.eval("f();"); }, 101);
+assertEq(hit, true);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/hooks-throw-02.js
@@ -0,0 +1,60 @@
+// |jit-test| debug
+// The throw hook is called multiple times as the stack unwinds.
+
+var g = newGlobal('new-compartment');
+g.debuggeeGlobal = this;
+g.dbg = null;
+g.eval("(" + function () {
+        dbg = new Debug(debuggeeGlobal);
+        dbg.hooks = {
+            throw: function (frame, exc) {
+                assertEq(frame instanceof Debug.Frame, true);
+                assertEq(exc instanceof Debug.Object, true);
+                var s = '!';
+                for (var f = frame; f; f = f.older)
+                    if (f.type === "call")
+                        s += f.callee.name;
+                s += ', ';
+                debuggeeGlobal.log += s;
+            }
+        };
+    } + ")();");
+
+var log;
+
+function k() {
+    try {
+        throw new Error("oops");  // hook call 1
+    } finally {
+        log += 'k-finally, ';
+    } // hook call 2
+}
+
+function j() {
+    k();  // hook call 3
+    log += 'j-unreached, ';
+}
+
+function h() {
+    try {
+        j();  // hook call 4
+        log += 'h-unreached, ';
+    } catch (exc) {
+        log += 'h-catch, ';
+        throw exc; // hook call 5
+    }
+}
+
+function f() {
+    try {
+        h(); // hook call 6
+    } catch (exc) {
+        log += 'f-catch, ';
+    }
+    log += 'f-after, ';
+}
+
+log = '';
+f();
+g.dbg.enabled = false;
+assertEq(log, '!kjhf, k-finally, !kjhf, !jhf, !hf, h-catch, !hf, !f, f-catch, f-after, ');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/hooks-throw-03.js
@@ -0,0 +1,21 @@
+// |jit-test| debug
+// hooks.throw is not called for exceptions thrown and handled in the debugger.
+
+var g = newGlobal('new-compartment');
+var dbg = Debug(g);
+g.log = '';
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        try {
+            throw new Error("oops");
+        } catch (exc) {
+            g.log += exc.message;
+        }
+    },
+    throw: function (frame) {
+        g.log += 'BAD';
+    }
+};
+
+g.eval("debugger; log += ' ok';");
+assertEq(g.log, 'oops ok');
--- a/js/src/jsdbg.cpp
+++ b/js/src/jsdbg.cpp
@@ -132,17 +132,18 @@ CheckThisClass(JSContext *cx, Value *vp,
 enum {
     JSSLOT_DEBUG_FRAME_PROTO,
     JSSLOT_DEBUG_OBJECT_PROTO,
     JSSLOT_DEBUG_COUNT
 };
 
 Debug::Debug(JSObject *dbg, JSObject *hooks, JSCompartment *compartment)
   : object(dbg), debuggeeCompartment(compartment), hooksObject(hooks),
-    uncaughtExceptionHook(NULL), enabled(true), hasDebuggerHandler(false)
+    uncaughtExceptionHook(NULL), enabled(true), hasDebuggerHandler(false),
+    hasThrowHandler(false)
 {
 }
 
 bool
 Debug::init()
 {
     return frames.init() && objects.init();
 }
@@ -378,16 +379,22 @@ CallMethodIfPresent(JSContext *cx, JSObj
     JSAtom *atom = js_Atomize(cx, name, strlen(name), 0);
     Value fval;
     return atom &&
            js_GetMethod(cx, obj, ATOM_TO_JSID(atom), JSGET_NO_METHOD_BARRIER, &fval) &&
            (!js_IsCallable(fval) ||
             ExternalInvoke(cx, ObjectValue(*obj), fval, argc, argv, rval));
 }
 
+bool
+Debug::observesDebuggerStatement() const
+{
+    return enabled && hasDebuggerHandler;
+}
+
 JSTrapStatus
 Debug::handleDebuggerStatement(JSContext *cx, Value *vp)
 {
     // Grab cx->fp() before pushing a dummy frame.
     StackFrame *fp = cx->fp();
 
     JS_ASSERT(hasDebuggerHandler);
     AutoCompartment ac(cx, hooksObject);
@@ -398,41 +405,74 @@ Debug::handleDebuggerStatement(JSContext
     if (!getScriptFrame(cx, fp, argv))
         return JSTRAP_ERROR;
 
     Value rv;
     bool ok = CallMethodIfPresent(cx, hooksObject, "debuggerHandler", 1, argv, &rv);
     return parseResumptionValue(ac, ok, rv, vp);
 }
 
+bool
+Debug::observesThrow() const
+{
+    return enabled && hasThrowHandler;
+}
+
 JSTrapStatus
-Debug::dispatchDebuggerStatement(JSContext *cx, js::Value *vp)
+Debug::handleThrow(JSContext *cx, Value *vp)
+{
+    // Grab cx->fp() and the exception value before preparing to call the hook.
+    StackFrame *fp = cx->fp();
+    Value exc = cx->getPendingException();
+
+    cx->clearPendingException();
+    JS_ASSERT(hasThrowHandler);
+    AutoCompartment ac(cx, hooksObject);
+    if (!ac.enter())
+        return JSTRAP_ERROR;
+
+    Value argv[2];
+    argv[1] = exc;
+    if (!getScriptFrame(cx, fp, &argv[0]) || !wrapDebuggeeValue(cx, &argv[1]))
+        return JSTRAP_ERROR;
+
+    Value rv;
+    bool ok = CallMethodIfPresent(cx, hooksObject, "throw", 2, argv, &rv);
+    JSTrapStatus st = parseResumptionValue(ac, ok, rv, vp);
+    if (st == JSTRAP_CONTINUE)
+        cx->setPendingException(argv[1]);
+    return st;
+}
+
+JSTrapStatus
+Debug::dispatchHook(JSContext *cx, js::Value *vp, DebugObservesMethod observesEvent,
+                    DebugHandleMethod handleEvent)
 {
     // Determine which debuggers will receive this event, and in what order.
     // Make a copy of the list, since the original is mutable and we will be
     // calling into arbitrary JS.
     // Note: In the general case, 'triggered' contains references to objects in
     // different compartments--every compartment *except* this one.
     AutoValueVector triggered(cx);
     JSCompartment *compartment = cx->compartment;
     const JSCompartment::DebugVector &debuggers = compartment->getDebuggers();
     for (Debug **p = debuggers.begin(); p != debuggers.end(); p++) {
         Debug *dbg = *p;
-        if (dbg->observesDebuggerStatement()) {
+        if ((dbg->*observesEvent)()) {
             if (!triggered.append(ObjectValue(*dbg->toJSObject())))
                 return JSTRAP_ERROR;
         }
     }
 
     // Deliver the event to each debugger, checking again to make sure it
     // should still be delivered.
     for (Value *p = triggered.begin(); p != triggered.end(); p++) {
         Debug *dbg = Debug::fromJSObject(&p->toObject());
-        if (dbg->observesCompartment(compartment) && dbg->observesDebuggerStatement()) {
-            JSTrapStatus st = dbg->handleDebuggerStatement(cx, vp);
+        if (dbg->observesCompartment(compartment) && (dbg->*observesEvent)()) {
+            JSTrapStatus st = (dbg->*handleEvent)(cx, vp);
             if (st != JSTRAP_CONTINUE)
                 return st;
         }
     }
     return JSTRAP_CONTINUE;
 }
 
 // === Debug JSObjects
@@ -587,22 +627,28 @@ JSBool
 Debug::setHooks(JSContext *cx, uintN argc, Value *vp)
 {
     REQUIRE_ARGC("Debug.set hooks", 1);
     THISOBJ(cx, vp, Debug, "set hooks", thisobj, dbg);
     if (!vp[2].isObject())
         return ReportObjectRequired(cx);
     JSObject *hooksobj = &vp[2].toObject();
 
+    bool hasDebuggerHandler, hasThrow;
     JSBool found;
     if (!JS_HasProperty(cx, hooksobj, "debuggerHandler", &found))
         return false;
-    dbg->hasDebuggerHandler = !!found;
+    hasDebuggerHandler = !!found;
+    if (!JS_HasProperty(cx, hooksobj, "throw", &found))
+        return false;
+    hasThrow = !!found;
 
     dbg->hooksObject = hooksobj;
+    dbg->hasDebuggerHandler = hasDebuggerHandler;
+    dbg->hasThrowHandler = hasThrow;
     vp->setUndefined();
     return true;
 }
 
 JSBool
 Debug::getEnabled(JSContext *cx, uintN argc, Value *vp)
 {
     THISOBJ(cx, vp, Debug, "get enabled", thisobj, dbg);
--- a/js/src/jsdbg.h
+++ b/js/src/jsdbg.h
@@ -56,19 +56,20 @@ class Debug {
 
   private:
     JSObject *object;  // The Debug object. Strong reference.
     JSCompartment *debuggeeCompartment;  // Weak reference.
     JSObject *hooksObject;  // See Debug.prototype.hooks. Strong reference.
     JSObject *uncaughtExceptionHook;  // Strong reference.
     bool enabled;
 
-    // True if hooksObject had a debuggerHandler property when the hooks
+    // True if hooksObject had a property of the respective name when the hooks
     // property was set.
-    bool hasDebuggerHandler;
+    bool hasDebuggerHandler;  // hooks.debuggerHandler
+    bool hasThrowHandler;     // hooks.throw
 
     typedef HashMap<StackFrame *, JSObject *, DefaultHasher<StackFrame *>, SystemAllocPolicy>
         FrameMap;
     FrameMap frames;
 
     typedef HashMap<JSObject *, JSObject *, DefaultHasher<JSObject *>, SystemAllocPolicy>
         ObjectMap;
     ObjectMap objects;
@@ -89,20 +90,28 @@ class Debug {
     static JSBool setUncaughtExceptionHook(JSContext *cx, uintN argc, Value *vp);
     static JSBool construct(JSContext *cx, uintN argc, Value *vp);
     static JSPropertySpec properties[];
 
     inline bool hasAnyLiveHooks() const;
 
     static void slowPathLeaveStackFrame(JSContext *cx);
 
-    inline bool observesDebuggerStatement() const;
-    static JSTrapStatus dispatchDebuggerStatement(JSContext *cx, Value *vp);
+    typedef bool (Debug::*DebugObservesMethod)() const;
+    typedef JSTrapStatus (Debug::*DebugHandleMethod)(JSContext *, Value *) const;
+    static JSTrapStatus dispatchHook(JSContext *cx, js::Value *vp,
+                                     DebugObservesMethod observesEvent,
+                                     DebugHandleMethod handleEvent);
+
+    bool observesDebuggerStatement() const;
     JSTrapStatus handleDebuggerStatement(JSContext *cx, Value *vp);
 
+    bool observesThrow() const;
+    JSTrapStatus handleThrow(JSContext *cx, Value *vp);
+
   public:
     Debug(JSObject *dbg, JSObject *hooks, JSCompartment *compartment);
     bool init();
     inline JSObject *toJSObject() const;
     static inline Debug *fromJSObject(JSObject *obj);
     static Debug *fromChildJSObject(JSObject *obj);
 
     /*********************************** Methods for interaction with the GC. */
@@ -126,16 +135,17 @@ class Debug {
     static void sweepAll(JSRuntime *rt);
     static void sweepCompartment(JSCompartment *compartment);
 
     inline bool observesCompartment(JSCompartment *c) const;
     void detachFrom(JSCompartment *c);
 
     static inline void leaveStackFrame(JSContext *cx);
     static inline JSTrapStatus onDebuggerStatement(JSContext *cx, js::Value *vp);
+    static inline JSTrapStatus onThrow(JSContext *cx, js::Value *vp);
 
     /**************************************** Functions for use by jsdbg.cpp. */
 
     // Precondition: *vp is a value from a debuggee compartment and cx is in
     // the debugger's compartment.
     //
     // Wrap *vp for the debugger compartment, wrap it in a Debug.Object if it's
     // an object, store the result in *vp, and return true.
@@ -202,25 +212,31 @@ Debug::fromJSObject(JSObject *obj)
 
 void
 Debug::leaveStackFrame(JSContext *cx)
 {
     if (!cx->compartment->getDebuggers().empty())
         slowPathLeaveStackFrame(cx);
 }
 
-bool
-Debug::observesDebuggerStatement() const
-{
-    return enabled && hasDebuggerHandler;
-}
-
 JSTrapStatus
 Debug::onDebuggerStatement(JSContext *cx, js::Value *vp)
 {
     return cx->compartment->getDebuggers().empty()
            ? JSTRAP_CONTINUE
-           : dispatchDebuggerStatement(cx, vp);
+           : dispatchHook(cx, vp,
+                          DebugObservesMethod(&Debug::observesDebuggerStatement),
+                          DebugHandleMethod(&Debug::handleDebuggerStatement));
+}
+
+JSTrapStatus
+Debug::onThrow(JSContext *cx, js::Value *vp)
+{
+    return cx->compartment->getDebuggers().empty()
+           ? JSTRAP_CONTINUE
+           : dispatchHook(cx, vp,
+                          DebugObservesMethod(&Debug::observesThrow),
+                          DebugHandleMethod(&Debug::handleThrow));
 }
 
 }
 
 #endif /* jsdbg_h__ */
--- a/js/src/jsinterp.cpp
+++ b/js/src/jsinterp.cpp
@@ -6631,33 +6631,38 @@ END_CASE(JSOP_ARRAYPUSH)
         JSThrowHook handler;
         JSTryNote *tn, *tnlimit;
         uint32 offset;
 
         /* Restore atoms local in case we will resume. */
         atoms = script->atomMap.vector;
 
         /* Call debugger throw hook if set. */
-        handler = cx->debugHooks->throwHook;
-        if (handler) {
+        if (cx->debugHooks->throwHook || !cx->compartment->getDebuggers().empty()) {
             Value rval;
-            switch (handler(cx, script, regs.pc, Jsvalify(&rval),
-                            cx->debugHooks->throwHookData)) {
-              case JSTRAP_ERROR:
+            JSTrapStatus st = Debug::onThrow(cx, &rval);
+            if (st == JSTRAP_CONTINUE) {
+                handler = cx->debugHooks->throwHook;
+                if (handler)
+                    st = handler(cx, script, regs.pc, Jsvalify(&rval), cx->debugHooks->throwHookData);
+            }
+
+            switch (st) {
+            case JSTRAP_ERROR:
                 cx->clearPendingException();
                 goto error;
-              case JSTRAP_RETURN:
+            case JSTRAP_RETURN:
                 cx->clearPendingException();
                 regs.fp()->setReturnValue(rval);
                 interpReturnOK = JS_TRUE;
                 goto forced_return;
-              case JSTRAP_THROW:
+            case JSTRAP_THROW:
                 cx->setPendingException(rval);
-              case JSTRAP_CONTINUE:
-              default:;
+            case JSTRAP_CONTINUE:
+            default:;
             }
             CHECK_INTERRUPT_HANDLER();
         }
 
         /*
          * Look for a try block in script that can catch this exception.
          */
         if (!JSScript::isValidOffset(script->trynotesOffset))