Initial work to support serializing proxies

This commit is contained in:
Cyrus Najmabadi 2019-10-10 11:29:02 -07:00
parent 97803a6591
commit e99cdc07a4
7 changed files with 141 additions and 15 deletions

View file

@ -20,6 +20,7 @@ build_package::
./node_modules/.bin/tsc
cp tests/runtime/jsClosureCases_8.js bin/tests/runtime
cp tests/runtime/jsClosureCases_10_4.js bin/tests/runtime
cp tests/runtime/jsClosureCases_11.js bin/tests/runtime
cp README.md ../../LICENSE package.json ./dist/* bin/
node ../../scripts/reversion.js bin/package.json ${VERSION}
node ../../scripts/reversion.js bin/version.js ${VERSION}
@ -44,7 +45,7 @@ install_plugin::
install:: install_package install_plugin
istanbul_tests::
istanbul test --print none _mocha -- --timeout 15000 'bin/tests/**/*.spec.js'
istanbul test --print none _mocha -- --timeout 15000 'bin/tests/**/closureLoader.spec.js'
istanbul report text-summary
istanbul report text

View file

@ -345,8 +345,19 @@ async function analyzeFunctionInfoAsync(
// logInfo = logInfo || func.name === "addHandler";
const { file, line, column } = await v8.getFunctionLocationAsync(func);
const functionString = func.toString();
// Some libraries may have wrapped a function up in a proxy (for example 'sequelize' does this).
// That's problematic if they then also provide their own static toString. For example:
//
// return new Proxy(class C { static toString() { ... } }, {})
//
// In order to support this, we need to first unwrap the proxy to get at the underlying function
// that has been wrapped. Then, we also need to ensure that we call Function's toString method,
// not any particular static derivation this function may have provided itself. We need the
// original user code code for this
const unwrappedFunction = await v8.unwrapIfProxyAsync(func);
const functionString = Function.prototype.toString.call(unwrappedFunction);
const { file, line, column } = await v8.getFunctionLocationAsync(unwrappedFunction);
const frame = { functionLocation: { func, file, line, column, functionString, isArrowFunction: false } };
context.frames.push(frame);

View file

@ -49,3 +49,9 @@ export const lookupCapturedVariableValueAsync = versionSpecificV8Module.lookupCa
* defined. Returns { "", 0, 0 } if the location cannot be found or if the given function has no Script.
*/
export const getFunctionLocationAsync = versionSpecificV8Module.getFunctionLocationAsync;
/**
* Given a function that is possibly a proxy wrapping a function, just return the true function
* being wrapped.
*/
export const unwrapIfProxyAsync = versionSpecificV8Module.unwrapIfProxyAsync;

View file

@ -133,6 +133,10 @@ export async function lookupCapturedVariableValueAsync(
return undefined;
}
export async function unwrapIfProxyAsync(func: Function): Promise<Function> {
return func;
}
// The last two intrinsics are `GetFunctionScopeCount` and `GetFunctionScopeDetails`.
// The former function returns the number of scopes in a given function's scope chain, while
// the latter function returns the i'th entry in a function's scope chain, given a function and

View file

@ -93,6 +93,47 @@ export async function lookupCapturedVariableValueAsync(
return undefined;
}
export async function unwrapIfProxyAsync(func: Function): Promise<Function> {
console.log("trying to unwrap proxy: " + func.toString());
console.log("util.types: " + (util.types !== undefined));
console.log("util.types.isProxy: " + (util.types.isProxy !== undefined));
console.log("util.types.isProxy(f): " + util.types.isProxy(func));
while (util.types && util.types.isProxy && util.types.isProxy(func)) {
console.log("Got proxy");
// First, find the runtime's internal id for this function.
const functionId = await getRuntimeIdForFunctionAsync(func);
// Now, query for the internal properties the runtime sets up for it.
const { internalProperties } = await runtimeGetPropertiesAsync(functionId, /*ownProperties:*/ false);
const target = internalProperties.find(p => p.name === "[[Target]]");
if (!target) {
throw new Error("Could not find [[Target]] property on proxied function");
}
if (!target.value) {
throw new Error("[[Target]] property did not have [value]");
}
if (!target.value.objectId) {
throw new Error("[[Target]].value have objectId");
}
const result = await getValueForObjectId(target.value.objectId);
if (!result) {
throw new Error("Could not retrieve target function of proxy.");
}
if (!(result instanceof Function)) {
throw new Error("Target of proxy was not a Function.");
}
func = result;
}
return func;
}
// We want to call util.promisify on inspector.Session.post. However, due to all the overloads of
// that method, promisify gets confused. To prevent this, we cast our session object down to an
// interface containing only the single overload we care about.
@ -161,8 +202,8 @@ async function getRuntimeIdForFunctionAsync(func: Function): Promise<inspector.R
}
async function runtimeGetPropertiesAsync(
objectId: inspector.Runtime.RemoteObjectId,
ownProperties: boolean | undefined) {
objectId: inspector.Runtime.RemoteObjectId,
ownProperties: boolean | undefined) {
const session = <GetPropertiesSession>await v8Hooks.getSessionAsync();
const post = util.promisify(session.post);
@ -210,11 +251,11 @@ async function getValueForObjectId(objectId: inspector.Runtime.RemoteObjectId):
// support typesafe '.call' calls.
const retType = <inspector.Runtime.CallFunctionOnReturnType>await post.call(
session, "Runtime.callFunctionOn", {
objectId,
functionDeclaration: `function () {
objectId,
functionDeclaration: `function () {
global.__inflightCalls["${tableId}"] = this;
}`,
});
});
if (retType.exceptionDetails) {
throw new Error(`Error calling "Runtime.callFunction(${objectId})": `

View file

@ -0,0 +1,56 @@
"use strict";
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
const cases = [];
{
class C {
toString() { return "x"; }
}
const proxy = new Proxy(C, {
apply(Target, thisArg, args) {
return new Target(...args);
},
construct(Target, args) {
return new Target(...args);
},
get(target, p) {
return target[p];
}
})
cases.push({
title: "Proxied class",
// tslint:disable-next-line
func: function () { return proxy; },
expectText: ` `,
});
}
{
class C {
static toString() { return "y"; }
}
const proxy = new Proxy(C, {
apply(Target, thisArg, args) {
return new Target(...args);
},
construct(Target, args) {
return new Target(...args);
},
get(target, p) {
return target[p];
}
})
cases.push({
title: "Proxied class with static toString",
// tslint:disable-next-line
func: function () { return proxy; },
expectText: ` `,
});
}
module.exports.cases = cases;

View file

@ -39,7 +39,7 @@ export const exportedValue = 42;
// This group of tests ensure that we serialize closures properly.
describe("closure", () => {
const cases: ClosureCase[] = [];
let cases: ClosureCase[] = [];
cases.push({
title: "Empty function closure",
@ -6523,17 +6523,24 @@ return function () { console.log(regex); foo(); };
});
}
cases = [];
// Run a bunch of direct checks on async js functions if we're in node 8 or above.
// We can't do this inline as node6 doesn't understand 'async functions'. And we
// can't do this in TS as TS will convert the async-function to be a normal non-async
// function.
if (semver.gte(process.version, "8.0.0")) {
const jsCases = require("./jsClosureCases_8");
cases.push(...jsCases.cases);
}
// if (semver.gte(process.version, "8.0.0")) {
// const jsCases = require("./jsClosureCases_8");
// cases.push(...jsCases.cases);
// }
if (semver.gte(process.version, "10.4.0")) {
const jsCases = require("./jsClosureCases_10_4");
// if (semver.gte(process.version, "10.4.0")) {
// const jsCases = require("./jsClosureCases_10_4");
// cases.push(...jsCases.cases);
// }
if (semver.gte(process.version, "11.0.0")) {
const jsCases = require("./jsClosureCases_11");
cases.push(...jsCases.cases);
}