Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/node/debugAdapter.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 cp from 'child_process';
7
import * as net from 'net';
8
import * as stream from 'stream';
9
import * as objects from '../../../../base/common/objects.js';
10
import * as path from '../../../../base/common/path.js';
11
import * as platform from '../../../../base/common/platform.js';
12
import * as strings from '../../../../base/common/strings.js';
13
import { Promises } from '../../../../base/node/pfs.js';
14
import * as nls from '../../../../nls.js';
15
import { IExtensionDescription } from '../../../../platform/extensions/common/extensions.js';
16
import { IDebugAdapterExecutable, IDebugAdapterNamedPipeServer, IDebugAdapterServer, IDebuggerContribution, IPlatformSpecificAdapterContribution } from '../common/debug.js';
17
import { AbstractDebugAdapter } from '../common/abstractDebugAdapter.js';
18
import { killTree } from '../../../../base/node/processes.js';
19
20
/**
21
* An implementation that communicates via two streams with the debug adapter.
22
*/
23
export abstract class StreamDebugAdapter extends AbstractDebugAdapter {
24
25
private static readonly TWO_CRLF = '\r\n\r\n';
26
private static readonly HEADER_LINESEPARATOR = /\r?\n/; // allow for non-RFC 2822 conforming line separators
27
private static readonly HEADER_FIELDSEPARATOR = /: */;
28
29
private outputStream!: stream.Writable;
30
private rawData = Buffer.allocUnsafe(0);
31
private contentLength = -1;
32
33
constructor() {
34
super();
35
}
36
37
protected connect(readable: stream.Readable, writable: stream.Writable): void {
38
39
this.outputStream = writable;
40
this.rawData = Buffer.allocUnsafe(0);
41
this.contentLength = -1;
42
43
readable.on('data', (data: Buffer) => this.handleData(data));
44
}
45
46
sendMessage(message: DebugProtocol.ProtocolMessage): void {
47
48
if (this.outputStream) {
49
const json = JSON.stringify(message);
50
this.outputStream.write(`Content-Length: ${Buffer.byteLength(json, 'utf8')}${StreamDebugAdapter.TWO_CRLF}${json}`, 'utf8');
51
}
52
}
53
54
private handleData(data: Buffer): void {
55
56
this.rawData = Buffer.concat([this.rawData, data]);
57
58
while (true) {
59
if (this.contentLength >= 0) {
60
if (this.rawData.length >= this.contentLength) {
61
const message = this.rawData.toString('utf8', 0, this.contentLength);
62
this.rawData = this.rawData.slice(this.contentLength);
63
this.contentLength = -1;
64
if (message.length > 0) {
65
try {
66
this.acceptMessage(<DebugProtocol.ProtocolMessage>JSON.parse(message));
67
} catch (e) {
68
this._onError.fire(new Error((e.message || e) + '\n' + message));
69
}
70
}
71
continue; // there may be more complete messages to process
72
}
73
} else {
74
const idx = this.rawData.indexOf(StreamDebugAdapter.TWO_CRLF);
75
if (idx !== -1) {
76
const header = this.rawData.toString('utf8', 0, idx);
77
const lines = header.split(StreamDebugAdapter.HEADER_LINESEPARATOR);
78
for (const h of lines) {
79
const kvPair = h.split(StreamDebugAdapter.HEADER_FIELDSEPARATOR);
80
if (kvPair[0] === 'Content-Length') {
81
this.contentLength = Number(kvPair[1]);
82
}
83
}
84
this.rawData = this.rawData.slice(idx + StreamDebugAdapter.TWO_CRLF.length);
85
continue;
86
}
87
}
88
break;
89
}
90
}
91
}
92
93
export abstract class NetworkDebugAdapter extends StreamDebugAdapter {
94
95
protected socket?: net.Socket;
96
97
protected abstract createConnection(connectionListener: () => void): net.Socket;
98
99
startSession(): Promise<void> {
100
return new Promise<void>((resolve, reject) => {
101
let connected = false;
102
103
this.socket = this.createConnection(() => {
104
this.connect(this.socket!, this.socket!);
105
resolve();
106
connected = true;
107
});
108
109
this.socket.on('close', () => {
110
if (connected) {
111
this._onError.fire(new Error('connection closed'));
112
} else {
113
reject(new Error('connection closed'));
114
}
115
});
116
117
this.socket.on('error', error => {
118
// On ipv6 posix this can be an AggregateError which lacks a message. Use the first.
119
if (error instanceof AggregateError) {
120
error = error.errors[0];
121
}
122
123
if (connected) {
124
this._onError.fire(error);
125
} else {
126
reject(error);
127
}
128
});
129
});
130
}
131
132
async stopSession(): Promise<void> {
133
await this.cancelPendingRequests();
134
if (this.socket) {
135
this.socket.end();
136
this.socket = undefined;
137
}
138
}
139
}
140
141
/**
142
* An implementation that connects to a debug adapter via a socket.
143
*/
144
export class SocketDebugAdapter extends NetworkDebugAdapter {
145
146
constructor(private adapterServer: IDebugAdapterServer) {
147
super();
148
}
149
150
protected createConnection(connectionListener: () => void): net.Socket {
151
return net.createConnection(this.adapterServer.port, this.adapterServer.host || '127.0.0.1', connectionListener);
152
}
153
}
154
155
/**
156
* An implementation that connects to a debug adapter via a NamedPipe (on Windows)/UNIX Domain Socket (on non-Windows).
157
*/
158
export class NamedPipeDebugAdapter extends NetworkDebugAdapter {
159
160
constructor(private adapterServer: IDebugAdapterNamedPipeServer) {
161
super();
162
}
163
164
protected createConnection(connectionListener: () => void): net.Socket {
165
return net.createConnection(this.adapterServer.path, connectionListener);
166
}
167
}
168
169
/**
170
* An implementation that launches the debug adapter as a separate process and communicates via stdin/stdout.
171
*/
172
export class ExecutableDebugAdapter extends StreamDebugAdapter {
173
174
private serverProcess: cp.ChildProcess | undefined;
175
176
constructor(private adapterExecutable: IDebugAdapterExecutable, private debugType: string) {
177
super();
178
}
179
180
async startSession(): Promise<void> {
181
182
const command = this.adapterExecutable.command;
183
const args = this.adapterExecutable.args;
184
const options = this.adapterExecutable.options || {};
185
186
try {
187
// verify executables asynchronously
188
if (command) {
189
if (path.isAbsolute(command)) {
190
const commandExists = await Promises.exists(command);
191
if (!commandExists) {
192
throw new Error(nls.localize('debugAdapterBinNotFound', "Debug adapter executable '{0}' does not exist.", command));
193
}
194
} else {
195
// relative path
196
if (command.indexOf('/') < 0 && command.indexOf('\\') < 0) {
197
// no separators: command looks like a runtime name like 'node' or 'mono'
198
// TODO: check that the runtime is available on PATH
199
}
200
}
201
} else {
202
throw new Error(nls.localize({ key: 'debugAdapterCannotDetermineExecutable', comment: ['Adapter executable file not found'] },
203
"Cannot determine executable for debug adapter '{0}'.", this.debugType));
204
}
205
206
let env = process.env;
207
if (options.env && Object.keys(options.env).length > 0) {
208
env = objects.mixin(objects.deepClone(process.env), options.env);
209
}
210
211
if (command === 'node') {
212
if (Array.isArray(args) && args.length > 0) {
213
const isElectron = !!process.env['ELECTRON_RUN_AS_NODE'] || !!process.versions['electron'];
214
const forkOptions: cp.ForkOptions = {
215
env: env,
216
execArgv: isElectron ? ['-e', 'delete process.env.ELECTRON_RUN_AS_NODE;require(process.argv[1])'] : [],
217
silent: true
218
};
219
if (options.cwd) {
220
forkOptions.cwd = options.cwd;
221
}
222
const child = cp.fork(args[0], args.slice(1), forkOptions);
223
if (!child.pid) {
224
throw new Error(nls.localize('unableToLaunchDebugAdapter', "Unable to launch debug adapter from '{0}'.", args[0]));
225
}
226
this.serverProcess = child;
227
} else {
228
throw new Error(nls.localize('unableToLaunchDebugAdapterNoArgs', "Unable to launch debug adapter."));
229
}
230
} else {
231
let spawnCommand = command;
232
let spawnArgs = args;
233
const spawnOptions: cp.SpawnOptions = {
234
env: env
235
};
236
if (options.cwd) {
237
spawnOptions.cwd = options.cwd;
238
}
239
if (platform.isWindows && (command.endsWith('.bat') || command.endsWith('.cmd'))) {
240
// https://github.com/microsoft/vscode/issues/224184
241
spawnOptions.shell = true;
242
spawnCommand = `"${command}"`;
243
spawnArgs = args.map(a => {
244
a = a.replace(/"/g, '\\"'); // Escape existing double quotes with \
245
// Wrap in double quotes
246
return `"${a}"`;
247
});
248
}
249
250
this.serverProcess = cp.spawn(spawnCommand, spawnArgs, spawnOptions);
251
}
252
253
this.serverProcess.on('error', err => {
254
this._onError.fire(err);
255
});
256
this.serverProcess.on('exit', (code, signal) => {
257
this._onExit.fire(code);
258
});
259
260
this.serverProcess.stdout!.on('close', () => {
261
this._onError.fire(new Error('read error'));
262
});
263
this.serverProcess.stdout!.on('error', error => {
264
this._onError.fire(error);
265
});
266
267
this.serverProcess.stdin!.on('error', error => {
268
this._onError.fire(error);
269
});
270
271
this.serverProcess.stderr!.resume();
272
273
// finally connect to the DA
274
this.connect(this.serverProcess.stdout!, this.serverProcess.stdin!);
275
276
} catch (err) {
277
this._onError.fire(err);
278
}
279
}
280
281
async stopSession(): Promise<void> {
282
283
if (!this.serverProcess) {
284
return Promise.resolve(undefined);
285
}
286
287
// when killing a process in windows its child
288
// processes are *not* killed but become root
289
// processes. Therefore we use TASKKILL.EXE
290
await this.cancelPendingRequests();
291
if (platform.isWindows) {
292
return killTree(this.serverProcess!.pid!, true).catch(() => {
293
this.serverProcess?.kill();
294
});
295
} else {
296
this.serverProcess.kill('SIGTERM');
297
return Promise.resolve(undefined);
298
}
299
}
300
301
private static extract(platformContribution: IPlatformSpecificAdapterContribution, extensionFolderPath: string): IDebuggerContribution | undefined {
302
if (!platformContribution) {
303
return undefined;
304
}
305
306
const result: IDebuggerContribution = Object.create(null);
307
if (platformContribution.runtime) {
308
if (platformContribution.runtime.indexOf('./') === 0) { // TODO
309
result.runtime = path.join(extensionFolderPath, platformContribution.runtime);
310
} else {
311
result.runtime = platformContribution.runtime;
312
}
313
}
314
if (platformContribution.runtimeArgs) {
315
result.runtimeArgs = platformContribution.runtimeArgs;
316
}
317
if (platformContribution.program) {
318
if (!path.isAbsolute(platformContribution.program)) {
319
result.program = path.join(extensionFolderPath, platformContribution.program);
320
} else {
321
result.program = platformContribution.program;
322
}
323
}
324
if (platformContribution.args) {
325
result.args = platformContribution.args;
326
}
327
328
const contribution = platformContribution as IDebuggerContribution;
329
330
if (contribution.win) {
331
result.win = ExecutableDebugAdapter.extract(contribution.win, extensionFolderPath);
332
}
333
if (contribution.winx86) {
334
result.winx86 = ExecutableDebugAdapter.extract(contribution.winx86, extensionFolderPath);
335
}
336
if (contribution.windows) {
337
result.windows = ExecutableDebugAdapter.extract(contribution.windows, extensionFolderPath);
338
}
339
if (contribution.osx) {
340
result.osx = ExecutableDebugAdapter.extract(contribution.osx, extensionFolderPath);
341
}
342
if (contribution.linux) {
343
result.linux = ExecutableDebugAdapter.extract(contribution.linux, extensionFolderPath);
344
}
345
return result;
346
}
347
348
static platformAdapterExecutable(extensionDescriptions: IExtensionDescription[], debugType: string): IDebugAdapterExecutable | undefined {
349
let result: IDebuggerContribution = Object.create(null);
350
debugType = debugType.toLowerCase();
351
352
// merge all contributions into one
353
for (const ed of extensionDescriptions) {
354
if (ed.contributes) {
355
const debuggers = <IDebuggerContribution[]>ed.contributes['debuggers'];
356
if (debuggers && debuggers.length > 0) {
357
debuggers.filter(dbg => typeof dbg.type === 'string' && strings.equalsIgnoreCase(dbg.type, debugType)).forEach(dbg => {
358
// extract relevant attributes and make them absolute where needed
359
const extractedDbg = ExecutableDebugAdapter.extract(dbg, ed.extensionLocation.fsPath);
360
361
// merge
362
result = objects.mixin(result, extractedDbg, ed.isBuiltin);
363
});
364
}
365
}
366
}
367
368
// select the right platform
369
let platformInfo: IPlatformSpecificAdapterContribution | undefined;
370
if (platform.isWindows && !process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) {
371
platformInfo = result.winx86 || result.win || result.windows;
372
} else if (platform.isWindows) {
373
platformInfo = result.win || result.windows;
374
} else if (platform.isMacintosh) {
375
platformInfo = result.osx;
376
} else if (platform.isLinux) {
377
platformInfo = result.linux;
378
}
379
platformInfo = platformInfo || result;
380
381
// these are the relevant attributes
382
const program = platformInfo.program || result.program;
383
const args = platformInfo.args || result.args;
384
const runtime = platformInfo.runtime || result.runtime;
385
const runtimeArgs = platformInfo.runtimeArgs || result.runtimeArgs;
386
387
if (runtime) {
388
return {
389
type: 'executable',
390
command: runtime,
391
args: (runtimeArgs || []).concat(typeof program === 'string' ? [program] : []).concat(args || [])
392
};
393
} else if (program) {
394
return {
395
type: 'executable',
396
command: program,
397
args: args || []
398
};
399
}
400
401
// nothing found
402
return undefined;
403
}
404
}
405
406