From ee2f65510bd8129adffb18d7e198bf12c1b84fee Mon Sep 17 00:00:00 2001 From: Daniel Sokolowski Date: Mon, 12 Apr 2021 18:13:47 +0200 Subject: [PATCH] [sdk/nodejs] Add multiple VM contexts support to closure serialization (#6648) --- CHANGELOG_PENDING.md | 3 + sdk/nodejs/runtime/closure/v8_v11andHigher.ts | 103 +++++++++++------- 2 files changed, 66 insertions(+), 40 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index f29c111b2..07af69fce 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -17,6 +17,9 @@ ### Improvements +- [sdk/nodejs] Add support for multiple V8 VM contexts in closure serialization. + [#6648](https://github.com/pulumi/pulumi/pull/6648) + - [cli] Add option to print absolute rather than relative dates in stack history [#6742](https://github.com/pulumi/pulumi/pull/6742) diff --git a/sdk/nodejs/runtime/closure/v8_v11andHigher.ts b/sdk/nodejs/runtime/closure/v8_v11andHigher.ts index a7dd33a76..5ce284e76 100644 --- a/sdk/nodejs/runtime/closure/v8_v11andHigher.ts +++ b/sdk/nodejs/runtime/closure/v8_v11andHigher.ts @@ -16,6 +16,7 @@ import * as inspector from "inspector"; import * as util from "util"; +import * as vm from "vm"; import * as v8Hooks from "./v8Hooks"; /** @internal */ @@ -105,45 +106,74 @@ type PostSession = { 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) => void): void; +}; + +type InflightContext = { + contextId: number; + functions: Record; + currentFunctionId: number; + calls: Record; + 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. +const inflightContext = createContext(); +async function createContext(): Promise { + const context: InflightContext = { + contextId: 0, + functions: {}, + currentFunctionId: 0, + calls: {}, + currentCallId: 0, + }; + const session = 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(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 { // 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 just make a special map on the - // 'global' object, 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. + // 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. - // - // We also lazily initialize this in case pulumi has been loaded through another module and has - // already initialize this global state. - const globalAny = global; - if (!globalAny.__inflightFunctions) { - globalAny.__inflightFunctions = {}; - globalAny.__currentFunctionId = 0; - } - // Place the function in a unique location off of the global object. - const currentFunctionName = "id" + globalAny.__currentFunctionId++; - globalAny.__inflightFunctions[currentFunctionName] = func; + const session = 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 session = await v8Hooks.getSessionAsync(); - const post = util.promisify(session.post); - - const expression = `global.__inflightFunctions.${currentFunctionName}`; - - // 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 = await post.call( - session, "Runtime.evaluate", { expression }); + const retType = await post.call(session, "Runtime.evaluate", { contextId, expression }); if (retType.exceptionDetails) { - throw new Error(`Error calling "Runtime.evaluate(${expression})": ` + retType.exceptionDetails.text); + throw new Error(`Error calling "Runtime.evaluate(${expression})" on context ${contextId}: ` + retType.exceptionDetails.text); } const remoteObject = retType.result; @@ -158,7 +188,7 @@ async function getRuntimeIdForFunctionAsync(func: Function): Promiseglobal; - if (!globalAny.__inflightCalls) { - globalAny.__inflightCalls = {}; - globalAny.__currentCallId = 0; - } const session = 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" + globalAny.__currentCallId++; + 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 @@ -214,7 +237,7 @@ async function getValueForObjectId(objectId: inspector.Runtime.RemoteObjectId): session, "Runtime.callFunctionOn", { objectId, functionDeclaration: `function () { - global.__inflightCalls["${tableId}"] = this; + calls["${tableId}"] = this; }`, }); @@ -223,13 +246,13 @@ async function getValueForObjectId(objectId: inspector.Runtime.RemoteObjectId): + retType.exceptionDetails.text); } - if (!globalAny.__inflightCalls.hasOwnProperty(tableId)) { + 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 = globalAny.__inflightCalls[tableId]; - delete globalAny.__inflightCalls[tableId]; + const val = context.calls[tableId]; + delete context.calls[tableId]; return val; }