forked from mirrors/gecko-dev
12522 lines
402 KiB
C++
12522 lines
402 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
|
|
* vim: set ts=8 sts=2 et sw=2 tw=80:
|
|
* 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/Sprintf.h"
|
|
#include "mozilla/TypeTraits.h"
|
|
|
|
#include <utility>
|
|
|
|
#include "jsfriendapi.h"
|
|
#include "jsnum.h"
|
|
|
|
#include "frontend/BytecodeCompilation.h"
|
|
#include "frontend/Parser.h"
|
|
#include "gc/FreeOp.h"
|
|
#include "gc/HashUtil.h"
|
|
#include "gc/Marking.h"
|
|
#include "gc/Policy.h"
|
|
#include "gc/PublicIterators.h"
|
|
#include "jit/BaselineDebugModeOSR.h"
|
|
#include "jit/BaselineJIT.h"
|
|
#include "js/CharacterEncoding.h"
|
|
#include "js/Date.h"
|
|
#include "js/PropertyDescriptor.h"
|
|
#include "js/PropertySpec.h"
|
|
#include "js/SourceText.h"
|
|
#include "js/StableStringChars.h"
|
|
#include "js/UbiNodeBreadthFirst.h"
|
|
#include "js/Vector.h"
|
|
#include "js/Wrapper.h"
|
|
#include "proxy/ScriptedProxyHandler.h"
|
|
#include "util/Text.h"
|
|
#include "vm/ArgumentsObject.h"
|
|
#include "vm/AsyncFunction.h"
|
|
#include "vm/AsyncIteration.h"
|
|
#include "vm/DebuggerMemory.h"
|
|
#include "vm/GeckoProfiler.h"
|
|
#include "vm/GeneratorObject.h"
|
|
#include "vm/JSContext.h"
|
|
#include "vm/JSObject.h"
|
|
#include "vm/Realm.h"
|
|
#include "vm/TraceLogging.h"
|
|
#include "vm/WrapperObject.h"
|
|
#include "wasm/WasmInstance.h"
|
|
|
|
#include "gc/GC-inl.h"
|
|
#include "vm/BytecodeUtil-inl.h"
|
|
#include "vm/Compartment-inl.h"
|
|
#include "vm/GeckoProfiler-inl.h"
|
|
#include "vm/JSObject-inl.h"
|
|
#include "vm/JSScript-inl.h"
|
|
#include "vm/NativeObject-inl.h"
|
|
#include "vm/Stack-inl.h"
|
|
|
|
using namespace js;
|
|
|
|
using JS::AutoStableStringChars;
|
|
using JS::CompileOptions;
|
|
using JS::SourceOwnership;
|
|
using JS::SourceText;
|
|
using JS::dbg::AutoEntryMonitor;
|
|
using JS::dbg::Builder;
|
|
using js::frontend::IsIdentifier;
|
|
using mozilla::AsVariant;
|
|
using mozilla::DebugOnly;
|
|
using mozilla::MakeScopeExit;
|
|
using mozilla::Maybe;
|
|
using mozilla::Nothing;
|
|
using mozilla::Some;
|
|
using mozilla::TimeDuration;
|
|
using mozilla::TimeStamp;
|
|
|
|
/*** Forward declarations, ClassOps and Classes *****************************/
|
|
|
|
static void DebuggerFrame_finalize(FreeOp* fop, JSObject* obj);
|
|
static void DebuggerFrame_trace(JSTracer* trc, JSObject* obj);
|
|
static void DebuggerEnv_trace(JSTracer* trc, JSObject* obj);
|
|
static void DebuggerObject_trace(JSTracer* trc, JSObject* obj);
|
|
static void DebuggerScript_trace(JSTracer* trc, JSObject* obj);
|
|
static void DebuggerSource_trace(JSTracer* trc, JSObject* obj);
|
|
|
|
enum {
|
|
JSSLOT_DEBUGFRAME_OWNER,
|
|
JSSLOT_DEBUGFRAME_ARGUMENTS,
|
|
JSSLOT_DEBUGFRAME_ONSTEP_HANDLER,
|
|
JSSLOT_DEBUGFRAME_ONPOP_HANDLER,
|
|
JSSLOT_DEBUGFRAME_COUNT
|
|
};
|
|
|
|
inline js::Debugger* js::DebuggerFrame::owner() const {
|
|
JSObject* dbgobj = &getReservedSlot(JSSLOT_DEBUGFRAME_OWNER).toObject();
|
|
return Debugger::fromJSObject(dbgobj);
|
|
}
|
|
|
|
const ClassOps DebuggerFrame::classOps_ = {nullptr, /* addProperty */
|
|
nullptr, /* delProperty */
|
|
nullptr, /* enumerate */
|
|
nullptr, /* newEnumerate */
|
|
nullptr, /* resolve */
|
|
nullptr, /* mayResolve */
|
|
DebuggerFrame_finalize,
|
|
nullptr, /* call */
|
|
nullptr, /* hasInstance */
|
|
nullptr, /* construct */
|
|
DebuggerFrame_trace};
|
|
|
|
const Class DebuggerFrame::class_ = {
|
|
"Frame",
|
|
JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGFRAME_COUNT) |
|
|
JSCLASS_BACKGROUND_FINALIZE,
|
|
&DebuggerFrame::classOps_};
|
|
|
|
enum { JSSLOT_DEBUGARGUMENTS_FRAME, JSSLOT_DEBUGARGUMENTS_COUNT };
|
|
|
|
const Class DebuggerArguments::class_ = {
|
|
"Arguments", JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGARGUMENTS_COUNT)};
|
|
|
|
const ClassOps DebuggerEnvironment::classOps_ = {nullptr, /* addProperty */
|
|
nullptr, /* delProperty */
|
|
nullptr, /* enumerate */
|
|
nullptr, /* newEnumerate */
|
|
nullptr, /* resolve */
|
|
nullptr, /* mayResolve */
|
|
nullptr, /* finalize */
|
|
nullptr, /* call */
|
|
nullptr, /* hasInstance */
|
|
nullptr, /* construct */
|
|
DebuggerEnv_trace};
|
|
|
|
const Class DebuggerEnvironment::class_ = {
|
|
"Environment",
|
|
JSCLASS_HAS_PRIVATE |
|
|
JSCLASS_HAS_RESERVED_SLOTS(DebuggerEnvironment::RESERVED_SLOTS),
|
|
&classOps_};
|
|
|
|
enum { JSSLOT_DEBUGOBJECT_OWNER, JSSLOT_DEBUGOBJECT_COUNT };
|
|
|
|
const ClassOps DebuggerObject::classOps_ = {nullptr, /* addProperty */
|
|
nullptr, /* delProperty */
|
|
nullptr, /* enumerate */
|
|
nullptr, /* newEnumerate */
|
|
nullptr, /* resolve */
|
|
nullptr, /* mayResolve */
|
|
nullptr, /* finalize */
|
|
nullptr, /* call */
|
|
nullptr, /* hasInstance */
|
|
nullptr, /* construct */
|
|
DebuggerObject_trace};
|
|
|
|
const Class DebuggerObject::class_ = {
|
|
"Object", JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS),
|
|
&classOps_};
|
|
|
|
enum { JSSLOT_DEBUGSCRIPT_OWNER, JSSLOT_DEBUGSCRIPT_COUNT };
|
|
|
|
static const ClassOps DebuggerScript_classOps = {nullptr, /* addProperty */
|
|
nullptr, /* delProperty */
|
|
nullptr, /* enumerate */
|
|
nullptr, /* newEnumerate */
|
|
nullptr, /* resolve */
|
|
nullptr, /* mayResolve */
|
|
nullptr, /* finalize */
|
|
nullptr, /* call */
|
|
nullptr, /* hasInstance */
|
|
nullptr, /* construct */
|
|
DebuggerScript_trace};
|
|
|
|
static const Class DebuggerScript_class = {
|
|
"Script",
|
|
JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGSCRIPT_COUNT),
|
|
&DebuggerScript_classOps};
|
|
|
|
enum {
|
|
JSSLOT_DEBUGSOURCE_OWNER,
|
|
JSSLOT_DEBUGSOURCE_TEXT,
|
|
JSSLOT_DEBUGSOURCE_COUNT
|
|
};
|
|
|
|
static const ClassOps DebuggerSource_classOps = {nullptr, /* addProperty */
|
|
nullptr, /* delProperty */
|
|
nullptr, /* enumerate */
|
|
nullptr, /* newEnumerate */
|
|
nullptr, /* resolve */
|
|
nullptr, /* mayResolve */
|
|
nullptr, /* finalize */
|
|
nullptr, /* call */
|
|
nullptr, /* hasInstance */
|
|
nullptr, /* construct */
|
|
DebuggerSource_trace};
|
|
|
|
static const Class DebuggerSource_class = {
|
|
"Source",
|
|
JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGSOURCE_COUNT),
|
|
&DebuggerSource_classOps};
|
|
|
|
/*** Utils ******************************************************************/
|
|
|
|
/*
|
|
* If fun is an interpreted function, remove any async function/generator
|
|
* wrapper and return the underlying scripted function. Otherwise, return fun
|
|
* unchanged.
|
|
*
|
|
* Async functions are implemented as native functions wrapped around a scripted
|
|
* function. JSScripts hold ordinary inner JSFunctions in their object arrays,
|
|
* and when we need to actually create a JS-visible function object, we build an
|
|
* ordinary JS closure and apply the async wrapper to it. Async generators are
|
|
* similar.
|
|
*
|
|
* This means that JSFunction::isInterpreted returns false for such functions,
|
|
* even though their actual code is indeed JavaScript. Debugger should treat
|
|
* async functions and generators like any other scripted function, so we must
|
|
* carefully check for them whenever we want inspect a function.
|
|
*/
|
|
|
|
static JSFunction* RemoveAsyncWrapper(JSFunction* fun) {
|
|
if (js::IsWrappedAsyncFunction(fun)) {
|
|
fun = js::GetUnwrappedAsyncFunction(fun);
|
|
} else if (js::IsWrappedAsyncGenerator(fun)) {
|
|
fun = js::GetUnwrappedAsyncGenerator(fun);
|
|
}
|
|
|
|
return fun;
|
|
}
|
|
|
|
static inline bool EnsureFunctionHasScript(JSContext* cx, HandleFunction fun) {
|
|
if (fun->isInterpretedLazy()) {
|
|
AutoRealm ar(cx, fun);
|
|
return !!JSFunction::getOrCreateScript(cx, fun);
|
|
}
|
|
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);
|
|
ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, val,
|
|
nullptr, "not an identifier");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
class js::AutoRestoreRealmDebugMode {
|
|
Realm* realm_;
|
|
unsigned bits_;
|
|
|
|
public:
|
|
explicit AutoRestoreRealmDebugMode(Realm* realm)
|
|
: realm_(realm), bits_(realm->debugModeBits_) {
|
|
MOZ_ASSERT(realm_);
|
|
}
|
|
|
|
~AutoRestoreRealmDebugMode() {
|
|
if (realm_) {
|
|
realm_->debugModeBits_ = bits_;
|
|
}
|
|
}
|
|
|
|
void release() { realm_ = nullptr; }
|
|
};
|
|
|
|
// Given a Debugger instance dbg, if it is enabled, prevents all its debuggee
|
|
// compartments from executing scripts. Attempts to run script will throw an
|
|
// instance of Debugger.DebuggeeWouldRun from the topmost locked Debugger's
|
|
// compartment.
|
|
class MOZ_RAII js::EnterDebuggeeNoExecute {
|
|
friend class js::LeaveDebuggeeNoExecute;
|
|
|
|
Debugger& dbg_;
|
|
EnterDebuggeeNoExecute** stack_;
|
|
EnterDebuggeeNoExecute* prev_;
|
|
|
|
// Non-nullptr when unlocked temporarily by a LeaveDebuggeeNoExecute.
|
|
LeaveDebuggeeNoExecute* unlocked_;
|
|
|
|
// When DebuggeeWouldRun is a warning instead of an error, whether we've
|
|
// reported a warning already.
|
|
bool reported_;
|
|
|
|
public:
|
|
explicit EnterDebuggeeNoExecute(JSContext* cx, Debugger& dbg)
|
|
: dbg_(dbg), unlocked_(nullptr), reported_(false) {
|
|
stack_ = &cx->noExecuteDebuggerTop.ref();
|
|
prev_ = *stack_;
|
|
*stack_ = this;
|
|
}
|
|
|
|
~EnterDebuggeeNoExecute() {
|
|
MOZ_ASSERT(*stack_ == this);
|
|
*stack_ = prev_;
|
|
}
|
|
|
|
Debugger& debugger() const { return dbg_; }
|
|
|
|
#ifdef DEBUG
|
|
static bool isLockedInStack(JSContext* cx, Debugger& dbg) {
|
|
for (EnterDebuggeeNoExecute* it = cx->noExecuteDebuggerTop; it;
|
|
it = it->prev_) {
|
|
if (&it->debugger() == &dbg) {
|
|
return !it->unlocked_;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
// Given a JSContext entered into a debuggee realm, find the lock
|
|
// that locks it. Returns nullptr if not found.
|
|
static EnterDebuggeeNoExecute* findInStack(JSContext* cx) {
|
|
Realm* debuggee = cx->realm();
|
|
for (EnterDebuggeeNoExecute* it = cx->noExecuteDebuggerTop; it;
|
|
it = it->prev_) {
|
|
Debugger& dbg = it->debugger();
|
|
if (!it->unlocked_ && dbg.isEnabled() &&
|
|
dbg.observesGlobal(debuggee->maybeGlobal())) {
|
|
return it;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Given a JSContext entered into a debuggee compartment, report a
|
|
// warning or an error if there is a lock that locks it.
|
|
static bool reportIfFoundInStack(JSContext* cx, HandleScript script) {
|
|
if (EnterDebuggeeNoExecute* nx = findInStack(cx)) {
|
|
bool warning = !cx->options().throwOnDebuggeeWouldRun();
|
|
if (!warning || !nx->reported_) {
|
|
AutoRealm ar(cx, nx->debugger().toJSObject());
|
|
nx->reported_ = true;
|
|
if (cx->options().dumpStackOnDebuggeeWouldRun()) {
|
|
fprintf(stdout, "Dumping stack for DebuggeeWouldRun:\n");
|
|
DumpBacktrace(cx);
|
|
}
|
|
const char* filename =
|
|
script->filename() ? script->filename() : "(none)";
|
|
char linenoStr[15];
|
|
SprintfLiteral(linenoStr, "%u", script->lineno());
|
|
unsigned flags = warning ? JSREPORT_WARNING : JSREPORT_ERROR;
|
|
// FIXME: filename should be UTF-8 (bug 987069).
|
|
return JS_ReportErrorFlagsAndNumberLatin1(
|
|
cx, flags, GetErrorMessage, nullptr, JSMSG_DEBUGGEE_WOULD_RUN,
|
|
filename, linenoStr);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
// Given a JSContext entered into a debuggee compartment, if it is in
|
|
// an NX section, unlock the topmost EnterDebuggeeNoExecute instance.
|
|
//
|
|
// Does nothing if debuggee is not in an NX section. For example, this
|
|
// situation arises when invocation functions are called without entering
|
|
// Debugger code, e.g., calling D.O.p.executeInGlobal or D.O.p.apply.
|
|
class MOZ_RAII js::LeaveDebuggeeNoExecute {
|
|
EnterDebuggeeNoExecute* prevLocked_;
|
|
|
|
public:
|
|
explicit LeaveDebuggeeNoExecute(JSContext* cx)
|
|
: prevLocked_(EnterDebuggeeNoExecute::findInStack(cx)) {
|
|
if (prevLocked_) {
|
|
MOZ_ASSERT(!prevLocked_->unlocked_);
|
|
prevLocked_->unlocked_ = this;
|
|
}
|
|
}
|
|
|
|
~LeaveDebuggeeNoExecute() {
|
|
if (prevLocked_) {
|
|
MOZ_ASSERT(prevLocked_->unlocked_ == this);
|
|
prevLocked_->unlocked_ = nullptr;
|
|
}
|
|
}
|
|
};
|
|
|
|
/* static */ bool Debugger::slowPathCheckNoExecute(JSContext* cx,
|
|
HandleScript script) {
|
|
MOZ_ASSERT(cx->realm()->isDebuggee());
|
|
MOZ_ASSERT(cx->noExecuteDebuggerTop);
|
|
return EnterDebuggeeNoExecute::reportIfFoundInStack(cx, script);
|
|
}
|
|
|
|
static inline void NukeDebuggerWrapper(NativeObject* wrapper) {
|
|
// In some OOM failure cases, we need to destroy the edge to the referent,
|
|
// to avoid trying to trace it during untimely collections.
|
|
wrapper->setPrivate(nullptr);
|
|
}
|
|
|
|
static bool ValueToStableChars(JSContext* cx, const char* fnname,
|
|
HandleValue value,
|
|
AutoStableStringChars& stableChars) {
|
|
if (!value.isString()) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_NOT_EXPECTED_TYPE, fnname, "string",
|
|
InformalValueTypeName(value));
|
|
return false;
|
|
}
|
|
RootedLinearString linear(cx, value.toString()->ensureLinear(cx));
|
|
if (!linear) {
|
|
return false;
|
|
}
|
|
if (!stableChars.initTwoByte(cx, linear)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool EvalOptions::setFilename(JSContext* cx, const char* filename) {
|
|
JS::UniqueChars copy;
|
|
if (filename) {
|
|
copy = DuplicateString(cx, filename);
|
|
if (!copy) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
filename_ = std::move(copy);
|
|
return true;
|
|
}
|
|
|
|
static bool ParseEvalOptions(JSContext* cx, HandleValue value,
|
|
EvalOptions& options) {
|
|
if (!value.isObject()) {
|
|
return true;
|
|
}
|
|
|
|
RootedObject opts(cx, &value.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;
|
|
}
|
|
UniqueChars url_bytes = JS_EncodeStringToLatin1(cx, url_str);
|
|
if (!url_bytes) {
|
|
return false;
|
|
}
|
|
if (!options.setFilename(cx, url_bytes.get())) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!JS_GetProperty(cx, opts, "lineNumber", &v)) {
|
|
return false;
|
|
}
|
|
if (!v.isUndefined()) {
|
|
uint32_t lineno;
|
|
if (!ToUint32(cx, v, &lineno)) {
|
|
return false;
|
|
}
|
|
options.setLineno(lineno);
|
|
}
|
|
|
|
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>()) {
|
|
ReportValueError(cx, JSMSG_DEBUG_WRAPPER_IN_WAY, JSDVG_SEARCH_STACK,
|
|
dbgobj, nullptr, isWrapper, isWindowProxy);
|
|
} else {
|
|
ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK, dbgobj,
|
|
nullptr, "a global object");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*** Breakpoints ************************************************************/
|
|
|
|
BreakpointSite::BreakpointSite(Type type) : type_(type), enabledCount(0) {}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
bool BreakpointSite::isEmpty() const { return breakpoints.isEmpty(); }
|
|
|
|
Breakpoint* BreakpointSite::firstBreakpoint() const {
|
|
if (isEmpty()) {
|
|
return nullptr;
|
|
}
|
|
return &(*breakpoints.begin());
|
|
}
|
|
|
|
bool BreakpointSite::hasBreakpoint(Breakpoint* toFind) {
|
|
const BreakpointList::Iterator bp(toFind);
|
|
for (auto p = breakpoints.begin(); p; p++) {
|
|
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());
|
|
debugger->breakpoints.pushBack(this);
|
|
site->breakpoints.pushBack(this);
|
|
}
|
|
|
|
void Breakpoint::destroy(FreeOp* fop,
|
|
MayDestroySite mayDestroySite /* true */) {
|
|
if (debugger->enabled) {
|
|
site->dec(fop);
|
|
}
|
|
debugger->breakpoints.remove(this);
|
|
site->breakpoints.remove(this);
|
|
if (mayDestroySite == MayDestroySite::True) {
|
|
site->destroyIfEmpty(fop);
|
|
}
|
|
fop->delete_(this);
|
|
}
|
|
|
|
Breakpoint* Breakpoint::nextInDebugger() { return debuggerLink.mNext; }
|
|
|
|
Breakpoint* Breakpoint::nextInSite() { return siteLink.mNext; }
|
|
|
|
JSBreakpointSite::JSBreakpointSite(JSScript* script, jsbytecode* pc)
|
|
: BreakpointSite(Type::JS), script(script), pc(pc) {
|
|
MOZ_ASSERT(!script->hasBreakpointsAt(pc));
|
|
}
|
|
|
|
void JSBreakpointSite::recompile(FreeOp* fop) {
|
|
if (script->hasBaselineScript()) {
|
|
script->baselineScript()->toggleDebugTraps(script, pc);
|
|
}
|
|
}
|
|
|
|
void JSBreakpointSite::destroyIfEmpty(FreeOp* fop) {
|
|
if (isEmpty()) {
|
|
script->destroyBreakpointSite(fop, pc);
|
|
}
|
|
}
|
|
|
|
WasmBreakpointSite::WasmBreakpointSite(wasm::DebugState* debug_,
|
|
uint32_t offset_)
|
|
: BreakpointSite(Type::Wasm), debug(debug_), offset(offset_) {
|
|
MOZ_ASSERT(debug_);
|
|
}
|
|
|
|
void WasmBreakpointSite::recompile(FreeOp* fop) {
|
|
debug->toggleBreakpointTrap(fop->runtime(), offset, isEnabled());
|
|
}
|
|
|
|
void WasmBreakpointSite::destroyIfEmpty(FreeOp* fop) {
|
|
if (isEmpty()) {
|
|
debug->destroyBreakpointSite(fop, offset);
|
|
}
|
|
}
|
|
|
|
/*** Debugger hook dispatch *************************************************/
|
|
|
|
Debugger::Debugger(JSContext* cx, NativeObject* dbg)
|
|
: object(dbg),
|
|
debuggees(cx->zone()),
|
|
uncaughtExceptionHook(nullptr),
|
|
enabled(true),
|
|
allowUnobservedAsmJS(false),
|
|
collectCoverageInfo(false),
|
|
observedGCs(cx->zone()),
|
|
allocationsLog(cx),
|
|
trackingAllocationSites(false),
|
|
allocationSamplingProbability(1.0),
|
|
maxAllocationsLogLength(DEFAULT_MAX_LOG_LENGTH),
|
|
allocationsLogOverflowed(false),
|
|
frames(cx->zone()),
|
|
generatorFrames(cx),
|
|
scripts(cx),
|
|
lazyScripts(cx),
|
|
sources(cx),
|
|
objects(cx),
|
|
environments(cx),
|
|
wasmInstanceScripts(cx),
|
|
wasmInstanceSources(cx),
|
|
#ifdef NIGHTLY_BUILD
|
|
traceLoggerLastDrainedSize(0),
|
|
traceLoggerLastDrainedIteration(0),
|
|
#endif
|
|
traceLoggerScriptedCallsLastDrainedSize(0),
|
|
traceLoggerScriptedCallsLastDrainedIteration(0) {
|
|
cx->check(dbg);
|
|
|
|
#ifdef JS_TRACE_LOGGING
|
|
TraceLoggerThread* logger = TraceLoggerForCurrentThread(cx);
|
|
if (logger) {
|
|
# ifdef NIGHTLY_BUILD
|
|
logger->getIterationAndSize(&traceLoggerLastDrainedIteration,
|
|
&traceLoggerLastDrainedSize);
|
|
# endif
|
|
logger->getIterationAndSize(&traceLoggerScriptedCallsLastDrainedIteration,
|
|
&traceLoggerScriptedCallsLastDrainedSize);
|
|
}
|
|
#endif
|
|
|
|
cx->runtime()->debuggerList().insertBack(this);
|
|
}
|
|
|
|
Debugger::~Debugger() {
|
|
MOZ_ASSERT(debuggees.empty());
|
|
allocationsLog.clear();
|
|
|
|
// We don't have to worry about locking here since Debugger is not
|
|
// background finalized.
|
|
JSContext* cx = TlsContext.get();
|
|
if (onNewGlobalObjectWatchersLink.mPrev ||
|
|
onNewGlobalObjectWatchersLink.mNext ||
|
|
cx->runtime()->onNewGlobalObjectWatchers().begin() ==
|
|
JSRuntime::WatchersList::Iterator(this)) {
|
|
cx->runtime()->onNewGlobalObjectWatchers().remove(this);
|
|
}
|
|
}
|
|
|
|
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(DebuggerEnvironment::OWNER_SLOT));
|
|
|
|
/* 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() == &DebuggerEnvironment::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::getFrame(JSContext* cx, const FrameIter& iter,
|
|
MutableHandleValue vp) {
|
|
RootedDebuggerFrame result(cx);
|
|
if (!Debugger::getFrame(cx, iter, &result)) {
|
|
return false;
|
|
}
|
|
vp.setObject(*result);
|
|
return true;
|
|
}
|
|
|
|
bool Debugger::getFrame(JSContext* cx, const FrameIter& iter,
|
|
MutableHandleDebuggerFrame result) {
|
|
AbstractFramePtr referent = iter.abstractFramePtr();
|
|
MOZ_ASSERT_IF(referent.hasScript(), !referent.script()->selfHosted());
|
|
|
|
if (referent.hasScript() &&
|
|
!referent.script()->ensureHasAnalyzedArgsUsage(cx)) {
|
|
return false;
|
|
}
|
|
|
|
FrameMap::AddPtr p = frames.lookupForAdd(referent);
|
|
if (!p) {
|
|
RootedDebuggerFrame frame(cx);
|
|
|
|
// If this is a generator frame, there may be an existing
|
|
// Debugger.Frame object that isn't in `frames` because the generator
|
|
// was suspended, popping the stack frame, and later resumed (and we
|
|
// were not stepping, so did not pass through slowPathOnResumeFrame).
|
|
Rooted<GeneratorObject*> genObj(cx);
|
|
GeneratorWeakMap::AddPtr gp;
|
|
if (referent.isGeneratorFrame()) {
|
|
{
|
|
AutoRealm ar(cx, referent.callee());
|
|
genObj = GetGeneratorObjectForFrame(cx, referent);
|
|
}
|
|
if (genObj) {
|
|
gp = generatorFrames.lookupForAdd(genObj);
|
|
if (gp) {
|
|
frame = &gp->value()->as<DebuggerFrame>();
|
|
|
|
// We have found an existing Debugger.Frame object. But
|
|
// since it was previously popped (see comment above), it
|
|
// is not currently "live". We must revive it.
|
|
if (!frame->resume(iter)) {
|
|
return false;
|
|
}
|
|
if (!ensureExecutionObservabilityOfFrame(cx, referent)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no GeneratorObject exists yet, we create a Debugger.Frame
|
|
// below anyway, and Debugger::onNewGenerator() will associate it
|
|
// with the GeneratorObject later when we hit JSOP_GENERATOR.
|
|
}
|
|
|
|
if (!frame) {
|
|
// Create and populate the Debugger.Frame object.
|
|
RootedObject proto(
|
|
cx, &object->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO).toObject());
|
|
RootedNativeObject debugger(cx, object);
|
|
|
|
frame = DebuggerFrame::create(cx, proto, iter, debugger);
|
|
if (!frame) {
|
|
return false;
|
|
}
|
|
|
|
if (!ensureExecutionObservabilityOfFrame(cx, referent)) {
|
|
return false;
|
|
}
|
|
|
|
if (genObj) {
|
|
DebuggerFrame* frameObj = frame;
|
|
if (!generatorFrames.relookupOrAdd(gp, genObj, frameObj)) {
|
|
ReportOutOfMemory(cx);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!frames.add(p, referent, frame)) {
|
|
NukeDebuggerWrapper(frame);
|
|
if (genObj) {
|
|
generatorFrames.remove(genObj);
|
|
}
|
|
ReportOutOfMemory(cx);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
result.set(&p->value()->as<DebuggerFrame>());
|
|
return true;
|
|
}
|
|
|
|
bool Debugger::addGeneratorFrame(JSContext* cx, Handle<GeneratorObject*> genObj,
|
|
HandleDebuggerFrame frameObj) {
|
|
GeneratorWeakMap::AddPtr p = generatorFrames.lookupForAdd(genObj);
|
|
if (p) {
|
|
MOZ_ASSERT(p->value() == frameObj);
|
|
} else {
|
|
if (!generatorFrames.relookupOrAdd(p, genObj, frameObj)) {
|
|
ReportOutOfMemory(cx);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool Debugger::hasLiveHook(GlobalObject* global, Hook which) {
|
|
if (GlobalObject::DebuggerVector* debuggers = global->getDebuggers()) {
|
|
for (auto 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(JSRuntime* rt) const {
|
|
if (!enabled) {
|
|
return false;
|
|
}
|
|
|
|
// A onNewGlobalObject hook does not hold its Debugger live, so its behavior
|
|
// is nondeterministic. This behavior is not satisfying, but it is at least
|
|
// documented.
|
|
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()) {
|
|
switch (bp->site->type()) {
|
|
case BreakpointSite::Type::JS:
|
|
if (IsMarkedUnbarriered(rt, &bp->site->asJS()->script)) {
|
|
return true;
|
|
}
|
|
break;
|
|
case BreakpointSite::Type::Wasm:
|
|
if (IsMarkedUnbarriered(rt, &bp->asWasm()->wasmInstance)) {
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check for hooks in live stack frames.
|
|
for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) {
|
|
DebuggerFrame& frameObj = r.front().value()->as<DebuggerFrame>();
|
|
if (frameObj.hasAnyLiveHooks()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Check for hooks set on suspended generator frames.
|
|
for (GeneratorWeakMap::Range r = generatorFrames.all(); !r.empty();
|
|
r.popFront()) {
|
|
JSObject* key = r.front().key();
|
|
DebuggerFrame& frameObj = r.front().value()->as<DebuggerFrame>();
|
|
if (IsMarkedUnbarriered(rt, &key) && frameObj.hasAnyLiveHooks()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/* static */ ResumeMode Debugger::slowPathOnEnterFrame(JSContext* cx,
|
|
AbstractFramePtr frame) {
|
|
RootedValue rval(cx);
|
|
ResumeMode resumeMode = dispatchHook(cx,
|
|
[frame](Debugger* dbg) -> bool {
|
|
return dbg->observesFrame(frame) &&
|
|
dbg->observesEnterFrame();
|
|
},
|
|
[&](Debugger* dbg) -> ResumeMode {
|
|
return dbg->fireEnterFrame(cx, &rval);
|
|
});
|
|
|
|
switch (resumeMode) {
|
|
case ResumeMode::Continue:
|
|
break;
|
|
|
|
case ResumeMode::Throw:
|
|
cx->setPendingException(rval);
|
|
break;
|
|
|
|
case ResumeMode::Terminate:
|
|
cx->clearPendingException();
|
|
break;
|
|
|
|
case ResumeMode::Return:
|
|
frame.setReturnValue(rval);
|
|
break;
|
|
|
|
default:
|
|
MOZ_CRASH("bad Debugger::onEnterFrame resume mode");
|
|
}
|
|
|
|
return resumeMode;
|
|
}
|
|
|
|
/* static */ ResumeMode Debugger::slowPathOnResumeFrame(
|
|
JSContext* cx, AbstractFramePtr frame) {
|
|
// Don't count on this method to be called every time a generator is
|
|
// resumed! This is called only if the frame's debuggee bit is set,
|
|
// i.e. the script has breakpoints or the frame is stepping.
|
|
MOZ_ASSERT(frame.isGeneratorFrame());
|
|
MOZ_ASSERT(frame.isDebuggee());
|
|
|
|
Rooted<GeneratorObject*> genObj(cx, GetGeneratorObjectForFrame(cx, frame));
|
|
MOZ_ASSERT(genObj);
|
|
|
|
// For each debugger, if there is an existing Debugger.Frame object for the
|
|
// resumed `frame`, update it with the new frame pointer and make sure the
|
|
// frame is observable.
|
|
if (GlobalObject::DebuggerVector* debuggers =
|
|
frame.global()->getDebuggers()) {
|
|
for (Debugger* dbg : *debuggers) {
|
|
if (GeneratorWeakMap::Ptr entry = dbg->generatorFrames.lookup(genObj)) {
|
|
DebuggerFrame* frameObj = &entry->value()->as<DebuggerFrame>();
|
|
if (!dbg->frames.putNew(frame, frameObj)) {
|
|
ReportOutOfMemory(cx);
|
|
return ResumeMode::Throw;
|
|
}
|
|
|
|
FrameIter iter(cx);
|
|
MOZ_ASSERT(iter.abstractFramePtr() == frame);
|
|
if (!frameObj->resume(iter)) {
|
|
return ResumeMode::Throw;
|
|
}
|
|
if (!ensureExecutionObservabilityOfFrame(cx, frame)) {
|
|
return ResumeMode::Throw;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return slowPathOnEnterFrame(cx, frame);
|
|
}
|
|
|
|
static void DebuggerFrame_maybeDecrementFrameScriptStepModeCount(
|
|
FreeOp* fop, AbstractFramePtr frame, NativeObject* frameobj);
|
|
|
|
/*
|
|
* RAII class to mark a generator as "running" temporarily while running
|
|
* debugger code.
|
|
*
|
|
* When Debugger::slowPathOnLeaveFrame is called for a frame that is yielding
|
|
* or awaiting, its generator is in the "suspended" state. Letting script
|
|
* observe this state, with the generator on stack yet also reenterable, would
|
|
* be bad, so we mark it running while we fire events.
|
|
*/
|
|
class MOZ_RAII AutoSetGeneratorRunning {
|
|
int32_t resumeIndex_;
|
|
Rooted<GeneratorObject*> genObj_;
|
|
|
|
public:
|
|
AutoSetGeneratorRunning(JSContext* cx, Handle<GeneratorObject*> genObj)
|
|
: resumeIndex_(0), genObj_(cx, genObj) {
|
|
if (genObj) {
|
|
if (!genObj->isClosed() && genObj->isSuspended()) {
|
|
// Yielding or awaiting.
|
|
resumeIndex_ =
|
|
genObj->getFixedSlot(GeneratorObject::RESUME_INDEX_SLOT).toInt32();
|
|
genObj->setRunning();
|
|
} else {
|
|
// Returning or throwing. The generator is already closed, if
|
|
// it was ever exposed at all.
|
|
genObj_ = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
~AutoSetGeneratorRunning() {
|
|
if (genObj_) {
|
|
MOZ_ASSERT(genObj_->isRunning());
|
|
genObj_->setFixedSlot(GeneratorObject::RESUME_INDEX_SLOT,
|
|
Int32Value(resumeIndex_));
|
|
}
|
|
}
|
|
};
|
|
|
|
/*
|
|
* 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,
|
|
jsbytecode* pc, bool frameOk) {
|
|
mozilla::DebugOnly<Handle<GlobalObject*>> debuggeeGlobal = cx->global();
|
|
|
|
// Determine if we are suspending this frame or popping it forever.
|
|
bool suspending = false;
|
|
Rooted<GeneratorObject*> genObj(cx);
|
|
if (frame.isGeneratorFrame()) {
|
|
// If we're leaving successfully at a yield opcode, we're probably
|
|
// suspending; the `isClosed()` check detects a debugger forced return
|
|
// from an `onStep` handler, which looks almost the same.
|
|
genObj = GetGeneratorObjectForFrame(cx, frame);
|
|
suspending =
|
|
frameOk && pc &&
|
|
(*pc == JSOP_INITIALYIELD || *pc == JSOP_YIELD || *pc == JSOP_AWAIT) &&
|
|
!genObj->isClosed();
|
|
}
|
|
|
|
bool success = false;
|
|
auto frameMapsGuard = MakeScopeExit([&] {
|
|
// Clean up all Debugger.Frame instances on exit. On suspending, pass
|
|
// the flag that says to leave those frames `.live`. Note that if
|
|
// suspending && !success, the generator is closed, not suspended.
|
|
removeFromFrameMapsAndClearBreakpointsIn(cx, frame, suspending && success);
|
|
});
|
|
|
|
// 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.
|
|
Rooted<DebuggerFrameVector> frames(cx, DebuggerFrameVector(cx));
|
|
if (!getDebuggerFrames(frame, &frames)) {
|
|
return false;
|
|
}
|
|
if (frames.empty()) {
|
|
return frameOk;
|
|
}
|
|
|
|
// Save the frame's completion value.
|
|
ResumeMode resumeMode;
|
|
RootedValue value(cx);
|
|
Debugger::resultToCompletion(cx, frameOk, frame.returnValue(), &resumeMode,
|
|
&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()) {
|
|
// For each Debugger.Frame, fire its onPop handler, if any.
|
|
for (size_t i = 0; i < frames.length(); i++) {
|
|
HandleDebuggerFrame frameobj = frames[i];
|
|
Debugger* dbg = Debugger::fromChildJSObject(frameobj);
|
|
EnterDebuggeeNoExecute nx(cx, *dbg);
|
|
|
|
if (dbg->enabled && frameobj->onPopHandler()) {
|
|
OnPopHandler* handler = frameobj->onPopHandler();
|
|
|
|
Maybe<AutoRealm> ar;
|
|
ar.emplace(cx, dbg->object);
|
|
|
|
RootedValue wrappedValue(cx, value);
|
|
RootedValue completion(cx);
|
|
if (!dbg->wrapDebuggeeValue(cx, &wrappedValue)) {
|
|
resumeMode = dbg->reportUncaughtException(ar);
|
|
break;
|
|
}
|
|
|
|
// Call the onPop handler.
|
|
ResumeMode nextResumeMode = resumeMode;
|
|
RootedValue nextValue(cx, wrappedValue);
|
|
bool success;
|
|
{
|
|
AutoSetGeneratorRunning asgr(cx, genObj);
|
|
success = handler->onPop(cx, frameobj, nextResumeMode, &nextValue);
|
|
}
|
|
nextResumeMode = dbg->processParsedHandlerResult(
|
|
ar, frame, pc, success, nextResumeMode, &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() == debuggeeGlobal->compartment());
|
|
MOZ_ASSERT(!cx->isExceptionPending());
|
|
|
|
// ResumeMode::Continue means "make no change".
|
|
if (nextResumeMode != ResumeMode::Continue) {
|
|
resumeMode = nextResumeMode;
|
|
value = nextValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Establish (resumeMode, value) as our resumption value.
|
|
switch (resumeMode) {
|
|
case ResumeMode::Return:
|
|
frame.setReturnValue(value);
|
|
success = true;
|
|
return true;
|
|
|
|
case ResumeMode::Throw:
|
|
cx->setPendingException(value);
|
|
return false;
|
|
|
|
case ResumeMode::Terminate:
|
|
MOZ_ASSERT(!cx->isExceptionPending());
|
|
return false;
|
|
|
|
default:
|
|
MOZ_CRASH("bad final onLeaveFrame resume mode");
|
|
}
|
|
}
|
|
|
|
/* static */ bool Debugger::slowPathOnNewGenerator(
|
|
JSContext* cx, AbstractFramePtr frame, Handle<GeneratorObject*> genObj) {
|
|
// This is called from JSOP_GENERATOR, after default parameter expressions
|
|
// are evaluated and well after onEnterFrame, so Debugger.Frame objects for
|
|
// `frame` may already have been exposed to debugger code. The
|
|
// GeneratorObject for this generator call, though, has just been
|
|
// created. It must be associated with any existing Debugger.Frames.
|
|
bool ok = true;
|
|
forEachDebuggerFrame(frame, [&](DebuggerFrame* frameObjPtr) {
|
|
if (!ok) {
|
|
return;
|
|
}
|
|
|
|
RootedDebuggerFrame frameObj(cx, frameObjPtr);
|
|
Debugger* dbg = Debugger::fromChildJSObject(frameObj);
|
|
if (!dbg->addGeneratorFrame(cx, genObj, frameObj)) {
|
|
ReportOutOfMemory(cx);
|
|
|
|
// This leaves `genObj` and `frameObj` unassociated. It's OK
|
|
// because we won't pause again with this generator on the stack:
|
|
// the caller will immediately discard `genObj` and unwind `frame`.
|
|
ok = false;
|
|
}
|
|
});
|
|
return ok;
|
|
}
|
|
|
|
/* static */ ResumeMode Debugger::slowPathOnDebuggerStatement(
|
|
JSContext* cx, AbstractFramePtr frame) {
|
|
RootedValue rval(cx);
|
|
ResumeMode resumeMode = dispatchHook(
|
|
cx,
|
|
[](Debugger* dbg) -> bool { return dbg->getHook(OnDebuggerStatement); },
|
|
[&](Debugger* dbg) -> ResumeMode {
|
|
return dbg->fireDebuggerStatement(cx, &rval);
|
|
});
|
|
|
|
switch (resumeMode) {
|
|
case ResumeMode::Continue:
|
|
case ResumeMode::Terminate:
|
|
break;
|
|
|
|
case ResumeMode::Return:
|
|
frame.setReturnValue(rval);
|
|
break;
|
|
|
|
case ResumeMode::Throw:
|
|
cx->setPendingException(rval);
|
|
break;
|
|
|
|
default:
|
|
MOZ_CRASH("Invalid onDebuggerStatement resume mode");
|
|
}
|
|
|
|
return resumeMode;
|
|
}
|
|
|
|
/* static */ ResumeMode 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 ResumeMode::Continue;
|
|
}
|
|
|
|
// The Debugger API mustn't muck with frames from self-hosted scripts.
|
|
if (frame.hasScript() && frame.script()->selfHosted()) {
|
|
return ResumeMode::Continue;
|
|
}
|
|
|
|
RootedValue rval(cx);
|
|
ResumeMode resumeMode = dispatchHook(
|
|
cx, [](Debugger* dbg) -> bool { return dbg->getHook(OnExceptionUnwind); },
|
|
[&](Debugger* dbg) -> ResumeMode {
|
|
return dbg->fireExceptionUnwind(cx, &rval);
|
|
});
|
|
|
|
switch (resumeMode) {
|
|
case ResumeMode::Continue:
|
|
break;
|
|
|
|
case ResumeMode::Throw:
|
|
cx->setPendingException(rval);
|
|
break;
|
|
|
|
case ResumeMode::Terminate:
|
|
cx->clearPendingException();
|
|
break;
|
|
|
|
case ResumeMode::Return:
|
|
cx->clearPendingException();
|
|
frame.setReturnValue(rval);
|
|
break;
|
|
|
|
default:
|
|
MOZ_CRASH("Invalid onExceptionUnwind resume mode");
|
|
}
|
|
|
|
return resumeMode;
|
|
}
|
|
|
|
// TODO: Remove Remove this function when all properties/methods returning a
|
|
/// DebuggerEnvironment have been given a C++ interface (bug 1271649).
|
|
bool Debugger::wrapEnvironment(JSContext* cx, Handle<Env*> env,
|
|
MutableHandleValue rval) {
|
|
if (!env) {
|
|
rval.setNull();
|
|
return true;
|
|
}
|
|
|
|
RootedDebuggerEnvironment envobj(cx);
|
|
|
|
if (!wrapEnvironment(cx, env, &envobj)) {
|
|
return false;
|
|
}
|
|
|
|
rval.setObject(*envobj);
|
|
return true;
|
|
}
|
|
|
|
bool Debugger::wrapEnvironment(JSContext* cx, Handle<Env*> env,
|
|
MutableHandleDebuggerEnvironment result) {
|
|
MOZ_ASSERT(env);
|
|
|
|
// DebuggerEnv should only wrap a debug scope chain obtained (transitively)
|
|
// from GetDebugEnvironmentFor(Frame|Function).
|
|
MOZ_ASSERT(!IsSyntacticEnvironment(env));
|
|
|
|
DependentAddPtr<ObjectWeakMap> p(cx, environments, env);
|
|
if (p) {
|
|
result.set(&p->value()->as<DebuggerEnvironment>());
|
|
} else {
|
|
// Create a new Debugger.Environment for env.
|
|
RootedObject proto(
|
|
cx, &object->getReservedSlot(JSSLOT_DEBUG_ENV_PROTO).toObject());
|
|
RootedNativeObject debugger(cx, object);
|
|
|
|
RootedDebuggerEnvironment envobj(
|
|
cx, DebuggerEnvironment::create(cx, proto, env, debugger));
|
|
if (!envobj) {
|
|
return false;
|
|
}
|
|
|
|
if (!p.add(cx, environments, env, envobj)) {
|
|
NukeDebuggerWrapper(envobj);
|
|
return false;
|
|
}
|
|
|
|
CrossCompartmentKey key(object, env,
|
|
CrossCompartmentKey::DebuggerEnvironment);
|
|
if (!object->compartment()->putWrapper(cx, key, ObjectValue(*envobj))) {
|
|
NukeDebuggerWrapper(envobj);
|
|
environments.remove(env);
|
|
return false;
|
|
}
|
|
|
|
result.set(envobj);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Debugger::wrapDebuggeeValue(JSContext* cx, MutableHandleValue vp) {
|
|
cx->check(object.get());
|
|
|
|
if (vp.isObject()) {
|
|
RootedObject obj(cx, &vp.toObject());
|
|
RootedDebuggerObject dobj(cx);
|
|
|
|
if (!wrapDebuggeeObject(cx, obj, &dobj)) {
|
|
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 (!DefineDataProperty(cx, optObj, name, trueVal)) {
|
|
return false;
|
|
}
|
|
|
|
vp.setObject(*optObj);
|
|
} else if (!cx->compartment()->wrap(cx, vp)) {
|
|
vp.setUndefined();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Debugger::wrapDebuggeeObject(JSContext* cx, HandleObject obj,
|
|
MutableHandleDebuggerObject result) {
|
|
MOZ_ASSERT(obj);
|
|
|
|
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) {
|
|
result.set(&p->value()->as<DebuggerObject>());
|
|
} else {
|
|
// Create a new Debugger.Object for obj.
|
|
RootedNativeObject debugger(cx, object);
|
|
RootedObject proto(
|
|
cx, &object->getReservedSlot(JSSLOT_DEBUG_OBJECT_PROTO).toObject());
|
|
RootedDebuggerObject dobj(cx,
|
|
DebuggerObject::create(cx, proto, obj, debugger));
|
|
if (!dobj) {
|
|
return false;
|
|
}
|
|
|
|
if (!p.add(cx, objects, obj, dobj)) {
|
|
NukeDebuggerWrapper(dobj);
|
|
return false;
|
|
}
|
|
|
|
if (obj->compartment() != object->compartment()) {
|
|
CrossCompartmentKey key(object, obj, CrossCompartmentKey::DebuggerObject);
|
|
if (!object->compartment()->putWrapper(cx, key, ObjectValue(*dobj))) {
|
|
NukeDebuggerWrapper(dobj);
|
|
objects.remove(obj);
|
|
ReportOutOfMemory(cx);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
result.set(dobj);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static NativeObject* ToNativeDebuggerObject(JSContext* cx,
|
|
MutableHandleObject obj) {
|
|
if (obj->getClass() != &DebuggerObject::class_) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_NOT_EXPECTED_TYPE, "Debugger",
|
|
"Debugger.Object", obj->getClass()->name);
|
|
return nullptr;
|
|
}
|
|
|
|
NativeObject* ndobj = &obj->as<NativeObject>();
|
|
|
|
Value owner = ndobj->getReservedSlot(JSSLOT_DEBUGOBJECT_OWNER);
|
|
if (owner.isUndefined()) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_PROTO,
|
|
"Debugger.Object", "Debugger.Object");
|
|
return nullptr;
|
|
}
|
|
|
|
return ndobj;
|
|
}
|
|
|
|
bool Debugger::unwrapDebuggeeObject(JSContext* cx, MutableHandleObject obj) {
|
|
NativeObject* ndobj = ToNativeDebuggerObject(cx, obj);
|
|
if (!ndobj) {
|
|
return false;
|
|
}
|
|
|
|
Value owner = ndobj->getReservedSlot(JSSLOT_DEBUGOBJECT_OWNER);
|
|
if (&owner.toObject() != object) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_WRONG_OWNER, "Debugger.Object");
|
|
return false;
|
|
}
|
|
|
|
obj.set(static_cast<JSObject*>(ndobj->getPrivate()));
|
|
return true;
|
|
}
|
|
|
|
bool Debugger::unwrapDebuggeeValue(JSContext* cx, MutableHandleValue vp) {
|
|
cx->check(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_ReportErrorNumberASCII(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;
|
|
}
|
|
|
|
/*** Debuggee resumption values and debugger error handling *****************/
|
|
|
|
static bool GetResumptionProperty(JSContext* cx, HandleObject obj,
|
|
HandlePropertyName name, ResumeMode namedMode,
|
|
ResumeMode& resumeMode, MutableHandleValue vp,
|
|
int* hits) {
|
|
bool found;
|
|
if (!HasProperty(cx, obj, name, &found)) {
|
|
return false;
|
|
}
|
|
if (found) {
|
|
++*hits;
|
|
resumeMode = namedMode;
|
|
if (!GetProperty(cx, obj, obj, name, vp)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool ParseResumptionValue(JSContext* cx, HandleValue rval,
|
|
ResumeMode& resumeMode,
|
|
MutableHandleValue vp) {
|
|
if (rval.isUndefined()) {
|
|
resumeMode = ResumeMode::Continue;
|
|
vp.setUndefined();
|
|
return true;
|
|
}
|
|
if (rval.isNull()) {
|
|
resumeMode = ResumeMode::Terminate;
|
|
vp.setUndefined();
|
|
return true;
|
|
}
|
|
|
|
int hits = 0;
|
|
if (rval.isObject()) {
|
|
RootedObject obj(cx, &rval.toObject());
|
|
if (!GetResumptionProperty(cx, obj, cx->names().return_, ResumeMode::Return,
|
|
resumeMode, vp, &hits)) {
|
|
return false;
|
|
}
|
|
if (!GetResumptionProperty(cx, obj, cx->names().throw_, ResumeMode::Throw,
|
|
resumeMode, vp, &hits)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (hits != 1) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_BAD_RESUMPTION);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool GetThisValueForCheck(JSContext* cx, AbstractFramePtr frame,
|
|
jsbytecode* pc, MutableHandleValue thisv,
|
|
Maybe<HandleValue>& maybeThisv) {
|
|
if (frame.debuggerNeedsCheckPrimitiveReturn()) {
|
|
{
|
|
AutoRealm ar(cx, frame.environmentChain());
|
|
if (!GetThisValueForDebuggerMaybeOptimizedOut(cx, frame, pc, thisv)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!cx->compartment()->wrap(cx, thisv)) {
|
|
return false;
|
|
}
|
|
|
|
MOZ_ASSERT_IF(thisv.isMagic(), thisv.isMagic(JS_UNINITIALIZED_LEXICAL));
|
|
maybeThisv.emplace(HandleValue(thisv));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool CheckResumptionValue(JSContext* cx, AbstractFramePtr frame,
|
|
const Maybe<HandleValue>& maybeThisv,
|
|
ResumeMode resumeMode, MutableHandleValue vp) {
|
|
if (maybeThisv.isSome()) {
|
|
const HandleValue& thisv = maybeThisv.ref();
|
|
if (resumeMode == ResumeMode::Return && vp.isPrimitive()) {
|
|
if (vp.isUndefined()) {
|
|
if (thisv.isMagic(JS_UNINITIALIZED_LEXICAL)) {
|
|
return ThrowUninitializedThis(cx, frame);
|
|
}
|
|
|
|
vp.set(thisv);
|
|
} else {
|
|
ReportValueError(cx, JSMSG_BAD_DERIVED_RETURN, JSDVG_IGNORE_STACK, vp,
|
|
nullptr);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void AdjustGeneratorResumptionValue(JSContext* cx,
|
|
AbstractFramePtr frame,
|
|
ResumeMode& resumeMode,
|
|
MutableHandleValue vp) {
|
|
if (resumeMode == ResumeMode::Return && frame && frame.isFunctionFrame() &&
|
|
frame.callee()->isGenerator()) {
|
|
// Treat `{return: <value>}` like a `return` statement. For generators,
|
|
// that means doing the work below. It's only what the debuggee would
|
|
// do for an ordinary `return` statement--using a few bytecode
|
|
// instructions--and it's simpler to do the work manually than to count
|
|
// on that bytecode sequence existing in the debuggee, somehow jump to
|
|
// it, and then avoid re-entering the debugger from it.
|
|
Rooted<GeneratorObject*> genObj(cx, GetGeneratorObjectForFrame(cx, frame));
|
|
if (genObj) {
|
|
// 1. `return <value>` creates and returns a new object,
|
|
// `{value: <value>, done: true}`.
|
|
if (!genObj->isBeforeInitialYield()) {
|
|
JSObject* pair = CreateIterResultObject(cx, vp, true);
|
|
if (!pair) {
|
|
// Out of memory in debuggee code. Arrange for this to propagate.
|
|
MOZ_ALWAYS_TRUE(cx->getPendingException(vp));
|
|
cx->clearPendingException();
|
|
resumeMode = ResumeMode::Throw;
|
|
return;
|
|
}
|
|
vp.setObject(*pair);
|
|
}
|
|
|
|
// 2. The generator must be closed.
|
|
genObj->setClosed();
|
|
} else {
|
|
// We're before the initial yield. Carry on with the forced return.
|
|
// The debuggee will see a call to a generator returning the
|
|
// non-generator value *vp.
|
|
}
|
|
}
|
|
}
|
|
|
|
ResumeMode Debugger::reportUncaughtException(Maybe<AutoRealm>& ar) {
|
|
JSContext* cx = ar->context();
|
|
|
|
// Uncaught exceptions arise from Debugger code, and so we must already be
|
|
// in an NX section.
|
|
MOZ_ASSERT(EnterDebuggeeNoExecute::isLockedInStack(cx, *this));
|
|
|
|
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 ReportErrorToGlobal will assert that
|
|
// we don't have one.
|
|
cx->clearPendingException();
|
|
ReportErrorToGlobal(cx, cx->global(), exn);
|
|
}
|
|
|
|
// And if not, or if PrepareScriptEnvironmentAndInvoke somehow left an
|
|
// exception on cx (which it totally shouldn't do), just give up.
|
|
cx->clearPendingException();
|
|
}
|
|
|
|
ar.reset();
|
|
return ResumeMode::Terminate;
|
|
}
|
|
|
|
ResumeMode Debugger::handleUncaughtExceptionHelper(
|
|
Maybe<AutoRealm>& ar, MutableHandleValue* vp,
|
|
const Maybe<HandleValue>& thisVForCheck, AbstractFramePtr frame) {
|
|
JSContext* cx = ar->context();
|
|
|
|
// Uncaught exceptions arise from Debugger code, and so we must already be
|
|
// in an NX section.
|
|
MOZ_ASSERT(EnterDebuggeeNoExecute::isLockedInStack(cx, *this));
|
|
|
|
if (cx->isExceptionPending()) {
|
|
if (uncaughtExceptionHook) {
|
|
RootedValue exc(cx);
|
|
if (!cx->getPendingException(&exc)) {
|
|
return ResumeMode::Terminate;
|
|
}
|
|
cx->clearPendingException();
|
|
|
|
RootedValue fval(cx, ObjectValue(*uncaughtExceptionHook));
|
|
RootedValue rv(cx);
|
|
if (js::Call(cx, fval, object, exc, &rv)) {
|
|
if (vp) {
|
|
ResumeMode resumeMode = ResumeMode::Continue;
|
|
if (!ParseResumptionValue(cx, rv, resumeMode, *vp)) {
|
|
return reportUncaughtException(ar);
|
|
}
|
|
return leaveDebugger(ar, frame, thisVForCheck,
|
|
CallUncaughtExceptionHook::No, resumeMode, *vp);
|
|
} else {
|
|
// Caller is something like onGarbageCollectionHook that
|
|
// doesn't allow Debugger to control debuggee resumption.
|
|
// The return value from uncaughtExceptionHook is ignored.
|
|
return ResumeMode::Continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
return reportUncaughtException(ar);
|
|
}
|
|
|
|
ar.reset();
|
|
return ResumeMode::Terminate;
|
|
}
|
|
|
|
ResumeMode Debugger::handleUncaughtException(
|
|
Maybe<AutoRealm>& ar, MutableHandleValue vp,
|
|
const Maybe<HandleValue>& thisVForCheck, AbstractFramePtr frame) {
|
|
return handleUncaughtExceptionHelper(ar, &vp, thisVForCheck, frame);
|
|
}
|
|
|
|
ResumeMode Debugger::handleUncaughtException(Maybe<AutoRealm>& ar) {
|
|
return handleUncaughtExceptionHelper(ar, nullptr, mozilla::Nothing(),
|
|
NullFramePtr());
|
|
}
|
|
|
|
ResumeMode Debugger::leaveDebugger(Maybe<AutoRealm>& ar, AbstractFramePtr frame,
|
|
const Maybe<HandleValue>& maybeThisv,
|
|
CallUncaughtExceptionHook callHook,
|
|
ResumeMode resumeMode,
|
|
MutableHandleValue vp) {
|
|
JSContext* cx = ar->context();
|
|
if (!unwrapDebuggeeValue(cx, vp) ||
|
|
!CheckResumptionValue(cx, frame, maybeThisv, resumeMode, vp)) {
|
|
if (callHook == CallUncaughtExceptionHook::Yes) {
|
|
return handleUncaughtException(ar, vp, maybeThisv, frame);
|
|
}
|
|
return reportUncaughtException(ar);
|
|
}
|
|
|
|
ar.reset();
|
|
if (!cx->compartment()->wrap(cx, vp)) {
|
|
resumeMode = ResumeMode::Terminate;
|
|
vp.setUndefined();
|
|
}
|
|
AdjustGeneratorResumptionValue(cx, frame, resumeMode, vp);
|
|
|
|
return resumeMode;
|
|
}
|
|
|
|
ResumeMode Debugger::processParsedHandlerResult(Maybe<AutoRealm>& ar,
|
|
AbstractFramePtr frame,
|
|
jsbytecode* pc, bool success,
|
|
ResumeMode resumeMode,
|
|
MutableHandleValue vp) {
|
|
JSContext* cx = ar->context();
|
|
|
|
RootedValue thisv(cx);
|
|
Maybe<HandleValue> maybeThisv;
|
|
if (!GetThisValueForCheck(cx, frame, pc, &thisv, maybeThisv)) {
|
|
ar.reset();
|
|
return ResumeMode::Terminate;
|
|
}
|
|
|
|
if (!success) {
|
|
return handleUncaughtException(ar, vp, maybeThisv, frame);
|
|
}
|
|
|
|
return leaveDebugger(ar, frame, maybeThisv, CallUncaughtExceptionHook::Yes,
|
|
resumeMode, vp);
|
|
}
|
|
|
|
ResumeMode Debugger::processHandlerResult(Maybe<AutoRealm>& ar, bool success,
|
|
const Value& rv,
|
|
AbstractFramePtr frame,
|
|
jsbytecode* pc,
|
|
MutableHandleValue vp) {
|
|
JSContext* cx = ar->context();
|
|
|
|
RootedValue thisv(cx);
|
|
Maybe<HandleValue> maybeThisv;
|
|
if (!GetThisValueForCheck(cx, frame, pc, &thisv, maybeThisv)) {
|
|
ar.reset();
|
|
return ResumeMode::Terminate;
|
|
}
|
|
|
|
if (!success) {
|
|
return handleUncaughtException(ar, vp, maybeThisv, frame);
|
|
}
|
|
|
|
RootedValue rootRv(cx, rv);
|
|
ResumeMode resumeMode = ResumeMode::Continue;
|
|
if (!ParseResumptionValue(cx, rootRv, resumeMode, vp)) {
|
|
return handleUncaughtException(ar, vp, maybeThisv, frame);
|
|
}
|
|
return leaveDebugger(ar, frame, maybeThisv, CallUncaughtExceptionHook::Yes,
|
|
resumeMode, vp);
|
|
}
|
|
|
|
/*** Debuggee completion values *********************************************/
|
|
|
|
/* static */ void Debugger::resultToCompletion(JSContext* cx, bool ok,
|
|
const Value& rv,
|
|
ResumeMode* resumeMode,
|
|
MutableHandleValue value) {
|
|
MOZ_ASSERT_IF(ok, !cx->isExceptionPending());
|
|
|
|
if (ok) {
|
|
*resumeMode = ResumeMode::Return;
|
|
value.set(rv);
|
|
} else if (cx->isExceptionPending()) {
|
|
*resumeMode = ResumeMode::Throw;
|
|
if (!cx->getPendingException(value)) {
|
|
*resumeMode = ResumeMode::Terminate;
|
|
}
|
|
cx->clearPendingException();
|
|
} else {
|
|
*resumeMode = ResumeMode::Terminate;
|
|
value.setUndefined();
|
|
}
|
|
}
|
|
|
|
bool Debugger::newCompletionValue(JSContext* cx, ResumeMode resumeMode,
|
|
const Value& value_,
|
|
MutableHandleValue result) {
|
|
// We must be in the debugger's compartment, since that's where we want
|
|
// to construct the completion value.
|
|
cx->check(object.get());
|
|
cx->check(value_);
|
|
|
|
RootedId key(cx);
|
|
RootedValue value(cx, value_);
|
|
|
|
switch (resumeMode) {
|
|
case ResumeMode::Return:
|
|
key = NameToId(cx->names().return_);
|
|
break;
|
|
|
|
case ResumeMode::Throw:
|
|
key = NameToId(cx->names().throw_);
|
|
break;
|
|
|
|
case ResumeMode::Terminate:
|
|
result.setNull();
|
|
return true;
|
|
|
|
default:
|
|
MOZ_CRASH("bad resume mode passed to Debugger::newCompletionValue");
|
|
}
|
|
|
|
// Common tail for ResumeMode::Return and ResumeMode::Throw.
|
|
RootedPlainObject obj(cx, NewBuiltinClassInstance<PlainObject>(cx));
|
|
if (!obj ||
|
|
!NativeDefineDataProperty(cx, obj, key, value, JSPROP_ENUMERATE)) {
|
|
return false;
|
|
}
|
|
|
|
result.setObject(*obj);
|
|
return true;
|
|
}
|
|
|
|
bool Debugger::receiveCompletionValue(Maybe<AutoRealm>& ar, bool ok,
|
|
HandleValue val, MutableHandleValue vp) {
|
|
JSContext* cx = ar->context();
|
|
|
|
ResumeMode resumeMode;
|
|
RootedValue value(cx);
|
|
resultToCompletion(cx, ok, val, &resumeMode, &value);
|
|
ar.reset();
|
|
return wrapDebuggeeValue(cx, &value) &&
|
|
newCompletionValue(cx, resumeMode, value, vp);
|
|
}
|
|
|
|
/*** Firing debugger hooks **************************************************/
|
|
|
|
static bool CallMethodIfPresent(JSContext* cx, HandleObject obj,
|
|
const char* name, size_t 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);
|
|
if (!GetProperty(cx, obj, obj, id, &fval)) {
|
|
return false;
|
|
}
|
|
|
|
if (!IsCallable(fval)) {
|
|
return true;
|
|
}
|
|
|
|
InvokeArgs args(cx);
|
|
if (!args.init(cx, argc)) {
|
|
return false;
|
|
}
|
|
|
|
for (size_t i = 0; i < argc; i++) {
|
|
args[i].set(argv[i]);
|
|
}
|
|
|
|
rval.setObject(*obj); // overwritten by successful Call
|
|
return js::Call(cx, fval, rval, args, rval);
|
|
}
|
|
|
|
ResumeMode Debugger::fireDebuggerStatement(JSContext* cx,
|
|
MutableHandleValue vp) {
|
|
RootedObject hook(cx, getHook(OnDebuggerStatement));
|
|
MOZ_ASSERT(hook);
|
|
MOZ_ASSERT(hook->isCallable());
|
|
|
|
Maybe<AutoRealm> ar;
|
|
ar.emplace(cx, object);
|
|
|
|
ScriptFrameIter iter(cx);
|
|
RootedValue scriptFrame(cx);
|
|
if (!getFrame(cx, iter, &scriptFrame)) {
|
|
return reportUncaughtException(ar);
|
|
}
|
|
|
|
RootedValue fval(cx, ObjectValue(*hook));
|
|
RootedValue rv(cx);
|
|
bool ok = js::Call(cx, fval, object, scriptFrame, &rv);
|
|
return processHandlerResult(ar, ok, rv, iter.abstractFramePtr(), iter.pc(),
|
|
vp);
|
|
}
|
|
|
|
ResumeMode 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 ResumeMode::Terminate;
|
|
}
|
|
cx->clearPendingException();
|
|
|
|
Maybe<AutoRealm> ar;
|
|
ar.emplace(cx, object);
|
|
|
|
RootedValue scriptFrame(cx);
|
|
RootedValue wrappedExc(cx, exc);
|
|
|
|
FrameIter iter(cx);
|
|
if (!getFrame(cx, iter, &scriptFrame) ||
|
|
!wrapDebuggeeValue(cx, &wrappedExc)) {
|
|
return reportUncaughtException(ar);
|
|
}
|
|
|
|
RootedValue fval(cx, ObjectValue(*hook));
|
|
RootedValue rv(cx);
|
|
bool ok = js::Call(cx, fval, object, scriptFrame, wrappedExc, &rv);
|
|
ResumeMode resumeMode =
|
|
processHandlerResult(ar, ok, rv, iter.abstractFramePtr(), iter.pc(), vp);
|
|
if (resumeMode == ResumeMode::Continue) {
|
|
cx->setPendingException(exc);
|
|
}
|
|
return resumeMode;
|
|
}
|
|
|
|
ResumeMode Debugger::fireEnterFrame(JSContext* cx, MutableHandleValue vp) {
|
|
RootedObject hook(cx, getHook(OnEnterFrame));
|
|
MOZ_ASSERT(hook);
|
|
MOZ_ASSERT(hook->isCallable());
|
|
|
|
RootedValue scriptFrame(cx);
|
|
|
|
FrameIter iter(cx);
|
|
|
|
#if DEBUG
|
|
// Assert that the hook won't be able to re-enter the generator.
|
|
if (iter.hasScript() && *iter.pc() == JSOP_DEBUGAFTERYIELD) {
|
|
GeneratorObject* genObj =
|
|
GetGeneratorObjectForFrame(cx, iter.abstractFramePtr());
|
|
MOZ_ASSERT(genObj->isRunning() || genObj->isClosing());
|
|
}
|
|
#endif
|
|
|
|
Maybe<AutoRealm> ar;
|
|
ar.emplace(cx, object);
|
|
|
|
if (!getFrame(cx, iter, &scriptFrame)) {
|
|
return reportUncaughtException(ar);
|
|
}
|
|
|
|
RootedValue fval(cx, ObjectValue(*hook));
|
|
RootedValue rv(cx);
|
|
bool ok = js::Call(cx, fval, object, scriptFrame, &rv);
|
|
|
|
return processHandlerResult(ar, ok, rv, iter.abstractFramePtr(), iter.pc(),
|
|
vp);
|
|
}
|
|
|
|
void Debugger::fireNewScript(JSContext* cx,
|
|
Handle<DebuggerScriptReferent> scriptReferent) {
|
|
RootedObject hook(cx, getHook(OnNewScript));
|
|
MOZ_ASSERT(hook);
|
|
MOZ_ASSERT(hook->isCallable());
|
|
|
|
Maybe<AutoRealm> ar;
|
|
ar.emplace(cx, object);
|
|
|
|
JSObject* dsobj = wrapVariantReferent(cx, scriptReferent);
|
|
if (!dsobj) {
|
|
reportUncaughtException(ar);
|
|
return;
|
|
}
|
|
|
|
RootedValue fval(cx, ObjectValue(*hook));
|
|
RootedValue dsval(cx, ObjectValue(*dsobj));
|
|
RootedValue rv(cx);
|
|
if (!js::Call(cx, fval, object, dsval, &rv)) {
|
|
handleUncaughtException(ar);
|
|
}
|
|
}
|
|
|
|
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<AutoRealm> ar;
|
|
ar.emplace(cx, object);
|
|
|
|
JSObject* dataObj = gcData->toJSObject(cx);
|
|
if (!dataObj) {
|
|
reportUncaughtException(ar);
|
|
return;
|
|
}
|
|
|
|
RootedValue fval(cx, ObjectValue(*hook));
|
|
RootedValue dataVal(cx, ObjectValue(*dataObj));
|
|
RootedValue rv(cx);
|
|
if (!js::Call(cx, fval, object, dataVal, &rv)) {
|
|
handleUncaughtException(ar);
|
|
}
|
|
}
|
|
|
|
template <typename HookIsEnabledFun /* bool (Debugger*) */,
|
|
typename FireHookFun /* ResumeMode (Debugger*) */>
|
|
/* static */ ResumeMode 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 (auto p = debuggers->begin(); p != debuggers->end(); p++) {
|
|
Debugger* dbg = *p;
|
|
if (dbg->enabled && hookIsEnabled(dbg)) {
|
|
if (!triggered.append(ObjectValue(*dbg->toJSObject()))) {
|
|
return ResumeMode::Terminate;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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());
|
|
EnterDebuggeeNoExecute nx(cx, *dbg);
|
|
if (dbg->debuggees.has(global) && dbg->enabled && hookIsEnabled(dbg)) {
|
|
ResumeMode resumeMode = fireHook(dbg);
|
|
if (resumeMode != ResumeMode::Continue) {
|
|
return resumeMode;
|
|
}
|
|
}
|
|
}
|
|
return ResumeMode::Continue;
|
|
}
|
|
|
|
void Debugger::slowPathOnNewScript(JSContext* cx, HandleScript script) {
|
|
ResumeMode resumeMode = dispatchHook(
|
|
cx,
|
|
[script](Debugger* dbg) -> bool {
|
|
return dbg->observesNewScript() && dbg->observesScript(script);
|
|
},
|
|
[&](Debugger* dbg) -> ResumeMode {
|
|
Rooted<DebuggerScriptReferent> scriptReferent(cx, script.get());
|
|
dbg->fireNewScript(cx, scriptReferent);
|
|
return ResumeMode::Continue;
|
|
});
|
|
|
|
// dispatchHook may fail due to OOM. This OOM is not handlable at the
|
|
// callsites of onNewScript in the engine.
|
|
if (resumeMode == ResumeMode::Terminate) {
|
|
cx->clearPendingException();
|
|
return;
|
|
}
|
|
|
|
MOZ_ASSERT(resumeMode == ResumeMode::Continue);
|
|
}
|
|
|
|
void Debugger::slowPathOnNewWasmInstance(
|
|
JSContext* cx, Handle<WasmInstanceObject*> wasmInstance) {
|
|
ResumeMode resumeMode = dispatchHook(
|
|
cx,
|
|
[wasmInstance](Debugger* dbg) -> bool {
|
|
return dbg->observesNewScript() &&
|
|
dbg->observesGlobal(&wasmInstance->global());
|
|
},
|
|
[&](Debugger* dbg) -> ResumeMode {
|
|
Rooted<DebuggerScriptReferent> scriptReferent(cx, wasmInstance.get());
|
|
dbg->fireNewScript(cx, scriptReferent);
|
|
return ResumeMode::Continue;
|
|
});
|
|
|
|
// dispatchHook may fail due to OOM. This OOM is not handlable at the
|
|
// callsites of onNewWasmInstance in the engine.
|
|
if (resumeMode == ResumeMode::Terminate) {
|
|
cx->clearPendingException();
|
|
return;
|
|
}
|
|
|
|
MOZ_ASSERT(resumeMode == ResumeMode::Continue);
|
|
}
|
|
|
|
/* static */ ResumeMode Debugger::onTrap(JSContext* cx, MutableHandleValue vp) {
|
|
FrameIter iter(cx);
|
|
JS::AutoSaveExceptionState savedExc(cx);
|
|
Rooted<GlobalObject*> global(cx);
|
|
BreakpointSite* site;
|
|
bool isJS; // true when iter.hasScript(), false when iter.isWasm()
|
|
jsbytecode* pc; // valid when isJS == true
|
|
uint32_t bytecodeOffset; // valid when isJS == false
|
|
if (iter.hasScript()) {
|
|
RootedScript script(cx, iter.script());
|
|
MOZ_ASSERT(script->isDebuggee());
|
|
global.set(&script->global());
|
|
isJS = true;
|
|
pc = iter.pc();
|
|
bytecodeOffset = 0;
|
|
site = script->getBreakpointSite(pc);
|
|
} else {
|
|
MOZ_ASSERT(iter.isWasm());
|
|
global.set(&iter.wasmInstance()->object()->global());
|
|
isJS = false;
|
|
pc = nullptr;
|
|
bytecodeOffset = iter.wasmBytecodeOffset();
|
|
site = iter.wasmInstance()->debug().getOrCreateBreakpointSite(
|
|
cx, bytecodeOffset);
|
|
}
|
|
|
|
// Build list of breakpoint handlers.
|
|
Vector<Breakpoint*> triggered(cx);
|
|
for (Breakpoint* bp = site->firstBreakpoint(); bp; bp = bp->nextInSite()) {
|
|
// Skip a breakpoint that is not set for the current wasm::Instance --
|
|
// single wasm::Code can handle breakpoints for mutiple instances.
|
|
if (!isJS &&
|
|
&bp->asWasm()->wasmInstance->instance() != iter.wasmInstance()) {
|
|
continue;
|
|
}
|
|
if (!triggered.append(bp)) {
|
|
return ResumeMode::Terminate;
|
|
}
|
|
}
|
|
|
|
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 global.
|
|
//
|
|
// 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(global);
|
|
if (hasDebuggee) {
|
|
Maybe<AutoRealm> ar;
|
|
ar.emplace(cx, dbg->object);
|
|
EnterDebuggeeNoExecute nx(cx, *dbg);
|
|
|
|
RootedValue scriptFrame(cx);
|
|
if (!dbg->getFrame(cx, iter, &scriptFrame)) {
|
|
return dbg->reportUncaughtException(ar);
|
|
}
|
|
RootedValue rv(cx);
|
|
Rooted<JSObject*> handler(cx, bp->handler);
|
|
bool ok = CallMethodIfPresent(cx, handler, "hit", 1,
|
|
scriptFrame.address(), &rv);
|
|
ResumeMode resumeMode = dbg->processHandlerResult(
|
|
ar, ok, rv, iter.abstractFramePtr(), iter.pc(), vp);
|
|
if (resumeMode != ResumeMode::Continue) {
|
|
savedExc.drop();
|
|
return resumeMode;
|
|
}
|
|
|
|
// Calling JS code invalidates site. Reload it.
|
|
if (isJS) {
|
|
site = iter.script()->getBreakpointSite(pc);
|
|
} else {
|
|
site = iter.wasmInstance()->debug().getOrCreateBreakpointSite(
|
|
cx, bytecodeOffset);
|
|
}
|
|
}
|
|
}
|
|
|
|
// By convention, return the true op to the interpreter in vp, and return
|
|
// undefined in vp to the wasm debug trap.
|
|
if (isJS) {
|
|
vp.setInt32(JSOp(*pc));
|
|
} else {
|
|
vp.set(UndefinedValue());
|
|
}
|
|
return ResumeMode::Continue;
|
|
}
|
|
|
|
/* static */ ResumeMode Debugger::onSingleStep(JSContext* cx,
|
|
MutableHandleValue vp) {
|
|
FrameIter 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).
|
|
JS::AutoSaveExceptionState savedExc(cx);
|
|
|
|
// Build list of Debugger.Frame instances referring to this frame with
|
|
// onStep handlers.
|
|
Rooted<DebuggerFrameVector> frames(cx, DebuggerFrameVector(cx));
|
|
if (!getDebuggerFrames(iter.abstractFramePtr(), &frames)) {
|
|
return ResumeMode::Terminate;
|
|
}
|
|
|
|
#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.
|
|
if (iter.hasScript()) {
|
|
uint32_t stepperCount = 0;
|
|
JSScript* trappingScript = iter.script();
|
|
GlobalObject* global = cx->global();
|
|
if (GlobalObject::DebuggerVector* debuggers = global->getDebuggers()) {
|
|
for (auto 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.isWasmDebugFrame()) {
|
|
continue;
|
|
}
|
|
if (frame.script() == trappingScript &&
|
|
!frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER)
|
|
.isUndefined()) {
|
|
stepperCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
MOZ_ASSERT(stepperCount == trappingScript->stepModeCount());
|
|
}
|
|
#endif
|
|
|
|
// Call onStep for frames that have the handler set.
|
|
for (size_t i = 0; i < frames.length(); i++) {
|
|
HandleDebuggerFrame frame = frames[i];
|
|
OnStepHandler* handler = frame->onStepHandler();
|
|
if (!handler) {
|
|
continue;
|
|
}
|
|
|
|
Debugger* dbg = Debugger::fromChildJSObject(frame);
|
|
EnterDebuggeeNoExecute nx(cx, *dbg);
|
|
|
|
Maybe<AutoRealm> ar;
|
|
ar.emplace(cx, dbg->object);
|
|
|
|
ResumeMode resumeMode = ResumeMode::Continue;
|
|
bool success = handler->onStep(cx, frame, resumeMode, vp);
|
|
resumeMode = dbg->processParsedHandlerResult(
|
|
ar, iter.abstractFramePtr(), iter.pc(), success, resumeMode, vp);
|
|
if (resumeMode != ResumeMode::Continue) {
|
|
savedExc.drop();
|
|
return resumeMode;
|
|
}
|
|
}
|
|
|
|
vp.setUndefined();
|
|
return ResumeMode::Continue;
|
|
}
|
|
|
|
ResumeMode Debugger::fireNewGlobalObject(JSContext* cx,
|
|
Handle<GlobalObject*> global,
|
|
MutableHandleValue vp) {
|
|
RootedObject hook(cx, getHook(OnNewGlobalObject));
|
|
MOZ_ASSERT(hook);
|
|
MOZ_ASSERT(hook->isCallable());
|
|
|
|
Maybe<AutoRealm> ar;
|
|
ar.emplace(cx, object);
|
|
|
|
RootedValue wrappedGlobal(cx, ObjectValue(*global));
|
|
if (!wrapDebuggeeValue(cx, &wrappedGlobal)) {
|
|
return reportUncaughtException(ar);
|
|
}
|
|
|
|
// 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.
|
|
RootedValue rv(cx);
|
|
RootedValue fval(cx, ObjectValue(*hook));
|
|
bool ok = js::Call(cx, fval, object, wrappedGlobal, &rv);
|
|
if (ok && !rv.isUndefined()) {
|
|
JS_ReportErrorNumberASCII(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.
|
|
ResumeMode resumeMode =
|
|
ok ? ResumeMode::Continue : handleUncaughtException(ar, vp);
|
|
MOZ_ASSERT(!cx->isExceptionPending());
|
|
return resumeMode;
|
|
}
|
|
|
|
void Debugger::slowPathOnNewGlobalObject(JSContext* cx,
|
|
Handle<GlobalObject*> global) {
|
|
MOZ_ASSERT(!cx->runtime()->onNewGlobalObjectWatchers().isEmpty());
|
|
if (global->realm()->creationOptions().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 (auto& dbg : cx->runtime()->onNewGlobalObjectWatchers()) {
|
|
MOZ_ASSERT(dbg.observesNewGlobalObject());
|
|
JSObject* obj = dbg.object;
|
|
JS::ExposeObjectToActiveJS(obj);
|
|
if (!watchers.append(obj)) {
|
|
if (cx->isExceptionPending()) {
|
|
cx->clearPendingException();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
ResumeMode resumeMode = ResumeMode::Continue;
|
|
RootedValue value(cx);
|
|
|
|
for (size_t i = 0; i < watchers.length(); i++) {
|
|
Debugger* dbg = fromJSObject(watchers[i]);
|
|
EnterDebuggeeNoExecute nx(cx, *dbg);
|
|
|
|
// 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 resume mode is returned.
|
|
if (dbg->observesNewGlobalObject()) {
|
|
resumeMode = dbg->fireNewGlobalObject(cx, global, &value);
|
|
if (resumeMode != ResumeMode::Continue &&
|
|
resumeMode != ResumeMode::Return) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
MOZ_ASSERT(!cx->isExceptionPending());
|
|
}
|
|
|
|
/* static */ bool Debugger::slowPathOnLogAllocationSite(
|
|
JSContext* cx, HandleObject obj, HandleSavedFrame frame,
|
|
mozilla::TimeStamp when, GlobalObject::DebuggerVector& dbgs) {
|
|
MOZ_ASSERT(!dbgs.empty());
|
|
mozilla::DebugOnly<ReadBarriered<Debugger*>*> begin = dbgs.begin();
|
|
|
|
// Root all the Debuggers while we're iterating over them;
|
|
// appendAllocationSite calls Compartment::wrap, and thus can GC.
|
|
//
|
|
// SpiderMonkey protocol is generally for the caller to prove that it has
|
|
// rooted the stuff it's asking you to operate on (i.e. by passing a
|
|
// Handle), but in this case, we're iterating over a global's list of
|
|
// Debuggers, and globals only hold their Debuggers weakly.
|
|
Rooted<GCVector<JSObject*>> activeDebuggers(cx, GCVector<JSObject*>(cx));
|
|
for (auto dbgp = dbgs.begin(); dbgp < dbgs.end(); dbgp++) {
|
|
if (!activeDebuggers.append((*dbgp)->object)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (auto 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;
|
|
}
|
|
|
|
bool Debugger::isDebuggeeUnbarriered(const Realm* realm) const {
|
|
MOZ_ASSERT(realm);
|
|
return realm->isDebuggee() &&
|
|
debuggees.has(realm->unsafeUnbarrieredMaybeGlobal());
|
|
}
|
|
|
|
bool Debugger::appendAllocationSite(JSContext* cx, HandleObject obj,
|
|
HandleSavedFrame frame,
|
|
mozilla::TimeStamp when) {
|
|
MOZ_ASSERT(trackingAllocationSites && enabled);
|
|
|
|
AutoRealm ar(cx, object);
|
|
RootedObject wrappedFrame(cx, frame);
|
|
if (!cx->compartment()->wrap(cx, &wrappedFrame)) {
|
|
return false;
|
|
}
|
|
|
|
// Try to get the constructor name from the ObjectGroup's TypeNewScript.
|
|
// This is only relevant for native objects.
|
|
RootedAtom ctorName(cx);
|
|
if (obj->is<NativeObject>()) {
|
|
AutoRealm ar(cx, obj);
|
|
if (!JSObject::constructorDisplayAtom(cx, obj, &ctorName)) {
|
|
return false;
|
|
}
|
|
}
|
|
if (ctorName) {
|
|
cx->markAtom(ctorName);
|
|
}
|
|
|
|
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) {
|
|
allocationsLog.popFront();
|
|
MOZ_ASSERT(allocationsLog.length() == maxAllocationsLogLength);
|
|
allocationsLogOverflowed = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
ResumeMode 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<AutoRealm> ar;
|
|
ar.emplace(cx, object);
|
|
|
|
RootedValue dbgObj(cx, ObjectValue(*promise));
|
|
if (!wrapDebuggeeValue(cx, &dbgObj)) {
|
|
return reportUncaughtException(ar);
|
|
}
|
|
|
|
// Like onNewGlobalObject, the Promise hooks are infallible and the comments
|
|
// in |Debugger::fireNewGlobalObject| apply here as well.
|
|
RootedValue fval(cx, ObjectValue(*hookObj));
|
|
RootedValue rv(cx);
|
|
bool ok = js::Call(cx, fval, object, dbgObj, &rv);
|
|
if (ok && !rv.isUndefined()) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_RESUMPTION_VALUE_DISALLOWED);
|
|
ok = false;
|
|
}
|
|
|
|
ResumeMode resumeMode =
|
|
ok ? ResumeMode::Continue : handleUncaughtException(ar, vp);
|
|
MOZ_ASSERT(!cx->isExceptionPending());
|
|
return resumeMode;
|
|
}
|
|
|
|
/* static */ void Debugger::slowPathPromiseHook(
|
|
JSContext* cx, Hook hook, Handle<PromiseObject*> promise) {
|
|
MOZ_ASSERT(hook == OnNewPromise || hook == OnPromiseSettled);
|
|
|
|
if (hook == OnPromiseSettled) {
|
|
// We should be in the right compartment, but for simplicity always enter
|
|
// the promise's realm below.
|
|
cx->check(promise);
|
|
}
|
|
|
|
AutoRealm ar(cx, promise);
|
|
|
|
RootedValue rval(cx);
|
|
ResumeMode resumeMode = dispatchHook(
|
|
cx, [hook](Debugger* dbg) -> bool { return dbg->getHook(hook); },
|
|
[&](Debugger* dbg) -> ResumeMode {
|
|
(void)dbg->firePromiseHook(cx, hook, promise, &rval);
|
|
return ResumeMode::Continue;
|
|
});
|
|
|
|
if (resumeMode == ResumeMode::Terminate) {
|
|
// 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(resumeMode == ResumeMode::Continue);
|
|
}
|
|
|
|
/*** Debugger code invalidation for observing execution *********************/
|
|
|
|
class MOZ_RAII ExecutionObservableRealms
|
|
: public Debugger::ExecutionObservableSet {
|
|
HashSet<Realm*> realms_;
|
|
HashSet<Zone*> zones_;
|
|
|
|
public:
|
|
explicit ExecutionObservableRealms(
|
|
JSContext* cx MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
|
|
: realms_(cx), zones_(cx) {
|
|
MOZ_GUARD_OBJECT_NOTIFIER_INIT;
|
|
}
|
|
|
|
bool add(Realm* realm) {
|
|
return realms_.put(realm) && zones_.put(realm->zone());
|
|
}
|
|
|
|
using RealmRange = HashSet<Realm*>::Range;
|
|
const HashSet<Realm*>* realms() const { return &realms_; }
|
|
|
|
const HashSet<Zone*>* zones() const override { return &zones_; }
|
|
bool shouldRecompileOrInvalidate(JSScript* script) const override {
|
|
return script->hasBaselineScript() && realms_.has(script->realm());
|
|
}
|
|
bool shouldMarkAsDebuggee(FrameIter& iter) const override {
|
|
// AbstractFramePtr can't refer to non-remateralized Ion frames or
|
|
// non-debuggee wasm frames, so if iter refers to one such, we know we
|
|
// don't match.
|
|
return iter.hasUsableAbstractFramePtr() && realms_.has(iter.realm());
|
|
}
|
|
|
|
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 override {
|
|
// We never inline across realms, let alone across zones, so
|
|
// frames_'s script's zone is the only one of interest.
|
|
return frame_.script()->zone();
|
|
}
|
|
|
|
JSScript* singleScriptForZoneInvalidation() const override {
|
|
MOZ_CRASH(
|
|
"ExecutionObservableFrame shouldn't need zone-wide invalidation.");
|
|
return nullptr;
|
|
}
|
|
|
|
bool shouldRecompileOrInvalidate(JSScript* script) const override {
|
|
// 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 (frame_.hasScript() && script == frame_.script()) {
|
|
return true;
|
|
}
|
|
|
|
return frame_.isRematerializedFrame() &&
|
|
script == frame_.asRematerializedFrame()->outerScript();
|
|
}
|
|
|
|
bool shouldMarkAsDebuggee(FrameIter& iter) const override {
|
|
// AbstractFramePtr can't refer to non-remateralized Ion frames or
|
|
// non-debuggee wasm 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 override { return script_->zone(); }
|
|
JSScript* singleScriptForZoneInvalidation() const override { return script_; }
|
|
bool shouldRecompileOrInvalidate(JSScript* script) const override {
|
|
return script->hasBaselineScript() && script == script_;
|
|
}
|
|
bool shouldMarkAsDebuggee(FrameIter& iter) const override {
|
|
// 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.
|
|
//
|
|
// Also AbstractFramePtr can't refer to non-debuggee wasm frames, so if
|
|
// iter refers to one such, we know we don't match.
|
|
return iter.hasUsableAbstractFramePtr() && !iter.isWasm() &&
|
|
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 (FrameIter iter(cx); !iter.done(); ++iter) {
|
|
if (obs.shouldMarkAsDebuggee(iter)) {
|
|
if (observing) {
|
|
if (!iter.abstractFramePtr().isDebuggee()) {
|
|
oldestEnabledFrame = iter.abstractFramePtr();
|
|
oldestEnabledFrame.setIsDebuggee();
|
|
}
|
|
if (iter.abstractFramePtr().isWasmDebugFrame()) {
|
|
iter.abstractFramePtr().asWasmDebugFrame()->observe(cx);
|
|
}
|
|
} 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.
|
|
MOZ_ASSERT(!inFrameMaps(iter.abstractFramePtr()));
|
|
#endif
|
|
iter.abstractFramePtr().unsetIsDebuggee();
|
|
}
|
|
}
|
|
}
|
|
|
|
// See comment in unsetPrevUpToDateUntil.
|
|
if (oldestEnabledFrame) {
|
|
AutoRealm ar(cx, oldestEnabledFrame.environmentChain());
|
|
DebugEnvironments::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 realm as addPendingRecompile attempts to
|
|
// cancel off-thread compilations, whose books are kept on the
|
|
// script's realm.
|
|
MOZ_ASSERT(script->zone() == zone);
|
|
AutoRealm ar(cx, script);
|
|
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;
|
|
|
|
AutoSuppressProfilerSampling suppressProfilerSampling(cx);
|
|
|
|
FreeOp* fop = cx->runtime()->defaultFreeOp();
|
|
|
|
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 (auto iter = zone->cellIter<JSScript>(); !iter.done(); iter.next()) {
|
|
JSScript* script = iter;
|
|
if (obs.shouldRecompileOrInvalidate(script) &&
|
|
!gc::IsAboutToBeFinalizedUnbarriered(&script)) {
|
|
if (!AppendAndInvalidateScript(cx, zone, script, scripts)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Code below this point must be infallible to ensure the active bit of
|
|
// BaselineScripts is in a consistent state.
|
|
//
|
|
// Mark active baseline scripts in the observable set so that they don't
|
|
// get discarded. They will be recompiled.
|
|
for (JitActivationIterator actIter(cx); !actIter.done(); ++actIter) {
|
|
if (actIter->compartment()->zone() != zone) {
|
|
continue;
|
|
}
|
|
|
|
for (OnlyJSJitFrameIter iter(actIter); !iter.done(); ++iter) {
|
|
const JSJitFrameIter& frame = iter.frame();
|
|
switch (frame.type()) {
|
|
case FrameType::BaselineJS:
|
|
MarkBaselineScriptActiveIfObservable(frame.script(), obs);
|
|
break;
|
|
case FrameType::IonJS:
|
|
MarkBaselineScriptActiveIfObservable(frame.script(), obs);
|
|
for (InlineFrameIterator inlineIter(cx, &frame); inlineIter.more();
|
|
++inlineIter) {
|
|
MarkBaselineScriptActiveIfObservable(inlineIter.script(), obs);
|
|
}
|
|
break;
|
|
default:;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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]);
|
|
}
|
|
|
|
// Iterate through all wasm instances to find ones that need to be updated.
|
|
for (RealmsInZoneIter r(zone); !r.done(); r.next()) {
|
|
for (wasm::Instance* instance : r->wasm.instances()) {
|
|
if (!instance->debugEnabled()) {
|
|
continue;
|
|
}
|
|
|
|
bool enableTrap = observing == Debugger::IsObserving::Observing;
|
|
instance->debug().ensureEnterFrameTrapsState(cx, enableTrap);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
template <typename FrameFn>
|
|
/* static */ void Debugger::forEachDebuggerFrame(AbstractFramePtr frame,
|
|
FrameFn fn) {
|
|
GlobalObject* global = frame.global();
|
|
if (GlobalObject::DebuggerVector* debuggers = global->getDebuggers()) {
|
|
for (auto p = debuggers->begin(); p != debuggers->end(); p++) {
|
|
Debugger* dbg = *p;
|
|
if (FrameMap::Ptr entry = dbg->frames.lookup(frame)) {
|
|
fn(entry->value());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* static */ bool Debugger::getDebuggerFrames(
|
|
AbstractFramePtr frame, MutableHandle<DebuggerFrameVector> frames) {
|
|
bool hadOOM = false;
|
|
forEachDebuggerFrame(frame, [&](DebuggerFrame* frameobj) {
|
|
if (!hadOOM && !frames.append(frameobj)) {
|
|
hadOOM = true;
|
|
}
|
|
});
|
|
return !hadOOM;
|
|
}
|
|
|
|
/* 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.hasScript() && frame.script()->isDebuggee(),
|
|
frame.isDebuggee());
|
|
MOZ_ASSERT_IF(frame.isWasmDebugFrame(), frame.wasmInstance()->debugEnabled());
|
|
if (frame.isDebuggee()) {
|
|
return true;
|
|
}
|
|
ExecutionObservableFrame obs(frame);
|
|
return updateExecutionObservabilityOfFrames(cx, obs, Observing);
|
|
}
|
|
|
|
/* static */ bool Debugger::ensureExecutionObservabilityOfRealm(JSContext* cx,
|
|
Realm* realm) {
|
|
if (realm->debuggerObservesAllExecution()) {
|
|
return true;
|
|
}
|
|
ExecutionObservableRealms obs(cx);
|
|
if (!obs.add(realm)) {
|
|
return false;
|
|
}
|
|
realm->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) {
|
|
ExecutionObservableRealms obs(cx);
|
|
|
|
for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty();
|
|
r.popFront()) {
|
|
GlobalObject* global = r.front();
|
|
JS::Realm* realm = global->realm();
|
|
|
|
if (realm->debuggerObservesAllExecution() == observing) {
|
|
continue;
|
|
}
|
|
|
|
// It's expensive to eagerly invalidate and recompile a realm,
|
|
// so add the realm to the set only if we are observing.
|
|
if (observing && !obs.add(realm)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!updateExecutionObservability(cx, obs, observing)) {
|
|
return false;
|
|
}
|
|
|
|
using RealmRange = ExecutionObservableRealms::RealmRange;
|
|
for (RealmRange r = obs.realms()->all(); !r.empty(); r.popFront()) {
|
|
r.front()->updateDebuggerObservesAllExecution();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Debugger::updateObservesCoverageOnDebuggees(JSContext* cx,
|
|
IsObserving observing) {
|
|
ExecutionObservableRealms obs(cx);
|
|
|
|
for (WeakGlobalObjectSet::Range r = debuggees.all(); !r.empty();
|
|
r.popFront()) {
|
|
GlobalObject* global = r.front();
|
|
Realm* realm = global->realm();
|
|
|
|
if (realm->debuggerObservesCoverage() == observing) {
|
|
continue;
|
|
}
|
|
|
|
// Invalidate and recompile a realm to add or remove PCCounts
|
|
// increments. We have to eagerly invalidate, as otherwise we might have
|
|
// dangling pointers to freed PCCounts.
|
|
if (!obs.add(realm)) {
|
|
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 (FrameIter iter(cx); !iter.done(); ++iter) {
|
|
if (obs.shouldMarkAsDebuggee(iter)) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_NOT_IDLE);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!updateExecutionObservability(cx, obs, observing)) {
|
|
return false;
|
|
}
|
|
|
|
// All realms can safely be toggled, and all scripts will be recompiled.
|
|
// Thus we can update each realm accordingly.
|
|
using RealmRange = ExecutionObservableRealms::RealmRange;
|
|
for (RealmRange r = obs.realms()->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();
|
|
Realm* realm = global->realm();
|
|
|
|
if (realm->debuggerObservesAsmJS() == observing) {
|
|
continue;
|
|
}
|
|
|
|
realm->updateDebuggerObservesAsmJS();
|
|
}
|
|
}
|
|
|
|
/*** Allocations Tracking ***************************************************/
|
|
|
|
/* static */ bool Debugger::cannotTrackAllocations(const GlobalObject& global) {
|
|
auto existingCallback = global.realm()->getAllocationMetadataBuilder();
|
|
return existingCallback && existingCallback != &SavedStacks::metadataBuilder;
|
|
}
|
|
|
|
/* static */ bool Debugger::isObservedByDebuggerTrackingAllocations(
|
|
const GlobalObject& debuggee) {
|
|
if (auto* v = debuggee.getDebuggers()) {
|
|
for (auto p = v->begin(); p != v->end(); p++) {
|
|
// Use unbarrieredGet() to prevent triggering read barrier while
|
|
// collecting, this is safe as long as dbg does not escape.
|
|
Debugger* dbg = p->unbarrieredGet();
|
|
if (dbg->trackingAllocationSites && dbg->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_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_OBJECT_METADATA_CALLBACK_ALREADY_SET);
|
|
return false;
|
|
}
|
|
|
|
debuggee->realm()->setAllocationMetadataBuilder(
|
|
&SavedStacks::metadataBuilder);
|
|
debuggee->realm()->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.realm()->chooseAllocationSamplingProbability();
|
|
return;
|
|
}
|
|
|
|
global.realm()->forgetAllocationMetadataBuilder();
|
|
}
|
|
|
|
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_ReportErrorNumberASCII(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::traceCrossCompartmentEdges(JSTracer* trc) {
|
|
generatorFrames.traceCrossCompartmentEdges<DebuggerFrame_trace>(trc);
|
|
objects.traceCrossCompartmentEdges<DebuggerObject_trace>(trc);
|
|
environments.traceCrossCompartmentEdges<DebuggerEnv_trace>(trc);
|
|
scripts.traceCrossCompartmentEdges<DebuggerScript_trace>(trc);
|
|
lazyScripts.traceCrossCompartmentEdges<DebuggerScript_trace>(trc);
|
|
sources.traceCrossCompartmentEdges<DebuggerSource_trace>(trc);
|
|
wasmInstanceScripts.traceCrossCompartmentEdges<DebuggerScript_trace>(trc);
|
|
wasmInstanceSources.traceCrossCompartmentEdges<DebuggerSource_trace>(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 five cross-compartment WeakMaps: objects, scripts,
|
|
* lazy 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 into zones that are being compacted.
|
|
*/
|
|
/* static */ void Debugger::traceIncomingCrossCompartmentEdges(JSTracer* trc) {
|
|
JSRuntime* rt = trc->runtime();
|
|
gc::State state = rt->gc.state();
|
|
MOZ_ASSERT(state == gc::State::MarkRoots || state == gc::State::Compact);
|
|
|
|
for (Debugger* dbg : rt->debuggerList()) {
|
|
Zone* zone = MaybeForwarded(dbg->object.get())->zone();
|
|
if (!zone->isCollecting() || state == gc::State::Compact) {
|
|
dbg->traceCrossCompartmentEdges(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::markIteratively(GCMarker* marker) {
|
|
MOZ_ASSERT(JS::RuntimeHeapIsCollecting(),
|
|
"This method should be called during GC.");
|
|
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 = marker->runtime();
|
|
for (RealmsIter r(rt); !r.done(); r.next()) {
|
|
if (r->isDebuggee()) {
|
|
GlobalObject* global = r->unsafeUnbarrieredMaybeGlobal();
|
|
if (!IsMarkedUnbarriered(rt, &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 (auto p = debuggers->begin(); p != debuggers->end(); p++) {
|
|
Debugger* dbg = p->unbarrieredGet();
|
|
|
|
// 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
|
|
GCPtrNativeObject& dbgobj = dbg->toJSObjectRef();
|
|
if (!dbgobj->zone()->isGCMarking()) {
|
|
continue;
|
|
}
|
|
|
|
bool dbgMarked = IsMarked(rt, &dbgobj);
|
|
if (!dbgMarked && dbg->hasAnyLiveHooks(rt)) {
|
|
// obj could be reachable only via its live, enabled
|
|
// debugger hooks, which may yet be called.
|
|
TraceEdge(marker, &dbgobj, "enabled Debugger");
|
|
markedAny = true;
|
|
dbgMarked = true;
|
|
}
|
|
|
|
if (dbgMarked) {
|
|
// Search for breakpoints to mark.
|
|
for (Breakpoint* bp = dbg->firstBreakpoint(); bp;
|
|
bp = bp->nextInDebugger()) {
|
|
switch (bp->site->type()) {
|
|
case BreakpointSite::Type::JS:
|
|
if (IsMarkedUnbarriered(rt, &bp->site->asJS()->script)) {
|
|
// The debugger and the script are both live.
|
|
// Therefore the breakpoint handler is live.
|
|
if (!IsMarked(rt, &bp->getHandlerRef())) {
|
|
TraceEdge(marker, &bp->getHandlerRef(),
|
|
"breakpoint handler");
|
|
markedAny = true;
|
|
}
|
|
}
|
|
break;
|
|
case BreakpointSite::Type::Wasm:
|
|
if (IsMarkedUnbarriered(rt, &bp->asWasm()->wasmInstance)) {
|
|
// The debugger and the wasm instance are both live.
|
|
// Therefore the breakpoint handler is live.
|
|
if (!IsMarked(rt, &bp->getHandlerRef())) {
|
|
TraceEdge(marker, &bp->getHandlerRef(),
|
|
"wasm breakpoint handler");
|
|
markedAny = true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return markedAny;
|
|
}
|
|
|
|
/* static */ void Debugger::traceAllForMovingGC(JSTracer* trc) {
|
|
JSRuntime* rt = trc->runtime();
|
|
for (Debugger* dbg : rt->debuggerList()) {
|
|
dbg->traceForMovingGC(trc);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Trace all debugger-owned GC things unconditionally. This is used during
|
|
* compacting GC and in minor GC: the minor GC cannot apply the weak constraints
|
|
* of the full GC because it visits only part of the heap.
|
|
*/
|
|
void Debugger::traceForMovingGC(JSTracer* trc) {
|
|
trace(trc);
|
|
|
|
for (WeakGlobalObjectSet::Enum e(debuggees); !e.empty(); e.popFront()) {
|
|
TraceManuallyBarrieredEdge(trc, e.mutableFront().unsafeGet(),
|
|
"Global Object");
|
|
}
|
|
|
|
for (Breakpoint* bp = firstBreakpoint(); bp; bp = bp->nextInDebugger()) {
|
|
switch (bp->site->type()) {
|
|
case BreakpointSite::Type::JS:
|
|
TraceManuallyBarrieredEdge(trc, &bp->site->asJS()->script,
|
|
"breakpoint script");
|
|
break;
|
|
case BreakpointSite::Type::Wasm:
|
|
TraceManuallyBarrieredEdge(trc, &bp->asWasm()->wasmInstance,
|
|
"breakpoint wasm instance");
|
|
break;
|
|
}
|
|
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) {
|
|
TraceEdge(trc, &object, "Debugger Object");
|
|
|
|
TraceNullableEdge(trc, &uncaughtExceptionHook, "hooks");
|
|
|
|
// Mark Debugger.Frame objects. These are all reachable from JS, because the
|
|
// corresponding JS frames are still on the stack.
|
|
//
|
|
// (We have weakly-referenced Debugger.Frame objects as well, for suspended
|
|
// generator frames; these are traced via generatorFrames just below.)
|
|
for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) {
|
|
HeapPtr<DebuggerFrame*>& frameobj = r.front().value();
|
|
TraceEdge(trc, &frameobj, "live Debugger.Frame");
|
|
MOZ_ASSERT(frameobj->getPrivate(frameobj->numFixedSlotsMaybeForwarded()));
|
|
}
|
|
|
|
allocationsLog.trace(trc);
|
|
|
|
generatorFrames.trace(trc);
|
|
scripts.trace(trc);
|
|
lazyScripts.trace(trc);
|
|
sources.trace(trc);
|
|
objects.trace(trc);
|
|
environments.trace(trc);
|
|
wasmInstanceScripts.trace(trc);
|
|
wasmInstanceSources.trace(trc);
|
|
}
|
|
|
|
/* static */ void Debugger::sweepAll(FreeOp* fop) {
|
|
JSRuntime* rt = fop->runtime();
|
|
|
|
Debugger* dbg = rt->debuggerList().getFirst();
|
|
while (dbg) {
|
|
Debugger* next = dbg->getNext();
|
|
|
|
// Detach dying debuggers and debuggees from each other. Since this
|
|
// requires access to both objects it must be done before either
|
|
// object is finalized.
|
|
bool debuggerDying = IsAboutToBeFinalized(&dbg->object);
|
|
for (WeakGlobalObjectSet::Enum e(dbg->debuggees); !e.empty();
|
|
e.popFront()) {
|
|
GlobalObject* global = e.front().unbarrieredGet();
|
|
if (debuggerDying || IsAboutToBeFinalizedUnbarriered(&global)) {
|
|
dbg->removeDebuggeeGlobal(fop, e.front().unbarrieredGet(), &e);
|
|
}
|
|
}
|
|
|
|
if (debuggerDying) {
|
|
fop->delete_(dbg);
|
|
}
|
|
|
|
dbg = next;
|
|
}
|
|
}
|
|
|
|
/* 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::ZoneComponentFinder& finder) {
|
|
JSRuntime* rt = zone->runtimeFromMainThread();
|
|
for (Debugger* dbg : rt->debuggerList()) {
|
|
Zone* debuggerZone = dbg->object->zone();
|
|
if (!debuggerZone->isGCMarking()) {
|
|
continue;
|
|
}
|
|
|
|
if (debuggerZone == zone) {
|
|
// Add edges to debuggee zones. These are weak references that are
|
|
// not in the cross compartment wrapper map.
|
|
for (auto e = dbg->debuggeeZones.all(); !e.empty(); e.popFront()) {
|
|
Zone* debuggeeZone = e.front();
|
|
if (debuggeeZone->isGCMarking()) {
|
|
finder.addEdgeTo(debuggeeZone);
|
|
}
|
|
}
|
|
} else {
|
|
// For debugger cross compartment wrappers, add edges in the
|
|
// opposite direction to those already added by
|
|
// Compartment::findOutgoingEdges and above. This ensure that
|
|
// debuggers and their debuggees are finalized in the same group.
|
|
if (dbg->debuggeeZones.has(zone) ||
|
|
dbg->generatorFrames.hasKeyInZone(zone) ||
|
|
dbg->scripts.hasKeyInZone(zone) ||
|
|
dbg->lazyScripts.hasKeyInZone(zone) ||
|
|
dbg->sources.hasKeyInZone(zone) || dbg->objects.hasKeyInZone(zone) ||
|
|
dbg->environments.hasKeyInZone(zone) ||
|
|
dbg->wasmInstanceScripts.hasKeyInZone(zone) ||
|
|
dbg->wasmInstanceSources.hasKeyInZone(zone)) {
|
|
finder.addEdgeTo(debuggerZone);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const ClassOps Debugger::classOps_ = {nullptr, /* addProperty */
|
|
nullptr, /* delProperty */
|
|
nullptr, /* enumerate */
|
|
nullptr, /* newEnumerate */
|
|
nullptr, /* resolve */
|
|
nullptr, /* mayResolve */
|
|
nullptr, /* finalize */
|
|
nullptr, /* call */
|
|
nullptr, /* hasInstance */
|
|
nullptr, /* construct */
|
|
Debugger::traceObject};
|
|
|
|
const Class Debugger::class_ = {
|
|
"Debugger",
|
|
JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUG_COUNT),
|
|
&Debugger::classOps_};
|
|
|
|
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::class_) {
|
|
JS_ReportErrorNumberASCII(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 = Debugger::fromJSObject(thisobj);
|
|
if (!dbg) {
|
|
JS_ReportErrorNumberASCII(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) {
|
|
cx->runtime()->onNewGlobalObjectWatchers().pushBack(dbg);
|
|
} else {
|
|
cx->runtime()->onNewGlobalObjectWatchers().remove(dbg);
|
|
}
|
|
}
|
|
|
|
// 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_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_NOT_CALLABLE_OR_UNDEFINED);
|
|
return false;
|
|
}
|
|
uint32_t slot = JSSLOT_DEBUG_HOOK_START + which;
|
|
RootedValue oldHook(cx, dbg.object->getReservedSlot(slot));
|
|
dbg.object->setReservedSlot(slot, args[0]);
|
|
if (hookObservesAllExecution(which)) {
|
|
if (!dbg.updateObservesAllExecutionOnDebuggees(
|
|
cx, dbg.observesAllExecution())) {
|
|
dbg.object->setReservedSlot(slot, oldHook);
|
|
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) {
|
|
cx->runtime()->onNewGlobalObjectWatchers().pushBack(dbg);
|
|
} else if (oldHook && !newHook) {
|
|
cx->runtime()->onNewGlobalObjectWatchers().remove(dbg);
|
|
}
|
|
}
|
|
|
|
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_ReportErrorNumberASCII(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();
|
|
Realm* realm = global->realm();
|
|
realm->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;
|
|
}
|
|
|
|
/*
|
|
* 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_ReportErrorNumberASCII(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) {
|
|
ReportAccessDenied(cx);
|
|
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_ReportErrorNumberASCII(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 (CompartmentsIter comp(cx->runtime()); !comp.done(); comp.next()) {
|
|
if (comp == dbg->object->compartment()) {
|
|
continue;
|
|
}
|
|
for (RealmsInCompartmentIter r(comp); !r.done(); r.next()) {
|
|
if (r->creationOptions().invisibleToDebugger()) {
|
|
continue;
|
|
}
|
|
r->compartment()->gcState.scheduledForDestruction = false;
|
|
GlobalObject* global = r->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;
|
|
}
|
|
|
|
ExecutionObservableRealms obs(cx);
|
|
|
|
if (dbg->debuggees.has(global)) {
|
|
dbg->removeDebuggeeGlobal(cx->runtime()->defaultFreeOp(), global, nullptr);
|
|
|
|
// Only update the realm 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->realm())) {
|
|
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);
|
|
|
|
ExecutionObservableRealms obs(cx);
|
|
|
|
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->realm())) {
|
|
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();
|
|
FrameIter iter(i.activation()->cx());
|
|
while (!iter.hasUsableAbstractFramePtr() ||
|
|
iter.abstractFramePtr() != frame) {
|
|
++iter;
|
|
}
|
|
return dbg->getFrame(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()->realm()->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_ReportErrorNumberASCII(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::class_);
|
|
|
|
// 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::class_, proto, TenuredObject));
|
|
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.
|
|
auto dbg = cx->make_unique<Debugger>(cx, obj.get());
|
|
if (!dbg) {
|
|
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++) {
|
|
JSObject& wrappedObj =
|
|
args[i].toObject().as<ProxyObject>().private_().toObject();
|
|
Rooted<GlobalObject*> debuggee(cx, &wrappedObj.nonCCWGlobal());
|
|
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.
|
|
Realm* debuggeeRealm = global->realm();
|
|
if (debuggeeRealm->creationOptions().invisibleToDebugger()) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_CANT_DEBUG_GLOBAL);
|
|
return false;
|
|
}
|
|
|
|
// Debugger and debuggee must be in different compartments.
|
|
if (debuggeeRealm->compartment() == object->compartment()) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_SAME_COMPARTMENT);
|
|
return false;
|
|
}
|
|
|
|
// Check for cycles. If global's realm is reachable from this Debugger
|
|
// object's realm 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<Realm*> visited(cx);
|
|
if (!visited.append(object->realm())) {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < visited.length(); i++) {
|
|
Realm* realm = visited[i];
|
|
if (realm == debuggeeRealm) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_LOOP);
|
|
return false;
|
|
}
|
|
|
|
// Find all realms containing debuggers debugging realm's global object.
|
|
// Add those realms to visited.
|
|
if (realm->isDebuggee()) {
|
|
GlobalObject::DebuggerVector* v = realm->maybeGlobal()->getDebuggers();
|
|
for (auto p = v->begin(); p != v->end(); p++) {
|
|
Realm* next = (*p)->object->realm();
|
|
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 SavedStacksMetadataBuilder must be
|
|
// installed for this realm, and
|
|
// 6. Realm::isDebuggee()'s bit must be set.
|
|
//
|
|
// All six indications must be kept consistent.
|
|
|
|
AutoRealm ar(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)
|
|
AutoRestoreRealmDebugMode debugModeGuard(debuggeeRealm);
|
|
debuggeeRealm->setIsDebuggee();
|
|
debuggeeRealm->updateDebuggerObservesAsmJS();
|
|
debuggeeRealm->updateDebuggerObservesCoverage();
|
|
if (observesAllExecution() &&
|
|
!ensureExecutionObservabilityOfRealm(cx, debuggeeRealm)) {
|
|
return false;
|
|
}
|
|
|
|
globalDebuggersGuard.release();
|
|
debuggeesGuard.release();
|
|
zoneDebuggersGuard.release();
|
|
debuggeeZonesGuard.release();
|
|
allocationsTrackingGuard.release();
|
|
debugModeGuard.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 T>
|
|
static T* findDebuggerInVector(Debugger* dbg,
|
|
Vector<T, 0, js::SystemAllocPolicy>* vec) {
|
|
T* p;
|
|
for (p = vec->begin(); p != vec->end(); p++) {
|
|
if (*p == dbg) {
|
|
break;
|
|
}
|
|
}
|
|
MOZ_ASSERT(p != vec->end());
|
|
return p;
|
|
}
|
|
|
|
// a ReadBarriered version for findDebuggerInVector
|
|
// TODO: Bug 1515934 - findDebuggerInVector<T> triggers read barriers.
|
|
static ReadBarriered<Debugger*>* findDebuggerInVector(
|
|
Debugger* dbg,
|
|
Vector<ReadBarriered<Debugger*>, 0, js::SystemAllocPolicy>* vec) {
|
|
ReadBarriered<Debugger*>* p;
|
|
for (p = vec->begin(); p != vec->end(); p++) {
|
|
if (p->unbarrieredGet() == 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();
|
|
DebuggerFrame* frameobj = e.front().value();
|
|
if (frame.hasGlobal(global)) {
|
|
frameobj->freeFrameIterData(fop);
|
|
DebuggerFrame_maybeDecrementFrameScriptStepModeCount(fop, frame,
|
|
frameobj);
|
|
e.removeFront();
|
|
}
|
|
}
|
|
|
|
// Clear this global's generators from generatorFrames as well.
|
|
//
|
|
// This method can be called either from script (dbg.removeDebuggee) or
|
|
// from an awkward time during GC sweeping. In the latter case, skip this
|
|
// loop to avoid touching dead objects. It's correct because, when we're
|
|
// called from GC, all `global`'s generators are guaranteed to be dying:
|
|
// live generators would keep the global alive and we wouldn't be here. GC
|
|
// will sweep dead keys from the weakmap.
|
|
if (!global->zone()->isGCSweeping()) {
|
|
generatorFrames.removeIf([global](JSObject* key) {
|
|
GeneratorObject& genObj = key->as<GeneratorObject>();
|
|
return genObj.isClosed() || &genObj.callee().global() == global;
|
|
});
|
|
}
|
|
|
|
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();
|
|
switch (bp->site->type()) {
|
|
case BreakpointSite::Type::JS:
|
|
if (bp->site->asJS()->script->realm() == global->realm()) {
|
|
bp->destroy(fop);
|
|
}
|
|
break;
|
|
case BreakpointSite::Type::Wasm:
|
|
if (bp->asWasm()->wasmInstance->realm() == global->realm()) {
|
|
bp->destroy(fop);
|
|
}
|
|
break;
|
|
default:
|
|
MOZ_CRASH("unknown breakpoint type");
|
|
}
|
|
}
|
|
MOZ_ASSERT_IF(debuggees.empty(), !firstBreakpoint());
|
|
|
|
// If we are tracking allocation sites, we need to remove the object
|
|
// metadata callback from this global's realm.
|
|
if (trackingAllocationSites) {
|
|
Debugger::removeAllocationsTracking(*global);
|
|
}
|
|
|
|
if (global->getDebuggers()->empty()) {
|
|
global->realm()->unsetIsDebuggee();
|
|
} else {
|
|
global->realm()->updateDebuggerObservesAllExecution();
|
|
global->realm()->updateDebuggerObservesAsmJS();
|
|
global->realm()->updateDebuggerObservesCoverage();
|
|
}
|
|
}
|
|
|
|
static inline DebuggerSourceReferent GetSourceReferent(JSObject* obj);
|
|
|
|
class MOZ_STACK_CLASS Debugger::QueryBase {
|
|
protected:
|
|
QueryBase(JSContext* cx, Debugger* dbg)
|
|
: cx(cx),
|
|
debugger(dbg),
|
|
iterMarker(&cx->runtime()->gc),
|
|
realms(cx->zone()),
|
|
oom(false) {}
|
|
|
|
// The context in which we should do our work.
|
|
JSContext* cx;
|
|
|
|
// The debugger for which we conduct queries.
|
|
Debugger* debugger;
|
|
|
|
// Require the set of realms to stay fixed while the query is alive.
|
|
gc::AutoEnterIteration iterMarker;
|
|
|
|
using RealmSet = HashSet<Realm*, DefaultHasher<Realm*>, ZoneAllocPolicy>;
|
|
|
|
// A script must be in one of these realms to match the query.
|
|
RealmSet realms;
|
|
|
|
// Indicates whether OOM has occurred while matching.
|
|
bool oom;
|
|
|
|
bool addRealm(Realm* realm) { return realms.put(realm); }
|
|
|
|
// Arrange for this query to match only scripts that run in |global|.
|
|
bool matchSingleGlobal(GlobalObject* global) {
|
|
MOZ_ASSERT(realms.count() == 0);
|
|
if (!addRealm(global->realm())) {
|
|
ReportOutOfMemory(cx);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Arrange for this ScriptQuery to match all scripts running in debuggee
|
|
// globals.
|
|
bool matchAllDebuggeeGlobals() {
|
|
MOZ_ASSERT(realms.count() == 0);
|
|
// Build our realm set from the debugger's set of debuggee globals.
|
|
for (WeakGlobalObjectSet::Range r = debugger->debuggees.all(); !r.empty();
|
|
r.popFront()) {
|
|
if (!addRealm(r.front()->realm())) {
|
|
ReportOutOfMemory(cx);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
/*
|
|
* A class for parsing 'findScripts' query arguments and searching for
|
|
* scripts that match the criteria they represent.
|
|
*/
|
|
class MOZ_STACK_CLASS Debugger::ScriptQuery : public Debugger::QueryBase {
|
|
public:
|
|
/* Construct a ScriptQuery to use matching scripts for |dbg|. */
|
|
ScriptQuery(JSContext* cx, Debugger* dbg)
|
|
: QueryBase(cx, dbg),
|
|
url(cx),
|
|
displayURLString(cx),
|
|
hasSource(false),
|
|
source(cx, AsVariant(static_cast<ScriptSourceObject*>(nullptr))),
|
|
hasLine(false),
|
|
line(0),
|
|
innermost(false),
|
|
innermostForRealm(cx->zone()),
|
|
scriptVector(cx, ScriptVector(cx)),
|
|
lazyScriptVector(cx, LazyScriptVector(cx)),
|
|
wasmInstanceVector(cx, WasmInstanceObjectVector(cx)) {}
|
|
|
|
/*
|
|
* 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()) {
|
|
if (!matchAllDebuggeeGlobals()) {
|
|
return false;
|
|
}
|
|
} 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_ReportErrorNumberASCII(
|
|
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_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_UNEXPECTED_TYPE,
|
|
"query object's 'source' property",
|
|
"not undefined nor a Debugger.Source object");
|
|
return false;
|
|
}
|
|
|
|
Value owner =
|
|
debuggerSource.toObject().as<NativeObject>().getReservedSlot(
|
|
JSSLOT_DEBUGSOURCE_OWNER);
|
|
|
|
// The given source must have an owner. Otherwise, it's a
|
|
// Debugger.Source.prototype, which would match no scripts, and is
|
|
// probably a mistake.
|
|
if (!owner.isObject()) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_PROTO, "Debugger.Source",
|
|
"Debugger.Source");
|
|
return false;
|
|
}
|
|
|
|
// If it does have an owner, it should match the Debugger we're
|
|
// calling findScripts on. It would work fine even if it didn't,
|
|
// but mixing Debugger.Sources is probably a sign of confusion.
|
|
if (&owner.toObject() != debugger->object) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_WRONG_OWNER, "Debugger.Source");
|
|
return false;
|
|
}
|
|
|
|
hasSource = true;
|
|
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_ReportErrorNumberASCII(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() && !hasSource) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_QUERY_LINE_WITHOUT_URL);
|
|
return false;
|
|
}
|
|
double doubleLine = lineProperty.toNumber();
|
|
if (doubleLine <= 0 || (unsigned int)doubleLine != doubleLine) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_BAD_LINE);
|
|
return false;
|
|
}
|
|
hasLine = true;
|
|
line = doubleLine;
|
|
} else {
|
|
JS_ReportErrorNumberASCII(
|
|
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() && !hasSource) ||
|
|
!hasLine) {
|
|
JS_ReportErrorNumberASCII(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 realms and the stack for scripts matching
|
|
* this query, and append the matching scripts to |scriptVector|.
|
|
*/
|
|
bool findScripts() {
|
|
if (!prepareQuery()) {
|
|
return false;
|
|
}
|
|
|
|
bool delazified = false;
|
|
if (needsDelazifyBeforeQuery()) {
|
|
if (!delazifyScripts()) {
|
|
return false;
|
|
}
|
|
delazified = true;
|
|
}
|
|
|
|
Realm* singletonRealm = nullptr;
|
|
if (realms.count() == 1) {
|
|
singletonRealm = realms.all().front();
|
|
}
|
|
|
|
// Search each realm for debuggee scripts.
|
|
MOZ_ASSERT(scriptVector.empty());
|
|
MOZ_ASSERT(lazyScriptVector.empty());
|
|
oom = false;
|
|
IterateScripts(cx, singletonRealm, this, considerScript);
|
|
if (!delazified) {
|
|
IterateLazyScripts(cx, singletonRealm, this, considerLazyScript);
|
|
}
|
|
if (oom) {
|
|
ReportOutOfMemory(cx);
|
|
return false;
|
|
}
|
|
|
|
// For most queries, we just accumulate results in 'scriptVector' and
|
|
// 'lazyScriptVector' as we find them. But if this is an 'innermost'
|
|
// query, then we've accumulated the results in the 'innermostForRealm'
|
|
// map. In that case, we now need to walk that map and
|
|
// populate 'scriptVector'.
|
|
if (innermost) {
|
|
for (RealmToScriptMap::Range r = innermostForRealm.all(); !r.empty();
|
|
r.popFront()) {
|
|
JS::ExposeScriptToActiveJS(r.front().value());
|
|
if (!scriptVector.append(r.front().value())) {
|
|
ReportOutOfMemory(cx);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Until such time that wasm modules are real ES6 modules,
|
|
// unconditionally consider all wasm toplevel instance scripts.
|
|
for (WeakGlobalObjectSet::Range r = debugger->allDebuggees(); !r.empty();
|
|
r.popFront()) {
|
|
for (wasm::Instance* instance : r.front()->realm()->wasm.instances()) {
|
|
consider(instance->object());
|
|
if (oom) {
|
|
ReportOutOfMemory(cx);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Handle<ScriptVector> foundScripts() const { return scriptVector; }
|
|
Handle<LazyScriptVector> foundLazyScripts() const { return lazyScriptVector; }
|
|
|
|
Handle<WasmInstanceObjectVector> foundWasmInstances() const {
|
|
return wasmInstanceVector;
|
|
}
|
|
|
|
private:
|
|
/* If this is a string, matching scripts have urls equal to it. */
|
|
RootedValue url;
|
|
|
|
/* url as a C string. */
|
|
UniqueChars urlCString;
|
|
|
|
/* If this is a string, matching scripts' sources have displayURLs equal to
|
|
* it. */
|
|
RootedLinearString displayURLString;
|
|
|
|
/*
|
|
* If this is a source referent, matching scripts will have sources equal
|
|
* to this instance. Ideally we'd use a Maybe here, but Maybe interacts
|
|
* very badly with Rooted's LIFO invariant.
|
|
*/
|
|
bool hasSource;
|
|
Rooted<DebuggerSourceReferent> 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;
|
|
|
|
using RealmToScriptMap =
|
|
HashMap<Realm*, JSScript*, DefaultHasher<Realm*>, ZoneAllocPolicy>;
|
|
|
|
/*
|
|
* For 'innermost' queries, a map from realms to the innermost script
|
|
* we've seen so far in that realm. (Template instantiation code size
|
|
* explosion ho!)
|
|
*/
|
|
RealmToScriptMap innermostForRealm;
|
|
|
|
/*
|
|
* Accumulate the scripts in an Rooted<ScriptVector> and
|
|
* Rooted<LazyScriptVector>, 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> scriptVector;
|
|
Rooted<LazyScriptVector> lazyScriptVector;
|
|
|
|
/*
|
|
* Like above, but for wasm modules.
|
|
*/
|
|
Rooted<WasmInstanceObjectVector> wasmInstanceVector;
|
|
|
|
/*
|
|
* 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()) {
|
|
urlCString = JS_EncodeStringToLatin1(cx, url.toString());
|
|
if (!urlCString) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool delazifyScripts() {
|
|
// All scripts in debuggee realms must be visible, so delazify
|
|
// everything.
|
|
for (auto r = realms.all(); !r.empty(); r.popFront()) {
|
|
Realm* realm = r.front();
|
|
if (!realm->ensureDelazifyScriptsForDebugger(cx)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void considerScript(JSRuntime* rt, void* data, JSScript* script,
|
|
const JS::AutoRequireNoGC& nogc) {
|
|
ScriptQuery* self = static_cast<ScriptQuery*>(data);
|
|
self->consider(script, nogc);
|
|
}
|
|
|
|
static void considerLazyScript(JSRuntime* rt, void* data,
|
|
LazyScript* lazyScript,
|
|
const JS::AutoRequireNoGC& nogc) {
|
|
ScriptQuery* self = static_cast<ScriptQuery*>(data);
|
|
self->consider(lazyScript, nogc);
|
|
}
|
|
|
|
bool needsDelazifyBeforeQuery() const {
|
|
// * innermost
|
|
// Currently not supported, since this is not used outside of test.
|
|
//
|
|
// * hasLine
|
|
// Only JSScript supports GetScriptLineExtent.
|
|
return innermost || hasLine;
|
|
}
|
|
|
|
template <typename T>
|
|
MOZ_MUST_USE bool commonFilter(T script, const JS::AutoRequireNoGC& nogc) {
|
|
if (urlCString) {
|
|
bool gotFilename = false;
|
|
if (script->filename() &&
|
|
strcmp(script->filename(), urlCString.get()) == 0) {
|
|
gotFilename = true;
|
|
}
|
|
|
|
bool gotSourceURL = false;
|
|
if (!gotFilename && script->scriptSource()->introducerFilename() &&
|
|
strcmp(script->scriptSource()->introducerFilename(),
|
|
urlCString.get()) == 0) {
|
|
gotSourceURL = true;
|
|
}
|
|
if (!gotFilename && !gotSourceURL) {
|
|
return false;
|
|
}
|
|
}
|
|
if (displayURLString) {
|
|
if (!script->scriptSource() || !script->scriptSource()->hasDisplayURL()) {
|
|
return false;
|
|
}
|
|
|
|
const char16_t* s = script->scriptSource()->displayURL();
|
|
if (CompareChars(s, js_strlen(s), displayURLString) != 0) {
|
|
return false;
|
|
}
|
|
}
|
|
if (hasSource && !(source.is<ScriptSourceObject*>() &&
|
|
source.as<ScriptSourceObject*>()->source() ==
|
|
script->scriptSource())) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* If |script| matches this query, append it to |scriptVector| or place it
|
|
* in |innermostForRealm|, as appropriate. Set |oom| if an out of memory
|
|
* condition occurred.
|
|
*/
|
|
void consider(JSScript* script, const JS::AutoRequireNoGC& nogc) {
|
|
if (oom || script->selfHosted()) {
|
|
return;
|
|
}
|
|
Realm* realm = script->realm();
|
|
if (!realms.has(realm)) {
|
|
return;
|
|
}
|
|
if (!commonFilter(script, nogc)) {
|
|
return;
|
|
}
|
|
if (hasLine) {
|
|
if (line < script->lineno() ||
|
|
script->lineno() + GetScriptLineExtent(script) < line) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (innermost) {
|
|
// For 'innermost' queries, we don't place scripts in
|
|
// |scriptVector| 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 realm in innermostForRealm,
|
|
// and only populate |scriptVector| 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 innermostForRealm, and replace that
|
|
// if it's better.
|
|
RealmToScriptMap::AddPtr p = innermostForRealm.lookupForAdd(realm);
|
|
if (p) {
|
|
// Is our newly found script deeper than the last one we found?
|
|
JSScript* incumbent = p->value();
|
|
if (script->innermostScope()->chainLength() >
|
|
incumbent->innermostScope()->chainLength()) {
|
|
p->value() = script;
|
|
}
|
|
} else {
|
|
// This is the first matching script we've encountered for this
|
|
// realm, so it is thus the innermost such script.
|
|
if (!innermostForRealm.add(p, realm, script)) {
|
|
oom = true;
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
// Record this matching script in the results scriptVector.
|
|
if (!scriptVector.append(script)) {
|
|
oom = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void consider(LazyScript* lazyScript, const JS::AutoRequireNoGC& nogc) {
|
|
MOZ_ASSERT(!needsDelazifyBeforeQuery());
|
|
|
|
if (oom) {
|
|
return;
|
|
}
|
|
Realm* realm = lazyScript->realm();
|
|
if (!realms.has(realm)) {
|
|
return;
|
|
}
|
|
|
|
// If the script is already delazified, it should be in scriptVector.
|
|
if (lazyScript->maybeScript()) {
|
|
return;
|
|
}
|
|
|
|
if (!commonFilter(lazyScript, nogc)) {
|
|
return;
|
|
}
|
|
|
|
/* Record this matching script in the results lazyScriptVector. */
|
|
if (!lazyScriptVector.append(lazyScript)) {
|
|
oom = true;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If |instanceObject| matches this query, append it to |wasmInstanceVector|.
|
|
* Set |oom| if an out of memory condition occurred.
|
|
*/
|
|
void consider(WasmInstanceObject* instanceObject) {
|
|
if (oom) {
|
|
return;
|
|
}
|
|
|
|
if (hasSource && source != AsVariant(instanceObject)) {
|
|
return;
|
|
}
|
|
|
|
if (!wasmInstanceVector.append(instanceObject)) {
|
|
oom = true;
|
|
}
|
|
}
|
|
};
|
|
|
|
/* static */ bool Debugger::findScripts(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER(cx, argc, vp, "findScripts", args, dbg);
|
|
|
|
ScriptQuery query(cx, dbg);
|
|
|
|
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());
|
|
Handle<LazyScriptVector> lazyScripts(query.foundLazyScripts());
|
|
Handle<WasmInstanceObjectVector> wasmInstances(query.foundWasmInstances());
|
|
|
|
size_t resultLength =
|
|
scripts.length() + lazyScripts.length() + wasmInstances.length();
|
|
RootedArrayObject result(cx, NewDenseFullyAllocatedArray(cx, resultLength));
|
|
if (!result) {
|
|
return false;
|
|
}
|
|
|
|
result->ensureDenseInitializedLength(cx, 0, resultLength);
|
|
|
|
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));
|
|
}
|
|
|
|
size_t lazyStart = scripts.length();
|
|
for (size_t i = 0; i < lazyScripts.length(); i++) {
|
|
JSObject* scriptObject = dbg->wrapLazyScript(cx, lazyScripts[i]);
|
|
if (!scriptObject) {
|
|
return false;
|
|
}
|
|
result->setDenseElement(lazyStart + i, ObjectValue(*scriptObject));
|
|
}
|
|
|
|
size_t wasmStart = scripts.length() + lazyScripts.length();
|
|
for (size_t i = 0; i < wasmInstances.length(); i++) {
|
|
JSObject* scriptObject = dbg->wrapWasmScript(cx, wasmInstances[i]);
|
|
if (!scriptObject) {
|
|
return false;
|
|
}
|
|
result->setDenseElement(wasmStart + i, ObjectValue(*scriptObject));
|
|
}
|
|
|
|
args.rval().setObject(*result);
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* A class for searching sources for 'findSources'.
|
|
*/
|
|
class MOZ_STACK_CLASS Debugger::SourceQuery : public Debugger::QueryBase {
|
|
public:
|
|
using SourceSet = JS::GCHashSet<JSObject*, js::MovableCellHasher<JSObject*>,
|
|
ZoneAllocPolicy>;
|
|
|
|
SourceQuery(JSContext* cx, Debugger* dbg)
|
|
: QueryBase(cx, dbg), sources(cx, SourceSet(cx->zone())) {}
|
|
|
|
bool findSources() {
|
|
if (!matchAllDebuggeeGlobals()) {
|
|
return false;
|
|
}
|
|
|
|
Realm* singletonRealm = nullptr;
|
|
if (realms.count() == 1) {
|
|
singletonRealm = realms.all().front();
|
|
}
|
|
|
|
// Search each realm for debuggee scripts.
|
|
MOZ_ASSERT(sources.empty());
|
|
oom = false;
|
|
IterateScripts(cx, singletonRealm, this, considerScript);
|
|
IterateLazyScripts(cx, singletonRealm, this, considerLazyScript);
|
|
if (oom) {
|
|
ReportOutOfMemory(cx);
|
|
return false;
|
|
}
|
|
|
|
// TODO: Until such time that wasm modules are real ES6 modules,
|
|
// unconditionally consider all wasm toplevel instance scripts.
|
|
for (WeakGlobalObjectSet::Range r = debugger->allDebuggees(); !r.empty();
|
|
r.popFront()) {
|
|
for (wasm::Instance* instance : r.front()->realm()->wasm.instances()) {
|
|
consider(instance->object());
|
|
if (oom) {
|
|
ReportOutOfMemory(cx);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Handle<SourceSet> foundSources() const { return sources; }
|
|
|
|
private:
|
|
Rooted<SourceSet> sources;
|
|
|
|
static void considerScript(JSRuntime* rt, void* data, JSScript* script,
|
|
const JS::AutoRequireNoGC& nogc) {
|
|
SourceQuery* self = static_cast<SourceQuery*>(data);
|
|
self->consider(script, nogc);
|
|
}
|
|
|
|
static void considerLazyScript(JSRuntime* rt, void* data,
|
|
LazyScript* lazyScript,
|
|
const JS::AutoRequireNoGC& nogc) {
|
|
SourceQuery* self = static_cast<SourceQuery*>(data);
|
|
self->consider(lazyScript, nogc);
|
|
}
|
|
|
|
void consider(JSScript* script, const JS::AutoRequireNoGC& nogc) {
|
|
if (oom || script->selfHosted()) {
|
|
return;
|
|
}
|
|
Realm* realm = script->realm();
|
|
if (!realms.has(realm)) {
|
|
return;
|
|
}
|
|
|
|
if (!script->sourceObject()) {
|
|
return;
|
|
}
|
|
|
|
ScriptSourceObject* source =
|
|
&UncheckedUnwrap(script->sourceObject())->as<ScriptSourceObject>();
|
|
if (!sources.put(source)) {
|
|
oom = true;
|
|
}
|
|
}
|
|
|
|
void consider(LazyScript* lazyScript, const JS::AutoRequireNoGC& nogc) {
|
|
if (oom) {
|
|
return;
|
|
}
|
|
Realm* realm = lazyScript->realm();
|
|
if (!realms.has(realm)) {
|
|
return;
|
|
}
|
|
|
|
// If the script is already delazified, it should already be handled.
|
|
if (lazyScript->maybeScript()) {
|
|
return;
|
|
}
|
|
|
|
ScriptSourceObject* source = &lazyScript->sourceObject();
|
|
if (!sources.put(source)) {
|
|
oom = true;
|
|
}
|
|
}
|
|
|
|
void consider(WasmInstanceObject* instanceObject) {
|
|
if (oom) {
|
|
return;
|
|
}
|
|
|
|
if (!sources.put(instanceObject)) {
|
|
oom = true;
|
|
}
|
|
}
|
|
};
|
|
|
|
static inline DebuggerSourceReferent AsSourceReferent(JSObject* obj) {
|
|
if (obj->is<ScriptSourceObject>()) {
|
|
return AsVariant(&obj->as<ScriptSourceObject>());
|
|
}
|
|
return AsVariant(&obj->as<WasmInstanceObject>());
|
|
}
|
|
|
|
/* static */ bool Debugger::findSources(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER(cx, argc, vp, "findSources", args, dbg);
|
|
|
|
SourceQuery query(cx, dbg);
|
|
if (!query.findSources()) {
|
|
return false;
|
|
}
|
|
|
|
Handle<SourceQuery::SourceSet> sources(query.foundSources());
|
|
|
|
size_t resultLength = sources.count();
|
|
RootedArrayObject result(cx, NewDenseFullyAllocatedArray(cx, resultLength));
|
|
if (!result) {
|
|
return false;
|
|
}
|
|
|
|
result->ensureDenseInitializedLength(cx, 0, resultLength);
|
|
|
|
size_t i = 0;
|
|
for (auto iter = sources.get().iter(); !iter.done(); iter.next()) {
|
|
Rooted<DebuggerSourceReferent> sourceReferent(cx,
|
|
AsSourceReferent(iter.get()));
|
|
RootedObject sourceObject(cx, dbg->wrapVariantReferent(cx, sourceReferent));
|
|
if (!sourceObject) {
|
|
return false;
|
|
}
|
|
result->setDenseElement(i, ObjectValue(*sourceObject));
|
|
i++;
|
|
}
|
|
|
|
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;
|
|
|
|
/* The set of debuggee compartments. */
|
|
JS::CompartmentSet debuggeeCompartments;
|
|
|
|
/*
|
|
* 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_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_UNEXPECTED_TYPE,
|
|
"query object's 'class' property",
|
|
"neither undefined nor a string");
|
|
return false;
|
|
}
|
|
JSLinearString* str = cls.toString()->ensureLinear(cx);
|
|
if (!str) {
|
|
return false;
|
|
}
|
|
if (!StringIsAscii(str)) {
|
|
JS_ReportErrorNumberASCII(
|
|
cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
|
|
"query object's 'class' property",
|
|
"not a string containing only ASCII characters");
|
|
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;
|
|
}
|
|
|
|
for (WeakGlobalObjectSet::Range r = dbg->allDebuggees(); !r.empty();
|
|
r.popFront()) {
|
|
if (!debuggeeCompartments.put(r.front()->compartment())) {
|
|
ReportOutOfMemory(cx);
|
|
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, maybeNoGC);
|
|
if (!rootList.init(dbgObj)) {
|
|
ReportOutOfMemory(cx);
|
|
return false;
|
|
}
|
|
|
|
Traversal traversal(cx, *this, maybeNoGC.ref());
|
|
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.
|
|
JS::Compartment* comp = referent.compartment();
|
|
if (comp && !debuggeeCompartments.has(comp)) {
|
|
traversal.abandonReferent();
|
|
return true;
|
|
}
|
|
|
|
// If the referent has an associated realm and it's not a debuggee
|
|
// realm, skip it. Don't abandonReferent() here like above: realms
|
|
// within a compartment can reference each other without going through
|
|
// cross-compartment wrappers.
|
|
Realm* realm = referent.realm();
|
|
if (realm && !dbg->isDebuggeeUnbarriered(realm)) {
|
|
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 EnvironmentObjects 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.get()) != 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. */
|
|
UniqueChars classNameCString;
|
|
|
|
/*
|
|
* Given that either omittedQuery or parseQuery has been called, prepare the
|
|
* query for matching objects.
|
|
*/
|
|
bool prepareQuery() {
|
|
if (className.isString()) {
|
|
classNameCString = JS_EncodeStringToASCII(cx, className.toString());
|
|
if (!classNameCString) {
|
|
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 realms from under us, while iterating.
|
|
JS::AutoCheckCannotGC nogc;
|
|
|
|
for (RealmsIter r(cx->runtime()); !r.done(); r.next()) {
|
|
if (r->creationOptions().invisibleToDebugger()) {
|
|
continue;
|
|
}
|
|
|
|
r->compartment()->gcState.scheduledForDestruction = false;
|
|
|
|
GlobalObject* global = r->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 realm,
|
|
// then from it we can reach function objects, scripts, environments, etc.,
|
|
// none of which we're ever supposed to see.
|
|
if (global->realm()->creationOptions().invisibleToDebugger()) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_INVISIBLE_COMPARTMENT);
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObject(*global);
|
|
return dbg->wrapDebuggeeValue(cx, args.rval());
|
|
}
|
|
|
|
bool Debugger::isCompilableUnit(JSContext* cx, unsigned argc, Value* vp) {
|
|
CallArgs args = CallArgsFromVp(argc, vp);
|
|
|
|
if (!args.requireAtLeast(cx, "Debugger.isCompilableUnit", 1)) {
|
|
return false;
|
|
}
|
|
|
|
if (!args[0].isString()) {
|
|
JS_ReportErrorNumberASCII(
|
|
cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE,
|
|
"Debugger.isCompilableUnit", "string", InformalValueTypeName(args[0]));
|
|
return false;
|
|
}
|
|
|
|
JSString* str = args[0].toString();
|
|
size_t length = str->length();
|
|
|
|
AutoStableStringChars chars(cx);
|
|
if (!chars.initTwoByte(cx, str)) {
|
|
return false;
|
|
}
|
|
|
|
bool result = true;
|
|
|
|
CompileOptions options(cx);
|
|
frontend::UsedNameTracker usedNames(cx);
|
|
|
|
RootedScriptSourceObject sourceObject(
|
|
cx, frontend::CreateScriptSourceObject(cx, options, Nothing()));
|
|
if (!sourceObject) {
|
|
return false;
|
|
}
|
|
|
|
JS::AutoSuppressWarningReporter suppressWarnings(cx);
|
|
frontend::Parser<frontend::FullParseHandler, char16_t> parser(
|
|
cx, cx->tempLifoAlloc(), options, chars.twoByteChars(), length,
|
|
/* foldConstants = */ true, usedNames, nullptr, nullptr, sourceObject,
|
|
frontend::ParseGoal::Script);
|
|
if (!parser.checkOptions() || !parser.parse()) {
|
|
// We ran into an error. If it was because we ran out of memory we report
|
|
// it in the usual way.
|
|
if (cx->isThrowingOutOfMemory()) {
|
|
return false;
|
|
}
|
|
|
|
// If it was because we ran out of source, we return false so our caller
|
|
// knows to try to collect more [source].
|
|
if (parser.isUnexpectedEOF()) {
|
|
result = false;
|
|
}
|
|
|
|
cx->clearPendingException();
|
|
}
|
|
|
|
args.rval().setBoolean(result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool Debugger::recordReplayProcessKind(JSContext* cx,
|
|
unsigned argc, Value* vp) {
|
|
CallArgs args = CallArgsFromVp(argc, vp);
|
|
|
|
if (mozilla::recordreplay::IsMiddleman()) {
|
|
JSString* str = JS_NewStringCopyZ(cx, "Middleman");
|
|
if (!str) {
|
|
return false;
|
|
}
|
|
args.rval().setString(str);
|
|
} else if (mozilla::recordreplay::IsRecordingOrReplaying()) {
|
|
JSString* str = JS_NewStringCopyZ(cx, "RecordingReplaying");
|
|
if (!str) {
|
|
return false;
|
|
}
|
|
args.rval().setString(str);
|
|
} else {
|
|
args.rval().setUndefined();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Debugger::adoptDebuggeeValue(JSContext* cx, unsigned argc, Value* vp) {
|
|
THIS_DEBUGGER(cx, argc, vp, "adoptDebuggeeValue", args, dbg);
|
|
if (!args.requireAtLeast(cx, "Debugger.adoptDebuggeeValue", 1)) {
|
|
return false;
|
|
}
|
|
|
|
RootedValue v(cx, args[0]);
|
|
if (v.isObject()) {
|
|
RootedObject obj(cx, &v.toObject());
|
|
NativeObject* ndobj = ToNativeDebuggerObject(cx, &obj);
|
|
if (!ndobj) {
|
|
return false;
|
|
}
|
|
|
|
obj.set(static_cast<JSObject*>(ndobj->getPrivate()));
|
|
v = ObjectValue(*obj);
|
|
|
|
if (!dbg->wrapDebuggeeValue(cx, &v)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
args.rval().set(v);
|
|
return true;
|
|
}
|
|
|
|
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_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("findSources", Debugger::findSources, 1, 0),
|
|
JS_FN("findObjects", Debugger::findObjects, 1, 0),
|
|
JS_FN("findAllGlobals", Debugger::findAllGlobals, 0, 0),
|
|
JS_FN("makeGlobalObjectReference", Debugger::makeGlobalObjectReference, 1,
|
|
0),
|
|
JS_FN("adoptDebuggeeValue", Debugger::adoptDebuggeeValue, 1, 0),
|
|
JS_FS_END};
|
|
|
|
const JSFunctionSpec Debugger::static_methods[]{
|
|
JS_FN("isCompilableUnit", Debugger::isCompilableUnit, 1, 0),
|
|
JS_FN("recordReplayProcessKind", Debugger::recordReplayProcessKind, 0, 0),
|
|
JS_FS_END};
|
|
|
|
/*** Debugger.Script ********************************************************/
|
|
|
|
// Get the Debugger.Script referent as bare Cell. This should only be used for
|
|
// GC operations like tracing. Please use GetScriptReferent below.
|
|
static inline gc::Cell* GetScriptReferentCell(JSObject* obj) {
|
|
MOZ_ASSERT(obj->getClass() == &DebuggerScript_class);
|
|
return static_cast<gc::Cell*>(obj->as<NativeObject>().getPrivate());
|
|
}
|
|
|
|
static inline DebuggerScriptReferent GetScriptReferent(JSObject* obj) {
|
|
MOZ_ASSERT(obj->getClass() == &DebuggerScript_class);
|
|
if (gc::Cell* cell = GetScriptReferentCell(obj)) {
|
|
if (cell->is<JSScript>()) {
|
|
return AsVariant(cell->as<JSScript>());
|
|
}
|
|
if (cell->is<LazyScript>()) {
|
|
return AsVariant(cell->as<LazyScript>());
|
|
}
|
|
MOZ_ASSERT(cell->is<JSObject>());
|
|
return AsVariant(
|
|
&static_cast<NativeObject*>(cell)->as<WasmInstanceObject>());
|
|
}
|
|
return AsVariant(static_cast<JSScript*>(nullptr));
|
|
}
|
|
|
|
void DebuggerScript_trace(JSTracer* trc, JSObject* obj) {
|
|
// This comes from a private pointer, so no barrier needed.
|
|
gc::Cell* cell = GetScriptReferentCell(obj);
|
|
if (cell) {
|
|
if (cell->is<JSScript>()) {
|
|
JSScript* script = cell->as<JSScript>();
|
|
TraceManuallyBarrieredCrossCompartmentEdge(
|
|
trc, obj, &script, "Debugger.Script script referent");
|
|
obj->as<NativeObject>().setPrivateUnbarriered(script);
|
|
} else if (cell->is<LazyScript>()) {
|
|
LazyScript* lazyScript = cell->as<LazyScript>();
|
|
TraceManuallyBarrieredCrossCompartmentEdge(
|
|
trc, obj, &lazyScript, "Debugger.Script lazy script referent");
|
|
obj->as<NativeObject>().setPrivateUnbarriered(lazyScript);
|
|
} else {
|
|
JSObject* wasm = cell->as<JSObject>();
|
|
TraceManuallyBarrieredCrossCompartmentEdge(
|
|
trc, obj, &wasm, "Debugger.Script wasm referent");
|
|
MOZ_ASSERT(wasm->is<WasmInstanceObject>());
|
|
obj->as<NativeObject>().setPrivateUnbarriered(wasm);
|
|
}
|
|
}
|
|
}
|
|
|
|
static JSScript* DelazifyScript(JSContext* cx, Handle<LazyScript*> lazyScript) {
|
|
if (lazyScript->maybeScript()) {
|
|
return lazyScript->maybeScript();
|
|
}
|
|
|
|
// JSFunction::getOrCreateScript requires the enclosing script not to be
|
|
// lazified.
|
|
MOZ_ASSERT(lazyScript->hasEnclosingLazyScript() ||
|
|
lazyScript->hasEnclosingScope());
|
|
if (lazyScript->hasEnclosingLazyScript()) {
|
|
Rooted<LazyScript*> enclosingLazyScript(cx,
|
|
lazyScript->enclosingLazyScript());
|
|
if (!DelazifyScript(cx, enclosingLazyScript)) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (!lazyScript->enclosingScriptHasEverBeenCompiled()) {
|
|
// It didn't work! Delazifying the enclosing script still didn't
|
|
// delazify this script. This happens when the function
|
|
// corresponding to this script was removed by constant folding.
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_OPTIMIZED_OUT_FUN);
|
|
return nullptr;
|
|
}
|
|
}
|
|
MOZ_ASSERT(lazyScript->enclosingScriptHasEverBeenCompiled());
|
|
|
|
RootedFunction fun0(cx, lazyScript->functionNonDelazifying());
|
|
AutoRealm ar(cx, fun0);
|
|
RootedFunction fun(cx, LazyScript::functionDelazifying(cx, lazyScript));
|
|
if (!fun) {
|
|
return nullptr;
|
|
}
|
|
return fun->getOrCreateScript(cx, fun);
|
|
}
|
|
|
|
class DebuggerScriptSetPrivateMatcher {
|
|
NativeObject* obj_;
|
|
|
|
public:
|
|
explicit DebuggerScriptSetPrivateMatcher(NativeObject* obj) : obj_(obj) {}
|
|
using ReturnType = void;
|
|
ReturnType match(HandleScript script) { obj_->setPrivateGCThing(script); }
|
|
ReturnType match(Handle<LazyScript*> lazyScript) {
|
|
obj_->setPrivateGCThing(lazyScript);
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> instance) {
|
|
obj_->setPrivateGCThing(instance);
|
|
}
|
|
};
|
|
|
|
NativeObject* Debugger::newDebuggerScript(
|
|
JSContext* cx, Handle<DebuggerScriptReferent> referent) {
|
|
cx->check(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));
|
|
DebuggerScriptSetPrivateMatcher matcher(scriptobj);
|
|
referent.match(matcher);
|
|
|
|
return scriptobj;
|
|
}
|
|
|
|
template <typename ReferentVariant, typename Referent, typename Map>
|
|
JSObject* Debugger::wrapVariantReferent(JSContext* cx, Map& map,
|
|
Handle<CrossCompartmentKey> key,
|
|
Handle<ReferentVariant> referent) {
|
|
cx->check(object);
|
|
|
|
Handle<Referent> untaggedReferent = referent.template as<Referent>();
|
|
MOZ_ASSERT(cx->compartment() != untaggedReferent->compartment());
|
|
|
|
DependentAddPtr<Map> p(cx, map, untaggedReferent);
|
|
if (!p) {
|
|
NativeObject* wrapper = newVariantWrapper(cx, referent);
|
|
if (!wrapper) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (!p.add(cx, map, untaggedReferent, wrapper)) {
|
|
NukeDebuggerWrapper(wrapper);
|
|
return nullptr;
|
|
}
|
|
|
|
if (!object->compartment()->putWrapper(cx, key, ObjectValue(*wrapper))) {
|
|
NukeDebuggerWrapper(wrapper);
|
|
map.remove(untaggedReferent);
|
|
ReportOutOfMemory(cx);
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
return p->value();
|
|
}
|
|
|
|
JSObject* Debugger::wrapVariantReferent(
|
|
JSContext* cx, Handle<DebuggerScriptReferent> referent) {
|
|
JSObject* obj;
|
|
if (referent.is<JSScript*>()) {
|
|
Handle<JSScript*> untaggedReferent = referent.template as<JSScript*>();
|
|
if (untaggedReferent->maybeLazyScript()) {
|
|
// If the JSScript has corresponding LazyScript, wrap the LazyScript
|
|
// instead.
|
|
//
|
|
// This is necessary for Debugger.Script identity. If we use both
|
|
// JSScript and LazyScript for same single script, those 2 wrapped
|
|
// scripts become not identical, while the referent script is
|
|
// actually identical.
|
|
//
|
|
// If a script has corresponding LazyScript and JSScript, the
|
|
// lifetime of the LazyScript is always longer than the JSScript.
|
|
// So we can use the LazyScript as a proxy for the JSScript.
|
|
Rooted<LazyScript*> lazyScript(cx, untaggedReferent->maybeLazyScript());
|
|
Rooted<DebuggerScriptReferent> lazyScriptReferent(cx, lazyScript.get());
|
|
|
|
Rooted<CrossCompartmentKey> key(cx,
|
|
CrossCompartmentKey(object, lazyScript));
|
|
obj = wrapVariantReferent<DebuggerScriptReferent, LazyScript*,
|
|
LazyScriptWeakMap>(cx, lazyScripts, key,
|
|
lazyScriptReferent);
|
|
MOZ_ASSERT_IF(obj, GetScriptReferent(obj) == lazyScriptReferent);
|
|
return obj;
|
|
} else {
|
|
// If the JSScript doesn't have corresponding LazyScript, the script
|
|
// is not lazifiable, and we can safely use JSScript as referent.
|
|
Rooted<CrossCompartmentKey> key(
|
|
cx, CrossCompartmentKey(object, untaggedReferent));
|
|
obj =
|
|
wrapVariantReferent<DebuggerScriptReferent, JSScript*, ScriptWeakMap>(
|
|
cx, scripts, key, referent);
|
|
}
|
|
} else if (referent.is<LazyScript*>()) {
|
|
Handle<LazyScript*> untaggedReferent = referent.template as<LazyScript*>();
|
|
Rooted<CrossCompartmentKey> key(
|
|
cx, CrossCompartmentKey(object, untaggedReferent));
|
|
obj =
|
|
wrapVariantReferent<DebuggerScriptReferent, LazyScript*,
|
|
LazyScriptWeakMap>(cx, lazyScripts, key, referent);
|
|
} else {
|
|
Handle<WasmInstanceObject*> untaggedReferent =
|
|
referent.template as<WasmInstanceObject*>();
|
|
Rooted<CrossCompartmentKey> key(
|
|
cx, CrossCompartmentKey(
|
|
object, untaggedReferent,
|
|
CrossCompartmentKey::DebuggerObjectKind::DebuggerWasmScript));
|
|
obj = wrapVariantReferent<DebuggerScriptReferent, WasmInstanceObject*,
|
|
WasmInstanceWeakMap>(cx, wasmInstanceScripts, key,
|
|
referent);
|
|
}
|
|
MOZ_ASSERT_IF(obj, GetScriptReferent(obj) == referent);
|
|
return obj;
|
|
}
|
|
|
|
JSObject* Debugger::wrapScript(JSContext* cx, HandleScript script) {
|
|
Rooted<DebuggerScriptReferent> referent(cx, script.get());
|
|
return wrapVariantReferent(cx, referent);
|
|
}
|
|
|
|
JSObject* Debugger::wrapLazyScript(JSContext* cx,
|
|
Handle<LazyScript*> lazyScript) {
|
|
Rooted<DebuggerScriptReferent> referent(cx, lazyScript.get());
|
|
return wrapVariantReferent(cx, referent);
|
|
}
|
|
|
|
JSObject* Debugger::wrapWasmScript(JSContext* cx,
|
|
Handle<WasmInstanceObject*> wasmInstance) {
|
|
Rooted<DebuggerScriptReferent> referent(cx, wasmInstance.get());
|
|
return wrapVariantReferent(cx, referent);
|
|
}
|
|
|
|
static JSObject* DebuggerScript_check(JSContext* cx, HandleValue v,
|
|
const char* fnname) {
|
|
JSObject* thisobj = NonNullObject(cx, v);
|
|
if (!thisobj) {
|
|
return nullptr;
|
|
}
|
|
if (thisobj->getClass() != &DebuggerScript_class) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_INCOMPATIBLE_PROTO, "Debugger.Script",
|
|
fnname, thisobj->getClass()->name);
|
|
return nullptr;
|
|
}
|
|
|
|
// Check for Debugger.Script.prototype, which is of class DebuggerScript_class
|
|
// but whose script is null.
|
|
if (!GetScriptReferentCell(thisobj)) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_INCOMPATIBLE_PROTO, "Debugger.Script",
|
|
fnname, "prototype object");
|
|
return nullptr;
|
|
}
|
|
|
|
return thisobj;
|
|
}
|
|
|
|
static JSObject* DebuggerScript_checkThis(JSContext* cx, const CallArgs& args,
|
|
const char* fnname) {
|
|
JSObject* thisobj = DebuggerScript_check(cx, args.thisv(), fnname);
|
|
if (!thisobj) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (!GetScriptReferent(thisobj).is<JSScript*>() &&
|
|
!GetScriptReferent(thisobj).is<LazyScript*>()) {
|
|
ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK,
|
|
args.thisv(), nullptr, "a JS script");
|
|
return nullptr;
|
|
}
|
|
|
|
return thisobj;
|
|
}
|
|
|
|
#define THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, fnname, args, obj, referent) \
|
|
CallArgs args = CallArgsFromVp(argc, vp); \
|
|
RootedObject obj(cx, DebuggerScript_check(cx, args.thisv(), fnname)); \
|
|
if (!obj) return false; \
|
|
Rooted<DebuggerScriptReferent> referent(cx, GetScriptReferent(obj))
|
|
|
|
#define THIS_DEBUGSCRIPT_SCRIPT_MAYBE_LAZY(cx, argc, vp, fnname, args, obj) \
|
|
CallArgs args = CallArgsFromVp(argc, vp); \
|
|
RootedObject obj(cx, DebuggerScript_checkThis(cx, args, fnname)); \
|
|
if (!obj) return false;
|
|
|
|
#define THIS_DEBUGSCRIPT_SCRIPT_DELAZIFY(cx, argc, vp, fnname, args, obj, \
|
|
script) \
|
|
THIS_DEBUGSCRIPT_SCRIPT_MAYBE_LAZY(cx, argc, vp, fnname, args, obj); \
|
|
RootedScript script(cx); \
|
|
if (GetScriptReferent(obj).is<JSScript*>()) { \
|
|
script = GetScriptReferent(obj).as<JSScript*>(); \
|
|
} else { \
|
|
Rooted<LazyScript*> lazyScript(cx, \
|
|
GetScriptReferent(obj).as<LazyScript*>()); \
|
|
script = DelazifyScript(cx, lazyScript); \
|
|
if (!script) return false; \
|
|
}
|
|
|
|
template <typename Result>
|
|
Result CallScriptMethod(HandleObject obj,
|
|
Result (JSScript::*ifJSScript)() const,
|
|
Result (LazyScript::*ifLazyScript)() const) {
|
|
if (GetScriptReferent(obj).is<JSScript*>()) {
|
|
JSScript* script = GetScriptReferent(obj).as<JSScript*>();
|
|
return (script->*ifJSScript)();
|
|
}
|
|
|
|
LazyScript* lazyScript = GetScriptReferent(obj).as<LazyScript*>();
|
|
return (lazyScript->*ifLazyScript)();
|
|
}
|
|
|
|
static bool DebuggerScript_getIsGeneratorFunction(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_SCRIPT_MAYBE_LAZY(cx, argc, vp, "(get isGeneratorFunction)",
|
|
args, obj);
|
|
args.rval().setBoolean(
|
|
CallScriptMethod(obj, &JSScript::isGenerator, &LazyScript::isGenerator));
|
|
return true;
|
|
}
|
|
|
|
static bool DebuggerScript_getIsAsyncFunction(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_SCRIPT_MAYBE_LAZY(cx, argc, vp, "(get isAsyncFunction)",
|
|
args, obj);
|
|
args.rval().setBoolean(
|
|
CallScriptMethod(obj, &JSScript::isAsync, &LazyScript::isAsync));
|
|
return true;
|
|
}
|
|
|
|
static bool DebuggerScript_getIsModule(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_SCRIPT_MAYBE_LAZY(cx, argc, vp, "(get isModule)", args, obj);
|
|
DebuggerScriptReferent referent = GetScriptReferent(obj);
|
|
args.rval().setBoolean(referent.is<JSScript*>() &&
|
|
referent.as<JSScript*>()->isModule());
|
|
return true;
|
|
}
|
|
|
|
static bool DebuggerScript_getDisplayName(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_SCRIPT_MAYBE_LAZY(cx, argc, vp, "(get displayName)", args,
|
|
obj);
|
|
JSFunction* func = CallScriptMethod(obj, &JSScript::functionNonDelazifying,
|
|
&LazyScript::functionNonDelazifying);
|
|
Debugger* dbg = Debugger::fromChildJSObject(obj);
|
|
|
|
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;
|
|
}
|
|
|
|
template <typename T>
|
|
static bool DebuggerScript_getUrlImpl(JSContext* cx, CallArgs& args,
|
|
Handle<T*> 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_getUrl(JSContext* cx, unsigned argc, Value* vp) {
|
|
THIS_DEBUGSCRIPT_SCRIPT_MAYBE_LAZY(cx, argc, vp, "(get url)", args, obj);
|
|
if (GetScriptReferent(obj).is<JSScript*>()) {
|
|
RootedScript script(cx, GetScriptReferent(obj).as<JSScript*>());
|
|
return DebuggerScript_getUrlImpl<JSScript>(cx, args, script);
|
|
}
|
|
|
|
Rooted<LazyScript*> lazyScript(cx, GetScriptReferent(obj).as<LazyScript*>());
|
|
return DebuggerScript_getUrlImpl<LazyScript>(cx, args, lazyScript);
|
|
}
|
|
|
|
struct DebuggerScriptGetStartLineMatcher {
|
|
using ReturnType = uint32_t;
|
|
|
|
ReturnType match(HandleScript script) { return script->lineno(); }
|
|
ReturnType match(Handle<LazyScript*> lazyScript) {
|
|
return lazyScript->lineno();
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { return 1; }
|
|
};
|
|
|
|
static bool DebuggerScript_getStartLine(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "(get startLine)", args, obj,
|
|
referent);
|
|
DebuggerScriptGetStartLineMatcher matcher;
|
|
args.rval().setNumber(referent.match(matcher));
|
|
return true;
|
|
}
|
|
|
|
struct DebuggerScriptGetLineCountMatcher {
|
|
JSContext* cx_;
|
|
double totalLines;
|
|
|
|
explicit DebuggerScriptGetLineCountMatcher(JSContext* cx)
|
|
: cx_(cx), totalLines(0.0) {}
|
|
using ReturnType = bool;
|
|
|
|
ReturnType match(HandleScript script) {
|
|
totalLines = double(GetScriptLineExtent(script));
|
|
return true;
|
|
}
|
|
ReturnType match(Handle<LazyScript*> lazyScript) {
|
|
RootedScript script(cx_, DelazifyScript(cx_, lazyScript));
|
|
if (!script) {
|
|
return false;
|
|
}
|
|
return match(script);
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
|
|
wasm::Instance& instance = instanceObj->instance();
|
|
if (instance.debugEnabled()) {
|
|
totalLines = double(instance.debug().bytecode().length());
|
|
} else {
|
|
totalLines = 0;
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
static bool DebuggerScript_getLineCount(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "(get lineCount)", args, obj,
|
|
referent);
|
|
DebuggerScriptGetLineCountMatcher matcher(cx);
|
|
if (!referent.match(matcher)) {
|
|
return false;
|
|
}
|
|
args.rval().setNumber(matcher.totalLines);
|
|
return true;
|
|
}
|
|
|
|
class DebuggerScriptGetSourceMatcher {
|
|
JSContext* cx_;
|
|
Debugger* dbg_;
|
|
|
|
public:
|
|
DebuggerScriptGetSourceMatcher(JSContext* cx, Debugger* dbg)
|
|
: cx_(cx), dbg_(dbg) {}
|
|
|
|
using ReturnType = JSObject*;
|
|
|
|
ReturnType match(HandleScript script) {
|
|
// JSScript holds the refefence to possibly wrapped ScriptSourceObject.
|
|
// It's wrapped when the script is cloned.
|
|
// See CreateEmptyScriptForClone for more info.
|
|
RootedScriptSourceObject source(
|
|
cx_,
|
|
&UncheckedUnwrap(script->sourceObject())->as<ScriptSourceObject>());
|
|
return dbg_->wrapSource(cx_, source);
|
|
}
|
|
ReturnType match(Handle<LazyScript*> lazyScript) {
|
|
// LazyScript holds the reference to the unwrapped ScriptSourceObject.
|
|
RootedScriptSourceObject source(cx_, &lazyScript->sourceObject());
|
|
return dbg_->wrapSource(cx_, source);
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> wasmInstance) {
|
|
return dbg_->wrapWasmSource(cx_, wasmInstance);
|
|
}
|
|
};
|
|
|
|
static bool DebuggerScript_getSource(JSContext* cx, unsigned argc, Value* vp) {
|
|
THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "(get source)", args, obj, referent);
|
|
Debugger* dbg = Debugger::fromChildJSObject(obj);
|
|
|
|
DebuggerScriptGetSourceMatcher matcher(cx, dbg);
|
|
RootedObject sourceObject(cx, referent.match(matcher));
|
|
if (!sourceObject) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObject(*sourceObject);
|
|
return true;
|
|
}
|
|
|
|
static bool DebuggerScript_getSourceStart(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_SCRIPT_MAYBE_LAZY(cx, argc, vp, "(get sourceStart)", args,
|
|
obj);
|
|
args.rval().setNumber(uint32_t(
|
|
CallScriptMethod(obj, &JSScript::sourceStart, &LazyScript::sourceStart)));
|
|
return true;
|
|
}
|
|
|
|
static bool DebuggerScript_getSourceLength(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_SCRIPT_MAYBE_LAZY(cx, argc, vp, "(get sourceEnd)", args,
|
|
obj);
|
|
args.rval().setNumber(uint32_t(CallScriptMethod(obj, &JSScript::sourceLength,
|
|
&LazyScript::sourceLength)));
|
|
return true;
|
|
}
|
|
|
|
static bool DebuggerScript_getMainOffset(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_SCRIPT_DELAZIFY(cx, argc, vp, "(get mainOffset)", args, obj,
|
|
script);
|
|
args.rval().setNumber(uint32_t(script->mainOffset()));
|
|
return true;
|
|
}
|
|
|
|
static bool DebuggerScript_getGlobal(JSContext* cx, unsigned argc, Value* vp) {
|
|
THIS_DEBUGSCRIPT_SCRIPT_DELAZIFY(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;
|
|
}
|
|
|
|
class DebuggerScriptGetFormatMatcher {
|
|
const JSAtomState& names_;
|
|
|
|
public:
|
|
explicit DebuggerScriptGetFormatMatcher(const JSAtomState& names)
|
|
: names_(names) {}
|
|
using ReturnType = JSAtom*;
|
|
ReturnType match(HandleScript script) { return names_.js; }
|
|
ReturnType match(Handle<LazyScript*> lazyScript) { return names_.js; }
|
|
ReturnType match(Handle<WasmInstanceObject*> wasmInstance) {
|
|
return names_.wasm;
|
|
}
|
|
};
|
|
|
|
static bool DebuggerScript_getFormat(JSContext* cx, unsigned argc, Value* vp) {
|
|
THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "(get format)", args, obj, referent);
|
|
DebuggerScriptGetFormatMatcher matcher(cx->names());
|
|
args.rval().setString(referent.match(matcher));
|
|
return true;
|
|
}
|
|
|
|
static bool DebuggerScript_getChildScripts(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_SCRIPT_DELAZIFY(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().
|
|
RootedFunction fun(cx);
|
|
RootedScript funScript(cx);
|
|
RootedObject s(cx);
|
|
for (const GCPtrObject& obj : script->objects()) {
|
|
if (obj->is<JSFunction>()) {
|
|
fun = &obj->as<JSFunction>();
|
|
// The inner function could be a wasm 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, 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) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_BAD_OFFSET);
|
|
return false;
|
|
}
|
|
*offsetp = off;
|
|
return true;
|
|
}
|
|
|
|
static bool EnsureScriptOffsetIsValid(JSContext* cx, JSScript* script,
|
|
size_t offset) {
|
|
if (IsValidBytecodeOffset(cx, script, offset)) {
|
|
return true;
|
|
}
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_BAD_OFFSET);
|
|
return false;
|
|
}
|
|
|
|
namespace {
|
|
|
|
/*
|
|
* 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:
|
|
* 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.
|
|
*
|
|
* 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.
|
|
*/
|
|
class FlowGraphSummary {
|
|
public:
|
|
class Entry {
|
|
public:
|
|
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() : lineno_(SIZE_MAX), column_(0) {}
|
|
|
|
bool hasNoEdges() const {
|
|
return lineno_ == SIZE_MAX && column_ != SIZE_MAX;
|
|
}
|
|
|
|
bool hasSingleEdge() const {
|
|
return lineno_ != SIZE_MAX && column_ != SIZE_MAX;
|
|
}
|
|
|
|
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();
|
|
|
|
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 = prevLineno;
|
|
size_t column = prevColumn;
|
|
JSOp op = r.frontOpcode();
|
|
|
|
if (FlowsIntoNext(prevOp)) {
|
|
addEdge(prevLineno, prevColumn, r.frontOffset());
|
|
}
|
|
|
|
// If we visit the branch target before we visit the
|
|
// branch op itself, just reuse the previous location.
|
|
// This is reasonable for the time being because this
|
|
// situation can currently only arise from loop heads,
|
|
// where this assumption holds.
|
|
if (BytecodeIsJumpTarget(op) && !entries_[r.frontOffset()].hasNoEdges()) {
|
|
lineno = entries_[r.frontOffset()].lineno();
|
|
column = entries_[r.frontOffset()].column();
|
|
}
|
|
|
|
if (r.frontIsEntryPoint()) {
|
|
lineno = r.frontLineNumber();
|
|
column = r.frontColumnNumber();
|
|
}
|
|
|
|
if (CodeSpec[op].type() == JOF_JUMP) {
|
|
addEdge(lineno, column, r.frontOffset() + GET_JUMP_OFFSET(r.frontPC()));
|
|
} else if (op == JSOP_TABLESWITCH) {
|
|
jsbytecode* const switchPC = r.frontPC();
|
|
jsbytecode* pc = switchPC;
|
|
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 = script->tableSwitchCaseOffset(switchPC, i);
|
|
addEdge(lineno, column, target);
|
|
}
|
|
} else if (op == JSOP_TRY) {
|
|
// As there is no literal incoming edge into the catch block, we
|
|
// make a fake one by copying the JSOP_TRY location, as-if this
|
|
// was an incoming edge of the catch block. This is needed
|
|
// because we only report offsets of entry points which have
|
|
// valid incoming edges.
|
|
for (const JSTryNote& tn : script->trynotes()) {
|
|
if (tn.start == r.frontOffset() + 1) {
|
|
uint32_t catchOffset = tn.start + tn.length;
|
|
if (tn.kind == JSTRY_CATCH || tn.kind == JSTRY_FINALLY) {
|
|
addEdge(lineno, column, catchOffset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 */
|
|
|
|
class DebuggerScriptGetOffsetLocationMatcher {
|
|
JSContext* cx_;
|
|
size_t offset_;
|
|
MutableHandlePlainObject result_;
|
|
|
|
public:
|
|
explicit DebuggerScriptGetOffsetLocationMatcher(
|
|
JSContext* cx, size_t offset, MutableHandlePlainObject result)
|
|
: cx_(cx), offset_(offset), result_(result) {}
|
|
using ReturnType = bool;
|
|
ReturnType match(HandleScript script) {
|
|
if (!EnsureScriptOffsetIsValid(cx_, script, offset_)) {
|
|
return false;
|
|
}
|
|
|
|
FlowGraphSummary flowData(cx_);
|
|
if (!flowData.populate(cx_, script)) {
|
|
return false;
|
|
}
|
|
|
|
result_.set(NewBuiltinClassInstance<PlainObject>(cx_));
|
|
if (!result_) {
|
|
return false;
|
|
}
|
|
|
|
BytecodeRangeWithPosition r(cx_, script);
|
|
while (!r.empty() && r.frontOffset() < offset_) {
|
|
r.popFront();
|
|
}
|
|
|
|
size_t offset = r.frontOffset();
|
|
bool isEntryPoint = r.frontIsEntryPoint();
|
|
|
|
// Line numbers are only correctly defined on entry points. Thus looks
|
|
// either for the next valid offset in the flowData, being the last entry
|
|
// point flowing into the current offset, or for the next valid entry point.
|
|
while (!r.frontIsEntryPoint() &&
|
|
!flowData[r.frontOffset()].hasSingleEdge()) {
|
|
r.popFront();
|
|
MOZ_ASSERT(!r.empty());
|
|
}
|
|
|
|
// If this is an entry point, take the line number associated with the entry
|
|
// point, otherwise settle on the next instruction and take the incoming
|
|
// edge position.
|
|
size_t lineno;
|
|
size_t column;
|
|
if (r.frontIsEntryPoint()) {
|
|
lineno = r.frontLineNumber();
|
|
column = r.frontColumnNumber();
|
|
} else {
|
|
MOZ_ASSERT(flowData[r.frontOffset()].hasSingleEdge());
|
|
lineno = flowData[r.frontOffset()].lineno();
|
|
column = flowData[r.frontOffset()].column();
|
|
}
|
|
|
|
RootedId id(cx_, NameToId(cx_->names().lineNumber));
|
|
RootedValue value(cx_, NumberValue(lineno));
|
|
if (!DefineDataProperty(cx_, result_, id, value)) {
|
|
return false;
|
|
}
|
|
|
|
value = NumberValue(column);
|
|
if (!DefineDataProperty(cx_, result_, cx_->names().columnNumber, value)) {
|
|
return false;
|
|
}
|
|
|
|
// The same entry point test that is used by getAllColumnOffsets.
|
|
isEntryPoint = (isEntryPoint && !flowData[offset].hasNoEdges() &&
|
|
(flowData[offset].lineno() != r.frontLineNumber() ||
|
|
flowData[offset].column() != r.frontColumnNumber()));
|
|
value.setBoolean(isEntryPoint);
|
|
if (!DefineDataProperty(cx_, result_, cx_->names().isEntryPoint, value)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
ReturnType match(Handle<LazyScript*> lazyScript) {
|
|
RootedScript script(cx_, DelazifyScript(cx_, lazyScript));
|
|
if (!script) {
|
|
return false;
|
|
}
|
|
return match(script);
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
|
|
wasm::Instance& instance = instanceObj->instance();
|
|
if (!instance.debugEnabled()) {
|
|
JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_BAD_OFFSET);
|
|
return false;
|
|
}
|
|
|
|
size_t lineno;
|
|
size_t column;
|
|
if (!instance.debug().getOffsetLocation(offset_, &lineno, &column)) {
|
|
JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_BAD_OFFSET);
|
|
return false;
|
|
}
|
|
|
|
result_.set(NewBuiltinClassInstance<PlainObject>(cx_));
|
|
if (!result_) {
|
|
return false;
|
|
}
|
|
|
|
RootedId id(cx_, NameToId(cx_->names().lineNumber));
|
|
RootedValue value(cx_, NumberValue(lineno));
|
|
if (!DefineDataProperty(cx_, result_, id, value)) {
|
|
return false;
|
|
}
|
|
|
|
value = NumberValue(column);
|
|
if (!DefineDataProperty(cx_, result_, cx_->names().columnNumber, value)) {
|
|
return false;
|
|
}
|
|
|
|
value.setBoolean(true);
|
|
if (!DefineDataProperty(cx_, result_, cx_->names().isEntryPoint, value)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
static bool DebuggerScript_getOffsetLocation(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "getOffsetLocation", args, obj,
|
|
referent);
|
|
if (!args.requireAtLeast(cx, "Debugger.Script.getOffsetLocation", 1)) {
|
|
return false;
|
|
}
|
|
size_t offset;
|
|
if (!ScriptOffset(cx, args[0], &offset)) {
|
|
return false;
|
|
}
|
|
|
|
RootedPlainObject result(cx);
|
|
DebuggerScriptGetOffsetLocationMatcher matcher(cx, offset, &result);
|
|
if (!referent.match(matcher)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObject(*result);
|
|
return true;
|
|
}
|
|
|
|
class DebuggerScriptGetSuccessorOrPredecessorOffsetsMatcher {
|
|
JSContext* cx_;
|
|
size_t offset_;
|
|
bool successor_;
|
|
MutableHandleObject result_;
|
|
|
|
public:
|
|
DebuggerScriptGetSuccessorOrPredecessorOffsetsMatcher(
|
|
JSContext* cx, size_t offset, bool successor, MutableHandleObject result)
|
|
: cx_(cx), offset_(offset), successor_(successor), result_(result) {}
|
|
|
|
using ReturnType = bool;
|
|
|
|
ReturnType match(HandleScript script) {
|
|
if (!EnsureScriptOffsetIsValid(cx_, script, offset_)) {
|
|
return false;
|
|
}
|
|
|
|
PcVector adjacent;
|
|
if (successor_) {
|
|
if (!GetSuccessorBytecodes(script, script->code() + offset_, adjacent)) {
|
|
ReportOutOfMemory(cx_);
|
|
return false;
|
|
}
|
|
} else {
|
|
if (!GetPredecessorBytecodes(script, script->code() + offset_,
|
|
adjacent)) {
|
|
ReportOutOfMemory(cx_);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
result_.set(NewDenseEmptyArray(cx_));
|
|
if (!result_) {
|
|
return false;
|
|
}
|
|
|
|
for (jsbytecode* pc : adjacent) {
|
|
if (!NewbornArrayPush(cx_, result_, NumberValue(pc - script->code()))) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
ReturnType match(Handle<LazyScript*> lazyScript) {
|
|
RootedScript script(cx_, DelazifyScript(cx_, lazyScript));
|
|
if (!script) {
|
|
return false;
|
|
}
|
|
return match(script);
|
|
}
|
|
|
|
ReturnType match(Handle<WasmInstanceObject*> instance) {
|
|
JS_ReportErrorASCII(
|
|
cx_, "getSuccessorOrPredecessorOffsets NYI on wasm instances");
|
|
return false;
|
|
}
|
|
};
|
|
|
|
static bool DebuggerScript_getSuccessorOrPredecessorOffsets(
|
|
JSContext* cx, unsigned argc, Value* vp, const char* name, bool successor) {
|
|
THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, name, args, obj, referent);
|
|
|
|
if (!args.requireAtLeast(cx, name, 1)) {
|
|
return false;
|
|
}
|
|
size_t offset;
|
|
if (!ScriptOffset(cx, args[0], &offset)) {
|
|
return false;
|
|
}
|
|
|
|
RootedObject result(cx);
|
|
DebuggerScriptGetSuccessorOrPredecessorOffsetsMatcher matcher(
|
|
cx, offset, successor, &result);
|
|
if (!referent.match(matcher)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObject(*result);
|
|
return true;
|
|
}
|
|
|
|
static bool DebuggerScript_getSuccessorOffsets(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
return DebuggerScript_getSuccessorOrPredecessorOffsets(
|
|
cx, argc, vp, "getSuccessorOffsets", true);
|
|
}
|
|
|
|
static bool DebuggerScript_getPredecessorOffsets(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
return DebuggerScript_getSuccessorOrPredecessorOffsets(
|
|
cx, argc, vp, "getPredecessorOffsets", false);
|
|
}
|
|
|
|
static bool DebuggerScript_getAllOffsets(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_SCRIPT_DELAZIFY(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 (!DefineDataProperty(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;
|
|
}
|
|
|
|
class DebuggerScriptGetAllColumnOffsetsMatcher {
|
|
JSContext* cx_;
|
|
MutableHandleObject result_;
|
|
|
|
bool appendColumnOffsetEntry(size_t lineno, size_t column, size_t offset) {
|
|
RootedPlainObject entry(cx_, NewBuiltinClassInstance<PlainObject>(cx_));
|
|
if (!entry) {
|
|
return false;
|
|
}
|
|
|
|
RootedId id(cx_, NameToId(cx_->names().lineNumber));
|
|
RootedValue value(cx_, NumberValue(lineno));
|
|
if (!DefineDataProperty(cx_, entry, id, value)) {
|
|
return false;
|
|
}
|
|
|
|
value = NumberValue(column);
|
|
if (!DefineDataProperty(cx_, entry, cx_->names().columnNumber, value)) {
|
|
return false;
|
|
}
|
|
|
|
id = NameToId(cx_->names().offset);
|
|
value = NumberValue(offset);
|
|
if (!DefineDataProperty(cx_, entry, id, value)) {
|
|
return false;
|
|
}
|
|
|
|
return NewbornArrayPush(cx_, result_, ObjectValue(*entry));
|
|
}
|
|
|
|
public:
|
|
explicit DebuggerScriptGetAllColumnOffsetsMatcher(JSContext* cx,
|
|
MutableHandleObject result)
|
|
: cx_(cx), result_(result) {}
|
|
using ReturnType = bool;
|
|
ReturnType match(HandleScript 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.
|
|
result_.set(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)) {
|
|
if (!appendColumnOffsetEntry(lineno, column, offset)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
ReturnType match(Handle<LazyScript*> lazyScript) {
|
|
RootedScript script(cx_, DelazifyScript(cx_, lazyScript));
|
|
if (!script) {
|
|
return false;
|
|
}
|
|
return match(script);
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
|
|
wasm::Instance& instance = instanceObj->instance();
|
|
|
|
Vector<wasm::ExprLoc> offsets(cx_);
|
|
if (instance.debugEnabled() &&
|
|
!instance.debug().getAllColumnOffsets(cx_, &offsets)) {
|
|
return false;
|
|
}
|
|
|
|
result_.set(NewDenseEmptyArray(cx_));
|
|
if (!result_) {
|
|
return false;
|
|
}
|
|
|
|
for (uint32_t i = 0; i < offsets.length(); i++) {
|
|
size_t lineno = offsets[i].lineno;
|
|
size_t column = offsets[i].column;
|
|
size_t offset = offsets[i].offset;
|
|
if (!appendColumnOffsetEntry(lineno, column, offset)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
static bool DebuggerScript_getAllColumnOffsets(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "getAllColumnOffsets", args, obj,
|
|
referent);
|
|
|
|
RootedObject result(cx);
|
|
DebuggerScriptGetAllColumnOffsetsMatcher matcher(cx, &result);
|
|
if (!referent.match(matcher)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObject(*result);
|
|
return true;
|
|
}
|
|
|
|
class DebuggerScriptGetLineOffsetsMatcher {
|
|
JSContext* cx_;
|
|
size_t lineno_;
|
|
MutableHandleObject result_;
|
|
|
|
public:
|
|
explicit DebuggerScriptGetLineOffsetsMatcher(JSContext* cx, size_t lineno,
|
|
MutableHandleObject result)
|
|
: cx_(cx), lineno_(lineno), result_(result) {}
|
|
using ReturnType = bool;
|
|
ReturnType match(HandleScript 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;
|
|
}
|
|
|
|
result_.set(NewDenseEmptyArray(cx_));
|
|
if (!result_) {
|
|
return false;
|
|
}
|
|
|
|
// Second pass: build the result array.
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
ReturnType match(Handle<LazyScript*> lazyScript) {
|
|
RootedScript script(cx_, DelazifyScript(cx_, lazyScript));
|
|
if (!script) {
|
|
return false;
|
|
}
|
|
return match(script);
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
|
|
wasm::Instance& instance = instanceObj->instance();
|
|
|
|
Vector<uint32_t> offsets(cx_);
|
|
if (instance.debugEnabled() &&
|
|
!instance.debug().getLineOffsets(cx_, lineno_, &offsets)) {
|
|
return false;
|
|
}
|
|
|
|
result_.set(NewDenseEmptyArray(cx_));
|
|
if (!result_) {
|
|
return false;
|
|
}
|
|
|
|
for (uint32_t i = 0; i < offsets.length(); i++) {
|
|
if (!NewbornArrayPush(cx_, result_, NumberValue(offsets[i]))) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
static bool DebuggerScript_getLineOffsets(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "getLineOffsets", args, obj,
|
|
referent);
|
|
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_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_BAD_LINE);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
RootedObject result(cx);
|
|
DebuggerScriptGetLineOffsetsMatcher matcher(cx, lineno, &result);
|
|
if (!referent.match(matcher)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObject(*result);
|
|
return true;
|
|
}
|
|
|
|
bool Debugger::observesFrame(AbstractFramePtr frame) const {
|
|
if (frame.isWasmDebugFrame()) {
|
|
return observesWasm(frame.wasmInstance());
|
|
}
|
|
|
|
return observesScript(frame.script());
|
|
}
|
|
|
|
bool Debugger::observesFrame(const FrameIter& iter) const {
|
|
// Skip frames not yet fully initialized during their prologue.
|
|
if (iter.isInterp() && iter.isFunctionFrame()) {
|
|
const Value& thisVal = iter.interpFrame()->thisArgument();
|
|
if (thisVal.isMagic() && thisVal.whyMagic() == JS_IS_CONSTRUCTING) {
|
|
return false;
|
|
}
|
|
}
|
|
if (iter.isWasm()) {
|
|
// Skip frame of wasm instances we cannot observe.
|
|
if (!iter.wasmDebugEnabled()) {
|
|
return false;
|
|
}
|
|
return observesWasm(iter.wasmInstance());
|
|
}
|
|
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();
|
|
}
|
|
|
|
bool Debugger::observesWasm(wasm::Instance* instance) const {
|
|
if (!enabled || !instance->debugEnabled()) {
|
|
return false;
|
|
}
|
|
return observesGlobal(&instance->object()->global());
|
|
}
|
|
|
|
/* static */ bool Debugger::replaceFrameGuts(JSContext* cx,
|
|
AbstractFramePtr from,
|
|
AbstractFramePtr to,
|
|
ScriptFrameIter& iter) {
|
|
auto removeFromDebuggerFramesOnExit = MakeScopeExit([&] {
|
|
// Remove any remaining old entries on exit, as the 'from' frame will
|
|
// be gone. This is only done in the failure case. On failure, the
|
|
// removeToDebuggerFramesOnExit lambda below will rollback any frames
|
|
// that were replaced, resulting in !frameMaps(to). On success, the
|
|
// range will be empty, as all from Frame.Debugger instances will have
|
|
// been removed.
|
|
MOZ_ASSERT_IF(inFrameMaps(to), !inFrameMaps(from));
|
|
removeFromFrameMapsAndClearBreakpointsIn(cx, from);
|
|
|
|
// Rekey missingScopes to maintain Debugger.Environment identity and
|
|
// forward liveScopes to point to the new frame.
|
|
DebugEnvironments::forwardLiveFrame(cx, from, to);
|
|
});
|
|
|
|
// Forward live Debugger.Frame objects.
|
|
Rooted<DebuggerFrameVector> frames(cx, DebuggerFrameVector(cx));
|
|
if (!getDebuggerFrames(from, &frames)) {
|
|
// An OOM here means that all Debuggers' frame maps still contain
|
|
// entries for 'from' and no entries for 'to'. Since the 'from' frame
|
|
// will be gone, they are removed by removeFromDebuggerFramesOnExit
|
|
// above.
|
|
return false;
|
|
}
|
|
|
|
// If during the loop below we hit an OOM, we must also rollback any of
|
|
// the frames that were successfully replaced. For OSR frames, OOM here
|
|
// means those frames will pop from the OSR trampoline, which does not
|
|
// call Debugger::onLeaveFrame.
|
|
auto removeToDebuggerFramesOnExit =
|
|
MakeScopeExit([&] { removeFromFrameMapsAndClearBreakpointsIn(cx, to); });
|
|
|
|
for (size_t i = 0; i < frames.length(); i++) {
|
|
HandleDebuggerFrame frameobj = frames[i];
|
|
Debugger* dbg = Debugger::fromChildJSObject(frameobj);
|
|
|
|
// Update frame object's ScriptFrameIter::data pointer.
|
|
frameobj->freeFrameIterData(cx->runtime()->defaultFreeOp());
|
|
ScriptFrameIter::Data* data = iter.copyData();
|
|
if (!data) {
|
|
// An OOM here means that some Debuggers' frame maps may still
|
|
// contain entries for 'from' and some Debuggers' frame maps may
|
|
// also contain entries for 'to'. Thus both
|
|
// removeFromDebuggerFramesOnExit and
|
|
// removeToDebuggerFramesOnExit must both run.
|
|
//
|
|
// The current frameobj in question is still in its Debugger's
|
|
// frame map keyed by 'from', so it will be covered by
|
|
// removeFromDebuggerFramesOnExit.
|
|
return false;
|
|
}
|
|
frameobj->setPrivate(data);
|
|
|
|
// Remove old frame.
|
|
dbg->frames.remove(from);
|
|
|
|
// Add the frame object with |to| as key.
|
|
if (!dbg->frames.putNew(to, frameobj)) {
|
|
// This OOM is subtle. At this point, both
|
|
// removeFromDebuggerFramesOnExit and removeToDebuggerFramesOnExit
|
|
// must both run for the same reason given above.
|
|
//
|
|
// The difference is that the current frameobj is no longer in its
|
|
// Debugger's frame map, so it will not be cleaned up by neither
|
|
// lambda. Manually clean it up here.
|
|
FreeOp* fop = cx->runtime()->defaultFreeOp();
|
|
frameobj->freeFrameIterData(fop);
|
|
DebuggerFrame_maybeDecrementFrameScriptStepModeCount(fop, to, frameobj);
|
|
|
|
ReportOutOfMemory(cx);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// All frames successfuly replaced, cancel the rollback.
|
|
removeToDebuggerFramesOnExit.release();
|
|
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool Debugger::inFrameMaps(AbstractFramePtr frame) {
|
|
bool foundAny = false;
|
|
forEachDebuggerFrame(frame,
|
|
[&](DebuggerFrame* frameobj) { foundAny = true; });
|
|
return foundAny;
|
|
}
|
|
|
|
/* static */ void Debugger::removeFromFrameMapsAndClearBreakpointsIn(
|
|
JSContext* cx, AbstractFramePtr frame, bool suspending) {
|
|
forEachDebuggerFrame(frame, [&](DebuggerFrame* frameobj) {
|
|
FreeOp* fop = cx->runtime()->defaultFreeOp();
|
|
frameobj->freeFrameIterData(fop);
|
|
if (!suspending) {
|
|
DebuggerFrame_maybeDecrementFrameScriptStepModeCount(fop, frame,
|
|
frameobj);
|
|
}
|
|
|
|
Debugger* dbg = Debugger::fromChildJSObject(frameobj);
|
|
dbg->frames.remove(frame);
|
|
|
|
if (!suspending && frame.isGeneratorFrame()) {
|
|
// Terminally exiting a generator.
|
|
GeneratorObject* genObj = GetGeneratorObjectForFrame(cx, frame);
|
|
if (GeneratorWeakMap::Ptr p = dbg->generatorFrames.lookup(genObj)) {
|
|
dbg->generatorFrames.remove(p);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 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);
|
|
}
|
|
|
|
struct DebuggerScriptSetBreakpointMatcher {
|
|
JSContext* cx_;
|
|
Debugger* dbg_;
|
|
size_t offset_;
|
|
RootedObject handler_;
|
|
|
|
public:
|
|
explicit DebuggerScriptSetBreakpointMatcher(JSContext* cx, Debugger* dbg,
|
|
size_t offset,
|
|
HandleObject handler)
|
|
: cx_(cx), dbg_(dbg), offset_(offset), handler_(cx, handler) {}
|
|
|
|
using ReturnType = bool;
|
|
|
|
ReturnType match(HandleScript script) {
|
|
if (!dbg_->observesScript(script)) {
|
|
JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_NOT_DEBUGGING);
|
|
return false;
|
|
}
|
|
|
|
if (!EnsureScriptOffsetIsValid(cx_, script, offset_)) {
|
|
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_->zone()->new_<Breakpoint>(dbg_, site, handler_)) {
|
|
return true;
|
|
}
|
|
site->dec(cx_->runtime()->defaultFreeOp());
|
|
site->destroyIfEmpty(cx_->runtime()->defaultFreeOp());
|
|
return false;
|
|
}
|
|
ReturnType match(Handle<LazyScript*> lazyScript) {
|
|
RootedScript script(cx_, DelazifyScript(cx_, lazyScript));
|
|
if (!script) {
|
|
return false;
|
|
}
|
|
return match(script);
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> wasmInstance) {
|
|
wasm::Instance& instance = wasmInstance->instance();
|
|
if (!instance.debugEnabled() ||
|
|
!instance.debug().hasBreakpointTrapAtOffset(offset_)) {
|
|
JS_ReportErrorNumberASCII(cx_, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_BAD_OFFSET);
|
|
return false;
|
|
}
|
|
WasmBreakpointSite* site =
|
|
instance.debug().getOrCreateBreakpointSite(cx_, offset_);
|
|
if (!site) {
|
|
return false;
|
|
}
|
|
site->inc(cx_->runtime()->defaultFreeOp());
|
|
if (cx_->zone()->new_<WasmBreakpoint>(dbg_, site, handler_,
|
|
instance.object())) {
|
|
return true;
|
|
}
|
|
site->dec(cx_->runtime()->defaultFreeOp());
|
|
site->destroyIfEmpty(cx_->runtime()->defaultFreeOp());
|
|
return false;
|
|
}
|
|
};
|
|
|
|
static bool DebuggerScript_setBreakpoint(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "setBreakpoint", args, obj, referent);
|
|
if (!args.requireAtLeast(cx, "Debugger.Script.setBreakpoint", 2)) {
|
|
return false;
|
|
}
|
|
Debugger* dbg = Debugger::fromChildJSObject(obj);
|
|
|
|
size_t offset;
|
|
if (!ScriptOffset(cx, args[0], &offset)) {
|
|
return false;
|
|
}
|
|
|
|
RootedObject handler(cx, NonNullObject(cx, args[1]));
|
|
if (!handler) {
|
|
return false;
|
|
}
|
|
|
|
DebuggerScriptSetBreakpointMatcher matcher(cx, dbg, offset, handler);
|
|
if (!referent.match(matcher)) {
|
|
return false;
|
|
}
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
static bool DebuggerScript_getBreakpoints(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_SCRIPT_DELAZIFY(cx, argc, vp, "getBreakpoints", args, obj,
|
|
script);
|
|
Debugger* dbg = Debugger::fromChildJSObject(obj);
|
|
|
|
jsbytecode* pc;
|
|
if (args.length() > 0) {
|
|
size_t offset;
|
|
if (!ScriptOffset(cx, args[0], &offset) ||
|
|
!EnsureScriptOffsetIsValid(cx, script, 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) {
|
|
continue;
|
|
}
|
|
MOZ_ASSERT(site->type() == BreakpointSite::Type::JS);
|
|
if (!pc || site->asJS()->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;
|
|
}
|
|
|
|
class DebuggerScriptClearBreakpointMatcher {
|
|
JSContext* cx_;
|
|
Debugger* dbg_;
|
|
JSObject* handler_;
|
|
|
|
public:
|
|
DebuggerScriptClearBreakpointMatcher(JSContext* cx, Debugger* dbg,
|
|
JSObject* handler)
|
|
: cx_(cx), dbg_(dbg), handler_(handler) {}
|
|
using ReturnType = bool;
|
|
|
|
ReturnType match(HandleScript script) {
|
|
script->clearBreakpointsIn(cx_->runtime()->defaultFreeOp(), dbg_, handler_);
|
|
return true;
|
|
}
|
|
ReturnType match(Handle<LazyScript*> lazyScript) {
|
|
RootedScript script(cx_, DelazifyScript(cx_, lazyScript));
|
|
if (!script) {
|
|
return false;
|
|
}
|
|
return match(script);
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
|
|
wasm::Instance& instance = instanceObj->instance();
|
|
if (!instance.debugEnabled()) {
|
|
return true;
|
|
}
|
|
instance.debug().clearBreakpointsIn(cx_->runtime()->defaultFreeOp(),
|
|
instanceObj, dbg_, handler_);
|
|
return true;
|
|
}
|
|
};
|
|
|
|
static bool DebuggerScript_clearBreakpoint(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "clearBreakpoint", args, obj,
|
|
referent);
|
|
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;
|
|
}
|
|
|
|
DebuggerScriptClearBreakpointMatcher matcher(cx, dbg, handler);
|
|
if (!referent.match(matcher)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
static bool DebuggerScript_clearAllBreakpoints(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "clearAllBreakpoints", args, obj,
|
|
referent);
|
|
Debugger* dbg = Debugger::fromChildJSObject(obj);
|
|
DebuggerScriptClearBreakpointMatcher matcher(cx, dbg, nullptr);
|
|
if (!referent.match(matcher)) {
|
|
return false;
|
|
}
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
class DebuggerScriptIsInCatchScopeMatcher {
|
|
JSContext* cx_;
|
|
size_t offset_;
|
|
bool isInCatch_;
|
|
|
|
public:
|
|
explicit DebuggerScriptIsInCatchScopeMatcher(JSContext* cx, size_t offset)
|
|
: cx_(cx), offset_(offset), isInCatch_(false) {}
|
|
using ReturnType = bool;
|
|
|
|
inline bool isInCatch() const { return isInCatch_; }
|
|
|
|
ReturnType match(HandleScript script) {
|
|
if (!EnsureScriptOffsetIsValid(cx_, script, offset_)) {
|
|
return false;
|
|
}
|
|
|
|
if (script->hasTrynotes()) {
|
|
for (const JSTryNote& tn : script->trynotes()) {
|
|
if (tn.start <= offset_ && offset_ < tn.start + tn.length &&
|
|
tn.kind == JSTRY_CATCH) {
|
|
isInCatch_ = true;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
isInCatch_ = false;
|
|
return true;
|
|
}
|
|
ReturnType match(Handle<LazyScript*> lazyScript) {
|
|
RootedScript script(cx_, DelazifyScript(cx_, lazyScript));
|
|
if (!script) {
|
|
return false;
|
|
}
|
|
return match(script);
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> instance) {
|
|
isInCatch_ = false;
|
|
return true;
|
|
}
|
|
};
|
|
|
|
static bool DebuggerScript_isInCatchScope(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_REFERENT(cx, argc, vp, "isInCatchScope", args, obj,
|
|
referent);
|
|
if (!args.requireAtLeast(cx, "Debugger.Script.isInCatchScope", 1)) {
|
|
return false;
|
|
}
|
|
|
|
size_t offset;
|
|
if (!ScriptOffset(cx, args[0], &offset)) {
|
|
return false;
|
|
}
|
|
|
|
DebuggerScriptIsInCatchScopeMatcher matcher(cx, offset);
|
|
if (!referent.match(matcher)) {
|
|
return false;
|
|
}
|
|
args.rval().setBoolean(matcher.isInCatch());
|
|
return true;
|
|
}
|
|
|
|
static bool DebuggerScript_getOffsetsCoverage(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSCRIPT_SCRIPT_DELAZIFY(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, NameToId(cx->names().offset));
|
|
RootedId lineNumberId(cx, NameToId(cx->names().lineNumber));
|
|
RootedId columnNumberId(cx, NameToId(cx->names().columnNumber));
|
|
RootedId countId(cx, NameToId(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 || !DefineDataProperty(cx, item, offsetId, offsetValue) ||
|
|
!DefineDataProperty(cx, item, lineNumberId, lineNumberValue) ||
|
|
!DefineDataProperty(cx, item, columnNumberId, columnNumberValue) ||
|
|
!DefineDataProperty(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_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR,
|
|
"Debugger.Script");
|
|
return false;
|
|
}
|
|
|
|
static const JSPropertySpec DebuggerScript_properties[] = {
|
|
JS_PSG("isGeneratorFunction", DebuggerScript_getIsGeneratorFunction, 0),
|
|
JS_PSG("isAsyncFunction", DebuggerScript_getIsAsyncFunction, 0),
|
|
JS_PSG("isModule", DebuggerScript_getIsModule, 0),
|
|
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("mainOffset", DebuggerScript_getMainOffset, 0),
|
|
JS_PSG("global", DebuggerScript_getGlobal, 0),
|
|
JS_PSG("format", DebuggerScript_getFormat, 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("getSuccessorOffsets", DebuggerScript_getSuccessorOffsets, 1, 0),
|
|
JS_FN("getPredecessorOffsets", DebuggerScript_getPredecessorOffsets, 1, 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 ********************************************************/
|
|
|
|
// For internal use only.
|
|
static inline NativeObject* GetSourceReferentRawObject(JSObject* obj) {
|
|
MOZ_ASSERT(obj->getClass() == &DebuggerSource_class);
|
|
return static_cast<NativeObject*>(obj->as<NativeObject>().getPrivate());
|
|
}
|
|
|
|
static inline DebuggerSourceReferent GetSourceReferent(JSObject* obj) {
|
|
if (NativeObject* referent = GetSourceReferentRawObject(obj)) {
|
|
if (referent->is<ScriptSourceObject>()) {
|
|
return AsVariant(&referent->as<ScriptSourceObject>());
|
|
}
|
|
return AsVariant(&referent->as<WasmInstanceObject>());
|
|
}
|
|
return AsVariant(static_cast<ScriptSourceObject*>(nullptr));
|
|
}
|
|
|
|
void DebuggerSource_trace(JSTracer* trc, JSObject* obj) {
|
|
// There is a barrier on private pointers, so the Unbarriered marking
|
|
// is okay.
|
|
if (JSObject* referent = GetSourceReferentRawObject(obj)) {
|
|
TraceManuallyBarrieredCrossCompartmentEdge(trc, obj, &referent,
|
|
"Debugger.Source referent");
|
|
obj->as<NativeObject>().setPrivateUnbarriered(referent);
|
|
}
|
|
}
|
|
|
|
class SetDebuggerSourcePrivateMatcher {
|
|
NativeObject* obj_;
|
|
|
|
public:
|
|
explicit SetDebuggerSourcePrivateMatcher(NativeObject* obj) : obj_(obj) {}
|
|
using ReturnType = void;
|
|
ReturnType match(HandleScriptSourceObject source) {
|
|
obj_->setPrivateGCThing(source);
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> instance) {
|
|
obj_->setPrivateGCThing(instance);
|
|
}
|
|
};
|
|
|
|
NativeObject* Debugger::newDebuggerSource(
|
|
JSContext* cx, Handle<DebuggerSourceReferent> referent) {
|
|
cx->check(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));
|
|
SetDebuggerSourcePrivateMatcher matcher(sourceobj);
|
|
referent.match(matcher);
|
|
|
|
return sourceobj;
|
|
}
|
|
|
|
JSObject* Debugger::wrapVariantReferent(
|
|
JSContext* cx, Handle<DebuggerSourceReferent> referent) {
|
|
JSObject* obj;
|
|
if (referent.is<ScriptSourceObject*>()) {
|
|
Handle<ScriptSourceObject*> untaggedReferent =
|
|
referent.template as<ScriptSourceObject*>();
|
|
Rooted<CrossCompartmentKey> key(
|
|
cx, CrossCompartmentKey(
|
|
object, untaggedReferent,
|
|
CrossCompartmentKey::DebuggerObjectKind::DebuggerSource));
|
|
obj = wrapVariantReferent<DebuggerSourceReferent, ScriptSourceObject*,
|
|
SourceWeakMap>(cx, sources, key, referent);
|
|
} else {
|
|
Handle<WasmInstanceObject*> untaggedReferent =
|
|
referent.template as<WasmInstanceObject*>();
|
|
Rooted<CrossCompartmentKey> key(
|
|
cx, CrossCompartmentKey(
|
|
object, untaggedReferent,
|
|
CrossCompartmentKey::DebuggerObjectKind::DebuggerWasmSource));
|
|
obj = wrapVariantReferent<DebuggerSourceReferent, WasmInstanceObject*,
|
|
WasmInstanceWeakMap>(cx, wasmInstanceSources, key,
|
|
referent);
|
|
}
|
|
MOZ_ASSERT_IF(obj, GetSourceReferent(obj) == referent);
|
|
return obj;
|
|
}
|
|
|
|
JSObject* Debugger::wrapSource(JSContext* cx, HandleScriptSourceObject source) {
|
|
Rooted<DebuggerSourceReferent> referent(cx, source.get());
|
|
return wrapVariantReferent(cx, referent);
|
|
}
|
|
|
|
JSObject* Debugger::wrapWasmSource(JSContext* cx,
|
|
Handle<WasmInstanceObject*> wasmInstance) {
|
|
Rooted<DebuggerSourceReferent> referent(cx, wasmInstance.get());
|
|
return wrapVariantReferent(cx, referent);
|
|
}
|
|
|
|
static bool DebuggerSource_construct(JSContext* cx, unsigned argc, Value* vp) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR,
|
|
"Debugger.Source");
|
|
return false;
|
|
}
|
|
|
|
static NativeObject* DebuggerSource_check(JSContext* cx, HandleValue thisv,
|
|
const char* fnname) {
|
|
JSObject* thisobj = NonNullObject(cx, thisv);
|
|
if (!thisobj) {
|
|
return nullptr;
|
|
}
|
|
if (thisobj->getClass() != &DebuggerSource_class) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_INCOMPATIBLE_PROTO, "Debugger.Source",
|
|
fnname, thisobj->getClass()->name);
|
|
return nullptr;
|
|
}
|
|
|
|
NativeObject* nthisobj = &thisobj->as<NativeObject>();
|
|
|
|
if (!GetSourceReferentRawObject(thisobj)) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_INCOMPATIBLE_PROTO, "Debugger.Source",
|
|
fnname, "prototype object");
|
|
return nullptr;
|
|
}
|
|
|
|
return nthisobj;
|
|
}
|
|
|
|
template <typename ReferentT>
|
|
static NativeObject* DebuggerSource_checkThis(JSContext* cx,
|
|
const CallArgs& args,
|
|
const char* fnname,
|
|
const char* refname) {
|
|
NativeObject* thisobj = DebuggerSource_check(cx, args.thisv(), fnname);
|
|
if (!thisobj) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (!GetSourceReferent(thisobj).is<ReferentT>()) {
|
|
ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK,
|
|
args.thisv(), nullptr, refname);
|
|
return nullptr;
|
|
}
|
|
|
|
return thisobj;
|
|
}
|
|
|
|
#define THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, fnname, args, obj, referent) \
|
|
CallArgs args = CallArgsFromVp(argc, vp); \
|
|
RootedNativeObject obj(cx, DebuggerSource_check(cx, args.thisv(), fnname)); \
|
|
if (!obj) return false; \
|
|
Rooted<DebuggerSourceReferent> referent(cx, GetSourceReferent(obj))
|
|
|
|
#define THIS_DEBUGSOURCE_SOURCE(cx, argc, vp, fnname, args, obj, sourceObject) \
|
|
CallArgs args = CallArgsFromVp(argc, vp); \
|
|
RootedNativeObject obj(cx, DebuggerSource_checkThis<ScriptSourceObject*>( \
|
|
cx, args, fnname, "a JS source")); \
|
|
if (!obj) return false; \
|
|
RootedScriptSourceObject sourceObject( \
|
|
cx, GetSourceReferent(obj).as<ScriptSourceObject*>())
|
|
|
|
class DebuggerSourceGetTextMatcher {
|
|
JSContext* cx_;
|
|
|
|
public:
|
|
explicit DebuggerSourceGetTextMatcher(JSContext* cx) : cx_(cx) {}
|
|
|
|
using ReturnType = JSString*;
|
|
|
|
ReturnType match(HandleScriptSourceObject sourceObject) {
|
|
ScriptSource* ss = sourceObject->source();
|
|
bool hasSourceText = ss->hasSourceText();
|
|
if (!ss->hasSourceText() &&
|
|
!JSScript::loadSource(cx_, ss, &hasSourceText)) {
|
|
return nullptr;
|
|
}
|
|
if (!hasSourceText) {
|
|
return NewStringCopyZ<CanGC>(cx_, "[no source]");
|
|
}
|
|
|
|
if (ss->isFunctionBody()) {
|
|
return ss->functionBodyString(cx_);
|
|
}
|
|
|
|
return ss->substring(cx_, 0, ss->length());
|
|
}
|
|
|
|
ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
|
|
wasm::Instance& instance = instanceObj->instance();
|
|
const char* msg;
|
|
if (!instance.debugEnabled()) {
|
|
msg = "Restart with developer tools open to view WebAssembly source.";
|
|
} else {
|
|
msg = "[debugger missing wasm binary-to-text conversion]";
|
|
}
|
|
return NewStringCopyZ<CanGC>(cx_, msg);
|
|
}
|
|
};
|
|
|
|
static bool DebuggerSource_getText(JSContext* cx, unsigned argc, Value* vp) {
|
|
THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get text)", args, obj, referent);
|
|
Value textv = obj->getReservedSlot(JSSLOT_DEBUGSOURCE_TEXT);
|
|
if (!textv.isUndefined()) {
|
|
MOZ_ASSERT(textv.isString());
|
|
args.rval().set(textv);
|
|
return true;
|
|
}
|
|
|
|
DebuggerSourceGetTextMatcher matcher(cx);
|
|
JSString* str = referent.match(matcher);
|
|
if (!str) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setString(str);
|
|
obj->setReservedSlot(JSSLOT_DEBUGSOURCE_TEXT, args.rval());
|
|
return true;
|
|
}
|
|
|
|
static bool DebuggerSource_getBinary(JSContext* cx, unsigned argc, Value* vp) {
|
|
THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get binary)", args, obj, referent);
|
|
|
|
if (!referent.is<WasmInstanceObject*>()) {
|
|
ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK,
|
|
args.thisv(), nullptr, "a wasm source");
|
|
return false;
|
|
}
|
|
|
|
RootedWasmInstanceObject instanceObj(cx, referent.as<WasmInstanceObject*>());
|
|
wasm::Instance& instance = instanceObj->instance();
|
|
|
|
if (!instance.debugEnabled()) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_NO_BINARY_SOURCE);
|
|
return false;
|
|
}
|
|
|
|
const wasm::Bytes& bytecode = instance.debug().bytecode();
|
|
RootedObject arr(cx, JS_NewUint8Array(cx, bytecode.length()));
|
|
if (!arr) {
|
|
return false;
|
|
}
|
|
|
|
memcpy(arr->as<TypedArrayObject>().dataPointerUnshared(), bytecode.begin(),
|
|
bytecode.length());
|
|
|
|
args.rval().setObject(*arr);
|
|
return true;
|
|
}
|
|
|
|
class DebuggerSourceGetURLMatcher {
|
|
JSContext* cx_;
|
|
|
|
public:
|
|
explicit DebuggerSourceGetURLMatcher(JSContext* cx) : cx_(cx) {}
|
|
|
|
using ReturnType = Maybe<JSString*>;
|
|
|
|
ReturnType match(HandleScriptSourceObject sourceObject) {
|
|
ScriptSource* ss = sourceObject->source();
|
|
MOZ_ASSERT(ss);
|
|
if (ss->filename()) {
|
|
JSString* str = NewStringCopyZ<CanGC>(cx_, ss->filename());
|
|
return Some(str);
|
|
}
|
|
return Nothing();
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
|
|
return Some(instanceObj->instance().createDisplayURL(cx_));
|
|
}
|
|
};
|
|
|
|
static bool DebuggerSource_getURL(JSContext* cx, unsigned argc, Value* vp) {
|
|
THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get url)", args, obj, referent);
|
|
|
|
DebuggerSourceGetURLMatcher matcher(cx);
|
|
Maybe<JSString*> str = referent.match(matcher);
|
|
if (str.isSome()) {
|
|
if (!*str) {
|
|
return false;
|
|
}
|
|
args.rval().setString(*str);
|
|
} else {
|
|
args.rval().setNull();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
struct DebuggerSourceGetDisplayURLMatcher {
|
|
using ReturnType = const char16_t*;
|
|
ReturnType match(HandleScriptSourceObject sourceObject) {
|
|
ScriptSource* ss = sourceObject->source();
|
|
MOZ_ASSERT(ss);
|
|
return ss->hasDisplayURL() ? ss->displayURL() : nullptr;
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> wasmInstance) {
|
|
return wasmInstance->instance().metadata().displayURL();
|
|
}
|
|
};
|
|
|
|
static bool DebuggerSource_getDisplayURL(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get url)", args, obj, referent);
|
|
|
|
DebuggerSourceGetDisplayURLMatcher matcher;
|
|
if (const char16_t* displayURL = referent.match(matcher)) {
|
|
JSString* str = JS_NewUCStringCopyZ(cx, displayURL);
|
|
if (!str) {
|
|
return false;
|
|
}
|
|
args.rval().setString(str);
|
|
} else {
|
|
args.rval().setNull();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
struct DebuggerSourceGetElementMatcher {
|
|
using ReturnType = JSObject*;
|
|
ReturnType match(HandleScriptSourceObject sourceObject) {
|
|
return sourceObject->unwrappedElement();
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { return nullptr; }
|
|
};
|
|
|
|
static bool DebuggerSource_getElement(JSContext* cx, unsigned argc, Value* vp) {
|
|
THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get element)", args, obj, referent);
|
|
|
|
DebuggerSourceGetElementMatcher matcher;
|
|
if (JSObject* element = referent.match(matcher)) {
|
|
args.rval().setObjectOrNull(element);
|
|
if (!Debugger::fromChildJSObject(obj)->wrapDebuggeeValue(cx, args.rval())) {
|
|
return false;
|
|
}
|
|
} else {
|
|
args.rval().setUndefined();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
struct DebuggerSourceGetElementPropertyMatcher {
|
|
using ReturnType = Value;
|
|
ReturnType match(HandleScriptSourceObject sourceObject) {
|
|
return sourceObject->unwrappedElementAttributeName();
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> wasmInstance) {
|
|
return UndefinedValue();
|
|
}
|
|
};
|
|
|
|
static bool DebuggerSource_getElementProperty(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get elementAttributeName)", args,
|
|
obj, referent);
|
|
DebuggerSourceGetElementPropertyMatcher matcher;
|
|
args.rval().set(referent.match(matcher));
|
|
return Debugger::fromChildJSObject(obj)->wrapDebuggeeValue(cx, args.rval());
|
|
}
|
|
|
|
class DebuggerSourceGetIntroductionScriptMatcher {
|
|
JSContext* cx_;
|
|
Debugger* dbg_;
|
|
MutableHandleValue rval_;
|
|
|
|
public:
|
|
DebuggerSourceGetIntroductionScriptMatcher(JSContext* cx, Debugger* dbg,
|
|
MutableHandleValue rval)
|
|
: cx_(cx), dbg_(dbg), rval_(rval) {}
|
|
|
|
using ReturnType = bool;
|
|
|
|
ReturnType match(HandleScriptSourceObject sourceObject) {
|
|
RootedScript script(cx_, sourceObject->unwrappedIntroductionScript());
|
|
if (script) {
|
|
RootedObject scriptDO(cx_, dbg_->wrapScript(cx_, script));
|
|
if (!scriptDO) {
|
|
return false;
|
|
}
|
|
rval_.setObject(*scriptDO);
|
|
} else {
|
|
rval_.setUndefined();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
ReturnType match(Handle<WasmInstanceObject*> wasmInstance) {
|
|
RootedObject ds(cx_, dbg_->wrapWasmScript(cx_, wasmInstance));
|
|
if (!ds) {
|
|
return false;
|
|
}
|
|
rval_.setObject(*ds);
|
|
return true;
|
|
}
|
|
};
|
|
|
|
static bool DebuggerSource_getIntroductionScript(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get introductionScript)", args, obj,
|
|
referent);
|
|
Debugger* dbg = Debugger::fromChildJSObject(obj);
|
|
DebuggerSourceGetIntroductionScriptMatcher matcher(cx, dbg, args.rval());
|
|
return referent.match(matcher);
|
|
}
|
|
|
|
struct DebuggerGetIntroductionOffsetMatcher {
|
|
using ReturnType = Value;
|
|
ReturnType match(HandleScriptSourceObject 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->unwrappedIntroductionScript()) {
|
|
return Int32Value(ss->introductionOffset());
|
|
}
|
|
return UndefinedValue();
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> wasmInstance) {
|
|
return UndefinedValue();
|
|
}
|
|
};
|
|
|
|
static bool DebuggerSource_getIntroductionOffset(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get introductionOffset)", args, obj,
|
|
referent);
|
|
DebuggerGetIntroductionOffsetMatcher matcher;
|
|
args.rval().set(referent.match(matcher));
|
|
return true;
|
|
}
|
|
|
|
struct DebuggerSourceGetIntroductionTypeMatcher {
|
|
using ReturnType = const char*;
|
|
ReturnType match(HandleScriptSourceObject sourceObject) {
|
|
ScriptSource* ss = sourceObject->source();
|
|
MOZ_ASSERT(ss);
|
|
return ss->hasIntroductionType() ? ss->introductionType() : nullptr;
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> wasmInstance) { return "wasm"; }
|
|
};
|
|
|
|
static bool DebuggerSource_getIntroductionType(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get introductionType)", args, obj,
|
|
referent);
|
|
|
|
DebuggerSourceGetIntroductionTypeMatcher matcher;
|
|
if (const char* introductionType = referent.match(matcher)) {
|
|
JSString* str = NewStringCopyZ<CanGC>(cx, 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_SOURCE(cx, argc, vp, "set sourceMapURL", args, obj,
|
|
sourceObject);
|
|
ScriptSource* ss = sourceObject->source();
|
|
MOZ_ASSERT(ss);
|
|
if (!args.requireAtLeast(cx, "set sourceMapURL", 1)) {
|
|
return false;
|
|
}
|
|
|
|
JSString* str = ToString<CanGC>(cx, args[0]);
|
|
if (!str) {
|
|
return false;
|
|
}
|
|
|
|
AutoStableStringChars stableChars(cx);
|
|
if (!stableChars.initTwoByte(cx, str)) {
|
|
return false;
|
|
}
|
|
|
|
if (!ss->setSourceMapURL(cx, stableChars.twoByteChars())) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
class DebuggerSourceGetSourceMapURLMatcher {
|
|
JSContext* cx_;
|
|
MutableHandleString result_;
|
|
|
|
public:
|
|
explicit DebuggerSourceGetSourceMapURLMatcher(JSContext* cx,
|
|
MutableHandleString result)
|
|
: cx_(cx), result_(result) {}
|
|
|
|
using ReturnType = bool;
|
|
ReturnType match(HandleScriptSourceObject sourceObject) {
|
|
ScriptSource* ss = sourceObject->source();
|
|
MOZ_ASSERT(ss);
|
|
if (!ss->hasSourceMapURL()) {
|
|
result_.set(nullptr);
|
|
return true;
|
|
}
|
|
JSString* str = JS_NewUCStringCopyZ(cx_, ss->sourceMapURL());
|
|
if (!str) {
|
|
return false;
|
|
}
|
|
result_.set(str);
|
|
return true;
|
|
}
|
|
ReturnType match(Handle<WasmInstanceObject*> instanceObj) {
|
|
wasm::Instance& instance = instanceObj->instance();
|
|
if (!instance.debugEnabled()) {
|
|
result_.set(nullptr);
|
|
return true;
|
|
}
|
|
|
|
RootedString str(cx_);
|
|
if (!instance.debug().getSourceMappingURL(cx_, &str)) {
|
|
return false;
|
|
}
|
|
|
|
result_.set(str);
|
|
return true;
|
|
}
|
|
};
|
|
|
|
static bool DebuggerSource_getSourceMapURL(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGSOURCE_REFERENT(cx, argc, vp, "(get sourceMapURL)", args, obj,
|
|
referent);
|
|
|
|
RootedString result(cx);
|
|
DebuggerSourceGetSourceMapURLMatcher matcher(cx, &result);
|
|
if (!referent.match(matcher)) {
|
|
return false;
|
|
}
|
|
if (result) {
|
|
args.rval().setString(result);
|
|
} else {
|
|
args.rval().setNull();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static const JSPropertySpec DebuggerSource_properties[] = {
|
|
JS_PSG("text", DebuggerSource_getText, 0),
|
|
JS_PSG("binary", DebuggerSource_getBinary, 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_PS_END};
|
|
|
|
static const JSFunctionSpec DebuggerSource_methods[] = {JS_FS_END};
|
|
|
|
/*** Debugger.Frame *********************************************************/
|
|
|
|
ScriptedOnStepHandler::ScriptedOnStepHandler(JSObject* object)
|
|
: object_(object) {
|
|
MOZ_ASSERT(object_->isCallable());
|
|
}
|
|
|
|
JSObject* ScriptedOnStepHandler::object() const { return object_; }
|
|
|
|
void ScriptedOnStepHandler::drop() {
|
|
this->~ScriptedOnStepHandler();
|
|
js_free(this);
|
|
}
|
|
|
|
void ScriptedOnStepHandler::trace(JSTracer* tracer) {
|
|
TraceEdge(tracer, &object_, "OnStepHandlerFunction.object");
|
|
}
|
|
|
|
bool ScriptedOnStepHandler::onStep(JSContext* cx, HandleDebuggerFrame frame,
|
|
ResumeMode& resumeMode,
|
|
MutableHandleValue vp) {
|
|
RootedValue fval(cx, ObjectValue(*object_));
|
|
RootedValue rval(cx);
|
|
if (!js::Call(cx, fval, frame, &rval)) {
|
|
return false;
|
|
}
|
|
|
|
return ParseResumptionValue(cx, rval, resumeMode, vp);
|
|
};
|
|
|
|
ScriptedOnPopHandler::ScriptedOnPopHandler(JSObject* object) : object_(object) {
|
|
MOZ_ASSERT(object->isCallable());
|
|
}
|
|
|
|
JSObject* ScriptedOnPopHandler::object() const { return object_; }
|
|
|
|
void ScriptedOnPopHandler::drop() {
|
|
this->~ScriptedOnPopHandler();
|
|
js_free(this);
|
|
}
|
|
|
|
void ScriptedOnPopHandler::trace(JSTracer* tracer) {
|
|
TraceEdge(tracer, &object_, "OnStepHandlerFunction.object");
|
|
}
|
|
|
|
bool ScriptedOnPopHandler::onPop(JSContext* cx, HandleDebuggerFrame frame,
|
|
ResumeMode& resumeMode,
|
|
MutableHandleValue vp) {
|
|
Debugger* dbg = frame->owner();
|
|
|
|
RootedValue completion(cx);
|
|
if (!dbg->newCompletionValue(cx, resumeMode, vp, &completion)) {
|
|
return false;
|
|
}
|
|
|
|
RootedValue fval(cx, ObjectValue(*object_));
|
|
RootedValue rval(cx);
|
|
if (!js::Call(cx, fval, frame, completion, &rval)) {
|
|
return false;
|
|
}
|
|
|
|
return ParseResumptionValue(cx, rval, resumeMode, vp);
|
|
};
|
|
|
|
bool DebuggerFrame::resume(const FrameIter& iter) {
|
|
FrameIter::Data* data = iter.copyData();
|
|
if (!data) {
|
|
return false;
|
|
}
|
|
setPrivate(data);
|
|
return true;
|
|
}
|
|
|
|
bool DebuggerFrame::hasAnyLiveHooks() const {
|
|
return !getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined() ||
|
|
!getReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER).isUndefined();
|
|
}
|
|
|
|
/* static */ NativeObject* DebuggerFrame::initClass(
|
|
JSContext* cx, HandleObject dbgCtor, Handle<GlobalObject*> global) {
|
|
RootedObject objProto(cx,
|
|
GlobalObject::getOrCreateObjectPrototype(cx, global));
|
|
|
|
return InitClass(cx, dbgCtor, objProto, &class_, construct, 0, properties_,
|
|
methods_, nullptr, nullptr);
|
|
}
|
|
|
|
/* static */ DebuggerFrame* DebuggerFrame::create(JSContext* cx,
|
|
HandleObject proto,
|
|
const FrameIter& iter,
|
|
HandleNativeObject debugger) {
|
|
DebuggerFrame* frame = NewObjectWithGivenProto<DebuggerFrame>(cx, proto);
|
|
if (!frame) {
|
|
return nullptr;
|
|
}
|
|
|
|
FrameIter::Data* data = iter.copyData();
|
|
if (!data) {
|
|
return nullptr;
|
|
}
|
|
frame->setPrivate(data);
|
|
|
|
frame->setReservedSlot(JSSLOT_DEBUGFRAME_OWNER, ObjectValue(*debugger));
|
|
|
|
return frame;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::getCallee(JSContext* cx,
|
|
HandleDebuggerFrame frame,
|
|
MutableHandleDebuggerObject result) {
|
|
MOZ_ASSERT(frame->isLive());
|
|
|
|
AbstractFramePtr referent = DebuggerFrame::getReferent(frame);
|
|
if (!referent.isFunctionFrame()) {
|
|
result.set(nullptr);
|
|
return true;
|
|
}
|
|
|
|
Debugger* dbg = frame->owner();
|
|
|
|
RootedObject callee(cx, referent.callee());
|
|
return dbg->wrapDebuggeeObject(cx, callee, result);
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::getIsConstructing(JSContext* cx,
|
|
HandleDebuggerFrame frame,
|
|
bool& result) {
|
|
MOZ_ASSERT(frame->isLive());
|
|
|
|
Maybe<FrameIter> maybeIter;
|
|
if (!DebuggerFrame::getFrameIter(cx, frame, maybeIter)) {
|
|
return false;
|
|
}
|
|
FrameIter& iter = *maybeIter;
|
|
|
|
result = iter.isFunctionFrame() && iter.isConstructing();
|
|
return true;
|
|
}
|
|
|
|
static void UpdateFrameIterPc(FrameIter& iter) {
|
|
if (iter.abstractFramePtr().isWasmDebugFrame()) {
|
|
// Wasm debug frames don't need their pc updated -- it's null.
|
|
return;
|
|
}
|
|
|
|
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();
|
|
|
|
JSContext* cx = TlsContext.get();
|
|
MOZ_ASSERT(cx == activation->cx());
|
|
|
|
ActivationIterator activationIter(cx);
|
|
while (activationIter.activation() != activation) {
|
|
++activationIter;
|
|
}
|
|
|
|
OnlyJSJitFrameIter jitIter(activationIter);
|
|
while (!jitIter.frame().isIonJS() || jitIter.frame().jsFrame() != jsFrame) {
|
|
++jitIter;
|
|
}
|
|
|
|
jit::InlineFrameIterator ionInlineIter(cx, &jitIter.frame());
|
|
while (ionInlineIter.frameNo() != frame->frameNo()) {
|
|
++ionInlineIter;
|
|
}
|
|
|
|
MOZ_ASSERT(ionInlineIter.pc() == iter.pc());
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
iter.updatePcQuadratic();
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::getEnvironment(
|
|
JSContext* cx, HandleDebuggerFrame frame,
|
|
MutableHandleDebuggerEnvironment result) {
|
|
MOZ_ASSERT(frame->isLive());
|
|
|
|
Debugger* dbg = frame->owner();
|
|
|
|
Maybe<FrameIter> maybeIter;
|
|
if (!DebuggerFrame::getFrameIter(cx, frame, maybeIter)) {
|
|
return false;
|
|
}
|
|
FrameIter& iter = *maybeIter;
|
|
|
|
Rooted<Env*> env(cx);
|
|
{
|
|
AutoRealm ar(cx, iter.abstractFramePtr().environmentChain());
|
|
UpdateFrameIterPc(iter);
|
|
env = GetDebugEnvironmentForFrame(cx, iter.abstractFramePtr(), iter.pc());
|
|
if (!env) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return dbg->wrapEnvironment(cx, env, result);
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::getIsGenerator(HandleDebuggerFrame frame) {
|
|
AbstractFramePtr referent = DebuggerFrame::getReferent(frame);
|
|
return referent.hasScript() && referent.script()->isGenerator();
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::getOffset(JSContext* cx,
|
|
HandleDebuggerFrame frame,
|
|
size_t& result) {
|
|
MOZ_ASSERT(frame->isLive());
|
|
|
|
Maybe<FrameIter> maybeIter;
|
|
if (!DebuggerFrame::getFrameIter(cx, frame, maybeIter)) {
|
|
return false;
|
|
}
|
|
FrameIter& iter = *maybeIter;
|
|
|
|
AbstractFramePtr referent = DebuggerFrame::getReferent(frame);
|
|
if (referent.isWasmDebugFrame()) {
|
|
iter.wasmUpdateBytecodeOffset();
|
|
result = iter.wasmBytecodeOffset();
|
|
} else {
|
|
JSScript* script = iter.script();
|
|
UpdateFrameIterPc(iter);
|
|
jsbytecode* pc = iter.pc();
|
|
result = script->pcToOffset(pc);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::getOlder(JSContext* cx,
|
|
HandleDebuggerFrame frame,
|
|
MutableHandleDebuggerFrame result) {
|
|
MOZ_ASSERT(frame->isLive());
|
|
|
|
Debugger* dbg = frame->owner();
|
|
|
|
Maybe<FrameIter> maybeIter;
|
|
if (!DebuggerFrame::getFrameIter(cx, frame, maybeIter)) {
|
|
return false;
|
|
}
|
|
FrameIter& iter = *maybeIter;
|
|
|
|
for (++iter; !iter.done(); ++iter) {
|
|
if (dbg->observesFrame(iter)) {
|
|
if (iter.isIon() && !iter.ensureHasRematerializedFrame(cx)) {
|
|
return false;
|
|
}
|
|
return dbg->getFrame(cx, iter, result);
|
|
}
|
|
}
|
|
|
|
result.set(nullptr);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::getThis(JSContext* cx,
|
|
HandleDebuggerFrame frame,
|
|
MutableHandleValue result) {
|
|
MOZ_ASSERT(frame->isLive());
|
|
|
|
if (!requireScriptReferent(cx, frame)) {
|
|
return false;
|
|
}
|
|
|
|
Debugger* dbg = frame->owner();
|
|
|
|
Maybe<FrameIter> maybeIter;
|
|
if (!DebuggerFrame::getFrameIter(cx, frame, maybeIter)) {
|
|
return false;
|
|
}
|
|
FrameIter& iter = *maybeIter;
|
|
|
|
{
|
|
AbstractFramePtr frame = iter.abstractFramePtr();
|
|
AutoRealm ar(cx, frame.environmentChain());
|
|
|
|
UpdateFrameIterPc(iter);
|
|
|
|
if (!GetThisValueForDebuggerMaybeOptimizedOut(cx, frame, iter.pc(),
|
|
result)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return dbg->wrapDebuggeeValue(cx, result);
|
|
}
|
|
|
|
/* static */ DebuggerFrameType DebuggerFrame::getType(
|
|
HandleDebuggerFrame frame) {
|
|
AbstractFramePtr referent = DebuggerFrame::getReferent(frame);
|
|
|
|
// Indirect eval frames are both isGlobalFrame() and isEvalFrame(), so the
|
|
// order of checks here is significant.
|
|
if (referent.isEvalFrame()) {
|
|
return DebuggerFrameType::Eval;
|
|
} else if (referent.isGlobalFrame()) {
|
|
return DebuggerFrameType::Global;
|
|
} else if (referent.isFunctionFrame()) {
|
|
return DebuggerFrameType::Call;
|
|
} else if (referent.isModuleFrame()) {
|
|
return DebuggerFrameType::Module;
|
|
} else if (referent.isWasmDebugFrame()) {
|
|
return DebuggerFrameType::WasmCall;
|
|
}
|
|
MOZ_CRASH("Unknown frame type");
|
|
}
|
|
|
|
/* static */ DebuggerFrameImplementation DebuggerFrame::getImplementation(
|
|
HandleDebuggerFrame frame) {
|
|
AbstractFramePtr referent = DebuggerFrame::getReferent(frame);
|
|
|
|
if (referent.isBaselineFrame()) {
|
|
return DebuggerFrameImplementation::Baseline;
|
|
} else if (referent.isRematerializedFrame()) {
|
|
return DebuggerFrameImplementation::Ion;
|
|
} else if (referent.isWasmDebugFrame()) {
|
|
return DebuggerFrameImplementation::Wasm;
|
|
}
|
|
return DebuggerFrameImplementation::Interpreter;
|
|
}
|
|
|
|
/*
|
|
* If succesful, transfers the ownership of the given `handler` to this
|
|
* Debugger.Frame. Note that on failure, the ownership of `handler` is not
|
|
* transferred, and the caller is responsible for cleaning it up.
|
|
*/
|
|
/* static */ bool DebuggerFrame::setOnStepHandler(JSContext* cx,
|
|
HandleDebuggerFrame frame,
|
|
OnStepHandler* handler) {
|
|
MOZ_ASSERT(frame->isLive());
|
|
|
|
OnStepHandler* prior = frame->onStepHandler();
|
|
if (prior && handler != prior) {
|
|
prior->drop();
|
|
}
|
|
|
|
AbstractFramePtr referent = DebuggerFrame::getReferent(frame);
|
|
if (referent.isWasmDebugFrame()) {
|
|
wasm::Instance* instance = referent.asWasmDebugFrame()->instance();
|
|
wasm::DebugFrame* wasmFrame = referent.asWasmDebugFrame();
|
|
if (handler && !prior) {
|
|
// Single stepping toggled off->on.
|
|
if (!instance->debug().incrementStepModeCount(cx,
|
|
wasmFrame->funcIndex())) {
|
|
return false;
|
|
}
|
|
} else if (!handler && prior) {
|
|
// Single stepping toggled on->off.
|
|
FreeOp* fop = cx->runtime()->defaultFreeOp();
|
|
if (!instance->debug().decrementStepModeCount(fop,
|
|
wasmFrame->funcIndex())) {
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
if (handler && !prior) {
|
|
// Single stepping toggled off->on.
|
|
AutoRealm ar(cx, referent.environmentChain());
|
|
// Ensure observability *before* incrementing the step mode count.
|
|
// Calling this function after calling incrementStepModeCount
|
|
// will make it a no-op.
|
|
Debugger* dbg = frame->owner();
|
|
if (!dbg->ensureExecutionObservabilityOfScript(cx, referent.script())) {
|
|
return false;
|
|
}
|
|
if (!referent.script()->incrementStepModeCount(cx)) {
|
|
return false;
|
|
}
|
|
} else if (!handler && prior) {
|
|
// Single stepping toggled on->off.
|
|
referent.script()->decrementStepModeCount(cx->runtime()->defaultFreeOp());
|
|
}
|
|
}
|
|
|
|
// Now that the step mode switch has succeeded, we can install the handler.
|
|
frame->setReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER,
|
|
handler ? PrivateValue(handler) : UndefinedValue());
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::getArguments(
|
|
JSContext* cx, HandleDebuggerFrame frame,
|
|
MutableHandleDebuggerArguments result) {
|
|
Value argumentsv = frame->getReservedSlot(JSSLOT_DEBUGFRAME_ARGUMENTS);
|
|
if (!argumentsv.isUndefined()) {
|
|
result.set(argumentsv.isObject()
|
|
? &argumentsv.toObject().as<DebuggerArguments>()
|
|
: nullptr);
|
|
return true;
|
|
}
|
|
|
|
AbstractFramePtr referent = DebuggerFrame::getReferent(frame);
|
|
|
|
RootedDebuggerArguments arguments(cx);
|
|
if (referent.hasArgs()) {
|
|
Rooted<GlobalObject*> global(cx, &frame->global());
|
|
RootedObject proto(cx, GlobalObject::getOrCreateArrayPrototype(cx, global));
|
|
if (!proto) {
|
|
return false;
|
|
}
|
|
arguments = DebuggerArguments::create(cx, proto, frame);
|
|
if (!arguments) {
|
|
return false;
|
|
}
|
|
} else {
|
|
arguments = nullptr;
|
|
}
|
|
|
|
result.set(arguments);
|
|
frame->setReservedSlot(JSSLOT_DEBUGFRAME_ARGUMENTS,
|
|
ObjectOrNullValue(result));
|
|
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,
|
|
mozilla::Range<const char16_t> chars,
|
|
const char* filename, unsigned lineno,
|
|
MutableHandleValue rval) {
|
|
cx->check(env, frame);
|
|
|
|
CompileOptions options(cx);
|
|
options.setIsRunOnce(true)
|
|
.setNoScriptRval(false)
|
|
.setFileAndLine(filename, lineno)
|
|
.setIntroductionType("debugger eval")
|
|
.maybeMakeStrictMode(frame && frame.hasScript() ? frame.script()->strict()
|
|
: false);
|
|
|
|
SourceText<char16_t> srcBuf;
|
|
if (!srcBuf.init(cx, chars.begin().get(), chars.length(),
|
|
SourceOwnership::Borrowed)) {
|
|
return false;
|
|
}
|
|
|
|
RootedScript callerScript(
|
|
cx, frame && frame.hasScript() ? frame.script() : nullptr);
|
|
RootedScript script(cx);
|
|
|
|
ScopeKind scopeKind;
|
|
if (IsGlobalLexicalEnvironment(env)) {
|
|
scopeKind = ScopeKind::Global;
|
|
} else {
|
|
scopeKind = ScopeKind::NonSyntactic;
|
|
}
|
|
|
|
if (frame) {
|
|
MOZ_ASSERT(scopeKind == ScopeKind::NonSyntactic);
|
|
RootedScope scope(cx,
|
|
GlobalScope::createEmpty(cx, ScopeKind::NonSyntactic));
|
|
if (!scope) {
|
|
return false;
|
|
}
|
|
|
|
frontend::EvalScriptInfo info(cx, options, env, scope);
|
|
script = frontend::CompileEvalScript(info, srcBuf);
|
|
if (!script) {
|
|
return false;
|
|
}
|
|
} else {
|
|
// 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.
|
|
frontend::GlobalScriptInfo info(cx, options, scopeKind);
|
|
script = frontend::CompileGlobalScript(info, srcBuf);
|
|
if (!script) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return ExecuteKernel(cx, script, *env, NullValue(), frame, rval.address());
|
|
}
|
|
|
|
static bool DebuggerGenericEval(JSContext* cx,
|
|
const mozilla::Range<const char16_t> chars,
|
|
HandleObject bindings,
|
|
const EvalOptions& options,
|
|
ResumeMode& resumeMode,
|
|
MutableHandleValue value, Debugger* dbg,
|
|
HandleObject envArg, FrameIter* iter) {
|
|
// Either we're specifying the frame, or a global.
|
|
MOZ_ASSERT_IF(iter, !envArg);
|
|
MOZ_ASSERT_IF(!iter, envArg && IsGlobalLexicalEnvironment(envArg));
|
|
|
|
// 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 (bindings) {
|
|
if (!GetPropertyKeys(cx, bindings, 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, bindings, bindings, keys[i], valp) ||
|
|
!dbg->unwrapDebuggeeValue(cx, valp)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Maybe<AutoRealm> ar;
|
|
if (iter) {
|
|
ar.emplace(cx, iter->environmentChain(cx));
|
|
} else {
|
|
ar.emplace(cx, envArg);
|
|
}
|
|
|
|
Rooted<Env*> env(cx);
|
|
if (iter) {
|
|
env = GetDebugEnvironmentForFrame(cx, iter->abstractFramePtr(), iter->pc());
|
|
if (!env) {
|
|
return false;
|
|
}
|
|
} else {
|
|
env = envArg;
|
|
}
|
|
|
|
// If evalWithBindings, create the inner environment.
|
|
if (bindings) {
|
|
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];
|
|
cx->markId(id);
|
|
MutableHandleValue val = values[i];
|
|
if (!cx->compartment()->wrap(cx, val) ||
|
|
!NativeDefineDataProperty(cx, nenv, id, val, 0)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
AutoObjectVector envChain(cx);
|
|
if (!envChain.append(nenv)) {
|
|
return false;
|
|
}
|
|
|
|
RootedObject newEnv(cx);
|
|
if (!CreateObjectsForEnvironmentChain(cx, envChain, env, &newEnv)) {
|
|
return false;
|
|
}
|
|
|
|
env = newEnv;
|
|
}
|
|
|
|
// Run the code and produce the completion value.
|
|
LeaveDebuggeeNoExecute nnx(cx);
|
|
RootedValue rval(cx);
|
|
AbstractFramePtr frame = iter ? iter->abstractFramePtr() : NullFramePtr();
|
|
|
|
bool ok = EvaluateInEnv(
|
|
cx, env, frame, chars,
|
|
options.filename() ? options.filename() : "debugger eval code",
|
|
options.lineno(), &rval);
|
|
Debugger::resultToCompletion(cx, ok, rval, &resumeMode, value);
|
|
ar.reset();
|
|
return dbg->wrapDebuggeeValue(cx, value);
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::eval(JSContext* cx, HandleDebuggerFrame frame,
|
|
mozilla::Range<const char16_t> chars,
|
|
HandleObject bindings,
|
|
const EvalOptions& options,
|
|
ResumeMode& resumeMode,
|
|
MutableHandleValue value) {
|
|
MOZ_ASSERT(frame->isLive());
|
|
|
|
Debugger* dbg = frame->owner();
|
|
|
|
Maybe<FrameIter> maybeIter;
|
|
if (!DebuggerFrame::getFrameIter(cx, frame, maybeIter)) {
|
|
return false;
|
|
}
|
|
FrameIter& iter = *maybeIter;
|
|
|
|
UpdateFrameIterPc(iter);
|
|
|
|
return DebuggerGenericEval(cx, chars, bindings, options, resumeMode, value,
|
|
dbg, nullptr, &iter);
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::isLive() const { return !!getPrivate(); }
|
|
|
|
OnStepHandler* DebuggerFrame::onStepHandler() const {
|
|
Value value = getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER);
|
|
return value.isUndefined() ? nullptr
|
|
: static_cast<OnStepHandler*>(value.toPrivate());
|
|
}
|
|
|
|
OnPopHandler* DebuggerFrame::onPopHandler() const {
|
|
Value value = getReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER);
|
|
return value.isUndefined() ? nullptr
|
|
: static_cast<OnPopHandler*>(value.toPrivate());
|
|
}
|
|
|
|
void DebuggerFrame::setOnPopHandler(OnPopHandler* handler) {
|
|
MOZ_ASSERT(isLive());
|
|
|
|
OnPopHandler* prior = onPopHandler();
|
|
if (prior && prior != handler) {
|
|
prior->drop();
|
|
}
|
|
|
|
setReservedSlot(JSSLOT_DEBUGFRAME_ONPOP_HANDLER,
|
|
handler ? PrivateValue(handler) : UndefinedValue());
|
|
}
|
|
|
|
static bool DebuggerFrame_requireLive(JSContext* cx,
|
|
HandleDebuggerFrame frame) {
|
|
if (!frame->isLive()) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_NOT_LIVE, "Debugger.Frame");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
FrameIter::Data* DebuggerFrame::frameIterData() const {
|
|
return static_cast<FrameIter::Data*>(getPrivate());
|
|
}
|
|
|
|
/* static */ AbstractFramePtr DebuggerFrame::getReferent(
|
|
HandleDebuggerFrame frame) {
|
|
FrameIter iter(*frame->frameIterData());
|
|
return iter.abstractFramePtr();
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::getFrameIter(JSContext* cx,
|
|
HandleDebuggerFrame frame,
|
|
Maybe<FrameIter>& result) {
|
|
result.emplace(*frame->frameIterData());
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::requireScriptReferent(
|
|
JSContext* cx, HandleDebuggerFrame frame) {
|
|
AbstractFramePtr referent = DebuggerFrame::getReferent(frame);
|
|
if (!referent.hasScript()) {
|
|
RootedValue frameobj(cx, ObjectValue(*frame));
|
|
ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK, frameobj,
|
|
nullptr, "a script frame");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void DebuggerFrame::freeFrameIterData(FreeOp* fop) {
|
|
if (FrameIter::Data* data = frameIterData()) {
|
|
fop->delete_(data);
|
|
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()) {
|
|
return;
|
|
}
|
|
if (frame.isWasmDebugFrame()) {
|
|
wasm::Instance* instance = frame.wasmInstance();
|
|
instance->debug().decrementStepModeCount(
|
|
fop, frame.asWasmDebugFrame()->funcIndex());
|
|
} else {
|
|
frame.script()->decrementStepModeCount(fop);
|
|
}
|
|
}
|
|
|
|
static void DebuggerFrame_finalize(FreeOp* fop, JSObject* obj) {
|
|
MOZ_ASSERT(fop->maybeOnHelperThread());
|
|
DebuggerFrame& frameobj = obj->as<DebuggerFrame>();
|
|
frameobj.freeFrameIterData(fop);
|
|
OnStepHandler* onStepHandler = frameobj.onStepHandler();
|
|
if (onStepHandler) {
|
|
onStepHandler->drop();
|
|
}
|
|
OnPopHandler* onPopHandler = frameobj.onPopHandler();
|
|
if (onPopHandler) {
|
|
onPopHandler->drop();
|
|
}
|
|
}
|
|
|
|
static void DebuggerFrame_trace(JSTracer* trc, JSObject* obj) {
|
|
OnStepHandler* onStepHandler = obj->as<DebuggerFrame>().onStepHandler();
|
|
if (onStepHandler) {
|
|
onStepHandler->trace(trc);
|
|
}
|
|
OnPopHandler* onPopHandler = obj->as<DebuggerFrame>().onPopHandler();
|
|
if (onPopHandler) {
|
|
onPopHandler->trace(trc);
|
|
}
|
|
}
|
|
|
|
static DebuggerFrame* DebuggerFrame_checkThis(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_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_INCOMPATIBLE_PROTO, "Debugger.Frame",
|
|
fnname, thisobj->getClass()->name);
|
|
return nullptr;
|
|
}
|
|
|
|
RootedDebuggerFrame frame(cx, &thisobj->as<DebuggerFrame>());
|
|
|
|
// 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 (!frame->getPrivate() &&
|
|
frame->getReservedSlot(JSSLOT_DEBUGFRAME_OWNER).isUndefined()) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_INCOMPATIBLE_PROTO, "Debugger.Frame",
|
|
fnname, "prototype object");
|
|
return nullptr;
|
|
}
|
|
|
|
if (checkLive) {
|
|
if (!DebuggerFrame_requireLive(cx, frame)) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
return frame;
|
|
}
|
|
|
|
/*
|
|
* Methods can use THIS_DEBUGGER_FRAME to check that `this` is a Debugger.Frame
|
|
* object and get it in a local Rooted.
|
|
*
|
|
* Methods that need the AbstractFramePtr should use THIS_FRAME.
|
|
*/
|
|
#define THIS_DEBUGGER_FRAME(cx, argc, vp, fnname, args, frame) \
|
|
CallArgs args = CallArgsFromVp(argc, vp); \
|
|
RootedDebuggerFrame frame(cx, \
|
|
DebuggerFrame_checkThis(cx, args, fnname, true)); \
|
|
if (!frame) return false;
|
|
|
|
#define THIS_FRAME(cx, argc, vp, fnname, args, thisobj, iter, frame) \
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, fnname, args, thisobj); \
|
|
FrameIter iter(*thisobj->frameIterData()); \
|
|
AbstractFramePtr frame = iter.abstractFramePtr()
|
|
|
|
/* static */ bool DebuggerFrame::typeGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, "get type", args, frame);
|
|
|
|
DebuggerFrameType type = DebuggerFrame::getType(frame);
|
|
|
|
JSString* str;
|
|
switch (type) {
|
|
case DebuggerFrameType::Eval:
|
|
str = cx->names().eval;
|
|
break;
|
|
case DebuggerFrameType::Global:
|
|
str = cx->names().global;
|
|
break;
|
|
case DebuggerFrameType::Call:
|
|
str = cx->names().call;
|
|
break;
|
|
case DebuggerFrameType::Module:
|
|
str = cx->names().module;
|
|
break;
|
|
case DebuggerFrameType::WasmCall:
|
|
str = cx->names().wasmcall;
|
|
break;
|
|
default:
|
|
MOZ_CRASH("bad DebuggerFrameType value");
|
|
}
|
|
|
|
args.rval().setString(str);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::implementationGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, "get implementation", args, frame);
|
|
|
|
DebuggerFrameImplementation implementation =
|
|
DebuggerFrame::getImplementation(frame);
|
|
|
|
const char* s;
|
|
switch (implementation) {
|
|
case DebuggerFrameImplementation::Baseline:
|
|
s = "baseline";
|
|
break;
|
|
case DebuggerFrameImplementation::Ion:
|
|
s = "ion";
|
|
break;
|
|
case DebuggerFrameImplementation::Interpreter:
|
|
s = "interpreter";
|
|
break;
|
|
case DebuggerFrameImplementation::Wasm:
|
|
s = "wasm";
|
|
break;
|
|
default:
|
|
MOZ_CRASH("bad DebuggerFrameImplementation value");
|
|
}
|
|
|
|
JSAtom* str = Atomize(cx, s, strlen(s));
|
|
if (!str) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setString(str);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::environmentGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, "get environment", args, frame);
|
|
|
|
RootedDebuggerEnvironment result(cx);
|
|
if (!DebuggerFrame::getEnvironment(cx, frame, &result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObject(*result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::calleeGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, "get callee", args, frame);
|
|
|
|
RootedDebuggerObject result(cx);
|
|
if (!DebuggerFrame::getCallee(cx, frame, &result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObjectOrNull(result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::generatorGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, "get callee", args, frame);
|
|
|
|
args.rval().setBoolean(DebuggerFrame::getIsGenerator(frame));
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::constructingGetter(JSContext* cx,
|
|
unsigned argc, Value* vp) {
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, "get callee", args, frame);
|
|
|
|
bool result;
|
|
if (!DebuggerFrame::getIsConstructing(cx, frame, result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setBoolean(result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::thisGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, "get this", args, frame);
|
|
|
|
return DebuggerFrame::getThis(cx, frame, args.rval());
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::olderGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, "get older", args, frame);
|
|
|
|
RootedDebuggerFrame result(cx);
|
|
if (!DebuggerFrame::getOlder(cx, frame, &result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObjectOrNull(result);
|
|
return true;
|
|
}
|
|
|
|
// 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_ReportErrorNumberASCII(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, frameIter, frame);
|
|
|
|
// TODO handle wasm frame arguments -- they are not yet reflectable.
|
|
MOZ_ASSERT(!frame.isWasmDebugFrame(), "a wasm frame args");
|
|
|
|
// 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();
|
|
{
|
|
AutoRealm ar(cx, script);
|
|
if (!script->ensureHasAnalyzedArgsUsage(cx)) {
|
|
return false;
|
|
}
|
|
}
|
|
if (unsigned(i) < frame.numFormalArgs()) {
|
|
for (PositionalFormalParameterIter fi(script); fi; fi++) {
|
|
if (fi.argumentSlot() == unsigned(i)) {
|
|
// We might've been called before the CallObject was
|
|
// created.
|
|
if (fi.closedOver() && frame.hasInitialEnvironment()) {
|
|
arg = frame.callObj().aliasedBinding(fi);
|
|
} else {
|
|
arg = frame.unaliasedActual(i, DONT_CHECK_ALIASING);
|
|
}
|
|
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 */ DebuggerArguments* DebuggerArguments::create(
|
|
JSContext* cx, HandleObject proto, HandleDebuggerFrame frame) {
|
|
AbstractFramePtr referent = DebuggerFrame::getReferent(frame);
|
|
|
|
Rooted<DebuggerArguments*> obj(
|
|
cx, NewObjectWithGivenProto<DebuggerArguments>(cx, proto));
|
|
if (!obj) {
|
|
return nullptr;
|
|
}
|
|
|
|
SetReservedSlot(obj, FRAME_SLOT, ObjectValue(*frame));
|
|
|
|
MOZ_ASSERT(referent.numActualArgs() <= 0x7fffffff);
|
|
unsigned fargc = referent.numActualArgs();
|
|
RootedValue fargcVal(cx, Int32Value(fargc));
|
|
if (!NativeDefineDataProperty(cx, obj, cx->names().length, fargcVal,
|
|
JSPROP_PERMANENT | JSPROP_READONLY)) {
|
|
return nullptr;
|
|
}
|
|
|
|
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 nullptr;
|
|
}
|
|
id = INT_TO_JSID(i);
|
|
if (!NativeDefineAccessorProperty(cx, obj, id, getobj, nullptr,
|
|
JSPROP_ENUMERATE | JSPROP_GETTER)) {
|
|
return nullptr;
|
|
}
|
|
getobj->setExtendedSlot(0, Int32Value(i));
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::argumentsGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, "get arguments", args, frame);
|
|
|
|
RootedDebuggerArguments result(cx);
|
|
if (!DebuggerFrame::getArguments(cx, frame, &result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObjectOrNull(result);
|
|
return true;
|
|
}
|
|
|
|
static bool DebuggerFrame_getScript(JSContext* cx, unsigned argc, Value* vp) {
|
|
THIS_FRAME(cx, argc, vp, "get script", args, thisobj, frameIter, frame);
|
|
Debugger* debug = Debugger::fromChildJSObject(thisobj);
|
|
|
|
RootedObject scriptObject(cx);
|
|
if (frame.isWasmDebugFrame()) {
|
|
RootedWasmInstanceObject instance(cx, frame.wasmInstance()->object());
|
|
scriptObject = debug->wrapWasmScript(cx, instance);
|
|
if (!scriptObject) {
|
|
return false;
|
|
}
|
|
} else {
|
|
RootedScript script(cx, frame.script());
|
|
scriptObject = debug->wrapScript(cx, script);
|
|
if (!scriptObject) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
MOZ_ASSERT(scriptObject);
|
|
args.rval().setObject(*scriptObject);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::offsetGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, "get offset", args, frame);
|
|
|
|
size_t result;
|
|
if (!DebuggerFrame::getOffset(cx, frame, result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setNumber(double(result));
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::liveGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
CallArgs args = CallArgsFromVp(argc, vp);
|
|
RootedDebuggerFrame frame(
|
|
cx, DebuggerFrame_checkThis(cx, args, "get live", false));
|
|
if (!frame) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setBoolean(frame->isLive());
|
|
return true;
|
|
}
|
|
|
|
static bool IsValidHook(const Value& v) {
|
|
return v.isUndefined() || (v.isObject() && v.toObject().isCallable());
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::onStepGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, "get onStep", args, frame);
|
|
|
|
OnStepHandler* handler = frame->onStepHandler();
|
|
RootedValue value(
|
|
cx, handler ? ObjectOrNullValue(handler->object()) : UndefinedValue());
|
|
MOZ_ASSERT(IsValidHook(value));
|
|
args.rval().set(value);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::onStepSetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, "set onStep", args, frame);
|
|
if (!args.requireAtLeast(cx, "Debugger.Frame.set onStep", 1)) {
|
|
return false;
|
|
}
|
|
if (!IsValidHook(args[0])) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_NOT_CALLABLE_OR_UNDEFINED);
|
|
return false;
|
|
}
|
|
|
|
ScriptedOnStepHandler* handler = nullptr;
|
|
if (!args[0].isUndefined()) {
|
|
handler = cx->new_<ScriptedOnStepHandler>(&args[0].toObject());
|
|
if (!handler) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!DebuggerFrame::setOnStepHandler(cx, frame, handler)) {
|
|
handler->drop();
|
|
return false;
|
|
}
|
|
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::onPopGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, "get onPop", args, frame);
|
|
|
|
OnPopHandler* handler = frame->onPopHandler();
|
|
RootedValue value(
|
|
cx, handler ? ObjectValue(*handler->object()) : UndefinedValue());
|
|
MOZ_ASSERT(IsValidHook(value));
|
|
args.rval().set(value);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::onPopSetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, "set onPop", args, frame);
|
|
if (!args.requireAtLeast(cx, "Debugger.Frame.set onPop", 1)) {
|
|
return false;
|
|
}
|
|
if (!IsValidHook(args[0])) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_NOT_CALLABLE_OR_UNDEFINED);
|
|
return false;
|
|
}
|
|
|
|
ScriptedOnPopHandler* handler = nullptr;
|
|
if (!args[0].isUndefined()) {
|
|
handler = cx->new_<ScriptedOnPopHandler>(&args[0].toObject());
|
|
if (!handler) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
frame->setOnPopHandler(handler);
|
|
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::evalMethod(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, "eval", args, frame);
|
|
if (!args.requireAtLeast(cx, "Debugger.Frame.prototype.eval", 1)) {
|
|
return false;
|
|
}
|
|
|
|
AutoStableStringChars stableChars(cx);
|
|
if (!ValueToStableChars(cx, "Debugger.Frame.prototype.eval", args[0],
|
|
stableChars)) {
|
|
return false;
|
|
}
|
|
mozilla::Range<const char16_t> chars = stableChars.twoByteRange();
|
|
|
|
EvalOptions options;
|
|
if (!ParseEvalOptions(cx, args.get(1), options)) {
|
|
return false;
|
|
}
|
|
|
|
ResumeMode resumeMode;
|
|
RootedValue value(cx);
|
|
if (!DebuggerFrame::eval(cx, frame, chars, nullptr, options, resumeMode,
|
|
&value)) {
|
|
return false;
|
|
}
|
|
|
|
return frame->owner()->newCompletionValue(cx, resumeMode, value, args.rval());
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::evalWithBindingsMethod(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_FRAME(cx, argc, vp, "evalWithBindings", args, frame);
|
|
if (!args.requireAtLeast(cx, "Debugger.Frame.prototype.evalWithBindings",
|
|
2)) {
|
|
return false;
|
|
}
|
|
|
|
AutoStableStringChars stableChars(cx);
|
|
if (!ValueToStableChars(cx, "Debugger.Frame.prototype.evalWithBindings",
|
|
args[0], stableChars)) {
|
|
return false;
|
|
}
|
|
mozilla::Range<const char16_t> chars = stableChars.twoByteRange();
|
|
|
|
RootedObject bindings(cx, NonNullObject(cx, args[1]));
|
|
if (!bindings) {
|
|
return false;
|
|
}
|
|
|
|
EvalOptions options;
|
|
if (!ParseEvalOptions(cx, args.get(2), options)) {
|
|
return false;
|
|
}
|
|
|
|
ResumeMode resumeMode;
|
|
RootedValue value(cx);
|
|
if (!DebuggerFrame::eval(cx, frame, chars, bindings, options, resumeMode,
|
|
&value)) {
|
|
return false;
|
|
}
|
|
|
|
return frame->owner()->newCompletionValue(cx, resumeMode, value, args.rval());
|
|
}
|
|
|
|
/* static */ bool DebuggerFrame::construct(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR,
|
|
"Debugger.Frame");
|
|
return false;
|
|
}
|
|
|
|
const JSPropertySpec DebuggerFrame::properties_[] = {
|
|
JS_PSG("arguments", DebuggerFrame::argumentsGetter, 0),
|
|
JS_PSG("callee", DebuggerFrame::calleeGetter, 0),
|
|
JS_PSG("constructing", DebuggerFrame::constructingGetter, 0),
|
|
JS_PSG("environment", DebuggerFrame::environmentGetter, 0),
|
|
JS_PSG("generator", DebuggerFrame::generatorGetter, 0),
|
|
JS_PSG("live", DebuggerFrame::liveGetter, 0),
|
|
JS_PSG("offset", DebuggerFrame::offsetGetter, 0),
|
|
JS_PSG("older", DebuggerFrame::olderGetter, 0),
|
|
JS_PSG("script", DebuggerFrame_getScript, 0),
|
|
JS_PSG("this", DebuggerFrame::thisGetter, 0),
|
|
JS_PSG("type", DebuggerFrame::typeGetter, 0),
|
|
JS_PSG("implementation", DebuggerFrame::implementationGetter, 0),
|
|
JS_PSGS("onStep", DebuggerFrame::onStepGetter, DebuggerFrame::onStepSetter,
|
|
0),
|
|
JS_PSGS("onPop", DebuggerFrame::onPopGetter, DebuggerFrame::onPopSetter, 0),
|
|
JS_PS_END};
|
|
|
|
const JSFunctionSpec DebuggerFrame::methods_[] = {
|
|
JS_FN("eval", DebuggerFrame::evalMethod, 1, 0),
|
|
JS_FN("evalWithBindings", DebuggerFrame::evalWithBindingsMethod, 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);
|
|
}
|
|
}
|
|
|
|
static DebuggerObject* 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_ReportErrorNumberASCII(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.
|
|
DebuggerObject* nthisobj = &thisobj->as<DebuggerObject>();
|
|
if (!nthisobj->getPrivate()) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_INCOMPATIBLE_PROTO, "Debugger.Object",
|
|
fnname, "prototype object");
|
|
return nullptr;
|
|
}
|
|
return nthisobj;
|
|
}
|
|
|
|
#define THIS_DEBUGOBJECT(cx, argc, vp, fnname, args, object) \
|
|
CallArgs args = CallArgsFromVp(argc, vp); \
|
|
RootedDebuggerObject object(cx, DebuggerObject_checkThis(cx, args, fnname)); \
|
|
if (!object) return false;
|
|
|
|
#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)
|
|
|
|
#define THIS_DEBUGOBJECT_PROMISE(cx, argc, vp, fnname, args, obj) \
|
|
THIS_DEBUGOBJECT_REFERENT(cx, argc, vp, fnname, args, obj); \
|
|
obj = CheckedUnwrap(obj); \
|
|
if (!obj) { \
|
|
ReportAccessDenied(cx); \
|
|
return false; \
|
|
} \
|
|
if (!obj->is<PromiseObject>()) { \
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, \
|
|
JSMSG_NOT_EXPECTED_TYPE, "Debugger", "Promise", \
|
|
obj->getClass()->name); \
|
|
return false; \
|
|
} \
|
|
Rooted<PromiseObject*> promise(cx, &obj->as<PromiseObject>());
|
|
|
|
#define THIS_DEBUGOBJECT_OWNER_PROMISE(cx, argc, vp, fnname, args, dbg, obj) \
|
|
THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, fnname, args, dbg, obj); \
|
|
obj = CheckedUnwrap(obj); \
|
|
if (!obj) { \
|
|
ReportAccessDenied(cx); \
|
|
return false; \
|
|
} \
|
|
if (!obj->is<PromiseObject>()) { \
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, \
|
|
JSMSG_NOT_EXPECTED_TYPE, "Debugger", "Promise", \
|
|
obj->getClass()->name); \
|
|
return false; \
|
|
} \
|
|
Rooted<PromiseObject*> promise(cx, &obj->as<PromiseObject>());
|
|
|
|
/* static */ bool DebuggerObject::construct(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR,
|
|
"Debugger.Object");
|
|
return false;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::callableGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get callable", args, object)
|
|
|
|
args.rval().setBoolean(object->isCallable());
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::isBoundFunctionGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get isBoundFunction", args, object)
|
|
|
|
if (!object->isDebuggeeFunction()) {
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
args.rval().setBoolean(object->isBoundFunction());
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::isArrowFunctionGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get isArrowFunction", args, object)
|
|
|
|
if (!object->isDebuggeeFunction()) {
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
args.rval().setBoolean(object->isArrowFunction());
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::isAsyncFunctionGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get isAsyncFunction", args, object)
|
|
|
|
if (!object->isDebuggeeFunction()) {
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
args.rval().setBoolean(object->isAsyncFunction());
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::isGeneratorFunctionGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get isGeneratorFunction", args, object)
|
|
|
|
if (!object->isDebuggeeFunction()) {
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
args.rval().setBoolean(object->isGeneratorFunction());
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::protoGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get proto", args, object)
|
|
|
|
RootedDebuggerObject result(cx);
|
|
if (!DebuggerObject::getPrototypeOf(cx, object, &result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObjectOrNull(result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::classGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get class", args, object)
|
|
|
|
RootedString result(cx);
|
|
if (!DebuggerObject::getClassName(cx, object, &result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setString(result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::nameGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get name", args, object)
|
|
|
|
if (!object->isFunction()) {
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
RootedString result(cx, object->name(cx));
|
|
if (result) {
|
|
args.rval().setString(result);
|
|
} else {
|
|
args.rval().setUndefined();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::displayNameGetter(JSContext* cx,
|
|
unsigned argc, Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get displayName", args, object)
|
|
|
|
if (!object->isFunction()) {
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
RootedString result(cx, object->displayName(cx));
|
|
if (result) {
|
|
args.rval().setString(result);
|
|
} else {
|
|
args.rval().setUndefined();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::parameterNamesGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get parameterNames", args, object)
|
|
|
|
if (!object->isDebuggeeFunction()) {
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
Rooted<StringVector> names(cx, StringVector(cx));
|
|
if (!DebuggerObject::getParameterNames(cx, object, &names)) {
|
|
return false;
|
|
}
|
|
|
|
RootedArrayObject obj(cx, NewDenseFullyAllocatedArray(cx, names.length()));
|
|
if (!obj) {
|
|
return false;
|
|
}
|
|
|
|
obj->ensureDenseInitializedLength(cx, 0, names.length());
|
|
for (size_t i = 0; i < names.length(); ++i) {
|
|
Value v;
|
|
if (names[i]) {
|
|
v = StringValue(names[i]);
|
|
} else {
|
|
v = UndefinedValue();
|
|
}
|
|
obj->setDenseElement(i, v);
|
|
}
|
|
|
|
args.rval().setObject(*obj);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::scriptGetter(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, RemoveAsyncWrapper(&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::environmentGetter(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>()) {
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
RootedFunction fun(cx, RemoveAsyncWrapper(&obj->as<JSFunction>()));
|
|
if (!fun->isInterpreted()) {
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
// Only hand out environments of debuggee functions.
|
|
if (!dbg->observesGlobal(&fun->global())) {
|
|
args.rval().setNull();
|
|
return true;
|
|
}
|
|
|
|
Rooted<Env*> env(cx);
|
|
{
|
|
AutoRealm ar(cx, fun);
|
|
env = GetDebugEnvironmentForFunction(cx, fun);
|
|
if (!env) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return dbg->wrapEnvironment(cx, env, args.rval());
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::boundTargetFunctionGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get boundTargetFunction", args, object)
|
|
|
|
if (!object->isDebuggeeFunction() || !object->isBoundFunction()) {
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
RootedDebuggerObject result(cx);
|
|
if (!DebuggerObject::getBoundTargetFunction(cx, object, &result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObject(*result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::boundThisGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get boundThis", args, object)
|
|
|
|
if (!object->isDebuggeeFunction() || !object->isBoundFunction()) {
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
return DebuggerObject::getBoundThis(cx, object, args.rval());
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::boundArgumentsGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get boundArguments", args, object)
|
|
|
|
if (!object->isDebuggeeFunction() || !object->isBoundFunction()) {
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
Rooted<ValueVector> result(cx, ValueVector(cx));
|
|
if (!DebuggerObject::getBoundArguments(cx, object, &result)) {
|
|
return false;
|
|
}
|
|
|
|
RootedObject obj(cx,
|
|
NewDenseCopiedArray(cx, result.length(), result.begin()));
|
|
if (!obj) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObject(*obj);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::allocationSiteGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get allocationSite", args, object)
|
|
|
|
RootedObject result(cx);
|
|
if (!DebuggerObject::getAllocationSite(cx, object, &result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObjectOrNull(result);
|
|
return true;
|
|
}
|
|
|
|
// Returns the "name" field (see js.msg), which may be used as a unique
|
|
// identifier, for any error object with a JSErrorReport or undefined
|
|
// if the object has no JSErrorReport.
|
|
/* static */ bool DebuggerObject::errorMessageNameGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get errorMessageName", args, object)
|
|
|
|
RootedString result(cx);
|
|
if (!DebuggerObject::getErrorMessageName(cx, object, &result)) {
|
|
return false;
|
|
}
|
|
|
|
if (result) {
|
|
args.rval().setString(result);
|
|
} else {
|
|
args.rval().setUndefined();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::errorNotesGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get errorNotes", args, object)
|
|
|
|
return DebuggerObject::getErrorNotes(cx, object, args.rval());
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::errorLineNumberGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get errorLineNumber", args, object)
|
|
|
|
return DebuggerObject::getErrorLineNumber(cx, object, args.rval());
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::errorColumnNumberGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get errorColumnNumber", args, object)
|
|
|
|
return DebuggerObject::getErrorColumnNumber(cx, object, args.rval());
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::isProxyGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get isProxy", args, object)
|
|
|
|
args.rval().setBoolean(object->isScriptedProxy());
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::proxyTargetGetter(JSContext* cx,
|
|
unsigned argc, Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get proxyTarget", args, object)
|
|
|
|
if (!object->isScriptedProxy()) {
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
Rooted<DebuggerObject*> result(cx);
|
|
if (!DebuggerObject::getScriptedProxyTarget(cx, object, &result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObjectOrNull(result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::proxyHandlerGetter(JSContext* cx,
|
|
unsigned argc, Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get proxyHandler", args, object)
|
|
|
|
if (!object->isScriptedProxy()) {
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
Rooted<DebuggerObject*> result(cx);
|
|
if (!DebuggerObject::getScriptedProxyHandler(cx, object, &result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObjectOrNull(result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::isPromiseGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get isPromise", args, object)
|
|
|
|
args.rval().setBoolean(object->isPromise());
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::promiseStateGetter(JSContext* cx,
|
|
unsigned argc, Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get promiseState", args, object);
|
|
|
|
if (!DebuggerObject::requirePromise(cx, object)) {
|
|
return false;
|
|
}
|
|
|
|
RootedValue result(cx);
|
|
switch (object->promiseState()) {
|
|
case JS::PromiseState::Pending:
|
|
result.setString(cx->names().pending);
|
|
break;
|
|
case JS::PromiseState::Fulfilled:
|
|
result.setString(cx->names().fulfilled);
|
|
break;
|
|
case JS::PromiseState::Rejected:
|
|
result.setString(cx->names().rejected);
|
|
break;
|
|
}
|
|
|
|
args.rval().set(result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::promiseValueGetter(JSContext* cx,
|
|
unsigned argc, Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get promiseValue", args, object);
|
|
|
|
if (!DebuggerObject::requirePromise(cx, object)) {
|
|
return false;
|
|
}
|
|
|
|
if (object->promiseState() != JS::PromiseState::Fulfilled) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_PROMISE_NOT_FULFILLED);
|
|
return false;
|
|
}
|
|
|
|
return DebuggerObject::getPromiseValue(cx, object, args.rval());
|
|
;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::promiseReasonGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get promiseReason", args, object);
|
|
|
|
if (!DebuggerObject::requirePromise(cx, object)) {
|
|
return false;
|
|
}
|
|
|
|
if (object->promiseState() != JS::PromiseState::Rejected) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_PROMISE_NOT_REJECTED);
|
|
return false;
|
|
}
|
|
|
|
return DebuggerObject::getPromiseReason(cx, object, args.rval());
|
|
;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::promiseLifetimeGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get promiseLifetime", args, object);
|
|
|
|
if (!DebuggerObject::requirePromise(cx, object)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setNumber(object->promiseLifetime());
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::promiseTimeToResolutionGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "get promiseTimeToResolution", args, object);
|
|
|
|
if (!DebuggerObject::requirePromise(cx, object)) {
|
|
return false;
|
|
}
|
|
|
|
if (object->promiseState() == JS::PromiseState::Pending) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_PROMISE_NOT_RESOLVED);
|
|
return false;
|
|
}
|
|
|
|
args.rval().setNumber(object->promiseTimeToResolution());
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::promiseAllocationSiteGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT_PROMISE(cx, argc, vp, "get promiseAllocationSite", args,
|
|
refobj);
|
|
|
|
RootedObject allocSite(cx, promise->allocationSite());
|
|
if (!allocSite) {
|
|
args.rval().setNull();
|
|
return true;
|
|
}
|
|
|
|
if (!cx->compartment()->wrap(cx, &allocSite)) {
|
|
return false;
|
|
}
|
|
args.rval().set(ObjectValue(*allocSite));
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::promiseResolutionSiteGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT_PROMISE(cx, argc, vp, "get promiseResolutionSite", args,
|
|
refobj);
|
|
|
|
if (promise->state() == JS::PromiseState::Pending) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_PROMISE_NOT_RESOLVED);
|
|
return false;
|
|
}
|
|
|
|
RootedObject resolutionSite(cx, promise->resolutionSite());
|
|
if (!resolutionSite) {
|
|
args.rval().setNull();
|
|
return true;
|
|
}
|
|
|
|
if (!cx->compartment()->wrap(cx, &resolutionSite)) {
|
|
return false;
|
|
}
|
|
args.rval().set(ObjectValue(*resolutionSite));
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::promiseIDGetter(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT_PROMISE(cx, argc, vp, "get promiseID", args, refobj);
|
|
|
|
args.rval().setNumber(double(promise->getID()));
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::promiseDependentPromisesGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT_OWNER_PROMISE(cx, argc, vp, "get promiseDependentPromises",
|
|
args, dbg, refobj);
|
|
|
|
Rooted<GCVector<Value>> values(cx, GCVector<Value>(cx));
|
|
{
|
|
JSAutoRealm ar(cx, promise);
|
|
if (!promise->dependentPromises(cx, &values)) {
|
|
return false;
|
|
}
|
|
}
|
|
for (size_t i = 0; i < values.length(); i++) {
|
|
if (!dbg->wrapDebuggeeValue(cx, values[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
RootedArrayObject promises(cx);
|
|
if (values.length() == 0) {
|
|
promises = NewDenseEmptyArray(cx);
|
|
} else {
|
|
promises = NewDenseCopiedArray(cx, values.length(), values[0].address());
|
|
}
|
|
if (!promises) {
|
|
return false;
|
|
}
|
|
args.rval().setObject(*promises);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::isExtensibleMethod(JSContext* cx,
|
|
unsigned argc, Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "isExtensible", args, object)
|
|
|
|
bool result;
|
|
if (!DebuggerObject::isExtensible(cx, object, result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setBoolean(result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::isSealedMethod(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "isSealed", args, object)
|
|
|
|
bool result;
|
|
if (!DebuggerObject::isSealed(cx, object, result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setBoolean(result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::isFrozenMethod(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "isFrozen", args, object)
|
|
|
|
bool result;
|
|
if (!DebuggerObject::isFrozen(cx, object, result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setBoolean(result);
|
|
return true;
|
|
}
|
|
|
|
static JSObject* IdVectorToArray(JSContext* cx, Handle<IdVector> ids) {
|
|
Rooted<ValueVector> vals(cx, ValueVector(cx));
|
|
if (!vals.growBy(ids.length())) {
|
|
return nullptr;
|
|
}
|
|
|
|
for (size_t i = 0, len = ids.length(); i < len; i++) {
|
|
jsid id = ids[i];
|
|
if (JSID_IS_INT(id)) {
|
|
JSString* str = Int32ToString<CanGC>(cx, JSID_TO_INT(id));
|
|
if (!str) {
|
|
return nullptr;
|
|
}
|
|
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(
|
|
"IdVector must contain only string, int, and Symbol jsids");
|
|
}
|
|
}
|
|
|
|
return NewDenseCopiedArray(cx, vals.length(), vals.begin());
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getOwnPropertyNamesMethod(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "getOwnPropertyNames", args, object)
|
|
|
|
Rooted<IdVector> ids(cx, IdVector(cx));
|
|
if (!DebuggerObject::getOwnPropertyNames(cx, object, &ids)) {
|
|
return false;
|
|
}
|
|
|
|
RootedObject obj(cx, IdVectorToArray(cx, ids));
|
|
if (!obj) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObject(*obj);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getOwnPropertySymbolsMethod(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "getOwnPropertySymbols", args, object)
|
|
|
|
Rooted<IdVector> ids(cx, IdVector(cx));
|
|
if (!DebuggerObject::getOwnPropertySymbols(cx, object, &ids)) {
|
|
return false;
|
|
}
|
|
|
|
RootedObject obj(cx, IdVectorToArray(cx, ids));
|
|
if (!obj) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObject(*obj);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getOwnPropertyDescriptorMethod(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "getOwnPropertyDescriptor", args, object)
|
|
|
|
RootedId id(cx);
|
|
if (!ValueToId<CanGC>(cx, args.get(0), &id)) {
|
|
return false;
|
|
}
|
|
|
|
Rooted<PropertyDescriptor> desc(cx);
|
|
if (!DebuggerObject::getOwnPropertyDescriptor(cx, object, id, &desc)) {
|
|
return false;
|
|
}
|
|
|
|
return JS::FromPropertyDescriptor(cx, desc, args.rval());
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::preventExtensionsMethod(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "preventExtensions", args, object)
|
|
|
|
if (!DebuggerObject::preventExtensions(cx, object)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::sealMethod(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "seal", args, object)
|
|
|
|
if (!DebuggerObject::seal(cx, object)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::freezeMethod(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "freeze", args, object)
|
|
|
|
if (!DebuggerObject::freeze(cx, object)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::definePropertyMethod(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "defineProperty", args, object)
|
|
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 (!DebuggerObject::defineProperty(cx, object, id, desc)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::definePropertiesMethod(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "defineProperties", args, object);
|
|
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;
|
|
}
|
|
Rooted<IdVector> ids2(cx, IdVector(cx));
|
|
if (!ids2.append(ids.begin(), ids.end())) {
|
|
return false;
|
|
}
|
|
|
|
if (!DebuggerObject::defineProperties(cx, object, ids2, descs)) {
|
|
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::deletePropertyMethod(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "deleteProperty", args, object)
|
|
|
|
RootedId id(cx);
|
|
if (!ValueToId<CanGC>(cx, args.get(0), &id)) {
|
|
return false;
|
|
}
|
|
|
|
ObjectOpResult result;
|
|
if (!DebuggerObject::deleteProperty(cx, object, id, result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setBoolean(result.ok());
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::callMethod(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "call", callArgs, object);
|
|
|
|
RootedValue thisv(cx, callArgs.get(0));
|
|
|
|
Rooted<ValueVector> args(cx, ValueVector(cx));
|
|
if (callArgs.length() >= 2) {
|
|
if (!args.growBy(callArgs.length() - 1)) {
|
|
return false;
|
|
}
|
|
for (size_t i = 1; i < callArgs.length(); ++i) {
|
|
args[i - 1].set(callArgs[i]);
|
|
}
|
|
}
|
|
|
|
return object->call(cx, object, thisv, args, callArgs.rval());
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getPropertyMethod(JSContext* cx,
|
|
unsigned argc, Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "getProperty", args, object)
|
|
|
|
RootedId id(cx);
|
|
if (!ValueToId<CanGC>(cx, args.get(0), &id)) {
|
|
return false;
|
|
}
|
|
|
|
RootedValue receiver(cx,
|
|
args.length() < 2 ? ObjectValue(*object) : args.get(1));
|
|
|
|
if (!DebuggerObject::getProperty(cx, object, id, receiver, args.rval())) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::setPropertyMethod(JSContext* cx,
|
|
unsigned argc, Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "setProperty", args, object)
|
|
|
|
RootedId id(cx);
|
|
if (!ValueToId<CanGC>(cx, args.get(0), &id)) {
|
|
return false;
|
|
}
|
|
|
|
RootedValue value(cx, args.get(1));
|
|
|
|
RootedValue receiver(cx,
|
|
args.length() < 3 ? ObjectValue(*object) : args.get(2));
|
|
|
|
if (!DebuggerObject::setProperty(cx, object, id, value, receiver,
|
|
args.rval())) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::applyMethod(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "apply", callArgs, object);
|
|
|
|
RootedValue thisv(cx, callArgs.get(0));
|
|
|
|
Rooted<ValueVector> args(cx, ValueVector(cx));
|
|
if (callArgs.length() >= 2 && !callArgs[1].isNullOrUndefined()) {
|
|
if (!callArgs[1].isObject()) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_BAD_APPLY_ARGS, js_apply_str);
|
|
return false;
|
|
}
|
|
|
|
RootedObject argsobj(cx, &callArgs[1].toObject());
|
|
|
|
unsigned argc = 0;
|
|
if (!GetLengthProperty(cx, argsobj, &argc)) {
|
|
return false;
|
|
}
|
|
argc = unsigned(Min(argc, ARGS_LENGTH_MAX));
|
|
|
|
if (!args.growBy(argc) || !GetElements(cx, argsobj, argc, args.begin())) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return object->call(cx, object, thisv, args, callArgs.rval());
|
|
}
|
|
|
|
static void EnterDebuggeeObjectRealm(JSContext* cx, Maybe<AutoRealm>& ar,
|
|
JSObject* referent) {
|
|
// |referent| may be a cross-compartment wrapper and CCWs normally
|
|
// shouldn't be used with AutoRealm, but here we use an arbitrary realm for
|
|
// now because we don't really have another option.
|
|
ar.emplace(cx, referent->maybeCCWRealm()->maybeGlobal());
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::asEnvironmentMethod(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);
|
|
{
|
|
AutoRealm ar(cx, referent);
|
|
env = GetDebugEnvironmentForGlobalLexicalEnvironment(cx);
|
|
if (!env) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return dbg->wrapEnvironment(cx, env, args.rval());
|
|
}
|
|
|
|
// Lookup a binding on the referent's global scope and change it to undefined
|
|
// if it is an uninitialized lexical, otherwise do nothing. The method's
|
|
// JavaScript return value is true _only_ when an uninitialized lexical has been
|
|
// altered, otherwise it is false.
|
|
/* static */ bool DebuggerObject::forceLexicalInitializationByNameMethod(
|
|
JSContext* cx, unsigned argc, Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "forceLexicalInitializationByName", args,
|
|
object)
|
|
if (!args.requireAtLeast(
|
|
cx, "Debugger.Object.prototype.forceLexicalInitializationByName",
|
|
1)) {
|
|
return false;
|
|
}
|
|
|
|
if (!DebuggerObject::requireGlobal(cx, object)) {
|
|
return false;
|
|
}
|
|
|
|
RootedId id(cx);
|
|
if (!ValueToIdentifier(cx, args[0], &id)) {
|
|
return false;
|
|
}
|
|
|
|
bool result;
|
|
if (!DebuggerObject::forceLexicalInitializationByName(cx, object, id,
|
|
result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setBoolean(result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::executeInGlobalMethod(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "executeInGlobal", args, object);
|
|
if (!args.requireAtLeast(cx, "Debugger.Object.prototype.executeInGlobal",
|
|
1)) {
|
|
return false;
|
|
}
|
|
|
|
if (!DebuggerObject::requireGlobal(cx, object)) {
|
|
return false;
|
|
}
|
|
|
|
AutoStableStringChars stableChars(cx);
|
|
if (!ValueToStableChars(cx, "Debugger.Object.prototype.executeInGlobal",
|
|
args[0], stableChars)) {
|
|
return false;
|
|
}
|
|
mozilla::Range<const char16_t> chars = stableChars.twoByteRange();
|
|
|
|
EvalOptions options;
|
|
if (!ParseEvalOptions(cx, args.get(1), options)) {
|
|
return false;
|
|
}
|
|
|
|
ResumeMode resumeMode;
|
|
RootedValue value(cx);
|
|
if (!DebuggerObject::executeInGlobal(cx, object, chars, nullptr, options,
|
|
resumeMode, &value)) {
|
|
return false;
|
|
}
|
|
|
|
return object->owner()->newCompletionValue(cx, resumeMode, value,
|
|
args.rval());
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::executeInGlobalWithBindingsMethod(
|
|
JSContext* cx, unsigned argc, Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "executeInGlobalWithBindings", args, object);
|
|
if (!args.requireAtLeast(
|
|
cx, "Debugger.Object.prototype.executeInGlobalWithBindings", 2)) {
|
|
return false;
|
|
}
|
|
|
|
if (!DebuggerObject::requireGlobal(cx, object)) {
|
|
return false;
|
|
}
|
|
|
|
AutoStableStringChars stableChars(cx);
|
|
if (!ValueToStableChars(
|
|
cx, "Debugger.Object.prototype.executeInGlobalWithBindings", args[0],
|
|
stableChars)) {
|
|
return false;
|
|
}
|
|
mozilla::Range<const char16_t> chars = stableChars.twoByteRange();
|
|
|
|
RootedObject bindings(cx, NonNullObject(cx, args[1]));
|
|
if (!bindings) {
|
|
return false;
|
|
}
|
|
|
|
EvalOptions options;
|
|
if (!ParseEvalOptions(cx, args.get(2), options)) {
|
|
return false;
|
|
}
|
|
|
|
ResumeMode resumeMode;
|
|
RootedValue value(cx);
|
|
if (!DebuggerObject::executeInGlobal(cx, object, chars, bindings, options,
|
|
resumeMode, &value)) {
|
|
return false;
|
|
}
|
|
|
|
return object->owner()->newCompletionValue(cx, resumeMode, value,
|
|
args.rval());
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::makeDebuggeeValueMethod(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "makeDebuggeeValue", args, object);
|
|
if (!args.requireAtLeast(cx, "Debugger.Object.prototype.makeDebuggeeValue",
|
|
1)) {
|
|
return false;
|
|
}
|
|
|
|
return DebuggerObject::makeDebuggeeValue(cx, object, args[0], args.rval());
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::unsafeDereferenceMethod(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "unsafeDereference", args, object);
|
|
|
|
RootedObject result(cx);
|
|
if (!DebuggerObject::unsafeDereference(cx, object, &result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObject(*result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::unwrapMethod(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGOBJECT(cx, argc, vp, "unwrap", args, object);
|
|
|
|
RootedDebuggerObject result(cx);
|
|
if (!DebuggerObject::unwrap(cx, object, &result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObjectOrNull(result);
|
|
return true;
|
|
}
|
|
|
|
const JSPropertySpec DebuggerObject::properties_[] = {
|
|
JS_PSG("callable", DebuggerObject::callableGetter, 0),
|
|
JS_PSG("isBoundFunction", DebuggerObject::isBoundFunctionGetter, 0),
|
|
JS_PSG("isArrowFunction", DebuggerObject::isArrowFunctionGetter, 0),
|
|
JS_PSG("isGeneratorFunction", DebuggerObject::isGeneratorFunctionGetter, 0),
|
|
JS_PSG("isAsyncFunction", DebuggerObject::isAsyncFunctionGetter, 0),
|
|
JS_PSG("proto", DebuggerObject::protoGetter, 0),
|
|
JS_PSG("class", DebuggerObject::classGetter, 0),
|
|
JS_PSG("name", DebuggerObject::nameGetter, 0),
|
|
JS_PSG("displayName", DebuggerObject::displayNameGetter, 0),
|
|
JS_PSG("parameterNames", DebuggerObject::parameterNamesGetter, 0),
|
|
JS_PSG("script", DebuggerObject::scriptGetter, 0),
|
|
JS_PSG("environment", DebuggerObject::environmentGetter, 0),
|
|
JS_PSG("boundTargetFunction", DebuggerObject::boundTargetFunctionGetter, 0),
|
|
JS_PSG("boundThis", DebuggerObject::boundThisGetter, 0),
|
|
JS_PSG("boundArguments", DebuggerObject::boundArgumentsGetter, 0),
|
|
JS_PSG("allocationSite", DebuggerObject::allocationSiteGetter, 0),
|
|
JS_PSG("errorMessageName", DebuggerObject::errorMessageNameGetter, 0),
|
|
JS_PSG("errorNotes", DebuggerObject::errorNotesGetter, 0),
|
|
JS_PSG("errorLineNumber", DebuggerObject::errorLineNumberGetter, 0),
|
|
JS_PSG("errorColumnNumber", DebuggerObject::errorColumnNumberGetter, 0),
|
|
JS_PSG("isProxy", DebuggerObject::isProxyGetter, 0),
|
|
JS_PSG("proxyTarget", DebuggerObject::proxyTargetGetter, 0),
|
|
JS_PSG("proxyHandler", DebuggerObject::proxyHandlerGetter, 0),
|
|
JS_PS_END};
|
|
|
|
const JSPropertySpec DebuggerObject::promiseProperties_[] = {
|
|
JS_PSG("isPromise", DebuggerObject::isPromiseGetter, 0),
|
|
JS_PSG("promiseState", DebuggerObject::promiseStateGetter, 0),
|
|
JS_PSG("promiseValue", DebuggerObject::promiseValueGetter, 0),
|
|
JS_PSG("promiseReason", DebuggerObject::promiseReasonGetter, 0),
|
|
JS_PSG("promiseLifetime", DebuggerObject::promiseLifetimeGetter, 0),
|
|
JS_PSG("promiseTimeToResolution",
|
|
DebuggerObject::promiseTimeToResolutionGetter, 0),
|
|
JS_PSG("promiseAllocationSite", DebuggerObject::promiseAllocationSiteGetter,
|
|
0),
|
|
JS_PSG("promiseResolutionSite", DebuggerObject::promiseResolutionSiteGetter,
|
|
0),
|
|
JS_PSG("promiseID", DebuggerObject::promiseIDGetter, 0),
|
|
JS_PSG("promiseDependentPromises",
|
|
DebuggerObject::promiseDependentPromisesGetter, 0),
|
|
JS_PS_END};
|
|
|
|
const JSFunctionSpec DebuggerObject::methods_[] = {
|
|
JS_FN("isExtensible", DebuggerObject::isExtensibleMethod, 0, 0),
|
|
JS_FN("isSealed", DebuggerObject::isSealedMethod, 0, 0),
|
|
JS_FN("isFrozen", DebuggerObject::isFrozenMethod, 0, 0),
|
|
JS_FN("getProperty", DebuggerObject::getPropertyMethod, 0, 0),
|
|
JS_FN("setProperty", DebuggerObject::setPropertyMethod, 0, 0),
|
|
JS_FN("getOwnPropertyNames", DebuggerObject::getOwnPropertyNamesMethod, 0,
|
|
0),
|
|
JS_FN("getOwnPropertySymbols", DebuggerObject::getOwnPropertySymbolsMethod,
|
|
0, 0),
|
|
JS_FN("getOwnPropertyDescriptor",
|
|
DebuggerObject::getOwnPropertyDescriptorMethod, 1, 0),
|
|
JS_FN("preventExtensions", DebuggerObject::preventExtensionsMethod, 0, 0),
|
|
JS_FN("seal", DebuggerObject::sealMethod, 0, 0),
|
|
JS_FN("freeze", DebuggerObject::freezeMethod, 0, 0),
|
|
JS_FN("defineProperty", DebuggerObject::definePropertyMethod, 2, 0),
|
|
JS_FN("defineProperties", DebuggerObject::definePropertiesMethod, 1, 0),
|
|
JS_FN("deleteProperty", DebuggerObject::deletePropertyMethod, 1, 0),
|
|
JS_FN("call", DebuggerObject::callMethod, 0, 0),
|
|
JS_FN("apply", DebuggerObject::applyMethod, 0, 0),
|
|
JS_FN("asEnvironment", DebuggerObject::asEnvironmentMethod, 0, 0),
|
|
JS_FN("forceLexicalInitializationByName",
|
|
DebuggerObject::forceLexicalInitializationByNameMethod, 1, 0),
|
|
JS_FN("executeInGlobal", DebuggerObject::executeInGlobalMethod, 1, 0),
|
|
JS_FN("executeInGlobalWithBindings",
|
|
DebuggerObject::executeInGlobalWithBindingsMethod, 2, 0),
|
|
JS_FN("makeDebuggeeValue", DebuggerObject::makeDebuggeeValueMethod, 1, 0),
|
|
JS_FN("unsafeDereference", DebuggerObject::unsafeDereferenceMethod, 0, 0),
|
|
JS_FN("unwrap", DebuggerObject::unwrapMethod, 0, 0),
|
|
JS_FS_END};
|
|
|
|
/* static */ NativeObject* DebuggerObject::initClass(
|
|
JSContext* cx, Handle<GlobalObject*> global, HandleObject debugCtor) {
|
|
RootedObject objProto(cx,
|
|
GlobalObject::getOrCreateObjectPrototype(cx, global));
|
|
|
|
RootedNativeObject objectProto(
|
|
cx, InitClass(cx, debugCtor, objProto, &class_, construct, 0, properties_,
|
|
methods_, nullptr, nullptr));
|
|
|
|
if (!objectProto) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (!DefinePropertiesAndFunctions(cx, objectProto, promiseProperties_,
|
|
nullptr)) {
|
|
return nullptr;
|
|
}
|
|
|
|
return objectProto;
|
|
}
|
|
|
|
/* static */ DebuggerObject* DebuggerObject::create(
|
|
JSContext* cx, HandleObject proto, HandleObject referent,
|
|
HandleNativeObject debugger) {
|
|
NewObjectKind newKind =
|
|
IsInsideNursery(referent) ? GenericObject : TenuredObject;
|
|
DebuggerObject* obj =
|
|
NewObjectWithGivenProto<DebuggerObject>(cx, proto, newKind);
|
|
if (!obj) {
|
|
return nullptr;
|
|
}
|
|
|
|
obj->setPrivateGCThing(referent);
|
|
obj->setReservedSlot(JSSLOT_DEBUGOBJECT_OWNER, ObjectValue(*debugger));
|
|
|
|
return obj;
|
|
}
|
|
|
|
bool DebuggerObject::isCallable() const { return referent()->isCallable(); }
|
|
|
|
bool DebuggerObject::isFunction() const { return referent()->is<JSFunction>(); }
|
|
|
|
bool DebuggerObject::isDebuggeeFunction() const {
|
|
return referent()->is<JSFunction>() &&
|
|
owner()->observesGlobal(&referent()->as<JSFunction>().global());
|
|
}
|
|
|
|
bool DebuggerObject::isBoundFunction() const {
|
|
MOZ_ASSERT(isDebuggeeFunction());
|
|
|
|
return referent()->isBoundFunction();
|
|
}
|
|
|
|
bool DebuggerObject::isArrowFunction() const {
|
|
MOZ_ASSERT(isDebuggeeFunction());
|
|
|
|
return RemoveAsyncWrapper(&referent()->as<JSFunction>())->isArrow();
|
|
}
|
|
|
|
bool DebuggerObject::isAsyncFunction() const {
|
|
MOZ_ASSERT(isDebuggeeFunction());
|
|
|
|
return RemoveAsyncWrapper(&referent()->as<JSFunction>())->isAsync();
|
|
}
|
|
|
|
bool DebuggerObject::isGeneratorFunction() const {
|
|
MOZ_ASSERT(isDebuggeeFunction());
|
|
|
|
JSFunction* fun = RemoveAsyncWrapper(&referent()->as<JSFunction>());
|
|
return fun->isGenerator();
|
|
}
|
|
|
|
bool DebuggerObject::isGlobal() const { return referent()->is<GlobalObject>(); }
|
|
|
|
bool DebuggerObject::isScriptedProxy() const {
|
|
return js::IsScriptedProxy(referent());
|
|
}
|
|
|
|
bool DebuggerObject::isPromise() const {
|
|
JSObject* referent = this->referent();
|
|
|
|
if (IsCrossCompartmentWrapper(referent)) {
|
|
referent = CheckedUnwrap(referent);
|
|
if (!referent) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return referent->is<PromiseObject>();
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getClassName(JSContext* cx,
|
|
HandleDebuggerObject object,
|
|
MutableHandleString result) {
|
|
RootedObject referent(cx, object->referent());
|
|
|
|
const char* className;
|
|
{
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
className = GetObjectClassName(cx, referent);
|
|
}
|
|
|
|
JSAtom* str = Atomize(cx, className, strlen(className));
|
|
if (!str) {
|
|
return false;
|
|
}
|
|
|
|
result.set(str);
|
|
return true;
|
|
}
|
|
|
|
JSAtom* DebuggerObject::name(JSContext* cx) const {
|
|
MOZ_ASSERT(isFunction());
|
|
|
|
JSAtom* atom = referent()->as<JSFunction>().explicitName();
|
|
if (atom) {
|
|
cx->markAtom(atom);
|
|
}
|
|
return atom;
|
|
}
|
|
|
|
JSAtom* DebuggerObject::displayName(JSContext* cx) const {
|
|
MOZ_ASSERT(isFunction());
|
|
|
|
JSAtom* atom = referent()->as<JSFunction>().displayAtom();
|
|
if (atom) {
|
|
cx->markAtom(atom);
|
|
}
|
|
return atom;
|
|
}
|
|
|
|
JS::PromiseState DebuggerObject::promiseState() const {
|
|
return promise()->state();
|
|
}
|
|
|
|
double DebuggerObject::promiseLifetime() const { return promise()->lifetime(); }
|
|
|
|
double DebuggerObject::promiseTimeToResolution() const {
|
|
MOZ_ASSERT(promiseState() != JS::PromiseState::Pending);
|
|
|
|
return promise()->timeToResolution();
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getParameterNames(
|
|
JSContext* cx, HandleDebuggerObject object,
|
|
MutableHandle<StringVector> result) {
|
|
MOZ_ASSERT(object->isDebuggeeFunction());
|
|
|
|
RootedFunction referent(
|
|
cx, RemoveAsyncWrapper(&object->referent()->as<JSFunction>()));
|
|
|
|
if (!result.growBy(referent->nargs())) {
|
|
return false;
|
|
}
|
|
if (referent->isInterpreted()) {
|
|
RootedScript script(cx, GetOrCreateFunctionScript(cx, referent));
|
|
if (!script) {
|
|
return false;
|
|
}
|
|
|
|
MOZ_ASSERT(referent->nargs() == script->numArgs());
|
|
|
|
if (referent->nargs() > 0) {
|
|
PositionalFormalParameterIter fi(script);
|
|
for (size_t i = 0; i < referent->nargs(); i++, fi++) {
|
|
MOZ_ASSERT(fi.argumentSlot() == i);
|
|
JSAtom* atom = fi.name();
|
|
if (atom) {
|
|
cx->markAtom(atom);
|
|
}
|
|
result[i].set(atom);
|
|
}
|
|
}
|
|
} else {
|
|
for (size_t i = 0; i < referent->nargs(); i++) {
|
|
result[i].set(nullptr);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getBoundTargetFunction(
|
|
JSContext* cx, HandleDebuggerObject object,
|
|
MutableHandleDebuggerObject result) {
|
|
MOZ_ASSERT(object->isBoundFunction());
|
|
|
|
RootedFunction referent(cx, &object->referent()->as<JSFunction>());
|
|
Debugger* dbg = object->owner();
|
|
|
|
RootedObject target(cx, referent->getBoundFunctionTarget());
|
|
return dbg->wrapDebuggeeObject(cx, target, result);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getBoundThis(JSContext* cx,
|
|
HandleDebuggerObject object,
|
|
MutableHandleValue result) {
|
|
MOZ_ASSERT(object->isBoundFunction());
|
|
|
|
RootedFunction referent(cx, &object->referent()->as<JSFunction>());
|
|
Debugger* dbg = object->owner();
|
|
|
|
result.set(referent->getBoundFunctionThis());
|
|
return dbg->wrapDebuggeeValue(cx, result);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getBoundArguments(
|
|
JSContext* cx, HandleDebuggerObject object,
|
|
MutableHandle<ValueVector> result) {
|
|
MOZ_ASSERT(object->isBoundFunction());
|
|
|
|
RootedFunction referent(cx, &object->referent()->as<JSFunction>());
|
|
Debugger* dbg = object->owner();
|
|
|
|
size_t length = referent->getBoundFunctionArgumentCount();
|
|
if (!result.resize(length)) {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < length; i++) {
|
|
result[i].set(referent->getBoundFunctionArgument(i));
|
|
if (!dbg->wrapDebuggeeValue(cx, result[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* static */ SavedFrame* Debugger::getObjectAllocationSite(JSObject& obj) {
|
|
JSObject* metadata = GetAllocationMetadata(&obj);
|
|
if (!metadata) {
|
|
return nullptr;
|
|
}
|
|
|
|
MOZ_ASSERT(!metadata->is<WrapperObject>());
|
|
return metadata->is<SavedFrame>() ? &metadata->as<SavedFrame>() : nullptr;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getAllocationSite(
|
|
JSContext* cx, HandleDebuggerObject object, MutableHandleObject result) {
|
|
RootedObject referent(cx, object->referent());
|
|
|
|
RootedObject allocSite(cx, Debugger::getObjectAllocationSite(*referent));
|
|
if (!cx->compartment()->wrap(cx, &allocSite)) {
|
|
return false;
|
|
}
|
|
|
|
result.set(allocSite);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getErrorReport(JSContext* cx,
|
|
HandleObject maybeError,
|
|
JSErrorReport*& report) {
|
|
JSObject* obj = maybeError;
|
|
if (IsCrossCompartmentWrapper(obj)) {
|
|
obj = CheckedUnwrap(obj);
|
|
}
|
|
|
|
if (!obj) {
|
|
ReportAccessDenied(cx);
|
|
return false;
|
|
}
|
|
|
|
if (!obj->is<ErrorObject>()) {
|
|
report = nullptr;
|
|
return true;
|
|
}
|
|
|
|
report = obj->as<ErrorObject>().getErrorReport();
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getErrorMessageName(
|
|
JSContext* cx, HandleDebuggerObject object, MutableHandleString result) {
|
|
RootedObject referent(cx, object->referent());
|
|
JSErrorReport* report;
|
|
if (!getErrorReport(cx, referent, report)) {
|
|
return false;
|
|
}
|
|
|
|
if (!report) {
|
|
result.set(nullptr);
|
|
return true;
|
|
}
|
|
|
|
const JSErrorFormatString* efs =
|
|
GetErrorMessage(nullptr, report->errorNumber);
|
|
if (!efs) {
|
|
result.set(nullptr);
|
|
return true;
|
|
}
|
|
|
|
RootedString str(cx, JS_NewStringCopyZ(cx, efs->name));
|
|
if (!str) {
|
|
return false;
|
|
}
|
|
result.set(str);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getErrorNotes(JSContext* cx,
|
|
HandleDebuggerObject object,
|
|
MutableHandleValue result) {
|
|
RootedObject referent(cx, object->referent());
|
|
JSErrorReport* report;
|
|
if (!getErrorReport(cx, referent, report)) {
|
|
return false;
|
|
}
|
|
|
|
if (!report) {
|
|
result.setUndefined();
|
|
return true;
|
|
}
|
|
|
|
RootedObject errorNotesArray(cx, CreateErrorNotesArray(cx, report));
|
|
if (!errorNotesArray) {
|
|
return false;
|
|
}
|
|
|
|
if (!cx->compartment()->wrap(cx, &errorNotesArray)) {
|
|
return false;
|
|
}
|
|
result.setObject(*errorNotesArray);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getErrorLineNumber(
|
|
JSContext* cx, HandleDebuggerObject object, MutableHandleValue result) {
|
|
RootedObject referent(cx, object->referent());
|
|
JSErrorReport* report;
|
|
if (!getErrorReport(cx, referent, report)) {
|
|
return false;
|
|
}
|
|
|
|
if (!report) {
|
|
result.setUndefined();
|
|
return true;
|
|
}
|
|
|
|
result.setNumber(report->lineno);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getErrorColumnNumber(
|
|
JSContext* cx, HandleDebuggerObject object, MutableHandleValue result) {
|
|
RootedObject referent(cx, object->referent());
|
|
JSErrorReport* report;
|
|
if (!getErrorReport(cx, referent, report)) {
|
|
return false;
|
|
}
|
|
|
|
if (!report) {
|
|
result.setUndefined();
|
|
return true;
|
|
}
|
|
|
|
result.setNumber(report->column);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getPromiseValue(JSContext* cx,
|
|
HandleDebuggerObject object,
|
|
MutableHandleValue result) {
|
|
MOZ_ASSERT(object->promiseState() == JS::PromiseState::Fulfilled);
|
|
|
|
result.set(object->promise()->value());
|
|
return object->owner()->wrapDebuggeeValue(cx, result);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getPromiseReason(JSContext* cx,
|
|
HandleDebuggerObject object,
|
|
MutableHandleValue result) {
|
|
MOZ_ASSERT(object->promiseState() == JS::PromiseState::Rejected);
|
|
|
|
result.set(object->promise()->reason());
|
|
return object->owner()->wrapDebuggeeValue(cx, result);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::isExtensible(JSContext* cx,
|
|
HandleDebuggerObject object,
|
|
bool& result) {
|
|
RootedObject referent(cx, object->referent());
|
|
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
|
|
ErrorCopier ec(ar);
|
|
return IsExtensible(cx, referent, &result);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::isSealed(JSContext* cx,
|
|
HandleDebuggerObject object,
|
|
bool& result) {
|
|
RootedObject referent(cx, object->referent());
|
|
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
|
|
ErrorCopier ec(ar);
|
|
return TestIntegrityLevel(cx, referent, IntegrityLevel::Sealed, &result);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::isFrozen(JSContext* cx,
|
|
HandleDebuggerObject object,
|
|
bool& result) {
|
|
RootedObject referent(cx, object->referent());
|
|
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
|
|
ErrorCopier ec(ar);
|
|
return TestIntegrityLevel(cx, referent, IntegrityLevel::Frozen, &result);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getPrototypeOf(
|
|
JSContext* cx, HandleDebuggerObject object,
|
|
MutableHandleDebuggerObject result) {
|
|
RootedObject referent(cx, object->referent());
|
|
Debugger* dbg = object->owner();
|
|
|
|
RootedObject proto(cx);
|
|
{
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
if (!GetPrototype(cx, referent, &proto)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!proto) {
|
|
result.set(nullptr);
|
|
return true;
|
|
}
|
|
|
|
return dbg->wrapDebuggeeObject(cx, proto, result);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getOwnPropertyNames(
|
|
JSContext* cx, HandleDebuggerObject object,
|
|
MutableHandle<IdVector> result) {
|
|
RootedObject referent(cx, object->referent());
|
|
|
|
AutoIdVector ids(cx);
|
|
{
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
|
|
ErrorCopier ec(ar);
|
|
if (!GetPropertyKeys(cx, referent, JSITER_OWNONLY | JSITER_HIDDEN, &ids)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (size_t i = 0; i < ids.length(); i++) {
|
|
cx->markId(ids[i]);
|
|
}
|
|
|
|
return result.append(ids.begin(), ids.end());
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getOwnPropertySymbols(
|
|
JSContext* cx, HandleDebuggerObject object,
|
|
MutableHandle<IdVector> result) {
|
|
RootedObject referent(cx, object->referent());
|
|
|
|
AutoIdVector ids(cx);
|
|
{
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
|
|
ErrorCopier ec(ar);
|
|
if (!GetPropertyKeys(cx, referent,
|
|
JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS |
|
|
JSITER_SYMBOLSONLY,
|
|
&ids))
|
|
return false;
|
|
}
|
|
|
|
for (size_t i = 0; i < ids.length(); i++) {
|
|
cx->markId(ids[i]);
|
|
}
|
|
|
|
return result.append(ids.begin(), ids.end());
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getOwnPropertyDescriptor(
|
|
JSContext* cx, HandleDebuggerObject object, HandleId id,
|
|
MutableHandle<PropertyDescriptor> desc) {
|
|
RootedObject referent(cx, object->referent());
|
|
Debugger* dbg = object->owner();
|
|
|
|
// Bug: This can cause the debuggee to run!
|
|
{
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
|
|
cx->markId(id);
|
|
|
|
ErrorCopier ec(ar);
|
|
if (!GetOwnPropertyDescriptor(cx, referent, 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());
|
|
}
|
|
|
|
// Avoid tripping same-compartment assertions in
|
|
// JS::FromPropertyDescriptor().
|
|
desc.object().set(object);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::preventExtensions(
|
|
JSContext* cx, HandleDebuggerObject object) {
|
|
RootedObject referent(cx, object->referent());
|
|
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
|
|
ErrorCopier ec(ar);
|
|
return PreventExtensions(cx, referent);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::seal(JSContext* cx,
|
|
HandleDebuggerObject object) {
|
|
RootedObject referent(cx, object->referent());
|
|
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
|
|
ErrorCopier ec(ar);
|
|
return SetIntegrityLevel(cx, referent, IntegrityLevel::Sealed);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::freeze(JSContext* cx,
|
|
HandleDebuggerObject object) {
|
|
RootedObject referent(cx, object->referent());
|
|
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
|
|
ErrorCopier ec(ar);
|
|
return SetIntegrityLevel(cx, referent, IntegrityLevel::Frozen);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::defineProperty(
|
|
JSContext* cx, HandleDebuggerObject object, HandleId id,
|
|
Handle<PropertyDescriptor> desc_) {
|
|
RootedObject referent(cx, object->referent());
|
|
Debugger* dbg = object->owner();
|
|
|
|
Rooted<PropertyDescriptor> desc(cx, desc_);
|
|
if (!dbg->unwrapPropertyDescriptor(cx, referent, &desc)) {
|
|
return false;
|
|
}
|
|
JS_TRY_OR_RETURN_FALSE(cx, CheckPropertyDescriptorAccessors(cx, desc));
|
|
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
|
|
if (!cx->compartment()->wrap(cx, &desc)) {
|
|
return false;
|
|
}
|
|
cx->markId(id);
|
|
|
|
ErrorCopier ec(ar);
|
|
if (!DefineProperty(cx, referent, id, desc)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::defineProperties(
|
|
JSContext* cx, HandleDebuggerObject object, Handle<IdVector> ids,
|
|
Handle<PropertyDescriptorVector> descs_) {
|
|
RootedObject referent(cx, object->referent());
|
|
Debugger* dbg = object->owner();
|
|
|
|
Rooted<PropertyDescriptorVector> descs(cx, PropertyDescriptorVector(cx));
|
|
if (!descs.append(descs_.begin(), descs_.end())) {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < descs.length(); i++) {
|
|
if (!dbg->unwrapPropertyDescriptor(cx, referent, descs[i])) {
|
|
return false;
|
|
}
|
|
JS_TRY_OR_RETURN_FALSE(cx, CheckPropertyDescriptorAccessors(cx, descs[i]));
|
|
}
|
|
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
|
|
for (size_t i = 0; i < descs.length(); i++) {
|
|
if (!cx->compartment()->wrap(cx, descs[i])) {
|
|
return false;
|
|
}
|
|
cx->markId(ids[i]);
|
|
}
|
|
|
|
ErrorCopier ec(ar);
|
|
for (size_t i = 0; i < descs.length(); i++) {
|
|
if (!DefineProperty(cx, referent, ids[i], descs[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::deleteProperty(JSContext* cx,
|
|
HandleDebuggerObject object,
|
|
HandleId id,
|
|
ObjectOpResult& result) {
|
|
RootedObject referent(cx, object->referent());
|
|
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
|
|
cx->markId(id);
|
|
|
|
ErrorCopier ec(ar);
|
|
return DeleteProperty(cx, referent, id, result);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getProperty(JSContext* cx,
|
|
HandleDebuggerObject object,
|
|
HandleId id,
|
|
HandleValue receiver_,
|
|
MutableHandleValue result) {
|
|
RootedObject referent(cx, object->referent());
|
|
Debugger* dbg = object->owner();
|
|
|
|
// Unwrap Debugger.Objects. This happens in the debugger's compartment since
|
|
// that is where any exceptions must be reported.
|
|
RootedValue receiver(cx, receiver_);
|
|
if (!dbg->unwrapDebuggeeValue(cx, &receiver)) {
|
|
return false;
|
|
}
|
|
|
|
// Enter the debuggee compartment and rewrap all input value for that
|
|
// compartment. (Rewrapping always takes place in the destination
|
|
// compartment.)
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
if (!cx->compartment()->wrap(cx, &referent) ||
|
|
!cx->compartment()->wrap(cx, &receiver)) {
|
|
return false;
|
|
}
|
|
cx->markId(id);
|
|
|
|
LeaveDebuggeeNoExecute nnx(cx);
|
|
|
|
bool ok = GetProperty(cx, referent, receiver, id, result);
|
|
|
|
return dbg->receiveCompletionValue(ar, ok, result, result);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::setProperty(JSContext* cx,
|
|
HandleDebuggerObject object,
|
|
HandleId id, HandleValue value_,
|
|
HandleValue receiver_,
|
|
MutableHandleValue result) {
|
|
RootedObject referent(cx, object->referent());
|
|
Debugger* dbg = object->owner();
|
|
|
|
// Unwrap Debugger.Objects. This happens in the debugger's compartment since
|
|
// that is where any exceptions must be reported.
|
|
RootedValue value(cx, value_);
|
|
RootedValue receiver(cx, receiver_);
|
|
if (!dbg->unwrapDebuggeeValue(cx, &value) ||
|
|
!dbg->unwrapDebuggeeValue(cx, &receiver)) {
|
|
return false;
|
|
}
|
|
|
|
// Enter the debuggee compartment and rewrap all input value for that
|
|
// compartment. (Rewrapping always takes place in the destination
|
|
// compartment.)
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
if (!cx->compartment()->wrap(cx, &referent) ||
|
|
!cx->compartment()->wrap(cx, &value) ||
|
|
!cx->compartment()->wrap(cx, &receiver)) {
|
|
return false;
|
|
}
|
|
cx->markId(id);
|
|
|
|
LeaveDebuggeeNoExecute nnx(cx);
|
|
|
|
ObjectOpResult opResult;
|
|
bool ok = SetProperty(cx, referent, id, value, receiver, opResult);
|
|
|
|
result.setBoolean(ok && opResult.reallyOk());
|
|
return dbg->receiveCompletionValue(ar, ok, result, result);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::call(JSContext* cx,
|
|
HandleDebuggerObject object,
|
|
HandleValue thisv_,
|
|
Handle<ValueVector> args,
|
|
MutableHandleValue result) {
|
|
RootedObject referent(cx, object->referent());
|
|
Debugger* dbg = object->owner();
|
|
|
|
if (!referent->isCallable()) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_INCOMPATIBLE_PROTO, "Debugger.Object",
|
|
"call", referent->getClass()->name);
|
|
return false;
|
|
}
|
|
|
|
RootedValue calleev(cx, ObjectValue(*referent));
|
|
|
|
// Unwrap Debugger.Objects. This happens in the debugger's compartment since
|
|
// that is where any exceptions must be reported.
|
|
RootedValue thisv(cx, thisv_);
|
|
if (!dbg->unwrapDebuggeeValue(cx, &thisv)) {
|
|
return false;
|
|
}
|
|
Rooted<ValueVector> args2(cx, ValueVector(cx));
|
|
if (!args2.append(args.begin(), args.end())) {
|
|
return false;
|
|
}
|
|
for (unsigned i = 0; i < args2.length(); ++i) {
|
|
if (!dbg->unwrapDebuggeeValue(cx, args2[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Enter the debuggee compartment and rewrap all input value for that
|
|
// compartment. (Rewrapping always takes place in the destination
|
|
// compartment.)
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
if (!cx->compartment()->wrap(cx, &calleev) ||
|
|
!cx->compartment()->wrap(cx, &thisv)) {
|
|
return false;
|
|
}
|
|
for (unsigned i = 0; i < args2.length(); ++i) {
|
|
if (!cx->compartment()->wrap(cx, args2[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Call the function. Use receiveCompletionValue to return to the debugger
|
|
// compartment and populate args.rval().
|
|
LeaveDebuggeeNoExecute nnx(cx);
|
|
|
|
bool ok;
|
|
{
|
|
InvokeArgs invokeArgs(cx);
|
|
|
|
ok = invokeArgs.init(cx, args2.length());
|
|
if (ok) {
|
|
for (size_t i = 0; i < args2.length(); ++i) {
|
|
invokeArgs[i].set(args2[i]);
|
|
}
|
|
|
|
ok = js::Call(cx, calleev, thisv, invokeArgs, result);
|
|
}
|
|
}
|
|
|
|
return dbg->receiveCompletionValue(ar, ok, result, result);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::forceLexicalInitializationByName(
|
|
JSContext* cx, HandleDebuggerObject object, HandleId id, bool& result) {
|
|
if (!JSID_IS_STRING(id)) {
|
|
JS_ReportErrorNumberASCII(
|
|
cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE,
|
|
"Debugger.Object.prototype.forceLexicalInitializationByName", "string",
|
|
InformalValueTypeName(IdToValue(id)));
|
|
return false;
|
|
}
|
|
|
|
MOZ_ASSERT(object->isGlobal());
|
|
|
|
Rooted<GlobalObject*> referent(cx, &object->referent()->as<GlobalObject>());
|
|
|
|
RootedObject globalLexical(cx, &referent->lexicalEnvironment());
|
|
RootedObject pobj(cx);
|
|
Rooted<PropertyResult> prop(cx);
|
|
if (!LookupProperty(cx, globalLexical, id, &pobj, &prop)) {
|
|
return false;
|
|
}
|
|
|
|
result = false;
|
|
if (prop) {
|
|
MOZ_ASSERT(prop.isNativeProperty());
|
|
Shape* shape = prop.shape();
|
|
Value v = globalLexical->as<NativeObject>().getSlot(shape->slot());
|
|
if (shape->isDataProperty() && v.isMagic() &&
|
|
v.whyMagic() == JS_UNINITIALIZED_LEXICAL) {
|
|
globalLexical->as<NativeObject>().setSlot(shape->slot(),
|
|
UndefinedValue());
|
|
result = true;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::executeInGlobal(
|
|
JSContext* cx, HandleDebuggerObject object,
|
|
mozilla::Range<const char16_t> chars, HandleObject bindings,
|
|
const EvalOptions& options, ResumeMode& resumeMode,
|
|
MutableHandleValue value) {
|
|
MOZ_ASSERT(object->isGlobal());
|
|
|
|
Rooted<GlobalObject*> referent(cx, &object->referent()->as<GlobalObject>());
|
|
Debugger* dbg = object->owner();
|
|
|
|
RootedObject globalLexical(cx, &referent->lexicalEnvironment());
|
|
return DebuggerGenericEval(cx, chars, bindings, options, resumeMode, value,
|
|
dbg, globalLexical, nullptr);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::makeDebuggeeValue(JSContext* cx,
|
|
HandleDebuggerObject object,
|
|
HandleValue value_,
|
|
MutableHandleValue result) {
|
|
RootedObject referent(cx, object->referent());
|
|
Debugger* dbg = object->owner();
|
|
|
|
RootedValue value(cx, value_);
|
|
|
|
// Non-objects are already debuggee values.
|
|
if (value.isObject()) {
|
|
// Enter this Debugger.Object's referent's compartment, and wrap the
|
|
// argument as appropriate for references from there.
|
|
{
|
|
Maybe<AutoRealm> ar;
|
|
EnterDebuggeeObjectRealm(cx, ar, referent);
|
|
if (!cx->compartment()->wrap(cx, &value)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Back in the debugger's compartment, produce a new Debugger.Object
|
|
// instance referring to the wrapped argument.
|
|
if (!dbg->wrapDebuggeeValue(cx, &value)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
result.set(value);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::unsafeDereference(
|
|
JSContext* cx, HandleDebuggerObject object, MutableHandleObject result) {
|
|
RootedObject referent(cx, object->referent());
|
|
|
|
if (!cx->compartment()->wrap(cx, &referent)) {
|
|
return false;
|
|
}
|
|
|
|
// Wrapping should return the WindowProxy.
|
|
MOZ_ASSERT(!IsWindow(referent));
|
|
|
|
result.set(referent);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::unwrap(JSContext* cx,
|
|
HandleDebuggerObject object,
|
|
MutableHandleDebuggerObject result) {
|
|
RootedObject referent(cx, object->referent());
|
|
Debugger* dbg = object->owner();
|
|
|
|
RootedObject unwrapped(cx, UnwrapOneChecked(referent));
|
|
if (!unwrapped) {
|
|
result.set(nullptr);
|
|
return true;
|
|
}
|
|
|
|
// Don't allow unwrapping to create a D.O whose referent is in an
|
|
// invisible-to-Debugger compartment. (If our referent is a *wrapper* to such,
|
|
// and the wrapper is in a visible compartment, that's fine.)
|
|
if (unwrapped->compartment()->invisibleToDebugger()) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_INVISIBLE_COMPARTMENT);
|
|
return false;
|
|
}
|
|
|
|
return dbg->wrapDebuggeeObject(cx, unwrapped, result);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::requireGlobal(JSContext* cx,
|
|
HandleDebuggerObject object) {
|
|
if (!object->isGlobal()) {
|
|
RootedObject referent(cx, object->referent());
|
|
|
|
const char* isWrapper = "";
|
|
const char* isWindowProxy = "";
|
|
|
|
// Help the poor programmer by pointing out wrappers around globals...
|
|
if (referent->is<WrapperObject>()) {
|
|
referent = js::UncheckedUnwrap(referent);
|
|
isWrapper = "a wrapper around ";
|
|
}
|
|
|
|
// ... and WindowProxies around Windows.
|
|
if (IsWindowProxy(referent)) {
|
|
referent = ToWindowIfWindowProxy(referent);
|
|
isWindowProxy = "a WindowProxy referring to ";
|
|
}
|
|
|
|
RootedValue dbgobj(cx, ObjectValue(*object));
|
|
if (referent->is<GlobalObject>()) {
|
|
ReportValueError(cx, JSMSG_DEBUG_WRAPPER_IN_WAY, JSDVG_SEARCH_STACK,
|
|
dbgobj, nullptr, isWrapper, isWindowProxy);
|
|
} else {
|
|
ReportValueError(cx, JSMSG_DEBUG_BAD_REFERENT, JSDVG_SEARCH_STACK, dbgobj,
|
|
nullptr, "a global object");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::requirePromise(JSContext* cx,
|
|
HandleDebuggerObject object) {
|
|
RootedObject referent(cx, object->referent());
|
|
|
|
if (IsCrossCompartmentWrapper(referent)) {
|
|
referent = CheckedUnwrap(referent);
|
|
if (!referent) {
|
|
ReportAccessDenied(cx);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!referent->is<PromiseObject>()) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_NOT_EXPECTED_TYPE, "Debugger", "Promise",
|
|
object->getClass()->name);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getScriptedProxyTarget(
|
|
JSContext* cx, HandleDebuggerObject object,
|
|
MutableHandleDebuggerObject result) {
|
|
MOZ_ASSERT(object->isScriptedProxy());
|
|
RootedObject referent(cx, object->referent());
|
|
Debugger* dbg = object->owner();
|
|
RootedObject unwrapped(cx, js::GetProxyTargetObject(referent));
|
|
if (!unwrapped) {
|
|
result.set(nullptr);
|
|
return true;
|
|
}
|
|
return dbg->wrapDebuggeeObject(cx, unwrapped, result);
|
|
}
|
|
|
|
/* static */ bool DebuggerObject::getScriptedProxyHandler(
|
|
JSContext* cx, HandleDebuggerObject object,
|
|
MutableHandleDebuggerObject result) {
|
|
MOZ_ASSERT(object->isScriptedProxy());
|
|
RootedObject referent(cx, object->referent());
|
|
Debugger* dbg = object->owner();
|
|
RootedObject unwrapped(cx, ScriptedProxyHandler::handlerObject(referent));
|
|
if (!unwrapped) {
|
|
result.set(nullptr);
|
|
return true;
|
|
}
|
|
return dbg->wrapDebuggeeObject(cx, unwrapped, result);
|
|
}
|
|
|
|
/*** 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);
|
|
}
|
|
}
|
|
|
|
static DebuggerEnvironment* DebuggerEnvironment_checkThis(
|
|
JSContext* cx, const CallArgs& args, const char* fnname,
|
|
bool requireDebuggee) {
|
|
JSObject* thisobj = NonNullObject(cx, args.thisv());
|
|
if (!thisobj) {
|
|
return nullptr;
|
|
}
|
|
if (thisobj->getClass() != &DebuggerEnvironment::class_) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_INCOMPATIBLE_PROTO, "Debugger.Environment",
|
|
fnname, thisobj->getClass()->name);
|
|
return nullptr;
|
|
}
|
|
|
|
// Forbid Debugger.Environment.prototype, which is of class
|
|
// DebuggerEnvironment::class_ but isn't a real working Debugger.Environment.
|
|
// The prototype object is distinguished by having no referent.
|
|
DebuggerEnvironment* nthisobj = &thisobj->as<DebuggerEnvironment>();
|
|
if (!nthisobj->getPrivate()) {
|
|
JS_ReportErrorNumberASCII(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->nonCCWGlobal())) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_NOT_DEBUGGEE,
|
|
"Debugger.Environment", "environment");
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
return nthisobj;
|
|
}
|
|
|
|
#define THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, fnname, args, environment) \
|
|
CallArgs args = CallArgsFromVp(argc, vp); \
|
|
Rooted<DebuggerEnvironment*> environment( \
|
|
cx, DebuggerEnvironment_checkThis(cx, args, fnname, false)); \
|
|
if (!environment) return false;
|
|
|
|
/* static */ bool DebuggerEnvironment::construct(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR,
|
|
"Debugger.Environment");
|
|
return false;
|
|
}
|
|
|
|
static bool IsDeclarative(Env* env) {
|
|
return env->is<DebugEnvironmentProxy>() &&
|
|
env->as<DebugEnvironmentProxy>().isForDeclarative();
|
|
}
|
|
|
|
template <typename T>
|
|
static bool IsDebugEnvironmentWrapper(Env* env) {
|
|
return env->is<DebugEnvironmentProxy>() &&
|
|
env->as<DebugEnvironmentProxy>().environment().is<T>();
|
|
}
|
|
|
|
bool DebuggerEnvironment::typeGetter(JSContext* cx, unsigned argc, Value* vp) {
|
|
THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get type", args, environment);
|
|
|
|
if (!environment->requireDebuggee(cx)) {
|
|
return false;
|
|
}
|
|
|
|
DebuggerEnvironmentType type = environment->type();
|
|
|
|
const char* s;
|
|
switch (type) {
|
|
case DebuggerEnvironmentType::Declarative:
|
|
s = "declarative";
|
|
break;
|
|
case DebuggerEnvironmentType::With:
|
|
s = "with";
|
|
break;
|
|
case DebuggerEnvironmentType::Object:
|
|
s = "object";
|
|
break;
|
|
}
|
|
|
|
JSAtom* str = Atomize(cx, s, strlen(s), PinAtom);
|
|
if (!str) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setString(str);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerEnvironment::parentGetter(JSContext* cx,
|
|
unsigned argc, Value* vp) {
|
|
THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get type", args, environment);
|
|
|
|
if (!environment->requireDebuggee(cx)) {
|
|
return false;
|
|
}
|
|
|
|
RootedDebuggerEnvironment result(cx);
|
|
if (!environment->getParent(cx, &result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObjectOrNull(result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerEnvironment::objectGetter(JSContext* cx,
|
|
unsigned argc, Value* vp) {
|
|
THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get type", args, environment);
|
|
|
|
if (!environment->requireDebuggee(cx)) {
|
|
return false;
|
|
}
|
|
|
|
if (environment->type() == DebuggerEnvironmentType::Declarative) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_NO_ENV_OBJECT);
|
|
return false;
|
|
}
|
|
|
|
RootedDebuggerObject result(cx);
|
|
if (!environment->getObject(cx, &result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObject(*result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerEnvironment::calleeGetter(JSContext* cx,
|
|
unsigned argc, Value* vp) {
|
|
THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get callee", args, environment);
|
|
|
|
if (!environment->requireDebuggee(cx)) {
|
|
return false;
|
|
}
|
|
|
|
RootedDebuggerObject result(cx);
|
|
if (!environment->getCallee(cx, &result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObjectOrNull(result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerEnvironment::inspectableGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get inspectable", args, environment);
|
|
|
|
args.rval().setBoolean(environment->isDebuggee());
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerEnvironment::optimizedOutGetter(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get optimizedOut", args,
|
|
environment);
|
|
|
|
args.rval().setBoolean(environment->isOptimized());
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerEnvironment::namesMethod(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "names", args, environment);
|
|
|
|
if (!environment->requireDebuggee(cx)) {
|
|
return false;
|
|
}
|
|
|
|
Rooted<IdVector> ids(cx, IdVector(cx));
|
|
if (!DebuggerEnvironment::getNames(cx, environment, &ids)) {
|
|
return false;
|
|
}
|
|
|
|
RootedObject obj(cx, IdVectorToArray(cx, ids));
|
|
if (!obj) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObject(*obj);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerEnvironment::findMethod(JSContext* cx, unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "find", args, environment);
|
|
if (!args.requireAtLeast(cx, "Debugger.Environment.find", 1)) {
|
|
return false;
|
|
}
|
|
|
|
if (!environment->requireDebuggee(cx)) {
|
|
return false;
|
|
}
|
|
|
|
RootedId id(cx);
|
|
if (!ValueToIdentifier(cx, args[0], &id)) {
|
|
return false;
|
|
}
|
|
|
|
RootedDebuggerEnvironment result(cx);
|
|
if (!DebuggerEnvironment::find(cx, environment, id, &result)) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setObjectOrNull(result);
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerEnvironment::getVariableMethod(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "getVariable", args, environment);
|
|
if (!args.requireAtLeast(cx, "Debugger.Environment.getVariable", 1)) {
|
|
return false;
|
|
}
|
|
|
|
if (!environment->requireDebuggee(cx)) {
|
|
return false;
|
|
}
|
|
|
|
RootedId id(cx);
|
|
if (!ValueToIdentifier(cx, args[0], &id)) {
|
|
return false;
|
|
}
|
|
|
|
return DebuggerEnvironment::getVariable(cx, environment, id, args.rval());
|
|
}
|
|
|
|
/* static */ bool DebuggerEnvironment::setVariableMethod(JSContext* cx,
|
|
unsigned argc,
|
|
Value* vp) {
|
|
THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "setVariable", args, environment);
|
|
if (!args.requireAtLeast(cx, "Debugger.Environment.setVariable", 2)) {
|
|
return false;
|
|
}
|
|
|
|
if (!environment->requireDebuggee(cx)) {
|
|
return false;
|
|
}
|
|
|
|
RootedId id(cx);
|
|
if (!ValueToIdentifier(cx, args[0], &id)) {
|
|
return false;
|
|
}
|
|
|
|
if (!DebuggerEnvironment::setVariable(cx, environment, id, args[1])) {
|
|
return false;
|
|
}
|
|
|
|
args.rval().setUndefined();
|
|
return true;
|
|
}
|
|
|
|
bool DebuggerEnvironment::requireDebuggee(JSContext* cx) const {
|
|
if (!isDebuggee()) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_NOT_DEBUGGEE, "Debugger.Environment",
|
|
"environment");
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
const JSPropertySpec DebuggerEnvironment::properties_[] = {
|
|
JS_PSG("type", DebuggerEnvironment::typeGetter, 0),
|
|
JS_PSG("parent", DebuggerEnvironment::parentGetter, 0),
|
|
JS_PSG("object", DebuggerEnvironment::objectGetter, 0),
|
|
JS_PSG("callee", DebuggerEnvironment::calleeGetter, 0),
|
|
JS_PSG("inspectable", DebuggerEnvironment::inspectableGetter, 0),
|
|
JS_PSG("optimizedOut", DebuggerEnvironment::optimizedOutGetter, 0),
|
|
JS_PS_END};
|
|
|
|
const JSFunctionSpec DebuggerEnvironment::methods_[] = {
|
|
JS_FN("names", DebuggerEnvironment::namesMethod, 0, 0),
|
|
JS_FN("find", DebuggerEnvironment::findMethod, 1, 0),
|
|
JS_FN("getVariable", DebuggerEnvironment::getVariableMethod, 1, 0),
|
|
JS_FN("setVariable", DebuggerEnvironment::setVariableMethod, 2, 0),
|
|
JS_FS_END};
|
|
|
|
/* static */ NativeObject* DebuggerEnvironment::initClass(
|
|
JSContext* cx, HandleObject dbgCtor, Handle<GlobalObject*> global) {
|
|
RootedObject objProto(cx,
|
|
GlobalObject::getOrCreateObjectPrototype(cx, global));
|
|
|
|
return InitClass(cx, dbgCtor, objProto, &DebuggerEnvironment::class_,
|
|
construct, 0, properties_, methods_, nullptr, nullptr);
|
|
}
|
|
|
|
/* static */ DebuggerEnvironment* DebuggerEnvironment::create(
|
|
JSContext* cx, HandleObject proto, HandleObject referent,
|
|
HandleNativeObject debugger) {
|
|
NewObjectKind newKind =
|
|
IsInsideNursery(referent) ? GenericObject : TenuredObject;
|
|
DebuggerEnvironment* obj =
|
|
NewObjectWithGivenProto<DebuggerEnvironment>(cx, proto, newKind);
|
|
if (!obj) {
|
|
return nullptr;
|
|
}
|
|
|
|
obj->setPrivateGCThing(referent);
|
|
obj->setReservedSlot(OWNER_SLOT, ObjectValue(*debugger));
|
|
|
|
return obj;
|
|
}
|
|
|
|
/* static */ DebuggerEnvironmentType DebuggerEnvironment::type() const {
|
|
// Don't bother switching compartments just to check env's type.
|
|
if (IsDeclarative(referent())) {
|
|
return DebuggerEnvironmentType::Declarative;
|
|
}
|
|
if (IsDebugEnvironmentWrapper<WithEnvironmentObject>(referent())) {
|
|
return DebuggerEnvironmentType::With;
|
|
}
|
|
return DebuggerEnvironmentType::Object;
|
|
}
|
|
|
|
bool DebuggerEnvironment::getParent(
|
|
JSContext* cx, MutableHandleDebuggerEnvironment result) const {
|
|
// Don't bother switching compartments just to get env's parent.
|
|
Rooted<Env*> parent(cx, referent()->enclosingEnvironment());
|
|
if (!parent) {
|
|
result.set(nullptr);
|
|
return true;
|
|
}
|
|
|
|
return owner()->wrapEnvironment(cx, parent, result);
|
|
}
|
|
|
|
bool DebuggerEnvironment::getObject(JSContext* cx,
|
|
MutableHandleDebuggerObject result) const {
|
|
MOZ_ASSERT(type() != DebuggerEnvironmentType::Declarative);
|
|
|
|
// Don't bother switching compartments just to get env's object.
|
|
RootedObject object(cx);
|
|
if (IsDebugEnvironmentWrapper<WithEnvironmentObject>(referent())) {
|
|
object.set(&referent()
|
|
->as<DebugEnvironmentProxy>()
|
|
.environment()
|
|
.as<WithEnvironmentObject>()
|
|
.object());
|
|
} else if (IsDebugEnvironmentWrapper<NonSyntacticVariablesObject>(
|
|
referent())) {
|
|
object.set(&referent()
|
|
->as<DebugEnvironmentProxy>()
|
|
.environment()
|
|
.as<NonSyntacticVariablesObject>());
|
|
} else {
|
|
object.set(referent());
|
|
MOZ_ASSERT(!object->is<DebugEnvironmentProxy>());
|
|
}
|
|
|
|
return owner()->wrapDebuggeeObject(cx, object, result);
|
|
}
|
|
|
|
bool DebuggerEnvironment::getCallee(JSContext* cx,
|
|
MutableHandleDebuggerObject result) const {
|
|
if (!referent()->is<DebugEnvironmentProxy>()) {
|
|
result.set(nullptr);
|
|
return true;
|
|
}
|
|
|
|
JSObject& scope = referent()->as<DebugEnvironmentProxy>().environment();
|
|
if (!scope.is<CallObject>()) {
|
|
result.set(nullptr);
|
|
return true;
|
|
}
|
|
|
|
RootedObject callee(cx, &scope.as<CallObject>().callee());
|
|
if (IsInternalFunctionObject(*callee)) {
|
|
result.set(nullptr);
|
|
return true;
|
|
}
|
|
|
|
return owner()->wrapDebuggeeObject(cx, callee, result);
|
|
}
|
|
|
|
bool DebuggerEnvironment::isDebuggee() const {
|
|
MOZ_ASSERT(referent());
|
|
MOZ_ASSERT(!referent()->is<EnvironmentObject>());
|
|
|
|
return owner()->observesGlobal(&referent()->nonCCWGlobal());
|
|
}
|
|
|
|
bool DebuggerEnvironment::isOptimized() const {
|
|
return referent()->is<DebugEnvironmentProxy>() &&
|
|
referent()->as<DebugEnvironmentProxy>().isOptimizedOut();
|
|
}
|
|
|
|
/* static */ bool DebuggerEnvironment::getNames(
|
|
JSContext* cx, HandleDebuggerEnvironment environment,
|
|
MutableHandle<IdVector> result) {
|
|
MOZ_ASSERT(environment->isDebuggee());
|
|
|
|
Rooted<Env*> referent(cx, environment->referent());
|
|
|
|
AutoIdVector ids(cx);
|
|
{
|
|
Maybe<AutoRealm> ar;
|
|
ar.emplace(cx, referent);
|
|
|
|
ErrorCopier ec(ar);
|
|
if (!GetPropertyKeys(cx, referent, JSITER_HIDDEN, &ids)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (size_t i = 0; i < ids.length(); ++i) {
|
|
jsid id = ids[i];
|
|
if (JSID_IS_ATOM(id) && IsIdentifier(JSID_TO_ATOM(id))) {
|
|
cx->markId(id);
|
|
if (!result.append(id)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* static */ bool DebuggerEnvironment::find(
|
|
JSContext* cx, HandleDebuggerEnvironment environment, HandleId id,
|
|
MutableHandleDebuggerEnvironment result) {
|
|
MOZ_ASSERT(environment->isDebuggee());
|
|
|
|
Rooted<Env*> env(cx, environment->referent());
|
|
Debugger* dbg = environment->owner();
|
|
|
|
{
|
|
Maybe<AutoRealm> ar;
|
|
ar.emplace(cx, env);
|
|
|
|
cx->markId(id);
|
|
|
|
// This can trigger resolve hooks.
|
|
ErrorCopier ec(ar);
|
|
for (; env; env = env->enclosingEnvironment()) {
|
|
bool found;
|
|
if (!HasProperty(cx, env, id, &found)) {
|
|
return false;
|
|
}
|
|
if (found) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!env) {
|
|
result.set(nullptr);
|
|
return true;
|
|
}
|
|
|
|
return dbg->wrapEnvironment(cx, env, result);
|
|
}
|
|
|
|
/* static */ bool DebuggerEnvironment::getVariable(
|
|
JSContext* cx, HandleDebuggerEnvironment environment, HandleId id,
|
|
MutableHandleValue result) {
|
|
MOZ_ASSERT(environment->isDebuggee());
|
|
|
|
Rooted<Env*> referent(cx, environment->referent());
|
|
Debugger* dbg = environment->owner();
|
|
|
|
{
|
|
Maybe<AutoRealm> ar;
|
|
ar.emplace(cx, referent);
|
|
|
|
cx->markId(id);
|
|
|
|
// This can trigger getters.
|
|
ErrorCopier ec(ar);
|
|
|
|
bool found;
|
|
if (!HasProperty(cx, referent, id, &found)) {
|
|
return false;
|
|
}
|
|
if (!found) {
|
|
result.setUndefined();
|
|
return true;
|
|
}
|
|
|
|
// For DebugEnvironmentProxys, 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 (referent->is<DebugEnvironmentProxy>()) {
|
|
Rooted<DebugEnvironmentProxy*> env(
|
|
cx, &referent->as<DebugEnvironmentProxy>());
|
|
if (!DebugEnvironmentProxy::getMaybeSentinelValue(cx, env, id, result)) {
|
|
return false;
|
|
}
|
|
} else {
|
|
if (!GetProperty(cx, referent, referent, id, result)) {
|
|
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 (result.isObject()) {
|
|
RootedObject obj(cx, &result.toObject());
|
|
if (obj->is<JSFunction>() &&
|
|
IsInternalFunctionObject(obj->as<JSFunction>()))
|
|
result.setMagic(JS_OPTIMIZED_OUT);
|
|
}
|
|
|
|
return dbg->wrapDebuggeeValue(cx, result);
|
|
}
|
|
|
|
/* static */ bool DebuggerEnvironment::setVariable(
|
|
JSContext* cx, HandleDebuggerEnvironment environment, HandleId id,
|
|
HandleValue value_) {
|
|
MOZ_ASSERT(environment->isDebuggee());
|
|
|
|
Rooted<Env*> referent(cx, environment->referent());
|
|
Debugger* dbg = environment->owner();
|
|
|
|
RootedValue value(cx, value_);
|
|
if (!dbg->unwrapDebuggeeValue(cx, &value)) {
|
|
return false;
|
|
}
|
|
|
|
{
|
|
Maybe<AutoRealm> ar;
|
|
ar.emplace(cx, referent);
|
|
if (!cx->compartment()->wrap(cx, &value)) {
|
|
return false;
|
|
}
|
|
cx->markId(id);
|
|
|
|
// This can trigger setters.
|
|
ErrorCopier ec(ar);
|
|
|
|
// Make sure the environment actually has the specified binding.
|
|
bool found;
|
|
if (!HasProperty(cx, referent, id, &found)) {
|
|
return false;
|
|
}
|
|
if (!found) {
|
|
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
|
|
JSMSG_DEBUG_VARIABLE_NOT_FOUND);
|
|
return false;
|
|
}
|
|
|
|
// Just set the property.
|
|
if (!SetProperty(cx, referent, id, value)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*** 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 DefineDataProperty(cx, value, id, trusted);
|
|
}
|
|
|
|
bool Builder::Object::defineProperty(JSContext* cx, const char* name,
|
|
JS::HandleValue propval_) {
|
|
AutoRealm ar(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_) {
|
|
AutoRealm ar(cx, debuggerObject());
|
|
|
|
RootedValue propval(cx, ObjectOrNullValue(propval_.value));
|
|
return definePropertyToTrusted(cx, name, &propval);
|
|
}
|
|
|
|
Builder::Object Builder::newObject(JSContext* cx) {
|
|
AutoRealm ar(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)
|
|
: cx_(cx), savedMonitor_(cx->entryMonitor) {
|
|
cx->entryMonitor = this;
|
|
}
|
|
|
|
AutoEntryMonitor::~AutoEntryMonitor() { cx_->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);
|
|
RootedObject debuggeeWouldRunProto(cx);
|
|
RootedValue debuggeeWouldRunCtor(cx);
|
|
Handle<GlobalObject*> global = obj.as<GlobalObject>();
|
|
|
|
objProto = GlobalObject::getOrCreateObjectPrototype(cx, global);
|
|
if (!objProto) {
|
|
return false;
|
|
}
|
|
debugProto =
|
|
InitClass(cx, global, objProto, &Debugger::class_, Debugger::construct, 1,
|
|
Debugger::properties, Debugger::methods, nullptr,
|
|
Debugger::static_methods, debugCtor.address());
|
|
if (!debugProto) {
|
|
return false;
|
|
}
|
|
|
|
frameProto = DebuggerFrame::initClass(cx, debugCtor, global);
|
|
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 = DebuggerObject::initClass(cx, global, debugCtor);
|
|
if (!objectProto) {
|
|
return false;
|
|
}
|
|
|
|
envProto = DebuggerEnvironment::initClass(cx, debugCtor, global);
|
|
if (!envProto) {
|
|
return false;
|
|
}
|
|
|
|
memoryProto =
|
|
InitClass(cx, debugCtor, objProto, &DebuggerMemory::class_,
|
|
DebuggerMemory::construct, 0, DebuggerMemory::properties,
|
|
DebuggerMemory::methods, nullptr, nullptr);
|
|
if (!memoryProto) {
|
|
return false;
|
|
}
|
|
|
|
debuggeeWouldRunProto = GlobalObject::getOrCreateCustomErrorPrototype(
|
|
cx, global, JSEXN_DEBUGGEEWOULDRUN);
|
|
if (!debuggeeWouldRunProto) {
|
|
return false;
|
|
}
|
|
debuggeeWouldRunCtor = global->getConstructor(JSProto_DebuggeeWouldRun);
|
|
RootedId debuggeeWouldRunId(
|
|
cx, NameToId(ClassName(JSProto_DebuggeeWouldRun, cx)));
|
|
if (!DefineDataProperty(cx, debugCtor, debuggeeWouldRunId,
|
|
debuggeeWouldRunCtor, 0)) {
|
|
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;
|
|
}
|
|
|
|
JS_PUBLIC_API bool JS::dbg::IsDebugger(JSObject& obj) {
|
|
JSObject* unwrapped = CheckedUnwrap(&obj);
|
|
return unwrapped && unwrapped->getClass() == &Debugger::class_ &&
|
|
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;
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
/* static */ bool Debugger::isDebuggerCrossCompartmentEdge(
|
|
JSObject* obj, const gc::Cell* target) {
|
|
MOZ_ASSERT(target);
|
|
|
|
auto cls = obj->getClass();
|
|
const gc::Cell* referent = nullptr;
|
|
if (cls == &DebuggerScript_class) {
|
|
referent = GetScriptReferentCell(obj);
|
|
} else if (cls == &DebuggerSource_class) {
|
|
referent = GetSourceReferentRawObject(obj);
|
|
} else if (obj->is<DebuggerObject>()) {
|
|
referent = static_cast<gc::Cell*>(obj->as<DebuggerObject>().getPrivate());
|
|
} else if (obj->is<DebuggerEnvironment>()) {
|
|
referent =
|
|
static_cast<gc::Cell*>(obj->as<DebuggerEnvironment>().getPrivate());
|
|
}
|
|
|
|
return referent == target;
|
|
}
|
|
|
|
static void CheckDebuggeeThingRealm(Realm* realm, bool invisibleOk) {
|
|
MOZ_ASSERT(!realm->creationOptions().mergeable());
|
|
MOZ_ASSERT_IF(!invisibleOk, !realm->creationOptions().invisibleToDebugger());
|
|
}
|
|
|
|
void js::CheckDebuggeeThing(JSScript* script, bool invisibleOk) {
|
|
CheckDebuggeeThingRealm(script->realm(), invisibleOk);
|
|
}
|
|
|
|
void js::CheckDebuggeeThing(LazyScript* script, bool invisibleOk) {
|
|
CheckDebuggeeThingRealm(script->realm(), invisibleOk);
|
|
}
|
|
|
|
void js::CheckDebuggeeThing(JSObject* obj, bool invisibleOk) {
|
|
if (Realm* realm = JS::GetObjectRealmOrNull(obj)) {
|
|
CheckDebuggeeThingRealm(realm, invisibleOk);
|
|
}
|
|
}
|
|
#endif // DEBUG
|
|
|
|
/*** JS::dbg::GarbageCollectionEvent ****************************************/
|
|
|
|
namespace JS {
|
|
namespace dbg {
|
|
|
|
/* static */ GarbageCollectionEvent::Ptr GarbageCollectionEvent::Create(
|
|
JSRuntime* rt, ::js::gcstats::Statistics& stats, uint64_t gcNumber) {
|
|
auto data = MakeUnique<GarbageCollectionEvent>(gcNumber);
|
|
if (!data) {
|
|
return nullptr;
|
|
}
|
|
|
|
data->nonincrementalReason = stats.nonincrementalReason();
|
|
|
|
for (auto& slice : stats.slices()) {
|
|
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 = ExplainGCReason(slice.reason);
|
|
MOZ_ASSERT(data->reason);
|
|
}
|
|
|
|
if (!data->collections.growBy(1)) {
|
|
return nullptr;
|
|
}
|
|
|
|
data->collections.back().startTimestamp = slice.start;
|
|
data->collections.back().endTimestamp = slice.end;
|
|
}
|
|
|
|
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 DefineDataProperty(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) ||
|
|
!DefineDataProperty(cx, obj, cx->names().gcCycleNumber,
|
|
gcCycleNumberVal)) {
|
|
return nullptr;
|
|
}
|
|
|
|
RootedArrayObject slicesArray(cx, NewDenseEmptyArray(cx));
|
|
if (!slicesArray) {
|
|
return nullptr;
|
|
}
|
|
|
|
TimeStamp originTime = TimeStamp::ProcessCreation();
|
|
|
|
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), end(cx);
|
|
start = NumberValue(
|
|
(range.front().startTimestamp - originTime).ToMilliseconds());
|
|
end =
|
|
NumberValue((range.front().endTimestamp - originTime).ToMilliseconds());
|
|
if (!DefineDataProperty(cx, collectionObj, cx->names().startTimestamp,
|
|
start) ||
|
|
!DefineDataProperty(cx, collectionObj, cx->names().endTimestamp, end)) {
|
|
return nullptr;
|
|
}
|
|
|
|
RootedValue collectionVal(cx, ObjectValue(*collectionObj));
|
|
if (!DefineDataElement(cx, slicesArray, idx++, collectionVal)) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
RootedValue slicesValue(cx, ObjectValue(*slicesArray));
|
|
if (!DefineDataProperty(cx, obj, cx->names().collections, slicesValue)) {
|
|
return nullptr;
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
JS_PUBLIC_API bool FireOnGarbageCollectionHookRequired(JSContext* cx) {
|
|
AutoCheckCannotGC noGC;
|
|
|
|
for (Debugger* dbg : cx->runtime()->debuggerList()) {
|
|
if (dbg->enabled && dbg->observedGC(cx->runtime()->gc.majorGCCount()) &&
|
|
dbg->getHook(Debugger::OnGarbageCollection)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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
|