| /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- |
| * vim: set ts=8 sts=4 et sw=4 tw=99: |
| * This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
| |
| #include "vm/Debugger-inl.h" |
| |
| #include "mozilla/DebugOnly.h" |
| #include "mozilla/ScopeExit.h" |
| #include "mozilla/TypeTraits.h" |
| |
| #include "jscntxt.h" |
| #include "jscompartment.h" |
| #include "jsfriendapi.h" |
| #include "jshashutil.h" |
| #include "jsnum.h" |
| #include "jsobj.h" |
| #include "jswrapper.h" |
| |
| #include "frontend/BytecodeCompiler.h" |
| #include "gc/Marking.h" |
| #include "jit/BaselineDebugModeOSR.h" |
| #include "jit/BaselineJIT.h" |
| #include "jit/JSONSpewer.h" |
| #include "jit/MIRGraph.h" |
| #include "js/GCAPI.h" |
| #include "js/UbiNodeBreadthFirst.h" |
| #include "js/Vector.h" |
| #include "vm/ArgumentsObject.h" |
| #include "vm/DebuggerMemory.h" |
| #include "vm/SPSProfiler.h" |
| #include "vm/TraceLogging.h" |
| #include "vm/WrapperObject.h" |
| |
| #include "jsgcinlines.h" |
| #include "jsobjinlines.h" |
| #include "jsopcodeinlines.h" |
| #include "jsscriptinlines.h" |
| |
| #include "vm/NativeObject-inl.h" |
| #include "vm/Stack-inl.h" |
| |
| using namespace js; |
| |
| using JS::dbg::AutoEntryMonitor; |
| using JS::dbg::Builder; |
| using js::frontend::IsIdentifier; |
| using mozilla::ArrayLength; |
| using mozilla::DebugOnly; |
| using mozilla::MakeScopeExit; |
| using mozilla::Maybe; |
| using mozilla::UniquePtr; |
| |
| |
| /*** Forward declarations ************************************************************************/ |
| |
| extern const Class DebuggerFrame_class; |
| |
| enum { |
| JSSLOT_DEBUGFRAME_OWNER, |
| JSSLOT_DEBUGFRAME_ARGUMENTS, |
| JSSLOT_DEBUGFRAME_ONSTEP_HANDLER, |
| JSSLOT_DEBUGFRAME_ONPOP_HANDLER, |
| JSSLOT_DEBUGFRAME_COUNT |
| }; |
| |
| extern const Class DebuggerArguments_class; |
| |
| enum { |
| JSSLOT_DEBUGARGUMENTS_FRAME, |
| JSSLOT_DEBUGARGUMENTS_COUNT |
| }; |
| |
| extern const Class DebuggerEnv_class; |
| |
| enum { |
| JSSLOT_DEBUGENV_OWNER, |
| JSSLOT_DEBUGENV_COUNT |
| }; |
| |
| extern const Class DebuggerObject_class; |
| |
| enum { |
| JSSLOT_DEBUGOBJECT_OWNER, |
| JSSLOT_DEBUGOBJECT_COUNT |
| }; |
| |
| extern const Class DebuggerScript_class; |
| |
| enum { |
| JSSLOT_DEBUGSCRIPT_OWNER, |
| JSSLOT_DEBUGSCRIPT_COUNT |
| }; |
| |
| extern const Class DebuggerSource_class; |
| |
| enum { |
| JSSLOT_DEBUGSOURCE_OWNER, |
| JSSLOT_DEBUGSOURCE_TEXT, |
| JSSLOT_DEBUGSOURCE_COUNT |
| }; |
| |
| void DebuggerObject_trace(JSTracer* trc, JSObject* obj); |
| void DebuggerEnv_trace(JSTracer* trc, JSObject* obj); |
| void DebuggerScript_trace(JSTracer* trc, JSObject* obj); |
| void DebuggerSource_trace(JSTracer* trc, JSObject* obj); |
| |
| |
| /*** Utils ***************************************************************************************/ |
| |
| static inline bool |
| EnsureFunctionHasScript(JSContext* cx, HandleFunction fun) |
| { |
| if (fun->isInterpretedLazy()) { |
| AutoCompartment ac(cx, fun); |
| return !!fun->getOrCreateScript(cx); |
| } |
| return true; |
| } |
| |
| static inline JSScript* |
| GetOrCreateFunctionScript(JSContext* cx, HandleFunction fun) |
| { |
| MOZ_ASSERT(fun->isInterpreted()); |
| if (!EnsureFunctionHasScript(cx, fun)) |
| return nullptr; |
| return fun->nonLazyScript(); |
| } |
| |
| static bool |
| ValueToIdentifier(JSContext* cx, HandleValue v, MutableHandleId id) |
| { |
| if (!ValueToId<CanGC>(cx, v, id)) |
| return false; |
| if (!JSID_IS_ATOM(id) || !IsIdentifier(JSID_TO_ATOM(id))) { |
| RootedValue val(cx, v); |
| ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_UNEXPECTED_TYPE, |
| JSDVG_SEARCH_STACK, val, nullptr, "not an identifier", |
| nullptr); |
| return false; |
| } |
| return true; |
| } |
| |
| /* |
| * A range of all the Debugger.Frame objects for a particular AbstractFramePtr. |
| * |
| * FIXME This checks only current debuggers, so it relies on a hack in |
| * Debugger::removeDebuggeeGlobal to make sure only current debuggers |
| * have Frame objects with .live === true. |
| */ |
| class Debugger::FrameRange |
| { |
| AbstractFramePtr frame; |
| |
| /* The debuggers in |fp|'s compartment, or nullptr if there are none. */ |
| GlobalObject::DebuggerVector* debuggers; |
| |
| /* |
| * The index of the front Debugger.Frame's debugger in debuggers. |
| * nextDebugger < debuggerCount if and only if the range is not empty. |
| */ |
| size_t debuggerCount, nextDebugger; |
| |
| /* |
| * If the range is not empty, this is front Debugger.Frame's entry in its |
| * debugger's frame table. |
| */ |
| FrameMap::Ptr entry; |
| |
| public: |
| /* |
| * Return a range containing all Debugger.Frame instances referring to |
| * |fp|. |global| is |fp|'s global object; if nullptr or omitted, we |
| * compute it ourselves from |fp|. |
| * |
| * We keep an index into the compartment's debugger list, and a |
| * FrameMap::Ptr into the current debugger's frame map. Thus, if the set of |
| * debuggers in |fp|'s compartment changes, this range becomes invalid. |
| * Similarly, if stack frames are added to or removed from frontDebugger(), |
| * then the range's front is invalid until popFront is called. |
| */ |
| explicit FrameRange(AbstractFramePtr frame, GlobalObject* global = nullptr) |
| : frame(frame) |
| { |
| nextDebugger = 0; |
| |
| /* Find our global, if we were not given one. */ |
| if (!global) |
| global = &frame.script()->global(); |
| |
| /* The frame and global must match. */ |
| MOZ_ASSERT(&frame.script()->global() == global); |
| |
| /* Find the list of debuggers we'll iterate over. There may be none. */ |
| debuggers = global->getDebuggers(); |
| if (debuggers) { |
| debuggerCount = debuggers->length(); |
| findNext(); |
| } else { |
| debuggerCount = 0; |
| } |
| } |
| |
| bool empty() const { |
| return nextDebugger >= debuggerCount; |
| } |
| |
| NativeObject* frontFrame() const { |
| MOZ_ASSERT(!empty()); |
| return entry->value(); |
| } |
| |
| Debugger* frontDebugger() const { |
| MOZ_ASSERT(!empty()); |
| return (*debuggers)[nextDebugger]; |
| } |
| |
| /* |
| * Delete the front frame from its Debugger's frame map. After this call, |
| * the range's front is invalid until popFront is called. |
| */ |
| void removeFrontFrame() const { |
| MOZ_ASSERT(!empty()); |
| frontDebugger()->frames.remove(entry); |
| } |
| |
| void popFront() { |
| MOZ_ASSERT(!empty()); |
| nextDebugger++; |
| findNext(); |
| } |
| |
| private: |
| /* |
| * Either make this range refer to the first appropriate Debugger.Frame at |
| * or after nextDebugger, or make it empty. |
| */ |
| void findNext() { |
| while (!empty()) { |
| Debugger* dbg = (*debuggers)[nextDebugger]; |
| entry = dbg->frames.lookup(frame); |
| if (entry) |
| break; |
| nextDebugger++; |
| } |
| } |
| }; |
| |
| |
| /*** Breakpoints *********************************************************************************/ |
| |
| BreakpointSite::BreakpointSite(JSScript* script, jsbytecode* pc) |
| : script(script), pc(pc), enabledCount(0) |
| { |
| MOZ_ASSERT(!script->hasBreakpointsAt(pc)); |
| JS_INIT_CLIST(&breakpoints); |
| } |
| |
| void |
| BreakpointSite::recompile(FreeOp* fop) |
| { |
| if (script->hasBaselineScript()) |
| script->baselineScript()->toggleDebugTraps(script, pc); |
| } |
| |
| void |
| BreakpointSite::inc(FreeOp* fop) |
| { |
| enabledCount++; |
| if (enabledCount == 1) |
| recompile(fop); |
| } |
| |
| void |
| BreakpointSite::dec(FreeOp* fop) |
| { |
| MOZ_ASSERT(enabledCount > 0); |
| enabledCount--; |
| if (enabledCount == 0) |
| recompile(fop); |
| } |
| |
| void |
| BreakpointSite::destroyIfEmpty(FreeOp* fop) |
| { |
| if (JS_CLIST_IS_EMPTY(&breakpoints)) |
| script->destroyBreakpointSite(fop, pc); |
| } |
| |
| Breakpoint* |
| BreakpointSite::firstBreakpoint() const |
| { |
| if (JS_CLIST_IS_EMPTY(&breakpoints)) |
| return nullptr; |
| return Breakpoint::fromSiteLinks(JS_NEXT_LINK(&breakpoints)); |
| } |
| |
| bool |
| BreakpointSite::hasBreakpoint(Breakpoint* bp) |
| { |
| for (Breakpoint* p = firstBreakpoint(); p; p = p->nextInSite()) |
| if (p == bp) |
| return true; |
| return false; |
| } |
| |
| Breakpoint::Breakpoint(Debugger* debugger, BreakpointSite* site, JSObject* handler) |
| : debugger(debugger), site(site), handler(handler) |
| { |
| MOZ_ASSERT(handler->compartment() == debugger->object->compartment()); |
| JS_APPEND_LINK(&debuggerLinks, &debugger->breakpoints); |
| JS_APPEND_LINK(&siteLinks, &site->breakpoints); |
| } |
| |
| Breakpoint* |
| Breakpoint::fromDebuggerLinks(JSCList* links) |
| { |
| return (Breakpoint*) ((unsigned char*) links - offsetof(Breakpoint, debuggerLinks)); |
| } |
| |
| Breakpoint* |
| Breakpoint::fromSiteLinks(JSCList* links) |
| { |
| return (Breakpoint*) ((unsigned char*) links - offsetof(Breakpoint, siteLinks)); |
| } |
| |
| void |
| Breakpoint::destroy(FreeOp* fop) |
| { |
| if (debugger->enabled) |
| site->dec(fop); |
| JS_REMOVE_LINK(&debuggerLinks); |
| JS_REMOVE_LINK(&siteLinks); |
| site->destroyIfEmpty(fop); |
| fop->delete_(this); |
| } |
| |
| Breakpoint* |
| Breakpoint::nextInDebugger() |
| { |
| JSCList* link = JS_NEXT_LINK(&debuggerLinks); |
| return (link == &debugger->breakpoints) ? nullptr : fromDebuggerLinks(link); |
| } |
| |
| Breakpoint* |
| Breakpoint::nextInSite() |
| { |
| JSCList* link = JS_NEXT_LINK(&siteLinks); |
| return (link == &site->breakpoints) ? nullptr : fromSiteLinks(link); |
| } |
| |
| |
| /*** Debugger hook dispatch **********************************************************************/ |
| |
| Debugger::Debugger(JSContext* cx, NativeObject* dbg) |
| : object(dbg), |
| uncaughtExceptionHook(nullptr), |
| enabled(true), |
| allowUnobservedAsmJS(false), |
| collectCoverageInfo(false), |
| observedGCs(cx), |
| tenurePromotionsLog(cx), |
| trackingTenurePromotions(false), |
| maxTenurePromotionsLogLength(DEFAULT_MAX_LOG_LENGTH), |
| tenurePromotionsLogOverflowed(false), |
| allocationsLog(cx), |
| trackingAllocationSites(false), |
| allocationSamplingProbability(1.0), |
| maxAllocationsLogLength(DEFAULT_MAX_LOG_LENGTH), |
| allocationsLogOverflowed(false), |
| frames(cx->runtime()), |
| scripts(cx), |
| sources(cx), |
| objects(cx), |
| environments(cx), |
| #ifdef NIGHTLY_BUILD |
| traceLoggerLastDrainedSize(0), |
| traceLoggerLastDrainedIteration(0), |
| #endif |
| traceLoggerScriptedCallsLastDrainedSize(0), |
| traceLoggerScriptedCallsLastDrainedIteration(0) |
| { |
| assertSameCompartment(cx, dbg); |
| |
| cx->runtime()->debuggerList.insertBack(this); |
| JS_INIT_CLIST(&breakpoints); |
| JS_INIT_CLIST(&onNewGlobalObjectWatchersLink); |
| } |
| |
| Debugger::~Debugger() |
| { |
| MOZ_ASSERT_IF(debuggees.initialized(), debuggees.empty()); |
| allocationsLog.clear(); |
| tenurePromotionsLog.clear(); |
| |
| /* |
| * Since the inactive state for this link is a singleton cycle, it's always |
| * safe to apply JS_REMOVE_LINK to it, regardless of whether we're in the list or not. |
| * |
| * We don't have to worry about locking here since Debugger is not |
| * background finalized. |
| */ |
| JS_REMOVE_LINK(&onNewGlobalObjectWatchersLink); |
| } |
| |
| bool |
| Debugger::init(JSContext* cx) |
| { |
| bool ok = debuggees.init() && |
| debuggeeZones.init() && |
| frames.init() && |
| scripts.init() && |
| sources.init() && |
| objects.init() && |
| observedGCs.init() && |
| environments.init(); |
| if (!ok) |
| ReportOutOfMemory(cx); |
| return ok; |
| } |
| |
| JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGSCRIPT_OWNER)); |
| JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGSOURCE_OWNER)); |
| JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGOBJECT_OWNER)); |
| JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGENV_OWNER)); |
| |
| /* static */ Debugger* |
| Debugger::fromChildJSObject(JSObject* obj) |
| { |
| MOZ_ASSERT(obj->getClass() == &DebuggerFrame_class || |
| obj->getClass() == &DebuggerScript_class || |
| obj->getClass() == &DebuggerSource_class || |
| obj->getClass() == &DebuggerObject_class || |
| obj->getClass() == &DebuggerEnv_class); |
| JSObject* dbgobj = &obj->as<NativeObject>().getReservedSlot(JSSLOT_DEBUGOBJECT_OWNER).toObject(); |
| return fromJSObject(dbgobj); |
| } |
| |
| bool |
| Debugger::hasMemory() const |
| { |
| return object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE).isObject(); |
| } |
| |
| DebuggerMemory& |
| Debugger::memory() const |
| { |
| MOZ_ASSERT(hasMemory()); |
| return object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE).toObject().as<DebuggerMemory>(); |
| } |
| |
| bool |
| Debugger::getScriptFrameWithIter(JSContext* cx, AbstractFramePtr frame, |
| const ScriptFrameIter* maybeIter, MutableHandleValue vp) |
| { |
| MOZ_ASSERT_IF(maybeIter, maybeIter->abstractFramePtr() == frame); |
| MOZ_ASSERT(!frame.script()->selfHosted()); |
| |
| FrameMap::AddPtr p = frames.lookupForAdd(frame); |
| if (!p) { |
| /* Create and populate the Debugger.Frame object. */ |
| RootedObject proto(cx, &object->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO).toObject()); |
| RootedNativeObject frameobj(cx, NewNativeObjectWithGivenProto(cx, &DebuggerFrame_class, |
| proto)); |
| if (!frameobj) |
| return false; |
| |
| // Eagerly copy ScriptFrameIter data if we've already walked the |
| // stack. |
| if (maybeIter) { |
| AbstractFramePtr data = maybeIter->copyDataAsAbstractFramePtr(); |
| if (!data) |
| return false; |
| frameobj->setPrivate(data.raw()); |
| } else { |
| frameobj->setPrivate(frame.raw()); |
| } |
| |
| frameobj->setReservedSlot(JSSLOT_DEBUGFRAME_OWNER, ObjectValue(*object)); |
| |
| if (!ensureExecutionObservabilityOfFrame(cx, frame)) |
| return false; |
| |
| if (!frames.add(p, frame, frameobj)) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| } |
| vp.setObject(*p->value()); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::hasLiveHook(GlobalObject* global, Hook which) |
| { |
| if (GlobalObject::DebuggerVector* debuggers = global->getDebuggers()) { |
| for (Debugger** p = debuggers->begin(); p != debuggers->end(); p++) { |
| Debugger* dbg = *p; |
| if (dbg->enabled && dbg->getHook(which)) |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| JSObject* |
| Debugger::getHook(Hook hook) const |
| { |
| MOZ_ASSERT(hook >= 0 && hook < HookCount); |
| const Value& v = object->getReservedSlot(JSSLOT_DEBUG_HOOK_START + hook); |
| return v.isUndefined() ? nullptr : &v.toObject(); |
| } |
| |
| bool |
| Debugger::hasAnyLiveHooks() const |
| { |
| if (!enabled) |
| return false; |
| |
| if (getHook(OnDebuggerStatement) || |
| getHook(OnExceptionUnwind) || |
| getHook(OnNewScript) || |
| getHook(OnEnterFrame)) |
| { |
| return true; |
| } |
| |
| /* If any breakpoints are in live scripts, return true. */ |
| for (Breakpoint* bp = firstBreakpoint(); bp; bp = bp->nextInDebugger()) { |
| if (IsMarkedUnbarriered(&bp->site->script)) |
| return true; |
| } |
| |
| for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) { |
| NativeObject* frameObj = r.front().value(); |
| if (!frameObj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined() || |
| !frameObj->getReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER).isUndefined()) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /* static */ JSTrapStatus |
| Debugger::slowPathOnEnterFrame(JSContext* cx, AbstractFramePtr frame) |
| { |
| RootedValue rval(cx); |
| JSTrapStatus status = dispatchHook( |
| cx, |
| [frame](Debugger* dbg) -> bool { |
| return dbg->observesFrame(frame) && dbg->observesEnterFrame(); |
| }, |
| [&](Debugger* dbg) -> JSTrapStatus { |
| return dbg->fireEnterFrame(cx, frame, &rval); |
| }); |
| |
| switch (status) { |
| case JSTRAP_CONTINUE: |
| break; |
| |
| case JSTRAP_THROW: |
| cx->setPendingException(rval); |
| break; |
| |
| case JSTRAP_ERROR: |
| cx->clearPendingException(); |
| break; |
| |
| case JSTRAP_RETURN: |
| frame.setReturnValue(rval); |
| break; |
| |
| default: |
| MOZ_CRASH("bad Debugger::onEnterFrame JSTrapStatus value"); |
| } |
| |
| return status; |
| } |
| |
| static void |
| DebuggerFrame_maybeDecrementFrameScriptStepModeCount(FreeOp* fop, AbstractFramePtr frame, |
| NativeObject* frameobj); |
| |
| static void |
| DebuggerFrame_freeScriptFrameIterData(FreeOp* fop, JSObject* obj); |
| |
| /* |
| * Handle leaving a frame with debuggers watching. |frameOk| indicates whether |
| * the frame is exiting normally or abruptly. Set |cx|'s exception and/or |
| * |cx->fp()|'s return value, and return a new success value. |
| */ |
| /* static */ bool |
| Debugger::slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame, bool frameOk) |
| { |
| Handle<GlobalObject*> global = cx->global(); |
| |
| // The onPop handler and associated clean up logic should not run multiple |
| // times on the same frame. If slowPathOnLeaveFrame has already been |
| // called, the frame will not be present in the Debugger frame maps. |
| FrameRange frameRange(frame, global); |
| if (frameRange.empty()) |
| return frameOk; |
| |
| auto frameMapsGuard = MakeScopeExit([&] { |
| // Clean up all Debugger.Frame instances. This call creates a fresh |
| // FrameRange, as one debugger's onPop handler could have caused another |
| // debugger to create its own Debugger.Frame instance. |
| removeFromFrameMapsAndClearBreakpointsIn(cx, frame); |
| }); |
| |
| /* Save the frame's completion value. */ |
| JSTrapStatus status; |
| RootedValue value(cx); |
| Debugger::resultToCompletion(cx, frameOk, frame.returnValue(), &status, &value); |
| |
| // This path can be hit via unwinding the stack due to over-recursion or |
| // OOM. In those cases, don't fire the frames' onPop handlers, because |
| // invoking JS will only trigger the same condition. See |
| // slowPathOnExceptionUnwind. |
| if (!cx->isThrowingOverRecursed() && !cx->isThrowingOutOfMemory()) { |
| /* Build a list of the recipients. */ |
| AutoObjectVector frames(cx); |
| for (; !frameRange.empty(); frameRange.popFront()) { |
| if (!frames.append(frameRange.frontFrame())) { |
| cx->clearPendingException(); |
| return false; |
| } |
| } |
| |
| /* For each Debugger.Frame, fire its onPop handler, if any. */ |
| for (JSObject** p = frames.begin(); p != frames.end(); p++) { |
| RootedNativeObject frameobj(cx, &(*p)->as<NativeObject>()); |
| Debugger* dbg = Debugger::fromChildJSObject(frameobj); |
| |
| if (dbg->enabled && |
| !frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER).isUndefined()) { |
| RootedValue handler(cx, frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER)); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, dbg->object); |
| |
| RootedValue completion(cx); |
| if (!dbg->newCompletionValue(cx, status, value, &completion)) { |
| status = dbg->handleUncaughtException(ac, false); |
| break; |
| } |
| |
| /* Call the onPop handler. */ |
| RootedValue rval(cx); |
| bool hookOk = Invoke(cx, ObjectValue(*frameobj), handler, 1, completion.address(), |
| &rval); |
| RootedValue nextValue(cx); |
| JSTrapStatus nextStatus = dbg->parseResumptionValue(ac, hookOk, rval, &nextValue); |
| |
| /* |
| * At this point, we are back in the debuggee compartment, and any error has |
| * been wrapped up as a completion value. |
| */ |
| MOZ_ASSERT(cx->compartment() == global->compartment()); |
| MOZ_ASSERT(!cx->isExceptionPending()); |
| |
| /* JSTRAP_CONTINUE means "make no change". */ |
| if (nextStatus != JSTRAP_CONTINUE) { |
| status = nextStatus; |
| value = nextValue; |
| } |
| } |
| } |
| } |
| |
| /* Establish (status, value) as our resumption value. */ |
| switch (status) { |
| case JSTRAP_RETURN: |
| frame.setReturnValue(value); |
| return true; |
| |
| case JSTRAP_THROW: |
| cx->setPendingException(value); |
| return false; |
| |
| case JSTRAP_ERROR: |
| MOZ_ASSERT(!cx->isExceptionPending()); |
| return false; |
| |
| default: |
| MOZ_CRASH("bad final trap status"); |
| } |
| } |
| |
| /* static */ JSTrapStatus |
| Debugger::slowPathOnDebuggerStatement(JSContext* cx, AbstractFramePtr frame) |
| { |
| RootedValue rval(cx); |
| JSTrapStatus status = dispatchHook( |
| cx, |
| [](Debugger* dbg) -> bool { return dbg->getHook(OnDebuggerStatement); }, |
| [&](Debugger* dbg) -> JSTrapStatus { |
| return dbg->fireDebuggerStatement(cx, &rval); |
| }); |
| |
| switch (status) { |
| case JSTRAP_CONTINUE: |
| case JSTRAP_ERROR: |
| break; |
| |
| case JSTRAP_RETURN: |
| frame.setReturnValue(rval); |
| break; |
| |
| case JSTRAP_THROW: |
| cx->setPendingException(rval); |
| break; |
| |
| default: |
| MOZ_CRASH("Invalid onDebuggerStatement trap status"); |
| } |
| |
| return status; |
| } |
| |
| /* static */ JSTrapStatus |
| Debugger::slowPathOnExceptionUnwind(JSContext* cx, AbstractFramePtr frame) |
| { |
| // Invoking more JS on an over-recursed stack or after OOM is only going |
| // to result in more of the same error. |
| if (cx->isThrowingOverRecursed() || cx->isThrowingOutOfMemory()) |
| return JSTRAP_CONTINUE; |
| |
| // The Debugger API mustn't muck with frames from self-hosted scripts. |
| if (frame.script()->selfHosted()) |
| return JSTRAP_CONTINUE; |
| |
| RootedValue rval(cx); |
| JSTrapStatus status = dispatchHook( |
| cx, |
| [](Debugger* dbg) -> bool { return dbg->getHook(OnExceptionUnwind); }, |
| [&](Debugger* dbg) -> JSTrapStatus { |
| return dbg->fireExceptionUnwind(cx, &rval); |
| }); |
| |
| switch (status) { |
| case JSTRAP_CONTINUE: |
| break; |
| |
| case JSTRAP_THROW: |
| cx->setPendingException(rval); |
| break; |
| |
| case JSTRAP_ERROR: |
| cx->clearPendingException(); |
| break; |
| |
| case JSTRAP_RETURN: |
| cx->clearPendingException(); |
| frame.setReturnValue(rval); |
| break; |
| |
| default: |
| MOZ_CRASH("Invalid onExceptionUnwind trap status"); |
| } |
| |
| return status; |
| } |
| |
| bool |
| Debugger::wrapEnvironment(JSContext* cx, Handle<Env*> env, MutableHandleValue rval) |
| { |
| if (!env) { |
| rval.setNull(); |
| return true; |
| } |
| |
| /* |
| * DebuggerEnv should only wrap a debug scope chain obtained (transitively) |
| * from GetDebugScopeFor(Frame|Function). |
| */ |
| MOZ_ASSERT(!IsSyntacticScope(env)); |
| |
| NativeObject* envobj; |
| DependentAddPtr<ObjectWeakMap> p(cx, environments, env); |
| if (p) { |
| envobj = &p->value()->as<NativeObject>(); |
| } else { |
| /* Create a new Debugger.Environment for env. */ |
| RootedObject proto(cx, &object->getReservedSlot(JSSLOT_DEBUG_ENV_PROTO).toObject()); |
| envobj = NewNativeObjectWithGivenProto(cx, &DebuggerEnv_class, proto, |
| TenuredObject); |
| if (!envobj) |
| return false; |
| envobj->setPrivateGCThing(env); |
| envobj->setReservedSlot(JSSLOT_DEBUGENV_OWNER, ObjectValue(*object)); |
| if (!p.add(cx, environments, env, envobj)) |
| return false; |
| |
| CrossCompartmentKey key(CrossCompartmentKey::DebuggerEnvironment, object, env); |
| if (!object->compartment()->putWrapper(cx, key, ObjectValue(*envobj))) { |
| environments.remove(env); |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| } |
| rval.setObject(*envobj); |
| return true; |
| } |
| |
| bool |
| Debugger::wrapDebuggeeValue(JSContext* cx, MutableHandleValue vp) |
| { |
| assertSameCompartment(cx, object.get()); |
| |
| if (vp.isObject()) { |
| RootedObject obj(cx, &vp.toObject()); |
| |
| if (obj->is<JSFunction>()) { |
| MOZ_ASSERT(!IsInternalFunctionObject(*obj)); |
| RootedFunction fun(cx, &obj->as<JSFunction>()); |
| if (!EnsureFunctionHasScript(cx, fun)) |
| return false; |
| } |
| |
| DependentAddPtr<ObjectWeakMap> p(cx, objects, obj); |
| if (p) { |
| vp.setObject(*p->value()); |
| } else { |
| /* Create a new Debugger.Object for obj. */ |
| RootedObject proto(cx, &object->getReservedSlot(JSSLOT_DEBUG_OBJECT_PROTO).toObject()); |
| NativeObject* dobj = |
| NewNativeObjectWithGivenProto(cx, &DebuggerObject_class, proto, |
| TenuredObject); |
| if (!dobj) |
| return false; |
| dobj->setPrivateGCThing(obj); |
| dobj->setReservedSlot(JSSLOT_DEBUGOBJECT_OWNER, ObjectValue(*object)); |
| |
| if (!p.add(cx, objects, obj, dobj)) |
| return false; |
| |
| if (obj->compartment() != object->compartment()) { |
| CrossCompartmentKey key(CrossCompartmentKey::DebuggerObject, object, obj); |
| if (!object->compartment()->putWrapper(cx, key, ObjectValue(*dobj))) { |
| objects.remove(obj); |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| } |
| |
| vp.setObject(*dobj); |
| } |
| } else if (vp.isMagic()) { |
| RootedPlainObject optObj(cx, NewBuiltinClassInstance<PlainObject>(cx)); |
| if (!optObj) |
| return false; |
| |
| // We handle three sentinel values: missing arguments (overloading |
| // JS_OPTIMIZED_ARGUMENTS), optimized out slots (JS_OPTIMIZED_OUT), |
| // and uninitialized bindings (JS_UNINITIALIZED_LEXICAL). |
| // |
| // Other magic values should not have escaped. |
| PropertyName* name; |
| switch (vp.whyMagic()) { |
| case JS_OPTIMIZED_ARGUMENTS: name = cx->names().missingArguments; break; |
| case JS_OPTIMIZED_OUT: name = cx->names().optimizedOut; break; |
| case JS_UNINITIALIZED_LEXICAL: name = cx->names().uninitialized; break; |
| default: MOZ_CRASH("Unsupported magic value escaped to Debugger"); |
| } |
| |
| RootedValue trueVal(cx, BooleanValue(true)); |
| if (!DefineProperty(cx, optObj, name, trueVal)) |
| return false; |
| |
| vp.setObject(*optObj); |
| } else if (!cx->compartment()->wrap(cx, vp)) { |
| vp.setUndefined(); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool |
| Debugger::unwrapDebuggeeObject(JSContext* cx, MutableHandleObject obj) |
| { |
| if (obj->getClass() != &DebuggerObject_class) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, |
| "Debugger", "Debugger.Object", obj->getClass()->name); |
| return false; |
| } |
| NativeObject* ndobj = &obj->as<NativeObject>(); |
| |
| Value owner = ndobj->getReservedSlot(JSSLOT_DEBUGOBJECT_OWNER); |
| if (owner.isUndefined() || &owner.toObject() != object) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, |
| owner.isUndefined() |
| ? JSMSG_DEBUG_OBJECT_PROTO |
| : JSMSG_DEBUG_OBJECT_WRONG_OWNER); |
| return false; |
| } |
| |
| obj.set(static_cast<JSObject*>(ndobj->getPrivate())); |
| return true; |
| } |
| |
| bool |
| Debugger::unwrapDebuggeeValue(JSContext* cx, MutableHandleValue vp) |
| { |
| assertSameCompartment(cx, object.get(), vp); |
| if (vp.isObject()) { |
| RootedObject dobj(cx, &vp.toObject()); |
| if (!unwrapDebuggeeObject(cx, &dobj)) |
| return false; |
| vp.setObject(*dobj); |
| } |
| return true; |
| } |
| |
| static bool |
| CheckArgCompartment(JSContext* cx, JSObject* obj, JSObject* arg, |
| const char* methodname, const char* propname) |
| { |
| if (arg->compartment() != obj->compartment()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_COMPARTMENT_MISMATCH, |
| methodname, propname); |
| return false; |
| } |
| return true; |
| } |
| |
| static bool |
| CheckArgCompartment(JSContext* cx, JSObject* obj, HandleValue v, |
| const char* methodname, const char* propname) |
| { |
| if (v.isObject()) |
| return CheckArgCompartment(cx, obj, &v.toObject(), methodname, propname); |
| return true; |
| } |
| |
| bool |
| Debugger::unwrapPropertyDescriptor(JSContext* cx, HandleObject obj, |
| MutableHandle<PropertyDescriptor> desc) |
| { |
| if (desc.hasValue()) { |
| RootedValue value(cx, desc.value()); |
| if (!unwrapDebuggeeValue(cx, &value) || |
| !CheckArgCompartment(cx, obj, value, "defineProperty", "value")) |
| { |
| return false; |
| } |
| desc.setValue(value); |
| } |
| |
| if (desc.hasGetterObject()) { |
| RootedObject get(cx, desc.getterObject()); |
| if (get) { |
| if (!unwrapDebuggeeObject(cx, &get)) |
| return false; |
| if (!CheckArgCompartment(cx, obj, get, "defineProperty", "get")) |
| return false; |
| } |
| desc.setGetterObject(get); |
| } |
| |
| if (desc.hasSetterObject()) { |
| RootedObject set(cx, desc.setterObject()); |
| if (set) { |
| if (!unwrapDebuggeeObject(cx, &set)) |
| return false; |
| if (!CheckArgCompartment(cx, obj, set, "defineProperty", "set")) |
| return false; |
| } |
| desc.setSetterObject(set); |
| } |
| |
| return true; |
| } |
| |
| namespace { |
| class MOZ_STACK_CLASS ReportExceptionClosure : public ScriptEnvironmentPreparer::Closure |
| { |
| public: |
| explicit ReportExceptionClosure(RootedValue& exn) |
| : exn_(exn) |
| { |
| } |
| |
| bool operator()(JSContext* cx) override |
| { |
| cx->setPendingException(exn_); |
| return false; |
| } |
| |
| private: |
| RootedValue& exn_; |
| }; |
| } // anonymous namespace |
| |
| JSTrapStatus |
| Debugger::handleUncaughtExceptionHelper(Maybe<AutoCompartment>& ac, |
| MutableHandleValue* vp, bool callHook) |
| { |
| JSContext* cx = ac->context()->asJSContext(); |
| if (cx->isExceptionPending()) { |
| if (callHook && uncaughtExceptionHook) { |
| RootedValue exc(cx); |
| if (!cx->getPendingException(&exc)) |
| return JSTRAP_ERROR; |
| cx->clearPendingException(); |
| RootedValue fval(cx, ObjectValue(*uncaughtExceptionHook)); |
| RootedValue rv(cx); |
| if (Invoke(cx, ObjectValue(*object), fval, 1, exc.address(), &rv)) |
| return vp ? parseResumptionValue(ac, true, rv, *vp, false) : JSTRAP_CONTINUE; |
| } |
| |
| if (cx->isExceptionPending()) { |
| /* |
| * We want to report the pending exception, but we want to let the |
| * embedding handle it however it wants to. So pretend like we're |
| * starting a new script execution on our current compartment (which |
| * is the debugger compartment, so reported errors won't get |
| * reported to various onerror handlers in debuggees) and as part of |
| * that "execution" simply throw our exception so the embedding can |
| * deal. |
| */ |
| RootedValue exn(cx); |
| if (cx->getPendingException(&exn)) { |
| /* |
| * Clear the exception, because |
| * PrepareScriptEnvironmentAndInvoke will assert that we don't |
| * have one. |
| */ |
| cx->clearPendingException(); |
| ReportExceptionClosure reportExn(exn); |
| PrepareScriptEnvironmentAndInvoke(cx, cx->global(), reportExn); |
| } |
| /* |
| * And if not, or if PrepareScriptEnvironmentAndInvoke somehow left |
| * an exception on cx (which it totally shouldn't do), just give |
| * up. |
| */ |
| cx->clearPendingException(); |
| } |
| } |
| ac.reset(); |
| return JSTRAP_ERROR; |
| } |
| |
| JSTrapStatus |
| Debugger::handleUncaughtException(Maybe<AutoCompartment>& ac, MutableHandleValue vp, bool callHook) |
| { |
| return handleUncaughtExceptionHelper(ac, &vp, callHook); |
| } |
| |
| JSTrapStatus |
| Debugger::handleUncaughtException(Maybe<AutoCompartment>& ac, bool callHook) |
| { |
| return handleUncaughtExceptionHelper(ac, nullptr, callHook); |
| } |
| |
| /* static */ void |
| Debugger::resultToCompletion(JSContext* cx, bool ok, const Value& rv, |
| JSTrapStatus* status, MutableHandleValue value) |
| { |
| MOZ_ASSERT_IF(ok, !cx->isExceptionPending()); |
| |
| if (ok) { |
| *status = JSTRAP_RETURN; |
| value.set(rv); |
| } else if (cx->isExceptionPending()) { |
| *status = JSTRAP_THROW; |
| if (!cx->getPendingException(value)) |
| *status = JSTRAP_ERROR; |
| cx->clearPendingException(); |
| } else { |
| *status = JSTRAP_ERROR; |
| value.setUndefined(); |
| } |
| } |
| |
| bool |
| Debugger::newCompletionValue(JSContext* cx, JSTrapStatus status, Value value_, |
| MutableHandleValue result) |
| { |
| /* |
| * We must be in the debugger's compartment, since that's where we want |
| * to construct the completion value. |
| */ |
| assertSameCompartment(cx, object.get()); |
| |
| RootedId key(cx); |
| RootedValue value(cx, value_); |
| |
| switch (status) { |
| case JSTRAP_RETURN: |
| key = NameToId(cx->names().return_); |
| break; |
| |
| case JSTRAP_THROW: |
| key = NameToId(cx->names().throw_); |
| break; |
| |
| case JSTRAP_ERROR: |
| result.setNull(); |
| return true; |
| |
| default: |
| MOZ_CRASH("bad status passed to Debugger::newCompletionValue"); |
| } |
| |
| /* Common tail for JSTRAP_RETURN and JSTRAP_THROW. */ |
| RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx)); |
| if (!obj || |
| !wrapDebuggeeValue(cx, &value) || |
| !NativeDefineProperty(cx, obj, key, value, nullptr, nullptr, JSPROP_ENUMERATE)) |
| { |
| return false; |
| } |
| |
| result.setObject(*obj); |
| return true; |
| } |
| |
| bool |
| Debugger::receiveCompletionValue(Maybe<AutoCompartment>& ac, bool ok, |
| HandleValue val, |
| MutableHandleValue vp) |
| { |
| JSContext* cx = ac->context()->asJSContext(); |
| |
| JSTrapStatus status; |
| RootedValue value(cx); |
| resultToCompletion(cx, ok, val, &status, &value); |
| ac.reset(); |
| return newCompletionValue(cx, status, value, vp); |
| } |
| |
| static bool |
| GetStatusProperty(JSContext* cx, HandleObject obj, HandlePropertyName name, JSTrapStatus status, |
| JSTrapStatus* statusOut, MutableHandleValue vp, int* hits) |
| { |
| bool found; |
| if (!HasProperty(cx, obj, name, &found)) |
| return false; |
| if (found) { |
| ++*hits; |
| *statusOut = status; |
| if (!GetProperty(cx, obj, obj, name, vp)) |
| return false; |
| } |
| return true; |
| } |
| |
| static bool |
| ParseResumptionValueAsObject(JSContext* cx, HandleValue rv, JSTrapStatus* statusp, |
| MutableHandleValue vp) |
| { |
| int hits = 0; |
| if (rv.isObject()) { |
| RootedObject obj(cx, &rv.toObject()); |
| if (!GetStatusProperty(cx, obj, cx->names().return_, JSTRAP_RETURN, statusp, vp, &hits)) |
| return false; |
| if (!GetStatusProperty(cx, obj, cx->names().throw_, JSTRAP_THROW, statusp, vp, &hits)) |
| return false; |
| } |
| |
| if (hits != 1) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_BAD_RESUMPTION); |
| return false; |
| } |
| return true; |
| } |
| |
| JSTrapStatus |
| Debugger::parseResumptionValue(Maybe<AutoCompartment>& ac, bool ok, const Value& rv, MutableHandleValue vp, |
| bool callHook) |
| { |
| vp.setUndefined(); |
| if (!ok) |
| return handleUncaughtException(ac, vp, callHook); |
| if (rv.isUndefined()) { |
| ac.reset(); |
| return JSTRAP_CONTINUE; |
| } |
| if (rv.isNull()) { |
| ac.reset(); |
| return JSTRAP_ERROR; |
| } |
| |
| JSContext* cx = ac->context()->asJSContext(); |
| JSTrapStatus status = JSTRAP_CONTINUE; |
| RootedValue v(cx); |
| RootedValue rvRoot(cx, rv); |
| if (!ParseResumptionValueAsObject(cx, rvRoot, &status, &v) || |
| !unwrapDebuggeeValue(cx, &v)) |
| { |
| return handleUncaughtException(ac, vp, callHook); |
| } |
| |
| ac.reset(); |
| if (!cx->compartment()->wrap(cx, &v)) { |
| vp.setUndefined(); |
| return JSTRAP_ERROR; |
| } |
| vp.set(v); |
| |
| return status; |
| } |
| |
| static bool |
| CallMethodIfPresent(JSContext* cx, HandleObject obj, const char* name, int argc, Value* argv, |
| MutableHandleValue rval) |
| { |
| rval.setUndefined(); |
| JSAtom* atom = Atomize(cx, name, strlen(name)); |
| if (!atom) |
| return false; |
| |
| RootedId id(cx, AtomToId(atom)); |
| RootedValue fval(cx); |
| return GetProperty(cx, obj, obj, id, &fval) && |
| (!IsCallable(fval) || Invoke(cx, ObjectValue(*obj), fval, argc, argv, rval)); |
| } |
| |
| JSTrapStatus |
| Debugger::fireDebuggerStatement(JSContext* cx, MutableHandleValue vp) |
| { |
| RootedObject hook(cx, getHook(OnDebuggerStatement)); |
| MOZ_ASSERT(hook); |
| MOZ_ASSERT(hook->isCallable()); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, object); |
| |
| ScriptFrameIter iter(cx); |
| RootedValue scriptFrame(cx); |
| if (!getScriptFrame(cx, iter, &scriptFrame)) |
| return handleUncaughtException(ac, false); |
| |
| RootedValue rv(cx); |
| bool ok = Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, scriptFrame.address(), &rv); |
| return parseResumptionValue(ac, ok, rv, vp); |
| } |
| |
| JSTrapStatus |
| Debugger::fireExceptionUnwind(JSContext* cx, MutableHandleValue vp) |
| { |
| RootedObject hook(cx, getHook(OnExceptionUnwind)); |
| MOZ_ASSERT(hook); |
| MOZ_ASSERT(hook->isCallable()); |
| |
| RootedValue exc(cx); |
| if (!cx->getPendingException(&exc)) |
| return JSTRAP_ERROR; |
| cx->clearPendingException(); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, object); |
| |
| JS::AutoValueArray<2> argv(cx); |
| argv[0].setUndefined(); |
| argv[1].set(exc); |
| |
| ScriptFrameIter iter(cx); |
| if (!getScriptFrame(cx, iter, argv[0]) || !wrapDebuggeeValue(cx, argv[1])) |
| return handleUncaughtException(ac, false); |
| |
| RootedValue rv(cx); |
| bool ok = Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 2, argv.begin(), &rv); |
| JSTrapStatus st = parseResumptionValue(ac, ok, rv, vp); |
| if (st == JSTRAP_CONTINUE) |
| cx->setPendingException(exc); |
| return st; |
| } |
| |
| JSTrapStatus |
| Debugger::fireEnterFrame(JSContext* cx, AbstractFramePtr frame, MutableHandleValue vp) |
| { |
| RootedObject hook(cx, getHook(OnEnterFrame)); |
| MOZ_ASSERT(hook); |
| MOZ_ASSERT(hook->isCallable()); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, object); |
| |
| RootedValue scriptFrame(cx); |
| if (!getScriptFrame(cx, frame, &scriptFrame)) |
| return handleUncaughtException(ac, false); |
| |
| RootedValue rv(cx); |
| bool ok = Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, scriptFrame.address(), &rv); |
| return parseResumptionValue(ac, ok, rv, vp); |
| } |
| |
| void |
| Debugger::fireNewScript(JSContext* cx, HandleScript script) |
| { |
| RootedObject hook(cx, getHook(OnNewScript)); |
| MOZ_ASSERT(hook); |
| MOZ_ASSERT(hook->isCallable()); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, object); |
| |
| JSObject* dsobj = wrapScript(cx, script); |
| if (!dsobj) { |
| handleUncaughtException(ac, false); |
| return; |
| } |
| |
| RootedValue scriptObject(cx, ObjectValue(*dsobj)); |
| RootedValue rv(cx); |
| if (!Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, scriptObject.address(), &rv)) |
| handleUncaughtException(ac, true); |
| } |
| |
| void |
| Debugger::fireOnGarbageCollectionHook(JSContext* cx, |
| const JS::dbg::GarbageCollectionEvent::Ptr& gcData) |
| { |
| MOZ_ASSERT(observedGC(gcData->majorGCNumber())); |
| observedGCs.remove(gcData->majorGCNumber()); |
| |
| RootedObject hook(cx, getHook(OnGarbageCollection)); |
| MOZ_ASSERT(hook); |
| MOZ_ASSERT(hook->isCallable()); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, object); |
| |
| JSObject* dataObj = gcData->toJSObject(cx); |
| if (!dataObj) { |
| handleUncaughtException(ac, false); |
| return; |
| } |
| |
| RootedValue dataVal(cx, ObjectValue(*dataObj)); |
| RootedValue rv(cx); |
| if (!Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, dataVal.address(), &rv)) |
| handleUncaughtException(ac, true); |
| } |
| |
| JSTrapStatus |
| Debugger::fireOnIonCompilationHook(JSContext* cx, Handle<ScriptVector> scripts, LSprinter& graph) |
| { |
| RootedObject hook(cx, getHook(OnIonCompilation)); |
| MOZ_ASSERT(hook); |
| MOZ_ASSERT(hook->isCallable()); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, object); |
| |
| // Copy the vector of scripts to a JS Array of Debugger.Script |
| RootedObject tmpObj(cx); |
| RootedValue tmpVal(cx); |
| AutoValueVector dbgScripts(cx); |
| for (size_t i = 0; i < scripts.length(); i++) { |
| tmpObj = wrapScript(cx, scripts[i]); |
| if (!tmpObj) |
| return handleUncaughtException(ac, false); |
| |
| tmpVal.setObject(*tmpObj); |
| if (!dbgScripts.append(tmpVal)) |
| return handleUncaughtException(ac, false); |
| } |
| |
| RootedObject dbgScriptsArray(cx, JS_NewArrayObject(cx, dbgScripts)); |
| if (!dbgScriptsArray) |
| return handleUncaughtException(ac, false); |
| |
| // Copy the JSON compilation graph to a JS String which is allocated as part |
| // of the Debugger compartment. |
| Sprinter jsonPrinter(cx); |
| if (!jsonPrinter.init()) |
| return handleUncaughtException(ac, false); |
| |
| graph.exportInto(jsonPrinter); |
| if (jsonPrinter.hadOutOfMemory()) |
| return handleUncaughtException(ac, false); |
| |
| RootedString json(cx, JS_NewStringCopyZ(cx, jsonPrinter.string())); |
| if (!json) |
| return handleUncaughtException(ac, false); |
| |
| // Create a JS Object which has the array of scripts, and the string of the |
| // JSON graph. |
| const char* names[] = { "scripts", "json" }; |
| JS::AutoValueArray<2> values(cx); |
| values[0].setObject(*dbgScriptsArray); |
| values[1].setString(json); |
| |
| RootedObject obj(cx, JS_NewObject(cx, nullptr)); |
| if (!obj) |
| return handleUncaughtException(ac, false); |
| |
| MOZ_ASSERT(mozilla::ArrayLength(names) == values.length()); |
| for (size_t i = 0; i < mozilla::ArrayLength(names); i++) { |
| if (!JS_DefineProperty(cx, obj, names[i], values[i], JSPROP_ENUMERATE, nullptr, nullptr)) |
| return handleUncaughtException(ac, false); |
| } |
| |
| // Call Debugger.onIonCompilation hook. |
| JS::AutoValueArray<1> argv(cx); |
| argv[0].setObject(*obj); |
| |
| RootedValue rv(cx); |
| if (!Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, argv.begin(), &rv)) |
| return handleUncaughtException(ac, true); |
| return JSTRAP_CONTINUE; |
| } |
| |
| template <typename HookIsEnabledFun /* bool (Debugger*) */, |
| typename FireHookFun /* JSTrapStatus (Debugger*) */> |
| /* static */ JSTrapStatus |
| Debugger::dispatchHook(JSContext* cx, HookIsEnabledFun hookIsEnabled, FireHookFun fireHook) |
| { |
| /* |
| * 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); |
| Handle<GlobalObject*> global = cx->global(); |
| if (GlobalObject::DebuggerVector* debuggers = global->getDebuggers()) { |
| for (Debugger** p = debuggers->begin(); p != debuggers->end(); p++) { |
| Debugger* dbg = *p; |
| if (dbg->enabled && hookIsEnabled(dbg)) { |
| 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++) { |
| Debugger* dbg = Debugger::fromJSObject(&p->toObject()); |
| if (dbg->debuggees.has(global) && dbg->enabled && hookIsEnabled(dbg)) { |
| JSTrapStatus st = fireHook(dbg); |
| if (st != JSTRAP_CONTINUE) |
| return st; |
| } |
| } |
| return JSTRAP_CONTINUE; |
| } |
| |
| void |
| Debugger::slowPathOnNewScript(JSContext* cx, HandleScript script) |
| { |
| JSTrapStatus status = dispatchHook( |
| cx, |
| [script](Debugger* dbg) -> bool { |
| return dbg->observesNewScript() && dbg->observesScript(script); |
| }, |
| [&](Debugger* dbg) -> JSTrapStatus { |
| dbg->fireNewScript(cx, script); |
| return JSTRAP_CONTINUE; |
| }); |
| |
| if (status == JSTRAP_ERROR) { |
| ReportOutOfMemory(cx); |
| return; |
| } |
| |
| MOZ_ASSERT(status == JSTRAP_CONTINUE); |
| } |
| |
| /* static */ JSTrapStatus |
| Debugger::onTrap(JSContext* cx, MutableHandleValue vp) |
| { |
| ScriptFrameIter iter(cx); |
| RootedScript script(cx, iter.script()); |
| MOZ_ASSERT(script->isDebuggee()); |
| Rooted<GlobalObject*> scriptGlobal(cx, &script->global()); |
| jsbytecode* pc = iter.pc(); |
| BreakpointSite* site = script->getBreakpointSite(pc); |
| JSOp op = JSOp(*pc); |
| |
| /* Build list of breakpoint handlers. */ |
| Vector<Breakpoint*> triggered(cx); |
| for (Breakpoint* bp = site->firstBreakpoint(); bp; bp = bp->nextInSite()) { |
| if (!triggered.append(bp)) |
| return JSTRAP_ERROR; |
| } |
| |
| for (Breakpoint** p = triggered.begin(); p != triggered.end(); p++) { |
| Breakpoint* bp = *p; |
| |
| /* Handlers can clear breakpoints. Check that bp still exists. */ |
| if (!site || !site->hasBreakpoint(bp)) |
| continue; |
| |
| /* |
| * There are two reasons we have to check whether dbg is enabled and |
| * debugging scriptGlobal. |
| * |
| * One is just that one breakpoint handler can disable other Debuggers |
| * or remove debuggees. |
| * |
| * The other has to do with non-compile-and-go scripts, which have no |
| * specific global--until they are executed. Only now do we know which |
| * global the script is running against. |
| */ |
| Debugger* dbg = bp->debugger; |
| bool hasDebuggee = dbg->enabled && dbg->debuggees.has(scriptGlobal); |
| if (hasDebuggee) { |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, dbg->object); |
| |
| RootedValue scriptFrame(cx); |
| if (!dbg->getScriptFrame(cx, iter, &scriptFrame)) |
| return dbg->handleUncaughtException(ac, false); |
| RootedValue rv(cx); |
| Rooted<JSObject*> handler(cx, bp->handler); |
| bool ok = CallMethodIfPresent(cx, handler, "hit", 1, scriptFrame.address(), &rv); |
| JSTrapStatus st = dbg->parseResumptionValue(ac, ok, rv, vp, true); |
| if (st != JSTRAP_CONTINUE) |
| return st; |
| |
| /* Calling JS code invalidates site. Reload it. */ |
| site = script->getBreakpointSite(pc); |
| } |
| } |
| |
| /* By convention, return the true op to the interpreter in vp. */ |
| vp.setInt32(op); |
| return JSTRAP_CONTINUE; |
| } |
| |
| /* static */ JSTrapStatus |
| Debugger::onSingleStep(JSContext* cx, MutableHandleValue vp) |
| { |
| ScriptFrameIter iter(cx); |
| |
| /* |
| * 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). |
| */ |
| RootedValue exception(cx, UndefinedValue()); |
| bool exceptionPending = cx->isExceptionPending(); |
| if (exceptionPending) { |
| if (!cx->getPendingException(&exception)) |
| return JSTRAP_ERROR; |
| cx->clearPendingException(); |
| } |
| |
| /* |
| * Build list of Debugger.Frame instances referring to this frame with |
| * onStep handlers. |
| */ |
| AutoObjectVector frames(cx); |
| for (FrameRange r(iter.abstractFramePtr()); !r.empty(); r.popFront()) { |
| NativeObject* frame = r.frontFrame(); |
| 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_t stepperCount = 0; |
| JSScript* trappingScript = iter.script(); |
| GlobalObject* global = cx->global(); |
| 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()) { |
| AbstractFramePtr frame = r.front().key(); |
| NativeObject* frameobj = r.front().value(); |
| if (frame.script() == trappingScript && |
| !frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined()) |
| { |
| stepperCount++; |
| } |
| } |
| } |
| } |
| MOZ_ASSERT(stepperCount == trappingScript->stepModeCount()); |
| } |
| #endif |
| |
| /* Call all the onStep handlers we found. */ |
| for (JSObject** p = frames.begin(); p != frames.end(); p++) { |
| RootedNativeObject frame(cx, &(*p)->as<NativeObject>()); |
| Debugger* dbg = Debugger::fromChildJSObject(frame); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, dbg->object); |
| |
| const Value& handler = frame->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER); |
| RootedValue rval(cx); |
| bool ok = Invoke(cx, ObjectValue(*frame), handler, 0, nullptr, &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; |
| } |
| |
| JSTrapStatus |
| Debugger::fireNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global, MutableHandleValue vp) |
| { |
| RootedObject hook(cx, getHook(OnNewGlobalObject)); |
| MOZ_ASSERT(hook); |
| MOZ_ASSERT(hook->isCallable()); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, object); |
| |
| RootedValue wrappedGlobal(cx, ObjectValue(*global)); |
| if (!wrapDebuggeeValue(cx, &wrappedGlobal)) |
| return handleUncaughtException(ac, false); |
| |
| RootedValue rv(cx); |
| |
| // onNewGlobalObject is infallible, and thus is only allowed to return |
| // undefined as a resumption value. If it returns anything else, we throw. |
| // And if that happens, or if the hook itself throws, we invoke the |
| // uncaughtExceptionHook so that we never leave an exception pending on the |
| // cx. This allows JS_NewGlobalObject to avoid handling failures from debugger |
| // hooks. |
| bool ok = Invoke(cx, ObjectValue(*object), ObjectValue(*hook), 1, wrappedGlobal.address(), &rv); |
| if (ok && !rv.isUndefined()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, |
| JSMSG_DEBUG_RESUMPTION_VALUE_DISALLOWED); |
| ok = false; |
| } |
| // NB: Even though we don't care about what goes into it, we have to pass vp |
| // to handleUncaughtException so that it parses resumption values from the |
| // uncaughtExceptionHook and tells the caller whether we should execute the |
| // rest of the onNewGlobalObject hooks or not. |
| JSTrapStatus status = ok ? JSTRAP_CONTINUE |
| : handleUncaughtException(ac, vp, true); |
| MOZ_ASSERT(!cx->isExceptionPending()); |
| return status; |
| } |
| |
| void |
| Debugger::slowPathOnNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global) |
| { |
| MOZ_ASSERT(!JS_CLIST_IS_EMPTY(&cx->runtime()->onNewGlobalObjectWatchers)); |
| if (global->compartment()->options().invisibleToDebugger()) |
| return; |
| |
| /* |
| * Make a copy of the runtime's onNewGlobalObjectWatchers before running the |
| * handlers. Since one Debugger's handler can disable another's, the list |
| * can be mutated while we're walking it. |
| */ |
| AutoObjectVector watchers(cx); |
| for (JSCList* link = JS_LIST_HEAD(&cx->runtime()->onNewGlobalObjectWatchers); |
| link != &cx->runtime()->onNewGlobalObjectWatchers; |
| link = JS_NEXT_LINK(link)) |
| { |
| Debugger* dbg = fromOnNewGlobalObjectWatchersLink(link); |
| MOZ_ASSERT(dbg->observesNewGlobalObject()); |
| JSObject* obj = dbg->object; |
| JS::ExposeObjectToActiveJS(obj); |
| if (!watchers.append(obj)) |
| return; |
| } |
| |
| JSTrapStatus status = JSTRAP_CONTINUE; |
| RootedValue value(cx); |
| |
| for (size_t i = 0; i < watchers.length(); i++) { |
| Debugger* dbg = fromJSObject(watchers[i]); |
| |
| // We disallow resumption values from onNewGlobalObject hooks, because we |
| // want the debugger hooks for global object creation to be infallible. |
| // But if an onNewGlobalObject hook throws, and the uncaughtExceptionHook |
| // decides to raise an error, we want to at least avoid invoking the rest |
| // of the onNewGlobalObject handlers in the list (not for any super |
| // compelling reason, just because it seems like the right thing to do). |
| // So we ignore whatever comes out in |value|, but break out of the loop |
| // if a non-success trap status is returned. |
| if (dbg->observesNewGlobalObject()) { |
| status = dbg->fireNewGlobalObject(cx, global, &value); |
| if (status != JSTRAP_CONTINUE && status != JSTRAP_RETURN) |
| break; |
| } |
| } |
| MOZ_ASSERT(!cx->isExceptionPending()); |
| } |
| |
| /* static */ bool |
| Debugger::slowPathOnLogAllocationSite(JSContext* cx, HandleObject obj, HandleSavedFrame frame, |
| double when, GlobalObject::DebuggerVector& dbgs) |
| { |
| MOZ_ASSERT(!dbgs.empty()); |
| mozilla::DebugOnly<Debugger**> begin = dbgs.begin(); |
| |
| for (Debugger** dbgp = dbgs.begin(); dbgp < dbgs.end(); dbgp++) { |
| // The set of debuggers had better not change while we're iterating, |
| // such that the vector gets reallocated. |
| MOZ_ASSERT(dbgs.begin() == begin); |
| |
| if ((*dbgp)->trackingAllocationSites && |
| (*dbgp)->enabled && |
| !(*dbgp)->appendAllocationSite(cx, obj, frame, when)) |
| { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| /* static */ void |
| Debugger::slowPathOnIonCompilation(JSContext* cx, Handle<ScriptVector> scripts, LSprinter& graph) |
| { |
| JSTrapStatus status = dispatchHook( |
| cx, |
| [](Debugger* dbg) -> bool { return dbg->getHook(OnIonCompilation); }, |
| [&](Debugger* dbg) -> JSTrapStatus { |
| (void) dbg->fireOnIonCompilationHook(cx, scripts, graph); |
| return JSTRAP_CONTINUE; |
| }); |
| |
| if (status == JSTRAP_ERROR) { |
| cx->clearPendingException(); |
| return; |
| } |
| |
| MOZ_ASSERT(status == JSTRAP_CONTINUE); |
| } |
| |
| bool |
| Debugger::isDebuggeeUnbarriered(const JSCompartment* compartment) const |
| { |
| MOZ_ASSERT(compartment); |
| return compartment->isDebuggee() && debuggees.has(compartment->unsafeUnbarrieredMaybeGlobal()); |
| } |
| |
| Debugger::TenurePromotionsLogEntry::TenurePromotionsLogEntry(JSRuntime* rt, JSObject& obj, double when) |
| : className(obj.getClass()->name), |
| when(when), |
| frame(getObjectAllocationSite(obj)), |
| size(JS::ubi::Node(&obj).size(rt->debuggerMallocSizeOf)) |
| { } |
| |
| |
| void |
| Debugger::logTenurePromotion(JSRuntime* rt, JSObject& obj, double when) |
| { |
| AutoEnterOOMUnsafeRegion oomUnsafe; |
| |
| if (!tenurePromotionsLog.emplaceBack(rt, obj, when)) |
| oomUnsafe.crash("Debugger::logTenurePromotion"); |
| |
| if (tenurePromotionsLog.length() > maxTenurePromotionsLogLength) { |
| if (!tenurePromotionsLog.popFront()) |
| oomUnsafe.crash("Debugger::logTenurePromotion"); |
| MOZ_ASSERT(tenurePromotionsLog.length() == maxTenurePromotionsLogLength); |
| tenurePromotionsLogOverflowed = true; |
| } |
| } |
| |
| bool |
| Debugger::appendAllocationSite(JSContext* cx, HandleObject obj, HandleSavedFrame frame, |
| double when) |
| { |
| MOZ_ASSERT(trackingAllocationSites && enabled); |
| |
| AutoCompartment ac(cx, object); |
| RootedObject wrappedFrame(cx, frame); |
| if (!cx->compartment()->wrap(cx, &wrappedFrame)) |
| return false; |
| |
| RootedAtom ctorName(cx); |
| { |
| AutoCompartment ac(cx, obj); |
| if (!obj->constructorDisplayAtom(cx, &ctorName)) |
| return false; |
| } |
| |
| auto className = obj->getClass()->name; |
| auto size = JS::ubi::Node(obj.get()).size(cx->runtime()->debuggerMallocSizeOf); |
| auto inNursery = gc::IsInsideNursery(obj); |
| |
| if (!allocationsLog.emplaceBack(wrappedFrame, when, className, ctorName, size, inNursery)) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| |
| if (allocationsLog.length() > maxAllocationsLogLength) { |
| if (!allocationsLog.popFront()) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| MOZ_ASSERT(allocationsLog.length() == maxAllocationsLogLength); |
| allocationsLogOverflowed = true; |
| } |
| |
| return true; |
| } |
| |
| JSTrapStatus |
| Debugger::firePromiseHook(JSContext* cx, Hook hook, HandleObject promise, MutableHandleValue vp) |
| { |
| MOZ_ASSERT(hook == OnNewPromise || hook == OnPromiseSettled); |
| |
| RootedObject hookObj(cx, getHook(hook)); |
| MOZ_ASSERT(hookObj); |
| MOZ_ASSERT(hookObj->isCallable()); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, object); |
| |
| RootedValue dbgObj(cx, ObjectValue(*promise)); |
| if (!wrapDebuggeeValue(cx, &dbgObj)) |
| return handleUncaughtException(ac, false); |
| |
| // Like onNewGlobalObject, the Promise hooks are infallible and the comments |
| // in |Debugger::fireNewGlobalObject| apply here as well. |
| |
| RootedValue rv(cx); |
| bool ok = Invoke(cx, ObjectValue(*object), ObjectValue(*hookObj), 1, dbgObj.address(), &rv); |
| if (ok && !rv.isUndefined()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, |
| JSMSG_DEBUG_RESUMPTION_VALUE_DISALLOWED); |
| ok = false; |
| } |
| |
| JSTrapStatus status = ok ? JSTRAP_CONTINUE |
| : handleUncaughtException(ac, vp, true); |
| MOZ_ASSERT(!cx->isExceptionPending()); |
| return status; |
| } |
| |
| /* static */ void |
| Debugger::slowPathPromiseHook(JSContext* cx, Hook hook, HandleObject promise) |
| { |
| MOZ_ASSERT(hook == OnNewPromise || hook == OnPromiseSettled); |
| RootedValue rval(cx); |
| |
| JSTrapStatus status = dispatchHook( |
| cx, |
| [hook](Debugger* dbg) -> bool { return dbg->getHook(hook); }, |
| [&](Debugger* dbg) -> JSTrapStatus { |
| (void) dbg->firePromiseHook(cx, hook, promise, &rval); |
| return JSTRAP_CONTINUE; |
| }); |
| |
| if (status == JSTRAP_ERROR) { |
| // The dispatch hook function might fail to append into the list of |
| // Debuggers which are watching for the hook. |
| cx->clearPendingException(); |
| return; |
| } |
| |
| // Promise hooks are infallible and we ignore errors from uncaught |
| // exceptions by design. |
| MOZ_ASSERT(status == JSTRAP_CONTINUE); |
| } |
| |
| |
| /*** Debugger code invalidation for observing execution ******************************************/ |
| |
| class MOZ_RAII ExecutionObservableCompartments : public Debugger::ExecutionObservableSet |
| { |
| HashSet<JSCompartment*> compartments_; |
| HashSet<Zone*> zones_; |
| |
| public: |
| explicit ExecutionObservableCompartments(JSContext* cx |
| MOZ_GUARD_OBJECT_NOTIFIER_PARAM) |
| : compartments_(cx), |
| zones_(cx) |
| { |
| MOZ_GUARD_OBJECT_NOTIFIER_INIT; |
| } |
| |
| bool init() { return compartments_.init() && zones_.init(); } |
| bool add(JSCompartment* comp) { return compartments_.put(comp) && zones_.put(comp->zone()); } |
| |
| typedef HashSet<JSCompartment*>::Range CompartmentRange; |
| const HashSet<JSCompartment*>* compartments() const { return &compartments_; } |
| |
| const HashSet<Zone*>* zones() const { return &zones_; } |
| bool shouldRecompileOrInvalidate(JSScript* script) const { |
| return script->hasBaselineScript() && compartments_.has(script->compartment()); |
| } |
| bool shouldMarkAsDebuggee(ScriptFrameIter& iter) const { |
| // AbstractFramePtr can't refer to non-remateralized Ion frames, so if |
| // iter refers to one such, we know we don't match. |
| return iter.hasUsableAbstractFramePtr() && compartments_.has(iter.compartment()); |
| } |
| |
| MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER |
| }; |
| |
| // Given a particular AbstractFramePtr F that has become observable, this |
| // represents the stack frames that need to be bailed out or marked as |
| // debuggees, and the scripts that need to be recompiled, taking inlining into |
| // account. |
| class MOZ_RAII ExecutionObservableFrame : public Debugger::ExecutionObservableSet |
| { |
| AbstractFramePtr frame_; |
| |
| public: |
| explicit ExecutionObservableFrame(AbstractFramePtr frame |
| MOZ_GUARD_OBJECT_NOTIFIER_PARAM) |
| : frame_(frame) |
| { |
| MOZ_GUARD_OBJECT_NOTIFIER_INIT; |
| } |
| |
| Zone* singleZone() const { |
| // We never inline across compartments, let alone across zones, so |
| // frames_'s script's zone is the only one of interest. |
| return frame_.script()->compartment()->zone(); |
| } |
| |
| JSScript* singleScriptForZoneInvalidation() const { |
| MOZ_CRASH("ExecutionObservableFrame shouldn't need zone-wide invalidation."); |
| return nullptr; |
| } |
| |
| bool shouldRecompileOrInvalidate(JSScript* script) const { |
| // Normally, *this represents exactly one script: the one frame_ is |
| // running. |
| // |
| // However, debug-mode OSR uses *this for both invalidating Ion frames, |
| // and recompiling the Baseline scripts that those Ion frames will bail |
| // out into. Suppose frame_ is an inline frame, executing a copy of its |
| // JSScript, S_inner, that has been inlined into the IonScript of some |
| // other JSScript, S_outer. We must match S_outer, to decide which Ion |
| // frame to invalidate; and we must match S_inner, to decide which |
| // Baseline script to recompile. |
| // |
| // Note that this does not, by design, invalidate *all* inliners of |
| // frame_.script(), as only frame_ is made observable, not |
| // frame_.script(). |
| if (!script->hasBaselineScript()) |
| return false; |
| |
| if (script == frame_.script()) |
| return true; |
| |
| return frame_.isRematerializedFrame() && |
| script == frame_.asRematerializedFrame()->outerScript(); |
| } |
| |
| bool shouldMarkAsDebuggee(ScriptFrameIter& iter) const { |
| // AbstractFramePtr can't refer to non-remateralized Ion frames, so if |
| // iter refers to one such, we know we don't match. |
| // |
| // We never use this 'has' overload for frame invalidation, only for |
| // frame debuggee marking; so this overload doesn't need a parallel to |
| // the just-so inlining logic above. |
| return iter.hasUsableAbstractFramePtr() && iter.abstractFramePtr() == frame_; |
| } |
| |
| MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER |
| }; |
| |
| class MOZ_RAII ExecutionObservableScript : public Debugger::ExecutionObservableSet |
| { |
| RootedScript script_; |
| |
| public: |
| ExecutionObservableScript(JSContext* cx, JSScript* script |
| MOZ_GUARD_OBJECT_NOTIFIER_PARAM) |
| : script_(cx, script) |
| { |
| MOZ_GUARD_OBJECT_NOTIFIER_INIT; |
| } |
| |
| Zone* singleZone() const { return script_->compartment()->zone(); } |
| JSScript* singleScriptForZoneInvalidation() const { return script_; } |
| bool shouldRecompileOrInvalidate(JSScript* script) const { |
| return script->hasBaselineScript() && script == script_; |
| } |
| bool shouldMarkAsDebuggee(ScriptFrameIter& iter) const { |
| // AbstractFramePtr can't refer to non-remateralized Ion frames, and |
| // while a non-rematerialized Ion frame may indeed be running script_, |
| // we cannot mark them as debuggees until they bail out. |
| // |
| // Upon bailing out, any newly constructed Baseline frames that came |
| // from Ion frames with scripts that are isDebuggee() is marked as |
| // debuggee. This is correct in that the only other way a frame may be |
| // marked as debuggee is via Debugger.Frame reflection, which would |
| // have rematerialized any Ion frames. |
| return iter.hasUsableAbstractFramePtr() && iter.abstractFramePtr().script() == script_; |
| } |
| |
| MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER |
| }; |
| |
| /* static */ bool |
| Debugger::updateExecutionObservabilityOfFrames(JSContext* cx, const ExecutionObservableSet& obs, |
| IsObserving observing) |
| { |
| AutoSuppressProfilerSampling suppressProfilerSampling(cx); |
| |
| { |
| jit::JitContext jctx(cx, nullptr); |
| if (!jit::RecompileOnStackBaselineScriptsForDebugMode(cx, obs, observing)) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| } |
| |
| AbstractFramePtr oldestEnabledFrame; |
| for (ScriptFrameIter iter(cx, ScriptFrameIter::ALL_CONTEXTS, |
| ScriptFrameIter::GO_THROUGH_SAVED); |
| !iter.done(); |
| ++iter) |
| { |
| if (obs.shouldMarkAsDebuggee(iter)) { |
| if (observing) { |
| if (!iter.abstractFramePtr().isDebuggee()) { |
| oldestEnabledFrame = iter.abstractFramePtr(); |
| oldestEnabledFrame.setIsDebuggee(); |
| } |
| } else { |
| #ifdef DEBUG |
| // Debugger.Frame lifetimes are managed by the debug epilogue, |
| // so in general it's unsafe to unmark a frame if it has a |
| // Debugger.Frame associated with it. |
| FrameRange r(iter.abstractFramePtr()); |
| MOZ_ASSERT(r.empty()); |
| #endif |
| iter.abstractFramePtr().unsetIsDebuggee(); |
| } |
| } |
| } |
| |
| // See comment in unsetPrevUpToDateUntil. |
| if (oldestEnabledFrame) { |
| AutoCompartment ac(cx, oldestEnabledFrame.compartment()); |
| DebugScopes::unsetPrevUpToDateUntil(cx, oldestEnabledFrame); |
| } |
| |
| return true; |
| } |
| |
| static inline void |
| MarkBaselineScriptActiveIfObservable(JSScript* script, const Debugger::ExecutionObservableSet& obs) |
| { |
| if (obs.shouldRecompileOrInvalidate(script)) |
| script->baselineScript()->setActive(); |
| } |
| |
| static bool |
| AppendAndInvalidateScript(JSContext* cx, Zone* zone, JSScript* script, Vector<JSScript*>& scripts) |
| { |
| // Enter the script's compartment as addPendingRecompile attempts to |
| // cancel off-thread compilations, whose books are kept on the |
| // script's compartment. |
| MOZ_ASSERT(script->compartment()->zone() == zone); |
| AutoCompartment ac(cx, script->compartment()); |
| zone->types.addPendingRecompile(cx, script); |
| return scripts.append(script); |
| } |
| |
| static bool |
| UpdateExecutionObservabilityOfScriptsInZone(JSContext* cx, Zone* zone, |
| const Debugger::ExecutionObservableSet& obs, |
| Debugger::IsObserving observing) |
| { |
| using namespace js::jit; |
| |
| // See note in js::ReleaseAllJITCode. |
| cx->runtime()->gc.evictNursery(); |
| |
| AutoSuppressProfilerSampling suppressProfilerSampling(cx); |
| |
| JSRuntime* rt = cx->runtime(); |
| FreeOp* fop = cx->runtime()->defaultFreeOp(); |
| |
| // Mark active baseline scripts in the observable set so that they don't |
| // get discarded. They will be recompiled. |
| for (JitActivationIterator actIter(rt); !actIter.done(); ++actIter) { |
| if (actIter->compartment()->zone() != zone) |
| continue; |
| |
| for (JitFrameIterator iter(actIter); !iter.done(); ++iter) { |
| switch (iter.type()) { |
| case JitFrame_BaselineJS: |
| MarkBaselineScriptActiveIfObservable(iter.script(), obs); |
| break; |
| case JitFrame_IonJS: |
| MarkBaselineScriptActiveIfObservable(iter.script(), obs); |
| for (InlineFrameIterator inlineIter(rt, &iter); inlineIter.more(); ++inlineIter) |
| MarkBaselineScriptActiveIfObservable(inlineIter.script(), obs); |
| break; |
| default:; |
| } |
| } |
| } |
| |
| Vector<JSScript*> scripts(cx); |
| |
| // Iterate through observable scripts, invalidating their Ion scripts and |
| // appending them to a vector for discarding their baseline scripts later. |
| { |
| AutoEnterAnalysis enter(fop, zone); |
| if (JSScript* script = obs.singleScriptForZoneInvalidation()) { |
| if (obs.shouldRecompileOrInvalidate(script)) { |
| if (!AppendAndInvalidateScript(cx, zone, script, scripts)) |
| return false; |
| } |
| } else { |
| for (gc::ZoneCellIter iter(zone, gc::AllocKind::SCRIPT); !iter.done(); iter.next()) { |
| JSScript* script = iter.get<JSScript>(); |
| if (obs.shouldRecompileOrInvalidate(script) && |
| !gc::IsAboutToBeFinalizedUnbarriered(&script)) |
| { |
| if (!AppendAndInvalidateScript(cx, zone, script, scripts)) |
| return false; |
| } |
| } |
| } |
| } |
| |
| // Iterate through the scripts again and finish discarding |
| // BaselineScripts. This must be done as a separate phase as we can only |
| // discard the BaselineScript on scripts that have no IonScript. |
| for (size_t i = 0; i < scripts.length(); i++) { |
| MOZ_ASSERT_IF(scripts[i]->isDebuggee(), observing); |
| FinishDiscardBaselineScript(fop, scripts[i]); |
| } |
| |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::updateExecutionObservabilityOfScripts(JSContext* cx, const ExecutionObservableSet& obs, |
| IsObserving observing) |
| { |
| if (Zone* zone = obs.singleZone()) |
| return UpdateExecutionObservabilityOfScriptsInZone(cx, zone, obs, observing); |
| |
| typedef ExecutionObservableSet::ZoneRange ZoneRange; |
| for (ZoneRange r = obs.zones()->all(); !r.empty(); r.popFront()) { |
| if (!UpdateExecutionObservabilityOfScriptsInZone(cx, r.front(), obs, observing)) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::updateExecutionObservability(JSContext* cx, ExecutionObservableSet& obs, |
| IsObserving observing) |
| { |
| if (!obs.singleZone() && obs.zones()->empty()) |
| return true; |
| |
| // Invalidate scripts first so we can set the needsArgsObj flag on scripts |
| // before patching frames. |
| return updateExecutionObservabilityOfScripts(cx, obs, observing) && |
| updateExecutionObservabilityOfFrames(cx, obs, observing); |
| } |
| |
| /* static */ bool |
| Debugger::ensureExecutionObservabilityOfScript(JSContext* cx, JSScript* script) |
| { |
| if (script->isDebuggee()) |
| return true; |
| ExecutionObservableScript obs(cx, script); |
| return updateExecutionObservability(cx, obs, Observing); |
| } |
| |
| /* static */ bool |
| Debugger::ensureExecutionObservabilityOfOsrFrame(JSContext* cx, InterpreterFrame* frame) |
| { |
| MOZ_ASSERT(frame->isDebuggee()); |
| if (frame->script()->hasBaselineScript() && |
| frame->script()->baselineScript()->hasDebugInstrumentation()) |
| { |
| return true; |
| } |
| ExecutionObservableFrame obs(frame); |
| return updateExecutionObservabilityOfFrames(cx, obs, Observing); |
| } |
| |
| /* static */ bool |
| Debugger::ensureExecutionObservabilityOfFrame(JSContext* cx, AbstractFramePtr frame) |
| { |
| MOZ_ASSERT_IF(frame.script()->isDebuggee(), frame.isDebuggee()); |
| if (frame.isDebuggee()) |
| return true; |
| ExecutionObservableFrame obs(frame); |
| return updateExecutionObservabilityOfFrames(cx, obs, Observing); |
| } |
| |
| /* static */ bool |
| Debugger::ensureExecutionObservabilityOfCompartment(JSContext* cx, JSCompartment* comp) |
| { |
| if (comp->debuggerObservesAllExecution()) |
| return true; |
| ExecutionObservableCompartments obs(cx); |
| if (!obs.init() || !obs.add(comp)) |
| return false; |
| comp->updateDebuggerObservesAllExecution(); |
| return updateExecutionObservability(cx, obs, Observing); |
| } |
| |
| /* static */ bool |
| Debugger::hookObservesAllExecution(Hook which) |
| { |
| return which == OnEnterFrame; |
| } |
| |
| Debugger::IsObserving |
| Debugger::observesAllExecution() const |
| { |
| if (enabled && !!getHook(OnEnterFrame)) |
| return Observing; |
| return NotObserving; |
| } |
| |
| Debugger::IsObserving |
| Debugger::observesAsmJS() const |
| { |
| if (enabled && !allowUnobservedAsmJS) |
| return Observing; |
| return NotObserving; |
| } |
| |
| Debugger::IsObserving |
| Debugger::observesCoverage() const |
| { |
| if (enabled && collectCoverageInfo) |
| return Observing; |
| return NotObserving; |
| } |
| |
| // Toggle whether this Debugger's debuggees observe all execution. This is |
| // called when a hook that observes all execution is set or unset. See |
| // hookObservesAllExecution. |
| bool |
| Debugger::updateObservesAllExecutionOnDebuggees(JSContext* cx, IsObserving observing) |
| { |
| ExecutionObservableCompartments obs(cx); |
| if (!obs.init()) |
| return false; |
| |
| for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { |
| GlobalObject* global = r.front(); |
| JSCompartment* comp = global->compartment(); |
| |
| if (comp->debuggerObservesAllExecution() == observing) |
| continue; |
| |
| // It's expensive to eagerly invalidate and recompile a compartment, |
| // so add the compartment to the set only if we are observing. |
| if (observing && !obs.add(comp)) |
| return false; |
| |
| comp->updateDebuggerObservesAllExecution(); |
| } |
| |
| return updateExecutionObservability(cx, obs, observing); |
| } |
| |
| bool |
| Debugger::updateObservesCoverageOnDebuggees(JSContext* cx, IsObserving observing) |
| { |
| ExecutionObservableCompartments obs(cx); |
| if (!obs.init()) |
| return false; |
| |
| for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { |
| GlobalObject* global = r.front(); |
| JSCompartment* comp = global->compartment(); |
| |
| if (comp->debuggerObservesCoverage() == observing) |
| continue; |
| |
| // Invalidate and recompile a compartment to add or remove PCCounts |
| // increments. We have to eagerly invalidate, as otherwise we might have |
| // dangling pointers to freed PCCounts. |
| if (!obs.add(comp)) |
| return false; |
| } |
| |
| // If any frame on the stack belongs to the debuggee, then we cannot update |
| // the ScriptCounts, because this would imply to invalidate a Debugger.Frame |
| // to recompile it with/without ScriptCount support. |
| for (ScriptFrameIter iter(cx, ScriptFrameIter::ALL_CONTEXTS, |
| ScriptFrameIter::GO_THROUGH_SAVED); |
| !iter.done(); |
| ++iter) |
| { |
| if (obs.shouldMarkAsDebuggee(iter)) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NOT_IDLE); |
| return false; |
| } |
| } |
| |
| if (!updateExecutionObservability(cx, obs, observing)) |
| return false; |
| |
| // All compartments can safely be toggled, and all scripts will be |
| // recompiled. Thus we can update each compartment accordingly. |
| typedef ExecutionObservableCompartments::CompartmentRange CompartmentRange; |
| for (CompartmentRange r = obs.compartments()->all(); !r.empty(); r.popFront()) |
| r.front()->updateDebuggerObservesCoverage(); |
| |
| return true; |
| } |
| |
| void |
| Debugger::updateObservesAsmJSOnDebuggees(IsObserving observing) |
| { |
| for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { |
| GlobalObject* global = r.front(); |
| JSCompartment* comp = global->compartment(); |
| |
| if (comp->debuggerObservesAsmJS() == observing) |
| continue; |
| |
| comp->updateDebuggerObservesAsmJS(); |
| } |
| } |
| |
| |
| /*** Allocations Tracking *************************************************************************/ |
| |
| /* static */ bool |
| Debugger::cannotTrackAllocations(const GlobalObject& global) |
| { |
| auto existingCallback = global.compartment()->getObjectMetadataCallback(); |
| return existingCallback && existingCallback != SavedStacksMetadataCallback; |
| } |
| |
| /* static */ bool |
| Debugger::isObservedByDebuggerTrackingAllocations(const GlobalObject& debuggee) |
| { |
| if (auto* v = debuggee.getDebuggers()) { |
| Debugger** p; |
| for (p = v->begin(); p != v->end(); p++) { |
| if ((*p)->trackingAllocationSites && (*p)->enabled) { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| /* static */ bool |
| Debugger::addAllocationsTracking(JSContext* cx, Handle<GlobalObject*> debuggee) |
| { |
| // Precondition: the given global object is being observed by at least one |
| // Debugger that is tracking allocations. |
| MOZ_ASSERT(isObservedByDebuggerTrackingAllocations(*debuggee)); |
| |
| if (Debugger::cannotTrackAllocations(*debuggee)) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, |
| JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET); |
| return false; |
| } |
| |
| debuggee->compartment()->setObjectMetadataCallback(SavedStacksMetadataCallback); |
| debuggee->compartment()->chooseAllocationSamplingProbability(); |
| return true; |
| } |
| |
| /* static */ void |
| Debugger::removeAllocationsTracking(GlobalObject& global) |
| { |
| // If there are still Debuggers that are observing allocations, we cannot |
| // remove the metadata callback yet. Recompute the sampling probability |
| // based on the remaining debuggers' needs. |
| if (isObservedByDebuggerTrackingAllocations(global)) { |
| global.compartment()->chooseAllocationSamplingProbability(); |
| return; |
| } |
| |
| global.compartment()->forgetObjectMetadataCallback(); |
| } |
| |
| bool |
| Debugger::addAllocationsTrackingForAllDebuggees(JSContext* cx) |
| { |
| MOZ_ASSERT(trackingAllocationSites); |
| |
| // We don't want to end up in a state where we added allocations |
| // tracking to some of our debuggees, but failed to do so for |
| // others. Before attempting to start tracking allocations in *any* of |
| // our debuggees, ensure that we will be able to track allocations for |
| // *all* of our debuggees. |
| for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { |
| if (Debugger::cannotTrackAllocations(*r.front().get())) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, |
| JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET); |
| return false; |
| } |
| } |
| |
| Rooted<GlobalObject*> g(cx); |
| for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) { |
| // This should always succeed, since we already checked for the |
| // error case above. |
| g = r.front().get(); |
| MOZ_ALWAYS_TRUE(Debugger::addAllocationsTracking(cx, g)); |
| } |
| |
| return true; |
| } |
| |
| void |
| Debugger::removeAllocationsTrackingForAllDebuggees() |
| { |
| for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) |
| Debugger::removeAllocationsTracking(*r.front().get()); |
| |
| allocationsLog.clear(); |
| } |
| |
| |
| |
| /*** Debugger JSObjects **************************************************************************/ |
| |
| void |
| Debugger::markCrossCompartmentEdges(JSTracer* trc) |
| { |
| objects.markCrossCompartmentEdges<DebuggerObject_trace>(trc); |
| environments.markCrossCompartmentEdges<DebuggerEnv_trace>(trc); |
| scripts.markCrossCompartmentEdges<DebuggerScript_trace>(trc); |
| sources.markCrossCompartmentEdges<DebuggerSource_trace>(trc); |
| |
| // Because we don't have access to a `cx` inside |
| // `Debugger::logTenurePromotion`, we can't hold onto CCWs inside the log, |
| // and instead have unwrapped cross-compartment edges. We need to be sure to |
| // mark those here. |
| TenurePromotionsLog::trace(&tenurePromotionsLog, trc); |
| } |
| |
| /* |
| * Ordinarily, WeakMap keys and values are marked because at some point it was |
| * discovered that the WeakMap was live; that is, some object containing the |
| * WeakMap was marked during mark phase. |
| * |
| * However, during zone GC, we have to do something about cross-compartment |
| * edges in non-GC'd compartments. Since the source may be live, we |
| * conservatively assume it is and mark the edge. |
| * |
| * Each Debugger object keeps four cross-compartment WeakMaps: objects, scripts, |
| * script source objects, and environments. They have the property that all |
| * their values are in the same compartment as the Debugger object, but we have |
| * to mark the keys and the private pointer in the wrapper object. |
| * |
| * We must scan all Debugger objects regardless of whether they *currently* have |
| * any debuggees in a compartment being GC'd, because the WeakMap entries |
| * persist even when debuggees are removed. |
| * |
| * This happens during the initial mark phase, not iterative marking, because |
| * all the edges being reported here are strong references. |
| * |
| * This method is also used during compacting GC to update cross compartment |
| * pointers in zones that are not currently being compacted. |
| */ |
| /* static */ void |
| Debugger::markIncomingCrossCompartmentEdges(JSTracer* trc) |
| { |
| JSRuntime* rt = trc->runtime(); |
| gc::State state = rt->gc.state(); |
| MOZ_ASSERT(state == gc::MARK_ROOTS || state == gc::COMPACT); |
| |
| for (Debugger* dbg : rt->debuggerList) { |
| Zone* zone = dbg->object->zone(); |
| if ((state == gc::MARK_ROOTS && !zone->isCollecting()) || |
| (state == gc::COMPACT && !zone->isGCCompacting())) |
| { |
| dbg->markCrossCompartmentEdges(trc); |
| } |
| } |
| } |
| |
| /* |
| * This method has two tasks: |
| * 1. Mark Debugger objects that are unreachable except for debugger hooks that |
| * may yet be called. |
| * 2. Mark breakpoint handlers. |
| * |
| * This happens during the iterative part of the GC mark phase. This method |
| * returns true if it has to mark anything; GC calls it repeatedly until it |
| * returns false. |
| */ |
| /* static */ bool |
| Debugger::markAllIteratively(GCMarker* trc) |
| { |
| bool markedAny = false; |
| |
| /* |
| * Find all Debugger objects in danger of GC. This code is a little |
| * convoluted since the easiest way to find them is via their debuggees. |
| */ |
| JSRuntime* rt = trc->runtime(); |
| for (CompartmentsIter c(rt, SkipAtoms); !c.done(); c.next()) { |
| if (c->isDebuggee()) { |
| GlobalObject* global = c->unsafeUnbarrieredMaybeGlobal(); |
| if (!IsMarkedUnbarriered(&global)) |
| continue; |
| |
| /* |
| * Every debuggee has at least one debugger, so in this case |
| * getDebuggers can't return nullptr. |
| */ |
| const GlobalObject::DebuggerVector* debuggers = global->getDebuggers(); |
| MOZ_ASSERT(debuggers); |
| for (Debugger * const* p = debuggers->begin(); p != debuggers->end(); p++) { |
| Debugger* dbg = *p; |
| |
| /* |
| * dbg is a Debugger with at least one debuggee. Check three things: |
| * - dbg is actually in a compartment that is being marked |
| * - it isn't already marked |
| * - it actually has hooks that might be called |
| */ |
| HeapPtrNativeObject& dbgobj = dbg->toJSObjectRef(); |
| if (!dbgobj->zone()->isGCMarking()) |
| continue; |
| |
| bool dbgMarked = IsMarked(&dbgobj); |
| if (!dbgMarked && dbg->hasAnyLiveHooks()) { |
| /* |
| * obj could be reachable only via its live, enabled |
| * debugger hooks, which may yet be called. |
| */ |
| TraceEdge(trc, &dbgobj, "enabled Debugger"); |
| markedAny = true; |
| dbgMarked = true; |
| } |
| |
| if (dbgMarked) { |
| /* Search for breakpoints to mark. */ |
| for (Breakpoint* bp = dbg->firstBreakpoint(); bp; bp = bp->nextInDebugger()) { |
| if (IsMarkedUnbarriered(&bp->site->script)) { |
| /* |
| * The debugger and the script are both live. |
| * Therefore the breakpoint handler is live. |
| */ |
| if (!IsMarked(&bp->getHandlerRef())) { |
| TraceEdge(trc, &bp->getHandlerRef(), "breakpoint handler"); |
| markedAny = true; |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| return markedAny; |
| } |
| |
| /* |
| * Mark all debugger-owned GC things unconditionally. This is used by the minor |
| * GC: the minor GC cannot apply the weak constraints of the full GC because it |
| * visits only part of the heap. |
| */ |
| /* static */ void |
| Debugger::markAll(JSTracer* trc) |
| { |
| JSRuntime* rt = trc->runtime(); |
| for (Debugger* dbg : rt->debuggerList) { |
| for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty(); e.popFront()) |
| TraceManuallyBarrieredEdge(trc, e.mutableFront().unsafeGet(), "Global Object"); |
| |
| HeapPtrNativeObject& dbgobj = dbg->toJSObjectRef(); |
| TraceEdge(trc, &dbgobj, "Debugger Object"); |
| |
| dbg->scripts.trace(trc); |
| dbg->sources.trace(trc); |
| dbg->objects.trace(trc); |
| dbg->environments.trace(trc); |
| |
| for (Breakpoint* bp = dbg->firstBreakpoint(); bp; bp = bp->nextInDebugger()) { |
| TraceManuallyBarrieredEdge(trc, &bp->site->script, "breakpoint script"); |
| TraceEdge(trc, &bp->getHandlerRef(), "breakpoint handler"); |
| } |
| } |
| } |
| |
| /* static */ void |
| Debugger::traceObject(JSTracer* trc, JSObject* obj) |
| { |
| if (Debugger* dbg = Debugger::fromJSObject(obj)) |
| dbg->trace(trc); |
| } |
| |
| void |
| Debugger::trace(JSTracer* trc) |
| { |
| if (uncaughtExceptionHook) |
| TraceEdge(trc, &uncaughtExceptionHook, "hooks"); |
| |
| /* |
| * Mark Debugger.Frame objects. These are all reachable from JS, because the |
| * corresponding JS frames 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()) { |
| RelocatablePtrNativeObject& frameobj = r.front().value(); |
| MOZ_ASSERT(MaybeForwarded(frameobj.get())->getPrivate()); |
| TraceEdge(trc, &frameobj, "live Debugger.Frame"); |
| } |
| |
| AllocationsLog::trace(&allocationsLog, trc); |
| TenurePromotionsLog::trace(&tenurePromotionsLog, trc); |
| |
| /* Trace the weak map from JSScript instances to Debugger.Script objects. */ |
| scripts.trace(trc); |
| |
| /* Trace the referent ->Debugger.Source weak map */ |
| sources.trace(trc); |
| |
| /* Trace the referent -> Debugger.Object weak map. */ |
| objects.trace(trc); |
| |
| /* Trace the referent -> Debugger.Environment weak map. */ |
| environments.trace(trc); |
| } |
| |
| /* static */ void |
| Debugger::sweepAll(FreeOp* fop) |
| { |
| JSRuntime* rt = fop->runtime(); |
| |
| for (Debugger* dbg : rt->debuggerList) { |
| if (IsAboutToBeFinalized(&dbg->object)) { |
| /* |
| * dbg is being GC'd. Detach it from its debuggees. The debuggee |
| * might be GC'd too. Since detaching requires access to both |
| * objects, this must be done before finalize time. |
| */ |
| for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty(); e.popFront()) |
| dbg->removeDebuggeeGlobal(fop, e.front().unbarrieredGet(), &e); |
| } |
| } |
| } |
| |
| /* static */ void |
| Debugger::detachAllDebuggersFromGlobal(FreeOp* fop, GlobalObject* global) |
| { |
| const GlobalObject::DebuggerVector* debuggers = global->getDebuggers(); |
| MOZ_ASSERT(!debuggers->empty()); |
| while (!debuggers->empty()) |
| debuggers->back()->removeDebuggeeGlobal(fop, global, nullptr); |
| } |
| |
| /* static */ void |
| Debugger::findZoneEdges(Zone* zone, js::gc::ComponentFinder<Zone>& finder) |
| { |
| /* |
| * For debugger cross compartment wrappers, add edges in the opposite |
| * direction to those already added by JSCompartment::findOutgoingEdges. |
| * This ensure that debuggers and their debuggees are finalized in the same |
| * group. |
| */ |
| for (Debugger* dbg : zone->runtimeFromMainThread()->debuggerList) { |
| Zone* w = dbg->object->zone(); |
| if (w == zone || !w->isGCMarking()) |
| continue; |
| if (dbg->debuggeeZones.has(zone) || |
| dbg->scripts.hasKeyInZone(zone) || |
| dbg->sources.hasKeyInZone(zone) || |
| dbg->objects.hasKeyInZone(zone) || |
| dbg->environments.hasKeyInZone(zone)) |
| { |
| finder.addEdgeTo(w); |
| } |
| } |
| } |
| |
| /* static */ void |
| Debugger::finalize(FreeOp* fop, JSObject* obj) |
| { |
| Debugger* dbg = fromJSObject(obj); |
| if (!dbg) |
| return; |
| fop->delete_(dbg); |
| } |
| |
| const Class Debugger::jsclass = { |
| "Debugger", |
| JSCLASS_HAS_PRIVATE | |
| JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUG_COUNT), |
| nullptr, nullptr, nullptr, nullptr, |
| nullptr, nullptr, nullptr, Debugger::finalize, |
| nullptr, /* call */ |
| nullptr, /* hasInstance */ |
| nullptr, /* construct */ |
| Debugger::traceObject |
| }; |
| |
| /* static */ Debugger* |
| Debugger::fromThisValue(JSContext* cx, const CallArgs& args, const char* fnname) |
| { |
| JSObject* thisobj = NonNullObject(cx, args.thisv()); |
| if (!thisobj) |
| return nullptr; |
| if (thisobj->getClass() != &Debugger::jsclass) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, |
| "Debugger", fnname, thisobj->getClass()->name); |
| return nullptr; |
| } |
| |
| /* |
| * Forbid Debugger.prototype, which is of the Debugger JSClass but isn't |
| * really a Debugger object. The prototype object is distinguished by |
| * having a nullptr private value. |
| */ |
| Debugger* dbg = fromJSObject(thisobj); |
| if (!dbg) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, |
| "Debugger", fnname, "prototype object"); |
| } |
| return dbg; |
| } |
| |
| #define THIS_DEBUGGER(cx, argc, vp, fnname, args, dbg) \ |
| CallArgs args = CallArgsFromVp(argc, vp); \ |
| Debugger* dbg = Debugger::fromThisValue(cx, args, fnname); \ |
| if (!dbg) \ |
| return false |
| |
| /* static */ bool |
| Debugger::getEnabled(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "get enabled", args, dbg); |
| args.rval().setBoolean(dbg->enabled); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::setEnabled(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "set enabled", args, dbg); |
| if (!args.requireAtLeast(cx, "Debugger.set enabled", 1)) |
| return false; |
| |
| bool wasEnabled = dbg->enabled; |
| dbg->enabled = ToBoolean(args[0]); |
| |
| if (wasEnabled != dbg->enabled) { |
| if (dbg->trackingAllocationSites) { |
| if (wasEnabled) { |
| dbg->removeAllocationsTrackingForAllDebuggees(); |
| } else { |
| if (!dbg->addAllocationsTrackingForAllDebuggees(cx)) { |
| dbg->enabled = false; |
| return false; |
| } |
| } |
| } |
| |
| for (Breakpoint* bp = dbg->firstBreakpoint(); bp; bp = bp->nextInDebugger()) { |
| if (!wasEnabled) |
| bp->site->inc(cx->runtime()->defaultFreeOp()); |
| else |
| bp->site->dec(cx->runtime()->defaultFreeOp()); |
| } |
| |
| /* |
| * Add or remove ourselves from the runtime's list of Debuggers |
| * that care about new globals. |
| */ |
| if (dbg->getHook(OnNewGlobalObject)) { |
| if (!wasEnabled) { |
| /* If we were not enabled, the link should be a singleton list. */ |
| MOZ_ASSERT(JS_CLIST_IS_EMPTY(&dbg->onNewGlobalObjectWatchersLink)); |
| JS_APPEND_LINK(&dbg->onNewGlobalObjectWatchersLink, |
| &cx->runtime()->onNewGlobalObjectWatchers); |
| } else { |
| /* If we were enabled, the link should be inserted in the list. */ |
| MOZ_ASSERT(!JS_CLIST_IS_EMPTY(&dbg->onNewGlobalObjectWatchersLink)); |
| JS_REMOVE_AND_INIT_LINK(&dbg->onNewGlobalObjectWatchersLink); |
| } |
| } |
| |
| // Ensure the compartment is observable if we are re-enabling a |
| // Debugger with hooks that observe all execution. |
| if (!dbg->updateObservesAllExecutionOnDebuggees(cx, dbg->observesAllExecution())) |
| return false; |
| |
| // Note: To toogle code coverage, we currently need to have no live |
| // stack frame, thus the coverage does not depend on the enabled flag. |
| |
| dbg->updateObservesAsmJSOnDebuggees(dbg->observesAsmJS()); |
| } |
| |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::getHookImpl(JSContext* cx, CallArgs& args, Debugger& dbg, Hook which) |
| { |
| MOZ_ASSERT(which >= 0 && which < HookCount); |
| args.rval().set(dbg.object->getReservedSlot(JSSLOT_DEBUG_HOOK_START + which)); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::setHookImpl(JSContext* cx, CallArgs& args, Debugger& dbg, Hook which) |
| { |
| MOZ_ASSERT(which >= 0 && which < HookCount); |
| if (!args.requireAtLeast(cx, "Debugger.setHook", 1)) |
| return false; |
| if (args[0].isObject()) { |
| if (!args[0].toObject().isCallable()) |
| return ReportIsNotFunction(cx, args[0], args.length() - 1); |
| } else if (!args[0].isUndefined()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NOT_CALLABLE_OR_UNDEFINED); |
| return false; |
| } |
| dbg.object->setReservedSlot(JSSLOT_DEBUG_HOOK_START + which, args[0]); |
| if (hookObservesAllExecution(which)) { |
| if (!dbg.updateObservesAllExecutionOnDebuggees(cx, dbg.observesAllExecution())) |
| return false; |
| } |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::getOnDebuggerStatement(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "(get onDebuggerStatement)", args, dbg); |
| return getHookImpl(cx, args, *dbg, OnDebuggerStatement); |
| } |
| |
| /* static */ bool |
| Debugger::setOnDebuggerStatement(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "(set onDebuggerStatement)", args, dbg); |
| return setHookImpl(cx, args, *dbg, OnDebuggerStatement); |
| } |
| |
| /* static */ bool |
| Debugger::getOnExceptionUnwind(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "(get onExceptionUnwind)", args, dbg); |
| return getHookImpl(cx, args, *dbg, OnExceptionUnwind); |
| } |
| |
| /* static */ bool |
| Debugger::setOnExceptionUnwind(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "(set onExceptionUnwind)", args, dbg); |
| return setHookImpl(cx, args, *dbg, OnExceptionUnwind); |
| } |
| |
| /* static */ bool |
| Debugger::getOnNewScript(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "(get onNewScript)", args, dbg); |
| return getHookImpl(cx, args, *dbg, OnNewScript); |
| } |
| |
| /* static */ bool |
| Debugger::setOnNewScript(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "(set onNewScript)", args, dbg); |
| return setHookImpl(cx, args, *dbg, OnNewScript); |
| } |
| |
| /* static */ bool |
| Debugger::getOnNewPromise(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "(get onNewPromise)", args, dbg); |
| return getHookImpl(cx, args, *dbg, OnNewPromise); |
| } |
| |
| /* static */ bool |
| Debugger::setOnNewPromise(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "(set onNewPromise)", args, dbg); |
| return setHookImpl(cx, args, *dbg, OnNewPromise); |
| } |
| |
| /* static */ bool |
| Debugger::getOnPromiseSettled(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "(get onPromiseSettled)", args, dbg); |
| return getHookImpl(cx, args, *dbg, OnPromiseSettled); |
| } |
| |
| /* static */ bool |
| Debugger::setOnPromiseSettled(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "(set onPromiseSettled)", args, dbg); |
| return setHookImpl(cx, args, *dbg, OnPromiseSettled); |
| } |
| |
| /* static */ bool |
| Debugger::getOnEnterFrame(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "(get onEnterFrame)", args, dbg); |
| return getHookImpl(cx, args, *dbg, OnEnterFrame); |
| } |
| |
| /* static */ bool |
| Debugger::setOnEnterFrame(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "(set onEnterFrame)", args, dbg); |
| return setHookImpl(cx, args, *dbg, OnEnterFrame); |
| } |
| |
| /* static */ bool |
| Debugger::getOnNewGlobalObject(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "(get onNewGlobalObject)", args, dbg); |
| return getHookImpl(cx, args, *dbg, OnNewGlobalObject); |
| } |
| |
| /* static */ bool |
| Debugger::setOnNewGlobalObject(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "setOnNewGlobalObject", args, dbg); |
| RootedObject oldHook(cx, dbg->getHook(OnNewGlobalObject)); |
| |
| if (!setHookImpl(cx, args, *dbg, OnNewGlobalObject)) |
| return false; |
| |
| /* |
| * Add or remove ourselves from the runtime's list of Debuggers that |
| * care about new globals. |
| */ |
| if (dbg->enabled) { |
| JSObject* newHook = dbg->getHook(OnNewGlobalObject); |
| if (!oldHook && newHook) { |
| /* If we didn't have a hook, the link should be a singleton list. */ |
| MOZ_ASSERT(JS_CLIST_IS_EMPTY(&dbg->onNewGlobalObjectWatchersLink)); |
| JS_APPEND_LINK(&dbg->onNewGlobalObjectWatchersLink, |
| &cx->runtime()->onNewGlobalObjectWatchers); |
| } else if (oldHook && !newHook) { |
| /* If we did have a hook, the link should be inserted in the list. */ |
| MOZ_ASSERT(!JS_CLIST_IS_EMPTY(&dbg->onNewGlobalObjectWatchersLink)); |
| JS_REMOVE_AND_INIT_LINK(&dbg->onNewGlobalObjectWatchersLink); |
| } |
| } |
| |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::getUncaughtExceptionHook(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "get uncaughtExceptionHook", args, dbg); |
| args.rval().setObjectOrNull(dbg->uncaughtExceptionHook); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::setUncaughtExceptionHook(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "set uncaughtExceptionHook", args, dbg); |
| if (!args.requireAtLeast(cx, "Debugger.set uncaughtExceptionHook", 1)) |
| return false; |
| if (!args[0].isNull() && (!args[0].isObject() || !args[0].toObject().isCallable())) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_ASSIGN_FUNCTION_OR_NULL, |
| "uncaughtExceptionHook"); |
| return false; |
| } |
| dbg->uncaughtExceptionHook = args[0].toObjectOrNull(); |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::getAllowUnobservedAsmJS(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "get allowUnobservedAsmJS", args, dbg); |
| args.rval().setBoolean(dbg->allowUnobservedAsmJS); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::setAllowUnobservedAsmJS(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "set allowUnobservedAsmJS", args, dbg); |
| if (!args.requireAtLeast(cx, "Debugger.set allowUnobservedAsmJS", 1)) |
| return false; |
| dbg->allowUnobservedAsmJS = ToBoolean(args[0]); |
| |
| for (WeakGlobalObjectSet::Range r = dbg->debuggees.all(); !r.empty(); r.popFront()) { |
| GlobalObject* global = r.front(); |
| JSCompartment* comp = global->compartment(); |
| comp->updateDebuggerObservesAsmJS(); |
| } |
| |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::getCollectCoverageInfo(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "get collectCoverageInfo", args, dbg); |
| args.rval().setBoolean(dbg->collectCoverageInfo); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::setCollectCoverageInfo(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "set collectCoverageInfo", args, dbg); |
| if (!args.requireAtLeast(cx, "Debugger.set collectCoverageInfo", 1)) |
| return false; |
| dbg->collectCoverageInfo = ToBoolean(args[0]); |
| |
| IsObserving observing = dbg->collectCoverageInfo ? Observing : NotObserving; |
| if (!dbg->updateObservesCoverageOnDebuggees(cx, observing)) |
| return false; |
| |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::getMemory(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "get memory", args, dbg); |
| Value memoryValue = dbg->object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE); |
| |
| if (!memoryValue.isObject()) { |
| RootedObject memory(cx, DebuggerMemory::create(cx, dbg)); |
| if (!memory) |
| return false; |
| memoryValue = ObjectValue(*memory); |
| } |
| |
| args.rval().set(memoryValue); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::getOnIonCompilation(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "(get onIonCompilation)", args, dbg); |
| return getHookImpl(cx, args, *dbg, OnIonCompilation); |
| } |
| |
| /* static */ bool |
| Debugger::setOnIonCompilation(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "(set onIonCompilation)", args, dbg); |
| return setHookImpl(cx, args, *dbg, OnIonCompilation); |
| } |
| |
| /* |
| * Given a value used to designate a global (there's quite a variety; see the |
| * docs), return the actual designee. |
| * |
| * Note that this does not check whether the designee is marked "invisible to |
| * Debugger" or not; different callers need to handle invisible-to-Debugger |
| * globals in different ways. |
| */ |
| GlobalObject* |
| Debugger::unwrapDebuggeeArgument(JSContext* cx, const Value& v) |
| { |
| if (!v.isObject()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, |
| "argument", "not a global object"); |
| return nullptr; |
| } |
| |
| RootedObject obj(cx, &v.toObject()); |
| |
| /* If it's a Debugger.Object belonging to this debugger, dereference that. */ |
| if (obj->getClass() == &DebuggerObject_class) { |
| RootedValue rv(cx, v); |
| if (!unwrapDebuggeeValue(cx, &rv)) |
| return nullptr; |
| obj = &rv.toObject(); |
| } |
| |
| /* If we have a cross-compartment wrapper, dereference as far as is secure. */ |
| obj = CheckedUnwrap(obj); |
| if (!obj) { |
| JS_ReportError(cx, "Permission denied to access object"); |
| return nullptr; |
| } |
| |
| /* If that produced a WindowProxy, get the Window (global). */ |
| obj = ToWindowIfWindowProxy(obj); |
| |
| /* If that didn't produce a global object, it's an error. */ |
| if (!obj->is<GlobalObject>()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, |
| "argument", "not a global object"); |
| return nullptr; |
| } |
| |
| return &obj->as<GlobalObject>(); |
| } |
| |
| /* static */ bool |
| Debugger::addDebuggee(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "addDebuggee", args, dbg); |
| if (!args.requireAtLeast(cx, "Debugger.addDebuggee", 1)) |
| return false; |
| Rooted<GlobalObject*> global(cx, dbg->unwrapDebuggeeArgument(cx, args[0])); |
| if (!global) |
| return false; |
| |
| if (!dbg->addDebuggeeGlobal(cx, global)) |
| return false; |
| |
| RootedValue v(cx, ObjectValue(*global)); |
| if (!dbg->wrapDebuggeeValue(cx, &v)) |
| return false; |
| args.rval().set(v); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::addAllGlobalsAsDebuggees(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "addAllGlobalsAsDebuggees", args, dbg); |
| for (ZonesIter zone(cx->runtime(), SkipAtoms); !zone.done(); zone.next()) { |
| for (CompartmentsInZoneIter c(zone); !c.done(); c.next()) { |
| if (c == dbg->object->compartment() || c->options().invisibleToDebugger()) |
| continue; |
| c->scheduledForDestruction = false; |
| GlobalObject* global = c->maybeGlobal(); |
| if (global) { |
| Rooted<GlobalObject*> rg(cx, global); |
| if (!dbg->addDebuggeeGlobal(cx, rg)) |
| return false; |
| } |
| } |
| } |
| |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::removeDebuggee(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "removeDebuggee", args, dbg); |
| |
| if (!args.requireAtLeast(cx, "Debugger.removeDebuggee", 1)) |
| return false; |
| Rooted<GlobalObject*> global(cx, dbg->unwrapDebuggeeArgument(cx, args[0])); |
| if (!global) |
| return false; |
| |
| ExecutionObservableCompartments obs(cx); |
| if (!obs.init()) |
| return false; |
| |
| if (dbg->debuggees.has(global)) { |
| dbg->removeDebuggeeGlobal(cx->runtime()->defaultFreeOp(), global, nullptr); |
| |
| // Only update the compartment if there are no Debuggers left, as it's |
| // expensive to check if no other Debugger has a live script or frame hook |
| // on any of the current on-stack debuggee frames. |
| if (global->getDebuggers()->empty() && !obs.add(global->compartment())) |
| return false; |
| if (!updateExecutionObservability(cx, obs, NotObserving)) |
| return false; |
| } |
| |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::removeAllDebuggees(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "removeAllDebuggees", args, dbg); |
| |
| ExecutionObservableCompartments obs(cx); |
| if (!obs.init()) |
| return false; |
| |
| for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty(); e.popFront()) { |
| Rooted<GlobalObject*> global(cx, e.front()); |
| dbg->removeDebuggeeGlobal(cx->runtime()->defaultFreeOp(), global, &e); |
| |
| // See note about adding to the observable set in removeDebuggee. |
| if (global->getDebuggers()->empty() && !obs.add(global->compartment())) |
| return false; |
| } |
| |
| if (!updateExecutionObservability(cx, obs, NotObserving)) |
| return false; |
| |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::hasDebuggee(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "hasDebuggee", args, dbg); |
| if (!args.requireAtLeast(cx, "Debugger.hasDebuggee", 1)) |
| return false; |
| GlobalObject* global = dbg->unwrapDebuggeeArgument(cx, args[0]); |
| if (!global) |
| return false; |
| args.rval().setBoolean(!!dbg->debuggees.lookup(global)); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::getDebuggees(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "getDebuggees", args, dbg); |
| |
| // Obtain the list of debuggees before wrapping each debuggee, as a GC could |
| // update the debuggees set while we are iterating it. |
| unsigned count = dbg->debuggees.count(); |
| AutoValueVector debuggees(cx); |
| if (!debuggees.resize(count)) |
| return false; |
| unsigned i = 0; |
| { |
| JS::AutoCheckCannotGC nogc; |
| for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty(); e.popFront()) |
| debuggees[i++].setObject(*e.front().get()); |
| } |
| |
| RootedArrayObject arrobj(cx, NewDenseFullyAllocatedArray(cx, count)); |
| if (!arrobj) |
| return false; |
| arrobj->ensureDenseInitializedLength(cx, 0, count); |
| for (i = 0; i < count; i++) { |
| RootedValue v(cx, debuggees[i]); |
| if (!dbg->wrapDebuggeeValue(cx, &v)) |
| return false; |
| arrobj->setDenseElement(i, v); |
| } |
| |
| args.rval().setObject(*arrobj); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::getNewestFrame(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "getNewestFrame", args, dbg); |
| |
| /* Since there may be multiple contexts, use AllFramesIter. */ |
| for (AllFramesIter i(cx); !i.done(); ++i) { |
| if (dbg->observesFrame(i)) { |
| // Ensure that Ion frames are rematerialized. Only rematerialized |
| // Ion frames may be used as AbstractFramePtrs. |
| if (i.isIon() && !i.ensureHasRematerializedFrame(cx)) |
| return false; |
| AbstractFramePtr frame = i.abstractFramePtr(); |
| ScriptFrameIter iter(i.activation()->cx(), ScriptFrameIter::GO_THROUGH_SAVED); |
| while (!iter.hasUsableAbstractFramePtr() || iter.abstractFramePtr() != frame) |
| ++iter; |
| return dbg->getScriptFrame(cx, iter, args.rval()); |
| } |
| } |
| args.rval().setNull(); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::clearAllBreakpoints(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "clearAllBreakpoints", args, dbg); |
| for (WeakGlobalObjectSet::Range r = dbg->debuggees.all(); !r.empty(); r.popFront()) |
| r.front()->compartment()->clearBreakpointsIn(cx->runtime()->defaultFreeOp(), |
| dbg, nullptr); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::construct(JSContext* cx, unsigned argc, Value* vp) |
| { |
| CallArgs args = CallArgsFromVp(argc, vp); |
| |
| /* Check that the arguments, if any, are cross-compartment wrappers. */ |
| for (unsigned i = 0; i < args.length(); i++) { |
| JSObject* argobj = NonNullObject(cx, args[i]); |
| if (!argobj) |
| return false; |
| if (!argobj->is<CrossCompartmentWrapperObject>()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_CCW_REQUIRED, |
| "Debugger"); |
| return false; |
| } |
| } |
| |
| /* Get Debugger.prototype. */ |
| RootedValue v(cx); |
| RootedObject callee(cx, &args.callee()); |
| if (!GetProperty(cx, callee, callee, cx->names().prototype, &v)) |
| return false; |
| RootedNativeObject proto(cx, &v.toObject().as<NativeObject>()); |
| MOZ_ASSERT(proto->getClass() == &Debugger::jsclass); |
| /* |
| * Make the new Debugger object. Each one has a reference to |
| * Debugger.{Frame,Object,Script,Memory}.prototype in reserved slots. The |
| * rest of the reserved slots are for hooks; they default to undefined. |
| */ |
| RootedNativeObject obj(cx, NewNativeObjectWithGivenProto(cx, &Debugger::jsclass, proto)); |
| if (!obj) |
| return false; |
| for (unsigned slot = JSSLOT_DEBUG_PROTO_START; slot < JSSLOT_DEBUG_PROTO_STOP; slot++) |
| obj->setReservedSlot(slot, proto->getReservedSlot(slot)); |
| obj->setReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE, NullValue()); |
| |
| Debugger* debugger; |
| { |
| /* Construct the underlying C++ object. */ |
| AutoInitGCManagedObject<Debugger> dbg(cx->make_unique<Debugger>(cx, obj.get())); |
| if (!dbg || !dbg->init(cx)) |
| return false; |
| |
| debugger = dbg.release(); |
| obj->setPrivate(debugger); // owns the released pointer |
| } |
| |
| /* Add the initial debuggees, if any. */ |
| for (unsigned i = 0; i < args.length(); i++) { |
| Rooted<GlobalObject*> |
| debuggee(cx, &args[i].toObject().as<ProxyObject>().private_().toObject().global()); |
| if (!debugger->addDebuggeeGlobal(cx, debuggee)) |
| return false; |
| } |
| |
| args.rval().setObject(*obj); |
| return true; |
| } |
| |
| bool |
| Debugger::addDebuggeeGlobal(JSContext* cx, Handle<GlobalObject*> global) |
| { |
| if (debuggees.has(global)) |
| return true; |
| |
| // Callers should generally be unable to get a reference to a debugger- |
| // invisible global in order to pass it to addDebuggee. But this is possible |
| // with certain testing aides we expose in the shell, so just make addDebuggee |
| // throw in that case. |
| JSCompartment* debuggeeCompartment = global->compartment(); |
| if (debuggeeCompartment->options().invisibleToDebugger()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, |
| JSMSG_DEBUG_CANT_DEBUG_GLOBAL); |
| return false; |
| } |
| |
| /* |
| * Check for cycles. If global's compartment is reachable from this |
| * Debugger object's compartment by following debuggee-to-debugger links, |
| * then adding global would create a cycle. (Typically nobody is debugging |
| * the debugger, in which case we zip through this code without looping.) |
| */ |
| Vector<JSCompartment*> visited(cx); |
| if (!visited.append(object->compartment())) |
| return false; |
| for (size_t i = 0; i < visited.length(); i++) { |
| JSCompartment* c = visited[i]; |
| if (c == debuggeeCompartment) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_LOOP); |
| return false; |
| } |
| |
| /* |
| * Find all compartments containing debuggers debugging c's global |
| * object. Add those compartments to visited. |
| */ |
| if (c->isDebuggee()) { |
| GlobalObject::DebuggerVector* v = c->maybeGlobal()->getDebuggers(); |
| for (Debugger** p = v->begin(); p != v->end(); p++) { |
| JSCompartment* next = (*p)->object->compartment(); |
| if (Find(visited, next) == visited.end() && !visited.append(next)) |
| return false; |
| } |
| } |
| } |
| |
| /* |
| * For global to become this js::Debugger's debuggee: |
| * |
| * 1. this js::Debugger must be in global->getDebuggers(), |
| * 2. global must be in this->debuggees, |
| * 3. it must be in zone->getDebuggers(), |
| * 4. the debuggee's zone must be in this->debuggeeZones, |
| * 5. if we are tracking allocations, the SavedStacksMetadataCallback must be |
| * installed for this compartment, and |
| * 6. JSCompartment::isDebuggee()'s bit must be set. |
| * |
| * All six indications must be kept consistent. |
| */ |
| |
| AutoCompartment ac(cx, global); |
| Zone* zone = global->zone(); |
| |
| // (1) |
| auto* globalDebuggers = GlobalObject::getOrCreateDebuggers(cx, global); |
| if (!globalDebuggers) |
| return false; |
| if (!globalDebuggers->append(this)) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| auto globalDebuggersGuard = MakeScopeExit([&] { |
| globalDebuggers->popBack(); |
| }); |
| |
| // (2) |
| if (!debuggees.put(global)) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| auto debuggeesGuard = MakeScopeExit([&] { |
| debuggees.remove(global); |
| }); |
| |
| bool addingZoneRelation = !debuggeeZones.has(zone); |
| |
| // (3) |
| auto* zoneDebuggers = zone->getOrCreateDebuggers(cx); |
| if (!zoneDebuggers) |
| return false; |
| if (addingZoneRelation && !zoneDebuggers->append(this)) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| auto zoneDebuggersGuard = MakeScopeExit([&] { |
| if (addingZoneRelation) |
| zoneDebuggers->popBack(); |
| }); |
| |
| // (4) |
| if (addingZoneRelation && !debuggeeZones.put(zone)) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| auto debuggeeZonesGuard = MakeScopeExit([&] { |
| if (addingZoneRelation) |
| debuggeeZones.remove(zone); |
| }); |
| |
| // (5) |
| if (trackingAllocationSites && enabled && !Debugger::addAllocationsTracking(cx, global)) |
| return false; |
| |
| auto allocationsTrackingGuard = MakeScopeExit([&] { |
| if (trackingAllocationSites && enabled) |
| Debugger::removeAllocationsTracking(*global); |
| }); |
| |
| // (6) |
| debuggeeCompartment->setIsDebuggee(); |
| debuggeeCompartment->updateDebuggerObservesAsmJS(); |
| debuggeeCompartment->updateDebuggerObservesCoverage(); |
| if (observesAllExecution() && !ensureExecutionObservabilityOfCompartment(cx, debuggeeCompartment)) |
| return false; |
| |
| globalDebuggersGuard.release(); |
| debuggeesGuard.release(); |
| zoneDebuggersGuard.release(); |
| debuggeeZonesGuard.release(); |
| allocationsTrackingGuard.release(); |
| return true; |
| } |
| |
| void |
| Debugger::recomputeDebuggeeZoneSet() |
| { |
| AutoEnterOOMUnsafeRegion oomUnsafe; |
| debuggeeZones.clear(); |
| for (auto range = debuggees.all(); !range.empty(); range.popFront()) { |
| if (!debuggeeZones.put(range.front().unbarrieredGet()->zone())) |
| oomUnsafe.crash("Debugger::removeDebuggeeGlobal"); |
| } |
| } |
| |
| template<typename V> |
| static Debugger** |
| findDebuggerInVector(Debugger* dbg, V* vec) |
| { |
| Debugger** p; |
| for (p = vec->begin(); p != vec->end(); p++) { |
| if (*p == dbg) |
| break; |
| } |
| MOZ_ASSERT(p != vec->end()); |
| return p; |
| } |
| |
| void |
| Debugger::removeDebuggeeGlobal(FreeOp* fop, GlobalObject* global, |
| WeakGlobalObjectSet::Enum* debugEnum) |
| { |
| /* |
| * The caller might have found global by enumerating this->debuggees; if |
| * so, use HashSet::Enum::removeFront rather than HashSet::remove below, |
| * to avoid invalidating the live enumerator. |
| */ |
| MOZ_ASSERT(debuggees.has(global)); |
| MOZ_ASSERT(debuggeeZones.has(global->zone())); |
| MOZ_ASSERT_IF(debugEnum, debugEnum->front().unbarrieredGet() == global); |
| |
| /* |
| * FIXME Debugger::slowPathOnLeaveFrame needs to kill all Debugger.Frame |
| * objects referring to a particular JS stack frame. This is hard if |
| * Debugger objects that are no longer debugging the relevant global might |
| * have live Frame objects. So we take the easy way out and kill them here. |
| * This is a bug, since it's observable and contrary to the spec. One |
| * possible fix would be to put such objects into a compartment-wide bag |
| * which slowPathOnLeaveFrame would have to examine. |
| */ |
| for (FrameMap::Enum e(frames); !e.empty(); e.popFront()) { |
| AbstractFramePtr frame = e.front().key(); |
| NativeObject* frameobj = e.front().value(); |
| if (&frame.script()->global() == global) { |
| DebuggerFrame_freeScriptFrameIterData(fop, frameobj); |
| DebuggerFrame_maybeDecrementFrameScriptStepModeCount(fop, frame, frameobj); |
| e.removeFront(); |
| } |
| } |
| |
| auto *globalDebuggersVector = global->getDebuggers(); |
| auto *zoneDebuggersVector = global->zone()->getDebuggers(); |
| |
| /* |
| * The relation must be removed from up to three places: |
| * globalDebuggersVector and debuggees for sure, and possibly the |
| * compartment's debuggee set. |
| * |
| * The debuggee zone set is recomputed on demand. This avoids refcounting |
| * and in practice we have relatively few debuggees that tend to all be in |
| * the same zone. If after recomputing the debuggee zone set, this global's |
| * zone is not in the set, then we must remove ourselves from the zone's |
| * vector of observing debuggers. |
| */ |
| globalDebuggersVector->erase(findDebuggerInVector(this, globalDebuggersVector)); |
| |
| if (debugEnum) |
| debugEnum->removeFront(); |
| else |
| debuggees.remove(global); |
| |
| recomputeDebuggeeZoneSet(); |
| |
| if (!debuggeeZones.has(global->zone())) |
| zoneDebuggersVector->erase(findDebuggerInVector(this, zoneDebuggersVector)); |
| |
| /* Remove all breakpoints for the debuggee. */ |
| Breakpoint* nextbp; |
| for (Breakpoint* bp = firstBreakpoint(); bp; bp = nextbp) { |
| nextbp = bp->nextInDebugger(); |
| if (bp->site->script->compartment() == global->compartment()) |
| bp->destroy(fop); |
| } |
| MOZ_ASSERT_IF(debuggees.empty(), !firstBreakpoint()); |
| |
| /* |
| * If we are tracking allocation sites, we need to remove the object |
| * metadata callback from this global's compartment. |
| */ |
| if (trackingAllocationSites) |
| Debugger::removeAllocationsTracking(*global); |
| |
| if (global->getDebuggers()->empty()) { |
| global->compartment()->unsetIsDebuggee(); |
| } else { |
| global->compartment()->updateDebuggerObservesAllExecution(); |
| global->compartment()->updateDebuggerObservesAsmJS(); |
| global->compartment()->updateDebuggerObservesCoverage(); |
| } |
| } |
| |
| |
| static inline ScriptSourceObject* GetSourceReferent(JSObject* obj); |
| |
| /* |
| * A class for parsing 'findScripts' query arguments and searching for |
| * scripts that match the criteria they represent. |
| */ |
| class MOZ_STACK_CLASS Debugger::ScriptQuery |
| { |
| public: |
| /* Construct a ScriptQuery to use matching scripts for |dbg|. */ |
| ScriptQuery(JSContext* cx, Debugger* dbg): |
| cx(cx), debugger(dbg), compartments(cx->runtime()), url(cx), displayURLString(cx), |
| source(cx), innermostForCompartment(cx->runtime()), vector(cx, ScriptVector(cx)) |
| {} |
| |
| /* |
| * Initialize this ScriptQuery. Raise an error and return false if we |
| * haven't enough memory. |
| */ |
| bool init() { |
| if (!compartments.init() || |
| !innermostForCompartment.init()) |
| { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /* |
| * Parse the query object |query|, and prepare to match only the scripts |
| * it specifies. |
| */ |
| bool parseQuery(HandleObject query) { |
| /* |
| * Check for a 'global' property, which limits the results to those |
| * scripts scoped to a particular global object. |
| */ |
| RootedValue global(cx); |
| if (!GetProperty(cx, query, query, cx->names().global, &global)) |
| return false; |
| if (global.isUndefined()) { |
| matchAllDebuggeeGlobals(); |
| } else { |
| GlobalObject* globalObject = debugger->unwrapDebuggeeArgument(cx, global); |
| if (!globalObject) |
| return false; |
| |
| /* |
| * If the given global isn't a debuggee, just leave the set of |
| * acceptable globals empty; we'll return no scripts. |
| */ |
| if (debugger->debuggees.has(globalObject)) { |
| if (!matchSingleGlobal(globalObject)) |
| return false; |
| } |
| } |
| |
| /* Check for a 'url' property. */ |
| if (!GetProperty(cx, query, query, cx->names().url, &url)) |
| return false; |
| if (!url.isUndefined() && !url.isString()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, |
| "query object's 'url' property", "neither undefined nor a string"); |
| return false; |
| } |
| |
| /* Check for a 'source' property */ |
| RootedValue debuggerSource(cx); |
| if (!GetProperty(cx, query, query, cx->names().source, &debuggerSource)) |
| return false; |
| if (!debuggerSource.isUndefined()) { |
| if (!debuggerSource.isObject() || |
| debuggerSource.toObject().getClass() != &DebuggerSource_class) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, |
| "query object's 'source' property", |
| "not undefined nor a Debugger.Source object"); |
| return false; |
| } |
| |
| source = GetSourceReferent(&debuggerSource.toObject()); |
| } |
| |
| /* Check for a 'displayURL' property. */ |
| RootedValue displayURL(cx); |
| if (!GetProperty(cx, query, query, cx->names().displayURL, &displayURL)) |
| return false; |
| if (!displayURL.isUndefined() && !displayURL.isString()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, |
| "query object's 'displayURL' property", |
| "neither undefined nor a string"); |
| return false; |
| } |
| |
| if (displayURL.isString()) { |
| displayURLString = displayURL.toString()->ensureLinear(cx); |
| if (!displayURLString) |
| return false; |
| } |
| |
| /* Check for a 'line' property. */ |
| RootedValue lineProperty(cx); |
| if (!GetProperty(cx, query, query, cx->names().line, &lineProperty)) |
| return false; |
| if (lineProperty.isUndefined()) { |
| hasLine = false; |
| } else if (lineProperty.isNumber()) { |
| if (displayURL.isUndefined() && url.isUndefined() && !source) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, |
| JSMSG_QUERY_LINE_WITHOUT_URL); |
| return false; |
| } |
| double doubleLine = lineProperty.toNumber(); |
| if (doubleLine <= 0 || (unsigned int) doubleLine != doubleLine) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_BAD_LINE); |
| return false; |
| } |
| hasLine = true; |
| line = doubleLine; |
| } else { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, |
| "query object's 'line' property", |
| "neither undefined nor an integer"); |
| return false; |
| } |
| |
| /* Check for an 'innermost' property. */ |
| PropertyName* innermostName = cx->names().innermost; |
| RootedValue innermostProperty(cx); |
| if (!GetProperty(cx, query, query, innermostName, &innermostProperty)) |
| return false; |
| innermost = ToBoolean(innermostProperty); |
| if (innermost) { |
| /* Technically, we need only check hasLine, but this is clearer. */ |
| if ((displayURL.isUndefined() && url.isUndefined() && !source) || !hasLine) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, |
| JSMSG_QUERY_INNERMOST_WITHOUT_LINE_URL); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| /* Set up this ScriptQuery appropriately for a missing query argument. */ |
| bool omittedQuery() { |
| url.setUndefined(); |
| hasLine = false; |
| innermost = false; |
| displayURLString = nullptr; |
| return matchAllDebuggeeGlobals(); |
| } |
| |
| /* |
| * Search all relevant compartments and the stack for scripts matching |
| * this query, and append the matching scripts to |vector|. |
| */ |
| bool findScripts() { |
| if (!prepareQuery()) |
| return false; |
| |
| JSCompartment* singletonComp = nullptr; |
| if (compartments.count() == 1) |
| singletonComp = compartments.all().front(); |
| |
| /* Search each compartment for debuggee scripts. */ |
| MOZ_ASSERT(vector.empty()); |
| oom = false; |
| IterateScripts(cx->runtime(), singletonComp, this, considerScript); |
| if (oom) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| |
| /* We cannot touch the gray bits while isHeapBusy, so do this now. */ |
| for (JSScript** i = vector.begin(); i != vector.end(); ++i) |
| JS::ExposeScriptToActiveJS(*i); |
| |
| /* |
| * For most queries, we just accumulate results in 'vector' as we find |
| * them. But if this is an 'innermost' query, then we've accumulated the |
| * results in the 'innermostForCompartment' map. In that case, we now need to |
| * walk that map and populate 'vector'. |
| */ |
| if (innermost) { |
| for (CompartmentToScriptMap::Range r = innermostForCompartment.all(); |
| !r.empty(); |
| r.popFront()) |
| { |
| JS::ExposeScriptToActiveJS(r.front().value()); |
| if (!vector.append(r.front().value())) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| Handle<ScriptVector> foundScripts() const { |
| return vector; |
| } |
| |
| private: |
| /* The context in which we should do our work. */ |
| JSContext* cx; |
| |
| /* The debugger for which we conduct queries. */ |
| Debugger* debugger; |
| |
| typedef HashSet<JSCompartment*, DefaultHasher<JSCompartment*>, RuntimeAllocPolicy> |
| CompartmentSet; |
| |
| /* A script must be in one of these compartments to match the query. */ |
| CompartmentSet compartments; |
| |
| /* If this is a string, matching scripts have urls equal to it. */ |
| RootedValue url; |
| |
| /* url as a C string. */ |
| JSAutoByteString urlCString; |
| |
| /* If this is a string, matching scripts' sources have displayURLs equal to |
| * it. */ |
| RootedLinearString displayURLString; |
| |
| /* |
| * If this is a source object, matching scripts will have sources |
| * equal to this instance. |
| */ |
| RootedScriptSource source; |
| |
| /* True if the query contained a 'line' property. */ |
| bool hasLine; |
| |
| /* The line matching scripts must cover. */ |
| unsigned int line; |
| |
| /* True if the query has an 'innermost' property whose value is true. */ |
| bool innermost; |
| |
| typedef HashMap<JSCompartment*, JSScript*, DefaultHasher<JSCompartment*>, RuntimeAllocPolicy> |
| CompartmentToScriptMap; |
| |
| /* |
| * For 'innermost' queries, a map from compartments to the innermost script |
| * we've seen so far in that compartment. (Template instantiation code size |
| * explosion ho!) |
| */ |
| CompartmentToScriptMap innermostForCompartment; |
| |
| /* |
| * Accumulate the scripts in an Rooted<ScriptVector>, instead of creating |
| * the JS array as we go, because we mustn't allocate JS objects or GC |
| * while we use the CellIter. |
| */ |
| Rooted<ScriptVector> vector; |
| |
| /* Indicates whether OOM has occurred while matching. */ |
| bool oom; |
| |
| bool addCompartment(JSCompartment* comp) { |
| { |
| // All scripts in the debuggee compartment must be visible, so |
| // delazify everything. |
| AutoCompartment ac(cx, comp); |
| if (!comp->ensureDelazifyScriptsForDebugger(cx)) |
| return false; |
| } |
| return compartments.put(comp); |
| } |
| |
| /* Arrange for this ScriptQuery to match only scripts that run in |global|. */ |
| bool matchSingleGlobal(GlobalObject* global) { |
| MOZ_ASSERT(compartments.count() == 0); |
| if (!addCompartment(global->compartment())) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| return true; |
| } |
| |
| /* |
| * Arrange for this ScriptQuery to match all scripts running in debuggee |
| * globals. |
| */ |
| bool matchAllDebuggeeGlobals() { |
| MOZ_ASSERT(compartments.count() == 0); |
| /* Build our compartment set from the debugger's set of debuggee globals. */ |
| for (WeakGlobalObjectSet::Range r = debugger->debuggees.all(); !r.empty(); r.popFront()) { |
| if (!addCompartment(r.front()->compartment())) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /* |
| * Given that parseQuery or omittedQuery has been called, prepare to match |
| * scripts. Set urlCString and displayURLChars as appropriate. |
| */ |
| bool prepareQuery() { |
| /* Compute urlCString and displayURLChars, if a url or displayURL was |
| * given respectively. */ |
| if (url.isString()) { |
| if (!urlCString.encodeLatin1(cx, url.toString())) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static void considerScript(JSRuntime* rt, void* data, JSScript* script) { |
| ScriptQuery* self = static_cast<ScriptQuery*>(data); |
| self->consider(script); |
| } |
| |
| /* |
| * If |script| matches this query, append it to |vector| or place it in |
| * |innermostForCompartment|, as appropriate. Set |oom| if an out of memory |
| * condition occurred. |
| */ |
| void consider(JSScript* script) { |
| // We check for presence of script->code() because it is possible that |
| // the script was created and thus exposed to GC, but *not* fully |
| // initialized from fullyInit{FromEmitter,Trivial} due to errors. |
| if (oom || script->selfHosted() || !script->code()) |
| return; |
| JSCompartment* compartment = script->compartment(); |
| if (!compartments.has(compartment)) |
| return; |
| if (urlCString.ptr()) { |
| bool gotFilename = false; |
| if (script->filename() && strcmp(script->filename(), urlCString.ptr()) == 0) |
| gotFilename = true; |
| |
| bool gotSourceURL = false; |
| if (!gotFilename && script->scriptSource()->introducerFilename() && |
| strcmp(script->scriptSource()->introducerFilename(), urlCString.ptr()) == 0) |
| { |
| gotSourceURL = true; |
| } |
| if (!gotFilename && !gotSourceURL) |
| return; |
| } |
| if (hasLine) { |
| if (line < script->lineno() || script->lineno() + GetScriptLineExtent(script) < line) |
| return; |
| } |
| if (displayURLString) { |
| if (!script->scriptSource() || !script->scriptSource()->hasDisplayURL()) |
| return; |
| |
| const char16_t* s = script->scriptSource()->displayURL(); |
| if (CompareChars(s, js_strlen(s), displayURLString) != 0) |
| return; |
| } |
| if (source && source != script->sourceObject()) |
| return; |
| |
| if (innermost) { |
| /* |
| * For 'innermost' queries, we don't place scripts in |vector| right |
| * away; we may later find another script that is nested inside this |
| * one. Instead, we record the innermost script we've found so far |
| * for each compartment in innermostForCompartment, and only |
| * populate |vector| at the bottom of findScripts, when we've |
| * traversed all the scripts. |
| * |
| * So: check this script against the innermost one we've found so |
| * far (if any), as recorded in innermostForCompartment, and replace |
| * that if it's better. |
| */ |
| CompartmentToScriptMap::AddPtr p = innermostForCompartment.lookupForAdd(compartment); |
| if (p) { |
| /* Is our newly found script deeper than the last one we found? */ |
| JSScript* incumbent = p->value(); |
| if (StaticScopeChainLength(script->innermostStaticScope()) > |
| StaticScopeChainLength(incumbent->innermostStaticScope())) |
| { |
| p->value() = script; |
| } |
| } else { |
| /* |
| * This is the first matching script we've encountered for this |
| * compartment, so it is thus the innermost such script. |
| */ |
| if (!innermostForCompartment.add(p, compartment, script)) { |
| oom = true; |
| return; |
| } |
| } |
| } else { |
| /* Record this matching script in the results vector. */ |
| if (!vector.append(script)) { |
| oom = true; |
| return; |
| } |
| } |
| |
| return; |
| } |
| }; |
| |
| /* static */ bool |
| Debugger::findScripts(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "findScripts", args, dbg); |
| |
| ScriptQuery query(cx, dbg); |
| if (!query.init()) |
| return false; |
| |
| if (args.length() >= 1) { |
| RootedObject queryObject(cx, NonNullObject(cx, args[0])); |
| if (!queryObject || !query.parseQuery(queryObject)) |
| return false; |
| } else { |
| if (!query.omittedQuery()) |
| return false; |
| } |
| |
| if (!query.findScripts()) |
| return false; |
| |
| Handle<ScriptVector> scripts(query.foundScripts()); |
| RootedArrayObject result(cx, NewDenseFullyAllocatedArray(cx, scripts.length())); |
| if (!result) |
| return false; |
| |
| result->ensureDenseInitializedLength(cx, 0, scripts.length()); |
| |
| for (size_t i = 0; i < scripts.length(); i++) { |
| JSObject* scriptObject = dbg->wrapScript(cx, scripts[i]); |
| if (!scriptObject) |
| return false; |
| result->setDenseElement(i, ObjectValue(*scriptObject)); |
| } |
| |
| args.rval().setObject(*result); |
| return true; |
| } |
| |
| /* |
| * A class for parsing 'findObjects' query arguments and searching for objects |
| * that match the criteria they represent. |
| */ |
| class MOZ_STACK_CLASS Debugger::ObjectQuery |
| { |
| public: |
| /* Construct an ObjectQuery to use matching scripts for |dbg|. */ |
| ObjectQuery(JSContext* cx, Debugger* dbg) : |
| objects(cx), cx(cx), dbg(dbg), className(cx) |
| { } |
| |
| /* The vector that we are accumulating results in. */ |
| AutoObjectVector objects; |
| |
| /* |
| * Parse the query object |query|, and prepare to match only the objects it |
| * specifies. |
| */ |
| bool parseQuery(HandleObject query) { |
| /* Check for the 'class' property */ |
| RootedValue cls(cx); |
| if (!GetProperty(cx, query, query, cx->names().class_, &cls)) |
| return false; |
| if (!cls.isUndefined()) { |
| if (!cls.isString()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, |
| "query object's 'class' property", |
| "neither undefined nor a string"); |
| return false; |
| } |
| className = cls; |
| } |
| return true; |
| } |
| |
| /* Set up this ObjectQuery appropriately for a missing query argument. */ |
| void omittedQuery() { |
| className.setUndefined(); |
| } |
| |
| /* |
| * Traverse the heap to find all relevant objects and add them to the |
| * provided vector. |
| */ |
| bool findObjects() { |
| if (!prepareQuery()) |
| return false; |
| |
| { |
| /* |
| * We can't tolerate the GC moving things around while we're |
| * searching the heap. Check that nothing we do causes a GC. |
| */ |
| Maybe<JS::AutoCheckCannotGC> maybeNoGC; |
| RootedObject dbgObj(cx, dbg->object); |
| JS::ubi::RootList rootList(cx->runtime(), maybeNoGC); |
| if (!rootList.init(dbgObj)) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| |
| Traversal traversal(cx->runtime(), *this, maybeNoGC.ref()); |
| if (!traversal.init()) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| traversal.wantNames = false; |
| |
| return traversal.addStart(JS::ubi::Node(&rootList)) && |
| traversal.traverse(); |
| } |
| } |
| |
| /* |
| * |ubi::Node::BreadthFirst| interface. |
| */ |
| class NodeData {}; |
| typedef JS::ubi::BreadthFirst<ObjectQuery> Traversal; |
| bool operator() (Traversal& traversal, JS::ubi::Node origin, const JS::ubi::Edge& edge, |
| NodeData*, bool first) |
| { |
| if (!first) |
| return true; |
| |
| JS::ubi::Node referent = edge.referent; |
| /* |
| * Only follow edges within our set of debuggee compartments; we don't |
| * care about the heap's subgraphs outside of our debuggee compartments, |
| * so we abandon the referent. Either (1) there is not a path from this |
| * non-debuggee node back to a node in our debuggee compartments, and we |
| * don't need to follow edges to or from this node, or (2) there does |
| * exist some path from this non-debuggee node back to a node in our |
| * debuggee compartments. However, if that were true, then the incoming |
| * cross compartment edge back into a debuggee compartment is already |
| * listed as an edge in the RootList we started traversal with, and |
| * therefore we don't need to follow edges to or from this non-debuggee |
| * node. |
| */ |
| JSCompartment* comp = referent.compartment(); |
| if (comp && !dbg->isDebuggeeUnbarriered(comp)) { |
| traversal.abandonReferent(); |
| return true; |
| } |
| |
| /* |
| * If the referent is an object and matches our query's restrictions, |
| * add it to the vector accumulating results. Skip objects that should |
| * never be exposed to JS, like ScopeObjects and internal functions. |
| */ |
| |
| if (!referent.is<JSObject>() || referent.exposeToJS().isUndefined()) |
| return true; |
| |
| JSObject* obj = referent.as<JSObject>(); |
| |
| if (!className.isUndefined()) { |
| const char* objClassName = obj->getClass()->name; |
| if (strcmp(objClassName, classNameCString.ptr()) != 0) |
| return true; |
| } |
| |
| return objects.append(obj); |
| } |
| |
| private: |
| /* The context in which we should do our work. */ |
| JSContext* cx; |
| |
| /* The debugger for which we conduct queries. */ |
| Debugger* dbg; |
| |
| /* |
| * If this is non-null, matching objects will have a class whose name is |
| * this property. |
| */ |
| RootedValue className; |
| |
| /* The className member, as a C string. */ |
| JSAutoByteString classNameCString; |
| |
| /* |
| * Given that either omittedQuery or parseQuery has been called, prepare the |
| * query for matching objects. |
| */ |
| bool prepareQuery() { |
| if (className.isString()) { |
| if (!classNameCString.encodeLatin1(cx, className.toString())) |
| return false; |
| } |
| |
| return true; |
| } |
| }; |
| |
| bool |
| Debugger::findObjects(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "findObjects", args, dbg); |
| |
| ObjectQuery query(cx, dbg); |
| |
| if (args.length() >= 1) { |
| RootedObject queryObject(cx, NonNullObject(cx, args[0])); |
| if (!queryObject || !query.parseQuery(queryObject)) |
| return false; |
| } else { |
| query.omittedQuery(); |
| } |
| |
| if (!query.findObjects()) |
| return false; |
| |
| size_t length = query.objects.length(); |
| RootedArrayObject result(cx, NewDenseFullyAllocatedArray(cx, length)); |
| if (!result) |
| return false; |
| |
| result->ensureDenseInitializedLength(cx, 0, length); |
| |
| for (size_t i = 0; i < length; i++) { |
| RootedValue debuggeeVal(cx, ObjectValue(*query.objects[i])); |
| if (!dbg->wrapDebuggeeValue(cx, &debuggeeVal)) |
| return false; |
| result->setDenseElement(i, debuggeeVal); |
| } |
| |
| args.rval().setObject(*result); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::findAllGlobals(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "findAllGlobals", args, dbg); |
| |
| AutoObjectVector globals(cx); |
| |
| { |
| // Accumulate the list of globals before wrapping them, because |
| // wrapping can GC and collect compartments from under us, while |
| // iterating. |
| JS::AutoCheckCannotGC nogc; |
| |
| for (CompartmentsIter c(cx->runtime(), SkipAtoms); !c.done(); c.next()) { |
| if (c->options().invisibleToDebugger()) |
| continue; |
| |
| c->scheduledForDestruction = false; |
| |
| GlobalObject* global = c->maybeGlobal(); |
| |
| if (cx->runtime()->isSelfHostingGlobal(global)) |
| continue; |
| |
| if (global) { |
| /* |
| * We pulled |global| out of nowhere, so it's possible that it was |
| * marked gray by XPConnect. Since we're now exposing it to JS code, |
| * we need to mark it black. |
| */ |
| JS::ExposeObjectToActiveJS(global); |
| if (!globals.append(global)) |
| return false; |
| } |
| } |
| } |
| |
| RootedObject result(cx, NewDenseEmptyArray(cx)); |
| if (!result) |
| return false; |
| |
| for (size_t i = 0; i < globals.length(); i++) { |
| RootedValue globalValue(cx, ObjectValue(*globals[i])); |
| if (!dbg->wrapDebuggeeValue(cx, &globalValue)) |
| return false; |
| if (!NewbornArrayPush(cx, result, globalValue)) |
| return false; |
| } |
| |
| args.rval().setObject(*result); |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::makeGlobalObjectReference(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "makeGlobalObjectReference", args, dbg); |
| if (!args.requireAtLeast(cx, "Debugger.makeGlobalObjectReference", 1)) |
| return false; |
| |
| Rooted<GlobalObject*> global(cx, dbg->unwrapDebuggeeArgument(cx, args[0])); |
| if (!global) |
| return false; |
| |
| // If we create a D.O referring to a global in an invisible compartment, |
| // then from it we can reach function objects, scripts, environments, etc., |
| // none of which we're ever supposed to see. |
| JSCompartment* globalCompartment = global->compartment(); |
| if (globalCompartment->options().invisibleToDebugger()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, |
| JSMSG_DEBUG_INVISIBLE_COMPARTMENT); |
| return false; |
| } |
| |
| args.rval().setObject(*global); |
| return dbg->wrapDebuggeeValue(cx, args.rval()); |
| } |
| |
| static bool |
| DefineProperty(JSContext* cx, HandleObject obj, HandleId id, const char* value, size_t n) |
| { |
| JSString* text = JS_NewStringCopyN(cx, value, n); |
| if (!text) |
| return false; |
| |
| RootedValue str(cx, StringValue(text)); |
| return JS_DefinePropertyById(cx, obj, id, str, JSPROP_ENUMERATE); |
| } |
| |
| #ifdef JS_TRACE_LOGGING |
| # ifdef NIGHTLY_BUILD |
| bool |
| Debugger::setupTraceLogger(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "setupTraceLogger", args, dbg); |
| if (!args.requireAtLeast(cx, "Debugger.setupTraceLogger", 1)) |
| return false; |
| |
| RootedObject obj(cx, ToObject(cx, args[0])); |
| if (!obj) |
| return false; |
| |
| AutoIdVector ids(cx); |
| if (!GetPropertyKeys(cx, obj, JSITER_OWNONLY, &ids)) |
| return false; |
| |
| if (ids.length() == 0) { |
| args.rval().setBoolean(true); |
| return true; |
| } |
| |
| Vector<uint32_t> textIds(cx); |
| if (!textIds.reserve(ids.length())) |
| return false; |
| |
| Vector<bool> values(cx); |
| if (!values.reserve(ids.length())) |
| return false; |
| |
| for (size_t i = 0; i < ids.length(); i++) { |
| if (!JSID_IS_STRING(ids[i])) { |
| args.rval().setBoolean(false); |
| return true; |
| } |
| |
| JSString* id = JSID_TO_STRING(ids[i]); |
| JSLinearString* linear = id->ensureLinear(cx); |
| if (!linear) |
| return false; |
| |
| uint32_t textId = TLStringToTextId(linear); |
| |
| if (!TLTextIdIsToggable(textId)) { |
| args.rval().setBoolean(false); |
| return true; |
| } |
| |
| RootedValue v(cx); |
| if (!GetProperty(cx, obj, obj, ids[i], &v)) |
| return false; |
| |
| textIds.infallibleAppend(textId); |
| values.infallibleAppend(ToBoolean(v)); |
| } |
| |
| MOZ_ASSERT(ids.length() == textIds.length()); |
| MOZ_ASSERT(textIds.length() == values.length()); |
| |
| for (size_t i = 0; i < textIds.length(); i++) { |
| if (values[i]) |
| TraceLogEnableTextId(cx, textIds[i]); |
| else |
| TraceLogDisableTextId(cx, textIds[i]); |
| } |
| |
| args.rval().setBoolean(true); |
| return true; |
| } |
| |
| bool |
| Debugger::drainTraceLogger(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "drainTraceLogger", args, dbg); |
| if (!args.requireAtLeast(cx, "Debugger.drainTraceLogger", 0)) |
| return false; |
| |
| size_t num; |
| TraceLoggerThread* logger = TraceLoggerForMainThread(cx->runtime()); |
| bool lostEvents = logger->lostEvents(dbg->traceLoggerLastDrainedIteration, |
| dbg->traceLoggerLastDrainedSize); |
| EventEntry* events = logger->getEventsStartingAt(&dbg->traceLoggerLastDrainedIteration, |
| &dbg->traceLoggerLastDrainedSize, |
| &num); |
| |
| RootedObject array(cx, NewDenseEmptyArray(cx)); |
| JSAtom* dataAtom = Atomize(cx, "data", strlen("data")); |
| if (!dataAtom) |
| return false; |
| RootedId dataId(cx, AtomToId(dataAtom)); |
| |
| /* Add all events to the array. */ |
| uint32_t index = 0; |
| for (EventEntry* eventItem = events; eventItem < events + num; eventItem++, index++) { |
| RootedObject item(cx, NewObjectWithGivenProto(cx, &PlainObject::class_, nullptr)); |
| if (!item) |
| return false; |
| |
| const char* eventText = logger->eventText(eventItem->textId); |
| if (!DefineProperty(cx, item, dataId, eventText, strlen(eventText))) |
| return false; |
| |
| RootedValue obj(cx, ObjectValue(*item)); |
| if (!JS_DefineElement(cx, array, index, obj, JSPROP_ENUMERATE)) |
| return false; |
| } |
| |
| /* Add "lostEvents" indicating if there are events that were lost. */ |
| RootedValue lost(cx, BooleanValue(lostEvents)); |
| if (!JS_DefineProperty(cx, array, "lostEvents", lost, JSPROP_ENUMERATE)) |
| return false; |
| |
| args.rval().setObject(*array); |
| |
| return true; |
| } |
| # endif // NIGHTLY_BUILD |
| |
| bool |
| Debugger::setupTraceLoggerScriptCalls(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "setupTraceLoggerScriptCalls", args, dbg); |
| if (!args.requireAtLeast(cx, "Debugger.setupTraceLoggerScriptCalls", 0)) |
| return false; |
| |
| TraceLogEnableTextId(cx, TraceLogger_Scripts); |
| TraceLogEnableTextId(cx, TraceLogger_InlinedScripts); |
| TraceLogDisableTextId(cx, TraceLogger_AnnotateScripts); |
| |
| args.rval().setBoolean(true); |
| |
| return true; |
| } |
| |
| bool |
| Debugger::startTraceLogger(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "startTraceLogger", args, dbg); |
| if (!args.requireAtLeast(cx, "Debugger.startTraceLogger", 0)) |
| return false; |
| |
| TraceLoggerThread* logger = TraceLoggerForMainThread(cx->runtime()); |
| if (!TraceLoggerEnable(logger, cx)) |
| return false; |
| |
| args.rval().setUndefined(); |
| |
| return true; |
| } |
| |
| bool |
| Debugger::endTraceLogger(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "endTraceLogger", args, dbg); |
| if (!args.requireAtLeast(cx, "Debugger.endTraceLogger", 0)) |
| return false; |
| |
| TraceLoggerThread* logger = TraceLoggerForMainThread(cx->runtime()); |
| TraceLoggerDisable(logger); |
| |
| args.rval().setUndefined(); |
| |
| return true; |
| } |
| |
| bool |
| Debugger::drainTraceLoggerScriptCalls(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGGER(cx, argc, vp, "drainTraceLoggerScriptCalls", args, dbg); |
| if (!args.requireAtLeast(cx, "Debugger.drainTraceLoggerScriptCalls", 0)) |
| return false; |
| |
| size_t num; |
| TraceLoggerThread* logger = TraceLoggerForMainThread(cx->runtime()); |
| bool lostEvents = logger->lostEvents(dbg->traceLoggerScriptedCallsLastDrainedIteration, |
| dbg->traceLoggerScriptedCallsLastDrainedSize); |
| EventEntry* events = logger->getEventsStartingAt( |
| &dbg->traceLoggerScriptedCallsLastDrainedIteration, |
| &dbg->traceLoggerScriptedCallsLastDrainedSize, |
| &num); |
| |
| RootedObject array(cx, NewDenseEmptyArray(cx)); |
| RootedId fileNameId(cx, AtomToId(cx->names().fileName)); |
| RootedId lineNumberId(cx, AtomToId(cx->names().lineNumber)); |
| RootedId columnNumberId(cx, AtomToId(cx->names().columnNumber)); |
| JSAtom* logTypeAtom = Atomize(cx, "logType", strlen("logType")); |
| if (!logTypeAtom) |
| return false; |
| RootedId logTypeId(cx, AtomToId(logTypeAtom)); |
| |
| /* Add all events to the array. */ |
| uint32_t index = 0; |
| for (EventEntry* eventItem = events; eventItem < events + num; eventItem++) { |
| RootedObject item(cx, NewObjectWithGivenProto(cx, &PlainObject::class_, nullptr)); |
| if (!item) |
| return false; |
| |
| uint32_t textId = eventItem->textId; |
| if (textId != TraceLogger_Stop && !logger->textIdIsScriptEvent(textId)) |
| continue; |
| |
| const char* type = (textId == TraceLogger_Stop) ? "Stop" : "Script"; |
| if (!DefineProperty(cx, item, logTypeId, type, strlen(type))) |
| return false; |
| |
| if (textId != TraceLogger_Stop) { |
| const char* filename; |
| const char* lineno; |
| const char* colno; |
| size_t filename_len, lineno_len, colno_len; |
| logger->extractScriptDetails(textId, &filename, &filename_len, &lineno, &lineno_len, |
| &colno, &colno_len); |
| |
| if (!DefineProperty(cx, item, fileNameId, filename, filename_len)) |
| return false; |
| if (!DefineProperty(cx, item, lineNumberId, lineno, lineno_len)) |
| return false; |
| if (!DefineProperty(cx, item, columnNumberId, colno, colno_len)) |
| return false; |
| } |
| |
| RootedValue obj(cx, ObjectValue(*item)); |
| if (!JS_DefineElement(cx, array, index, obj, JSPROP_ENUMERATE)) |
| return false; |
| |
| index++; |
| } |
| |
| /* Add "lostEvents" indicating if there are events that were lost. */ |
| RootedValue lost(cx, BooleanValue(lostEvents)); |
| if (!JS_DefineProperty(cx, array, "lostEvents", lost, JSPROP_ENUMERATE)) |
| return false; |
| |
| args.rval().setObject(*array); |
| |
| return true; |
| } |
| #endif |
| |
| const JSPropertySpec Debugger::properties[] = { |
| JS_PSGS("enabled", Debugger::getEnabled, Debugger::setEnabled, 0), |
| JS_PSGS("onDebuggerStatement", Debugger::getOnDebuggerStatement, |
| Debugger::setOnDebuggerStatement, 0), |
| JS_PSGS("onExceptionUnwind", Debugger::getOnExceptionUnwind, |
| Debugger::setOnExceptionUnwind, 0), |
| JS_PSGS("onNewScript", Debugger::getOnNewScript, Debugger::setOnNewScript, 0), |
| JS_PSGS("onNewPromise", Debugger::getOnNewPromise, Debugger::setOnNewPromise, 0), |
| JS_PSGS("onPromiseSettled", Debugger::getOnPromiseSettled, Debugger::setOnPromiseSettled, 0), |
| JS_PSGS("onEnterFrame", Debugger::getOnEnterFrame, Debugger::setOnEnterFrame, 0), |
| JS_PSGS("onNewGlobalObject", Debugger::getOnNewGlobalObject, Debugger::setOnNewGlobalObject, 0), |
| JS_PSGS("uncaughtExceptionHook", Debugger::getUncaughtExceptionHook, |
| Debugger::setUncaughtExceptionHook, 0), |
| JS_PSGS("allowUnobservedAsmJS", Debugger::getAllowUnobservedAsmJS, |
| Debugger::setAllowUnobservedAsmJS, 0), |
| JS_PSGS("collectCoverageInfo", Debugger::getCollectCoverageInfo, |
| Debugger::setCollectCoverageInfo, 0), |
| JS_PSG("memory", Debugger::getMemory, 0), |
| JS_PSGS("onIonCompilation", Debugger::getOnIonCompilation, Debugger::setOnIonCompilation, 0), |
| JS_PS_END |
| }; |
| const JSFunctionSpec Debugger::methods[] = { |
| JS_FN("addDebuggee", Debugger::addDebuggee, 1, 0), |
| JS_FN("addAllGlobalsAsDebuggees", Debugger::addAllGlobalsAsDebuggees, 0, 0), |
| JS_FN("removeDebuggee", Debugger::removeDebuggee, 1, 0), |
| JS_FN("removeAllDebuggees", Debugger::removeAllDebuggees, 0, 0), |
| JS_FN("hasDebuggee", Debugger::hasDebuggee, 1, 0), |
| JS_FN("getDebuggees", Debugger::getDebuggees, 0, 0), |
| JS_FN("getNewestFrame", Debugger::getNewestFrame, 0, 0), |
| JS_FN("clearAllBreakpoints", Debugger::clearAllBreakpoints, 0, 0), |
| JS_FN("findScripts", Debugger::findScripts, 1, 0), |
| JS_FN("findObjects", Debugger::findObjects, 1, 0), |
| JS_FN("findAllGlobals", Debugger::findAllGlobals, 0, 0), |
| JS_FN("makeGlobalObjectReference", Debugger::makeGlobalObjectReference, 1, 0), |
| #ifdef JS_TRACE_LOGGING |
| JS_FN("setupTraceLoggerScriptCalls", Debugger::setupTraceLoggerScriptCalls, 0, 0), |
| JS_FN("drainTraceLoggerScriptCalls", Debugger::drainTraceLoggerScriptCalls, 0, 0), |
| JS_FN("startTraceLogger", Debugger::startTraceLogger, 0, 0), |
| JS_FN("endTraceLogger", Debugger::endTraceLogger, 0, 0), |
| # ifdef NIGHTLY_BUILD |
| JS_FN("setupTraceLogger", Debugger::setupTraceLogger, 1, 0), |
| JS_FN("drainTraceLogger", Debugger::drainTraceLogger, 0, 0), |
| # endif |
| #endif |
| JS_FS_END |
| }; |
| |
| |
| /*** Debugger.Script *****************************************************************************/ |
| |
| static inline JSScript* |
| GetScriptReferent(JSObject* obj) |
| { |
| MOZ_ASSERT(obj->getClass() == &DebuggerScript_class); |
| return static_cast<JSScript*>(obj->as<NativeObject>().getPrivate()); |
| } |
| |
| void |
| DebuggerScript_trace(JSTracer* trc, JSObject* obj) |
| { |
| /* This comes from a private pointer, so no barrier needed. */ |
| if (JSScript* script = GetScriptReferent(obj)) { |
| TraceManuallyBarrieredCrossCompartmentEdge(trc, obj, &script, "Debugger.Script referent"); |
| obj->as<NativeObject>().setPrivateUnbarriered(script); |
| } |
| } |
| |
| const Class DebuggerScript_class = { |
| "Script", |
| JSCLASS_HAS_PRIVATE | |
| JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGSCRIPT_COUNT), |
| nullptr, nullptr, nullptr, nullptr, |
| nullptr, nullptr, nullptr, nullptr, |
| nullptr, /* call */ |
| nullptr, /* hasInstance */ |
| nullptr, /* construct */ |
| DebuggerScript_trace |
| }; |
| |
| JSObject* |
| Debugger::newDebuggerScript(JSContext* cx, HandleScript script) |
| { |
| assertSameCompartment(cx, object.get()); |
| |
| RootedObject proto(cx, &object->getReservedSlot(JSSLOT_DEBUG_SCRIPT_PROTO).toObject()); |
| MOZ_ASSERT(proto); |
| NativeObject* scriptobj = NewNativeObjectWithGivenProto(cx, &DebuggerScript_class, |
| proto, TenuredObject); |
| if (!scriptobj) |
| return nullptr; |
| scriptobj->setReservedSlot(JSSLOT_DEBUGSCRIPT_OWNER, ObjectValue(*object)); |
| scriptobj->setPrivateGCThing(script); |
| |
| return scriptobj; |
| } |
| |
| JSObject* |
| Debugger::wrapScript(JSContext* cx, HandleScript script) |
| { |
| assertSameCompartment(cx, object.get()); |
| MOZ_ASSERT(cx->compartment() != script->compartment()); |
| DependentAddPtr<ScriptWeakMap> p(cx, scripts, script); |
| if (!p) { |
| JSObject* scriptobj = newDebuggerScript(cx, script); |
| if (!scriptobj) |
| return nullptr; |
| |
| if (!p.add(cx, scripts, script, scriptobj)) |
| return nullptr; |
| |
| CrossCompartmentKey key(CrossCompartmentKey::DebuggerScript, object, script); |
| if (!object->compartment()->putWrapper(cx, key, ObjectValue(*scriptobj))) { |
| scripts.remove(script); |
| ReportOutOfMemory(cx); |
| return nullptr; |
| } |
| } |
| |
| MOZ_ASSERT(GetScriptReferent(p->value()) == script); |
| return p->value(); |
| } |
| |
| static JSObject* |
| DebuggerScript_check(JSContext* cx, const Value& v, const char* clsname, const char* fnname) |
| { |
| JSObject* thisobj = NonNullObject(cx, v); |
| if (!thisobj) |
| return nullptr; |
| if (thisobj->getClass() != &DebuggerScript_class) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, |
| clsname, fnname, thisobj->getClass()->name); |
| return nullptr; |
| } |
| |
| /* |
| * Check for Debugger.Script.prototype, which is of class DebuggerScript_class |
| * but whose script is null. |
| */ |
| if (!GetScriptReferent(thisobj)) { |
| MOZ_ASSERT(!GetScriptReferent(thisobj)); |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, |
| clsname, fnname, "prototype object"); |
| return nullptr; |
| } |
| |
| return thisobj; |
| } |
| |
| static JSObject* |
| DebuggerScript_checkThis(JSContext* cx, const CallArgs& args, const char* fnname) |
| { |
| return DebuggerScript_check(cx, args.thisv(), "Debugger.Script", fnname); |
| } |
| |
| #define THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, fnname, args, obj, script) \ |
| CallArgs args = CallArgsFromVp(argc, vp); \ |
| RootedObject obj(cx, DebuggerScript_checkThis(cx, args, fnname)); \ |
| if (!obj) \ |
| return false; \ |
| Rooted<JSScript*> script(cx, GetScriptReferent(obj)) |
| |
| static bool |
| DebuggerScript_getDisplayName(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get displayName)", args, obj, script); |
| Debugger* dbg = Debugger::fromChildJSObject(obj); |
| |
| JSFunction* func = script->functionNonDelazifying(); |
| JSString* name = func ? func->displayAtom() : nullptr; |
| if (!name) { |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| RootedValue namev(cx, StringValue(name)); |
| if (!dbg->wrapDebuggeeValue(cx, &namev)) |
| return false; |
| args.rval().set(namev); |
| return true; |
| } |
| |
| static bool |
| DebuggerScript_getUrl(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get url)", args, obj, script); |
| |
| if (script->filename()) { |
| JSString* str; |
| if (script->scriptSource()->introducerFilename()) |
| str = NewStringCopyZ<CanGC>(cx, script->scriptSource()->introducerFilename()); |
| else |
| str = NewStringCopyZ<CanGC>(cx, script->filename()); |
| if (!str) |
| return false; |
| args.rval().setString(str); |
| } else { |
| args.rval().setNull(); |
| } |
| return true; |
| } |
| |
| static bool |
| DebuggerScript_getStartLine(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get startLine)", args, obj, script); |
| args.rval().setNumber(uint32_t(script->lineno())); |
| return true; |
| } |
| |
| static bool |
| DebuggerScript_getLineCount(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get lineCount)", args, obj, script); |
| |
| unsigned maxLine = GetScriptLineExtent(script); |
| args.rval().setNumber(double(maxLine)); |
| return true; |
| } |
| |
| static bool |
| DebuggerScript_getSource(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get source)", args, obj, script); |
| Debugger* dbg = Debugger::fromChildJSObject(obj); |
| |
| RootedScriptSource source(cx, &UncheckedUnwrap(script->sourceObject())->as<ScriptSourceObject>()); |
| RootedObject sourceObject(cx, dbg->wrapSource(cx, source)); |
| if (!sourceObject) |
| return false; |
| |
| args.rval().setObject(*sourceObject); |
| return true; |
| } |
| |
| static bool |
| DebuggerScript_getSourceStart(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get sourceStart)", args, obj, script); |
| args.rval().setNumber(uint32_t(script->sourceStart())); |
| return true; |
| } |
| |
| static bool |
| DebuggerScript_getSourceLength(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get sourceEnd)", args, obj, script); |
| args.rval().setNumber(uint32_t(script->sourceEnd() - script->sourceStart())); |
| return true; |
| } |
| |
| static bool |
| DebuggerScript_getGlobal(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "(get global)", args, obj, script); |
| Debugger* dbg = Debugger::fromChildJSObject(obj); |
| |
| RootedValue v(cx, ObjectValue(script->global())); |
| if (!dbg->wrapDebuggeeValue(cx, &v)) |
| return false; |
| args.rval().set(v); |
| return true; |
| } |
| |
| static bool |
| DebuggerScript_getChildScripts(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getChildScripts", args, obj, script); |
| Debugger* dbg = Debugger::fromChildJSObject(obj); |
| |
| RootedObject result(cx, NewDenseEmptyArray(cx)); |
| if (!result) |
| return false; |
| if (script->hasObjects()) { |
| /* |
| * script->savedCallerFun indicates that this is a direct eval script |
| * and the calling function is stored as script->objects()->vector[0]. |
| * It is not really a child script of this script, so skip it using |
| * innerObjectsStart(). |
| */ |
| ObjectArray* objects = script->objects(); |
| RootedFunction fun(cx); |
| RootedScript funScript(cx); |
| RootedObject obj(cx), s(cx); |
| for (uint32_t i = script->innerObjectsStart(); i < objects->length; i++) { |
| obj = objects->vector[i]; |
| if (obj->is<JSFunction>()) { |
| fun = &obj->as<JSFunction>(); |
| // The inner function could be an asm.js native. |
| if (fun->isNative()) |
| continue; |
| funScript = GetOrCreateFunctionScript(cx, fun); |
| if (!funScript) |
| return false; |
| s = dbg->wrapScript(cx, funScript); |
| if (!s || !NewbornArrayPush(cx, result, ObjectValue(*s))) |
| return false; |
| } |
| } |
| } |
| args.rval().setObject(*result); |
| return true; |
| } |
| |
| static bool |
| ScriptOffset(JSContext* cx, JSScript* script, const Value& v, size_t* offsetp) |
| { |
| double d; |
| size_t off; |
| |
| bool ok = v.isNumber(); |
| if (ok) { |
| d = v.toNumber(); |
| off = size_t(d); |
| } |
| if (!ok || off != d || !IsValidBytecodeOffset(cx, script, off)) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_BAD_OFFSET); |
| return false; |
| } |
| *offsetp = off; |
| return true; |
| } |
| |
| namespace { |
| |
| class BytecodeRangeWithPosition : private BytecodeRange |
| { |
| public: |
| using BytecodeRange::empty; |
| using BytecodeRange::frontPC; |
| using BytecodeRange::frontOpcode; |
| using BytecodeRange::frontOffset; |
| |
| BytecodeRangeWithPosition(JSContext* cx, JSScript* script) |
| : BytecodeRange(cx, script), lineno(script->lineno()), column(0), |
| sn(script->notes()), snpc(script->code()), isEntryPoint(false) |
| { |
| if (!SN_IS_TERMINATOR(sn)) |
| snpc += SN_DELTA(sn); |
| updatePosition(); |
| while (frontPC() != script->main()) |
| popFront(); |
| isEntryPoint = true; |
| } |
| |
| void popFront() { |
| BytecodeRange::popFront(); |
| if (empty()) |
| isEntryPoint = false; |
| else |
| updatePosition(); |
| } |
| |
| size_t frontLineNumber() const { return lineno; } |
| size_t frontColumnNumber() const { return column; } |
| |
| // Entry points are restricted to bytecode offsets that have an |
| // explicit mention in the line table. This restriction avoids a |
| // number of failing cases caused by some instructions not having |
| // sensible (to the user) line numbers, and it is one way to |
| // implement the idea that the bytecode emitter should tell the |
| // debugger exactly which offsets represent "interesting" (to the |
| // user) places to stop. |
| bool frontIsEntryPoint() const { return isEntryPoint; } |
| |
| private: |
| void updatePosition() { |
| /* |
| * Determine the current line number by reading all source notes up to |
| * and including the current offset. |
| */ |
| jsbytecode *lastLinePC = nullptr; |
| while (!SN_IS_TERMINATOR(sn) && snpc <= frontPC()) { |
| SrcNoteType type = (SrcNoteType) SN_TYPE(sn); |
| if (type == SRC_COLSPAN) { |
| ptrdiff_t colspan = SN_OFFSET_TO_COLSPAN(GetSrcNoteOffset(sn, 0)); |
| MOZ_ASSERT(ptrdiff_t(column) + colspan >= 0); |
| column += colspan; |
| lastLinePC = snpc; |
| } else if (type == SRC_SETLINE) { |
| lineno = size_t(GetSrcNoteOffset(sn, 0)); |
| column = 0; |
| lastLinePC = snpc; |
| } else if (type == SRC_NEWLINE) { |
| lineno++; |
| column = 0; |
| lastLinePC = snpc; |
| } |
| |
| sn = SN_NEXT(sn); |
| snpc += SN_DELTA(sn); |
| } |
| isEntryPoint = lastLinePC == frontPC(); |
| } |
| |
| size_t lineno; |
| size_t column; |
| jssrcnote* sn; |
| jsbytecode* snpc; |
| bool isEntryPoint; |
| }; |
| |
| /* |
| * FlowGraphSummary::populate(cx, script) computes a summary of script's |
| * control flow graph used by DebuggerScript_{getAllOffsets,getLineOffsets}. |
| * |
| * An instruction on a given line is an entry point for that line if it can be |
| * reached from (an instruction on) a different line. We distinguish between the |
| * following cases: |
| * - hasNoEdges: |
| * The instruction cannot be reached, so the instruction is not an entry |
| * point for the line it is on. |
| * - hasSingleEdge: |
| * - hasMultipleEdgesFromSingleLine: |
| * The instruction can be reached from a single line. If this line is |
| * different from the line the instruction is on, the instruction is an |
| * entry point for that line. |
| * - hasMultipleEdgesFromMultipleLines: |
| * The instruction can be reached from multiple lines. At least one of |
| * these lines is guaranteed to be different from the line the instruction |
| * is on, so the instruction is an entry point for that line. |
| * |
| * Similarly, an instruction on a given position (line/column pair) is an |
| * entry point for that position if it can be reached from (an instruction on) a |
| * different position. Again, we distinguish between the following cases: |
| * - hasNoEdges: |
| * The instruction cannot be reached, so the instruction is not an entry |
| * point for the position it is on. |
| * - hasSingleEdge: |
| * The instruction can be reached from a single position. If this line is |
| * different from the position the instruction is on, the instruction is |
| * an entry point for that position. |
| * - hasMultipleEdgesFromSingleLine: |
| * - hasMultipleEdgesFromMultipleLines: |
| * The instruction can be reached from multiple positions. At least one |
| * of these positions is guaranteed to be different from the position the |
| * instruction is on, so the instruction is an entry point for that |
| * position. |
| */ |
| class FlowGraphSummary { |
| public: |
| class Entry { |
| public: |
| static Entry createWithNoEdges() { |
| return Entry(SIZE_MAX, 0); |
| } |
| |
| static Entry createWithSingleEdge(size_t lineno, size_t column) { |
| return Entry(lineno, column); |
| } |
| |
| static Entry createWithMultipleEdgesFromSingleLine(size_t lineno) { |
| return Entry(lineno, SIZE_MAX); |
| } |
| |
| static Entry createWithMultipleEdgesFromMultipleLines() { |
| return Entry(SIZE_MAX, SIZE_MAX); |
| } |
| |
| Entry() {} |
| |
| bool hasNoEdges() const { |
| return lineno_ == SIZE_MAX && column_ != SIZE_MAX; |
| } |
| |
| bool hasSingleEdge() const { |
| return lineno_ != SIZE_MAX && column_ != SIZE_MAX; |
| } |
| |
| bool hasMultipleEdgesFromSingleLine() const { |
| return lineno_ != SIZE_MAX && column_ == SIZE_MAX; |
| } |
| |
| bool hasMultipleEdgesFromMultipleLines() const { |
| return lineno_ == SIZE_MAX && column_ == SIZE_MAX; |
| } |
| |
| bool operator==(const Entry& other) const { |
| return lineno_ == other.lineno_ && column_ == other.column_; |
| } |
| |
| bool operator!=(const Entry& other) const { |
| return lineno_ != other.lineno_ || column_ != other.column_; |
| } |
| |
| size_t lineno() const { |
| return lineno_; |
| } |
| |
| size_t column() const { |
| return column_; |
| } |
| |
| private: |
| Entry(size_t lineno, size_t column) : lineno_(lineno), column_(column) {} |
| |
| size_t lineno_; |
| size_t column_; |
| }; |
| |
| explicit FlowGraphSummary(JSContext* cx) : entries_(cx) {} |
| |
| Entry& operator[](size_t index) { |
| return entries_[index]; |
| } |
| |
| bool populate(JSContext* cx, JSScript* script) { |
| if (!entries_.growBy(script->length())) |
| return false; |
| unsigned mainOffset = script->pcToOffset(script->main()); |
| entries_[mainOffset] = Entry::createWithMultipleEdgesFromMultipleLines(); |
| for (size_t i = mainOffset + 1; i < script->length(); i++) |
| entries_[i] = Entry::createWithNoEdges(); |
| |
| size_t prevLineno = script->lineno(); |
| size_t prevColumn = 0; |
| JSOp prevOp = JSOP_NOP; |
| for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) { |
| size_t lineno = r.frontLineNumber(); |
| size_t column = r.frontColumnNumber(); |
| JSOp op = r.frontOpcode(); |
| |
| if (FlowsIntoNext(prevOp)) |
| addEdge(prevLineno, prevColumn, r.frontOffset()); |
| |
| if (CodeSpec[op].type() == JOF_JUMP) { |
| addEdge(lineno, column, r.frontOffset() + GET_JUMP_OFFSET(r.frontPC())); |
| } else if (op == JSOP_TABLESWITCH) { |
| jsbytecode* pc = r.frontPC(); |
| size_t offset = r.frontOffset(); |
| ptrdiff_t step = JUMP_OFFSET_LEN; |
| size_t defaultOffset = offset + GET_JUMP_OFFSET(pc); |
| pc += step; |
| addEdge(lineno, column, defaultOffset); |
| |
| int32_t low = GET_JUMP_OFFSET(pc); |
| pc += JUMP_OFFSET_LEN; |
| int ncases = GET_JUMP_OFFSET(pc) - low + 1; |
| pc += JUMP_OFFSET_LEN; |
| |
| for (int i = 0; i < ncases; i++) { |
| size_t target = offset + GET_JUMP_OFFSET(pc); |
| addEdge(lineno, column, target); |
| pc += step; |
| } |
| } |
| |
| prevLineno = lineno; |
| prevColumn = column; |
| prevOp = op; |
| } |
| |
| return true; |
| } |
| |
| private: |
| void addEdge(size_t sourceLineno, size_t sourceColumn, size_t targetOffset) { |
| if (entries_[targetOffset].hasNoEdges()) |
| entries_[targetOffset] = Entry::createWithSingleEdge(sourceLineno, sourceColumn); |
| else if (entries_[targetOffset].lineno() != sourceLineno) |
| entries_[targetOffset] = Entry::createWithMultipleEdgesFromMultipleLines(); |
| else if (entries_[targetOffset].column() != sourceColumn) |
| entries_[targetOffset] = Entry::createWithMultipleEdgesFromSingleLine(sourceLineno); |
| } |
| |
| Vector<Entry> entries_; |
| }; |
| |
| } /* anonymous namespace */ |
| |
| static bool |
| DebuggerScript_getOffsetLocation(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getOffsetLocation", args, obj, script); |
| if (!args.requireAtLeast(cx, "Debugger.Script.getOffsetLocation", 1)) |
| return false; |
| size_t offset; |
| if (!ScriptOffset(cx, script, args[0], &offset)) |
| return false; |
| |
| FlowGraphSummary flowData(cx); |
| if (!flowData.populate(cx, script)) |
| return false; |
| |
| RootedPlainObject result(cx, NewBuiltinClassInstance<PlainObject>(cx)); |
| if (!result) |
| return false; |
| |
| BytecodeRangeWithPosition r(cx, script); |
| while (!r.empty() && r.frontOffset() < offset) |
| r.popFront(); |
| |
| RootedId id(cx, NameToId(cx->names().lineNumber)); |
| RootedValue value(cx, NumberValue(r.frontLineNumber())); |
| if (!DefineProperty(cx, result, id, value)) |
| return false; |
| |
| value = NumberValue(r.frontColumnNumber()); |
| if (!DefineProperty(cx, result, cx->names().columnNumber, value)) |
| return false; |
| |
| // The same entry point test that is used by getAllColumnOffsets. |
| bool isEntryPoint = (r.frontIsEntryPoint() && |
| !flowData[offset].hasNoEdges() && |
| (flowData[offset].lineno() != r.frontLineNumber() || |
| flowData[offset].column() != r.frontColumnNumber())); |
| value.setBoolean(isEntryPoint); |
| if (!DefineProperty(cx, result, cx->names().isEntryPoint, value)) |
| return false; |
| |
| args.rval().setObject(*result); |
| return true; |
| } |
| |
| static bool |
| DebuggerScript_getAllOffsets(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getAllOffsets", args, obj, script); |
| |
| /* |
| * First pass: determine which offsets in this script are jump targets and |
| * which line numbers jump to them. |
| */ |
| FlowGraphSummary flowData(cx); |
| if (!flowData.populate(cx, script)) |
| return false; |
| |
| /* Second pass: build the result array. */ |
| RootedObject result(cx, NewDenseEmptyArray(cx)); |
| if (!result) |
| return false; |
| for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) { |
| if (!r.frontIsEntryPoint()) |
| continue; |
| |
| size_t offset = r.frontOffset(); |
| size_t lineno = r.frontLineNumber(); |
| |
| /* Make a note, if the current instruction is an entry point for the current line. */ |
| if (!flowData[offset].hasNoEdges() && flowData[offset].lineno() != lineno) { |
| /* Get the offsets array for this line. */ |
| RootedObject offsets(cx); |
| RootedValue offsetsv(cx); |
| |
| RootedId id(cx, INT_TO_JSID(lineno)); |
| |
| bool found; |
| if (!HasOwnProperty(cx, result, id, &found)) |
| return false; |
| if (found && !GetProperty(cx, result, result, id, &offsetsv)) |
| return false; |
| |
| if (offsetsv.isObject()) { |
| offsets = &offsetsv.toObject(); |
| } else { |
| MOZ_ASSERT(offsetsv.isUndefined()); |
| |
| /* |
| * Create an empty offsets array for this line. |
| * Store it in the result array. |
| */ |
| RootedId id(cx); |
| RootedValue v(cx, NumberValue(lineno)); |
| offsets = NewDenseEmptyArray(cx); |
| if (!offsets || |
| !ValueToId<CanGC>(cx, v, &id)) |
| { |
| return false; |
| } |
| |
| RootedValue value(cx, ObjectValue(*offsets)); |
| if (!DefineProperty(cx, result, id, value)) |
| return false; |
| } |
| |
| /* Append the current offset to the offsets array. */ |
| if (!NewbornArrayPush(cx, offsets, NumberValue(offset))) |
| return false; |
| } |
| } |
| |
| args.rval().setObject(*result); |
| return true; |
| } |
| |
| static bool |
| DebuggerScript_getAllColumnOffsets(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getAllColumnOffsets", args, obj, script); |
| |
| /* |
| * First pass: determine which offsets in this script are jump targets and |
| * which positions jump to them. |
| */ |
| FlowGraphSummary flowData(cx); |
| if (!flowData.populate(cx, script)) |
| return false; |
| |
| /* Second pass: build the result array. */ |
| RootedObject result(cx, NewDenseEmptyArray(cx)); |
| if (!result) |
| return false; |
| for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) { |
| size_t lineno = r.frontLineNumber(); |
| size_t column = r.frontColumnNumber(); |
| size_t offset = r.frontOffset(); |
| |
| /* Make a note, if the current instruction is an entry point for the current position. */ |
| if (r.frontIsEntryPoint() && |
| !flowData[offset].hasNoEdges() && |
| (flowData[offset].lineno() != lineno || |
| flowData[offset].column() != column)) { |
| RootedPlainObject entry(cx, NewBuiltinClassInstance<PlainObject>(cx)); |
| if (!entry) |
| return false; |
| |
| RootedId id(cx, NameToId(cx->names().lineNumber)); |
| RootedValue value(cx, NumberValue(lineno)); |
| if (!DefineProperty(cx, entry, id, value)) |
| return false; |
| |
| value = NumberValue(column); |
| if (!DefineProperty(cx, entry, cx->names().columnNumber, value)) |
| return false; |
| |
| id = NameToId(cx->names().offset); |
| value = NumberValue(offset); |
| if (!DefineProperty(cx, entry, id, value)) |
| return false; |
| |
| if (!NewbornArrayPush(cx, result, ObjectValue(*entry))) |
| return false; |
| } |
| } |
| |
| args.rval().setObject(*result); |
| return true; |
| } |
| |
| static bool |
| DebuggerScript_getLineOffsets(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getLineOffsets", args, obj, script); |
| if (!args.requireAtLeast(cx, "Debugger.Script.getLineOffsets", 1)) |
| return false; |
| |
| /* Parse lineno argument. */ |
| RootedValue linenoValue(cx, args[0]); |
| size_t lineno; |
| if (!ToNumber(cx, &linenoValue)) |
| return false; |
| { |
| double d = linenoValue.toNumber(); |
| lineno = size_t(d); |
| if (lineno != d) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_BAD_LINE); |
| return false; |
| } |
| } |
| |
| /* |
| * First pass: determine which offsets in this script are jump targets and |
| * which line numbers jump to them. |
| */ |
| FlowGraphSummary flowData(cx); |
| if (!flowData.populate(cx, script)) |
| return false; |
| |
| /* Second pass: build the result array. */ |
| RootedObject result(cx, NewDenseEmptyArray(cx)); |
| if (!result) |
| return false; |
| for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) { |
| if (!r.frontIsEntryPoint()) |
| continue; |
| |
| size_t offset = r.frontOffset(); |
| |
| /* If the op at offset is an entry point, append offset to result. */ |
| if (r.frontLineNumber() == lineno && |
| !flowData[offset].hasNoEdges() && |
| flowData[offset].lineno() != lineno) |
| { |
| if (!NewbornArrayPush(cx, result, NumberValue(offset))) |
| return false; |
| } |
| } |
| |
| args.rval().setObject(*result); |
| return true; |
| } |
| |
| bool |
| Debugger::observesFrame(AbstractFramePtr frame) const |
| { |
| return observesScript(frame.script()); |
| } |
| |
| bool |
| Debugger::observesFrame(const ScriptFrameIter& iter) const |
| { |
| return observesScript(iter.script()); |
| } |
| |
| bool |
| Debugger::observesScript(JSScript* script) const |
| { |
| if (!enabled) |
| return false; |
| // Don't ever observe self-hosted scripts: the Debugger API can break |
| // self-hosted invariants. |
| return observesGlobal(&script->global()) && !script->selfHosted(); |
| } |
| |
| /* static */ bool |
| Debugger::replaceFrameGuts(JSContext* cx, AbstractFramePtr from, AbstractFramePtr to, |
| ScriptFrameIter& iter) |
| { |
| // Forward live Debugger.Frame objects. |
| for (Debugger::FrameRange r(from); !r.empty(); r.popFront()) { |
| RootedNativeObject frameobj(cx, r.frontFrame()); |
| Debugger* dbg = r.frontDebugger(); |
| MOZ_ASSERT(dbg == Debugger::fromChildJSObject(frameobj)); |
| |
| // Update frame object's ScriptFrameIter::data pointer. |
| DebuggerFrame_freeScriptFrameIterData(cx->runtime()->defaultFreeOp(), frameobj); |
| ScriptFrameIter::Data* data = iter.copyData(); |
| if (!data) |
| return false; |
| frameobj->setPrivate(data); |
| |
| // Remove the old entry before mutating the HashMap. |
| r.removeFrontFrame(); |
| |
| // Add the frame object with |to| as key. |
| if (!dbg->frames.putNew(to, frameobj)) { |
| ReportOutOfMemory(cx); |
| return false; |
| } |
| } |
| |
| // Rekey missingScopes to maintain Debugger.Environment identity and |
| // forward liveScopes to point to the new frame, as the old frame will be |
| // gone. |
| DebugScopes::forwardLiveFrame(cx, from, to); |
| |
| return true; |
| } |
| |
| /* static */ bool |
| Debugger::inFrameMaps(AbstractFramePtr frame) |
| { |
| FrameRange r(frame); |
| return !r.empty(); |
| } |
| |
| /* static */ void |
| Debugger::removeFromFrameMapsAndClearBreakpointsIn(JSContext* cx, AbstractFramePtr frame) |
| { |
| Handle<GlobalObject*> global = cx->global(); |
| |
| for (FrameRange r(frame, global); !r.empty(); r.popFront()) { |
| RootedNativeObject frameobj(cx, r.frontFrame()); |
| Debugger* dbg = r.frontDebugger(); |
| MOZ_ASSERT(dbg == Debugger::fromChildJSObject(frameobj)); |
| |
| FreeOp* fop = cx->runtime()->defaultFreeOp(); |
| DebuggerFrame_freeScriptFrameIterData(fop, frameobj); |
| DebuggerFrame_maybeDecrementFrameScriptStepModeCount(fop, frame, frameobj); |
| |
| dbg->frames.remove(frame); |
| } |
| |
| /* |
| * If this is an eval frame, then from the debugger's perspective the |
| * script is about to be destroyed. Remove any breakpoints in it. |
| */ |
| if (frame.isEvalFrame()) { |
| RootedScript script(cx, frame.script()); |
| script->clearBreakpointsIn(cx->runtime()->defaultFreeOp(), nullptr, nullptr); |
| } |
| } |
| |
| /* static */ bool |
| Debugger::handleBaselineOsr(JSContext* cx, InterpreterFrame* from, jit::BaselineFrame* to) |
| { |
| ScriptFrameIter iter(cx); |
| MOZ_ASSERT(iter.abstractFramePtr() == to); |
| return replaceFrameGuts(cx, from, to, iter); |
| } |
| |
| /* static */ bool |
| Debugger::handleIonBailout(JSContext* cx, jit::RematerializedFrame* from, jit::BaselineFrame* to) |
| { |
| // When we return to a bailed-out Ion real frame, we must update all |
| // Debugger.Frames that refer to its inline frames. However, since we |
| // can't pop individual inline frames off the stack (we can only pop the |
| // real frame that contains them all, as a unit), we cannot assume that |
| // the frame we're dealing with is the top frame. Advance the iterator |
| // across any inlined frames younger than |to|, the baseline frame |
| // reconstructed during bailout from the Ion frame corresponding to |
| // |from|. |
| ScriptFrameIter iter(cx); |
| while (iter.abstractFramePtr() != to) |
| ++iter; |
| return replaceFrameGuts(cx, from, to, iter); |
| } |
| |
| /* static */ void |
| Debugger::handleUnrecoverableIonBailoutError(JSContext* cx, jit::RematerializedFrame* frame) |
| { |
| // Ion bailout can fail due to overrecursion. In such cases we cannot |
| // honor any further Debugger hooks on the frame, and need to ensure that |
| // its Debugger.Frame entry is cleaned up. |
| removeFromFrameMapsAndClearBreakpointsIn(cx, frame); |
| } |
| |
| /* static */ void |
| Debugger::propagateForcedReturn(JSContext* cx, AbstractFramePtr frame, HandleValue rval) |
| { |
| // Invoking the interrupt handler is considered a step and invokes the |
| // youngest frame's onStep handler, if any. However, we cannot handle |
| // { return: ... } resumption values straightforwardly from the interrupt |
| // handler. Instead, we set the intended return value in the frame's rval |
| // slot and set the propagating-forced-return flag on the JSContext. |
| // |
| // The interrupt handler then returns false with no exception set, |
| // signaling an uncatchable exception. In the exception handlers, we then |
| // check for the special propagating-forced-return flag. |
| MOZ_ASSERT(!cx->isExceptionPending()); |
| cx->setPropagatingForcedReturn(); |
| frame.setReturnValue(rval); |
| } |
| |
| static bool |
| DebuggerScript_setBreakpoint(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "setBreakpoint", args, obj, script); |
| if (!args.requireAtLeast(cx, "Debugger.Script.setBreakpoint", 2)) |
| return false; |
| Debugger* dbg = Debugger::fromChildJSObject(obj); |
| |
| if (!dbg->observesScript(script)) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NOT_DEBUGGING); |
| return false; |
| } |
| |
| size_t offset; |
| if (!ScriptOffset(cx, script, args[0], &offset)) |
| return false; |
| |
| RootedObject handler(cx, NonNullObject(cx, args[1])); |
| if (!handler) |
| return false; |
| |
| // Ensure observability *before* setting the breakpoint. If the script is |
| // not already a debuggee, trying to ensure observability after setting |
| // the breakpoint (and thus marking the script as a debuggee) will skip |
| // actually ensuring observability. |
| if (!dbg->ensureExecutionObservabilityOfScript(cx, script)) |
| return false; |
| |
| jsbytecode* pc = script->offsetToPC(offset); |
| BreakpointSite* site = script->getOrCreateBreakpointSite(cx, pc); |
| if (!site) |
| return false; |
| site->inc(cx->runtime()->defaultFreeOp()); |
| if (cx->runtime()->new_<Breakpoint>(dbg, site, handler)) { |
| args.rval().setUndefined(); |
| return true; |
| } |
| site->dec(cx->runtime()->defaultFreeOp()); |
| site->destroyIfEmpty(cx->runtime()->defaultFreeOp()); |
| return false; |
| } |
| |
| static bool |
| DebuggerScript_getBreakpoints(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getBreakpoints", args, obj, script); |
| Debugger* dbg = Debugger::fromChildJSObject(obj); |
| |
| jsbytecode* pc; |
| if (args.length() > 0) { |
| size_t offset; |
| if (!ScriptOffset(cx, script, args[0], &offset)) |
| return false; |
| pc = script->offsetToPC(offset); |
| } else { |
| pc = nullptr; |
| } |
| |
| RootedObject arr(cx, NewDenseEmptyArray(cx)); |
| if (!arr) |
| return false; |
| |
| for (unsigned i = 0; i < script->length(); i++) { |
| BreakpointSite* site = script->getBreakpointSite(script->offsetToPC(i)); |
| if (site && (!pc || site->pc == pc)) { |
| for (Breakpoint* bp = site->firstBreakpoint(); bp; bp = bp->nextInSite()) { |
| if (bp->debugger == dbg && |
| !NewbornArrayPush(cx, arr, ObjectValue(*bp->getHandler()))) |
| { |
| return false; |
| } |
| } |
| } |
| } |
| args.rval().setObject(*arr); |
| return true; |
| } |
| |
| static bool |
| DebuggerScript_clearBreakpoint(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "clearBreakpoint", args, obj, script); |
| if (!args.requireAtLeast(cx, "Debugger.Script.clearBreakpoint", 1)) |
| return false; |
| Debugger* dbg = Debugger::fromChildJSObject(obj); |
| |
| JSObject* handler = NonNullObject(cx, args[0]); |
| if (!handler) |
| return false; |
| |
| script->clearBreakpointsIn(cx->runtime()->defaultFreeOp(), dbg, handler); |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| static bool |
| DebuggerScript_clearAllBreakpoints(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "clearAllBreakpoints", args, obj, script); |
| Debugger* dbg = Debugger::fromChildJSObject(obj); |
| script->clearBreakpointsIn(cx->runtime()->defaultFreeOp(), dbg, nullptr); |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| static bool |
| DebuggerScript_isInCatchScope(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "isInCatchScope", args, obj, script); |
| if (!args.requireAtLeast(cx, "Debugger.Script.isInCatchScope", 1)) |
| return false; |
| |
| size_t offset; |
| if (!ScriptOffset(cx, script, args[0], &offset)) |
| return false; |
| |
| /* |
| * Try note ranges are relative to the mainOffset of the script, so adjust |
| * offset accordingly. |
| */ |
| offset -= script->mainOffset(); |
| |
| args.rval().setBoolean(false); |
| if (script->hasTrynotes()) { |
| JSTryNote* tnBegin = script->trynotes()->vector; |
| JSTryNote* tnEnd = tnBegin + script->trynotes()->length; |
| while (tnBegin != tnEnd) { |
| if (tnBegin->start <= offset && |
| offset <= tnBegin->start + tnBegin->length && |
| tnBegin->kind == JSTRY_CATCH) |
| { |
| args.rval().setBoolean(true); |
| break; |
| } |
| ++tnBegin; |
| } |
| } |
| return true; |
| } |
| |
| static bool |
| DebuggerScript_getOffsetsCoverage(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSCRIPT_SCRIPT(cx, argc, vp, "getOffsetsCoverage", args, obj, script); |
| |
| // If the script has no coverage information, then skip this and return null |
| // instead. |
| if (!script->hasScriptCounts()) { |
| args.rval().setNull(); |
| return true; |
| } |
| |
| ScriptCounts* sc = &script->getScriptCounts(); |
| |
| // If the main ever got visited, then assume that any code before main got |
| // visited once. |
| uint64_t hits = 0; |
| const PCCounts* counts = sc->maybeGetPCCounts(script->pcToOffset(script->main())); |
| if (counts->numExec()) |
| hits = 1; |
| |
| // Build an array of objects which are composed of 4 properties: |
| // - offset PC offset of the current opcode. |
| // - lineNumber Line of the current opcode. |
| // - columnNumber Column of the current opcode. |
| // - count Number of times the instruction got executed. |
| RootedObject result(cx, NewDenseEmptyArray(cx)); |
| if (!result) |
| return false; |
| |
| RootedId offsetId(cx, AtomToId(cx->names().offset)); |
| RootedId lineNumberId(cx, AtomToId(cx->names().lineNumber)); |
| RootedId columnNumberId(cx, AtomToId(cx->names().columnNumber)); |
| RootedId countId(cx, AtomToId(cx->names().count)); |
| |
| RootedObject item(cx); |
| RootedValue offsetValue(cx); |
| RootedValue lineNumberValue(cx); |
| RootedValue columnNumberValue(cx); |
| RootedValue countValue(cx); |
| |
| // Iterate linearly over the bytecode. |
| for (BytecodeRangeWithPosition r(cx, script); !r.empty(); r.popFront()) { |
| size_t offset = r.frontOffset(); |
| |
| // The beginning of each non-branching sequences of instruction set the |
| // number of execution of the current instruction and any following |
| // instruction. |
| counts = sc->maybeGetPCCounts(offset); |
| if (counts) |
| hits = counts->numExec(); |
| |
| offsetValue.setNumber(double(offset)); |
| lineNumberValue.setNumber(double(r.frontLineNumber())); |
| columnNumberValue.setNumber(double(r.frontColumnNumber())); |
| countValue.setNumber(double(hits)); |
| |
| // Create a new object with the offset, line number, column number, the |
| // number of hit counts, and append it to the array. |
| item = NewObjectWithGivenProto<PlainObject>(cx, nullptr); |
| if (!item || |
| !DefineProperty(cx, item, offsetId, offsetValue) || |
| !DefineProperty(cx, item, lineNumberId, lineNumberValue) || |
| !DefineProperty(cx, item, columnNumberId, columnNumberValue) || |
| !DefineProperty(cx, item, countId, countValue) || |
| !NewbornArrayPush(cx, result, ObjectValue(*item))) |
| { |
| return false; |
| } |
| |
| // If the current instruction has thrown, then decrement the hit counts |
| // with the number of throws. |
| counts = sc->maybeGetThrowCounts(offset); |
| if (counts) |
| hits -= counts->numExec(); |
| } |
| |
| args.rval().setObject(*result); |
| return true; |
| } |
| |
| static bool |
| DebuggerScript_construct(JSContext* cx, unsigned argc, Value* vp) |
| { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, |
| "Debugger.Script"); |
| return false; |
| } |
| |
| static const JSPropertySpec DebuggerScript_properties[] = { |
| JS_PSG("displayName", DebuggerScript_getDisplayName, 0), |
| JS_PSG("url", DebuggerScript_getUrl, 0), |
| JS_PSG("startLine", DebuggerScript_getStartLine, 0), |
| JS_PSG("lineCount", DebuggerScript_getLineCount, 0), |
| JS_PSG("source", DebuggerScript_getSource, 0), |
| JS_PSG("sourceStart", DebuggerScript_getSourceStart, 0), |
| JS_PSG("sourceLength", DebuggerScript_getSourceLength, 0), |
| JS_PSG("global", DebuggerScript_getGlobal, 0), |
| JS_PS_END |
| }; |
| |
| static const JSFunctionSpec DebuggerScript_methods[] = { |
| JS_FN("getChildScripts", DebuggerScript_getChildScripts, 0, 0), |
| JS_FN("getAllOffsets", DebuggerScript_getAllOffsets, 0, 0), |
| JS_FN("getAllColumnOffsets", DebuggerScript_getAllColumnOffsets, 0, 0), |
| JS_FN("getLineOffsets", DebuggerScript_getLineOffsets, 1, 0), |
| JS_FN("getOffsetLocation", DebuggerScript_getOffsetLocation, 0, 0), |
| JS_FN("setBreakpoint", DebuggerScript_setBreakpoint, 2, 0), |
| JS_FN("getBreakpoints", DebuggerScript_getBreakpoints, 1, 0), |
| JS_FN("clearBreakpoint", DebuggerScript_clearBreakpoint, 1, 0), |
| JS_FN("clearAllBreakpoints", DebuggerScript_clearAllBreakpoints, 0, 0), |
| JS_FN("isInCatchScope", DebuggerScript_isInCatchScope, 1, 0), |
| JS_FN("getOffsetsCoverage", DebuggerScript_getOffsetsCoverage, 0, 0), |
| JS_FS_END |
| }; |
| |
| |
| /*** Debugger.Source *****************************************************************************/ |
| |
| static inline ScriptSourceObject* |
| GetSourceReferent(JSObject* obj) |
| { |
| MOZ_ASSERT(obj->getClass() == &DebuggerSource_class); |
| return static_cast<ScriptSourceObject*>(obj->as<NativeObject>().getPrivate()); |
| } |
| |
| void |
| DebuggerSource_trace(JSTracer* trc, JSObject* obj) |
| { |
| /* |
| * There is a barrier on private pointers, so the Unbarriered marking |
| * is okay. |
| */ |
| if (JSObject* referent = GetSourceReferent(obj)) { |
| TraceManuallyBarrieredCrossCompartmentEdge(trc, obj, &referent, |
| "Debugger.Source referent"); |
| obj->as<NativeObject>().setPrivateUnbarriered(referent); |
| } |
| } |
| |
| const Class DebuggerSource_class = { |
| "Source", |
| JSCLASS_HAS_PRIVATE | |
| JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGSOURCE_COUNT), |
| nullptr, nullptr, nullptr, nullptr, |
| nullptr, nullptr, nullptr, nullptr, |
| nullptr, /* call */ |
| nullptr, /* hasInstance */ |
| nullptr, /* construct */ |
| DebuggerSource_trace |
| }; |
| |
| JSObject* |
| Debugger::newDebuggerSource(JSContext* cx, HandleScriptSource source) |
| { |
| assertSameCompartment(cx, object.get()); |
| |
| RootedObject proto(cx, &object->getReservedSlot(JSSLOT_DEBUG_SOURCE_PROTO).toObject()); |
| MOZ_ASSERT(proto); |
| NativeObject* sourceobj = NewNativeObjectWithGivenProto(cx, &DebuggerSource_class, |
| proto, TenuredObject); |
| if (!sourceobj) |
| return nullptr; |
| sourceobj->setReservedSlot(JSSLOT_DEBUGSOURCE_OWNER, ObjectValue(*object)); |
| sourceobj->setPrivateGCThing(source); |
| |
| return sourceobj; |
| } |
| |
| JSObject* |
| Debugger::wrapSource(JSContext* cx, HandleScriptSource source) |
| { |
| assertSameCompartment(cx, object.get()); |
| MOZ_ASSERT(cx->compartment() != source->compartment()); |
| DependentAddPtr<SourceWeakMap> p(cx, sources, source); |
| if (!p) { |
| JSObject* sourceobj = newDebuggerSource(cx, source); |
| if (!sourceobj) |
| return nullptr; |
| |
| if (!p.add(cx, sources, source, sourceobj)) |
| return nullptr; |
| |
| CrossCompartmentKey key(CrossCompartmentKey::DebuggerSource, object, source); |
| if (!object->compartment()->putWrapper(cx, key, ObjectValue(*sourceobj))) { |
| sources.remove(source); |
| ReportOutOfMemory(cx); |
| return nullptr; |
| } |
| } |
| |
| MOZ_ASSERT(GetSourceReferent(p->value()) == source); |
| return p->value(); |
| } |
| |
| static bool |
| DebuggerSource_construct(JSContext* cx, unsigned argc, Value* vp) |
| { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, |
| "Debugger.Source"); |
| return false; |
| } |
| |
| static NativeObject* |
| DebuggerSource_checkThis(JSContext* cx, const CallArgs& args, const char* fnname) |
| { |
| JSObject* thisobj = NonNullObject(cx, args.thisv()); |
| if (!thisobj) |
| return nullptr; |
| if (thisobj->getClass() != &DebuggerSource_class) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, |
| "Debugger.Source", fnname, thisobj->getClass()->name); |
| return nullptr; |
| } |
| |
| NativeObject* nthisobj = &thisobj->as<NativeObject>(); |
| |
| if (!GetSourceReferent(thisobj)) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, |
| "Debugger.Frame", fnname, "prototype object"); |
| return nullptr; |
| } |
| |
| return nthisobj; |
| } |
| |
| #define THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, fnname, args, obj, sourceObject) \ |
| CallArgs args = CallArgsFromVp(argc, vp); \ |
| RootedNativeObject obj(cx, DebuggerSource_checkThis(cx, args, fnname)); \ |
| if (!obj) \ |
| return false; \ |
| RootedScriptSource sourceObject(cx, GetSourceReferent(obj)); \ |
| if (!sourceObject) \ |
| return false; |
| |
| static bool |
| DebuggerSource_getText(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get text)", args, obj, sourceObject); |
| Value textv = obj->getReservedSlot(JSSLOT_DEBUGSOURCE_TEXT); |
| if (!textv.isUndefined()) { |
| MOZ_ASSERT(textv.isString()); |
| args.rval().set(textv); |
| return true; |
| } |
| |
| ScriptSource* ss = sourceObject->source(); |
| bool hasSourceData = ss->hasSourceData(); |
| if (!ss->hasSourceData() && !JSScript::loadSource(cx, ss, &hasSourceData)) |
| return false; |
| |
| JSString* str = hasSourceData ? ss->substring(cx, 0, ss->length()) |
| : NewStringCopyZ<CanGC>(cx, "[no source]"); |
| if (!str) |
| return false; |
| |
| args.rval().setString(str); |
| obj->setReservedSlot(JSSLOT_DEBUGSOURCE_TEXT, args.rval()); |
| return true; |
| } |
| |
| static bool |
| DebuggerSource_getUrl(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get url)", args, obj, sourceObject); |
| |
| ScriptSource* ss = sourceObject->source(); |
| if (ss->filename()) { |
| JSString* str = NewStringCopyZ<CanGC>(cx, ss->filename()); |
| if (!str) |
| return false; |
| args.rval().setString(str); |
| } else { |
| args.rval().setNull(); |
| } |
| return true; |
| } |
| |
| static bool |
| DebuggerSource_getDisplayURL(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get url)", args, obj, sourceObject); |
| |
| ScriptSource* ss = sourceObject->source(); |
| MOZ_ASSERT(ss); |
| |
| if (ss->hasDisplayURL()) { |
| JSString* str = JS_NewUCStringCopyZ(cx, ss->displayURL()); |
| if (!str) |
| return false; |
| args.rval().setString(str); |
| } else { |
| args.rval().setNull(); |
| } |
| |
| return true; |
| } |
| |
| static bool |
| DebuggerSource_getElement(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get element)", args, obj, sourceObject); |
| |
| if (sourceObject->element()) { |
| args.rval().setObjectOrNull(sourceObject->element()); |
| if (!Debugger::fromChildJSObject(obj)->wrapDebuggeeValue(cx, args.rval())) |
| return false; |
| } else { |
| args.rval().setUndefined(); |
| } |
| return true; |
| } |
| |
| static bool |
| DebuggerSource_getElementProperty(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get elementAttributeName)", args, obj, sourceObject); |
| args.rval().set(sourceObject->elementAttributeName()); |
| return Debugger::fromChildJSObject(obj)->wrapDebuggeeValue(cx, args.rval()); |
| } |
| |
| static bool |
| DebuggerSource_getIntroductionScript(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get introductionScript)", args, obj, sourceObject); |
| |
| RootedScript script(cx, sourceObject->introductionScript()); |
| if (script) { |
| RootedObject scriptDO(cx, Debugger::fromChildJSObject(obj)->wrapScript(cx, script)); |
| if (!scriptDO) |
| return false; |
| args.rval().setObject(*scriptDO); |
| } else { |
| args.rval().setUndefined(); |
| } |
| return true; |
| } |
| |
| static bool |
| DebuggerSource_getIntroductionOffset(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get introductionOffset)", args, obj, sourceObject); |
| |
| // Regardless of what's recorded in the ScriptSourceObject and |
| // ScriptSource, only hand out the introduction offset if we also have |
| // the script within which it applies. |
| ScriptSource* ss = sourceObject->source(); |
| if (ss->hasIntroductionOffset() && sourceObject->introductionScript()) |
| args.rval().setInt32(ss->introductionOffset()); |
| else |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| static bool |
| DebuggerSource_getIntroductionType(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get introductionType)", args, obj, sourceObject); |
| |
| ScriptSource* ss = sourceObject->source(); |
| if (ss->hasIntroductionType()) { |
| JSString* str = NewStringCopyZ<CanGC>(cx, ss->introductionType()); |
| if (!str) |
| return false; |
| args.rval().setString(str); |
| } else { |
| args.rval().setUndefined(); |
| } |
| return true; |
| } |
| |
| static bool |
| DebuggerSource_setSourceMapUrl(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "sourceMapURL", args, obj, sourceObject); |
| ScriptSource* ss = sourceObject->source(); |
| MOZ_ASSERT(ss); |
| |
| JSString* str = ToString<CanGC>(cx, args[0]); |
| if (!str) |
| return false; |
| |
| AutoStableStringChars stableChars(cx); |
| if (!stableChars.initTwoByte(cx, str)) |
| return false; |
| |
| ss->setSourceMapURL(cx, stableChars.twoByteChars()); |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| static bool |
| DebuggerSource_getSourceMapUrl(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get sourceMapURL)", args, obj, sourceObject); |
| |
| ScriptSource* ss = sourceObject->source(); |
| MOZ_ASSERT(ss); |
| |
| if (ss->hasSourceMapURL()) { |
| JSString* str = JS_NewUCStringCopyZ(cx, ss->sourceMapURL()); |
| if (!str) |
| return false; |
| args.rval().setString(str); |
| } else { |
| args.rval().setNull(); |
| } |
| |
| return true; |
| } |
| |
| static bool |
| DebuggerSource_getCanonicalId(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get sourceMapURL)", args, obj, sourceObject); |
| |
| ScriptSource* ss = sourceObject->source(); |
| MOZ_ASSERT(ss); |
| |
| static_assert(!mozilla::IsBaseOf<gc::Cell, ScriptSource>::value, |
| "We rely on ScriptSource* pointers to be stable, and not move in memory. " |
| "Currently, this holds true because ScriptSource is not managed by the GC. If " |
| "that changes, it doesn't necessarily mean that it will start moving, but we " |
| "will need a new assertion here. If we do start moving ScriptSources in memory, " |
| "then DebuggerSource_getCanonicalId will need to be reworked!"); |
| auto id = uintptr_t(ss); |
| |
| // IEEE 754 doubles can precisely store integers of up 53 bits. On 32 bit |
| // platforms, pointers trivially fit. On 64 bit platforms, pointers only use |
| // 48 bits so we are still good. |
| MOZ_ASSERT(Value::isNumberRepresentable(id)); |
| |
| args.rval().set(NumberValue(id)); |
| return true; |
| } |
| |
| static const JSPropertySpec DebuggerSource_properties[] = { |
| JS_PSG("text", DebuggerSource_getText, 0), |
| JS_PSG("url", DebuggerSource_getUrl, 0), |
| JS_PSG("element", DebuggerSource_getElement, 0), |
| JS_PSG("displayURL", DebuggerSource_getDisplayURL, 0), |
| JS_PSG("introductionScript", DebuggerSource_getIntroductionScript, 0), |
| JS_PSG("introductionOffset", DebuggerSource_getIntroductionOffset, 0), |
| JS_PSG("introductionType", DebuggerSource_getIntroductionType, 0), |
| JS_PSG("elementAttributeName", DebuggerSource_getElementProperty, 0), |
| JS_PSGS("sourceMapURL", DebuggerSource_getSourceMapUrl, DebuggerSource_setSourceMapUrl, 0), |
| JS_PSG("canonicalId", DebuggerSource_getCanonicalId, 0), |
| JS_PS_END |
| }; |
| |
| static const JSFunctionSpec DebuggerSource_methods[] = { |
| JS_FS_END |
| }; |
| |
| |
| /*** Debugger.Frame ******************************************************************************/ |
| |
| static void |
| UpdateFrameIterPc(FrameIter& iter) |
| { |
| if (iter.abstractFramePtr().isRematerializedFrame()) { |
| #ifdef DEBUG |
| // Rematerialized frames don't need their pc updated. The reason we |
| // need to update pc is because we might get the same Debugger.Frame |
| // object for multiple re-entries into debugger code from debuggee |
| // code. This reentrancy is not possible with rematerialized frames, |
| // because when returning to debuggee code, we would have bailed out |
| // to baseline. |
| // |
| // We walk the stack to assert that it doesn't need updating. |
| jit::RematerializedFrame* frame = iter.abstractFramePtr().asRematerializedFrame(); |
| jit::JitFrameLayout* jsFrame = (jit::JitFrameLayout*)frame->top(); |
| jit::JitActivation* activation = iter.activation()->asJit(); |
| |
| ActivationIterator activationIter(activation->cx()->runtime()); |
| while (activationIter.activation() != activation) |
| ++activationIter; |
| |
| jit::JitFrameIterator jitIter(activationIter); |
| while (!jitIter.isIonJS() || jitIter.jsFrame() != jsFrame) |
| ++jitIter; |
| |
| jit::InlineFrameIterator ionInlineIter(activation->cx(), &jitIter); |
| while (ionInlineIter.frameNo() != frame->frameNo()) |
| ++ionInlineIter; |
| |
| MOZ_ASSERT(ionInlineIter.pc() == iter.pc()); |
| #endif |
| return; |
| } |
| |
| iter.updatePcQuadratic(); |
| } |
| |
| static void |
| DebuggerFrame_freeScriptFrameIterData(FreeOp* fop, JSObject* obj) |
| { |
| AbstractFramePtr frame = AbstractFramePtr::FromRaw(obj->as<NativeObject>().getPrivate()); |
| if (frame.isScriptFrameIterData()) |
| fop->delete_((ScriptFrameIter::Data*) frame.raw()); |
| obj->as<NativeObject>().setPrivate(nullptr); |
| } |
| |
| static void |
| DebuggerFrame_maybeDecrementFrameScriptStepModeCount(FreeOp* fop, AbstractFramePtr frame, |
| NativeObject* frameobj) |
| { |
| /* If this frame has an onStep handler, decrement the script's count. */ |
| if (!frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined()) |
| frame.script()->decrementStepModeCount(fop); |
| } |
| |
| static void |
| DebuggerFrame_finalize(FreeOp* fop, JSObject* obj) |
| { |
| DebuggerFrame_freeScriptFrameIterData(fop, obj); |
| } |
| |
| const Class DebuggerFrame_class = { |
| "Frame", JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGFRAME_COUNT), |
| nullptr, nullptr, nullptr, nullptr, |
| nullptr, nullptr, nullptr, DebuggerFrame_finalize |
| }; |
| |
| static NativeObject* |
| CheckThisFrame(JSContext* cx, const CallArgs& args, const char* fnname, bool checkLive) |
| { |
| JSObject* thisobj = NonNullObject(cx, args.thisv()); |
| if (!thisobj) |
| return nullptr; |
| if (thisobj->getClass() != &DebuggerFrame_class) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, |
| "Debugger.Frame", fnname, thisobj->getClass()->name); |
| return nullptr; |
| } |
| |
| NativeObject* nthisobj = &thisobj->as<NativeObject>(); |
| |
| /* |
| * Forbid Debugger.Frame.prototype, which is of class DebuggerFrame_class |
| * but isn't really a working Debugger.Frame object. The prototype object |
| * is distinguished by having a nullptr private value. Also, forbid popped |
| * frames. |
| */ |
| if (!nthisobj->getPrivate()) { |
| if (nthisobj->getReservedSlot(JSSLOT_DEBUGFRAME_OWNER).isUndefined()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, |
| "Debugger.Frame", fnname, "prototype object"); |
| return nullptr; |
| } |
| if (checkLive) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NOT_LIVE, |
| "Debugger.Frame"); |
| return nullptr; |
| } |
| } |
| return nthisobj; |
| } |
| |
| /* |
| * To make frequently fired hooks like onEnterFrame more performant, |
| * Debugger.Frame methods should not create a ScriptFrameIter unless it |
| * absolutely needs to. That is, unless the method has to call a method on |
| * ScriptFrameIter that's otherwise not available on AbstractFramePtr. |
| * |
| * When a Debugger.Frame is first created, its private slot is set to the |
| * AbstractFramePtr itself. The first time the users asks for a |
| * ScriptFrameIter, we construct one, have it settle on the frame pointed to |
| * by the AbstractFramePtr and cache its internal Data in the Debugger.Frame |
| * object's private slot. Subsequent uses of the Debugger.Frame object will |
| * always create a ScriptFrameIter from the cached Data. |
| * |
| * Methods that only need the AbstractFramePtr should use THIS_FRAME. |
| * Methods that need a ScriptFrameIterator should use THIS_FRAME_ITER. |
| */ |
| |
| #define THIS_FRAME_THISOBJ(cx, argc, vp, fnname, args, thisobj) \ |
| CallArgs args = CallArgsFromVp(argc, vp); \ |
| RootedNativeObject thisobj(cx, CheckThisFrame(cx, args, fnname, true)); \ |
| if (!thisobj) \ |
| return false |
| |
| #define THIS_FRAME(cx, argc, vp, fnname, args, thisobj, frame) \ |
| THIS_FRAME_THISOBJ(cx, argc, vp, fnname, args, thisobj); \ |
| AbstractFramePtr frame = AbstractFramePtr::FromRaw(thisobj->getPrivate()); \ |
| if (frame.isScriptFrameIterData()) { \ |
| ScriptFrameIter iter(*(ScriptFrameIter::Data*)(frame.raw())); \ |
| frame = iter.abstractFramePtr(); \ |
| } |
| |
| #define THIS_FRAME_ITER(cx, argc, vp, fnname, args, thisobj, maybeIter, iter) \ |
| THIS_FRAME_THISOBJ(cx, argc, vp, fnname, args, thisobj); \ |
| Maybe<ScriptFrameIter> maybeIter; \ |
| { \ |
| AbstractFramePtr f = AbstractFramePtr::FromRaw(thisobj->getPrivate()); \ |
| if (f.isScriptFrameIterData()) { \ |
| maybeIter.emplace(*(ScriptFrameIter::Data*)(f.raw())); \ |
| } else { \ |
| maybeIter.emplace(cx, ScriptFrameIter::ALL_CONTEXTS, \ |
| ScriptFrameIter::GO_THROUGH_SAVED, \ |
| ScriptFrameIter::IGNORE_DEBUGGER_EVAL_PREV_LINK); \ |
| ScriptFrameIter& iter = *maybeIter; \ |
| while (!iter.hasUsableAbstractFramePtr() || iter.abstractFramePtr() != f) \ |
| ++iter; \ |
| AbstractFramePtr data = iter.copyDataAsAbstractFramePtr(); \ |
| if (!data) \ |
| return false; \ |
| thisobj->setPrivate(data.raw()); \ |
| } \ |
| } \ |
| ScriptFrameIter& iter = *maybeIter |
| |
| #define THIS_FRAME_OWNER(cx, argc, vp, fnname, args, thisobj, frame, dbg) \ |
| THIS_FRAME(cx, argc, vp, fnname, args, thisobj, frame); \ |
| Debugger* dbg = Debugger::fromChildJSObject(thisobj) |
| |
| #define THIS_FRAME_OWNER_ITER(cx, argc, vp, fnname, args, thisobj, maybeIter, iter, dbg) \ |
| THIS_FRAME_ITER(cx, argc, vp, fnname, args, thisobj, maybeIter, iter); \ |
| Debugger* dbg = Debugger::fromChildJSObject(thisobj) |
| |
| static bool |
| DebuggerFrame_getType(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME(cx, argc, vp, "get type", args, thisobj, frame); |
| |
| /* |
| * Indirect eval frames are both isGlobalFrame() and isEvalFrame(), so the |
| * order of checks here is significant. |
| */ |
| JSString* type; |
| if (frame.isEvalFrame()) |
| type = cx->names().eval; |
| else if (frame.isGlobalFrame()) |
| type = cx->names().global; |
| else if (frame.isFunctionFrame()) |
| type = cx->names().call; |
| else if (frame.isModuleFrame()) |
| type = cx->names().module; |
| else |
| MOZ_CRASH("Unknown frame type"); |
| args.rval().setString(type); |
| return true; |
| } |
| |
| static bool |
| DebuggerFrame_getImplementation(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME(cx, argc, vp, "get implementation", args, thisobj, frame); |
| |
| const char* s; |
| if (frame.isBaselineFrame()) |
| s = "baseline"; |
| else if (frame.isRematerializedFrame()) |
| s = "ion"; |
| else |
| s = "interpreter"; |
| |
| JSAtom* str = Atomize(cx, s, strlen(s)); |
| if (!str) |
| return false; |
| |
| args.rval().setString(str); |
| return true; |
| } |
| |
| static bool |
| DebuggerFrame_getEnvironment(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME_OWNER_ITER(cx, argc, vp, "get environment", args, thisobj, _, iter, dbg); |
| |
| Rooted<Env*> env(cx); |
| { |
| AutoCompartment ac(cx, iter.abstractFramePtr().scopeChain()); |
| UpdateFrameIterPc(iter); |
| env = GetDebugScopeForFrame(cx, iter.abstractFramePtr(), iter.pc()); |
| if (!env) |
| return false; |
| } |
| |
| return dbg->wrapEnvironment(cx, env, args.rval()); |
| } |
| |
| static bool |
| DebuggerFrame_getCallee(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME(cx, argc, vp, "get callee", args, thisobj, frame); |
| RootedValue calleev(cx, frame.isNonEvalFunctionFrame() ? frame.calleev() : NullValue()); |
| if (!Debugger::fromChildJSObject(thisobj)->wrapDebuggeeValue(cx, &calleev)) |
| return false; |
| args.rval().set(calleev); |
| return true; |
| } |
| |
| static bool |
| DebuggerFrame_getGenerator(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME(cx, argc, vp, "get generator", args, thisobj, frame); |
| args.rval().setBoolean(frame.script()->isGenerator()); |
| return true; |
| } |
| |
| static bool |
| DebuggerFrame_getConstructing(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME_ITER(cx, argc, vp, "get constructing", args, thisobj, _, iter); |
| args.rval().setBoolean(iter.isFunctionFrame() && iter.isConstructing()); |
| return true; |
| } |
| |
| static bool |
| DebuggerFrame_getThis(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME_ITER(cx, argc, vp, "get this", args, thisobj, _, iter); |
| RootedValue thisv(cx); |
| { |
| AbstractFramePtr frame = iter.abstractFramePtr(); |
| AutoCompartment ac(cx, frame.scopeChain()); |
| |
| UpdateFrameIterPc(iter); |
| |
| if (!GetThisValueForDebuggerMaybeOptimizedOut(cx, frame, iter.pc(), &thisv)) |
| return false; |
| } |
| |
| if (!Debugger::fromChildJSObject(thisobj)->wrapDebuggeeValue(cx, &thisv)) |
| return false; |
| args.rval().set(thisv); |
| return true; |
| } |
| |
| static bool |
| DebuggerFrame_getOlder(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME_ITER(cx, argc, vp, "get this", args, thisobj, _, iter); |
| Debugger* dbg = Debugger::fromChildJSObject(thisobj); |
| |
| for (++iter; !iter.done(); ++iter) { |
| if (dbg->observesFrame(iter)) { |
| if (iter.isIon() && !iter.ensureHasRematerializedFrame(cx)) |
| return false; |
| return dbg->getScriptFrame(cx, iter, args.rval()); |
| } |
| } |
| args.rval().setNull(); |
| return true; |
| } |
| |
| const Class DebuggerArguments_class = { |
| "Arguments", JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGARGUMENTS_COUNT) |
| }; |
| |
| /* The getter used for each element of frame.arguments. See DebuggerFrame_getArguments. */ |
| static bool |
| DebuggerArguments_getArg(JSContext* cx, unsigned argc, Value* vp) |
| { |
| CallArgs args = CallArgsFromVp(argc, vp); |
| int32_t i = args.callee().as<JSFunction>().getExtendedSlot(0).toInt32(); |
| |
| /* Check that the this value is an Arguments object. */ |
| RootedObject argsobj(cx, NonNullObject(cx, args.thisv())); |
| if (!argsobj) |
| return false; |
| if (argsobj->getClass() != &DebuggerArguments_class) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, |
| "Arguments", "getArgument", argsobj->getClass()->name); |
| return false; |
| } |
| |
| /* |
| * Put the Debugger.Frame into the this-value slot, then use THIS_FRAME |
| * to check that it is still live and get the fp. |
| */ |
| args.setThis(argsobj->as<NativeObject>().getReservedSlot(JSSLOT_DEBUGARGUMENTS_FRAME)); |
| THIS_FRAME(cx, argc, vp, "get argument", ca2, thisobj, frame); |
| |
| /* |
| * Since getters can be extracted and applied to other objects, |
| * there is no guarantee this object has an ith argument. |
| */ |
| MOZ_ASSERT(i >= 0); |
| RootedValue arg(cx); |
| RootedScript script(cx); |
| if (unsigned(i) < frame.numActualArgs()) { |
| script = frame.script(); |
| { |
| AutoCompartment ac(cx, script->compartment()); |
| if (!script->ensureHasAnalyzedArgsUsage(cx)) |
| return false; |
| } |
| if (unsigned(i) < frame.numFormalArgs() && script->formalIsAliased(i)) { |
| for (AliasedFormalIter fi(script); ; fi++) { |
| if (fi.frameIndex() == unsigned(i)) { |
| arg = frame.callObj().aliasedVar(fi); |
| break; |
| } |
| } |
| } else if (script->argsObjAliasesFormals() && frame.hasArgsObj()) { |
| arg = frame.argsObj().arg(i); |
| } else { |
| arg = frame.unaliasedActual(i, DONT_CHECK_ALIASING); |
| } |
| } else { |
| arg.setUndefined(); |
| } |
| |
| if (!Debugger::fromChildJSObject(thisobj)->wrapDebuggeeValue(cx, &arg)) |
| return false; |
| args.rval().set(arg); |
| return true; |
| } |
| |
| static bool |
| DebuggerFrame_getArguments(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME(cx, argc, vp, "get arguments", args, thisobj, frame); |
| Value argumentsv = thisobj->getReservedSlot(JSSLOT_DEBUGFRAME_ARGUMENTS); |
| if (!argumentsv.isUndefined()) { |
| MOZ_ASSERT(argumentsv.isObjectOrNull()); |
| args.rval().set(argumentsv); |
| return true; |
| } |
| |
| RootedNativeObject argsobj(cx); |
| if (frame.hasArgs()) { |
| /* Create an arguments object. */ |
| Rooted<GlobalObject*> global(cx, &args.callee().global()); |
| RootedObject proto(cx, GlobalObject::getOrCreateArrayPrototype(cx, global)); |
| if (!proto) |
| return false; |
| argsobj = NewNativeObjectWithGivenProto(cx, &DebuggerArguments_class, proto); |
| if (!argsobj) |
| return false; |
| SetReservedSlot(argsobj, JSSLOT_DEBUGARGUMENTS_FRAME, ObjectValue(*thisobj)); |
| |
| MOZ_ASSERT(frame.numActualArgs() <= 0x7fffffff); |
| unsigned fargc = frame.numActualArgs(); |
| RootedValue fargcVal(cx, Int32Value(fargc)); |
| if (!NativeDefineProperty(cx, argsobj, cx->names().length, fargcVal, nullptr, nullptr, |
| JSPROP_PERMANENT | JSPROP_READONLY)) |
| { |
| return false; |
| } |
| |
| Rooted<jsid> id(cx); |
| for (unsigned i = 0; i < fargc; i++) { |
| RootedFunction getobj(cx); |
| getobj = NewNativeFunction(cx, DebuggerArguments_getArg, 0, nullptr, |
| gc::AllocKind::FUNCTION_EXTENDED); |
| if (!getobj) |
| return false; |
| id = INT_TO_JSID(i); |
| if (!getobj || |
| !NativeDefineProperty(cx, argsobj, id, UndefinedHandleValue, |
| JS_DATA_TO_FUNC_PTR(GetterOp, getobj.get()), nullptr, |
| JSPROP_ENUMERATE | JSPROP_SHARED | JSPROP_GETTER)) |
| { |
| return false; |
| } |
| getobj->setExtendedSlot(0, Int32Value(i)); |
| } |
| } else { |
| argsobj = nullptr; |
| } |
| args.rval().setObjectOrNull(argsobj); |
| thisobj->setReservedSlot(JSSLOT_DEBUGFRAME_ARGUMENTS, args.rval()); |
| return true; |
| } |
| |
| static bool |
| DebuggerFrame_getScript(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME(cx, argc, vp, "get script", args, thisobj, frame); |
| Debugger* debug = Debugger::fromChildJSObject(thisobj); |
| |
| RootedObject scriptObject(cx); |
| if (frame.isFunctionFrame() && !frame.isEvalFrame()) { |
| RootedFunction callee(cx, frame.callee()); |
| if (callee->isInterpreted()) { |
| RootedScript script(cx, callee->nonLazyScript()); |
| scriptObject = debug->wrapScript(cx, script); |
| if (!scriptObject) |
| return false; |
| } |
| } else { |
| /* |
| * We got eval, JS_Evaluate*, or JS_ExecuteScript non-function script |
| * frames. |
| */ |
| RootedScript script(cx, frame.script()); |
| scriptObject = debug->wrapScript(cx, script); |
| if (!scriptObject) |
| return false; |
| } |
| args.rval().setObjectOrNull(scriptObject); |
| return true; |
| } |
| |
| static bool |
| DebuggerFrame_getOffset(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME_ITER(cx, argc, vp, "get offset", args, thisobj, _, iter); |
| JSScript* script = iter.script(); |
| UpdateFrameIterPc(iter); |
| jsbytecode* pc = iter.pc(); |
| size_t offset = script->pcToOffset(pc); |
| args.rval().setNumber(double(offset)); |
| return true; |
| } |
| |
| static bool |
| DebuggerFrame_getLive(JSContext* cx, unsigned argc, Value* vp) |
| { |
| CallArgs args = CallArgsFromVp(argc, vp); |
| NativeObject* thisobj = CheckThisFrame(cx, args, "get live", false); |
| if (!thisobj) |
| return false; |
| bool hasFrame = !!thisobj->getPrivate(); |
| args.rval().setBoolean(hasFrame); |
| return true; |
| } |
| |
| static bool |
| IsValidHook(const Value& v) |
| { |
| return v.isUndefined() || (v.isObject() && v.toObject().isCallable()); |
| } |
| |
| static bool |
| DebuggerFrame_getOnStep(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME(cx, argc, vp, "get onStep", args, thisobj, frame); |
| (void) frame; // Silence GCC warning |
| RootedValue handler(cx, thisobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER)); |
| MOZ_ASSERT(IsValidHook(handler)); |
| args.rval().set(handler); |
| return true; |
| } |
| |
| static bool |
| DebuggerFrame_setOnStep(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME(cx, argc, vp, "set onStep", args, thisobj, frame); |
| if (!args.requireAtLeast(cx, "Debugger.Frame.set onStep", 1)) |
| return false; |
| if (!IsValidHook(args[0])) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NOT_CALLABLE_OR_UNDEFINED); |
| return false; |
| } |
| |
| Value prior = thisobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER); |
| if (!args[0].isUndefined() && prior.isUndefined()) { |
| // Single stepping toggled off->on. |
| AutoCompartment ac(cx, frame.scopeChain()); |
| // Ensure observability *before* incrementing the step mode |
| // count. Calling this function after calling incrementStepModeCount |
| // will make it a no-op. |
| Debugger* dbg = Debugger::fromChildJSObject(thisobj); |
| if (!dbg->ensureExecutionObservabilityOfScript(cx, frame.script())) |
| return false; |
| if (!frame.script()->incrementStepModeCount(cx)) |
| return false; |
| } else if (args[0].isUndefined() && !prior.isUndefined()) { |
| // Single stepping toggled on->off. |
| frame.script()->decrementStepModeCount(cx->runtime()->defaultFreeOp()); |
| } |
| |
| /* 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; |
| } |
| |
| static bool |
| DebuggerFrame_getOnPop(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME(cx, argc, vp, "get onPop", args, thisobj, frame); |
| (void) frame; // Silence GCC warning |
| RootedValue handler(cx, thisobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER)); |
| MOZ_ASSERT(IsValidHook(handler)); |
| args.rval().set(handler); |
| return true; |
| } |
| |
| static bool |
| DebuggerFrame_setOnPop(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME(cx, argc, vp, "set onPop", args, thisobj, frame); |
| if (!args.requireAtLeast(cx, "Debugger.Frame.set onPop", 1)) |
| return false; |
| (void) frame; // Silence GCC warning |
| if (!IsValidHook(args[0])) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NOT_CALLABLE_OR_UNDEFINED); |
| return false; |
| } |
| |
| thisobj->setReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER, args[0]); |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| /* |
| * Evaluate |chars[0..length-1]| in the environment |env|, treating that |
| * source as appearing starting at |lineno| in |filename|. Store the return |
| * value in |*rval|. Use |thisv| as the 'this' value. |
| * |
| * If |frame| is non-nullptr, evaluate as for a direct eval in that frame; |env| |
| * must be either |frame|'s DebugScopeObject, or some extension of that |
| * environment; either way, |frame|'s scope is where newly declared variables |
| * go. In this case, |frame| must have a computed 'this' value, equal to |thisv|. |
| */ |
| static bool |
| EvaluateInEnv(JSContext* cx, Handle<Env*> env, AbstractFramePtr frame, |
| jsbytecode* pc, mozilla::Range<const char16_t> chars, const char* filename, |
| unsigned lineno, MutableHandleValue rval) |
| { |
| assertSameCompartment(cx, env, frame); |
| MOZ_ASSERT_IF(frame, pc); |
| |
| /* |
| * Pass in a StaticEvalObject *not* linked to env for evalStaticScope, as |
| * ScopeIter should stop at any non-ScopeObject or non-syntactic With |
| * boundaries, and we are putting a DebugScopeProxy or non-syntactic With on |
| * the scope chain. |
| */ |
| Rooted<ScopeObject*> enclosingStaticScope(cx); |
| if (!IsGlobalLexicalScope(env)) { |
| // If we are doing a global evalWithBindings, we will still need to |
| // link the static global lexical scope to the static non-syntactic |
| // scope. |
| if (IsGlobalLexicalScope(env->enclosingScope())) |
| enclosingStaticScope = &cx->global()->lexicalScope().staticBlock(); |
| enclosingStaticScope = StaticNonSyntacticScopeObjects::create(cx, enclosingStaticScope); |
| if (!enclosingStaticScope) |
| return false; |
| } else { |
| enclosingStaticScope = &cx->global()->lexicalScope().staticBlock(); |
| } |
| |
| // Do not consider executeInGlobal{WithBindings} as an eval, but instead |
| // as executing a series of statements at the global level. This is to |
| // circumvent the fresh lexical scope that all eval have, so that the |
| // users of executeInGlobal, like the web console, may add new bindings to |
| // the global scope. |
| Rooted<ScopeObject*> staticScope(cx); |
| if (frame) { |
| staticScope = StaticEvalObject::create(cx, enclosingStaticScope); |
| if (!staticScope) |
| return false; |
| } else { |
| staticScope = enclosingStaticScope; |
| } |
| |
| CompileOptions options(cx); |
| options.setIsRunOnce(true) |
| .setForEval(true) |
| .setNoScriptRval(false) |
| .setFileAndLine(filename, lineno) |
| .setCanLazilyParse(false) |
| .setIntroductionType("debugger eval") |
| .maybeMakeStrictMode(frame ? frame.script()->strict() : false); |
| RootedScript callerScript(cx, frame ? frame.script() : nullptr); |
| SourceBufferHolder srcBuf(chars.start().get(), chars.length(), SourceBufferHolder::NoOwnership); |
| RootedScript script(cx, frontend::CompileScript(cx, &cx->tempLifoAlloc(), env, staticScope, |
| callerScript, options, srcBuf, |
| /* source = */ nullptr)); |
| if (!script) |
| return false; |
| |
| // Again, executeInGlobal is not considered eval. |
| if (frame) { |
| if (script->strict()) |
| staticScope->as<StaticEvalObject>().setStrict(); |
| script->setActiveEval(); |
| } |
| |
| ExecuteType type = !frame ? EXECUTE_GLOBAL : EXECUTE_DEBUG; |
| return ExecuteKernel(cx, script, *env, NullValue(), type, frame, rval.address()); |
| } |
| |
| enum EvalBindings { EvalHasExtraBindings = true, EvalWithDefaultBindings = false }; |
| |
| static bool |
| DebuggerGenericEval(JSContext* cx, const char* fullMethodName, const Value& code, |
| EvalBindings evalWithBindings, HandleValue bindings, HandleValue options, |
| MutableHandleValue vp, Debugger* dbg, HandleObject scope, |
| ScriptFrameIter* iter) |
| { |
| /* Either we're specifying the frame, or a global. */ |
| MOZ_ASSERT_IF(iter, !scope); |
| MOZ_ASSERT_IF(!iter, scope && IsGlobalLexicalScope(scope)); |
| |
| /* Check the first argument, the eval code string. */ |
| if (!code.isString()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, |
| fullMethodName, "string", InformalValueTypeName(code)); |
| return false; |
| } |
| RootedLinearString linear(cx, code.toString()->ensureLinear(cx)); |
| if (!linear) |
| return false; |
| |
| /* |
| * Gather keys and values of bindings, if any. This must be done in the |
| * debugger compartment, since that is where any exceptions must be |
| * thrown. |
| */ |
| AutoIdVector keys(cx); |
| AutoValueVector values(cx); |
| if (evalWithBindings) { |
| RootedObject bindingsobj(cx, NonNullObject(cx, bindings)); |
| if (!bindingsobj || |
| !GetPropertyKeys(cx, bindingsobj, JSITER_OWNONLY, &keys) || |
| !values.growBy(keys.length())) |
| { |
| return false; |
| } |
| for (size_t i = 0; i < keys.length(); i++) { |
| MutableHandleValue valp = values[i]; |
| if (!GetProperty(cx, bindingsobj, bindingsobj, keys[i], valp) || |
| !dbg->unwrapDebuggeeValue(cx, valp)) |
| { |
| return false; |
| } |
| } |
| } |
| |
| /* Set options from object if provided. */ |
| JSAutoByteString url_bytes; |
| char* url = nullptr; |
| unsigned lineNumber = 1; |
| |
| if (options.isObject()) { |
| RootedObject opts(cx, &options.toObject()); |
| RootedValue v(cx); |
| |
| if (!JS_GetProperty(cx, opts, "url", &v)) |
| return false; |
| if (!v.isUndefined()) { |
| RootedString url_str(cx, ToString<CanGC>(cx, v)); |
| if (!url_str) |
| return false; |
| url = url_bytes.encodeLatin1(cx, url_str); |
| if (!url) |
| return false; |
| } |
| |
| if (!JS_GetProperty(cx, opts, "lineNumber", &v)) |
| return false; |
| if (!v.isUndefined()) { |
| uint32_t lineno; |
| if (!ToUint32(cx, v, &lineno)) |
| return false; |
| lineNumber = lineno; |
| } |
| } |
| |
| Maybe<AutoCompartment> ac; |
| if (iter) |
| ac.emplace(cx, iter->scopeChain(cx)); |
| else |
| ac.emplace(cx, scope); |
| |
| Rooted<Env*> env(cx); |
| if (iter) { |
| env = GetDebugScopeForFrame(cx, iter->abstractFramePtr(), iter->pc()); |
| if (!env) |
| return false; |
| } else { |
| env = scope; |
| } |
| |
| /* If evalWithBindings, create the inner environment. */ |
| if (evalWithBindings) { |
| RootedPlainObject nenv(cx, NewObjectWithGivenProto<PlainObject>(cx, nullptr)); |
| if (!nenv) |
| return false; |
| RootedId id(cx); |
| for (size_t i = 0; i < keys.length(); i++) { |
| id = keys[i]; |
| MutableHandleValue val = values[i]; |
| if (!cx->compartment()->wrap(cx, val) || |
| !NativeDefineProperty(cx, nenv, id, val, nullptr, nullptr, 0)) |
| { |
| return false; |
| } |
| } |
| |
| AutoObjectVector scopeChain(cx); |
| if (!scopeChain.append(nenv)) |
| return false; |
| |
| RootedObject dynamicScope(cx); |
| if (!CreateScopeObjectsForScopeChain(cx, scopeChain, env, &dynamicScope)) |
| return false; |
| |
| env = dynamicScope; |
| } |
| |
| /* Run the code and produce the completion value. */ |
| RootedValue rval(cx); |
| AbstractFramePtr frame = iter ? iter->abstractFramePtr() : NullFramePtr(); |
| jsbytecode* pc = iter ? iter->pc() : nullptr; |
| AutoStableStringChars stableChars(cx); |
| if (!stableChars.initTwoByte(cx, linear)) |
| return false; |
| |
| mozilla::Range<const char16_t> chars = stableChars.twoByteRange(); |
| bool ok = EvaluateInEnv(cx, env, frame, pc, chars, url ? url : "debugger eval code", |
| lineNumber, &rval); |
| return dbg->receiveCompletionValue(ac, ok, rval, vp); |
| } |
| |
| static bool |
| DebuggerFrame_eval(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME_ITER(cx, argc, vp, "eval", args, thisobj, _, iter); |
| if (!args.requireAtLeast(cx, "Debugger.Frame.prototype.eval", 1)) |
| return false; |
| Debugger* dbg = Debugger::fromChildJSObject(thisobj); |
| UpdateFrameIterPc(iter); |
| return DebuggerGenericEval(cx, "Debugger.Frame.prototype.eval", |
| args[0], EvalWithDefaultBindings, JS::UndefinedHandleValue, |
| args.get(1), args.rval(), dbg, nullptr, &iter); |
| } |
| |
| static bool |
| DebuggerFrame_evalWithBindings(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_FRAME_ITER(cx, argc, vp, "evalWithBindings", args, thisobj, _, iter); |
| if (!args.requireAtLeast(cx, "Debugger.Frame.prototype.evalWithBindings", 2)) |
| return false; |
| Debugger* dbg = Debugger::fromChildJSObject(thisobj); |
| UpdateFrameIterPc(iter); |
| return DebuggerGenericEval(cx, "Debugger.Frame.prototype.evalWithBindings", |
| args[0], EvalHasExtraBindings, args[1], args.get(2), |
| args.rval(), dbg, nullptr, &iter); |
| } |
| |
| static bool |
| DebuggerFrame_construct(JSContext* cx, unsigned argc, Value* vp) |
| { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, |
| "Debugger.Frame"); |
| return false; |
| } |
| |
| static const JSPropertySpec DebuggerFrame_properties[] = { |
| JS_PSG("arguments", DebuggerFrame_getArguments, 0), |
| JS_PSG("callee", DebuggerFrame_getCallee, 0), |
| JS_PSG("constructing", DebuggerFrame_getConstructing, 0), |
| JS_PSG("environment", DebuggerFrame_getEnvironment, 0), |
| JS_PSG("generator", DebuggerFrame_getGenerator, 0), |
| JS_PSG("live", DebuggerFrame_getLive, 0), |
| JS_PSG("offset", DebuggerFrame_getOffset, 0), |
| JS_PSG("older", DebuggerFrame_getOlder, 0), |
| JS_PSG("script", DebuggerFrame_getScript, 0), |
| JS_PSG("this", DebuggerFrame_getThis, 0), |
| JS_PSG("type", DebuggerFrame_getType, 0), |
| JS_PSG("implementation", DebuggerFrame_getImplementation, 0), |
| JS_PSGS("onStep", DebuggerFrame_getOnStep, DebuggerFrame_setOnStep, 0), |
| JS_PSGS("onPop", DebuggerFrame_getOnPop, DebuggerFrame_setOnPop, 0), |
| JS_PS_END |
| }; |
| |
| static const JSFunctionSpec DebuggerFrame_methods[] = { |
| JS_FN("eval", DebuggerFrame_eval, 1, 0), |
| JS_FN("evalWithBindings", DebuggerFrame_evalWithBindings, 1, 0), |
| JS_FS_END |
| }; |
| |
| |
| /*** Debugger.Object *****************************************************************************/ |
| |
| void |
| DebuggerObject_trace(JSTracer* trc, JSObject* obj) |
| { |
| /* |
| * There is a barrier on private pointers, so the Unbarriered marking |
| * is okay. |
| */ |
| if (JSObject* referent = (JSObject*) obj->as<NativeObject>().getPrivate()) { |
| TraceManuallyBarrieredCrossCompartmentEdge(trc, obj, &referent, |
| "Debugger.Object referent"); |
| obj->as<NativeObject>().setPrivateUnbarriered(referent); |
| } |
| } |
| |
| const Class DebuggerObject_class = { |
| "Object", |
| JSCLASS_HAS_PRIVATE | |
| JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGOBJECT_COUNT), |
| nullptr, nullptr, nullptr, nullptr, |
| nullptr, nullptr, nullptr, nullptr, |
| nullptr, /* call */ |
| nullptr, /* hasInstance */ |
| nullptr, /* construct */ |
| DebuggerObject_trace |
| }; |
| |
| static NativeObject* |
| DebuggerObject_checkThis(JSContext* cx, const CallArgs& args, const char* fnname) |
| { |
| JSObject* thisobj = NonNullObject(cx, args.thisv()); |
| if (!thisobj) |
| return nullptr; |
| if (thisobj->getClass() != &DebuggerObject_class) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, |
| "Debugger.Object", fnname, thisobj->getClass()->name); |
| return nullptr; |
| } |
| |
| /* |
| * Forbid Debugger.Object.prototype, which is of class DebuggerObject_class |
| * but isn't a real working Debugger.Object. The prototype object is |
| * distinguished by having no referent. |
| */ |
| NativeObject* nthisobj = &thisobj->as<NativeObject>(); |
| if (!nthisobj->getPrivate()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, |
| "Debugger.Object", fnname, "prototype object"); |
| return nullptr; |
| } |
| return nthisobj; |
| } |
| |
| #define THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, fnname, args, obj) \ |
| CallArgs args = CallArgsFromVp(argc, vp); \ |
| RootedObject obj(cx, DebuggerObject_checkThis(cx, args, fnname)); \ |
| if (!obj) \ |
| return false; \ |
| obj = (JSObject*) obj->as<NativeObject>().getPrivate(); \ |
| MOZ_ASSERT(obj) |
| |
| #define THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, fnname, args, dbg, obj) \ |
| CallArgs args = CallArgsFromVp(argc, vp); \ |
| RootedObject obj(cx, DebuggerObject_checkThis(cx, args, fnname)); \ |
| if (!obj) \ |
| return false; \ |
| Debugger* dbg = Debugger::fromChildJSObject(obj); \ |
| obj = (JSObject*) obj->as<NativeObject>().getPrivate(); \ |
| MOZ_ASSERT(obj) |
| |
| static bool |
| DebuggerObject_construct(JSContext* cx, unsigned argc, Value* vp) |
| { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, |
| "Debugger.Object"); |
| return false; |
| } |
| |
| static bool |
| DebuggerObject_getProto(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "get proto", args, dbg, refobj); |
| RootedObject proto(cx); |
| { |
| AutoCompartment ac(cx, refobj); |
| if (!GetPrototype(cx, refobj, &proto)) |
| return false; |
| } |
| RootedValue protov(cx, ObjectOrNullValue(proto)); |
| if (!dbg->wrapDebuggeeValue(cx, &protov)) |
| return false; |
| args.rval().set(protov); |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_getClass(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, "get class", args, refobj); |
| const char* className; |
| { |
| AutoCompartment ac(cx, refobj); |
| className = GetObjectClassName(cx, refobj); |
| } |
| JSAtom* str = Atomize(cx, className, strlen(className)); |
| if (!str) |
| return false; |
| args.rval().setString(str); |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_getCallable(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, "get callable", args, refobj); |
| args.rval().setBoolean(refobj->isCallable()); |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_getName(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "get name", args, dbg, obj); |
| if (!obj->is<JSFunction>()) { |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| JSString* name = obj->as<JSFunction>().atom(); |
| if (!name) { |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| RootedValue namev(cx, StringValue(name)); |
| if (!dbg->wrapDebuggeeValue(cx, &namev)) |
| return false; |
| args.rval().set(namev); |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_getDisplayName(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "get display name", args, dbg, obj); |
| if (!obj->is<JSFunction>()) { |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| JSString* name = obj->as<JSFunction>().displayAtom(); |
| if (!name) { |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| RootedValue namev(cx, StringValue(name)); |
| if (!dbg->wrapDebuggeeValue(cx, &namev)) |
| return false; |
| args.rval().set(namev); |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_getParameterNames(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "get parameterNames", args, dbg, obj); |
| if (!obj->is<JSFunction>()) { |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| RootedFunction fun(cx, &obj->as<JSFunction>()); |
| |
| /* Only hand out parameter info for debuggee functions. */ |
| if (!dbg->observesGlobal(&fun->global())) { |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| RootedArrayObject result(cx, NewDenseFullyAllocatedArray(cx, fun->nargs())); |
| if (!result) |
| return false; |
| result->ensureDenseInitializedLength(cx, 0, fun->nargs()); |
| |
| if (fun->isInterpreted()) { |
| RootedScript script(cx, GetOrCreateFunctionScript(cx, fun)); |
| if (!script) |
| return false; |
| |
| MOZ_ASSERT(fun->nargs() == script->bindings.numArgs()); |
| |
| if (fun->nargs() > 0) { |
| BindingIter bi(script); |
| for (size_t i = 0; i < fun->nargs(); i++, bi++) { |
| MOZ_ASSERT(bi.argIndex() == i); |
| Value v; |
| if (bi->name()->length() == 0) |
| v = UndefinedValue(); |
| else |
| v = StringValue(bi->name()); |
| result->setDenseElement(i, v); |
| } |
| } |
| } else { |
| for (size_t i = 0; i < fun->nargs(); i++) |
| result->setDenseElement(i, UndefinedValue()); |
| } |
| |
| args.rval().setObject(*result); |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_getScript(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "get script", args, dbg, obj); |
| |
| if (!obj->is<JSFunction>()) { |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| RootedFunction fun(cx, &obj->as<JSFunction>()); |
| if (!fun->isInterpreted()) { |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| RootedScript script(cx, GetOrCreateFunctionScript(cx, fun)); |
| if (!script) |
| return false; |
| |
| /* Only hand out debuggee scripts. */ |
| if (!dbg->observesScript(script)) { |
| args.rval().setNull(); |
| return true; |
| } |
| |
| RootedObject scriptObject(cx, dbg->wrapScript(cx, script)); |
| if (!scriptObject) |
| return false; |
| |
| args.rval().setObject(*scriptObject); |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_getEnvironment(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "get environment", args, dbg, obj); |
| |
| /* Don't bother switching compartments just to check obj's type and get its env. */ |
| if (!obj->is<JSFunction>() || !obj->as<JSFunction>().isInterpreted()) { |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| /* Only hand out environments of debuggee functions. */ |
| if (!dbg->observesGlobal(&obj->global())) { |
| args.rval().setNull(); |
| return true; |
| } |
| |
| Rooted<Env*> env(cx); |
| { |
| AutoCompartment ac(cx, obj); |
| RootedFunction fun(cx, &obj->as<JSFunction>()); |
| env = GetDebugScopeForFunction(cx, fun); |
| if (!env) |
| return false; |
| } |
| |
| return dbg->wrapEnvironment(cx, env, args.rval()); |
| } |
| |
| static bool |
| DebuggerObject_getIsArrowFunction(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, "get isArrowFunction", args, refobj); |
| |
| args.rval().setBoolean(refobj->is<JSFunction>() |
| && refobj->as<JSFunction>().isArrow()); |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_getIsBoundFunction(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, "get isBoundFunction", args, refobj); |
| |
| args.rval().setBoolean(refobj->isBoundFunction()); |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_getBoundTargetFunction(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "get boundFunctionTarget", args, dbg, refobj); |
| |
| if (!refobj->isBoundFunction()) { |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| args.rval().setObject(*refobj->as<JSFunction>().getBoundFunctionTarget()); |
| return dbg->wrapDebuggeeValue(cx, args.rval()); |
| } |
| |
| static bool |
| DebuggerObject_getBoundThis(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "get boundThis", args, dbg, refobj); |
| |
| if (!refobj->isBoundFunction()) { |
| args.rval().setUndefined(); |
| return true; |
| } |
| args.rval().set(refobj->as<JSFunction>().getBoundFunctionThis()); |
| return dbg->wrapDebuggeeValue(cx, args.rval()); |
| } |
| |
| static bool |
| DebuggerObject_getBoundArguments(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "get boundArguments", args, dbg, refobj); |
| |
| if (!refobj->isBoundFunction()) { |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| Rooted<JSFunction*> fun(cx, &refobj->as<JSFunction>()); |
| size_t length = fun->getBoundFunctionArgumentCount(); |
| AutoValueVector boundArgs(cx); |
| if (!boundArgs.resize(length)) |
| return false; |
| for (size_t i = 0; i < length; i++) { |
| boundArgs[i].set(fun->getBoundFunctionArgument(i)); |
| if (!dbg->wrapDebuggeeValue(cx, boundArgs[i])) |
| return false; |
| } |
| |
| JSObject* aobj = NewDenseCopiedArray(cx, boundArgs.length(), boundArgs.begin()); |
| if (!aobj) |
| return false; |
| args.rval().setObject(*aobj); |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_getGlobal(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "get global", args, dbg, obj); |
| |
| RootedValue v(cx, ObjectValue(obj->global())); |
| if (!dbg->wrapDebuggeeValue(cx, &v)) |
| return false; |
| args.rval().set(v); |
| return true; |
| } |
| |
| static bool |
| null(CallArgs& args) |
| { |
| args.rval().setNull(); |
| return true; |
| } |
| |
| /* static */ SavedFrame* |
| Debugger::getObjectAllocationSite(JSObject& obj) |
| { |
| JSObject* metadata = GetObjectMetadata(&obj); |
| if (!metadata) |
| return nullptr; |
| |
| MOZ_ASSERT(!metadata->is<WrapperObject>()); |
| return SavedFrame::isSavedFrameAndNotProto(*metadata) |
| ? &metadata->as<SavedFrame>() |
| : nullptr; |
| } |
| |
| static bool |
| DebuggerObject_getAllocationSite(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, "get allocationSite", args, obj); |
| |
| RootedObject allocSite(cx, Debugger::getObjectAllocationSite(*obj)); |
| if (!allocSite) |
| return null(args); |
| if (!cx->compartment()->wrap(cx, &allocSite)) |
| return false; |
| args.rval().setObject(*allocSite); |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_getOwnPropertyDescriptor(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "getOwnPropertyDescriptor", args, dbg, obj); |
| |
| RootedId id(cx); |
| if (!ValueToId<CanGC>(cx, args.get(0), &id)) |
| return false; |
| |
| /* Bug: This can cause the debuggee to run! */ |
| Rooted<PropertyDescriptor> desc(cx); |
| { |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, obj); |
| |
| ErrorCopier ec(ac); |
| if (!GetOwnPropertyDescriptor(cx, obj, id, &desc)) |
| return false; |
| } |
| |
| if (desc.object()) { |
| /* Rewrap the debuggee values in desc for the debugger. */ |
| if (!dbg->wrapDebuggeeValue(cx, desc.value())) |
| return false; |
| |
| if (desc.hasGetterObject()) { |
| RootedValue get(cx, ObjectOrNullValue(desc.getterObject())); |
| if (!dbg->wrapDebuggeeValue(cx, &get)) |
| return false; |
| desc.setGetterObject(get.toObjectOrNull()); |
| } |
| if (desc.hasSetterObject()) { |
| RootedValue set(cx, ObjectOrNullValue(desc.setterObject())); |
| if (!dbg->wrapDebuggeeValue(cx, &set)) |
| return false; |
| desc.setSetterObject(set.toObjectOrNull()); |
| } |
| } |
| |
| return FromPropertyDescriptor(cx, desc, args.rval()); |
| } |
| |
| |
| static bool |
| getOwnPropertyKeys(JSContext* cx, unsigned argc, unsigned flags, Value* vp) |
| { |
| THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, "getOwnPropertyKeys", args, obj); |
| AutoIdVector keys(cx); |
| { |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, obj); |
| ErrorCopier ec(ac); |
| if (!GetPropertyKeys(cx, obj, flags, &keys)) |
| return false; |
| } |
| |
| AutoValueVector vals(cx); |
| if (!vals.resize(keys.length())) |
| return false; |
| |
| for (size_t i = 0, len = keys.length(); i < len; i++) { |
| jsid id = keys[i]; |
| if (JSID_IS_INT(id)) { |
| JSString* str = Int32ToString<CanGC>(cx, JSID_TO_INT(id)); |
| if (!str) |
| return false; |
| vals[i].setString(str); |
| } else if (JSID_IS_ATOM(id)) { |
| vals[i].setString(JSID_TO_STRING(id)); |
| } else if (JSID_IS_SYMBOL(id)) { |
| vals[i].setSymbol(JSID_TO_SYMBOL(id)); |
| } else { |
| MOZ_ASSERT_UNREACHABLE("GetPropertyKeys must return only string, int, and Symbol jsids"); |
| } |
| } |
| |
| JSObject* aobj = NewDenseCopiedArray(cx, vals.length(), vals.begin()); |
| if (!aobj) |
| return false; |
| args.rval().setObject(*aobj); |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_getOwnPropertyNames(JSContext* cx, unsigned argc, Value* vp) { |
| return getOwnPropertyKeys(cx, argc, JSITER_OWNONLY | JSITER_HIDDEN, vp); |
| } |
| |
| static bool |
| DebuggerObject_getOwnPropertySymbols(JSContext* cx, unsigned argc, Value* vp) { |
| return getOwnPropertyKeys(cx, argc, |
| JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS | JSITER_SYMBOLSONLY, |
| vp); |
| } |
| |
| static bool |
| DebuggerObject_defineProperty(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "defineProperty", args, dbg, obj); |
| if (!args.requireAtLeast(cx, "Debugger.Object.defineProperty", 2)) |
| return false; |
| |
| RootedId id(cx); |
| if (!ValueToId<CanGC>(cx, args[0], &id)) |
| return false; |
| |
| Rooted<PropertyDescriptor> desc(cx); |
| if (!ToPropertyDescriptor(cx, args[1], false, &desc)) |
| return false; |
| |
| if (!dbg->unwrapPropertyDescriptor(cx, obj, &desc)) |
| return false; |
| if (!CheckPropertyDescriptorAccessors(cx, desc)) |
| return false; |
| |
| { |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, obj); |
| if (!cx->compartment()->wrap(cx, &desc)) |
| return false; |
| |
| ErrorCopier ec(ac); |
| if (!DefineProperty(cx, obj, id, desc)) |
| return false; |
| } |
| |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_defineProperties(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "defineProperties", args, dbg, obj); |
| if (!args.requireAtLeast(cx, "Debugger.Object.defineProperties", 1)) |
| return false; |
| |
| RootedValue arg(cx, args[0]); |
| RootedObject props(cx, ToObject(cx, arg)); |
| if (!props) |
| return false; |
| |
| AutoIdVector ids(cx); |
| Rooted<PropertyDescriptorVector> descs(cx, PropertyDescriptorVector(cx)); |
| if (!ReadPropertyDescriptors(cx, props, false, &ids, &descs)) |
| return false; |
| size_t n = ids.length(); |
| |
| for (size_t i = 0; i < n; i++) { |
| if (!dbg->unwrapPropertyDescriptor(cx, obj, descs[i])) |
| return false; |
| if (!CheckPropertyDescriptorAccessors(cx, descs[i])) |
| return false; |
| } |
| |
| { |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, obj); |
| for (size_t i = 0; i < n; i++) { |
| if (!cx->compartment()->wrap(cx, descs[i])) |
| return false; |
| } |
| |
| ErrorCopier ec(ac); |
| for (size_t i = 0; i < n; i++) { |
| if (!DefineProperty(cx, obj, ids[i], descs[i])) |
| return false; |
| } |
| } |
| |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| /* |
| * This does a non-strict delete, as a matter of API design. The case where the |
| * property is non-configurable isn't necessarily exceptional here. |
| */ |
| static bool |
| DebuggerObject_deleteProperty(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, "deleteProperty", args, obj); |
| RootedId id(cx); |
| if (!ValueToId<CanGC>(cx, args.get(0), &id)) |
| return false; |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, obj); |
| ErrorCopier ec(ac); |
| |
| ObjectOpResult result; |
| if (!DeleteProperty(cx, obj, id, result)) |
| return false; |
| args.rval().setBoolean(result.ok()); |
| return true; |
| } |
| |
| enum SealHelperOp { OpSeal, OpFreeze, OpPreventExtensions }; |
| |
| static bool |
| DebuggerObject_sealHelper(JSContext* cx, unsigned argc, Value* vp, SealHelperOp op, const char* name) |
| { |
| THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, name, args, obj); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, obj); |
| ErrorCopier ec(ac); |
| if (op == OpSeal) { |
| if (!SetIntegrityLevel(cx, obj, IntegrityLevel::Sealed)) |
| return false; |
| } else if (op == OpFreeze) { |
| if (!SetIntegrityLevel(cx, obj, IntegrityLevel::Frozen)) |
| return false; |
| } else { |
| MOZ_ASSERT(op == OpPreventExtensions); |
| if (!PreventExtensions(cx, obj)) |
| return false; |
| } |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_seal(JSContext* cx, unsigned argc, Value* vp) |
| { |
| return DebuggerObject_sealHelper(cx, argc, vp, OpSeal, "seal"); |
| } |
| |
| static bool |
| DebuggerObject_freeze(JSContext* cx, unsigned argc, Value* vp) |
| { |
| return DebuggerObject_sealHelper(cx, argc, vp, OpFreeze, "freeze"); |
| } |
| |
| static bool |
| DebuggerObject_preventExtensions(JSContext* cx, unsigned argc, Value* vp) |
| { |
| return DebuggerObject_sealHelper(cx, argc, vp, OpPreventExtensions, "preventExtensions"); |
| } |
| |
| static bool |
| DebuggerObject_isSealedHelper(JSContext* cx, unsigned argc, Value* vp, SealHelperOp op, |
| const char* name) |
| { |
| THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, name, args, obj); |
| |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, obj); |
| ErrorCopier ec(ac); |
| bool r; |
| if (op == OpSeal) { |
| if (!TestIntegrityLevel(cx, obj, IntegrityLevel::Sealed, &r)) |
| return false; |
| } else if (op == OpFreeze) { |
| if (!TestIntegrityLevel(cx, obj, IntegrityLevel::Frozen, &r)) |
| return false; |
| } else { |
| if (!IsExtensible(cx, obj, &r)) |
| return false; |
| } |
| args.rval().setBoolean(r); |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_isSealed(JSContext* cx, unsigned argc, Value* vp) |
| { |
| return DebuggerObject_isSealedHelper(cx, argc, vp, OpSeal, "isSealed"); |
| } |
| |
| static bool |
| DebuggerObject_isFrozen(JSContext* cx, unsigned argc, Value* vp) |
| { |
| return DebuggerObject_isSealedHelper(cx, argc, vp, OpFreeze, "isFrozen"); |
| } |
| |
| static bool |
| DebuggerObject_isExtensible(JSContext* cx, unsigned argc, Value* vp) |
| { |
| return DebuggerObject_isSealedHelper(cx, argc, vp, OpPreventExtensions, "isExtensible"); |
| } |
| |
| enum ApplyOrCallMode { ApplyMode, CallMode }; |
| |
| static bool |
| ApplyOrCall(JSContext* cx, unsigned argc, Value* vp, ApplyOrCallMode mode) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "apply", args, dbg, obj); |
| |
| /* |
| * Any JS exceptions thrown must be in the debugger compartment, so do |
| * sanity checks and fallible conversions before entering the debuggee. |
| */ |
| RootedValue calleev(cx, ObjectValue(*obj)); |
| if (!obj->isCallable()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, |
| "Debugger.Object", "apply", obj->getClass()->name); |
| return false; |
| } |
| |
| /* |
| * Unwrap Debugger.Objects. This happens in the debugger's compartment since |
| * that is where any exceptions must be reported. |
| */ |
| RootedValue thisv(cx, args.get(0)); |
| if (!dbg->unwrapDebuggeeValue(cx, &thisv)) |
| return false; |
| unsigned callArgc = 0; |
| Value* callArgv = nullptr; |
| AutoValueVector argv(cx); |
| if (mode == ApplyMode) { |
| if (args.length() >= 2 && !args[1].isNullOrUndefined()) { |
| if (!args[1].isObject()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_BAD_APPLY_ARGS, |
| js_apply_str); |
| return false; |
| } |
| RootedObject argsobj(cx, &args[1].toObject()); |
| if (!GetLengthProperty(cx, argsobj, &callArgc)) |
| return false; |
| callArgc = unsigned(Min(callArgc, ARGS_LENGTH_MAX)); |
| if (!argv.growBy(callArgc) || !GetElements(cx, argsobj, callArgc, argv.begin())) |
| return false; |
| callArgv = argv.begin(); |
| } |
| } else { |
| callArgc = args.length() > 0 ? unsigned(Min(args.length() - 1, ARGS_LENGTH_MAX)) : 0; |
| callArgv = args.array() + 1; |
| } |
| |
| AutoArrayRooter callArgvRooter(cx, callArgc, callArgv); |
| for (unsigned i = 0; i < callArgc; i++) { |
| if (!dbg->unwrapDebuggeeValue(cx, callArgvRooter.handleAt(i))) |
| return false; |
| } |
| |
| /* |
| * Enter the debuggee compartment and rewrap all input value for that compartment. |
| * (Rewrapping always takes place in the destination compartment.) |
| */ |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, obj); |
| if (!cx->compartment()->wrap(cx, &calleev) || !cx->compartment()->wrap(cx, &thisv)) |
| return false; |
| |
| RootedValue arg(cx); |
| for (unsigned i = 0; i < callArgc; i++) { |
| if (!cx->compartment()->wrap(cx, callArgvRooter.handleAt(i))) |
| return false; |
| } |
| |
| /* |
| * Call the function. Use receiveCompletionValue to return to the debugger |
| * compartment and populate args.rval(). |
| */ |
| RootedValue rval(cx); |
| bool ok = Invoke(cx, thisv, calleev, callArgc, callArgv, &rval); |
| return dbg->receiveCompletionValue(ac, ok, rval, args.rval()); |
| } |
| |
| static bool |
| DebuggerObject_apply(JSContext* cx, unsigned argc, Value* vp) |
| { |
| return ApplyOrCall(cx, argc, vp, ApplyMode); |
| } |
| |
| static bool |
| DebuggerObject_call(JSContext* cx, unsigned argc, Value* vp) |
| { |
| return ApplyOrCall(cx, argc, vp, CallMode); |
| } |
| |
| static bool |
| DebuggerObject_makeDebuggeeValue(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "makeDebuggeeValue", args, dbg, referent); |
| if (!args.requireAtLeast(cx, "Debugger.Object.prototype.makeDebuggeeValue", 1)) |
| return false; |
| |
| RootedValue arg0(cx, args[0]); |
| |
| /* Non-objects are already debuggee values. */ |
| if (arg0.isObject()) { |
| // Enter this Debugger.Object's referent's compartment, and wrap the |
| // argument as appropriate for references from there. |
| { |
| AutoCompartment ac(cx, referent); |
| if (!cx->compartment()->wrap(cx, &arg0)) |
| return false; |
| } |
| |
| // Back in the debugger's compartment, produce a new Debugger.Object |
| // instance referring to the wrapped argument. |
| if (!dbg->wrapDebuggeeValue(cx, &arg0)) |
| return false; |
| } |
| |
| args.rval().set(arg0); |
| return true; |
| } |
| |
| static bool |
| RequireGlobalObject(JSContext* cx, HandleValue dbgobj, HandleObject referent) |
| { |
| RootedObject obj(cx, referent); |
| |
| if (!obj->is<GlobalObject>()) { |
| const char* isWrapper = ""; |
| const char* isWindowProxy = ""; |
| |
| /* Help the poor programmer by pointing out wrappers around globals... */ |
| if (obj->is<WrapperObject>()) { |
| obj = js::UncheckedUnwrap(obj); |
| isWrapper = "a wrapper around "; |
| } |
| |
| /* ... and WindowProxies around Windows. */ |
| if (IsWindowProxy(obj)) { |
| obj = ToWindowIfWindowProxy(obj); |
| isWindowProxy = "a WindowProxy referring to "; |
| } |
| |
| if (obj->is<GlobalObject>()) { |
| ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_DEBUG_WRAPPER_IN_WAY, |
| JSDVG_SEARCH_STACK, dbgobj, nullptr, |
| isWrapper, isWindowProxy); |
| } else { |
| ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_DEBUG_BAD_REFERENT, |
| JSDVG_SEARCH_STACK, dbgobj, nullptr, |
| "a global object", nullptr); |
| } |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_executeInGlobal(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "executeInGlobal", args, dbg, referent); |
| if (!args.requireAtLeast(cx, "Debugger.Object.prototype.executeInGlobal", 1)) |
| return false; |
| if (!RequireGlobalObject(cx, args.thisv(), referent)) |
| return false; |
| |
| RootedObject globalLexical(cx, &referent->as<GlobalObject>().lexicalScope()); |
| return DebuggerGenericEval(cx, "Debugger.Object.prototype.executeInGlobal", |
| args[0], EvalWithDefaultBindings, JS::UndefinedHandleValue, |
| args.get(1), args.rval(), dbg, globalLexical, nullptr); |
| } |
| |
| static bool |
| DebuggerObject_executeInGlobalWithBindings(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "executeInGlobalWithBindings", args, dbg, |
| referent); |
| if (!args.requireAtLeast(cx, "Debugger.Object.prototype.executeInGlobalWithBindings", 2)) |
| return false; |
| if (!RequireGlobalObject(cx, args.thisv(), referent)) |
| return false; |
| |
| RootedObject globalLexical(cx, &referent->as<GlobalObject>().lexicalScope()); |
| return DebuggerGenericEval(cx, "Debugger.Object.prototype.executeInGlobalWithBindings", |
| args[0], EvalHasExtraBindings, args[1], args.get(2), |
| args.rval(), dbg, globalLexical, nullptr); |
| } |
| |
| static bool |
| DebuggerObject_asEnvironment(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "asEnvironment", args, dbg, referent); |
| if (!RequireGlobalObject(cx, args.thisv(), referent)) |
| return false; |
| |
| Rooted<Env*> env(cx); |
| { |
| AutoCompartment ac(cx, referent); |
| env = GetDebugScopeForGlobalLexicalScope(cx); |
| if (!env) |
| return false; |
| } |
| |
| return dbg->wrapEnvironment(cx, env, args.rval()); |
| } |
| |
| static bool |
| DebuggerObject_unwrap(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "unwrap", args, dbg, referent); |
| JSObject* unwrapped = UnwrapOneChecked(referent); |
| if (!unwrapped) { |
| args.rval().setNull(); |
| return true; |
| } |
| |
| // Don't allow unwrapping to create a D.O whose referent is in an |
| // invisible-to-Debugger global. (If our referent is a *wrapper* to such, |
| // and the wrapper is in a visible compartment, that's fine.) |
| JSCompartment* unwrappedCompartment = unwrapped->compartment(); |
| if (unwrappedCompartment->options().invisibleToDebugger()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, |
| JSMSG_DEBUG_INVISIBLE_COMPARTMENT); |
| return false; |
| } |
| |
| args.rval().setObject(*unwrapped); |
| if (!dbg->wrapDebuggeeValue(cx, args.rval())) |
| return false; |
| return true; |
| } |
| |
| static bool |
| DebuggerObject_unsafeDereference(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, "unsafeDereference", args, referent); |
| args.rval().setObject(*referent); |
| if (!cx->compartment()->wrap(cx, args.rval())) |
| return false; |
| |
| // Wrapping should return the WindowProxy. |
| MOZ_ASSERT(!IsWindow(&args.rval().toObject())); |
| |
| return true; |
| } |
| |
| static const JSPropertySpec DebuggerObject_properties[] = { |
| JS_PSG("proto", DebuggerObject_getProto, 0), |
| JS_PSG("class", DebuggerObject_getClass, 0), |
| JS_PSG("callable", DebuggerObject_getCallable, 0), |
| JS_PSG("name", DebuggerObject_getName, 0), |
| JS_PSG("displayName", DebuggerObject_getDisplayName, 0), |
| JS_PSG("parameterNames", DebuggerObject_getParameterNames, 0), |
| JS_PSG("script", DebuggerObject_getScript, 0), |
| JS_PSG("environment", DebuggerObject_getEnvironment, 0), |
| JS_PSG("isArrowFunction", DebuggerObject_getIsArrowFunction, 0), |
| JS_PSG("isBoundFunction", DebuggerObject_getIsBoundFunction, 0), |
| JS_PSG("boundTargetFunction", DebuggerObject_getBoundTargetFunction, 0), |
| JS_PSG("boundThis", DebuggerObject_getBoundThis, 0), |
| JS_PSG("boundArguments", DebuggerObject_getBoundArguments, 0), |
| JS_PSG("global", DebuggerObject_getGlobal, 0), |
| JS_PSG("allocationSite", DebuggerObject_getAllocationSite, 0), |
| JS_PS_END |
| }; |
| |
| static const JSFunctionSpec DebuggerObject_methods[] = { |
| JS_FN("getOwnPropertyDescriptor", DebuggerObject_getOwnPropertyDescriptor, 1, 0), |
| JS_FN("getOwnPropertyNames", DebuggerObject_getOwnPropertyNames, 0, 0), |
| JS_FN("getOwnPropertySymbols", DebuggerObject_getOwnPropertySymbols, 0, 0), |
| JS_FN("defineProperty", DebuggerObject_defineProperty, 2, 0), |
| JS_FN("defineProperties", DebuggerObject_defineProperties, 1, 0), |
| JS_FN("deleteProperty", DebuggerObject_deleteProperty, 1, 0), |
| JS_FN("seal", DebuggerObject_seal, 0, 0), |
| JS_FN("freeze", DebuggerObject_freeze, 0, 0), |
| JS_FN("preventExtensions", DebuggerObject_preventExtensions, 0, 0), |
| JS_FN("isSealed", DebuggerObject_isSealed, 0, 0), |
| JS_FN("isFrozen", DebuggerObject_isFrozen, 0, 0), |
| JS_FN("isExtensible", DebuggerObject_isExtensible, 0, 0), |
| JS_FN("apply", DebuggerObject_apply, 0, 0), |
| JS_FN("call", DebuggerObject_call, 0, 0), |
| JS_FN("makeDebuggeeValue", DebuggerObject_makeDebuggeeValue, 1, 0), |
| JS_FN("executeInGlobal", DebuggerObject_executeInGlobal, 1, 0), |
| JS_FN("executeInGlobalWithBindings", DebuggerObject_executeInGlobalWithBindings, 2, 0), |
| JS_FN("asEnvironment", DebuggerObject_asEnvironment, 0, 0), |
| JS_FN("unwrap", DebuggerObject_unwrap, 0, 0), |
| JS_FN("unsafeDereference", DebuggerObject_unsafeDereference, 0, 0), |
| JS_FS_END |
| }; |
| |
| |
| /*** Debugger.Environment ************************************************************************/ |
| |
| void |
| DebuggerEnv_trace(JSTracer* trc, JSObject* obj) |
| { |
| /* |
| * There is a barrier on private pointers, so the Unbarriered marking |
| * is okay. |
| */ |
| if (Env* referent = (JSObject*) obj->as<NativeObject>().getPrivate()) { |
| TraceManuallyBarrieredCrossCompartmentEdge(trc, obj, &referent, |
| "Debugger.Environment referent"); |
| obj->as<NativeObject>().setPrivateUnbarriered(referent); |
| } |
| } |
| |
| const Class DebuggerEnv_class = { |
| "Environment", |
| JSCLASS_HAS_PRIVATE | |
| JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGENV_COUNT), |
| nullptr, nullptr, nullptr, nullptr, |
| nullptr, nullptr, nullptr, nullptr, |
| nullptr, /* call */ |
| nullptr, /* hasInstance */ |
| nullptr, /* construct */ |
| DebuggerEnv_trace |
| }; |
| |
| static NativeObject* |
| DebuggerEnv_checkThis(JSContext* cx, const CallArgs& args, const char* fnname, |
| bool requireDebuggee = true) |
| { |
| JSObject* thisobj = NonNullObject(cx, args.thisv()); |
| if (!thisobj) |
| return nullptr; |
| if (thisobj->getClass() != &DebuggerEnv_class) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, |
| "Debugger.Environment", fnname, thisobj->getClass()->name); |
| return nullptr; |
| } |
| |
| /* |
| * Forbid Debugger.Environment.prototype, which is of class DebuggerEnv_class |
| * but isn't a real working Debugger.Environment. The prototype object is |
| * distinguished by having no referent. |
| */ |
| NativeObject* nthisobj = &thisobj->as<NativeObject>(); |
| if (!nthisobj->getPrivate()) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO, |
| "Debugger.Environment", fnname, "prototype object"); |
| return nullptr; |
| } |
| |
| /* |
| * Forbid access to Debugger.Environment objects that are not debuggee |
| * environments. |
| */ |
| if (requireDebuggee) { |
| Rooted<Env*> env(cx, static_cast<Env*>(nthisobj->getPrivate())); |
| if (!Debugger::fromChildJSObject(nthisobj)->observesGlobal(&env->global())) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NOT_DEBUGGEE, |
| "Debugger.Environment", "environment"); |
| return nullptr; |
| } |
| } |
| |
| return nthisobj; |
| } |
| |
| #define THIS_DEBUGENV(cx, argc, vp, fnname, args, envobj, env) \ |
| CallArgs args = CallArgsFromVp(argc, vp); \ |
| NativeObject* envobj = DebuggerEnv_checkThis(cx, args, fnname); \ |
| if (!envobj) \ |
| return false; \ |
| Rooted<Env*> env(cx, static_cast<Env*>(envobj->getPrivate())); \ |
| MOZ_ASSERT(env); \ |
| MOZ_ASSERT(!IsSyntacticScope(env)); |
| |
| #define THIS_DEBUGENV_OWNER(cx, argc, vp, fnname, args, envobj, env, dbg) \ |
| THIS_DEBUGENV(cx, argc, vp, fnname, args, envobj, env); \ |
| Debugger* dbg = Debugger::fromChildJSObject(envobj) |
| |
| static bool |
| DebuggerEnv_construct(JSContext* cx, unsigned argc, Value* vp) |
| { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR, |
| "Debugger.Environment"); |
| return false; |
| } |
| |
| static bool |
| IsDeclarative(Env* env) |
| { |
| return env->is<DebugScopeObject>() && env->as<DebugScopeObject>().isForDeclarative(); |
| } |
| |
| static bool |
| IsWith(Env* env) |
| { |
| return env->is<DebugScopeObject>() && |
| env->as<DebugScopeObject>().scope().is<DynamicWithObject>(); |
| } |
| |
| static bool |
| DebuggerEnv_getType(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGENV(cx, argc, vp, "get type", args, envobj, env); |
| |
| /* Don't bother switching compartments just to check env's class. */ |
| const char* s; |
| if (IsDeclarative(env)) |
| s = "declarative"; |
| else if (IsWith(env)) |
| s = "with"; |
| else |
| s = "object"; |
| |
| JSAtom* str = Atomize(cx, s, strlen(s), PinAtom); |
| if (!str) |
| return false; |
| args.rval().setString(str); |
| return true; |
| } |
| |
| static bool |
| DebuggerEnv_getParent(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGENV_OWNER(cx, argc, vp, "get parent", args, envobj, env, dbg); |
| |
| /* Don't bother switching compartments just to get env's parent. */ |
| Rooted<Env*> parent(cx, env->enclosingScope()); |
| return dbg->wrapEnvironment(cx, parent, args.rval()); |
| } |
| |
| static bool |
| DebuggerEnv_getObject(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGENV_OWNER(cx, argc, vp, "get type", args, envobj, env, dbg); |
| |
| /* |
| * Don't bother switching compartments just to check env's class and |
| * possibly get its proto. |
| */ |
| if (IsDeclarative(env)) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NO_SCOPE_OBJECT); |
| return false; |
| } |
| |
| JSObject* obj; |
| if (IsWith(env)) { |
| obj = &env->as<DebugScopeObject>().scope().as<DynamicWithObject>().object(); |
| } else { |
| obj = env; |
| MOZ_ASSERT(!obj->is<DebugScopeObject>()); |
| } |
| |
| args.rval().setObject(*obj); |
| if (!dbg->wrapDebuggeeValue(cx, args.rval())) |
| return false; |
| return true; |
| } |
| |
| static bool |
| DebuggerEnv_getCallee(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGENV_OWNER(cx, argc, vp, "get callee", args, envobj, env, dbg); |
| |
| args.rval().setNull(); |
| |
| if (!env->is<DebugScopeObject>()) |
| return true; |
| |
| JSObject& scope = env->as<DebugScopeObject>().scope(); |
| if (!scope.is<CallObject>()) |
| return true; |
| |
| CallObject& callobj = scope.as<CallObject>(); |
| if (callobj.isForEval()) |
| return true; |
| |
| JSFunction& callee = callobj.callee(); |
| if (IsInternalFunctionObject(callee)) |
| return true; |
| |
| args.rval().setObject(callee); |
| if (!dbg->wrapDebuggeeValue(cx, args.rval())) |
| return false; |
| return true; |
| } |
| |
| static bool |
| DebuggerEnv_getInspectable(JSContext* cx, unsigned argc, Value* vp) |
| { |
| CallArgs args = CallArgsFromVp(argc, vp); |
| NativeObject* envobj = DebuggerEnv_checkThis(cx, args, "get inspectable", false); |
| if (!envobj) |
| return false; |
| Rooted<Env*> env(cx, static_cast<Env*>(envobj->getPrivate())); |
| MOZ_ASSERT(env); |
| MOZ_ASSERT(!env->is<ScopeObject>()); |
| |
| Debugger* dbg = Debugger::fromChildJSObject(envobj); |
| |
| args.rval().setBoolean(dbg->observesGlobal(&env->global())); |
| return true; |
| } |
| |
| static bool |
| DebuggerEnv_getOptimizedOut(JSContext* cx, unsigned argc, Value* vp) |
| { |
| CallArgs args = CallArgsFromVp(argc, vp); |
| NativeObject* envobj = DebuggerEnv_checkThis(cx, args, "get optimizedOut", false); |
| if (!envobj) |
| return false; |
| Rooted<Env*> env(cx, static_cast<Env*>(envobj->getPrivate())); |
| MOZ_ASSERT(env); |
| MOZ_ASSERT(!env->is<ScopeObject>()); |
| |
| args.rval().setBoolean(env->is<DebugScopeObject>() && |
| env->as<DebugScopeObject>().isOptimizedOut()); |
| return true; |
| } |
| |
| static bool |
| DebuggerEnv_names(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGENV(cx, argc, vp, "names", args, envobj, env); |
| |
| AutoIdVector keys(cx); |
| { |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, env); |
| ErrorCopier ec(ac); |
| if (!GetPropertyKeys(cx, env, JSITER_HIDDEN, &keys)) |
| return false; |
| } |
| |
| RootedObject arr(cx, NewDenseEmptyArray(cx)); |
| if (!arr) |
| return false; |
| RootedId id(cx); |
| for (size_t i = 0, len = keys.length(); i < len; i++) { |
| id = keys[i]; |
| if (JSID_IS_ATOM(id) && IsIdentifier(JSID_TO_ATOM(id))) { |
| if (!NewbornArrayPush(cx, arr, StringValue(JSID_TO_STRING(id)))) |
| return false; |
| } |
| } |
| args.rval().setObject(*arr); |
| return true; |
| } |
| |
| static bool |
| DebuggerEnv_find(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGENV_OWNER(cx, argc, vp, "find", args, envobj, env, dbg); |
| if (!args.requireAtLeast(cx, "Debugger.Environment.find", 1)) |
| return false; |
| |
| RootedId id(cx); |
| if (!ValueToIdentifier(cx, args[0], &id)) |
| return false; |
| |
| { |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, env); |
| |
| /* This can trigger resolve hooks. */ |
| ErrorCopier ec(ac); |
| bool found; |
| for (; env; env = env->enclosingScope()) { |
| if (!HasProperty(cx, env, id, &found)) |
| return false; |
| if (found) |
| break; |
| } |
| } |
| |
| return dbg->wrapEnvironment(cx, env, args.rval()); |
| } |
| |
| static bool |
| DebuggerEnv_getVariable(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGENV_OWNER(cx, argc, vp, "getVariable", args, envobj, env, dbg); |
| if (!args.requireAtLeast(cx, "Debugger.Environment.getVariable", 1)) |
| return false; |
| |
| RootedId id(cx); |
| if (!ValueToIdentifier(cx, args[0], &id)) |
| return false; |
| |
| RootedValue v(cx); |
| { |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, env); |
| |
| /* This can trigger getters. */ |
| ErrorCopier ec(ac); |
| |
| // For DebugScopeObjects, we get sentinel values for optimized out |
| // slots and arguments instead of throwing (the default behavior). |
| // |
| // See wrapDebuggeeValue for how the sentinel values are wrapped. |
| if (env->is<DebugScopeObject>()) { |
| if (!env->as<DebugScopeObject>().getMaybeSentinelValue(cx, id, &v)) |
| return false; |
| } else { |
| if (!GetProperty(cx, env, env, id, &v)) |
| return false; |
| } |
| } |
| |
| // When we've faked up scope chain objects for optimized-out scopes, |
| // declarative environments may contain internal JSFunction objects, which |
| // we shouldn't expose to the user. |
| if (v.isObject()) { |
| RootedObject obj(cx, &v.toObject()); |
| if (obj->is<JSFunction>() && |
| IsInternalFunctionObject(obj->as<JSFunction>())) |
| v.setMagic(JS_OPTIMIZED_OUT); |
| } |
| |
| if (!dbg->wrapDebuggeeValue(cx, &v)) |
| return false; |
| args.rval().set(v); |
| return true; |
| } |
| |
| static bool |
| DebuggerEnv_setVariable(JSContext* cx, unsigned argc, Value* vp) |
| { |
| THIS_DEBUGENV_OWNER(cx, argc, vp, "setVariable", args, envobj, env, dbg); |
| if (!args.requireAtLeast(cx, "Debugger.Environment.setVariable", 2)) |
| return false; |
| |
| RootedId id(cx); |
| if (!ValueToIdentifier(cx, args[0], &id)) |
| return false; |
| |
| RootedValue v(cx, args[1]); |
| if (!dbg->unwrapDebuggeeValue(cx, &v)) |
| return false; |
| |
| { |
| Maybe<AutoCompartment> ac; |
| ac.emplace(cx, env); |
| if (!cx->compartment()->wrap(cx, &v)) |
| return false; |
| |
| /* This can trigger setters. */ |
| ErrorCopier ec(ac); |
| |
| /* Make sure the environment actually has the specified binding. */ |
| bool has; |
| if (!HasProperty(cx, env, id, &has)) |
| return false; |
| if (!has) { |
| JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_VARIABLE_NOT_FOUND); |
| return false; |
| } |
| |
| /* Just set the property. */ |
| if (!SetProperty(cx, env, id, v)) |
| return false; |
| } |
| |
| args.rval().setUndefined(); |
| return true; |
| } |
| |
| static const JSPropertySpec DebuggerEnv_properties[] = { |
| JS_PSG("type", DebuggerEnv_getType, 0), |
| JS_PSG("object", DebuggerEnv_getObject, 0), |
| JS_PSG("parent", DebuggerEnv_getParent, 0), |
| JS_PSG("callee", DebuggerEnv_getCallee, 0), |
| JS_PSG("inspectable", DebuggerEnv_getInspectable, 0), |
| JS_PSG("optimizedOut", DebuggerEnv_getOptimizedOut, 0), |
| JS_PS_END |
| }; |
| |
| static const JSFunctionSpec DebuggerEnv_methods[] = { |
| JS_FN("names", DebuggerEnv_names, 0, 0), |
| JS_FN("find", DebuggerEnv_find, 1, 0), |
| JS_FN("getVariable", DebuggerEnv_getVariable, 1, 0), |
| JS_FN("setVariable", DebuggerEnv_setVariable, 2, 0), |
| JS_FS_END |
| }; |
| |
| |
| |
| /*** JS::dbg::Builder ****************************************************************************/ |
| |
| Builder::Builder(JSContext* cx, js::Debugger* debugger) |
| : debuggerObject(cx, debugger->toJSObject().get()), |
| debugger(debugger) |
| { } |
| |
| |
| #if DEBUG |
| void |
| Builder::assertBuilt(JSObject* obj) |
| { |
| // We can't use assertSameCompartment here, because that is always keyed to |
| // some JSContext's current compartment, whereas BuiltThings can be |
| // constructed and assigned to without respect to any particular context; |
| // the only constraint is that they should be in their debugger's compartment. |
| MOZ_ASSERT_IF(obj, debuggerObject->compartment() == obj->compartment()); |
| } |
| #endif |
| |
| bool |
| Builder::Object::definePropertyToTrusted(JSContext* cx, const char* name, |
| JS::MutableHandleValue trusted) |
| { |
| // We should have checked for false Objects before calling this. |
| MOZ_ASSERT(value); |
| |
| JSAtom* atom = Atomize(cx, name, strlen(name)); |
| if (!atom) |
| return false; |
| RootedId id(cx, AtomToId(atom)); |
| |
| return DefineProperty(cx, value, id, trusted); |
| } |
| |
| bool |
| Builder::Object::defineProperty(JSContext* cx, const char* name, JS::HandleValue propval_) |
| { |
| AutoCompartment ac(cx, debuggerObject()); |
| |
| RootedValue propval(cx, propval_); |
| if (!debugger()->wrapDebuggeeValue(cx, &propval)) |
| return false; |
| |
| return definePropertyToTrusted(cx, name, &propval); |
| } |
| |
| bool |
| Builder::Object::defineProperty(JSContext* cx, const char* name, JS::HandleObject propval_) |
| { |
| RootedValue propval(cx, ObjectOrNullValue(propval_)); |
| return defineProperty(cx, name, propval); |
| } |
| |
| bool |
| Builder::Object::defineProperty(JSContext* cx, const char* name, Builder::Object& propval_) |
| { |
| AutoCompartment ac(cx, debuggerObject()); |
| |
| RootedValue propval(cx, ObjectOrNullValue(propval_.value)); |
| return definePropertyToTrusted(cx, name, &propval); |
| } |
| |
| Builder::Object |
| Builder::newObject(JSContext* cx) |
| { |
| AutoCompartment ac(cx, debuggerObject); |
| |
| RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx)); |
| |
| // If the allocation failed, this will return a false Object, as the spec promises. |
| return Object(cx, *this, obj); |
| } |
| |
| |
| /*** JS::dbg::AutoEntryMonitor ******************************************************************/ |
| |
| AutoEntryMonitor::AutoEntryMonitor(JSContext* cx) |
| : runtime_(cx->runtime()), |
| savedMonitor_(cx->runtime()->entryMonitor) |
| { |
| runtime_->entryMonitor = this; |
| } |
| |
| AutoEntryMonitor::~AutoEntryMonitor() |
| { |
| runtime_->entryMonitor = savedMonitor_; |
| } |
| |
| |
| /*** Glue ****************************************************************************************/ |
| |
| extern JS_PUBLIC_API(bool) |
| JS_DefineDebuggerObject(JSContext* cx, HandleObject obj) |
| { |
| RootedNativeObject |
| objProto(cx), |
| debugCtor(cx), |
| debugProto(cx), |
| frameProto(cx), |
| scriptProto(cx), |
| sourceProto(cx), |
| objectProto(cx), |
| envProto(cx), |
| memoryProto(cx); |
| objProto = obj->as<GlobalObject>().getOrCreateObjectPrototype(cx); |
| if (!objProto) |
| return false; |
| debugProto = InitClass(cx, obj, |
| objProto, &Debugger::jsclass, Debugger::construct, |
| 1, Debugger::properties, Debugger::methods, nullptr, nullptr, |
| debugCtor.address()); |
| if (!debugProto) |
| return false; |
| |
| frameProto = InitClass(cx, debugCtor, objProto, &DebuggerFrame_class, |
| DebuggerFrame_construct, 0, |
| DebuggerFrame_properties, DebuggerFrame_methods, |
| nullptr, nullptr); |
| if (!frameProto) |
| return false; |
| |
| scriptProto = InitClass(cx, debugCtor, objProto, &DebuggerScript_class, |
| DebuggerScript_construct, 0, |
| DebuggerScript_properties, DebuggerScript_methods, |
| nullptr, nullptr); |
| if (!scriptProto) |
| return false; |
| |
| sourceProto = InitClass(cx, debugCtor, sourceProto, &DebuggerSource_class, |
| DebuggerSource_construct, 0, |
| DebuggerSource_properties, DebuggerSource_methods, |
| nullptr, nullptr); |
| if (!sourceProto) |
| return false; |
| |
| objectProto = InitClass(cx, debugCtor, objProto, &DebuggerObject_class, |
| DebuggerObject_construct, 0, |
| DebuggerObject_properties, DebuggerObject_methods, |
| nullptr, nullptr); |
| if (!objectProto) |
| return false; |
| envProto = InitClass(cx, debugCtor, objProto, &DebuggerEnv_class, |
| DebuggerEnv_construct, 0, |
| DebuggerEnv_properties, DebuggerEnv_methods, |
| nullptr, nullptr); |
| if (!envProto) |
| return false; |
| memoryProto = InitClass(cx, debugCtor, objProto, &DebuggerMemory::class_, |
| DebuggerMemory::construct, 0, DebuggerMemory::properties, |
| DebuggerMemory::methods, nullptr, nullptr); |
| if (!memoryProto) |
| return false; |
| |
| debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_FRAME_PROTO, ObjectValue(*frameProto)); |
| debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_OBJECT_PROTO, ObjectValue(*objectProto)); |
| debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_SCRIPT_PROTO, ObjectValue(*scriptProto)); |
| debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_SOURCE_PROTO, ObjectValue(*sourceProto)); |
| debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_ENV_PROTO, ObjectValue(*envProto)); |
| debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_MEMORY_PROTO, ObjectValue(*memoryProto)); |
| return true; |
| } |
| |
| static inline void |
| AssertIsPromise(JSContext* cx, HandleObject promise) |
| { |
| MOZ_ASSERT(promise); |
| assertSameCompartment(cx, promise); |
| MOZ_ASSERT(strcmp(promise->getClass()->name, "Promise") == 0 || |
| strcmp(promise->getClass()->name, "MozAbortablePromise") == 0); |
| } |
| |
| JS_PUBLIC_API(void) |
| JS::dbg::onNewPromise(JSContext* cx, HandleObject promise) |
| { |
| AssertIsPromise(cx, promise); |
| Debugger::slowPathPromiseHook(cx, Debugger::OnNewPromise, promise); |
| } |
| |
| JS_PUBLIC_API(void) |
| JS::dbg::onPromiseSettled(JSContext* cx, HandleObject promise) |
| { |
| AssertIsPromise(cx, promise); |
| Debugger::slowPathPromiseHook(cx, Debugger::OnPromiseSettled, promise); |
| } |
| |
| JS_PUBLIC_API(bool) |
| JS::dbg::IsDebugger(JSObject& obj) |
| { |
| JSObject* unwrapped = CheckedUnwrap(&obj); |
| return unwrapped && |
| js::GetObjectClass(unwrapped) == &Debugger::jsclass && |
| js::Debugger::fromJSObject(unwrapped) != nullptr; |
| } |
| |
| JS_PUBLIC_API(bool) |
| JS::dbg::GetDebuggeeGlobals(JSContext* cx, JSObject& dbgObj, AutoObjectVector& vector) |
| { |
| MOZ_ASSERT(IsDebugger(dbgObj)); |
| js::Debugger* dbg = js::Debugger::fromJSObject(CheckedUnwrap(&dbgObj)); |
| |
| if (!vector.reserve(vector.length() + dbg->debuggees.count())) { |
| JS_ReportOutOfMemory(cx); |
| return false; |
| } |
| |
| for (WeakGlobalObjectSet::Range r = dbg->allDebuggees(); !r.empty(); r.popFront()) |
| vector.infallibleAppend(static_cast<JSObject*>(r.front())); |
| |
| return true; |
| } |
| |
| |
| /*** JS::dbg::GarbageCollectionEvent **************************************************************/ |
| |
| namespace JS { |
| namespace dbg { |
| |
| /* static */ GarbageCollectionEvent::Ptr |
| GarbageCollectionEvent::Create(JSRuntime* rt, ::js::gcstats::Statistics& stats, uint64_t gcNumber) |
| { |
| auto data = rt->make_unique<GarbageCollectionEvent>(gcNumber); |
| if (!data) |
| return nullptr; |
| |
| data->nonincrementalReason = stats.nonincrementalReason(); |
| |
| for (auto range = stats.sliceRange(); !range.empty(); range.popFront()) { |
| if (!data->reason) { |
| // There is only one GC reason for the whole cycle, but for legacy |
| // reasons this data is stored and replicated on each slice. Each |
| // slice used to have its own GCReason, but now they are all the |
| // same. |
| data->reason = gcstats::ExplainReason(range.front().reason); |
| MOZ_ASSERT(data->reason); |
| } |
| |
| if (!data->collections.growBy(1)) |
| return nullptr; |
| |
| data->collections.back().startTimestamp = range.front().startTimestamp; |
| data->collections.back().endTimestamp = range.front().endTimestamp; |
| } |
| |
| |
| return data; |
| } |
| |
| static bool |
| DefineStringProperty(JSContext* cx, HandleObject obj, PropertyName* propName, const char* strVal) |
| { |
| RootedValue val(cx, UndefinedValue()); |
| if (strVal) { |
| JSAtom* atomized = Atomize(cx, strVal, strlen(strVal)); |
| if (!atomized) |
| return false; |
| val = StringValue(atomized); |
| } |
| return DefineProperty(cx, obj, propName, val); |
| } |
| |
| JSObject* |
| GarbageCollectionEvent::toJSObject(JSContext* cx) const |
| { |
| RootedObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx)); |
| RootedValue gcCycleNumberVal(cx, NumberValue(majorGCNumber_)); |
| if (!obj || |
| !DefineStringProperty(cx, obj, cx->names().nonincrementalReason, nonincrementalReason) || |
| !DefineStringProperty(cx, obj, cx->names().reason, reason) || |
| !DefineProperty(cx, obj, cx->names().gcCycleNumber, gcCycleNumberVal)) |
| { |
| return nullptr; |
| } |
| |
| RootedArrayObject slicesArray(cx, NewDenseEmptyArray(cx)); |
| if (!slicesArray) |
| return nullptr; |
| |
| size_t idx = 0; |
| for (auto range = collections.all(); !range.empty(); range.popFront()) { |
| RootedPlainObject collectionObj(cx, NewBuiltinClassInstance<PlainObject>(cx)); |
| if (!collectionObj) |
| return nullptr; |
| |
| RootedValue start(cx, NumberValue(range.front().startTimestamp)); |
| RootedValue end(cx, NumberValue(range.front().endTimestamp)); |
| if (!DefineProperty(cx, collectionObj, cx->names().startTimestamp, start) || |
| !DefineProperty(cx, collectionObj, cx->names().endTimestamp, end)) |
| { |
| return nullptr; |
| } |
| |
| RootedValue collectionVal(cx, ObjectValue(*collectionObj)); |
| if (!DefineElement(cx, slicesArray, idx++, collectionVal)) |
| return nullptr; |
| } |
| |
| RootedValue slicesValue(cx, ObjectValue(*slicesArray)); |
| if (!DefineProperty(cx, obj, cx->names().collections, slicesValue)) |
| return nullptr; |
| |
| return obj; |
| } |
| |
| JS_PUBLIC_API(bool) |
| FireOnGarbageCollectionHook(JSContext* cx, JS::dbg::GarbageCollectionEvent::Ptr&& data) |
| { |
| AutoObjectVector triggered(cx); |
| |
| { |
| // We had better not GC (and potentially get a dangling Debugger |
| // pointer) while finding all Debuggers observing a debuggee that |
| // participated in this GC. |
| AutoCheckCannotGC noGC; |
| |
| for (Debugger* dbg : cx->runtime()->debuggerList) { |
| if (dbg->enabled && |
| dbg->observedGC(data->majorGCNumber()) && |
| dbg->getHook(Debugger::OnGarbageCollection)) |
| { |
| if (!triggered.append(dbg->object)) { |
| JS_ReportOutOfMemory(cx); |
| return false; |
| } |
| } |
| } |
| } |
| |
| for ( ; !triggered.empty(); triggered.popBack()) { |
| Debugger* dbg = Debugger::fromJSObject(triggered.back()); |
| dbg->fireOnGarbageCollectionHook(cx, data); |
| MOZ_ASSERT(!cx->isExceptionPending()); |
| } |
| |
| return true; |
| } |
| |
| } // namespace dbg |
| } // namespace JS |