Skip to content

Conversation

@mizdra
Copy link

@mizdra mizdra commented Dec 30, 2025

close: #285297

Background

An extension with code like the following will cause a deadlock.

// src/extension.cjs
const vscode = require('vscode');
require('./sub.js');
exports.activate = function (context) {
  // ...
};
// src/sub.js
// The following will hang the test.
import * as vscode from 'vscode';

The detailed reasons are explained at #285297 (comment). This PR attempts to fix that issue using module.registerHooks.

How to test

The test for https://github.com/mizdra/repro-vscode-test-hang can run to completion without hanging.

$ gh pr checkout 285417

$ # Build vscode
$ npm install
$ git clone git@github.com:mizdra/repro-vscode-test-hang.git
$ cd repro-vscode-test-hang

$ # Edit files...
$ vim
$ git diff -U1                 
diff --git a/test/runTest.js b/test/runTest.js
index 01ac63b..99c3376 100644
--- a/test/runTest.js
+++ b/test/runTest.js
@@ -14,3 +14,3 @@ async function main() {
       launchArgs: ['--disable-extensions'],
-      // vscodeExecutablePath: '/Users/mizdra/src/github.com/microsoft/vscode/scripts/code.sh',
+      vscodeExecutablePath: '/Users/mizdra/src/github.com/microsoft/vscode/scripts/code.sh',
     });

$ npm start

Protected by Socket Firewall

> repro-vscode-test-hang@1.0.0 start
> node test/runTest.js

npm warn Unknown project config "disturl". This will stop working in the next major version of npm.
npm warn Unknown project config "target". This will stop working in the next major version of npm.
npm warn Unknown project config "ms_build_id". This will stop working in the next major version of npm.
npm warn Unknown project config "runtime". This will stop working in the next major version of npm.
npm warn Unknown project config "build_from_source". This will stop working in the next major version of npm.
npm warn Unknown project config "timeout". This will stop working in the next major version of npm.

> code-oss-dev@1.108.0 electron
> node build/lib/electron.ts

[21:57:55] Synchronizing built-in extensions...
[21:57:55] You can manage built-in extensions with the --builtin flag
[21:57:55] [github] ms-vscode.js-debug-companion@1.1.3 ✔︎
[21:57:55] [github] ms-vscode.js-debug@1.105.0 ✔︎
[21:57:55] [github] ms-vscode.vscode-js-profile-table@1.0.10 ✔︎
[main 2025-12-30T12:57:55.974Z] [CSS_DEV] DONE, 283 css modules (52ms)
[main 2025-12-30T12:57:56.178Z] update#setState disabled
Started local extension host with pid 81065.
No default agent registered
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:

1 !== 2

	at file:///Users/mizdra/src/github.com/mizdra/repro-vscode-test-hang/test/index.test.js:6:8
...
// 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.
Copy link
Author

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 by NodeModuleRequireInterceptor. So I tried modifying it as follows to have require('vscode') intercepted by NodeModuleInterceptor.

