-
Notifications
You must be signed in to change notification settings - Fork 266
/
nodeBinaryProvider.ts
383 lines (332 loc) · 12 KB
/
nodeBinaryProvider.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { inject, injectable, optional } from 'inversify';
import { basename, dirname, extname, isAbsolute, resolve } from 'path';
import type * as vscodeType from 'vscode';
import * as nls from 'vscode-nls';
import { EnvironmentVars } from '../../common/environmentVars';
import { ILogger, LogTag } from '../../common/logging';
import { findExecutable, findInPath } from '../../common/pathUtils';
import { spawnAsync } from '../../common/processUtils';
import { Semver } from '../../common/semver';
import {
cannotFindNodeBinary,
ErrorCodes,
isErrorOfType,
nodeBinaryOutOfDate,
} from '../../dap/errors';
import { ProtocolError } from '../../dap/protocolError';
import { FS, FsPromises, VSCodeApi } from '../../ioc-extras';
import { IPackageJsonProvider } from './packageJsonProvider';
const localize = nls.loadMessageBundle();
export const INodeBinaryProvider = Symbol('INodeBinaryProvider');
export const enum Capability {
UseSpacesInRequirePath,
UseInspectPublishUid,
}
/**
* If the Node binary supports it, adds an option to the NODE_OPTIONS that
* prevents spewing extra debug info to the console.
* @see https://github.com/microsoft/vscode-js-debug/issues/558
*/
export function hideDebugInfoFromConsole(binary: NodeBinary, env: EnvironmentVars) {
return binary.has(Capability.UseInspectPublishUid)
? env.addNodeOption('--inspect-publish-uid=http')
: env;
}
const packageManagers: ReadonlySet<string> = new Set(['npm', 'yarn', 'pnpm', 'tnpm', 'cnpm']);
export const isPackageManager = (exe: string) => packageManagers.has(basename(exe, extname(exe)));
/**
* Detects an "npm run"-style invokation, and if found gets the script that the
* user intends to run.
*/
export const getRunScript = (
runtimeExecutable: string | null,
runtimeArgs: ReadonlyArray<string>,
) => {
if (!runtimeExecutable || !isPackageManager(runtimeExecutable)) {
return;
}
return runtimeArgs.find(a => !a.startsWith('-') && a !== 'run' && a !== 'run-script');
};
const assumedVersion = new Semver(12, 0, 0);
const minimumVersion = new Semver(8, 0, 0);
export interface IWarningMessage {
inclusiveMin: Semver;
inclusiveMax: Semver;
message: string;
}
const warningMessages: ReadonlyArray<IWarningMessage> = [
{
inclusiveMin: new Semver(16, 0, 0),
inclusiveMax: new Semver(16, 3, 99),
message: localize(
'warning.16bpIssue',
'Some breakpoints might not work in your version of Node.js. We recommend upgrading for the latest bug, performance, and security fixes. Details: https://aka.ms/AAcsvqm',
),
},
{
inclusiveMin: new Semver(7, 0, 0),
inclusiveMax: new Semver(8, 99, 99),
message: localize(
'warning.8outdated',
"You're running an outdated version of Node.js. We recommend upgrading for the latest bug, performance, and security fixes.",
),
},
];
/**
* DTO returned from the NodeBinaryProvider.
*/
export class NodeBinary {
/**
* Gets whether this version was detected exactly, or just assumed.
*/
public get isPreciselyKnown() {
return this.version !== undefined;
}
/**
* Gets a warning message for users of the binary.
*/
public get warning() {
if (!this.version) {
return;
}
for (const message of warningMessages) {
if (message.inclusiveMax.gte(this.version) && message.inclusiveMin.lte(this.version)) {
return message;
}
}
return undefined;
}
private capabilities = new Set<Capability>();
constructor(public readonly path: string, public version: Semver | undefined) {
if (version === undefined) {
version = assumedVersion;
}
if (version.gte(new Semver(12, 0, 0))) {
this.capabilities.add(Capability.UseSpacesInRequirePath);
}
if (version.gte(new Semver(12, 6, 0))) {
this.capabilities.add(Capability.UseInspectPublishUid);
}
}
/**
* Gets whether the Node program has the capability. If `defaultIfImprecise`
* is passed and the Node Binary's version is not exactly know, that default
* will be returned instead.
*/
public has(capability: Capability, defaultIfImprecise?: boolean): boolean {
if (!this.isPreciselyKnown && defaultIfImprecise !== undefined) {
return defaultIfImprecise;
}
return this.capabilities.has(capability);
}
}
export class NodeBinaryOutOfDateError extends ProtocolError {
constructor(public readonly version: string | Semver, public readonly location: string) {
super(nodeBinaryOutOfDate(version.toString(), location));
}
}
const exeRe = /^(node|electron)(64)?(\.exe|\.cmd)?$/i;
/**
* Mapping of electron versions to *effective* node versions. This is not
* as simple as it looks. Electron bundles their own Node version, but that
* Node version is not actually the same as the released version. For example
* Electron 5 is Node 12 but doesn't contain the NODE_OPTIONS parsing fixes
* that Node 12.0.0 does.
*
* todo: we should move to individual feature flags if/when we need additional
* functionality here.
*/
const electronNodeVersion = new Map<number, Semver>([
[11, new Semver(12, 0, 0)],
[10, new Semver(12, 0, 0)],
[9, new Semver(12, 0, 0)],
[8, new Semver(12, 0, 0)],
[7, new Semver(12, 0, 0)],
[6, new Semver(12, 0, 0)],
[5, new Semver(10, 0, 0)], // 12, but doesn't include the NODE_OPTIONS parsing fixes
[4, new Semver(10, 0, 0)],
[3, new Semver(10, 0, 0)],
[2, new Semver(8, 0, 0)],
[1, new Semver(8, 0, 0)], // 7 earlier, but that will throw an error -- at least try
]);
export interface INodeBinaryProvider {
/**
* Validates the path and returns an absolute path to the Node binary to run.
* @param env The environment variables to use to resolve the node binary.
* @param executable An explicit executable path to resolve, will bypass
* path-based detection if given.
* @param explicitVersion An explicit Node.js version to use, will bypass
* version checking on the binary ig given.
*/
resolveAndValidate(
env: EnvironmentVars,
executable?: string,
explicitVersion?: number,
): Promise<NodeBinary>;
}
/**
* Utility that resolves a path to Node.js and validates
* it's a debuggable version./
*/
@injectable()
export class NodeBinaryProvider {
/**
* A set of binary paths we know are good and which can skip additional
* validation. We don't store bad mappings, because a user might reinstall
* or upgrade node in-place after we tell them it's outdated.
*/
private readonly knownGoodMappings = new Map<string, NodeBinary>();
constructor(
@inject(ILogger) private readonly logger: ILogger,
@inject(FS) private readonly fs: FsPromises,
@inject(IPackageJsonProvider) private readonly packageJson: IPackageJsonProvider,
) {}
/**
* Validates the path and returns an absolute path to the Node binary to run.
*/
public async resolveAndValidate(
env: EnvironmentVars,
executable = 'node',
explicitVersion?: number,
): Promise<NodeBinary> {
try {
return await this.resolveAndValidateInner(env, executable, explicitVersion);
} catch (e) {
if (!(e instanceof NodeBinaryOutOfDateError)) {
throw e;
}
if (await this.shouldTryDebuggingAnyway(e)) {
return new NodeBinary(e.location, e.version instanceof Semver ? e.version : undefined);
}
throw e;
}
}
/**
* Gets whether we should continue to try to debug even if we saw an outdated
* Node.js version.
*/
protected shouldTryDebuggingAnyway(_outatedReason: NodeBinaryOutOfDateError) {
return Promise.resolve(false);
}
private async resolveAndValidateInner(
env: EnvironmentVars,
executable: string,
explicitVersion: number | undefined,
): Promise<NodeBinary> {
const location = await this.resolveBinaryLocation(executable, env);
this.logger.info(LogTag.RuntimeLaunch, 'Using binary at', { location, executable });
if (!location) {
throw new ProtocolError(
cannotFindNodeBinary(
executable,
localize('runtime.node.notfound.enoent', 'path does not exist'),
),
);
}
if (explicitVersion) {
return new NodeBinary(location, new Semver(explicitVersion, 0, 0));
}
// If the runtime executable doesn't look like Node.js (could be a shell
// script that boots Node by itself, for instance) try to find Node itself
// on the path as a fallback.
const exeInfo = exeRe.exec(basename(location).toLowerCase());
if (!exeInfo) {
if (isPackageManager(location)) {
const packageJson = await this.packageJson.getPath();
if (packageJson) {
env = env.addToPath(resolve(dirname(packageJson), 'node_modules/.bin'), 'prepend');
}
}
try {
const realBinary = await this.resolveAndValidateInner(env, 'node', undefined);
return new NodeBinary(location, realBinary.version);
} catch (e) {
// if we verified it's outdated, still throw the error. If it's not
// found, at least try to run it since the package manager exists.
if (isErrorOfType(e, ErrorCodes.NodeBinaryOutOfDate)) {
throw e;
}
return new NodeBinary(location, undefined);
}
}
// Seems like we can't get stdout from Node installed in snap, see:
// https://github.com/microsoft/vscode/issues/102355#issuecomment-657707702
if (location.startsWith('/snap/')) {
return new NodeBinary(location, undefined);
}
const knownGood = this.knownGoodMappings.get(location);
if (knownGood) {
return knownGood;
}
// match the "12" in "v12.34.56"
const versionText = await this.getVersionText(location);
this.logger.info(LogTag.RuntimeLaunch, 'Discovered version', { version: versionText.trim() });
const majorVersionMatch = /v([0-9]+)\.([0-9]+)\.([0-9]+)/.exec(versionText);
if (!majorVersionMatch) {
throw new NodeBinaryOutOfDateError(versionText.trim(), location);
}
const [, major, minor, patch] = majorVersionMatch.map(Number);
let version = new Semver(major, minor, patch);
// remap the node version bundled if we're running electron
if (exeInfo[1] === 'electron') {
const nodeVersion = await this.resolveAndValidate(env);
version = Semver.min(
electronNodeVersion.get(version.major) ?? assumedVersion,
nodeVersion.version ?? assumedVersion,
);
}
if (version.lt(minimumVersion)) {
throw new NodeBinaryOutOfDateError(version, location);
}
const entry = new NodeBinary(location, version);
this.knownGoodMappings.set(location, entry);
return entry;
}
public async resolveBinaryLocation(executable: string, env: EnvironmentVars) {
return executable && isAbsolute(executable)
? await findExecutable(this.fs, executable, env)
: await findInPath(this.fs, executable, env.value);
}
public async getVersionText(binary: string) {
try {
const { stdout } = await spawnAsync(binary, ['--version'], {
env: EnvironmentVars.processEnv().defined(),
});
return stdout;
} catch (e) {
throw new ProtocolError(
cannotFindNodeBinary(
binary,
localize('runtime.node.notfound.spawnErr', 'error getting version: {0}', e.message),
),
);
}
}
}
export class InteractiveNodeBinaryProvider extends NodeBinaryProvider {
constructor(
@inject(ILogger) logger: ILogger,
@inject(FS) fs: FsPromises,
@inject(IPackageJsonProvider) packageJson: IPackageJsonProvider,
@optional() @inject(VSCodeApi) private readonly vscode: typeof vscodeType | undefined,
) {
super(logger, fs, packageJson);
}
/**
* @override
*/
protected async shouldTryDebuggingAnyway({ message }: NodeBinaryOutOfDateError) {
if (!this.vscode) {
return false;
}
const yes = localize('yes', 'Yes');
const response = await this.vscode.window.showErrorMessage(
localize('outOfDate', '{0} Would you like to try debugging anyway?', message),
yes,
);
return response === yes;
}
}