This repository was archived by the owner on Dec 15, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 60
Expand file tree
/
Copy pathnodeDebugAdapter.ts
More file actions
550 lines (473 loc) · 23.4 KB
/
nodeDebugAdapter.ts
File metadata and controls
550 lines (473 loc) · 23.4 KB
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
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import {ChromeDebugAdapter, logger, chromeUtils, ISourceMapPathOverrides} from 'vscode-chrome-debug-core';
import Crdp from 'chrome-remote-debug-protocol';
import {DebugProtocol} from 'vscode-debugprotocol';
import {OutputEvent} from 'vscode-debugadapter';
import * as path from 'path';
import * as fs from 'fs';
import * as cp from 'child_process';
import {LaunchRequestArguments, AttachRequestArguments} from './nodeDebugInterfaces';
import * as pathUtils from './pathUtils';
import * as utils from './utils';
import {localize} from './utils';
import * as errors from './errors';
const DefaultSourceMapPathOverrides: ISourceMapPathOverrides = {
'webpack:///./*': '${cwd}/*',
'webpack:///*': '*',
'meteor://💻app/*': '${cwd}/*',
};
export class NodeDebugAdapter extends ChromeDebugAdapter {
private static NODE = 'node';
private static RUNINTERMINAL_TIMEOUT = 5000;
private static NODE_TERMINATION_POLL_INTERVAL = 3000;
private _loggedTargetVersion: boolean;
private _nodeProcessId: number;
private _pollForNodeProcess: boolean;
// Flags relevant during init
private _continueAfterConfigDone = true;
private _entryPauseEvent: Crdp.Debugger.PausedEvent;
private _waitingForEntryPauseEvent = true;
private _finishedConfig = false;
private _supportsRunInTerminalRequest: boolean;
private _restartMode: boolean;
private _isTerminated: boolean;
private _adapterID: string;
public initialize(args: DebugProtocol.InitializeRequestArguments): DebugProtocol.Capabilities {
this._supportsRunInTerminalRequest = args.supportsRunInTerminalRequest;
this._adapterID = args.adapterID;
return super.initialize(args);
}
public launch(args: LaunchRequestArguments): Promise<void> {
args.sourceMapPathOverrides = getSourceMapPathOverrides(args.cwd, args.sourceMapPathOverrides);
super.launch(args);
const port = args.port || utils.random(3000, 50000);
let runtimeExecutable = args.runtimeExecutable;
if (runtimeExecutable) {
if (!path.isAbsolute(runtimeExecutable)) {
if (!pathUtils.isOnPath(runtimeExecutable)) {
return this.getRuntimeNotOnPathErrorResponse(runtimeExecutable);
}
} else if (!fs.existsSync(runtimeExecutable)) {
return this.getNotExistErrorResponse('runtimeExecutable', runtimeExecutable);
}
} else {
if (!utils.isOnPath(NodeDebugAdapter.NODE)) {
return Promise.reject(errors.runtimeNotFound(NodeDebugAdapter.NODE));
}
// use node from PATH
runtimeExecutable = NodeDebugAdapter.NODE;
}
if (this._adapterID === 'extensionHost') {
// we always launch in 'debug-brk' mode, but we only show the break event if 'stopOnEntry' attribute is true.
let launchArgs = [];
if (!args.noDebug) {
launchArgs.push(`--debugBrkPluginHost=${port}`, '--inspect');
}
const runtimeArgs = args.runtimeArgs || [];
const programArgs = args.args || [];
launchArgs = launchArgs.concat(runtimeArgs, programArgs);
return this.launchInInternalConsole(runtimeExecutable, launchArgs);
}
let programPath = args.program;
if (programPath) {
if (!path.isAbsolute(programPath)) {
return this.getRelativePathErrorResponse('program', programPath);
}
if (!fs.existsSync(programPath)) {
return this.getNotExistErrorResponse('program', programPath);
}
programPath = path.normalize(programPath);
if (pathUtils.normalizeDriveLetter(programPath) !== pathUtils.realPath(programPath)) {
logger.log(localize('program.path.case.mismatch.warning', "Program path uses differently cased character as file on disk; this might result in breakpoints not being hit."), /*forceLog=*/true);
}
}
return this.resolveProgramPath(programPath, args.sourceMaps).then<void>(resolvedProgramPath => {
let program: string;
let cwd = args.cwd;
if (cwd) {
if (!path.isAbsolute(cwd)) {
return this.getRelativePathErrorResponse('cwd', cwd);
}
if (!fs.existsSync(cwd)) {
return this.getNotExistErrorResponse('cwd', cwd);
}
// if working dir is given and if the executable is within that folder, we make the executable path relative to the working dir
if (resolvedProgramPath) {
program = path.relative(cwd, resolvedProgramPath);
}
} else if (resolvedProgramPath) {
// if no working dir given, we use the direct folder of the executable
cwd = path.dirname(resolvedProgramPath);
program = path.basename(resolvedProgramPath);
}
const runtimeArgs = args.runtimeArgs || [];
const programArgs = args.args || [];
let launchArgs = [runtimeExecutable];
if (!args.noDebug) {
launchArgs.push(`--inspect=${port}`);
// Always stop on entry to set breakpoints
launchArgs.push('--debug-brk');
}
this._continueAfterConfigDone = !args.stopOnEntry;
launchArgs = launchArgs.concat(runtimeArgs, program ? [program] : [], programArgs);
let launchP: Promise<void>;
if (args.console === 'integratedTerminal' || args.console === 'externalTerminal') {
const termArgs: DebugProtocol.RunInTerminalRequestArguments = {
kind: args.console === 'integratedTerminal' ? 'integrated' : 'external',
title: localize('node.console.title', "Node Debug Console"),
cwd,
args: launchArgs,
env: args.env
};
launchP = this.launchInTerminal(termArgs);
} else if (!args.console || args.console === 'internalConsole') {
// merge environment variables into a copy of the process.env
const env = Object.assign({}, process.env, args.env);
launchP = this.launchInInternalConsole(runtimeExecutable, launchArgs.slice(1), { cwd, env });
} else {
return Promise.reject(errors.unknownConsoleType(args.console));
}
return launchP
.then(() => {
return args.noDebug ?
Promise.resolve() :
this.doAttach(port, undefined, args.address, args.timeout);
});
});
}
public attach(args: AttachRequestArguments): Promise<void> {
args.sourceMapPathOverrides = getSourceMapPathOverrides(args.cwd, args.sourceMapPathOverrides);
this._restartMode = args.restart;
return super.attach(args);
}
protected doAttach(port: number, targetUrl?: string, address?: string, timeout?: number): Promise<void> {
return super.doAttach(port, targetUrl, address, timeout)
.then(() => {
this.beginWaitingForDebuggerPaused();
this.getNodeProcessDetailsIfNeeded();
});
}
private launchInTerminal(termArgs: DebugProtocol.RunInTerminalRequestArguments): Promise<void> {
return new Promise<void>((resolve, reject) => {
this._session.sendRequest('runInTerminal', termArgs, NodeDebugAdapter.RUNINTERMINAL_TIMEOUT, response => {
if (response.success) {
// since node starts in a terminal, we cannot track it with an 'exit' handler
// plan for polling after we have gotten the process pid.
this._pollForNodeProcess = true;
resolve();
} else {
reject(errors.cannotLaunchInTerminal(response.message));
this.terminateSession('terminal error: ' + response.message);
}
});
});
}
private launchInInternalConsole(runtimeExecutable: string, launchArgs: string[], spawnOpts?: cp.SpawnOptions): Promise<void> {
this.logLaunchCommand(runtimeExecutable, launchArgs);
const nodeProcess = cp.spawn(runtimeExecutable, launchArgs, spawnOpts);
return new Promise<void>((resolve, reject) => {
this._nodeProcessId = nodeProcess.pid;
nodeProcess.on('error', (error) => {
reject(errors.cannotLaunchDebugTarget(errors.toString()));
const msg = `Node process error: ${error}`;
logger.error(msg);
this.terminateSession(msg);
});
nodeProcess.on('exit', () => {
const msg = 'Target exited';
logger.log(msg);
this.terminateSession(msg);
});
nodeProcess.on('close', (code) => {
const msg = 'Target closed';
logger.log(msg);
this.terminateSession(msg);
});
const noDebugMode = (<LaunchRequestArguments>this._launchAttachArgs).noDebug;
// Listen to stderr at least until the debugger is attached
const onStderr = (data: string) => {
let msg = data.toString();
// Stop listening after 'Debugger attached' msg (unless this is noDebugMode)
if (!noDebugMode && msg.indexOf('Debugger attached.') >= 0) {
nodeProcess.stderr.removeListener('data', onStderr);
}
// Chop off the Chrome-specific debug URL message
const chromeMsgIndex = msg.indexOf('To start debugging, open the following URL in Chrome:');
if (chromeMsgIndex >= 0) {
msg = msg.substr(0, chromeMsgIndex);
}
this._session.sendEvent(new OutputEvent(msg, 'stderr'));
};
nodeProcess.stderr.on('data', onStderr);
// If only running, use stdout/stderr instead of debug protocol logs
if (noDebugMode) {
nodeProcess.stdout.on('data', (data: string) => {
let msg = data.toString();
this._session.sendEvent(new OutputEvent(msg, 'stdout'));
});
}
resolve();
});
}
/**
* Override so that -core's call on attach will be ignored, and we can wait until the first break when ready to set BPs.
*/
protected sendInitializedEvent(): void {
if (!this._waitingForEntryPauseEvent) {
super.sendInitializedEvent();
}
}
public configurationDone(): Promise<void> {
// This message means that all breakpoints have been set by the client. We should be paused at this point.
// So tell the target to continue, or tell the client that we paused, as needed
this._finishedConfig = true;
if (this._continueAfterConfigDone) {
this._expectingStopReason = undefined;
this.continue(/*internal=*/true);
} else if (this._entryPauseEvent) {
this.onPaused(this._entryPauseEvent);
}
return super.configurationDone();
}
private killNodeProcess(): void {
if (this._nodeProcessId && !this._attachMode) {
logger.log('Killing process with id: ' + this._nodeProcessId);
utils.killTree(this._nodeProcessId);
this._nodeProcessId = 0;
}
}
public terminateSession(reason: string): void {
const requestRestart = this._restartMode && !this._inShutdown;
this.killNodeProcess();
super.terminateSession(reason, requestRestart);
}
protected onPaused(notification: Crdp.Debugger.PausedEvent, expectingStopReason?: string): void {
// If we don't have the entry location, this must be the entry pause
if (this._waitingForEntryPauseEvent) {
logger.log('Paused on entry');
this._expectingStopReason = 'entry';
this._entryPauseEvent = notification;
this._waitingForEntryPauseEvent = false;
if (this._attachMode) {
// In attach mode, and we did pause right away,
// so assume --debug-brk was set and we should show paused
this._continueAfterConfigDone = false;
}
this.getNodeProcessDetailsIfNeeded()
.then(() => this.sendInitializedEvent());
} else {
super.onPaused(notification, expectingStopReason);
}
}
private resolveProgramPath(programPath: string, sourceMaps: boolean): Promise<string> {
return Promise.resolve().then(() => {
if (!programPath) {
return programPath;
}
if (utils.isJavaScript(programPath)) {
if (!sourceMaps) {
return programPath;
}
// if programPath is a JavaScript file and sourceMaps are enabled, we don't know whether
// programPath is the generated file or whether it is the source (and we need source mapping).
// Typically this happens if a tool like 'babel' or 'uglify' is used (because they both transpile js to js).
// We use the source maps to find a 'source' file for the given js file.
return this._sourceMapTransformer.getGeneratedPathFromAuthoredPath(programPath).then(generatedPath => {
if (generatedPath && generatedPath !== programPath) {
// programPath must be source because there seems to be a generated file for it
logger.log(`Launch: program '${programPath}' seems to be the source; launch the generated file '${generatedPath}' instead`);
programPath = generatedPath;
} else {
logger.log(`Launch: program '${programPath}' seems to be the generated file`);
}
return programPath;
});
} else {
// node cannot execute the program directly
if (!sourceMaps) {
return Promise.reject<string>(errors.cannotLaunchBecauseSourceMaps(programPath));
}
return this._sourceMapTransformer.getGeneratedPathFromAuthoredPath(programPath).then(generatedPath => {
if (!generatedPath) { // cannot find generated file
return Promise.reject<string>(errors.cannotLaunchBecauseOutFiles(programPath));
}
logger.log(`Launch: program '${programPath}' seems to be the source; launch the generated file '${generatedPath}' instead`);
return generatedPath;
});
}
});
}
/**
* Wait 500ms for the entry pause event, and if it doesn't come, move on with life.
* During attach, we don't know whether it's paused when attaching.
*/
private beginWaitingForDebuggerPaused(): void {
let count = 10;
const id = setInterval(() => {
if (this._entryPauseEvent || this._isTerminated) {
// Got the entry pause, stop waiting
clearInterval(id);
} else if (--count <= 0) {
// No entry event, so fake it and continue
logger.log('Did not get a pause event 500ms after starting, so continuing');
clearInterval(id);
this._continueAfterConfigDone = false;
this._waitingForEntryPauseEvent = false;
this.getNodeProcessDetailsIfNeeded()
.then(() => this.sendInitializedEvent());
}
}, 50);
}
/**
* Override addBreakpoints, which is called by setBreakpoints to make the actual call to Chrome.
*/
protected addBreakpoints(url: string, breakpoints: DebugProtocol.SourceBreakpoint[]): Promise<Crdp.Debugger.SetBreakpointResponse[]> {
return super.addBreakpoints(url, breakpoints).then(responses => {
if (this._entryPauseEvent && !this._finishedConfig) {
const entryLocation = this._entryPauseEvent.callFrames[0].location;
if (this._continueAfterConfigDone) {
const bpAtEntryLocation = responses.some(response => {
// Don't compare column location, because you can have a bp at col 0, then break at some other column
return response && response.actualLocation && response.actualLocation.lineNumber === entryLocation.lineNumber &&
response.actualLocation.scriptId === entryLocation.scriptId;
});
if (bpAtEntryLocation) {
// There is some initial breakpoint being set to the location where we stopped on entry, so need to pause even if
// the stopOnEntry flag is not set
logger.log('Got a breakpoint set in the entry location, so will stop even though stopOnEntry is not set');
this._continueAfterConfigDone = false;
this._expectingStopReason = 'breakpoint';
}
}
}
return responses;
});
}
private getNodeProcessDetailsIfNeeded(): Promise<void> {
if (this._loggedTargetVersion || !this.chrome) {
return Promise.resolve();
}
return this.chrome.Runtime.evaluate({ expression: '[process.pid, process.version, process.arch]', returnByValue: true, contextId: 1 }).then(response => {
if (response.exceptionDetails) {
const description = chromeUtils.errorMessageFromExceptionDetails(response.exceptionDetails);
if (description.startsWith('ReferenceError: process is not defined')) {
logger.verbose('Got expected exception: `process is not defined`. Will try again later.');
} else {
logger.log('Exception evaluating `process.pid`: ' + description + '. Will try again later.');
}
} else {
const value = response.result.value;
if (this._pollForNodeProcess) {
this._nodeProcessId = value[0];
this.startPollingForNodeTermination();
}
this._loggedTargetVersion = true;
logger.log(`Target node version: ${value[1]} ${value[2]}`);
}
},
error => logger.error('Error evaluating `process.pid`: ' + error.message));
}
private startPollingForNodeTermination(): void {
const intervalId = setInterval(() => {
try {
if (this._nodeProcessId) {
// kill with signal=0 just test for whether the proc is alive. It throws if not.
process.kill(this._nodeProcessId, 0);
} else {
clearInterval(intervalId);
}
} catch (e) {
clearInterval(intervalId);
logger.log('Target process died');
this.terminateSession('Target process died');
}
}, NodeDebugAdapter.NODE_TERMINATION_POLL_INTERVAL);
}
private logLaunchCommand(executable: string, args: string[]) {
// print the command to launch the target to the debug console
let cli = executable + ' ';
for (let a of args) {
if (a.indexOf(' ') >= 0) {
cli += '\'' + a + '\'';
} else {
cli += a;
}
cli += ' ';
}
this._session.sendEvent(new OutputEvent(cli + '\n', 'console'));
}
protected globalEvaluate(args: Crdp.Runtime.EvaluateRequest): Promise<Crdp.Runtime.EvaluateResponse> {
// contextId: 1 - see https://github.com/nodejs/node/issues/8426
if (!args.contextId) args.contextId = 1;
return super.globalEvaluate(args);
}
/**
* 'Path does not exist' error
*/
private getNotExistErrorResponse(attribute: string, path: string): Promise<void> {
return Promise.reject(<DebugProtocol.Message>{
id: 2007,
format: localize('attribute.path.not.exist', "Attribute '{0}' does not exist ('{1}').", attribute, '{path}'),
variables: { path }
});
}
/**
* 'Path not absolute' error with 'More Information' link.
*/
private getRelativePathErrorResponse(attribute: string, path: string): Promise<void> {
const format = localize('attribute.path.not.absolute', "Attribute '{0}' is not absolute ('{1}'); consider adding '{2}' as a prefix to make it absolute.", attribute, '{path}', '${workspaceRoot}/');
return this.getErrorResponseWithInfoLink(2008, format, { path }, 20003);
}
private getRuntimeNotOnPathErrorResponse(runtime: string): Promise<void> {
return Promise.reject(<DebugProtocol.Message>{
id: 2001,
format: localize('VSND2001', "Cannot find runtime '{0}' on PATH.", '{_runtime}'),
variables: { _runtime: runtime }
});
}
/**
* Send error response with 'More Information' link.
*/
private getErrorResponseWithInfoLink(code: number, format: string, variables: any, infoId: number): Promise<void> {
return Promise.reject(<DebugProtocol.Message>{
id: code,
format,
variables,
showUser: true,
url: 'http://go.microsoft.com/fwlink/?linkID=534832#_' + infoId.toString(),
urlLabel: localize('more.information', "More Information")
});
}
protected getReadonlyOrigin(aPath: string): string {
return path.isAbsolute(aPath) ?
localize('origin.from.node', "read-only content from Node.js") :
localize('origin.core.module', "read-only core module");
}
}
function getSourceMapPathOverrides(cwd: string, sourceMapPathOverrides?: ISourceMapPathOverrides): ISourceMapPathOverrides {
return sourceMapPathOverrides ? resolveCwdPattern(cwd, sourceMapPathOverrides, /*warnOnMissing=*/true) :
resolveCwdPattern(cwd, DefaultSourceMapPathOverrides, /*warnOnMissing=*/false);
}
/**
* Returns a copy of sourceMapPathOverrides with the ${cwd} pattern resolved in all entries.
*/
function resolveCwdPattern(cwd: string, sourceMapPathOverrides: ISourceMapPathOverrides, warnOnMissing: boolean): ISourceMapPathOverrides {
const resolvedOverrides: ISourceMapPathOverrides = {};
for (let pattern in sourceMapPathOverrides) {
const replacePattern = sourceMapPathOverrides[pattern];
resolvedOverrides[pattern] = replacePattern;
const cwdIndex = replacePattern.indexOf('${cwd}');
if (cwdIndex === 0) {
if (cwd) {
resolvedOverrides[pattern] = replacePattern.replace('${cwd}', cwd);
} else if (warnOnMissing) {
logger.log('Warning: sourceMapPathOverrides entry contains ${cwd}, but cwd is not set');
}
} else if (cwdIndex > 0) {
logger.log('Warning: in a sourceMapPathOverrides entry, ${cwd} is only valid at the beginning of the path');
}
}
return resolvedOverrides;
}