diff options
Diffstat (limited to 'remote/cdp/domains/content/runtime/ExecutionContext.sys.mjs')
-rw-r--r-- | remote/cdp/domains/content/runtime/ExecutionContext.sys.mjs | 564 |
1 files changed, 564 insertions, 0 deletions
diff --git a/remote/cdp/domains/content/runtime/ExecutionContext.sys.mjs b/remote/cdp/domains/content/runtime/ExecutionContext.sys.mjs new file mode 100644 index 0000000000..4d394f6bf9 --- /dev/null +++ b/remote/cdp/domains/content/runtime/ExecutionContext.sys.mjs @@ -0,0 +1,564 @@ +/* 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. + // See https://firefox-source-docs.mozilla.org/devtools-user/debugger-api/debugger.object/index.html + 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; + } +} |