/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */ /* 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 { Cu, Ci } = require("chrome"); const { GeneratedLocation } = require("devtools/server/actors/common"); const { DebuggerServer } = require("devtools/server/main"); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const { assert, dumpn } = DevToolsUtils; loader.lazyRequireGetter(this, "ThreadSafeChromeUtils"); const TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array", "Uint32Array", "Int8Array", "Int16Array", "Int32Array", "Float32Array", "Float64Array"]; // Number of items to preview in objects, arrays, maps, sets, lists, // collections, etc. const OBJECT_PREVIEW_MAX_ITEMS = 10; /** * Creates an actor for the specified object. * * @param obj Debugger.Object * The debuggee object. * @param hooks Object * A collection of abstract methods that are implemented by the caller. * ObjectActor requires the following functions to be implemented by * the caller: * - createValueGrip * Creates a value grip for the given object * - sources * TabSources getter that manages the sources of a thread * - createEnvironmentActor * Creates and return an environment actor * - getGripDepth * An actor's grip depth getter * - incrementGripDepth * Increment the actor's grip depth * - decrementGripDepth * Decrement the actor's grip depth * - globalDebugObject * The Debuggee Global Object as given by the ThreadActor */ function ObjectActor(obj, { createValueGrip, sources, createEnvironmentActor, getGripDepth, incrementGripDepth, decrementGripDepth, getGlobalDebugObject }) { assert(!obj.optimizedOut, "Should not create object actors for optimized out values!"); this.obj = obj; this.hooks = { createValueGrip, sources, createEnvironmentActor, getGripDepth, incrementGripDepth, decrementGripDepth, getGlobalDebugObject }; this.iterators = new Set(); } ObjectActor.prototype = { actorPrefix: "obj", /** * Returns a grip for this actor for returning in a protocol message. */ grip: function () { this.hooks.incrementGripDepth(); let g = { "type": "object", "actor": this.actorID }; // If it's a proxy, lie and tell that it belongs to an invented // "Proxy" class, and avoid calling the [[IsExtensible]] trap if(this.obj.isProxy) { g.class = "Proxy"; g.proxyTarget = this.hooks.createValueGrip(this.obj.proxyTarget); g.proxyHandler = this.hooks.createValueGrip(this.obj.proxyHandler); } else { g.class = this.obj.class; g.extensible = this.obj.isExtensible(); g.frozen = this.obj.isFrozen(); g.sealed = this.obj.isSealed(); } if (g.class != "DeadObject") { if (g.class == "Promise") { g.promiseState = this._createPromiseState(); } // FF40+: Allow to know how many properties an object has // to lazily display them when there is a bunch. // Throws on some MouseEvent object in tests. try { // Bug 1163520: Assert on internal functions if (!["Function", "Proxy"].includes(g.class)) { g.ownPropertyLength = this.obj.getOwnPropertyNames().length; } } catch (e) {} let raw = this.obj.unsafeDereference(); // If Cu is not defined, we are running on a worker thread, where xrays // don't exist. if (Cu) { raw = Cu.unwaiveXrays(raw); } if (!DevToolsUtils.isSafeJSObject(raw)) { raw = null; } let previewers = DebuggerServer.ObjectActorPreviewers[g.class] || DebuggerServer.ObjectActorPreviewers.Object; for (let fn of previewers) { try { if (fn(this, g, raw)) { break; } } catch (e) { let msg = "ObjectActor.prototype.grip previewer function"; DevToolsUtils.reportException(msg, e); } } } this.hooks.decrementGripDepth(); return g; }, /** * Returns an object exposing the internal Promise state. */ _createPromiseState: function () { const { state, value, reason } = getPromiseState(this.obj); let promiseState = { state }; if (state == "fulfilled") { promiseState.value = this.hooks.createValueGrip(value); } else if (state == "rejected") { promiseState.reason = this.hooks.createValueGrip(reason); } promiseState.creationTimestamp = Date.now() - this.obj.promiseLifetime; // Only add the timeToSettle property if the Promise isn't pending. if (state !== "pending") { promiseState.timeToSettle = this.obj.promiseTimeToResolution; } return promiseState; }, /** * Releases this actor from the pool. */ release: function () { if (this.registeredPool.objectActors) { this.registeredPool.objectActors.delete(this.obj); } this.iterators.forEach(actor => this.registeredPool.removeActor(actor)); this.iterators.clear(); this.registeredPool.removeActor(this); }, /** * Handle a protocol request to provide the definition site of this function * object. */ onDefinitionSite: function () { if (this.obj.class != "Function") { return { from: this.actorID, error: "objectNotFunction", message: this.actorID + " is not a function." }; } if (!this.obj.script) { return { from: this.actorID, error: "noScript", message: this.actorID + " has no Debugger.Script" }; } return this.hooks.sources().getOriginalLocation(new GeneratedLocation( this.hooks.sources().createNonSourceMappedActor(this.obj.script.source), this.obj.script.startLine, 0 // TODO bug 901138: use Debugger.Script.prototype.startColumn )).then((originalLocation) => { return { source: originalLocation.originalSourceActor.form(), line: originalLocation.originalLine, column: originalLocation.originalColumn }; }); }, /** * Handle a protocol request to provide the names of the properties defined on * the object and not its prototype. */ onOwnPropertyNames: function () { return { from: this.actorID, ownPropertyNames: this.obj.getOwnPropertyNames() }; }, /** * Creates an actor to iterate over an object property names and values. * See PropertyIteratorActor constructor for more info about options param. * * @param request object * The protocol request object. */ onEnumProperties: function (request) { let actor = new PropertyIteratorActor(this, request.options); this.registeredPool.addActor(actor); this.iterators.add(actor); return { iterator: actor.grip() }; }, /** * Creates an actor to iterate over entries of a Map/Set-like object. */ onEnumEntries: function () { let actor = new PropertyIteratorActor(this, { enumEntries: true }); this.registeredPool.addActor(actor); this.iterators.add(actor); return { iterator: actor.grip() }; }, /** * Handle a protocol request to provide the prototype and own properties of * the object. */ onPrototypeAndProperties: function () { let ownProperties = Object.create(null); let names; try { names = this.obj.getOwnPropertyNames(); } catch (ex) { // The above can throw if this.obj points to a dead object. // TODO: we should use Cu.isDeadWrapper() - see bug 885800. return { from: this.actorID, prototype: this.hooks.createValueGrip(null), ownProperties: ownProperties, safeGetterValues: Object.create(null) }; } for (let name of names) { ownProperties[name] = this._propertyDescriptor(name); } return { from: this.actorID, prototype: this.hooks.createValueGrip(this.obj.proto), ownProperties: ownProperties, safeGetterValues: this._findSafeGetterValues(names) }; }, /** * Find the safe getter values for the current Debugger.Object, |this.obj|. * * @private * @param array ownProperties * The array that holds the list of known ownProperties names for * |this.obj|. * @param number [limit=0] * Optional limit of getter values to find. * @return object * An object that maps property names to safe getter descriptors as * defined by the remote debugging protocol. */ _findSafeGetterValues: function (ownProperties, limit = 0) { let safeGetterValues = Object.create(null); let obj = this.obj; let level = 0, i = 0; // Most objects don't have any safe getters but inherit some from their // prototype. Avoid calling getOwnPropertyNames on objects that may have // many properties like Array, strings or js objects. That to avoid // freezing firefox when doing so. if (TYPED_ARRAY_CLASSES.includes(this.obj.class) || ["Array", "Object", "String"].includes(this.obj.class)) { obj = obj.proto; level++; } while (obj) { let getters = this._findSafeGetters(obj); for (let name of getters) { // Avoid overwriting properties from prototypes closer to this.obj. Also // avoid providing safeGetterValues from prototypes if property |name| // is already defined as an own property. if (name in safeGetterValues || (obj != this.obj && ownProperties.indexOf(name) !== -1)) { continue; } // Ignore __proto__ on Object.prototye. if (!obj.proto && name == "__proto__") { continue; } let desc = null, getter = null; try { desc = obj.getOwnPropertyDescriptor(name); getter = desc.get; } catch (ex) { // The above can throw if the cache becomes stale. } if (!getter) { obj._safeGetters = null; continue; } let result = getter.call(this.obj); if (result && !("throw" in result)) { let getterValue = undefined; if ("return" in result) { getterValue = result.return; } else if ("yield" in result) { getterValue = result.yield; } // WebIDL attributes specified with the LenientThis extended attribute // return undefined and should be ignored. if (getterValue !== undefined) { safeGetterValues[name] = { getterValue: this.hooks.createValueGrip(getterValue), getterPrototypeLevel: level, enumerable: desc.enumerable, writable: level == 0 ? desc.writable : true, }; if (limit && ++i == limit) { break; } } } } if (limit && i == limit) { break; } obj = obj.proto; level++; } return safeGetterValues; }, /** * Find the safe getters for a given Debugger.Object. Safe getters are native * getters which are safe to execute. * * @private * @param Debugger.Object object * The Debugger.Object where you want to find safe getters. * @return Set * A Set of names of safe getters. This result is cached for each * Debugger.Object. */ _findSafeGetters: function (object) { if (object._safeGetters) { return object._safeGetters; } let getters = new Set(); let names = []; try { names = object.getOwnPropertyNames(); } catch (ex) { // Calling getOwnPropertyNames() on some wrapped native prototypes is not // allowed: "cannot modify properties of a WrappedNative". See bug 952093. } for (let name of names) { let desc = null; try { desc = object.getOwnPropertyDescriptor(name); } catch (e) { // Calling getOwnPropertyDescriptor on wrapped native prototypes is not // allowed (bug 560072). } if (!desc || desc.value !== undefined || !("get" in desc)) { continue; } if (DevToolsUtils.hasSafeGetter(desc)) { getters.add(name); } } object._safeGetters = getters; return getters; }, /** * Handle a protocol request to provide the prototype of the object. */ onPrototype: function () { return { from: this.actorID, prototype: this.hooks.createValueGrip(this.obj.proto) }; }, /** * Handle a protocol request to provide the property descriptor of the * object's specified property. * * @param request object * The protocol request object. */ onProperty: function (request) { if (!request.name) { return { error: "missingParameter", message: "no property name was specified" }; } return { from: this.actorID, descriptor: this._propertyDescriptor(request.name) }; }, /** * Handle a protocol request to provide the display string for the object. */ onDisplayString: function () { const string = stringify(this.obj); return { from: this.actorID, displayString: this.hooks.createValueGrip(string) }; }, /** * A helper method that creates a property descriptor for the provided object, * properly formatted for sending in a protocol response. * * @private * @param string name * The property that the descriptor is generated for. * @param boolean [onlyEnumerable] * Optional: true if you want a descriptor only for an enumerable * property, false otherwise. * @return object|undefined * The property descriptor, or undefined if this is not an enumerable * property and onlyEnumerable=true. */ _propertyDescriptor: function (name, onlyEnumerable) { let desc; try { desc = this.obj.getOwnPropertyDescriptor(name); } catch (e) { // Calling getOwnPropertyDescriptor on wrapped native prototypes is not // allowed (bug 560072). Inform the user with a bogus, but hopefully // explanatory, descriptor. return { configurable: false, writable: false, enumerable: false, value: e.name }; } if (!desc || onlyEnumerable && !desc.enumerable) { return undefined; } let retval = { configurable: desc.configurable, enumerable: desc.enumerable }; if ("value" in desc) { retval.writable = desc.writable; retval.value = this.hooks.createValueGrip(desc.value); } else { if ("get" in desc) { retval.get = this.hooks.createValueGrip(desc.get); } if ("set" in desc) { retval.set = this.hooks.createValueGrip(desc.set); } } return retval; }, /** * Handle a protocol request to provide the source code of a function. * * @param request object * The protocol request object. */ onDecompile: function (request) { if (this.obj.class !== "Function") { return { error: "objectNotFunction", message: "decompile request is only valid for object grips " + "with a 'Function' class." }; } return { from: this.actorID, decompiledCode: this.obj.decompile(!!request.pretty) }; }, /** * Handle a protocol request to provide the parameters of a function. */ onParameterNames: function () { if (this.obj.class !== "Function") { return { error: "objectNotFunction", message: "'parameterNames' request is only valid for object " + "grips with a 'Function' class." }; } return { parameterNames: this.obj.parameterNames }; }, /** * Handle a protocol request to release a thread-lifetime grip. */ onRelease: function () { this.release(); return {}; }, /** * Handle a protocol request to provide the lexical scope of a function. */ onScope: function () { if (this.obj.class !== "Function") { return { error: "objectNotFunction", message: "scope request is only valid for object grips with a" + " 'Function' class." }; } let envActor = this.hooks.createEnvironmentActor(this.obj.environment, this.registeredPool); if (!envActor) { return { error: "notDebuggee", message: "cannot access the environment of this function." }; } return { from: this.actorID, scope: envActor.form() }; }, /** * Handle a protocol request to get the list of dependent promises of a * promise. * * @return object * Returns an object containing an array of object grips of the * dependent promises */ onDependentPromises: function () { if (this.obj.class != "Promise") { return { error: "objectNotPromise", message: "'dependentPromises' request is only valid for " + "object grips with a 'Promise' class." }; } let promises = this.obj.promiseDependentPromises.map(p => this.hooks.createValueGrip(p)); return { promises }; }, /** * Handle a protocol request to get the allocation stack of a promise. */ onAllocationStack: function () { if (this.obj.class != "Promise") { return { error: "objectNotPromise", message: "'allocationStack' request is only valid for " + "object grips with a 'Promise' class." }; } let stack = this.obj.promiseAllocationSite; let allocationStacks = []; while (stack) { if (stack.source) { let source = this._getSourceOriginalLocation(stack); if (source) { allocationStacks.push(source); } } stack = stack.parent; } return Promise.all(allocationStacks).then(stacks => { return { allocationStack: stacks }; }); }, /** * Handle a protocol request to get the fulfillment stack of a promise. */ onFulfillmentStack: function () { if (this.obj.class != "Promise") { return { error: "objectNotPromise", message: "'fulfillmentStack' request is only valid for " + "object grips with a 'Promise' class." }; } let stack = this.obj.promiseResolutionSite; let fulfillmentStacks = []; while (stack) { if (stack.source) { let source = this._getSourceOriginalLocation(stack); if (source) { fulfillmentStacks.push(source); } } stack = stack.parent; } return Promise.all(fulfillmentStacks).then(stacks => { return { fulfillmentStack: stacks }; }); }, /** * Handle a protocol request to get the rejection stack of a promise. */ onRejectionStack: function () { if (this.obj.class != "Promise") { return { error: "objectNotPromise", message: "'rejectionStack' request is only valid for " + "object grips with a 'Promise' class." }; } let stack = this.obj.promiseResolutionSite; let rejectionStacks = []; while (stack) { if (stack.source) { let source = this._getSourceOriginalLocation(stack); if (source) { rejectionStacks.push(source); } } stack = stack.parent; } return Promise.all(rejectionStacks).then(stacks => { return { rejectionStack: stacks }; }); }, /** * Helper function for fetching the source location of a SavedFrame stack. * * @param SavedFrame stack * The promise allocation stack frame * @return object * Returns an object containing the source location of the SavedFrame * stack. */ _getSourceOriginalLocation: function (stack) { let source; // Catch any errors if the source actor cannot be found try { source = this.hooks.sources().getSourceActorByURL(stack.source); } catch (e) {} if (!source) { return null; } return this.hooks.sources().getOriginalLocation(new GeneratedLocation( source, stack.line, stack.column )).then((originalLocation) => { return { source: originalLocation.originalSourceActor.form(), line: originalLocation.originalLine, column: originalLocation.originalColumn, functionDisplayName: stack.functionDisplayName }; }); } }; ObjectActor.prototype.requestTypes = { "definitionSite": ObjectActor.prototype.onDefinitionSite, "parameterNames": ObjectActor.prototype.onParameterNames, "prototypeAndProperties": ObjectActor.prototype.onPrototypeAndProperties, "enumProperties": ObjectActor.prototype.onEnumProperties, "prototype": ObjectActor.prototype.onPrototype, "property": ObjectActor.prototype.onProperty, "displayString": ObjectActor.prototype.onDisplayString, "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames, "decompile": ObjectActor.prototype.onDecompile, "release": ObjectActor.prototype.onRelease, "scope": ObjectActor.prototype.onScope, "dependentPromises": ObjectActor.prototype.onDependentPromises, "allocationStack": ObjectActor.prototype.onAllocationStack, "fulfillmentStack": ObjectActor.prototype.onFulfillmentStack, "rejectionStack": ObjectActor.prototype.onRejectionStack, "enumEntries": ObjectActor.prototype.onEnumEntries, }; /** * Creates an actor to iterate over an object's property names and values. * * @param objectActor ObjectActor * The object actor. * @param options Object * A dictionary object with various boolean attributes: * - enumEntries Boolean * If true, enumerates the entries of a Map or Set object * instead of enumerating properties. * - ignoreIndexedProperties Boolean * If true, filters out Array items. * e.g. properties names between `0` and `object.length`. * - ignoreNonIndexedProperties Boolean * If true, filters out items that aren't array items * e.g. properties names that are not a number between `0` * and `object.length`. * - sort Boolean * If true, the iterator will sort the properties by name * before dispatching them. * - query String * If non-empty, will filter the properties by names and values * containing this query string. The match is not case-sensitive. * Regarding value filtering it just compare to the stringification * of the property value. */ function PropertyIteratorActor(objectActor, options) { if (options.enumEntries) { let cls = objectActor.obj.class; if (cls == "Map") { this.iterator = enumMapEntries(objectActor); } else if (cls == "WeakMap") { this.iterator = enumWeakMapEntries(objectActor); } else if (cls == "Set") { this.iterator = enumSetEntries(objectActor); } else if (cls == "WeakSet") { this.iterator = enumWeakSetEntries(objectActor); } else { throw new Error("Unsupported class to enumerate entries from: " + cls); } } else if (options.ignoreNonIndexedProperties && !options.query) { this.iterator = enumArrayProperties(objectActor, options); } else { this.iterator = enumObjectProperties(objectActor, options); } } PropertyIteratorActor.prototype = { actorPrefix: "propertyIterator", grip() { return { type: this.actorPrefix, actor: this.actorID, count: this.iterator.size }; }, names({ indexes }) { let list = []; for (let idx of indexes) { list.push(this.iterator.propertyName(idx)); } return { names: indexes }; }, slice({ start, count }) { let ownProperties = Object.create(null); for (let i = start, m = start + count; i < m; i++) { let name = this.iterator.propertyName(i); ownProperties[name] = this.iterator.propertyDescription(i); } return { ownProperties }; }, all() { return this.slice({ start: 0, count: this.length }); } }; PropertyIteratorActor.prototype.requestTypes = { "names": PropertyIteratorActor.prototype.names, "slice": PropertyIteratorActor.prototype.slice, "all": PropertyIteratorActor.prototype.all, }; function enumArrayProperties(objectActor, options) { let length = DevToolsUtils.getProperty(objectActor.obj, "length"); if (typeof length !== "number") { // Pseudo arrays are flagged as ArrayLike if they have // subsequent indexed properties without having any length attribute. length = 0; let names = objectActor.obj.getOwnPropertyNames(); for (let key of names) { if (isNaN(key) || key != length++) { break; } } } return { size: length, propertyName(index) { return index; }, propertyDescription(index) { return objectActor._propertyDescriptor(index); } }; } function enumObjectProperties(objectActor, options) { let names = []; try { names = objectActor.obj.getOwnPropertyNames(); } catch (ex) { // Calling getOwnPropertyNames() on some wrapped native prototypes is not // allowed: "cannot modify properties of a WrappedNative". See bug 952093. } if (options.ignoreNonIndexedProperties || options.ignoreIndexedProperties) { let length = DevToolsUtils.getProperty(objectActor.obj, "length"); if (typeof length !== "number") { // Pseudo arrays are flagged as ArrayLike if they have // subsequent indexed properties without having any length attribute. length = 0; for (let key of names) { if (isNaN(key) || key != length++) { break; } } } // It appears that getOwnPropertyNames always returns indexed properties // first, so we can safely slice `names` for/against indexed properties. // We do such clever operation to optimize very large array inspection, // like webaudio buffers. if (options.ignoreIndexedProperties) { // Keep items after `length` index names = names.slice(length); } else if (options.ignoreNonIndexedProperties) { // Remove `length` first items names.splice(length); } } let safeGetterValues = objectActor._findSafeGetterValues(names, 0); let safeGetterNames = Object.keys(safeGetterValues); // Merge the safe getter values into the existing properties list. for (let name of safeGetterNames) { if (!names.includes(name)) { names.push(name); } } if (options.query) { let { query } = options; query = query.toLowerCase(); names = names.filter(name => { // Filter on attribute names if (name.toLowerCase().includes(query)) { return true; } // and then on attribute values let desc; try { desc = objectActor.obj.getOwnPropertyDescriptor(name); } catch (e) { // Calling getOwnPropertyDescriptor on wrapped native prototypes is not // allowed (bug 560072). } if (desc && desc.value && String(desc.value).includes(query)) { return true; } return false; }); } if (options.sort) { names.sort(); } return { size: names.length, propertyName(index) { return names[index]; }, propertyDescription(index) { let name = names[index]; let desc = objectActor._propertyDescriptor(name); if (!desc) { desc = safeGetterValues[name]; } else if (name in safeGetterValues) { // Merge the safe getter values into the existing properties list. let { getterValue, getterPrototypeLevel } = safeGetterValues[name]; desc.getterValue = getterValue; desc.getterPrototypeLevel = getterPrototypeLevel; } return desc; } }; } /** * Helper function to create a grip from a Map/Set entry */ function gripFromEntry({ obj, hooks }, entry) { return hooks.createValueGrip( makeDebuggeeValueIfNeeded(obj, Cu.unwaiveXrays(entry))); } function enumMapEntries(objectActor) { // Iterating over a Map via .entries goes through various intermediate // objects - an Iterator object, then a 2-element Array object, then the // actual values we care about. We don't have Xrays to Iterator objects, // so we get Opaque wrappers for them. And even though we have Xrays to // Arrays, the semantics often deny access to the entires based on the // nature of the values. So we need waive Xrays for the iterator object // and the tupes, and then re-apply them on the underlying values until // we fix bug 1023984. // // Even then though, we might want to continue waiving Xrays here for the // same reason we do so for Arrays above - this filtering behavior is likely // to be more confusing than beneficial in the case of Object previews. let raw = objectActor.obj.unsafeDereference(); let keys = [...Cu.waiveXrays(Map.prototype.keys.call(raw))]; return { [Symbol.iterator]: function* () { for (let key of keys) { let value = Map.prototype.get.call(raw, key); yield [ key, value ].map(val => gripFromEntry(objectActor, val)); } }, size: keys.length, propertyName(index) { return index; }, propertyDescription(index) { let key = keys[index]; let val = Map.prototype.get.call(raw, key); return { enumerable: true, value: { type: "mapEntry", preview: { key: gripFromEntry(objectActor, key), value: gripFromEntry(objectActor, val) } } }; } }; } function enumWeakMapEntries(objectActor) { // We currently lack XrayWrappers for WeakMap, so when we iterate over // the values, the temporary iterator objects get created in the target // compartment. However, we _do_ have Xrays to Object now, so we end up // Xraying those temporary objects, and filtering access to |it.value| // based on whether or not it's Xrayable and/or callable, which breaks // the for/of iteration. // // This code is designed to handle untrusted objects, so we can safely // waive Xrays on the iterable, and relying on the Debugger machinery to // make sure we handle the resulting objects carefully. let raw = objectActor.obj.unsafeDereference(); let keys = Cu.waiveXrays( ThreadSafeChromeUtils.nondeterministicGetWeakMapKeys(raw)); return { [Symbol.iterator]: function* () { for (let key of keys) { let value = WeakMap.prototype.get.call(raw, key); yield [ key, value ].map(val => gripFromEntry(objectActor, val)); } }, size: keys.length, propertyName(index) { return index; }, propertyDescription(index) { let key = keys[index]; let val = WeakMap.prototype.get.call(raw, key); return { enumerable: true, value: { type: "mapEntry", preview: { key: gripFromEntry(objectActor, key), value: gripFromEntry(objectActor, val) } } }; } }; } function enumSetEntries(objectActor) { // We currently lack XrayWrappers for Set, so when we iterate over // the values, the temporary iterator objects get created in the target // compartment. However, we _do_ have Xrays to Object now, so we end up // Xraying those temporary objects, and filtering access to |it.value| // based on whether or not it's Xrayable and/or callable, which breaks // the for/of iteration. // // This code is designed to handle untrusted objects, so we can safely // waive Xrays on the iterable, and relying on the Debugger machinery to // make sure we handle the resulting objects carefully. let raw = objectActor.obj.unsafeDereference(); let values = [...Cu.waiveXrays(Set.prototype.values.call(raw))]; return { [Symbol.iterator]: function* () { for (let item of values) { yield gripFromEntry(objectActor, item); } }, size: values.length, propertyName(index) { return index; }, propertyDescription(index) { let val = values[index]; return { enumerable: true, value: gripFromEntry(objectActor, val) }; } }; } function enumWeakSetEntries(objectActor) { // We currently lack XrayWrappers for WeakSet, so when we iterate over // the values, the temporary iterator objects get created in the target // compartment. However, we _do_ have Xrays to Object now, so we end up // Xraying those temporary objects, and filtering access to |it.value| // based on whether or not it's Xrayable and/or callable, which breaks // the for/of iteration. // // This code is designed to handle untrusted objects, so we can safely // waive Xrays on the iterable, and relying on the Debugger machinery to // make sure we handle the resulting objects carefully. let raw = objectActor.obj.unsafeDereference(); let keys = Cu.waiveXrays( ThreadSafeChromeUtils.nondeterministicGetWeakSetKeys(raw)); return { [Symbol.iterator]: function* () { for (let item of keys) { yield gripFromEntry(objectActor, item); } }, size: keys.length, propertyName(index) { return index; }, propertyDescription(index) { let val = keys[index]; return { enumerable: true, value: gripFromEntry(objectActor, val) }; } }; } /** * Functions for adding information to ObjectActor grips for the purpose of * having customized output. This object holds arrays mapped by * Debugger.Object.prototype.class. * * In each array you can add functions that take three * arguments: * - the ObjectActor instance and its hooks to make a preview for, * - the grip object being prepared for the client, * - the raw JS object after calling Debugger.Object.unsafeDereference(). This * argument is only provided if the object is safe for reading properties and * executing methods. See DevToolsUtils.isSafeJSObject(). * * Functions must return false if they cannot provide preview * information for the debugger object, or true otherwise. */ DebuggerServer.ObjectActorPreviewers = { String: [function (objectActor, grip, rawObj) { return wrappedPrimitivePreviewer("String", String, objectActor, grip, rawObj); }], Boolean: [function (objectActor, grip, rawObj) { return wrappedPrimitivePreviewer("Boolean", Boolean, objectActor, grip, rawObj); }], Number: [function (objectActor, grip, rawObj) { return wrappedPrimitivePreviewer("Number", Number, objectActor, grip, rawObj); }], Function: [function ({obj, hooks}, grip) { if (obj.name) { grip.name = obj.name; } if (obj.displayName) { grip.displayName = obj.displayName.substr(0, 500); } if (obj.parameterNames) { grip.parameterNames = obj.parameterNames; } // Check if the developer has added a de-facto standard displayName // property for us to use. let userDisplayName; try { userDisplayName = obj.getOwnPropertyDescriptor("displayName"); } catch (e) { // Calling getOwnPropertyDescriptor with displayName might throw // with "permission denied" errors for some functions. dumpn(e); } if (userDisplayName && typeof userDisplayName.value == "string" && userDisplayName.value) { grip.userDisplayName = hooks.createValueGrip(userDisplayName.value); } let dbgGlobal = hooks.getGlobalDebugObject(); if (dbgGlobal) { let script = dbgGlobal.makeDebuggeeValue(obj.unsafeDereference()).script; if (script) { grip.location = { url: script.url, line: script.startLine }; } } return true; }], RegExp: [function ({obj, hooks}, grip) { let str = RegExp.prototype.toString.call(obj.unsafeDereference()); grip.displayString = hooks.createValueGrip(str); return true; }], Date: [function ({obj, hooks}, grip) { let time = Date.prototype.getTime.call(obj.unsafeDereference()); grip.preview = { timestamp: hooks.createValueGrip(time), }; return true; }], Array: [function ({obj, hooks}, grip) { let length = DevToolsUtils.getProperty(obj, "length"); if (typeof length != "number") { return false; } grip.preview = { kind: "ArrayLike", length: length, }; if (hooks.getGripDepth() > 1) { return true; } let raw = obj.unsafeDereference(); let items = grip.preview.items = []; for (let i = 0; i < length; ++i) { // Array Xrays filter out various possibly-unsafe properties (like // functions, and claim that the value is undefined instead. This // is generally the right thing for privileged code accessing untrusted // objects, but quite confusing for Object previews. So we manually // override this protection by waiving Xrays on the array, and re-applying // Xrays on any indexed value props that we pull off of it. let desc = Object.getOwnPropertyDescriptor(Cu.waiveXrays(raw), i); if (desc && !desc.get && !desc.set) { let value = Cu.unwaiveXrays(desc.value); value = makeDebuggeeValueIfNeeded(obj, value); items.push(hooks.createValueGrip(value)); } else { items.push(null); } if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { break; } } return true; }], Set: [function (objectActor, grip) { let size = DevToolsUtils.getProperty(objectActor.obj, "size"); if (typeof size != "number") { return false; } grip.preview = { kind: "ArrayLike", length: size, }; // Avoid recursive object grips. if (objectActor.hooks.getGripDepth() > 1) { return true; } let items = grip.preview.items = []; for (let item of enumSetEntries(objectActor)) { items.push(item); if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { break; } } return true; }], WeakSet: [function (objectActor, grip) { let enumEntries = enumWeakSetEntries(objectActor); grip.preview = { kind: "ArrayLike", length: enumEntries.size }; // Avoid recursive object grips. if (objectActor.hooks.getGripDepth() > 1) { return true; } let items = grip.preview.items = []; for (let item of enumEntries) { items.push(item); if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { break; } } return true; }], Map: [function (objectActor, grip) { let size = DevToolsUtils.getProperty(objectActor.obj, "size"); if (typeof size != "number") { return false; } grip.preview = { kind: "MapLike", size: size, }; if (objectActor.hooks.getGripDepth() > 1) { return true; } let entries = grip.preview.entries = []; for (let entry of enumMapEntries(objectActor)) { entries.push(entry); if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { break; } } return true; }], WeakMap: [function (objectActor, grip) { let enumEntries = enumWeakMapEntries(objectActor); grip.preview = { kind: "MapLike", size: enumEntries.size }; if (objectActor.hooks.getGripDepth() > 1) { return true; } let entries = grip.preview.entries = []; for (let entry of enumEntries) { entries.push(entry); if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { break; } } return true; }], DOMStringMap: [function ({obj, hooks}, grip, rawObj) { if (!rawObj) { return false; } let keys = obj.getOwnPropertyNames(); grip.preview = { kind: "MapLike", size: keys.length, }; if (hooks.getGripDepth() > 1) { return true; } let entries = grip.preview.entries = []; for (let key of keys) { let value = makeDebuggeeValueIfNeeded(obj, rawObj[key]); entries.push([key, hooks.createValueGrip(value)]); if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { break; } } return true; }], Proxy: [function ({obj, hooks}, grip, rawObj) { grip.preview = { kind: "Object", ownProperties: Object.create(null), ownPropertiesLength: 2 }; if (hooks.getGripDepth() > 1) { return true; } grip.preview.ownProperties[''] = {value: grip.proxyTarget}; grip.preview.ownProperties[''] = {value: grip.proxyHandler}; return true; }], }; /** * Generic previewer for classes wrapping primitives, like String, * Number and Boolean. * * @param string className * Class name to expect. * @param object classObj * The class to expect, eg. String. The valueOf() method of the class is * invoked on the given object. * @param ObjectActor objectActor * The object actor * @param Object grip * The result grip to fill in * @return Booolean true if the object was handled, false otherwise */ function wrappedPrimitivePreviewer(className, classObj, objectActor, grip, rawObj) { let {obj, hooks} = objectActor; if (!obj.proto || obj.proto.class != className) { return false; } let v = null; try { v = classObj.prototype.valueOf.call(rawObj); } catch (ex) { // valueOf() can throw if the raw JS object is "misbehaved". return false; } if (v === null) { return false; } let canHandle = GenericObject(objectActor, grip, rawObj, className === "String"); if (!canHandle) { return false; } grip.preview.wrappedValue = hooks.createValueGrip(makeDebuggeeValueIfNeeded(obj, v)); return true; } function GenericObject(objectActor, grip, rawObj, specialStringBehavior = false) { let {obj, hooks} = objectActor; if (grip.preview || grip.displayString || hooks.getGripDepth() > 1) { return false; } let i = 0, names = []; let preview = grip.preview = { kind: "Object", ownProperties: Object.create(null), }; try { names = obj.getOwnPropertyNames(); } catch (ex) { // Calling getOwnPropertyNames() on some wrapped native prototypes is not // allowed: "cannot modify properties of a WrappedNative". See bug 952093. } preview.ownPropertiesLength = names.length; let length; if (specialStringBehavior) { length = DevToolsUtils.getProperty(obj, "length"); if (typeof length != "number") { specialStringBehavior = false; } } for (let name of names) { if (specialStringBehavior && /^[0-9]+$/.test(name)) { let num = parseInt(name, 10); if (num.toString() === name && num >= 0 && num < length) { continue; } } let desc = objectActor._propertyDescriptor(name, true); if (!desc) { continue; } preview.ownProperties[name] = desc; if (++i == OBJECT_PREVIEW_MAX_ITEMS) { break; } } if (i < OBJECT_PREVIEW_MAX_ITEMS) { preview.safeGetterValues = objectActor._findSafeGetterValues( Object.keys(preview.ownProperties), OBJECT_PREVIEW_MAX_ITEMS - i); } return true; } // Preview functions that do not rely on the object class. DebuggerServer.ObjectActorPreviewers.Object = [ function TypedArray({obj, hooks}, grip) { if (TYPED_ARRAY_CLASSES.indexOf(obj.class) == -1) { return false; } let length = DevToolsUtils.getProperty(obj, "length"); if (typeof length != "number") { return false; } grip.preview = { kind: "ArrayLike", length: length, }; if (hooks.getGripDepth() > 1) { return true; } let raw = obj.unsafeDereference(); let global = Cu.getGlobalForObject(DebuggerServer); let classProto = global[obj.class].prototype; // The Xray machinery for TypedArrays denies indexed access on the grounds // that it's slow, and advises callers to do a structured clone instead. let safeView = Cu.cloneInto(classProto.subarray.call(raw, 0, OBJECT_PREVIEW_MAX_ITEMS), global); let items = grip.preview.items = []; for (let i = 0; i < safeView.length; i++) { items.push(safeView[i]); } return true; }, function Error({obj, hooks}, grip) { switch (obj.class) { case "Error": case "EvalError": case "RangeError": case "ReferenceError": case "SyntaxError": case "TypeError": case "URIError": let name = DevToolsUtils.getProperty(obj, "name"); let msg = DevToolsUtils.getProperty(obj, "message"); let stack = DevToolsUtils.getProperty(obj, "stack"); let fileName = DevToolsUtils.getProperty(obj, "fileName"); let lineNumber = DevToolsUtils.getProperty(obj, "lineNumber"); let columnNumber = DevToolsUtils.getProperty(obj, "columnNumber"); grip.preview = { kind: "Error", name: hooks.createValueGrip(name), message: hooks.createValueGrip(msg), stack: hooks.createValueGrip(stack), fileName: hooks.createValueGrip(fileName), lineNumber: hooks.createValueGrip(lineNumber), columnNumber: hooks.createValueGrip(columnNumber), }; return true; default: return false; } }, function CSSMediaRule({obj, hooks}, grip, rawObj) { if (isWorker || !rawObj || !(rawObj instanceof Ci.nsIDOMCSSMediaRule)) { return false; } grip.preview = { kind: "ObjectWithText", text: hooks.createValueGrip(rawObj.conditionText), }; return true; }, function CSSStyleRule({obj, hooks}, grip, rawObj) { if (isWorker || !rawObj || !(rawObj instanceof Ci.nsIDOMCSSStyleRule)) { return false; } grip.preview = { kind: "ObjectWithText", text: hooks.createValueGrip(rawObj.selectorText), }; return true; }, function ObjectWithURL({obj, hooks}, grip, rawObj) { if (isWorker || !rawObj || !(rawObj instanceof Ci.nsIDOMCSSImportRule || rawObj instanceof Ci.nsIDOMCSSStyleSheet || rawObj instanceof Ci.nsIDOMLocation || rawObj instanceof Ci.nsIDOMWindow)) { return false; } let url; if (rawObj instanceof Ci.nsIDOMWindow && rawObj.location) { url = rawObj.location.href; } else if (rawObj.href) { url = rawObj.href; } else { return false; } grip.preview = { kind: "ObjectWithURL", url: hooks.createValueGrip(url), }; return true; }, function ArrayLike({obj, hooks}, grip, rawObj) { if (isWorker || !rawObj || obj.class != "DOMStringList" && obj.class != "DOMTokenList" && !(rawObj instanceof Ci.nsIDOMMozNamedAttrMap || rawObj instanceof Ci.nsIDOMCSSRuleList || rawObj instanceof Ci.nsIDOMCSSValueList || rawObj instanceof Ci.nsIDOMFileList || rawObj instanceof Ci.nsIDOMFontFaceList || rawObj instanceof Ci.nsIDOMMediaList || rawObj instanceof Ci.nsIDOMNodeList || rawObj instanceof Ci.nsIDOMStyleSheetList)) { return false; } if (typeof rawObj.length != "number") { return false; } grip.preview = { kind: "ArrayLike", length: rawObj.length, }; if (hooks.getGripDepth() > 1) { return true; } let items = grip.preview.items = []; for (let i = 0; i < rawObj.length && items.length < OBJECT_PREVIEW_MAX_ITEMS; i++) { let value = makeDebuggeeValueIfNeeded(obj, rawObj[i]); items.push(hooks.createValueGrip(value)); } return true; }, function CSSStyleDeclaration({obj, hooks}, grip, rawObj) { if (isWorker || !rawObj || !(rawObj instanceof Ci.nsIDOMCSSStyleDeclaration)) { return false; } grip.preview = { kind: "MapLike", size: rawObj.length, }; let entries = grip.preview.entries = []; for (let i = 0; i < OBJECT_PREVIEW_MAX_ITEMS && i < rawObj.length; i++) { let prop = rawObj[i]; let value = rawObj.getPropertyValue(prop); entries.push([prop, hooks.createValueGrip(value)]); } return true; }, function DOMNode({obj, hooks}, grip, rawObj) { if (isWorker || obj.class == "Object" || !rawObj || !(rawObj instanceof Ci.nsIDOMNode)) { return false; } let preview = grip.preview = { kind: "DOMNode", nodeType: rawObj.nodeType, nodeName: rawObj.nodeName, }; if (rawObj instanceof Ci.nsIDOMDocument && rawObj.location) { preview.location = hooks.createValueGrip(rawObj.location.href); } else if (rawObj instanceof Ci.nsIDOMDocumentFragment) { preview.childNodesLength = rawObj.childNodes.length; if (hooks.getGripDepth() < 2) { preview.childNodes = []; for (let node of rawObj.childNodes) { let actor = hooks.createValueGrip(obj.makeDebuggeeValue(node)); preview.childNodes.push(actor); if (preview.childNodes.length == OBJECT_PREVIEW_MAX_ITEMS) { break; } } } } else if (rawObj instanceof Ci.nsIDOMElement) { // Add preview for DOM element attributes. if (rawObj instanceof Ci.nsIDOMHTMLElement) { preview.nodeName = preview.nodeName.toLowerCase(); } let i = 0; preview.attributes = {}; preview.attributesLength = rawObj.attributes.length; for (let attr of rawObj.attributes) { preview.attributes[attr.nodeName] = hooks.createValueGrip(attr.value); } } else if (rawObj instanceof Ci.nsIDOMAttr) { preview.value = hooks.createValueGrip(rawObj.value); } else if (rawObj instanceof Ci.nsIDOMText || rawObj instanceof Ci.nsIDOMComment) { preview.textContent = hooks.createValueGrip(rawObj.textContent); } return true; }, function DOMEvent({obj, hooks}, grip, rawObj) { if (isWorker || !rawObj || !(rawObj instanceof Ci.nsIDOMEvent)) { return false; } let preview = grip.preview = { kind: "DOMEvent", type: rawObj.type, properties: Object.create(null), }; if (hooks.getGripDepth() < 2) { let target = obj.makeDebuggeeValue(rawObj.target); preview.target = hooks.createValueGrip(target); } let props = []; if (rawObj instanceof Ci.nsIDOMMouseEvent) { props.push("buttons", "clientX", "clientY", "layerX", "layerY"); } else if (rawObj instanceof Ci.nsIDOMKeyEvent) { let modifiers = []; if (rawObj.altKey) { modifiers.push("Alt"); } if (rawObj.ctrlKey) { modifiers.push("Control"); } if (rawObj.metaKey) { modifiers.push("Meta"); } if (rawObj.shiftKey) { modifiers.push("Shift"); } preview.eventKind = "key"; preview.modifiers = modifiers; props.push("key", "charCode", "keyCode"); } else if (rawObj instanceof Ci.nsIDOMTransitionEvent) { props.push("propertyName", "pseudoElement"); } else if (rawObj instanceof Ci.nsIDOMAnimationEvent) { props.push("animationName", "pseudoElement"); } else if (rawObj instanceof Ci.nsIDOMClipboardEvent) { props.push("clipboardData"); } // Add event-specific properties. for (let prop of props) { let value = rawObj[prop]; if (value && (typeof value == "object" || typeof value == "function")) { // Skip properties pointing to objects. if (hooks.getGripDepth() > 1) { continue; } value = obj.makeDebuggeeValue(value); } preview.properties[prop] = hooks.createValueGrip(value); } // Add any properties we find on the event object. if (!props.length) { let i = 0; for (let prop in rawObj) { let value = rawObj[prop]; if (prop == "target" || prop == "type" || value === null || typeof value == "function") { continue; } if (value && typeof value == "object") { if (hooks.getGripDepth() > 1) { continue; } value = obj.makeDebuggeeValue(value); } preview.properties[prop] = hooks.createValueGrip(value); if (++i == OBJECT_PREVIEW_MAX_ITEMS) { break; } } } return true; }, function DOMException({obj, hooks}, grip, rawObj) { if (isWorker || !rawObj || !(rawObj instanceof Ci.nsIDOMDOMException)) { return false; } grip.preview = { kind: "DOMException", name: hooks.createValueGrip(rawObj.name), message: hooks.createValueGrip(rawObj.message), code: hooks.createValueGrip(rawObj.code), result: hooks.createValueGrip(rawObj.result), filename: hooks.createValueGrip(rawObj.filename), lineNumber: hooks.createValueGrip(rawObj.lineNumber), columnNumber: hooks.createValueGrip(rawObj.columnNumber), }; return true; }, function PseudoArray({obj, hooks}, grip, rawObj) { let length; let keys = obj.getOwnPropertyNames(); if (keys.length == 0) { return false; } // If no item is going to be displayed in preview, better display as sparse object. // The first key should contain the smallest integer index (if any). if(keys[0] >= OBJECT_PREVIEW_MAX_ITEMS) { return false; } // Pseudo-arrays should only have array indices and, optionally, a "length" property. // Since integer indices are sorted first, check if the last property is "length". if(keys[keys.length-1] === "length") { keys.pop(); length = DevToolsUtils.getProperty(obj, "length"); } else { // Otherwise, let length be the (presumably) greatest array index plus 1. length = +keys[keys.length-1] + 1; } // Check if length is a valid array length, i.e. is a Uint32 number. if(typeof length !== "number" || length >>> 0 !== length) { return false; } // Ensure all keys are increasing array indices smaller than length. The order is not // guaranteed for exotic objects but, in most cases, big array indices and properties // which are not integer indices should be at the end. Then, iterating backwards // allows us to return earlier when the object is not completely a pseudo-array. let prev = length; for(let i = keys.length - 1; i >= 0; --i) { let key = keys[i]; let numKey = key >>> 0; // ToUint32(key) if (numKey + '' !== key || numKey >= prev) { return false; } prev = numKey; } grip.preview = { kind: "ArrayLike", length: length, }; // Avoid recursive object grips. if (hooks.getGripDepth() > 1) { return true; } let items = grip.preview.items = []; let numItems = Math.min(OBJECT_PREVIEW_MAX_ITEMS, length); for (let i = 0; i < numItems; ++i) { let desc = obj.getOwnPropertyDescriptor(i); if (desc && 'value' in desc) { items.push(hooks.createValueGrip(desc.value)); } else { items.push(null); } } return true; }, function Object(objectActor, grip, rawObj) { return GenericObject(objectActor, grip, rawObj, /* specialStringBehavior = */ false); }, ]; /** * 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."); } let state = { state: obj.promiseState }; if (state.state === "fulfilled") { state.value = obj.promiseValue; } else if (state.state === "rejected") { state.reason = obj.promiseReason; } return state; } /** * Determine if a given value is non-primitive. * * @param Any value * The value to test. * @return Boolean * Whether the value is non-primitive. */ function isObject(value) { const type = typeof value; return type == "object" ? value !== null : type == "function"; } /** * Create a function that can safely stringify Debugger.Objects of a given * builtin type. * * @param Function ctor * The builtin class constructor. * @return Function * The stringifier for the class. */ function createBuiltinStringifier(ctor) { return obj => ctor.prototype.toString.call(obj.unsafeDereference()); } /** * Stringify a Debugger.Object-wrapped Error instance. * * @param Debugger.Object obj * The object to stringify. * @return String * The stringification of the object. */ function errorStringify(obj) { let name = DevToolsUtils.getProperty(obj, "name"); if (name === "" || name === undefined) { name = obj.class; } else if (isObject(name)) { name = stringify(name); } let message = DevToolsUtils.getProperty(obj, "message"); if (isObject(message)) { message = stringify(message); } if (message === "" || message === undefined) { return name; } return name + ": " + message; } /** * Stringify a Debugger.Object based on its class. * * @param Debugger.Object obj * The object to stringify. * @return String * The stringification for the object. */ function stringify(obj) { if (obj.class == "DeadObject") { const error = new Error("Dead object encountered."); DevToolsUtils.reportException("stringify", error); return ""; } const stringifier = stringifiers[obj.class] || stringifiers.Object; try { return stringifier(obj); } catch (e) { DevToolsUtils.reportException("stringify", e); return ""; } } // Used to prevent infinite recursion when an array is found inside itself. var seen = null; var stringifiers = { Error: errorStringify, EvalError: errorStringify, RangeError: errorStringify, ReferenceError: errorStringify, SyntaxError: errorStringify, TypeError: errorStringify, URIError: errorStringify, Boolean: createBuiltinStringifier(Boolean), Function: createBuiltinStringifier(Function), Number: createBuiltinStringifier(Number), RegExp: createBuiltinStringifier(RegExp), String: createBuiltinStringifier(String), Object: obj => "[object " + obj.class + "]", Array: obj => { // If we're at the top level then we need to create the Set for tracking // previously stringified arrays. const topLevel = !seen; if (topLevel) { seen = new Set(); } else if (seen.has(obj)) { return ""; } seen.add(obj); const len = DevToolsUtils.getProperty(obj, "length"); let string = ""; // The following check is only required because the debuggee could possibly // be a Proxy and return any value. For normal objects, array.length is // always a non-negative integer. if (typeof len == "number" && len > 0) { for (let i = 0; i < len; i++) { const desc = obj.getOwnPropertyDescriptor(i); if (desc) { const { value } = desc; if (value != null) { string += isObject(value) ? stringify(value) : value; } } if (i < len - 1) { string += ","; } } } if (topLevel) { seen = null; } return string; }, DOMException: obj => { const message = DevToolsUtils.getProperty(obj, "message") || ""; const result = (+DevToolsUtils.getProperty(obj, "result")).toString(16); const code = DevToolsUtils.getProperty(obj, "code"); const name = DevToolsUtils.getProperty(obj, "name") || ""; return '[Exception... "' + message + '" ' + 'code: "' + code + '" ' + 'nsresult: "0x' + result + " (" + name + ')"]'; }, Promise: obj => { const { state, value, reason } = getPromiseState(obj); let statePreview = state; if (state != "pending") { const settledValue = state === "fulfilled" ? value : reason; statePreview += ": " + (typeof settledValue === "object" && settledValue !== null ? stringify(settledValue) : settledValue); } return "Promise (" + statePreview + ")"; }, }; /** * 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 (value && (typeof value == "object" || typeof value == "function")) { return obj.makeDebuggeeValue(value); } return value; } /** * Creates an actor for the specied "very long" string. "Very long" is specified * at the server's discretion. * * @param string String * The string. */ function LongStringActor(string) { this.string = string; this.stringLength = string.length; } LongStringActor.prototype = { actorPrefix: "longString", disconnect: function () { // Because longStringActors is not a weak map, we won't automatically leave // it so we need to manually leave on disconnect so that we don't leak // memory. this._releaseActor(); }, /** * Returns a grip for this actor for returning in a protocol message. */ grip: function () { return { "type": "longString", "initial": this.string.substring( 0, DebuggerServer.LONG_STRING_INITIAL_LENGTH), "length": this.stringLength, "actor": this.actorID }; }, /** * Handle a request to extract part of this actor's string. * * @param request object * The protocol request object. */ onSubstring: function (request) { return { "from": this.actorID, "substring": this.string.substring(request.start, request.end) }; }, /** * Handle a request to release this LongStringActor instance. */ onRelease: function () { // TODO: also check if registeredPool === threadActor.threadLifetimePool // when the web console moves aray from manually releasing pause-scoped // actors. this._releaseActor(); this.registeredPool.removeActor(this); return {}; }, _releaseActor: function () { if (this.registeredPool && this.registeredPool.longStringActors) { delete this.registeredPool.longStringActors[this.string]; } } }; LongStringActor.prototype.requestTypes = { "substring": LongStringActor.prototype.onSubstring, "release": LongStringActor.prototype.onRelease }; /** * Create a grip for the given debuggee value. If the value is an * object, will create an actor with the given lifetime. */ function createValueGrip(value, pool, makeObjectGrip) { switch (typeof value) { case "boolean": return value; case "string": if (stringIsLong(value)) { return longStringGrip(value, pool); } return 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 "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 makeObjectGrip(value, pool); case "symbol": let form = { type: "symbol" }; let name = getSymbolName(value); if (name !== undefined) { form.name = createValueGrip(name, pool, makeObjectGrip); } return form; default: assert(false, "Failed to provide a grip for: " + value); return null; } } const symbolProtoToString = Symbol.prototype.toString; function getSymbolName(symbol) { const name = symbolProtoToString.call(symbol).slice("Symbol(".length, -1); return name || undefined; } /** * Returns true if the string is long enough to use a LongStringActor instead * 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 >= DebuggerServer.LONG_STRING_LENGTH; } /** * Create a grip for the given string. * * @param str String * The string we are creating a grip for. * @param pool ActorPool * The actor pool where the new actor will be added. */ function longStringGrip(str, pool) { if (!pool.longStringActors) { pool.longStringActors = {}; } if (pool.longStringActors.hasOwnProperty(str)) { return pool.longStringActors[str].grip(); } let actor = new LongStringActor(str); pool.addActor(actor); pool.longStringActors[str] = actor; return actor.grip(); } exports.ObjectActor = ObjectActor; exports.PropertyIteratorActor = PropertyIteratorActor; exports.LongStringActor = LongStringActor; exports.createValueGrip = createValueGrip; exports.stringIsLong = stringIsLong; exports.longStringGrip = longStringGrip;