-
Notifications
You must be signed in to change notification settings - Fork 37.2k
Fix deadlock caused by import 'vscode' from modules loaded via require(esm)
#285417
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,7 @@ import nodeModule from 'node:module'; | |
| import { assertType } from '../../../base/common/types.js'; | ||
| import { generateUuid } from '../../../base/common/uuid.js'; | ||
| import { BidirectionalMap } from '../../../base/common/map.js'; | ||
| import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js'; | ||
| import { DisposableStore } from '../../../base/common/lifecycle.js'; | ||
|
|
||
| const require = nodeModule.createRequire(import.meta.url); | ||
|
|
||
|
|
@@ -75,43 +75,12 @@ class NodeModuleRequireInterceptor extends RequireInterceptor { | |
| } | ||
| } | ||
|
|
||
| class NodeModuleESMInterceptor extends RequireInterceptor { | ||
| class NodeModuleInterceptor extends RequireInterceptor { | ||
|
|
||
| private static _createDataUri(scriptContent: string): string { | ||
| return `data:text/javascript;base64,${Buffer.from(scriptContent).toString('base64')}`; | ||
| } | ||
|
|
||
| // This string is a script that runs in the loader thread of NodeJS. | ||
| private static _loaderScript = ` | ||
| let lookup; | ||
| export const initialize = async (context) => { | ||
| let requestIds = 0; | ||
| const { port } = context; | ||
| const pendingRequests = new Map(); | ||
| port.onmessage = (event) => { | ||
| const { id, url } = event.data; | ||
| pendingRequests.get(id)?.(url); | ||
| }; | ||
| lookup = url => { | ||
| // debugger; | ||
| const myId = requestIds++; | ||
| return new Promise((resolve) => { | ||
| pendingRequests.set(myId, resolve); | ||
| port.postMessage({ id: myId, url, }); | ||
| }); | ||
| }; | ||
| }; | ||
| export const resolve = async (specifier, context, nextResolve) => { | ||
| if (specifier !== 'vscode' || !context.parentURL) { | ||
| return nextResolve(specifier, context); | ||
| } | ||
| const otherUrl = await lookup(context.parentURL); | ||
| return { | ||
| url: otherUrl, | ||
| shortCircuit: true, | ||
| }; | ||
| };`; | ||
|
|
||
| private static _vscodeImportFnName = `_VSCODE_IMPORT_VSCODE_API`; | ||
|
|
||
| private readonly _store = new DisposableStore(); | ||
|
|
@@ -121,14 +90,11 @@ class NodeModuleESMInterceptor extends RequireInterceptor { | |
| } | ||
|
|
||
| protected override _installInterceptor(): void { | ||
|
|
||
| type Message = { id: string; url: string }; | ||
|
|
||
| const apiInstances = new BidirectionalMap<typeof vscode, string>(); | ||
| const apiImportDataUrl = new Map<string, string>(); | ||
|
|
||
| // define a global function that can be used to get API instances given a random key | ||
| Object.defineProperty(globalThis, NodeModuleESMInterceptor._vscodeImportFnName, { | ||
| Object.defineProperty(globalThis, NodeModuleInterceptor._vscodeImportFnName, { | ||
| enumerable: false, | ||
| configurable: false, | ||
| writable: false, | ||
|
|
@@ -137,24 +103,16 @@ class NodeModuleESMInterceptor extends RequireInterceptor { | |
| } | ||
| }); | ||
|
|
||
| const { port1, port2 } = new MessageChannel(); | ||
|
|
||
| let apiModuleFactory: INodeModuleFactory | undefined; | ||
|
|
||
| // this is a workaround for the fact that the layer checker does not understand | ||
| // that onmessage is NodeJS API here | ||
| const port1LayerCheckerWorkaround: any = port1; | ||
|
|
||
| port1LayerCheckerWorkaround.onmessage = (e: { data: Message }) => { | ||
|
|
||
| const lookup = (url: string): string => { | ||
| // Get the vscode-module factory - which is the same logic that's also used by | ||
| // the CommonJS require interceptor | ||
| if (!apiModuleFactory) { | ||
| apiModuleFactory = this._factories.get('vscode'); | ||
| assertType(apiModuleFactory); | ||
| } | ||
|
|
||
| const { id, url } = e.data; | ||
| const uri = URI.parse(url); | ||
|
|
||
| // Get or create the API instance. The interface is per extension and extensions are | ||
|
|
@@ -169,27 +127,25 @@ class NodeModuleESMInterceptor extends RequireInterceptor { | |
| // Create and cache a data-url which is the import script for the API instance | ||
| let scriptDataUrlSrc = apiImportDataUrl.get(key); | ||
| if (!scriptDataUrlSrc) { | ||
| const jsCode = `const _vscodeInstance = globalThis.${NodeModuleESMInterceptor._vscodeImportFnName}('${key}');\n\n${Object.keys(apiInstance).map((name => `export const ${name} = _vscodeInstance['${name}'];`)).join('\n')}`; | ||
| scriptDataUrlSrc = NodeModuleESMInterceptor._createDataUri(jsCode); | ||
| const jsCode = `const _vscodeInstance = globalThis.${NodeModuleInterceptor._vscodeImportFnName}('${key}');\n\n${Object.keys(apiInstance).map((name => `export const ${name} = _vscodeInstance['${name}'];`)).join('\n')}`; | ||
| scriptDataUrlSrc = NodeModuleInterceptor._createDataUri(jsCode); | ||
| apiImportDataUrl.set(key, scriptDataUrlSrc); | ||
| } | ||
|
|
||
| port1.postMessage({ | ||
| id, | ||
| url: scriptDataUrlSrc | ||
| }); | ||
| return scriptDataUrlSrc; | ||
| }; | ||
|
|
||
| nodeModule.register(NodeModuleESMInterceptor._createDataUri(NodeModuleESMInterceptor._loaderScript), { | ||
| parentURL: import.meta.url, | ||
| data: { port: port2 }, | ||
| transferList: [port2], | ||
| nodeModule.registerHooks({ | ||
| resolve: (specifier, context, nextResolve) => { | ||
| if (specifier !== 'vscode' || !context.parentURL) { | ||
| return nextResolve(specifier, context); | ||
| } | ||
| console.log('ESM resolve', specifier, context); | ||
| const otherUrl = lookup(context.parentURL); | ||
| return { | ||
| url: otherUrl, | ||
| shortCircuit: true, | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| this._store.add(toDisposable(() => { | ||
| port1.close(); | ||
| port2.close(); | ||
| })); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -216,12 +172,17 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { | |
| // Register local file system shortcut | ||
| this._instaService.createInstance(ExtHostDiskFileSystemProvider); | ||
|
|
||
| // Module loading tricks | ||
| // Module loading tricks based on `module._load`. | ||
| // `module._load` intercepts `require(...)`. | ||
| await this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry }) | ||
| .install(); | ||
|
|
||
| // ESM loading tricks | ||
| await this._store.add(this._instaService.createInstance(NodeModuleESMInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry })) | ||
| // Module loading tricks based on `module.registerHooks`. | ||
| // `module.registerHooks` is a generic interceptor that intercepts `require(...)`, `import ...`, and `import(...)`. | ||
| // However, at this time, `NodeModuleInterceptor` only intercepts `import 'vscode'` and `import('vscode')`. | ||
| // This can also intercept `require('vscode')`, but interception by `NodeModuleRequireInterceptor` takes precedence. | ||
| // In the future, we can consider migrating all interception logic to `NodeModuleInterceptor` and removing `NodeModuleRequireInterceptor`. | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Incidentally, it appears Node.js plans to implement hooks that can return objects. Once implemented, this complex workaround could be removed. Porting the logic from |
||
| await this._store.add(this._instaService.createInstance(NodeModuleInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry })) | ||
| .install(); | ||
|
|
||
| performance.mark('code/extHost/didInitAPI'); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit confusing that only
require('vscode')gets intercepted byNodeModuleRequireInterceptor. So I tried modifying it as follows to haverequire('vscode')intercepted byNodeModuleInterceptor.However, this causes an
ENAMETOOLONGerror.For some reason,
originalLoad.apply()appears to return a base64-encoded path generated by NodeModuleInterceptor instead of the module instance. This seems to be a Node.js bug.Since I couldn't find a workaround, I decided to abandon intercepting
require('vscode')withNodeModuleInterceptor.