mirror of https://github.com/pulumi/pulumi.git
345 lines
14 KiB
TypeScript
345 lines
14 KiB
TypeScript
// Copyright 2016-2018, Pulumi Corporation.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// This file provides a low-level interface to a few V8 runtime objects. We will use this low-level
|
|
// interface when serializing closures to walk the scope chain and find the value of free variables
|
|
// captured by closures, as well as getting source-level debug information so that we can present
|
|
// high-quality error messages.
|
|
//
|
|
// As a side-effect of importing this file, we must enable the --allow-natives-syntax V8 flag. This
|
|
// is because we are using V8 intrinsics in order to implement this module.
|
|
import * as v8 from "v8";
|
|
v8.setFlagsFromString("--allow-natives-syntax");
|
|
|
|
import * as inspector from "inspector";
|
|
import * as util from "util";
|
|
import * as vm from "vm";
|
|
import * as v8Hooks from "./v8Hooks";
|
|
|
|
/**
|
|
* Given a function, returns the file, line and column number in the file where
|
|
* this function was defined. Returns `{ "", 0, 0 }` if the location cannot be
|
|
* found or if the given function has no script.
|
|
*
|
|
* @internal
|
|
*/
|
|
export async function getFunctionLocationAsync(func: Function) {
|
|
// 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);
|
|
|
|
// There should normally be an internal property called [[FunctionLocation]]:
|
|
// https://chromium.googlesource.com/v8/v8.git/+/3f99afc93c9ba1ba5df19f123b93cc3079893c9b/src/inspector/v8-debugger.cc#793
|
|
const functionLocation = internalProperties.find((p) => p.name === "[[FunctionLocation]]");
|
|
if (!functionLocation || !functionLocation.value || !functionLocation.value.value) {
|
|
return { file: "", line: 0, column: 0 };
|
|
}
|
|
|
|
const value = functionLocation.value.value;
|
|
|
|
// Map from the scriptId the value has to a file-url.
|
|
const file = v8Hooks.getScriptUrl(value.scriptId) || "";
|
|
const line = value.lineNumber || 0;
|
|
const column = value.columnNumber || 0;
|
|
|
|
return { file, line, column };
|
|
}
|
|
|
|
/**
|
|
* Given a function and a free variable name, looks up the value of that free
|
|
* variable in the scope chain of the provided function. If the free variable is
|
|
* not found, `throwOnFailure` indicates whether or not this function should
|
|
* throw or return `undefined`.
|
|
*
|
|
* @param func
|
|
* The function whose scope chain is to be analyzed
|
|
* @param freeVariable
|
|
* The name of the free variable to inspect
|
|
* @param throwOnFailure
|
|
* If true, throws if the free variable can't be found.
|
|
* @returns
|
|
* The value of the free variable. If `throwOnFailure` is false, returns
|
|
* `undefined` if not found.
|
|
*
|
|
* @internal
|
|
*/
|
|
export async function lookupCapturedVariableValueAsync(
|
|
func: Function,
|
|
freeVariable: string,
|
|
throwOnFailure: boolean,
|
|
): Promise<any> {
|
|
// 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);
|
|
|
|
// There should normally be an internal property called [[Scopes]]:
|
|
// https://chromium.googlesource.com/v8/v8.git/+/3f99afc93c9ba1ba5df19f123b93cc3079893c9b/src/inspector/v8-debugger.cc#820
|
|
const scopes = internalProperties.find((p) => p.name === "[[Scopes]]");
|
|
if (!scopes) {
|
|
throw new Error("Could not find [[Scopes]] property");
|
|
}
|
|
|
|
if (!scopes.value) {
|
|
throw new Error("[[Scopes]] property did not have [value]");
|
|
}
|
|
|
|
if (!scopes.value.objectId) {
|
|
throw new Error("[[Scopes]].value have objectId");
|
|
}
|
|
|
|
// This is sneaky, but we can actually map back from the [[Scopes]] object to a real in-memory
|
|
// v8 array-like value. Note: this isn't actually a real array. For example, it cannot be
|
|
// iterated. Nor can any actual methods be called on it. However, we can directly index into
|
|
// it, and we can. Similarly, the 'object' type it optionally points at is not a true JS
|
|
// object. So we can't call things like .hasOwnProperty on it. However, the values pointed to
|
|
// by 'object' are the real in-memory JS objects we are looking for. So we can find and return
|
|
// those successfully to our caller.
|
|
const scopesArray: { object?: Record<string, any> }[] = await getValueForObjectId(scopes.value.objectId);
|
|
|
|
// scopesArray is ordered from innermost to outermost.
|
|
for (let i = 0, n = scopesArray.length; i < n; i++) {
|
|
const scope = scopesArray[i];
|
|
if (scope.object) {
|
|
if (freeVariable in scope.object) {
|
|
const val = scope.object[freeVariable];
|
|
return val;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (throwOnFailure) {
|
|
throw new Error("Unexpected missing variable in closure environment: " + freeVariable);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
// 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.
|
|
type PostSession<TMethod, TParams, TReturn> = {
|
|
post(method: TMethod, params?: TParams, callback?: (err: Error | null, params: TReturn) => void): void;
|
|
};
|
|
|
|
type EvaluationSession = PostSession<
|
|
"Runtime.evaluate",
|
|
inspector.Runtime.EvaluateParameterType,
|
|
inspector.Runtime.EvaluateReturnType
|
|
>;
|
|
type GetPropertiesSession = PostSession<
|
|
"Runtime.getProperties",
|
|
inspector.Runtime.GetPropertiesParameterType,
|
|
inspector.Runtime.GetPropertiesReturnType
|
|
>;
|
|
type CallFunctionSession = PostSession<
|
|
"Runtime.callFunctionOn",
|
|
inspector.Runtime.CallFunctionOnParameterType,
|
|
inspector.Runtime.CallFunctionOnReturnType
|
|
>;
|
|
type ContextSession = {
|
|
post(method: "Runtime.disable" | "Runtime.enable", callback?: (err: Error | null) => void): void;
|
|
once(
|
|
event: "Runtime.executionContextCreated",
|
|
listener: (
|
|
message: inspector.InspectorNotification<inspector.Runtime.ExecutionContextCreatedEventDataType>,
|
|
) => void,
|
|
): void;
|
|
};
|
|
|
|
type InflightContext = {
|
|
contextId: number;
|
|
functions: Record<string, any>;
|
|
currentFunctionId: number;
|
|
calls: Record<string, any>;
|
|
currentCallId: number;
|
|
};
|
|
|
|
// Isolated singleton context accessible from the inspector.
|
|
// Used instead of `global` object to support executions with multiple V8 vm contexts as, e.g., done by Jest.
|
|
let inflightContextCache: Promise<InflightContext> | undefined;
|
|
function inflightContext() {
|
|
if (inflightContextCache) {
|
|
return inflightContextCache;
|
|
}
|
|
inflightContextCache = createContext();
|
|
return inflightContextCache;
|
|
}
|
|
|
|
async function createContext(): Promise<InflightContext> {
|
|
const context: InflightContext = {
|
|
contextId: 0,
|
|
functions: {},
|
|
currentFunctionId: 0,
|
|
calls: {},
|
|
currentCallId: 0,
|
|
};
|
|
const session = <ContextSession>await v8Hooks.getSessionAsync();
|
|
const post = util.promisify(session.post);
|
|
|
|
// Create own context with known context id and functionsContext as `global`
|
|
await post.call(session, "Runtime.enable");
|
|
const contextIdAsync = new Promise<number>((resolve) => {
|
|
session.once("Runtime.executionContextCreated", (event) => {
|
|
resolve(event.params.context.id);
|
|
});
|
|
});
|
|
vm.createContext(context);
|
|
context.contextId = await contextIdAsync;
|
|
await post.call(session, "Runtime.disable");
|
|
|
|
return context;
|
|
}
|
|
|
|
async function getRuntimeIdForFunctionAsync(func: Function): Promise<inspector.Runtime.RemoteObjectId> {
|
|
// In order to get information about an object, we need to put it in a well known location so
|
|
// that we can call Runtime.evaluate and find it. To do this, we use a special map on the
|
|
// 'global' object of a vm context only used for this purpose, and map from a unique-id to that
|
|
// object. We then call Runtime.evaluate with an expression that then points to that unique-id
|
|
// in that global object. The runtime will then find the object and give us back an internal id
|
|
// for it. We can then query for information about the object through that internal id.
|
|
//
|
|
// Note: the reason for the mapping object and the unique-id we create is so that we don't run
|
|
// into any issues when being called asynchronously. We don't want to place the object in a
|
|
// location that might be overwritten by another call while we're asynchronously waiting for our
|
|
// original call to complete.
|
|
|
|
const session = <EvaluationSession>await v8Hooks.getSessionAsync();
|
|
const post = util.promisify(session.post);
|
|
|
|
// Place the function in a unique location
|
|
const context = await inflightContext();
|
|
const currentFunctionName = "id" + context.currentFunctionId++;
|
|
context.functions[currentFunctionName] = func;
|
|
const contextId = context.contextId;
|
|
const expression = `functions.${currentFunctionName}`;
|
|
|
|
try {
|
|
const retType = await post.call(session, "Runtime.evaluate", { contextId, expression });
|
|
|
|
if (retType.exceptionDetails) {
|
|
throw new Error(
|
|
`Error calling "Runtime.evaluate(${expression})" on context ${contextId}: ` +
|
|
retType.exceptionDetails.text,
|
|
);
|
|
}
|
|
|
|
const remoteObject = retType.result;
|
|
if (remoteObject.type !== "function") {
|
|
throw new Error("Remote object was not 'function': " + JSON.stringify(remoteObject));
|
|
}
|
|
|
|
if (!remoteObject.objectId) {
|
|
throw new Error("Remote function does not have 'objectId': " + JSON.stringify(remoteObject));
|
|
}
|
|
|
|
return remoteObject.objectId;
|
|
} finally {
|
|
delete context.functions[currentFunctionName];
|
|
}
|
|
}
|
|
|
|
async function runtimeGetPropertiesAsync(
|
|
objectId: inspector.Runtime.RemoteObjectId,
|
|
ownProperties: boolean | undefined,
|
|
) {
|
|
const session = <GetPropertiesSession>await v8Hooks.getSessionAsync();
|
|
const post = util.promisify(session.post);
|
|
|
|
// This cast will become unnecessary when we move to TS 3.1.6 or above. In that version they
|
|
// support typesafe '.call' calls.
|
|
const retType = <inspector.Runtime.GetPropertiesReturnType>(
|
|
await post.call(session, "Runtime.getProperties", { objectId, ownProperties })
|
|
);
|
|
|
|
if (retType.exceptionDetails) {
|
|
throw new Error(
|
|
`Error calling "Runtime.getProperties(${objectId}, ${ownProperties})": ` + retType.exceptionDetails.text,
|
|
);
|
|
}
|
|
|
|
return { internalProperties: retType.internalProperties || [], properties: retType.result };
|
|
}
|
|
|
|
async function getValueForObjectId(objectId: inspector.Runtime.RemoteObjectId): Promise<any> {
|
|
// In order to get the raw JS value for the *remote wrapper* of the [[Scopes]] array, we use
|
|
// Runtime.callFunctionOn on it passing in a fresh function-declaration. The Node runtime will
|
|
// then compile that function, invoking it with the 'real' underlying scopes-array value in
|
|
// memory as the bound 'this' value. Inside that function declaration, we can then access
|
|
// 'this' and assign it to a unique-id in a well known mapping table we have set up. As above,
|
|
// the unique-id is to prevent any issues with multiple in-flight asynchronous calls.
|
|
|
|
const session = <CallFunctionSession>await v8Hooks.getSessionAsync();
|
|
const post = util.promisify(session.post);
|
|
const context = await inflightContext();
|
|
|
|
// Get an id for an unused location in the global table.
|
|
const tableId = "id" + context.currentCallId++;
|
|
|
|
// Now, ask the runtime to call a fictitious method on the scopes-array object. When it
|
|
// does, it will get the actual underlying value for the scopes array and bind it to the
|
|
// 'this' value inside the function. Inside the function we then just grab 'this' and
|
|
// stash it in our global table. After this completes, we'll then have access to it.
|
|
|
|
// This cast will become unnecessary when we move to TS 3.1.6 or above. In that version they
|
|
// support typesafe '.call' calls.
|
|
const retType = <inspector.Runtime.CallFunctionOnReturnType>await post.call(session, "Runtime.callFunctionOn", {
|
|
objectId,
|
|
functionDeclaration: `function () {
|
|
calls["${tableId}"] = this;
|
|
}`,
|
|
});
|
|
|
|
if (retType.exceptionDetails) {
|
|
throw new Error(`Error calling "Runtime.callFunction(${objectId})": ` + retType.exceptionDetails.text);
|
|
}
|
|
|
|
if (!context.calls.hasOwnProperty(tableId)) {
|
|
throw new Error(`Value was not stored into table after calling "Runtime.callFunctionOn(${objectId})"`);
|
|
}
|
|
|
|
// Extract value and clear our table entry.
|
|
const val = context.calls[tableId];
|
|
delete context.calls[tableId];
|
|
|
|
return val;
|
|
}
|
|
|
|
export async function getBoundFunction(
|
|
func: Function,
|
|
): Promise<{ targetFunctionText: string; boundThisValue: any; boundArgsValues: any[] }> {
|
|
const functionId = await getRuntimeIdForFunctionAsync(func);
|
|
const { internalProperties } = await runtimeGetPropertiesAsync(functionId, /*ownProperties:*/ false);
|
|
|
|
const desc = internalProperties.find((p) => p.name === "[[TargetFunction]]");
|
|
const targetFunctionText = desc?.value?.description;
|
|
if (!targetFunctionText) {
|
|
throw new Error("function is not a bound function");
|
|
}
|
|
|
|
const boundThisValue = internalProperties.find((p) => p.name === "[[BoundThis]]")?.value?.value;
|
|
|
|
const boundArgsObjectId = internalProperties.find((p) => p.name === "[[BoundArgs]]")?.value?.objectId;
|
|
let boundArgsValues: any[] = [];
|
|
if (boundArgsObjectId) {
|
|
const { properties } = await runtimeGetPropertiesAsync(boundArgsObjectId, /*ownProperties:*/ false);
|
|
boundArgsValues = properties.filter((p) => p.enumerable).map((p) => p.value?.value);
|
|
}
|
|
|
|
return { targetFunctionText, boundThisValue, boundArgsValues };
|
|
}
|