543 lines
14 KiB
JavaScript
543 lines
14 KiB
JavaScript
/* 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/. */
|
|
|
|
"use strict";
|
|
|
|
const {
|
|
DevToolsServer,
|
|
} = require("resource://devtools/server/devtools-server.js");
|
|
const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
|
|
const { assert } = DevToolsUtils;
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"LongStringActor",
|
|
"resource://devtools/server/actors/string.js",
|
|
true
|
|
);
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"symbolGrip",
|
|
"resource://devtools/server/actors/object/symbol.js",
|
|
true
|
|
);
|
|
|
|
/**
|
|
* Get thisDebugger.Object referent's `promiseState`.
|
|
*
|
|
* @returns Object
|
|
* An object of one of the following forms:
|
|
* - { state: "pending" }
|
|
* - { state: "fulfilled", value }
|
|
* - { state: "rejected", reason }
|
|
*/
|
|
function getPromiseState(obj) {
|
|
if (obj.class != "Promise") {
|
|
throw new Error(
|
|
"Can't call `getPromiseState` on `Debugger.Object`s that don't " +
|
|
"refer to Promise objects."
|
|
);
|
|
}
|
|
|
|
const state = { state: obj.promiseState };
|
|
if (state.state === "fulfilled") {
|
|
state.value = obj.promiseValue;
|
|
} else if (state.state === "rejected") {
|
|
state.reason = obj.promiseReason;
|
|
}
|
|
return state;
|
|
}
|
|
|
|
/**
|
|
* Returns true if value is an object or function.
|
|
*
|
|
* @param value
|
|
* @returns {boolean}
|
|
*/
|
|
|
|
function isObjectOrFunction(value) {
|
|
// Handle null, whose typeof is object
|
|
if (!value) {
|
|
return false;
|
|
}
|
|
|
|
const type = typeof value;
|
|
return type == "object" || type == "function";
|
|
}
|
|
|
|
/**
|
|
* Make a debuggee value for the given object, if needed. Primitive values
|
|
* are left the same.
|
|
*
|
|
* Use case: you have a raw JS object (after unsafe dereference) and you want to
|
|
* send it to the client. In that case you need to use an ObjectActor which
|
|
* requires a debuggee value. The Debugger.Object.prototype.makeDebuggeeValue()
|
|
* method works only for JS objects and functions.
|
|
*
|
|
* @param Debugger.Object obj
|
|
* @param any value
|
|
* @return object
|
|
*/
|
|
function makeDebuggeeValueIfNeeded(obj, value) {
|
|
if (isObjectOrFunction(value)) {
|
|
return obj.makeDebuggeeValue(value);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Convert a debuggee value into the underlying raw object, if needed.
|
|
*/
|
|
function unwrapDebuggeeValue(value) {
|
|
if (value && typeof value == "object") {
|
|
return value.unsafeDereference();
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Create a grip for the given debuggee value. If the value is an object or a long string,
|
|
* it will create an actor and add it to the pool
|
|
* @param {ThreadActor} threadActor
|
|
* The related Thread Actor.
|
|
* @param {any} value
|
|
* The debuggee value.
|
|
* @param {Pool} pool
|
|
* The pool where the created actor will be added to.
|
|
* @param {Number} [depth]
|
|
* The current depth within the chain of nested object actor being previewed.
|
|
* @param {Object} [objectActorAttributes]
|
|
* An optional object whose properties will be assigned to the ObjectActor if one
|
|
* is created.
|
|
*/
|
|
function createValueGrip(threadActor, value, pool, depth = 0, objectActorAttributes = {}) {
|
|
switch (typeof value) {
|
|
case "boolean":
|
|
return value;
|
|
|
|
case "string":
|
|
return createStringGrip(pool, value);
|
|
|
|
case "number":
|
|
if (value === Infinity) {
|
|
return { type: "Infinity" };
|
|
} else if (value === -Infinity) {
|
|
return { type: "-Infinity" };
|
|
} else if (Number.isNaN(value)) {
|
|
return { type: "NaN" };
|
|
} else if (!value && 1 / value === -Infinity) {
|
|
return { type: "-0" };
|
|
}
|
|
return value;
|
|
|
|
case "bigint":
|
|
return createBigIntValueGrip(value);
|
|
|
|
// TODO(bug 1772157)
|
|
// Record/tuple grips aren't fully implemented yet.
|
|
case "record":
|
|
return {
|
|
class: "Record",
|
|
};
|
|
case "tuple":
|
|
return {
|
|
class: "Tuple",
|
|
};
|
|
case "undefined":
|
|
return { type: "undefined" };
|
|
|
|
case "object":
|
|
if (value === null) {
|
|
return { type: "null" };
|
|
} else if (
|
|
value.optimizedOut ||
|
|
value.uninitialized ||
|
|
value.missingArguments
|
|
) {
|
|
// The slot is optimized out, an uninitialized binding, or
|
|
// arguments on a dead scope
|
|
return {
|
|
type: "null",
|
|
optimizedOut: value.optimizedOut,
|
|
uninitialized: value.uninitialized,
|
|
missingArguments: value.missingArguments,
|
|
};
|
|
}
|
|
return pool.createObjectGrip(
|
|
value,
|
|
depth,
|
|
objectActorAttributes,
|
|
);
|
|
|
|
case "symbol":
|
|
return symbolGrip(threadActor, value, pool);
|
|
|
|
default:
|
|
assert(false, "Failed to provide a grip for: " + value);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a grip for the passed BigInt
|
|
*
|
|
* @param {BigInt} value
|
|
* @returns {Object}
|
|
*/
|
|
function createBigIntValueGrip(value) {
|
|
return {
|
|
type: "BigInt",
|
|
text: value.toString(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* of passing the value directly over the protocol.
|
|
*
|
|
* @param str String
|
|
* The string we are checking the length of.
|
|
*/
|
|
function stringIsLong(str) {
|
|
return str.length >= DevToolsServer.LONG_STRING_LENGTH;
|
|
}
|
|
|
|
const TYPED_ARRAY_CLASSES = [
|
|
"Uint8Array",
|
|
"Uint8ClampedArray",
|
|
"Uint16Array",
|
|
"Uint32Array",
|
|
"Int8Array",
|
|
"Int16Array",
|
|
"Int32Array",
|
|
"Float32Array",
|
|
"Float64Array",
|
|
"BigInt64Array",
|
|
"BigUint64Array",
|
|
];
|
|
|
|
/**
|
|
* Returns true if a debuggee object is a typed array.
|
|
*
|
|
* @param obj Debugger.Object
|
|
* The debuggee object to test.
|
|
* @return Boolean
|
|
*/
|
|
function isTypedArray(object) {
|
|
return TYPED_ARRAY_CLASSES.includes(object.class);
|
|
}
|
|
|
|
/**
|
|
* Returns true if a debuggee object is an array, including a typed array.
|
|
*
|
|
* @param obj Debugger.Object
|
|
* The debuggee object to test.
|
|
* @return Boolean
|
|
*/
|
|
function isArray(object) {
|
|
return isTypedArray(object) || object.class === "Array";
|
|
}
|
|
|
|
/**
|
|
* Returns the length of an array (or typed array).
|
|
*
|
|
* @param object Debugger.Object
|
|
* The debuggee object of the array.
|
|
* @return Number
|
|
* @throws if the object is not an array.
|
|
*/
|
|
function getArrayLength(object) {
|
|
if (!isArray(object)) {
|
|
throw new Error("Expected an array, got a " + object.class);
|
|
}
|
|
|
|
// Real arrays have a reliable `length` own property.
|
|
if (object.class === "Array") {
|
|
return DevToolsUtils.getProperty(object, "length");
|
|
}
|
|
|
|
// For typed arrays, `DevToolsUtils.getProperty` is not reliable because the `length`
|
|
// getter could be shadowed by an own property, and `getOwnPropertyNames` is
|
|
// unnecessarily slow. Obtain the `length` getter safely and call it manually.
|
|
const typedProto = Object.getPrototypeOf(Uint8Array.prototype);
|
|
const getter = Object.getOwnPropertyDescriptor(typedProto, "length").get;
|
|
return getter.call(object.unsafeDereference());
|
|
}
|
|
|
|
/**
|
|
* Returns true if the parameter is suitable to be an array index.
|
|
*
|
|
* @param str String
|
|
* @return Boolean
|
|
*/
|
|
function isArrayIndex(str) {
|
|
// Transform the parameter to a 32-bit unsigned integer.
|
|
const num = str >>> 0;
|
|
// Check that the parameter is a canonical Uint32 index.
|
|
return (
|
|
num + "" === str &&
|
|
// Array indices cannot attain the maximum Uint32 value.
|
|
num != -1 >>> 0
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns true if a debuggee object is a local or sessionStorage object.
|
|
*
|
|
* @param object Debugger.Object
|
|
* The debuggee object to test.
|
|
* @return Boolean
|
|
*/
|
|
function isStorage(object) {
|
|
return object.class === "Storage";
|
|
}
|
|
|
|
/**
|
|
* Returns the length of a local or sessionStorage object.
|
|
*
|
|
* @param object Debugger.Object
|
|
* The debuggee object of the array.
|
|
* @return Number
|
|
* @throws if the object is not a local or sessionStorage object.
|
|
*/
|
|
function getStorageLength(object) {
|
|
if (!isStorage(object)) {
|
|
throw new Error("Expected a storage object, got a " + object.class);
|
|
}
|
|
return DevToolsUtils.getProperty(object, "length");
|
|
}
|
|
|
|
/**
|
|
* Returns an array of properties based on event class name.
|
|
*
|
|
* @param className
|
|
* @returns {Array}
|
|
*/
|
|
function getPropsForEvent(className) {
|
|
const positionProps = ["buttons", "clientX", "clientY", "layerX", "layerY"];
|
|
const eventToPropsMap = {
|
|
MouseEvent: positionProps,
|
|
DragEvent: positionProps,
|
|
PointerEvent: positionProps,
|
|
SimpleGestureEvent: positionProps,
|
|
WheelEvent: positionProps,
|
|
KeyboardEvent: ["key", "charCode", "keyCode"],
|
|
TransitionEvent: ["propertyName", "pseudoElement"],
|
|
AnimationEvent: ["animationName", "pseudoElement"],
|
|
ClipboardEvent: ["clipboardData"],
|
|
};
|
|
|
|
if (className in eventToPropsMap) {
|
|
return eventToPropsMap[className];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Returns an array of of all properties of an object
|
|
*
|
|
* @param obj
|
|
* @param rawObj
|
|
* @returns {Array|Iterable} If rawObj is localStorage/sessionStorage, we don't return an
|
|
* array but an iterable object (with the proper `length` property) to avoid
|
|
* performance issues.
|
|
*/
|
|
function getPropNamesFromObject(obj, rawObj) {
|
|
try {
|
|
if (isStorage(obj)) {
|
|
// local and session storage cannot be iterated over using
|
|
// Object.getOwnPropertyNames() because it skips keys that are duplicated
|
|
// on the prototype e.g. "key", "getKeys" so we need to gather the real
|
|
// keys using the storage.key() function.
|
|
// As the method is pretty slow, we return an iterator here, so we don't consume
|
|
// more than we need, especially since we're calling this from previewers in which
|
|
// we only need the first 10 entries for the preview (See Bug 1741804).
|
|
|
|
// Still return the proper number of entries.
|
|
const length = rawObj.length;
|
|
const iterable = { length };
|
|
iterable[Symbol.iterator] = function*() {
|
|
for (let j = 0; j < length; j++) {
|
|
yield rawObj.key(j);
|
|
}
|
|
};
|
|
return iterable;
|
|
}
|
|
|
|
return obj.getOwnPropertyNames();
|
|
} catch (ex) {
|
|
// Calling getOwnPropertyNames() on some wrapped native prototypes is not
|
|
// allowed: "cannot modify properties of a WrappedNative". See bug 952093.
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Returns an array of private properties of an object
|
|
*
|
|
* @param obj
|
|
* @returns {Array}
|
|
*/
|
|
function getSafePrivatePropertiesSymbols(obj) {
|
|
try {
|
|
return obj.getOwnPrivateProperties();
|
|
} catch (ex) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an array of all symbol properties of an object
|
|
*
|
|
* @param obj
|
|
* @returns {Array}
|
|
*/
|
|
function getSafeOwnPropertySymbols(obj) {
|
|
try {
|
|
return obj.getOwnPropertySymbols();
|
|
} catch (ex) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an array modifiers based on keys
|
|
*
|
|
* @param rawObj
|
|
* @returns {Array}
|
|
*/
|
|
function getModifiersForEvent(rawObj) {
|
|
const modifiers = [];
|
|
const keysToModifiersMap = {
|
|
altKey: "Alt",
|
|
ctrlKey: "Control",
|
|
metaKey: "Meta",
|
|
shiftKey: "Shift",
|
|
};
|
|
|
|
for (const key in keysToModifiersMap) {
|
|
if (keysToModifiersMap.hasOwnProperty(key) && rawObj[key]) {
|
|
modifiers.push(keysToModifiersMap[key]);
|
|
}
|
|
}
|
|
|
|
return modifiers;
|
|
}
|
|
|
|
/**
|
|
* Make a debuggee value for the given value.
|
|
*
|
|
* @param TargetActor targetActor
|
|
* The Target Actor from which this object originates.
|
|
* @param mixed value
|
|
* The value you want to get a debuggee value for.
|
|
* @return object
|
|
* Debuggee value for |value|.
|
|
*/
|
|
function makeDebuggeeValue(targetActor, value) {
|
|
// Primitive types are debuggee values and Debugger.Object.makeDebuggeeValue
|
|
// would return them unchanged. So avoid the expense of:
|
|
// getGlobalForObject+makeGlobalObjectReference+makeDebugeeValue for them.
|
|
//
|
|
// It is actually easier to identify non primitive which can only be object or function.
|
|
if (!isObjectOrFunction(value)) {
|
|
return value;
|
|
}
|
|
|
|
// `value` may come from various globals.
|
|
// And Debugger.Object.makeDebuggeeValue only works for objects
|
|
// related to the same global. So fetch the global first,
|
|
// in order to instantiate a Debugger.Object for it.
|
|
//
|
|
// In the worker thread, we don't have access to Cu,
|
|
// but at the same time, there is only one global, the worker one.
|
|
const valueGlobal = isWorker ? targetActor.targetGlobal : Cu.getGlobalForObject(value);
|
|
let dbgGlobal;
|
|
try {
|
|
dbgGlobal = targetActor.dbg.makeGlobalObjectReference(
|
|
valueGlobal
|
|
);
|
|
} catch(e) {
|
|
// makeGlobalObjectReference will throw if the global is invisible to Debugger,
|
|
// in this case instantiate a Debugger.Object for the top level global
|
|
// of the target. Even if value will come from another global, it will "work",
|
|
// but the Debugger.Object created via dbgGlobal.makeDebuggeeValue will throw
|
|
// on most methods as the object will also be invisible to Debuggee...
|
|
if (e.message.includes("object in compartment marked as invisible to Debugger")) {
|
|
dbgGlobal = targetActor.dbg.makeGlobalObjectReference(
|
|
targetActor.window
|
|
);
|
|
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
return dbgGlobal.makeDebuggeeValue(value);
|
|
}
|
|
|
|
/**
|
|
* Create a grip for the given string.
|
|
*
|
|
* @param TargetActor targetActor
|
|
* The Target Actor from which this object originates.
|
|
*/
|
|
function createStringGrip(targetActor, string) {
|
|
if (string && stringIsLong(string)) {
|
|
const actor = new LongStringActor(targetActor.conn, string);
|
|
targetActor.manage(actor);
|
|
return actor.form();
|
|
}
|
|
return string;
|
|
}
|
|
|
|
/**
|
|
* Create a grip for the given value.
|
|
*
|
|
* @param TargetActor targetActor
|
|
* The Target Actor from which this object originates.
|
|
* @param mixed value
|
|
* The value you want to get a debuggee value for.
|
|
* @param Number depth
|
|
* Depth of the object compared to the top level object,
|
|
* when we are inspecting nested attributes.
|
|
* @param Object [objectActorAttributes]
|
|
* An optional object whose properties will be assigned to the ObjectActor if one
|
|
* is created.
|
|
* @return object
|
|
*/
|
|
function createValueGripForTarget(
|
|
targetActor,
|
|
value,
|
|
depth = 0,
|
|
objectActorAttributes = {}
|
|
) {
|
|
return createValueGrip(targetActor.threadActor, value, targetActor.objectsPool, depth, objectActorAttributes);
|
|
}
|
|
|
|
module.exports = {
|
|
getPromiseState,
|
|
makeDebuggeeValueIfNeeded,
|
|
unwrapDebuggeeValue,
|
|
createBigIntValueGrip,
|
|
createValueGrip,
|
|
stringIsLong,
|
|
isTypedArray,
|
|
isArray,
|
|
isStorage,
|
|
getArrayLength,
|
|
getStorageLength,
|
|
isArrayIndex,
|
|
getPropsForEvent,
|
|
getPropNamesFromObject,
|
|
getSafeOwnPropertySymbols,
|
|
getSafePrivatePropertiesSymbols,
|
|
getModifiersForEvent,
|
|
isObjectOrFunction,
|
|
createStringGrip,
|
|
makeDebuggeeValue,
|
|
createValueGripForTarget,
|
|
};
|