Bug 1392760 - Avoid exponential behavior when inspecting nested proxies. r=nchevobbe

Differential Revision: https://phabricator.services.mozilla.com/D28214

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Oriol Brufau 2019-05-14 06:46:30 +00:00
parent d7daa3c130
commit 4be85f2295
25 changed files with 320 additions and 86 deletions

View File

@ -54,7 +54,8 @@ function mount(props, { initialState } = {}) {
const client = {
createObjectClient: grip =>
ObjectClient(grip, {
getPrototype: () => Promise.resolve(protoStub)
getPrototype: () => Promise.resolve(protoStub),
getProxySlots: () => Promise.resolve(gripRepStubs.get("testProxySlots"))
}),
createLongStringClient: grip =>

View File

@ -6,7 +6,9 @@
const { mountObjectInspector } = require("../test-utils");
const { MODE } = require("../../../reps/constants");
const stub = require("../../../reps/stubs/grip").get("testProxy");
const gripStubs = require("../../../reps/stubs/grip");
const stub = gripStubs.get("testProxy");
const proxySlots = gripStubs.get("testProxySlots");
const { formatObjectInspector } = require("../test-utils");
const ObjectClient = require("../__mocks__/object-client");
@ -32,11 +34,17 @@ function getEnumPropertiesMock() {
}));
}
function getProxySlotsMock() {
return jest.fn(() => proxySlots);
}
function mount(props, { initialState } = {}) {
const enumProperties = getEnumPropertiesMock();
const getProxySlots = getProxySlotsMock();
const client = {
createObjectClient: grip => ObjectClient(grip, { enumProperties })
createObjectClient: grip =>
ObjectClient(grip, { enumProperties, getProxySlots })
};
const obj = mountObjectInspector({
@ -45,19 +53,19 @@ function mount(props, { initialState } = {}) {
initialState
});
return { ...obj, enumProperties };
return { ...obj, enumProperties, getProxySlots };
}
describe("ObjectInspector - Proxy", () => {
it("renders Proxy as expected", () => {
const { wrapper, enumProperties } = mount(
const { wrapper, enumProperties, getProxySlots } = mount(
{},
{
initialState: {
objectInspector: {
// Have the prototype already loaded so the component does not call
// enumProperties for the root's properties.
loadedProperties: new Map([["root", { prototype: {} }]]),
loadedProperties: new Map([["root", proxySlots]]),
evaluations: new Map()
}
}
@ -68,6 +76,9 @@ describe("ObjectInspector - Proxy", () => {
// enumProperties should not have been called.
expect(enumProperties.mock.calls).toHaveLength(0);
// getProxySlots should not have been called.
expect(getProxySlots.mock.calls).toHaveLength(0);
});
it("calls enumProperties on <target> and <handler> clicks", () => {
@ -78,7 +89,7 @@ describe("ObjectInspector - Proxy", () => {
objectInspector: {
// Have the prototype already loaded so the component does not call
// enumProperties for the root's properties.
loadedProperties: new Map([["root", { prototype: {} }]]),
loadedProperties: new Map([["root", proxySlots]]),
evaluations: new Map()
}
}

View File

@ -75,14 +75,15 @@ describe("getChildren", () => {
});
it("returns the expected nodes for Proxy", () => {
const nodes = getChildren({
item: createNode({
name: "root",
path: "rootpath",
contents: { value: gripStubs.get("testProxy") }
})
const proxyNode = createNode({
name: "root",
path: "rootpath",
contents: { value: gripStubs.get("testProxy") }
});
const loadedProperties = new Map([
[proxyNode.path, gripStubs.get("testProxySlots")]
]);
const nodes = getChildren({ item: proxyNode, loadedProperties });
const names = nodes.map(n => n.name);
const paths = nodes.map(n => n.path.toString());
@ -248,7 +249,7 @@ describe("getChildren", () => {
const children = getChildren({
cachedNodes,
item: node,
loadedProperties: new Map([[node.path, { prototype: {} }]])
loadedProperties: new Map([[node.path, gripStubs.get("testProxySlots")]])
});
expect(cachedNodes.get(node.path)).toBe(children);
});

View File

@ -210,7 +210,10 @@ describe("shouldLoadItemIndexedProperties", () => {
value: gripStubs.get("testProxy")
}
});
const [targetNode] = getChildren({ item: proxyNode });
const loadedProperties = new Map([
[proxyNode.path, gripStubs.get("testProxySlots")]
]);
const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
// Make sure we have the target node.
expect(targetNode.name).toBe("<target>");
expect(shouldLoadItemIndexedProperties(targetNode)).toBeTruthy();

View File

@ -173,7 +173,10 @@ describe("shouldLoadItemNonIndexedProperties", () => {
value: gripStubs.get("testProxy")
}
});
const [targetNode] = getChildren({ item: proxyNode });
const loadedProperties = new Map([
[proxyNode.path, gripStubs.get("testProxySlots")]
]);
const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
// Make sure we have the target node.
expect(targetNode.name).toBe("<target>");
expect(shouldLoadItemNonIndexedProperties(targetNode)).toBeTruthy();

View File

@ -152,14 +152,14 @@ describe("shouldLoadItemPrototype", () => {
expect(shouldLoadItemPrototype(node)).toBeFalsy();
});
it("returns true for a Proxy node", () => {
it("returns false for a Proxy node", () => {
const node = createNode({
name: "root",
contents: {
value: gripStubs.get("testProxy")
}
});
expect(shouldLoadItemPrototype(node)).toBeTruthy();
expect(shouldLoadItemPrototype(node)).toBeFalsy();
});
it("returns true for a Proxy target node", () => {
@ -169,7 +169,10 @@ describe("shouldLoadItemPrototype", () => {
value: gripStubs.get("testProxy")
}
});
const [targetNode] = getChildren({ item: proxyNode });
const loadedProperties = new Map([
[proxyNode.path, gripStubs.get("testProxySlots")]
]);
const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
// Make sure we have the target node.
expect(targetNode.name).toBe("<target>");
expect(shouldLoadItemPrototype(targetNode)).toBeTruthy();

View File

@ -169,7 +169,10 @@ describe("shouldLoadItemSymbols", () => {
value: gripStubs.get("testProxy")
}
});
const [targetNode] = getChildren({ item: proxyNode });
const loadedProperties = new Map([
[proxyNode.path, gripStubs.get("testProxySlots")]
]);
const [targetNode] = getChildren({ item: proxyNode, loadedProperties });
// Make sure we have the target node.
expect(targetNode.name).toBe("<target>");
expect(shouldLoadItemSymbols(targetNode)).toBeTruthy();

View File

@ -73,7 +73,8 @@ export type ObjectClient = {
enumEntries: () => Promise<PropertiesIterator>,
enumProperties: (options: Object) => Promise<PropertiesIterator>,
enumSymbols: () => Promise<PropertiesIterator>,
getPrototype: () => Promise<{ prototype: Object }>
getPrototype: () => Promise<{ prototype: Object }>,
getProxySlots: () => Promise<{ proxyTarget: Object, proxyHandler: Object }>
};
export type LongStringClient = {

View File

@ -117,6 +117,12 @@ async function getFullText(
});
}
async function getProxySlots(
objectClient: ObjectClient
): Promise<{ proxyTarget?: Object, proxyHandler?: Object }> {
return objectClient.getProxySlots();
}
function iteratorSlice(
iterator: PropertiesIterator,
start: ?number,
@ -137,5 +143,6 @@ module.exports = {
enumNonIndexedProperties,
enumSymbols,
getPrototype,
getFullText
getFullText,
getProxySlots
};

View File

@ -8,7 +8,8 @@ const {
enumNonIndexedProperties,
getPrototype,
enumSymbols,
getFullText
getFullText,
getProxySlots
} = require("./client");
const {
@ -77,6 +78,10 @@ function loadItemProperties(
promises.push(getFullText(createLongStringClient(value), item));
}
if (shouldLoadItemProxySlots(item, loadedProperties)) {
promises.push(getProxySlots(getObjectClient()));
}
return Promise.all(promises).then(mergeResponses);
}
@ -99,6 +104,11 @@ function mergeResponses(responses: Array<Object>): Object {
if (response.fullText) {
data.fullText = response.fullText;
}
if (response.proxyTarget && response.proxyHandler) {
data.proxyTarget = response.proxyTarget;
data.proxyHandler = response.proxyHandler;
}
}
return data;
@ -173,7 +183,8 @@ function shouldLoadItemPrototype(
!nodeIsDefaultProperties(item) &&
!nodeHasAccessors(item) &&
!nodeIsPrimitive(item) &&
!nodeIsLongString(item)
!nodeIsLongString(item) &&
!nodeIsProxy(item)
);
}
@ -204,6 +215,13 @@ function shouldLoadItemFullText(
return !loadedProperties.has(item.path) && nodeIsLongString(item);
}
function shouldLoadItemProxySlots(
item: Node,
loadedProperties: LoadedProperties = new Map()
): boolean {
return !loadedProperties.has(item.path) && nodeIsProxy(item);
}
module.exports = {
loadItemProperties,
mergeResponses,
@ -212,5 +230,6 @@ module.exports = {
shouldLoadItemNonIndexedProperties,
shouldLoadItemPrototype,
shouldLoadItemSymbols,
shouldLoadItemFullText
shouldLoadItemFullText,
shouldLoadItemProxySlots
};

View File

@ -346,8 +346,11 @@ function makeNodesForPromiseProperties(item: Node): Array<Node> {
return properties;
}
function makeNodesForProxyProperties(item: Node): Array<Node> {
const { proxyHandler, proxyTarget } = getValue(item);
function makeNodesForProxyProperties(
loadedProps: GripProperties,
item: Node
): Array<Node> {
const { proxyHandler, proxyTarget } = loadedProps;
return [
createNode({
@ -792,8 +795,8 @@ function getChildren(options: {
return addToCache(makeNodesForMapEntry(item));
}
if (nodeIsProxy(item)) {
return addToCache(makeNodesForProxyProperties(item));
if (nodeIsProxy(item) && hasLoadedProps) {
return addToCache(makeNodesForProxyProperties(loadedProps, item));
}
if (nodeIsLongString(item) && hasLoadedProps) {

View File

@ -303,22 +303,6 @@ stubs.set("testProxy", {
type: "object",
actor: "server1.conn1.child1/obj47",
class: "Proxy",
proxyTarget: {
type: "object",
actor: "server1.conn1.child1/obj48",
class: "Object",
ownPropertyLength: 1
},
proxyHandler: {
type: "object",
actor: "server1.conn1.child1/obj49",
class: "Array",
ownPropertyLength: 4,
preview: {
kind: "ArrayLike",
length: 3
}
},
preview: {
kind: "Object",
ownProperties: {
@ -346,6 +330,24 @@ stubs.set("testProxy", {
ownPropertiesLength: 2
}
});
stubs.set("testProxySlots", {
proxyTarget: {
type: "object",
actor: "server1.conn1.child1/obj48",
class: "Object",
ownPropertyLength: 1
},
proxyHandler: {
type: "object",
actor: "server1.conn1.child1/obj49",
class: "Array",
ownPropertyLength: 4,
preview: {
kind: "ArrayLike",
length: 3
}
}
});
stubs.set("testArrayBuffer", {
type: "object",
actor: "server1.conn1.child1/obj170",

View File

@ -124,7 +124,8 @@ describe("Grip - Proxy", () => {
it("renders as expected", () => {
const renderRep = props => shallowRenderRep(object, props);
const handlerLength = getGripLengthBubbleText(object.proxyHandler, {
const handler = object.preview.ownProperties["<handler>"].value;
const handlerLength = getGripLengthBubbleText(handler, {
mode: MODE.TINY
});
const out = `Proxy { <target>: {…}, <handler>: ${handlerLength} […] }`;

View File

@ -1712,8 +1712,8 @@ function makeNodesForPromiseProperties(item) {
return properties;
}
function makeNodesForProxyProperties(item) {
const { proxyHandler, proxyTarget } = getValue(item);
function makeNodesForProxyProperties(loadedProps, item) {
const { proxyHandler, proxyTarget } = loadedProps;
return [createNode({
parent: item,
@ -2106,8 +2106,8 @@ function getChildren(options) {
return addToCache(makeNodesForMapEntry(item));
}
if (nodeIsProxy(item)) {
return addToCache(makeNodesForProxyProperties(item));
if (nodeIsProxy(item) && hasLoadedProps) {
return addToCache(makeNodesForProxyProperties(loadedProps, item));
}
if (nodeIsLongString(item) && hasLoadedProps) {
@ -3481,7 +3481,8 @@ const {
enumNonIndexedProperties,
getPrototype,
enumSymbols,
getFullText
getFullText,
getProxySlots
} = __webpack_require__(197);
const {
@ -3535,6 +3536,10 @@ function loadItemProperties(item, createObjectClient, createLongStringClient, lo
promises.push(getFullText(createLongStringClient(value), item));
}
if (shouldLoadItemProxySlots(item, loadedProperties)) {
promises.push(getProxySlots(getObjectClient()));
}
return Promise.all(promises).then(mergeResponses);
}
@ -3557,6 +3562,11 @@ function mergeResponses(responses) {
if (response.fullText) {
data.fullText = response.fullText;
}
if (response.proxyTarget && response.proxyHandler) {
data.proxyTarget = response.proxyTarget;
data.proxyHandler = response.proxyHandler;
}
}
return data;
@ -3590,7 +3600,7 @@ function shouldLoadItemEntries(item, loadedProperties = new Map()) {
function shouldLoadItemPrototype(item, loadedProperties = new Map()) {
const value = getValue(item);
return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item);
return value && !loadedProperties.has(item.path) && !nodeIsBucket(item) && !nodeIsMapEntry(item) && !nodeIsEntries(item) && !nodeIsDefaultProperties(item) && !nodeHasAccessors(item) && !nodeIsPrimitive(item) && !nodeIsLongString(item) && !nodeIsProxy(item);
}
function shouldLoadItemSymbols(item, loadedProperties = new Map()) {
@ -3603,6 +3613,10 @@ function shouldLoadItemFullText(item, loadedProperties = new Map()) {
return !loadedProperties.has(item.path) && nodeIsLongString(item);
}
function shouldLoadItemProxySlots(item, loadedProperties = new Map()) {
return !loadedProperties.has(item.path) && nodeIsProxy(item);
}
module.exports = {
loadItemProperties,
mergeResponses,
@ -3611,7 +3625,8 @@ module.exports = {
shouldLoadItemNonIndexedProperties,
shouldLoadItemPrototype,
shouldLoadItemSymbols,
shouldLoadItemFullText
shouldLoadItemFullText,
shouldLoadItemProxySlots
};
/***/ }),
@ -3706,6 +3721,10 @@ async function getFullText(longStringClient, item) {
});
}
async function getProxySlots(objectClient) {
return objectClient.getProxySlots();
}
function iteratorSlice(iterator, start, end) {
start = start || 0;
const count = end ? end - start + 1 : iterator.count;
@ -3722,7 +3741,8 @@ module.exports = {
enumNonIndexedProperties,
enumSymbols,
getPrototype,
getFullText
getFullText,
getProxySlots
};
/***/ }),

View File

@ -318,16 +318,18 @@ VariablesViewController.prototype = {
*/
_populateFromObject: function(aTarget, aGrip) {
if (aGrip.class === "Proxy") {
this.addExpander(
aTarget.addItem("<target>", { value: aGrip.proxyTarget }, { internalItem: true }),
aGrip.proxyTarget);
this.addExpander(
aTarget.addItem("<handler>", { value: aGrip.proxyHandler }, { internalItem: true }),
aGrip.proxyHandler);
// Refuse to play the proxy's stupid game and return immediately
// Refuse to play the proxy's stupid game and just expose the target and handler.
const deferred = defer();
deferred.resolve();
const objectClient = this._getObjectClient(aGrip);
objectClient.getProxySlots(aResponse => {
const target = aTarget.addItem("<target>", { value: aResponse.proxyTarget },
{ internalItem: true });
this.addExpander(target, aResponse.proxyTarget);
const handler = aTarget.addItem("<handler>", { value: aResponse.proxyHandler },
{ internalItem: true });
this.addExpander(handler, aResponse.proxyHandler);
deferred.resolve();
});
return deferred.promise;
}

View File

@ -358,6 +358,7 @@ skip-if = true # Bug 1438979
[browser_webconsole_object_inspector_getters_shadowed.js]
[browser_webconsole_object_inspector_key_sorting.js]
[browser_webconsole_object_inspector_local_session_storage.js]
[browser_webconsole_object_inspector_nested_proxy.js]
[browser_webconsole_object_inspector_selected_text.js]
[browser_webconsole_object_inspector_scroll.js]
[browser_webconsole_object_inspector_while_debugging_and_inspecting.js]

View File

@ -0,0 +1,50 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Check evaluating and expanding getters in the console.
const TEST_URI = "data:text/html;charset=utf8,"
+ "<h1>Object Inspector on deeply nested proxies</h1>";
add_task(async function() {
const hud = await openNewTabAndConsole(TEST_URI);
await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
let proxy = new Proxy({}, {});
for (let i = 0; i < 1e5; ++i) {
proxy = new Proxy(proxy, proxy);
}
content.wrappedJSObject.console.log("oi-test", proxy);
});
const node = await waitFor(() => findMessage(hud, "oi-test"));
const oi = node.querySelector(".tree");
const [proxyNode] = getObjectInspectorNodes(oi);
expandObjectInspectorNode(proxyNode);
await waitFor(() => getObjectInspectorNodes(oi).length > 1);
checkChildren(proxyNode, [`<target>`, `<handler>`]);
const targetNode = findObjectInspectorNode(oi, "<target>");
expandObjectInspectorNode(targetNode);
await waitFor(() => getObjectInspectorChildrenNodes(targetNode).length > 0);
checkChildren(targetNode, [`<target>`, `<handler>`]);
const handlerNode = findObjectInspectorNode(oi, "<handler>");
expandObjectInspectorNode(handlerNode);
await waitFor(() => getObjectInspectorChildrenNodes(handlerNode).length > 0);
checkChildren(handlerNode, [`<target>`, `<handler>`]);
});
function checkChildren(node, expectedChildren) {
const children = getObjectInspectorChildrenNodes(node);
is(children.length, expectedChildren.length,
"There is the expected number of children");
children.forEach((child, index) => {
ok(child.textContent.includes(expectedChildren[index]),
`Expected "${expectedChildren[index]}" child`);
});
}

View File

@ -865,6 +865,24 @@ const proto = {
};
},
/**
* Handle a protocol request to get the target and handler internal slots of a proxy.
*/
proxySlots: function() {
// There could be transparent security wrappers, unwrap to check if it's a proxy.
// However, retrieve proxyTarget and proxyHandler from `this.obj` to avoid exposing
// the unwrapped target and handler.
const unwrapped = DevToolsUtils.unwrap(this.obj);
if (!unwrapped || !unwrapped.isProxy) {
return this.throwError("objectNotProxy",
"'proxySlots' request is only valid for grips with a 'Proxy' class.");
}
return {
proxyTarget: this.hooks.createValueGrip(this.obj.proxyTarget),
proxyHandler: this.hooks.createValueGrip(this.obj.proxyHandler),
};
},
/**
* Release the actor, when it isn't needed anymore.
* Protocol.js uses this release method to call the destroy method.

View File

@ -288,13 +288,15 @@ const previewers = {
}],
Proxy: [function({obj, hooks}, grip, rawObj) {
// Only preview top-level proxies, avoiding recursion. Otherwise, since both the
// target and handler can also be proxies, we could get an exponential behavior.
if (hooks.getGripDepth() > 1) {
return true;
}
// The `isProxy` getter of the debuggee object only detects proxies without
// security wrappers. If false, the target and handler are not available.
const hasTargetAndHandler = obj.isProxy;
if (hasTargetAndHandler) {
grip.proxyTarget = hooks.createValueGrip(obj.proxyTarget);
grip.proxyHandler = hooks.createValueGrip(obj.proxyHandler);
}
grip.preview = {
kind: "Object",
@ -302,13 +304,11 @@ const previewers = {
ownPropertiesLength: 2 * hasTargetAndHandler,
};
if (hooks.getGripDepth() > 1) {
return true;
}
if (hasTargetAndHandler) {
grip.preview.ownProperties["<target>"] = {value: grip.proxyTarget};
grip.preview.ownProperties["<handler>"] = {value: grip.proxyHandler};
Object.assign(grip.preview.ownProperties, {
"<target>": {value: hooks.createValueGrip(obj.proxyTarget)},
"<handler>": {value: hooks.createValueGrip(obj.proxyHandler)},
});
}
return true;

View File

@ -57,8 +57,12 @@ function test({ threadClient, debuggee }, testOptions) {
// Check the grip of the proxy object.
check_proxy_grip(debuggee, testOptions, proxyGrip);
// Check the prototype and properties of the proxy object.
// Check the target and handler slots of the proxy object.
const proxyClient = threadClient.pauseGrip(proxyGrip);
const proxySlots = await proxyClient.getProxySlots();
check_proxy_slots(debuggee, testOptions, proxyGrip, proxySlots);
// Check the prototype and properties of the proxy object.
const proxyResponse = await proxyClient.getPrototypeAndProperties();
check_properties(testOptions, proxyResponse.ownProperties, true, false);
check_prototype(debuggee, testOptions, proxyResponse.prototype, true, false);
@ -117,20 +121,17 @@ function check_proxy_grip(debuggee, testOptions, grip) {
if (global === debuggee) {
// The proxy has no security wrappers.
strictEqual(grip.class, "Proxy", "The grip has a Proxy class.");
ok(grip.proxyTarget, "There is a [[ProxyTarget]] grip.");
ok(grip.proxyHandler, "There is a [[ProxyHandler]] grip.");
strictEqual(preview.ownPropertiesLength, 2, "The preview has 2 properties.");
const target = preview.ownProperties["<target>"].value;
strictEqual(target, grip.proxyTarget, "<target> contains the [[ProxyTarget]].");
const handler = preview.ownProperties["<handler>"].value;
strictEqual(handler, grip.proxyHandler, "<handler> contains the [[ProxyHandler]].");
const props = preview.ownProperties;
ok(props["<target>"].value, "<target> contains the [[ProxyTarget]].");
ok(props["<handler>"].value, "<handler> contains the [[ProxyHandler]].");
} else if (isOpaque) {
// The proxy has opaque security wrappers.
strictEqual(grip.class, "Opaque", "The grip has an Opaque class.");
strictEqual(grip.ownPropertyLength, 0, "The grip has no properties.");
} else if (!subsumes) {
// The proxy belongs to compartment not subsumed by the debuggee.
strictEqual(grip.class, "Restricted", "The grip has an Restricted class.");
strictEqual(grip.class, "Restricted", "The grip has a Restricted class.");
ok(!("ownPropertyLength" in grip), "The grip doesn't know the number of properties.");
} else if (globalIsInvisible) {
// The proxy belongs to an invisible-to-debugger compartment.
@ -140,14 +141,28 @@ function check_proxy_grip(debuggee, testOptions, grip) {
} else {
// The proxy has non-opaque security wrappers.
strictEqual(grip.class, "Proxy", "The grip has a Proxy class.");
ok(!("proxyTarget" in grip), "There is no [[ProxyTarget]] grip.");
ok(!("proxyHandler" in grip), "There is no [[ProxyHandler]] grip.");
strictEqual(preview.ownPropertiesLength, 0, "The preview has no properties.");
ok(!("<target>" in preview), "The preview has no <target> property.");
ok(!("<handler>" in preview), "The preview has no <handler> property.");
}
}
function check_proxy_slots(debuggee, testOptions, grip, proxySlots) {
const { global } = testOptions;
if (grip.class !== "Proxy") {
strictEqual(proxySlots, undefined, "Slots can only be retrived for Proxy grips.");
} else if (global === debuggee) {
const { proxyTarget, proxyHandler } = proxySlots;
strictEqual(proxyTarget.type, "object", "There is a [[ProxyTarget]] grip.");
strictEqual(proxyHandler.type, "object", "There is a [[ProxyHandler]] grip.");
} else {
const { proxyTarget, proxyHandler } = proxySlots;
strictEqual(proxyTarget.type, "undefined", "There is no [[ProxyTarget]] grip.");
strictEqual(proxyHandler.type, "undefined", "There is no [[ProxyHandler]] grip.");
}
}
function check_properties(testOptions, props, isProxy, createdInDebuggee) {
const { subsumes, globalIsInvisible } = testOptions;
const ownPropertiesLength = Reflect.ownKeys(props).length;

View File

@ -0,0 +1,36 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-disable no-shadow, max-nested-callbacks */
"use strict";
Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
});
add_task(threadClientTest(async ({ threadClient, debuggee, client }) => {
await new Promise(function(resolve) {
threadClient.addOneTimeListener("paused", async function(event, packet) {
const [grip] = packet.frame.arguments;
const objClient = threadClient.pauseGrip(grip);
const {proxyTarget, proxyHandler} = await objClient.getProxySlots();
strictEqual(grip.class, "Proxy", "Its a proxy grip.");
strictEqual(proxyTarget.class, "Proxy", "The target is also a proxy.");
strictEqual(proxyHandler.class, "Proxy", "The handler is also a proxy.");
await threadClient.resume();
resolve();
});
debuggee.eval(function stopMe(arg) {
debugger;
}.toString());
debuggee.eval(`
var proxy = new Proxy({}, {});
for (let i = 0; i < 1e5; ++i)
proxy = new Proxy(proxy, proxy);
stopMe(proxy);
`);
});
}));

View File

@ -170,6 +170,7 @@ skip-if = true # breakpoint sliding is not supported bug 1525685
[test_objectgrips-fn-apply-01.js]
[test_objectgrips-fn-apply-02.js]
[test_objectgrips-fn-apply-03.js]
[test_objectgrips-nested-proxy.js]
[test_promise_state-01.js]
[test_promise_state-02.js]
[test_promise_state-03.js]

View File

@ -359,10 +359,11 @@ DebuggerClient.prototype = {
const deferred = promise.defer();
function listenerJson(resp) {
removeRequestListeners();
resp = safeOnResponse(resp);
if (resp.error) {
deferred.reject(safeOnResponse(resp));
deferred.reject(resp);
} else {
deferred.resolve(safeOnResponse(resp));
deferred.resolve(resp);
}
}
function listenerBulk(resp) {

View File

@ -303,6 +303,29 @@ ObjectClient.prototype = {
return packet;
},
}),
/**
* Request the target and handler internal slots of a proxy.
*/
getProxySlots: DebuggerClient.requester({
type: "proxySlots",
}, {
before: function(packet) {
if (this._grip.class !== "Proxy") {
throw new Error("getProxySlots is only valid for proxy grips.");
}
return packet;
},
after: function(response) {
// Before Firefox 68 (bug 1392760), the proxySlots request didn't exist.
// The proxy target and handler were directly included in the grip.
if (response.error === "unrecognizedPacketType") {
const {proxyTarget, proxyHandler} = this._grip;
return {proxyTarget, proxyHandler};
}
return response;
},
}),
};
module.exports = ObjectClient;

View File

@ -102,6 +102,11 @@ types.addDictType("object.originalSourceLocation", {
functionDisplayName: "string",
});
types.addDictType("object.proxySlots", {
proxyTarget: "object.descriptor",
proxyHandler: "object.descriptor",
});
const objectSpec = generateActorSpec({
typeName: "obj",
@ -198,6 +203,10 @@ const objectSpec = generateActorSpec({
rejectionStack: RetVal("array:object.originalSourceLocation"),
},
},
proxySlots: {
request: {},
response: RetVal("object.proxySlots"),
},
release: { release: true },
scope: {
request: {},