Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
});
const TYPED_ARRAY_CLASSES = [
"Uint8Array",
"Uint8ClampedArray",
"Uint16Array",
"Uint32Array",
"Int8Array",
"Int16Array",
"Int32Array",
"Float32Array",
"Float64Array",
];
// Bug 1786299: Puppeteer expects specific error messages.
const ERROR_CYCLIC_REFERENCE = "Object reference chain is too long";
const ERROR_CANNOT_RETURN_BY_VALUE = "Object couldn't be returned by value";
function randomInt() {
return crypto.getRandomValues(new Uint32Array(1))[0];
}
/**
* This class represent a debuggable context onto which we can evaluate Javascript.
* This is typically a document, but it could also be a worker, an add-on, ... or
* any kind of context involving JS scripts.
*
* @param {Debugger} dbg
* A Debugger instance that we can use to inspect the given global.
* @param {GlobalObject} debuggee
* The debuggable context's global object. This is typically the document window
* object. But it can also be any global object, like a worker global scope object.
*/
export class ExecutionContext {
constructor(dbg, debuggee, id, isDefault) {
this._debugger = dbg;
this._debuggee = this._debugger.addDebuggee(debuggee);
// Here, we assume that debuggee is a window object and we will propably have
// to adapt that once we cover workers or contexts that aren't a document.
this.window = debuggee;
this.windowId = this.window.windowGlobalChild.innerWindowId;
this.id = id;
this.frameId = this.window.browsingContext.id.toString();
this.isDefault = isDefault;
// objectId => Debugger.Object
this._remoteObjects = new Map();
}
destructor() {
this._debugger.removeDebuggee(this._debuggee);
}
get browsingContext() {
return this.window.browsingContext;
}
hasRemoteObject(objectId) {
return this._remoteObjects.has(objectId);
}
getRemoteObject(objectId) {
return this._remoteObjects.get(objectId);
}
getRemoteObjectByNodeId(nodeId) {
for (const value of this._remoteObjects.values()) {
if (value.nodeId == nodeId) {
return value;
}
}
return null;
}
releaseObject(objectId) {
return this._remoteObjects.delete(objectId);
}
/**
* Add a new debuggerObj to the object cache.
*
* Whenever an object is returned as reference, a new entry is added
* to the internal object cache. It means the same underlying object or node
* can be represented via multiple references.
*/
setRemoteObject(debuggerObj) {
const objectId = lazy.generateUUID();
// TODO: Wrap Symbol into an object,
// which would allow us to set the objectId.
if (typeof debuggerObj == "object") {
debuggerObj.objectId = objectId;
}
// For node objects add an unique identifier.
if (
debuggerObj instanceof Debugger.Object &&
Node.isInstance(debuggerObj.unsafeDereference())
) {
debuggerObj.nodeId = randomInt();
// We do not differentiate between backendNodeId and nodeId (yet)
debuggerObj.backendNodeId = debuggerObj.nodeId;
}
this._remoteObjects.set(objectId, debuggerObj);
return objectId;
}
/**
* Evaluate a Javascript expression.
*
* @param {string} expression
* The JS expression to evaluate against the JS context.
* @param {boolean} awaitPromise
* Whether execution should `await` for resulting value
* and return once awaited promise is resolved.
* @param {boolean} returnByValue
* Whether the result is expected to be a JSON object
* that should be sent by value.
*
* @returns {object} A multi-form object depending if the execution
* succeed or failed. If the expression failed to evaluate,
* it will return an object with an `exceptionDetails` attribute
* matching the `ExceptionDetails` CDP type. Otherwise it will
* return an object with `result` attribute whose type is
* `RemoteObject` CDP type.
*/
async evaluate(expression, awaitPromise, returnByValue) {
let rv = this._debuggee.executeInGlobal(expression);
if (!rv) {
return {
exceptionDetails: {
text: "Evaluation terminated!",
},
};
}
if (rv.throw) {
return this._returnError(rv.throw);
}
let result = rv.return;
if (result && result.isPromise && awaitPromise) {
if (result.promiseState === "fulfilled") {
result = result.promiseValue;
} else if (result.promiseState === "rejected") {
return this._returnError(result.promiseReason);
} else {
try {
const promiseResult = await result.unsafeDereference();
result = this._debuggee.makeDebuggeeValue(promiseResult);
} catch (e) {
// The promise has been rejected
return this._returnError(this._debuggee.makeDebuggeeValue(e));
}
}
}
if (returnByValue) {
result = this._toRemoteObjectByValue(result);
} else {
result = this._toRemoteObject(result);
}
return { result };
}
/**
* Given a Debugger.Object reference for an Exception, return a JSON object
* describing the exception by following CDP ExceptionDetails specification.
*/
_returnError(exception) {
if (
this._debuggee.executeInGlobalWithBindings("exception instanceof Error", {
exception,
}).return
) {
const text = this._debuggee.executeInGlobalWithBindings(
"exception.message",
{ exception }
).return;
return {
exceptionDetails: {
text,
},
};
}
// If that isn't an Error, consider the exception as a JS value
return {
exceptionDetails: {
exception: this._toRemoteObject(exception),
},
};
}
async callFunctionOn(
functionDeclaration,
callArguments = [],
returnByValue = false,
awaitPromise = false,
objectId = null
) {
// Map the given objectId to a JS reference.
let thisArg = null;
if (objectId) {
thisArg = this.getRemoteObject(objectId);
if (!thisArg) {
throw new Error(`Unable to get target object with id: ${objectId}`);
}
}
// First evaluate the function
const fun = this._debuggee.executeInGlobal("(" + functionDeclaration + ")");
if (!fun) {
return {
exceptionDetails: {
text: "Evaluation terminated!",
},
};
}
if (fun.throw) {
return this._returnError(fun.throw);
}
// Then map all input arguments, which are matching CDP's CallArguments type,
// into JS values
const args = callArguments.map(arg => this._fromCallArgument(arg));
// Finally, call the function with these arguments
const rv = fun.return.apply(thisArg, args);
if (rv.throw) {
return this._returnError(rv.throw);
}
let result = rv.return;
if (result && result.isPromise && awaitPromise) {
if (result.promiseState === "fulfilled") {
result = result.promiseValue;
} else if (result.promiseState === "rejected") {
return this._returnError(result.promiseReason);
} else {
try {
const promiseResult = await result.unsafeDereference();
result = this._debuggee.makeDebuggeeValue(promiseResult);
} catch (e) {
// The promise has been rejected
return this._returnError(this._debuggee.makeDebuggeeValue(e));
}
}
}
if (returnByValue) {
result = this._toRemoteObjectByValue(result);
} else {
result = this._toRemoteObject(result);
}
return { result };
}
getProperties({ objectId, ownProperties }) {
let debuggerObj = this.getRemoteObject(objectId);
if (!debuggerObj) {
throw new Error("Could not find object with given id");
}
const result = [];
const serializeObject = (debuggerObj, isOwn) => {
for (const propertyName of debuggerObj.getOwnPropertyNames()) {
const descriptor = debuggerObj.getOwnPropertyDescriptor(propertyName);
result.push({
name: propertyName,
configurable: descriptor.configurable,
enumerable: descriptor.enumerable,
writable: descriptor.writable,
value: this._toRemoteObject(descriptor.value),
get: descriptor.get
? this._toRemoteObject(descriptor.get)
: undefined,
set: descriptor.set
? this._toRemoteObject(descriptor.set)
: undefined,
isOwn,
});
}
};
// When `ownProperties` is set to true, we only iterate over own properties.
// Otherwise, we also iterate over propreties inherited from the prototype chain.
serializeObject(debuggerObj, true);
if (!ownProperties) {
while (true) {
debuggerObj = debuggerObj.proto;
if (!debuggerObj) {
break;
}
serializeObject(debuggerObj, false);
}
}
return {
result,
};
}
/**
* Given a CDP `CallArgument`, return a JS value that represent this argument.
* Note that `CallArgument` is actually very similar to `RemoteObject`
*/
_fromCallArgument(arg) {
if (arg.objectId) {
if (!this.hasRemoteObject(arg.objectId)) {
throw new Error("Could not find object with given id");
}
return this.getRemoteObject(arg.objectId);
}
if (arg.unserializableValue) {
switch (arg.unserializableValue) {
case "-0":
return -0;
case "Infinity":
return Infinity;
case "-Infinity":
return -Infinity;
case "NaN":
return NaN;
default:
if (/^\d+n$/.test(arg.unserializableValue)) {
// eslint-disable-next-line no-undef
return BigInt(arg.unserializableValue.slice(0, -1));
}
throw new Error("Couldn't parse value object in call argument");
}
}
return this._deserialize(arg.value);
}
/**
* Given a JS value, create a copy of it within the debugee compartment.
*/
_deserialize(obj) {
if (typeof obj !== "object") {
return obj;
}
const result = this._debuggee.executeInGlobalWithBindings(
"JSON.parse(obj)",
{ obj: JSON.stringify(obj) }
);
if (result.throw) {
throw new Error("Unable to deserialize object");
}
return result.return;
}
/**
* Given a `Debugger.Object` object, return a JSON-serializable description of it
* matching `RemoteObject` CDP type.
*
* @param {Debugger.Object} debuggerObj
* The object to serialize
* @returns {RemoteObject}
* The serialized description of the given object
*/
_toRemoteObject(debuggerObj) {
const result = {};
// First handle all non-primitive values which are going to be wrapped by the
// Debugger API into Debugger.Object instances
if (debuggerObj instanceof Debugger.Object) {
const rawObj = debuggerObj.unsafeDereference();
result.objectId = this.setRemoteObject(debuggerObj);
result.type = typeof rawObj;
// Map the Debugger API `class` attribute to CDP `subtype`
const cls = debuggerObj.class;
if (debuggerObj.isProxy) {
result.subtype = "proxy";
} else if (cls == "Array") {
result.subtype = "array";
} else if (cls == "RegExp") {
result.subtype = "regexp";
} else if (cls == "Date") {
result.subtype = "date";
} else if (cls == "Map") {
result.subtype = "map";
} else if (cls == "Set") {
result.subtype = "set";
} else if (cls == "WeakMap") {
result.subtype = "weakmap";
} else if (cls == "WeakSet") {
result.subtype = "weakset";
} else if (cls == "Error") {
result.subtype = "error";
} else if (cls == "Promise") {
result.subtype = "promise";
} else if (TYPED_ARRAY_CLASSES.includes(cls)) {
result.subtype = "typedarray";
} else if (Node.isInstance(rawObj)) {
result.subtype = "node";
result.className = ChromeUtils.getClassName(rawObj);
result.description = rawObj.localName || rawObj.nodeName;
if (rawObj.id) {
result.description += `#${rawObj.id}`;
}
}
return result;
}
// Now, handle all values that Debugger API isn't wrapping into Debugger.API.
// This is all the primitive JS types.
result.type = typeof debuggerObj;
// Symbol and BigInt are primitive values but aren't serializable.
// CDP expects them to be considered as objects, with an objectId to later inspect
// them.
if (result.type == "symbol") {
result.description = debuggerObj.toString();
result.objectId = this.setRemoteObject(debuggerObj);
return result;
}
// A few primitive type can't be serialized and CDP has special case for them
if (Object.is(debuggerObj, NaN)) {
result.unserializableValue = "NaN";
} else if (Object.is(debuggerObj, -0)) {
result.unserializableValue = "-0";
} else if (Object.is(debuggerObj, Infinity)) {
result.unserializableValue = "Infinity";
} else if (Object.is(debuggerObj, -Infinity)) {
result.unserializableValue = "-Infinity";
} else if (result.type == "bigint") {
result.unserializableValue = `${debuggerObj}n`;
}
if (result.unserializableValue) {
result.description = result.unserializableValue;
return result;
}
// Otherwise, we serialize the primitive values as-is via `value` attribute
result.value = debuggerObj;
// null is special as it has a dedicated subtype
if (debuggerObj === null) {
result.subtype = "null";
}
return result;
}
/**
* Given a `Debugger.Object` object, return a JSON-serializable description of it
* matching `RemoteObject` CDP type.
*
* @param {Debugger.Object} debuggerObj
* The object to serialize
* @returns {RemoteObject}
* The serialized description of the given object
*/
_toRemoteObjectByValue(debuggerObj) {
const type = typeof debuggerObj;
if (type == "undefined") {
return { type };
}
let unserializableValue;
if (Object.is(debuggerObj, -0)) {
unserializableValue = "-0";
} else if (Object.is(debuggerObj, NaN)) {
unserializableValue = "NaN";
} else if (Object.is(debuggerObj, Infinity)) {
unserializableValue = "Infinity";
} else if (Object.is(debuggerObj, -Infinity)) {
unserializableValue = "-Infinity";
} else if (typeof debuggerObj == "bigint") {
unserializableValue = `${debuggerObj}n`;
}
if (unserializableValue) {
return {
type,
unserializableValue,
description: unserializableValue,
};
}
const value = this._serialize(debuggerObj);
return {
type: typeof value,
value,
description: value != null ? value.toString() : value,
};
}
/**
* Convert a given `Debugger.Object` to an object.
*
* @param {Debugger.Object} debuggerObj
* The object to convert
*
* @returns {object}
* The converted object
*/
_serialize(debuggerObj) {
const result = this._debuggee.executeInGlobalWithBindings(
`
JSON.stringify(e, (key, value) => {
if (typeof value === "symbol") {
// CDP cannot return Symbols
throw new Error();
}
return value;
});
`,
{ e: debuggerObj }
);
if (result.throw) {
const exception = this._toRawObject(result.throw);
if (exception.message === "cyclic object value") {
throw new Error(ERROR_CYCLIC_REFERENCE);
}
throw new Error(ERROR_CANNOT_RETURN_BY_VALUE);
}
return JSON.parse(result.return);
}
_toRawObject(maybeDebuggerObject) {
if (maybeDebuggerObject instanceof Debugger.Object) {
// Retrieve the referent for the provided Debugger.object.
const rawObject = maybeDebuggerObject.unsafeDereference();
return Cu.waiveXrays(rawObject);
}
// If maybeDebuggerObject was not a Debugger.Object, it is a primitive value
// which can be used as is.
return maybeDebuggerObject;
}
}