658 lines
21 KiB
JavaScript
658 lines
21 KiB
JavaScript
/*
|
|
* Copyright (C) 2009 Google Inc. All rights reserved.
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are
|
|
* met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the following disclaimer
|
|
* in the documentation and/or other materials provided with the
|
|
* distribution.
|
|
* * Neither the name of Google Inc. nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.RemoteObject = class RemoteObject
|
|
{
|
|
constructor(target, objectId, type, subtype, value, description, size, classPrototype, className, preview)
|
|
{
|
|
console.assert(type);
|
|
console.assert(!preview || preview instanceof WI.ObjectPreview);
|
|
console.assert(!target || target instanceof WI.Target);
|
|
|
|
this._target = target || WI.mainTarget;
|
|
this._type = type;
|
|
this._subtype = subtype;
|
|
|
|
if (objectId) {
|
|
// Object, Function, or Symbol.
|
|
console.assert(!subtype || typeof subtype === "string");
|
|
console.assert(!description || typeof description === "string");
|
|
console.assert(!value);
|
|
|
|
this._objectId = objectId;
|
|
this._description = description || "";
|
|
this._hasChildren = type !== "symbol";
|
|
this._size = size;
|
|
this._classPrototype = classPrototype;
|
|
this._preview = preview;
|
|
|
|
if (subtype === "class") {
|
|
this._functionDescription = this._description;
|
|
this._description = "class " + className;
|
|
}
|
|
} else {
|
|
// Primitive, BigInt, or null.
|
|
console.assert(type !== "object" || value === null);
|
|
console.assert(!preview);
|
|
|
|
this._description = description || (value + "");
|
|
this._hasChildren = false;
|
|
this._value = value;
|
|
|
|
if (type === "bigint") {
|
|
console.assert(value === undefined);
|
|
console.assert(description.endsWith("n"));
|
|
if (window.BigInt)
|
|
this._value = BigInt(description.substring(0, description.length - 1));
|
|
else
|
|
this._value = `${description} [BigInt Not Enabled in Web Inspector]`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Static
|
|
|
|
static createFakeRemoteObject()
|
|
{
|
|
return new WI.RemoteObject(undefined, WI.RemoteObject.FakeRemoteObjectId, "object");
|
|
}
|
|
|
|
static fromPrimitiveValue(value)
|
|
{
|
|
return new WI.RemoteObject(undefined, undefined, typeof value, undefined, value, undefined, undefined, undefined, undefined);
|
|
}
|
|
|
|
static createBigIntFromDescriptionString(description)
|
|
{
|
|
console.assert(description.endsWith("n"));
|
|
|
|
return new WI.RemoteObject(undefined, undefined, "bigint", undefined, undefined, description, undefined, undefined, undefined);
|
|
}
|
|
|
|
static fromPayload(payload, target)
|
|
{
|
|
console.assert(typeof payload === "object", "Remote object payload should only be an object");
|
|
|
|
if (payload.classPrototype)
|
|
payload.classPrototype = WI.RemoteObject.fromPayload(payload.classPrototype, target);
|
|
|
|
if (payload.preview)
|
|
payload.preview = WI.ObjectPreview.fromPayload(payload.preview);
|
|
|
|
return new WI.RemoteObject(target, payload.objectId, payload.type, payload.subtype, payload.value, payload.description, payload.size, payload.classPrototype, payload.className, payload.preview);
|
|
}
|
|
|
|
static createCallArgument(valueOrObject)
|
|
{
|
|
if (valueOrObject instanceof WI.RemoteObject) {
|
|
if (valueOrObject.objectId)
|
|
return {objectId: valueOrObject.objectId};
|
|
return {value: valueOrObject.value};
|
|
}
|
|
|
|
return {value: valueOrObject};
|
|
}
|
|
|
|
static resolveNode(node, objectGroup)
|
|
{
|
|
console.assert(node instanceof WI.DOMNode, node);
|
|
|
|
if (node.destroyed)
|
|
return Promise.reject("ERROR: node is destroyed");
|
|
|
|
let target = WI.assumingMainTarget();
|
|
return target.DOMAgent.resolveNode(node.id, objectGroup)
|
|
.then(({object}) => WI.RemoteObject.fromPayload(object, WI.mainTarget));
|
|
}
|
|
|
|
static resolveWebSocket(webSocketResource, objectGroup, callback)
|
|
{
|
|
console.assert(typeof callback === "function");
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.NetworkAgent.resolveWebSocket(webSocketResource.requestIdentifier, objectGroup, (error, object) => {
|
|
if (error || !object)
|
|
callback(null);
|
|
else
|
|
callback(WI.RemoteObject.fromPayload(object, webSocketResource.target));
|
|
});
|
|
}
|
|
|
|
static resolveCanvasContext(canvas, objectGroup, callback)
|
|
{
|
|
console.assert(typeof callback === "function");
|
|
|
|
function wrapCallback(error, object) {
|
|
if (error || !object)
|
|
callback(null);
|
|
else
|
|
callback(WI.RemoteObject.fromPayload(object, WI.mainTarget));
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
// COMPATIBILITY (iOS 13): Canvas.resolveCanvasContext was renamed to Canvas.resolveContext.
|
|
if (!target.hasCommand("Canvas.resolveContext")) {
|
|
target.CanvasAgent.resolveCanvasContext(canvas.identifier, objectGroup, wrapCallback);
|
|
return;
|
|
}
|
|
|
|
target.CanvasAgent.resolveContext(canvas.identifier, objectGroup, wrapCallback);
|
|
}
|
|
|
|
static resolveAnimation(animation, objectGroup, callback)
|
|
{
|
|
console.assert(typeof callback === "function");
|
|
|
|
function wrapCallback(error, object) {
|
|
if (error || !object)
|
|
callback(null);
|
|
else
|
|
callback(WI.RemoteObject.fromPayload(object, WI.mainTarget));
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
// COMPATIBILITY (iOS 13.1): Animation.resolveAnimation did not exist yet.
|
|
console.assert(target.hasCommand("Animation.resolveAnimation"));
|
|
|
|
target.AnimationAgent.resolveAnimation(animation.animationId, objectGroup, wrapCallback);
|
|
}
|
|
|
|
// Public
|
|
|
|
get target()
|
|
{
|
|
return this._target;
|
|
}
|
|
|
|
get objectId()
|
|
{
|
|
return this._objectId;
|
|
}
|
|
|
|
get type()
|
|
{
|
|
return this._type;
|
|
}
|
|
|
|
get subtype()
|
|
{
|
|
return this._subtype;
|
|
}
|
|
|
|
get description()
|
|
{
|
|
return this._description;
|
|
}
|
|
|
|
get functionDescription()
|
|
{
|
|
console.assert(this.type === "function");
|
|
|
|
return this._functionDescription || this._description;
|
|
}
|
|
|
|
get hasChildren()
|
|
{
|
|
return this._hasChildren;
|
|
}
|
|
|
|
get value()
|
|
{
|
|
return this._value;
|
|
}
|
|
|
|
get size()
|
|
{
|
|
return this._size || 0;
|
|
}
|
|
|
|
get classPrototype()
|
|
{
|
|
return this._classPrototype;
|
|
}
|
|
|
|
get preview()
|
|
{
|
|
return this._preview;
|
|
}
|
|
|
|
hasSize()
|
|
{
|
|
return this.isArray() || this.isCollectionType();
|
|
}
|
|
|
|
hasValue()
|
|
{
|
|
return "_value" in this;
|
|
}
|
|
|
|
canLoadPreview()
|
|
{
|
|
if (this._failedToLoadPreview)
|
|
return false;
|
|
|
|
if (this._type !== "object")
|
|
return false;
|
|
|
|
if (!this._objectId || this._isSymbol() || this._isFakeObject())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
updatePreview(callback)
|
|
{
|
|
if (!this.canLoadPreview()) {
|
|
callback(null);
|
|
return;
|
|
}
|
|
|
|
if (!this._target.hasCommand("Runtime.getPreview")) {
|
|
this._failedToLoadPreview = true;
|
|
callback(null);
|
|
return;
|
|
}
|
|
|
|
this._target.RuntimeAgent.getPreview(this._objectId, (error, payload) => {
|
|
if (error) {
|
|
this._failedToLoadPreview = true;
|
|
callback(null);
|
|
return;
|
|
}
|
|
|
|
this._preview = WI.ObjectPreview.fromPayload(payload);
|
|
callback(this._preview);
|
|
});
|
|
}
|
|
|
|
getPropertyDescriptors(callback, options = {})
|
|
{
|
|
if (!this._objectId || this._isSymbol() || this._isFakeObject()) {
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
this._getProperties(this._getPropertyDescriptorsResolver.bind(this, callback), options);
|
|
}
|
|
|
|
getDisplayablePropertyDescriptors(callback, options = {})
|
|
{
|
|
if (!this._objectId || this._isSymbol() || this._isFakeObject()) {
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
this._getDisplayableProperties(this._getPropertyDescriptorsResolver.bind(this, callback), options);
|
|
}
|
|
|
|
setPropertyValue(name, value, callback)
|
|
{
|
|
if (!this._objectId || this._isSymbol() || this._isFakeObject()) {
|
|
callback("Can't set a property of non-object.");
|
|
return;
|
|
}
|
|
|
|
// FIXME: It doesn't look like setPropertyValue is used yet. This will need to be tested when it is again (editable ObjectTrees).
|
|
this._target.RuntimeAgent.evaluate.invoke({expression: appendWebInspectorSourceURL(value), doNotPauseOnExceptionsAndMuteConsole: true}, evaluatedCallback.bind(this));
|
|
|
|
function evaluatedCallback(error, result, wasThrown)
|
|
{
|
|
if (error || wasThrown) {
|
|
callback(error || result.description);
|
|
return;
|
|
}
|
|
|
|
function setPropertyValue(propertyName, propertyValue)
|
|
{
|
|
this[propertyName] = propertyValue;
|
|
}
|
|
|
|
delete result.description; // Optimize on traffic.
|
|
|
|
this._target.RuntimeAgent.callFunctionOn(this._objectId, appendWebInspectorSourceURL(setPropertyValue.toString()), [{value: name}, result], true, undefined, propertySetCallback.bind(this));
|
|
|
|
if (result._objectId)
|
|
this._target.RuntimeAgent.releaseObject(result._objectId);
|
|
}
|
|
|
|
function propertySetCallback(error, result, wasThrown)
|
|
{
|
|
if (error || wasThrown) {
|
|
callback(error || result.description);
|
|
return;
|
|
}
|
|
|
|
callback();
|
|
}
|
|
}
|
|
|
|
isUndefined()
|
|
{
|
|
return this._type === "undefined";
|
|
}
|
|
|
|
isNode()
|
|
{
|
|
return this._subtype === "node";
|
|
}
|
|
|
|
isArray()
|
|
{
|
|
return this._subtype === "array";
|
|
}
|
|
|
|
isClass()
|
|
{
|
|
return this._subtype === "class";
|
|
}
|
|
|
|
isCollectionType()
|
|
{
|
|
return this._subtype === "map" || this._subtype === "set" || this._subtype === "weakmap" || this._subtype === "weakset";
|
|
}
|
|
|
|
isWeakCollection()
|
|
{
|
|
return this._subtype === "weakmap" || this._subtype === "weakset";
|
|
}
|
|
|
|
getCollectionEntries(callback, {fetchStart, fetchCount} = {})
|
|
{
|
|
console.assert(this.isCollectionType());
|
|
console.assert(typeof fetchStart === "undefined" || (typeof fetchStart === "number" && fetchStart >= 0), fetchStart);
|
|
console.assert(typeof fetchCount === "undefined" || (typeof fetchCount === "number" && fetchCount > 0), fetchCount);
|
|
|
|
// WeakMaps and WeakSets are not ordered. We should never send a non-zero start.
|
|
console.assert(!this.isWeakCollection() || typeof fetchStart === "undefined" || fetchStart === 0, fetchStart);
|
|
|
|
let objectGroup = this.isWeakCollection() ? this._weakCollectionObjectGroup() : "";
|
|
|
|
// COMPATIBILITY (iOS 13): `startIndex` and `numberToFetch` were renamed to `fetchStart` and `fetchCount` (but kept in the same position).
|
|
this._target.RuntimeAgent.getCollectionEntries(this._objectId, objectGroup, fetchStart, fetchCount, (error, entries) => {
|
|
callback(entries.map((x) => WI.CollectionEntry.fromPayload(x, this._target)));
|
|
});
|
|
}
|
|
|
|
releaseWeakCollectionEntries()
|
|
{
|
|
console.assert(this.isWeakCollection());
|
|
|
|
this._target.RuntimeAgent.releaseObjectGroup(this._weakCollectionObjectGroup());
|
|
}
|
|
|
|
pushNodeToFrontend(callback)
|
|
{
|
|
if (this._objectId && InspectorBackend.hasCommand("DOM.requestNode"))
|
|
WI.domManager.pushNodeToFrontend(this._objectId, callback);
|
|
else
|
|
callback(0);
|
|
}
|
|
|
|
async fetchProperties(propertyNames, resultObject = {})
|
|
{
|
|
let seenPropertyNames = new Set;
|
|
let requestedValues = [];
|
|
for (let propertyName of propertyNames) {
|
|
// Check this here, otherwise things like '{}' would be valid Set keys.
|
|
if (typeof propertyName !== "string" && typeof propertyName !== "number")
|
|
throw new Error(`Tried to get property using key is not a string or number: ${propertyName}`);
|
|
|
|
if (seenPropertyNames.has(propertyName))
|
|
continue;
|
|
|
|
seenPropertyNames.add(propertyName);
|
|
requestedValues.push(this.getProperty(propertyName));
|
|
}
|
|
|
|
// Return primitive values directly, otherwise return a WI.RemoteObject instance.
|
|
function maybeUnwrapValue(remoteObject) {
|
|
return remoteObject.hasValue() ? remoteObject.value : remoteObject;
|
|
}
|
|
|
|
// Request property values one by one, since returning an array of property
|
|
// values would then be subject to arbitrary object preview size limits.
|
|
let fetchedKeys = Array.from(seenPropertyNames);
|
|
let fetchedValues = await Promise.all(requestedValues);
|
|
for (let i = 0; i < fetchedKeys.length; ++i)
|
|
resultObject[fetchedKeys[i]] = maybeUnwrapValue(fetchedValues[i]);
|
|
|
|
return resultObject;
|
|
}
|
|
|
|
getProperty(propertyName, callback = null)
|
|
{
|
|
function inspectedPage_object_getProperty(property) {
|
|
if (typeof property !== "string" && typeof property !== "number")
|
|
throw new Error(`Tried to get property using key is not a string or number: ${property}`);
|
|
|
|
return this[property];
|
|
}
|
|
|
|
if (callback && typeof callback === "function")
|
|
this.callFunction(inspectedPage_object_getProperty, [propertyName], true, callback);
|
|
else
|
|
return this.callFunction(inspectedPage_object_getProperty, [propertyName], true);
|
|
}
|
|
|
|
callFunction(functionDeclaration, args, generatePreview, callback = null)
|
|
{
|
|
let translateResult = (result) => result ? WI.RemoteObject.fromPayload(result, this._target) : null;
|
|
|
|
if (args)
|
|
args = args.map(WI.RemoteObject.createCallArgument);
|
|
|
|
if (callback && typeof callback === "function") {
|
|
this._target.RuntimeAgent.callFunctionOn(this._objectId, appendWebInspectorSourceURL(functionDeclaration.toString()), args, true, undefined, !!generatePreview, (error, result, wasThrown) => {
|
|
callback(error, translateResult(result), wasThrown);
|
|
});
|
|
} else {
|
|
// Protocol errors and results that were thrown should cause promise rejection with the same.
|
|
return this._target.RuntimeAgent.callFunctionOn(this._objectId, appendWebInspectorSourceURL(functionDeclaration.toString()), args, true, undefined, !!generatePreview)
|
|
.then(({result, wasThrown}) => {
|
|
result = translateResult(result);
|
|
if (result && wasThrown)
|
|
return Promise.reject(result);
|
|
return Promise.resolve(result);
|
|
});
|
|
}
|
|
}
|
|
|
|
callFunctionJSON(functionDeclaration, args, callback)
|
|
{
|
|
function mycallback(error, result, wasThrown)
|
|
{
|
|
callback((error || wasThrown) ? null : result.value);
|
|
}
|
|
|
|
this._target.RuntimeAgent.callFunctionOn(this._objectId, appendWebInspectorSourceURL(functionDeclaration.toString()), args, true, true, mycallback);
|
|
}
|
|
|
|
invokeGetter(getterRemoteObject, callback)
|
|
{
|
|
console.assert(getterRemoteObject instanceof WI.RemoteObject);
|
|
|
|
function backendInvokeGetter(getter)
|
|
{
|
|
return getter ? getter.call(this) : undefined;
|
|
}
|
|
|
|
this.callFunction(backendInvokeGetter, [getterRemoteObject], true, callback);
|
|
}
|
|
|
|
getOwnPropertyDescriptor(propertyName, callback)
|
|
{
|
|
function backendGetOwnPropertyDescriptor(propertyName)
|
|
{
|
|
return this[propertyName];
|
|
}
|
|
|
|
function wrappedCallback(error, result, wasThrown)
|
|
{
|
|
if (error || wasThrown || !(result instanceof WI.RemoteObject)) {
|
|
callback(null);
|
|
return;
|
|
}
|
|
|
|
var fakeDescriptor = {name: propertyName, value: result, writable: true, configurable: true};
|
|
var fakePropertyDescriptor = new WI.PropertyDescriptor(fakeDescriptor, null, true, false, false, false);
|
|
callback(fakePropertyDescriptor);
|
|
}
|
|
|
|
// FIXME: Implement a real RuntimeAgent.getOwnPropertyDescriptor?
|
|
this.callFunction(backendGetOwnPropertyDescriptor, [propertyName], false, wrappedCallback.bind(this));
|
|
}
|
|
|
|
release()
|
|
{
|
|
if (this._objectId && !this._isFakeObject())
|
|
this._target.RuntimeAgent.releaseObject(this._objectId);
|
|
}
|
|
|
|
arrayLength()
|
|
{
|
|
if (this._subtype !== "array")
|
|
return 0;
|
|
|
|
var matches = this._description.match(/\[([0-9]+)\]/);
|
|
if (!matches)
|
|
return 0;
|
|
|
|
return parseInt(matches[1], 10);
|
|
}
|
|
|
|
asCallArgument()
|
|
{
|
|
return WI.RemoteObject.createCallArgument(this);
|
|
}
|
|
|
|
findFunctionSourceCodeLocation()
|
|
{
|
|
var result = new WI.WrappedPromise;
|
|
|
|
if (!this._isFunction() || !this._objectId) {
|
|
result.resolve(WI.RemoteObject.SourceCodeLocationPromise.MissingObjectId);
|
|
return result.promise;
|
|
}
|
|
|
|
this._target.DebuggerAgent.getFunctionDetails(this._objectId, (error, response) => {
|
|
if (error) {
|
|
result.resolve(WI.RemoteObject.SourceCodeLocationPromise.NoSourceFound);
|
|
return;
|
|
}
|
|
|
|
var location = response.location;
|
|
var sourceCode = WI.debuggerManager.scriptForIdentifier(location.scriptId, this._target);
|
|
|
|
if (!sourceCode || (!WI.settings.engineeringShowInternalScripts.value && isWebKitInternalScript(sourceCode.sourceURL))) {
|
|
result.resolve(WI.RemoteObject.SourceCodeLocationPromise.NoSourceFound);
|
|
return;
|
|
}
|
|
|
|
var sourceCodeLocation = sourceCode.createSourceCodeLocation(location.lineNumber, location.columnNumber || 0);
|
|
result.resolve(sourceCodeLocation);
|
|
});
|
|
|
|
return result.promise;
|
|
}
|
|
|
|
// Private
|
|
|
|
_isFakeObject()
|
|
{
|
|
return this._objectId === WI.RemoteObject.FakeRemoteObjectId;
|
|
}
|
|
|
|
_isSymbol()
|
|
{
|
|
return this._type === "symbol";
|
|
}
|
|
|
|
_isFunction()
|
|
{
|
|
return this._type === "function";
|
|
}
|
|
|
|
_weakCollectionObjectGroup()
|
|
{
|
|
return JSON.stringify(this._objectId) + "-" + this._subtype;
|
|
}
|
|
|
|
_getProperties(callback, {ownProperties, fetchStart, fetchCount, generatePreview} = {})
|
|
{
|
|
// COMPATIBILITY (iOS 13): `result` was renamed to `properties` (but kept in the same position).
|
|
this._target.RuntimeAgent.getProperties.invoke({
|
|
objectId: this._objectId,
|
|
ownProperties,
|
|
fetchStart,
|
|
fetchCount,
|
|
generatePreview,
|
|
}, callback);
|
|
}
|
|
|
|
_getDisplayableProperties(callback, {fetchStart, fetchCount, generatePreview} = {})
|
|
{
|
|
console.assert(this._target.hasCommand("Runtime.getDisplayableProperties"));
|
|
|
|
this._target.RuntimeAgent.getDisplayableProperties.invoke({
|
|
objectId: this._objectId,
|
|
fetchStart,
|
|
fetchCount,
|
|
generatePreview,
|
|
}, callback);
|
|
}
|
|
|
|
_getPropertyDescriptorsResolver(callback, error, properties, internalProperties)
|
|
{
|
|
if (error) {
|
|
callback(null);
|
|
return;
|
|
}
|
|
|
|
let descriptors = properties.map((payload) => WI.PropertyDescriptor.fromPayload(payload, false, this._target));
|
|
|
|
if (internalProperties) {
|
|
for (let payload of internalProperties)
|
|
descriptors.push(WI.PropertyDescriptor.fromPayload(payload, true, this._target));
|
|
}
|
|
|
|
callback(descriptors);
|
|
}
|
|
};
|
|
|
|
WI.RemoteObject.FakeRemoteObjectId = "fake-remote-object";
|
|
|
|
WI.RemoteObject.SourceCodeLocationPromise = {
|
|
NoSourceFound: "remote-object-source-code-location-promise-no-source-found",
|
|
MissingObjectId: "remote-object-source-code-location-promise-missing-object-id"
|
|
};
|