--- a/src/vs/workbench/api/node/extHostExtensionService.ts
+++ b/src/vs/workbench/api/node/extHostExtensionService.ts
@@ -35,7 +35,7 @@ class NodeModuleRequireInterceptor extends RequireInterceptor {
                const originalLoad = node_module._load;
                node_module._load = function load(request: string, parent: { filename: string }, isMain: boolean) {
                        request = applyAlternatives(request);
-                       if (!that._factories.has(request)) {
+                       if (!that._factories.has(request) || request === 'vscode') {
                                return originalLoad.apply(this, arguments);
                        }

However, this causes an ENAMETOOLONG error.

$ cd repro-vscode-test-hang

$ # Edit files
$ vim

$ git diff
diff --git a/test/index.test.js b/test/index.test.js
index 209123b..3b8b882 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -1,6 +1,10 @@
 import assert from 'node:assert/strict';

 // The following will hang the test.
-import * as vscode from 'vscode';
+// import * as vscode from 'vscode';
+
+import { createRequire } from 'node:module';
+const require = createRequire(import.meta.url);
+const vscode = require('vscode');

 assert.equal(1, 2);
diff --git a/test/runTest.js b/test/runTest.js
index 01ac63b..99c3376 100644
--- a/test/runTest.js
+++ b/test/runTest.js
@@ -12,7 +12,7 @@ async function main() {
       extensionDevelopmentPath,
       extensionTestsPath,
       launchArgs: ['--disable-extensions'],
-      // vscodeExecutablePath: '/Users/mizdra/src/github.com/microsoft/vscode/scripts/code.sh',
+      vscodeExecutablePath: '/Users/mizdra/src/github.com/microsoft/vscode/scripts/code.sh',
     });
   } catch (err) {
     console.error(err);


$ npm start
Error: ENAMETOOLONG: name too long, open 'data:text/javascript;base64,Y29uc3QgX3ZzY29kZUluc3RhbmNlID0gZ2xvYmFsVGhpcy5fVlNDT0RFX0lNUE9SVF9WU0NPREVfQVBJKCc5MGMzNTUwMC0yNjY0LTQ2M2...(long text)...107'
	at Object.readFileSync (node:fs:441:20)
	at t.readFileSync (node:electron/js2c/node_init:2:11013)
	at defaultLoadImpl (node:internal/modules/cjs/loader:1112:17)
	at loadSource (node:internal/modules/cjs/loader:1742:20)
	at Module._extensions..js (node:internal/modules/cjs/loader:1841:44)
	at Module.load (node:internal/modules/cjs/loader:1448:32)
	at Module._load (node:internal/modules/cjs/loader:1270:12)
	at c._load (node:electron/js2c/node_init:2:17993)
	at Module._load (file:///Users/mizdra/src/github.com/microsoft/vscode/out/vs/workbench/api/node/extensionHostProcess.js:68:29)
	at Module.load (file:///Users/mizdra/src/github.com/microsoft/vscode/out/vs/workbench/api/node/extHostExtensionService.js:32:37)
	at Module.load [as _load] (file:///Users/mizdra/src/github.com/microsoft/vscode/out/vs/workbench/api/node/proxyResolver.js:290:33)
	at TracingChannel.traceSync (node:diagnostics_channel:328:14)
	at wrapModuleLoad (node:internal/modules/cjs/loader:244:24)
	at Module.require (node:internal/modules/cjs/loader:1470:12)
	at require (node:internal/modules/helpers:147:16)
	at file:///Users/mizdra/src/github.com/mizdra/repro-vscode-test-hang/test/index.test.js:8:16
	at ModuleJobSync.runSync (node:internal/modules/esm/module_job:454:37)
	at ModuleLoader.importSyncForRequire (node:internal/modules/esm/loader:435:47)
	at loadESMFromCJS (node:internal/modules/cjs/loader:1544:24)
	at Module._compile (node:internal/modules/cjs/loader:1695:5)
	at Module._extensions..js (node:internal/modules/cjs/loader:1848:10)
	at Module.load (node:internal/modules/cjs/loader:1448:32)
	at Module._load (node:internal/modules/cjs/loader:1270:12)
	at c._load (node:electron/js2c/node_init:2:17993)
	at Module._load (file:///Users/mizdra/src/github.com/microsoft/vscode/out/vs/workbench/api/node/extensionHostProcess.js:68:29)
	at Module.load (file:///Users/mizdra/src/github.com/microsoft/vscode/out/vs/workbench/api/node/extHostExtensionService.js:32:37)
	at Module.load [as _load] (file:///Users/mizdra/src/github.com/microsoft/vscode/out/vs/workbench/api/node/proxyResolver.js:290:33)
	at TracingChannel.traceSync (node:diagnostics_channel:328:14)
	at wrapModuleLoad (node:internal/modules/cjs/loader:244:24)
	at Module.require (node:internal/modules/cjs/loader:1470:12)
	at require (node:internal/modules/helpers:147:16)
	at Object.run (/Users/mizdra/src/github.com/mizdra/repro-vscode-test-hang/test/runner.cjs:3:3)
	at file:///Users/mizdra/src/github.com/microsoft/vscode/out/vs/workbench/api/common/extHostExtensionService.js:601:42
	at new Promise (<anonymous>)
	at ExtHostExtensionService._doHandleExtensionTests (file:///Users/mizdra/src/github.com/microsoft/vscode/out/vs/workbench/api/common/extHostExtensionService.js:580:16)
	at async ExtHostExtensionService.$extensionTestsExecute (file:///Users/mizdra/src/github.com/microsoft/vscode/out/vs/workbench/api/common/extHostExtensionService.js:558:20)

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') with NodeModuleInterceptor.

// `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`.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

module._load was able to return an object from a hook. However, hooks registered via module.registerHooks currently cannot do this. Therefore, NodeModuleInterceptor circumvents this limitation by using base64-encoded source text and NodeModuleInterceptor._vscodeImportFnName. This is a complex workaround.

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 NodeModuleRequireInterceptor to NodeModuleInterceptor should also become easier.

@mizdra mizdra marked this pull request as ready for review December 30, 2025 13:29
@mizdra
Copy link
Author

mizdra commented Dec 30, 2025

The review is ready. Could you please review it? @mjbvz

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants