Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/node/extHostExtensionService.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as performance from '../../../base/common/performance.js';
7
import type * as vscode from 'vscode';
8
import { createApiFactoryAndRegisterActors } from '../common/extHost.api.impl.js';
9
import { INodeModuleFactory, RequireInterceptor } from '../common/extHostRequireInterceptor.js';
10
import { ExtensionActivationTimesBuilder } from '../common/extHostExtensionActivator.js';
11
import { connectProxyResolver } from './proxyResolver.js';
12
import { AbstractExtHostExtensionService } from '../common/extHostExtensionService.js';
13
import { ExtHostDownloadService } from './extHostDownloadService.js';
14
import { URI } from '../../../base/common/uri.js';
15
import { Schemas } from '../../../base/common/network.js';
16
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
17
import { ExtensionRuntime } from '../common/extHostTypes.js';
18
import { CLIServer } from './extHostCLIServer.js';
19
import { realpathSync } from '../../../base/node/pfs.js';
20
import { ExtHostConsoleForwarder } from './extHostConsoleForwarder.js';
21
import { ExtHostDiskFileSystemProvider } from './extHostDiskFileSystemProvider.js';
22
import nodeModule from 'node:module';
23
import { assertType } from '../../../base/common/types.js';
24
import { generateUuid } from '../../../base/common/uuid.js';
25
import { BidirectionalMap } from '../../../base/common/map.js';
26
import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
27
28
const require = nodeModule.createRequire(import.meta.url);
29
30
class NodeModuleRequireInterceptor extends RequireInterceptor {
31
32
protected _installInterceptor(): void {
33
const that = this;
34
const node_module = require('module');
35
const originalLoad = node_module._load;
36
node_module._load = function load(request: string, parent: { filename: string }, isMain: boolean) {
37
request = applyAlternatives(request);
38
if (!that._factories.has(request)) {
39
return originalLoad.apply(this, arguments);
40
}
41
return that._factories.get(request)!.load(
42
request,
43
URI.file(realpathSync(parent.filename)),
44
request => originalLoad.apply(this, [request, parent, isMain])
45
);
46
};
47
48
const originalLookup = node_module._resolveLookupPaths;
49
node_module._resolveLookupPaths = (request: string, parent: unknown) => {
50
return originalLookup.call(this, applyAlternatives(request), parent);
51
};
52
53
const originalResolveFilename = node_module._resolveFilename;
54
node_module._resolveFilename = function resolveFilename(request: string, parent: unknown, isMain: boolean, options?: { paths?: string[] }) {
55
if (request === 'vsda' && Array.isArray(options?.paths) && options.paths.length === 0) {
56
// ESM: ever since we moved to ESM, `require.main` will be `undefined` for extensions
57
// Some extensions have been using `require.resolve('vsda', { paths: require.main.paths })`
58
// to find the `vsda` module in our app root. To be backwards compatible with this pattern,
59
// we help by filling in the `paths` array with the node modules paths of the current module.
60
options.paths = node_module._nodeModulePaths(import.meta.dirname);
61
}
62
return originalResolveFilename.call(this, request, parent, isMain, options);
63
};
64
65
const applyAlternatives = (request: string) => {
66
for (const alternativeModuleName of that._alternatives) {
67
const alternative = alternativeModuleName(request);
68
if (alternative) {
69
request = alternative;
70
break;
71
}
72
}
73
return request;
74
};
75
}
76
}
77
78
class NodeModuleESMInterceptor extends RequireInterceptor {
79
80
private static _createDataUri(scriptContent: string): string {
81
return `data:text/javascript;base64,${Buffer.from(scriptContent).toString('base64')}`;
82
}
83
84
// This string is a script that runs in the loader thread of NodeJS.
85
private static _loaderScript = `
86
let lookup;
87
export const initialize = async (context) => {
88
let requestIds = 0;
89
const { port } = context;
90
const pendingRequests = new Map();
91
port.onmessage = (event) => {
92
const { id, url } = event.data;
93
pendingRequests.get(id)?.(url);
94
};
95
lookup = url => {
96
// debugger;
97
const myId = requestIds++;
98
return new Promise((resolve) => {
99
pendingRequests.set(myId, resolve);
100
port.postMessage({ id: myId, url, });
101
});
102
};
103
};
104
export const resolve = async (specifier, context, nextResolve) => {
105
if (specifier !== 'vscode' || !context.parentURL) {
106
return nextResolve(specifier, context);
107
}
108
const otherUrl = await lookup(context.parentURL);
109
return {
110
url: otherUrl,
111
shortCircuit: true,
112
};
113
};`;
114
115
private static _vscodeImportFnName = `_VSCODE_IMPORT_VSCODE_API`;
116
117
private readonly _store = new DisposableStore();
118
119
dispose(): void {
120
this._store.dispose();
121
}
122
123
protected override _installInterceptor(): void {
124
125
type Message = { id: string; url: string };
126
127
const apiInstances = new BidirectionalMap<typeof vscode, string>();
128
const apiImportDataUrl = new Map<string, string>();
129
130
// define a global function that can be used to get API instances given a random key
131
Object.defineProperty(globalThis, NodeModuleESMInterceptor._vscodeImportFnName, {
132
enumerable: false,
133
configurable: false,
134
writable: false,
135
value: (key: string) => {
136
return apiInstances.getKey(key);
137
}
138
});
139
140
const { port1, port2 } = new MessageChannel();
141
142
let apiModuleFactory: INodeModuleFactory | undefined;
143
144
// this is a workaround for the fact that the layer checker does not understand
145
// that onmessage is NodeJS API here
146
const port1LayerCheckerWorkaround: any = port1;
147
148
port1LayerCheckerWorkaround.onmessage = (e: { data: Message }) => {
149
150
// Get the vscode-module factory - which is the same logic that's also used by
151
// the CommonJS require interceptor
152
if (!apiModuleFactory) {
153
apiModuleFactory = this._factories.get('vscode');
154
assertType(apiModuleFactory);
155
}
156
157
const { id, url } = e.data;
158
const uri = URI.parse(url);
159
160
// Get or create the API instance. The interface is per extension and extensions are
161
// looked up by the uri (e.data.url) and path containment.
162
const apiInstance = apiModuleFactory.load('_not_used', uri, () => { throw new Error('CANNOT LOAD MODULE from here.'); });
163
let key = apiInstances.get(apiInstance);
164
if (!key) {
165
key = generateUuid();
166
apiInstances.set(apiInstance, key);
167
}
168
169
// Create and cache a data-url which is the import script for the API instance
170
let scriptDataUrlSrc = apiImportDataUrl.get(key);
171
if (!scriptDataUrlSrc) {
172
const jsCode = `const _vscodeInstance = globalThis.${NodeModuleESMInterceptor._vscodeImportFnName}('${key}');\n\n${Object.keys(apiInstance).map((name => `export const ${name} = _vscodeInstance['${name}'];`)).join('\n')}`;
173
scriptDataUrlSrc = NodeModuleESMInterceptor._createDataUri(jsCode);
174
apiImportDataUrl.set(key, scriptDataUrlSrc);
175
}
176
177
port1.postMessage({
178
id,
179
url: scriptDataUrlSrc
180
});
181
};
182
183
nodeModule.register(NodeModuleESMInterceptor._createDataUri(NodeModuleESMInterceptor._loaderScript), {
184
parentURL: import.meta.url,
185
data: { port: port2 },
186
transferList: [port2],
187
});
188
189
this._store.add(toDisposable(() => {
190
port1.close();
191
port2.close();
192
}));
193
}
194
}
195
196
export class ExtHostExtensionService extends AbstractExtHostExtensionService {
197
198
readonly extensionRuntime = ExtensionRuntime.Node;
199
200
protected async _beforeAlmostReadyToRunExtensions(): Promise<void> {
201
// make sure console.log calls make it to the render
202
this._instaService.createInstance(ExtHostConsoleForwarder);
203
204
// initialize API and register actors
205
const extensionApiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors);
206
207
// Register Download command
208
this._instaService.createInstance(ExtHostDownloadService);
209
210
// Register CLI Server for ipc
211
if (this._initData.remote.isRemote && this._initData.remote.authority) {
212
const cliServer = this._instaService.createInstance(CLIServer);
213
process.env['VSCODE_IPC_HOOK_CLI'] = cliServer.ipcHandlePath;
214
}
215
216
// Register local file system shortcut
217
this._instaService.createInstance(ExtHostDiskFileSystemProvider);
218
219
// Module loading tricks
220
await this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry })
221
.install();
222
223
// ESM loading tricks
224
await this._store.add(this._instaService.createInstance(NodeModuleESMInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry }))
225
.install();
226
227
performance.mark('code/extHost/didInitAPI');
228
229
// Do this when extension service exists, but extensions are not being activated yet.
230
const configProvider = await this._extHostConfiguration.getConfigProvider();
231
await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._logService, this._mainThreadTelemetryProxy, this._initData, this._store);
232
performance.mark('code/extHost/didInitProxyResolver');
233
}
234
235
protected _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined {
236
return extensionDescription.main;
237
}
238
239
private async _doLoadModule<T>(extension: IExtensionDescription | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder, mode: 'esm' | 'cjs'): Promise<T> {
240
if (module.scheme !== Schemas.file) {
241
throw new Error(`Cannot load URI: '${module}', must be of file-scheme`);
242
}
243
let r: T | null = null;
244
activationTimesBuilder.codeLoadingStart();
245
this._logService.trace(`ExtensionService#loadModule [${mode}] -> ${module.toString(true)}`);
246
this._logService.flush();
247
const extensionId = extension?.identifier.value;
248
if (extension) {
249
await this._extHostLocalizationService.initializeLocalizedMessages(extension);
250
}
251
try {
252
if (extensionId) {
253
performance.mark(`code/extHost/willLoadExtensionCode/${extensionId}`);
254
}
255
if (mode === 'esm') {
256
r = <T>await import(module.toString(true));
257
} else {
258
r = <T>require(module.fsPath);
259
}
260
} finally {
261
if (extensionId) {
262
performance.mark(`code/extHost/didLoadExtensionCode/${extensionId}`);
263
}
264
activationTimesBuilder.codeLoadingStop();
265
}
266
return r;
267
}
268
269
protected async _loadCommonJSModule<T>(extension: IExtensionDescription | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
270
return this._doLoadModule<T>(extension, module, activationTimesBuilder, 'cjs');
271
}
272
273
protected async _loadESMModule<T>(extension: IExtensionDescription | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
274
return this._doLoadModule<T>(extension, module, activationTimesBuilder, 'esm');
275
}
276
277
public async $setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void> {
278
if (!this._initData.remote.isRemote) {
279
return;
280
}
281
282
for (const key in env) {
283
const value = env[key];
284
if (value === null) {
285
delete process.env[key];
286
} else {
287
process.env[key] = value;
288
}
289
}
290
}
291
}
292
293