src/vs/workbench/contrib/debug/node/debugAdapter.ts
404 LOC · 238 covered · 166 uncovered · 45 ranges · 9 concepts · 5 introducers · 5 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
debugAdapter.ts ×16
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as cp from 'child_process';
import * as net from 'net';
import * as stream from 'stream';
import * as objects from '../../../../base/common/objects.js';
import * as path from '../../../../base/common/path.js';
import * as platform from '../../../../base/common/platform.js';
import * as strings from '../../../../base/common/strings.js';
import { Promises } from '../../../../base/node/pfs.js';
import * as nls from '../../../../nls.js';
import { IExtensionDescription } from '../../../../platform/extensions/common/extensions.js';
import { IDebugAdapterExecutable, IDebugAdapterNamedPipeServer, IDebugAdapterServer, IDebuggerContribution, IPlatformSpecificAdapterContribution } from '../common/debug.js';
import { AbstractDebugAdapter } from '../common/abstractDebugAdapter.js';
import { killTree } from '../../../../base/node/processes.js';
/**
* An implementation that communicates via two streams with the debug adapter.
*/
export abstract class StreamDebugAdapter extends AbstractDebugAdapter {
private static readonly TWO_CRLF = '\r\n\r\n';
private static readonly HEADER_LINESEPARATOR = /\r?\n/; // allow for non-RFC 2822 conforming line separators
private static readonly HEADER_FIELDSEPARATOR = /: */;
private outputStream!: stream.Writable;
private rawData = Buffer.allocUnsafe(0);
private contentLength = -1;
constructor() {
}
protected connect(readable: stream.Readable, writable: stream.Writable): void {
this.outputStream = writable;
this.rawData = Buffer.allocUnsafe(0);
this.contentLength = -1;
readable.on('data', (data: Buffer) => this.handleData(data));
}
sendMessage(message: DebugProtocol.ProtocolMessage): void {
if (this.outputStream) {
const json = JSON.stringify(message);
this.outputStream.write(`Content-Length: ${Buffer.byteLength(json, 'utf8')}${StreamDebugAdapter.TWO_CRLF}${json}`, 'utf8');
}
}
private handleData(data: Buffer): void {
this.rawData = Buffer.concat([this.rawData, data]);
while (true) {
if (this.contentLength >= 0) {
if (this.rawData.length >= this.contentLength) {
const message = this.rawData.toString('utf8', 0, this.contentLength);
this.rawData = this.rawData.slice(this.contentLength);
this.contentLength = -1;
if (message.length > 0) {
try {
this.acceptMessage(<DebugProtocol.ProtocolMessage>JSON.parse(message));
} catch (e) {
this._onError.fire(new Error((e.message || e) + '\n' + message));
}
continue; // there may be more complete messages to process
}
} else {
const idx = this.rawData.indexOf(StreamDebugAdapter.TWO_CRLF);
if (idx !== -1) {
const header = this.rawData.toString('utf8', 0, idx);
const lines = header.split(StreamDebugAdapter.HEADER_LINESEPARATOR);
for (const h of lines) {
const kvPair = h.split(StreamDebugAdapter.HEADER_FIELDSEPARATOR);
if (kvPair[0] === 'Content-Length') {
this.contentLength = Number(kvPair[1]);
}
}
this.rawData = this.rawData.slice(idx + StreamDebugAdapter.TWO_CRLF.length);
continue;
}
}
break;
}
}
export abstract class NetworkDebugAdapter extends StreamDebugAdapter {
protected socket?: net.Socket;
protected abstract createConnection(connectionListener: () => void): net.Socket;
startSession(): Promise<void> {
let connected = false;
this.socket = this.createConnection(() => {
this.connect(this.socket!, this.socket!);
resolve();
connected = true;
});
this.socket.on('close', () => {
if (connected) {
this._onError.fire(new Error('connection closed'));
} else {
reject(new Error('connection closed'));
}
this.socket.on('error', error => {
// On ipv6 posix this can be an AggregateError which lacks a message. Use the first.
if (error instanceof AggregateError) {
error = error.errors[0];
}
if (connected) {
this._onError.fire(error);
} else {
reject(error);
}
});
}
async stopSession(): Promise<void> {
if (this.socket) {
this.socket.end();
this.socket = undefined;
}
}
/**
* An implementation that connects to a debug adapter via a socket.
*/
export class SocketDebugAdapter extends NetworkDebugAdapter {
constructor(private adapterServer: IDebugAdapterServer) {
}
protected createConnection(connectionListener: () => void): net.Socket {
return net.createConnection(this.adapterServer.port, this.adapterServer.host || '127.0.0.1', connectionListener);
debugAdapter.ts ×2
}
/**
* An implementation that connects to a debug adapter via a NamedPipe (on Windows)/UNIX Domain Socket (on non-Windows).
*/
export class NamedPipeDebugAdapter extends NetworkDebugAdapter {
constructor(private adapterServer: IDebugAdapterNamedPipeServer) {
}
protected createConnection(connectionListener: () => void): net.Socket {
}
/**
* An implementation that launches the debug adapter as a separate process and communicates via stdin/stdout.
*/
export class ExecutableDebugAdapter extends StreamDebugAdapter {
private serverProcess: cp.ChildProcess | undefined;
constructor(private adapterExecutable: IDebugAdapterExecutable, private debugType: string) {
super();
}
async startSession(): Promise<void> {
const command = this.adapterExecutable.command;
const args = this.adapterExecutable.args;
const options = this.adapterExecutable.options || {};
try {
// verify executables asynchronously
if (command) {
if (path.isAbsolute(command)) {
const commandExists = await Promises.exists(command);
if (!commandExists) {
throw new Error(nls.localize('debugAdapterBinNotFound', "Debug adapter executable '{0}' does not exist.", command));
}
} else {
// relative path
if (command.indexOf('/') < 0 && command.indexOf('\\') < 0) {
// no separators: command looks like a runtime name like 'node' or 'mono'
// TODO: check that the runtime is available on PATH
}
}
} else {
throw new Error(nls.localize({ key: 'debugAdapterCannotDetermineExecutable', comment: ['Adapter executable file not found'] },
"Cannot determine executable for debug adapter '{0}'.", this.debugType));
}
let env = process.env;
if (options.env && Object.keys(options.env).length > 0) {
env = objects.mixin(objects.deepClone(process.env), options.env);
}
if (command === 'node') {
if (Array.isArray(args) && args.length > 0) {
const isElectron = !!process.env['ELECTRON_RUN_AS_NODE'] || !!process.versions['electron'];
const forkOptions: cp.ForkOptions = {
env: env,
execArgv: isElectron ? ['-e', 'delete process.env.ELECTRON_RUN_AS_NODE;require(process.argv[1])'] : [],
silent: true
};
if (options.cwd) {
forkOptions.cwd = options.cwd;
}
const child = cp.fork(args[0], args.slice(1), forkOptions);
if (!child.pid) {
throw new Error(nls.localize('unableToLaunchDebugAdapter', "Unable to launch debug adapter from '{0}'.", args[0]));
}
this.serverProcess = child;
} else {
throw new Error(nls.localize('unableToLaunchDebugAdapterNoArgs', "Unable to launch debug adapter."));
}
} else {
let spawnCommand = command;
let spawnArgs = args;
const spawnOptions: cp.SpawnOptions = {
env: env
};
if (options.cwd) {
spawnOptions.cwd = options.cwd;
}
if (platform.isWindows && (command.endsWith('.bat') || command.endsWith('.cmd'))) {
// https://github.com/microsoft/vscode/issues/224184
spawnOptions.shell = true;
spawnCommand = `"${command}"`;
spawnArgs = args.map(a => {
a = a.replace(/"/g, '\\"'); // Escape existing double quotes with \
// Wrap in double quotes
return `"${a}"`;
});
}
this.serverProcess = cp.spawn(spawnCommand, spawnArgs, spawnOptions);
}
this.serverProcess.on('error', err => {
this._onError.fire(err);
});
this.serverProcess.on('exit', (code, signal) => {
this._onExit.fire(code);
});
this.serverProcess.stdout!.on('close', () => {
this._onError.fire(new Error('read error'));
});
this.serverProcess.stdout!.on('error', error => {
this._onError.fire(error);
});
this.serverProcess.stdin!.on('error', error => {
this._onError.fire(error);
});
this.serverProcess.stderr!.resume();
// finally connect to the DA
this.connect(this.serverProcess.stdout!, this.serverProcess.stdin!);
} catch (err) {
this._onError.fire(err);
}
}
async stopSession(): Promise<void> {
if (!this.serverProcess) {
return Promise.resolve(undefined);
}
// when killing a process in windows its child
// processes are *not* killed but become root
// processes. Therefore we use TASKKILL.EXE
await this.cancelPendingRequests();
if (platform.isWindows) {
return killTree(this.serverProcess!.pid!, true).catch(() => {
this.serverProcess?.kill();
});
} else {
this.serverProcess.kill('SIGTERM');
return Promise.resolve(undefined);
}
}
private static extract(platformContribution: IPlatformSpecificAdapterContribution, extensionFolderPath: string): IDebuggerContribution | undefined {
return undefined;
}
const result: IDebuggerContribution = Object.create(null);
if (platformContribution.runtime) {
if (platformContribution.runtime.indexOf('./') === 0) { // TODO
result.runtime = path.join(extensionFolderPath, platformContribution.runtime);
} else {
result.runtime = platformContribution.runtime;
}
}
result.runtimeArgs = platformContribution.runtimeArgs;
}
if (!path.isAbsolute(platformContribution.program)) {
result.program = path.join(extensionFolderPath, platformContribution.program);
} else {
result.program = platformContribution.program;
}
if (platformContribution.args) {
result.args = platformContribution.args;
}
const contribution = platformContribution as IDebuggerContribution;
if (contribution.win) {
result.win = ExecutableDebugAdapter.extract(contribution.win, extensionFolderPath);
}
result.winx86 = ExecutableDebugAdapter.extract(contribution.winx86, extensionFolderPath);
}
result.windows = ExecutableDebugAdapter.extract(contribution.windows, extensionFolderPath);
}
result.osx = ExecutableDebugAdapter.extract(contribution.osx, extensionFolderPath);
}
result.linux = ExecutableDebugAdapter.extract(contribution.linux, extensionFolderPath);
}
}
static platformAdapterExecutable(extensionDescriptions: IExtensionDescription[], debugType: string): IDebugAdapterExecutable | undefined {
debugType = debugType.toLowerCase();
// merge all contributions into one
for (const ed of extensionDescriptions) {
if (ed.contributes) {
const debuggers = <IDebuggerContribution[]>ed.contributes['debuggers'];
if (debuggers && debuggers.length > 0) {
debuggers.filter(dbg => typeof dbg.type === 'string' && strings.equalsIgnoreCase(dbg.type, debugType)).forEach(dbg => {
// extract relevant attributes and make them absolute where needed
const extractedDbg = ExecutableDebugAdapter.extract(dbg, ed.extensionLocation.fsPath);
// merge
result = objects.mixin(result, extractedDbg, ed.isBuiltin);
});
}
}
}
// select the right platform
let platformInfo: IPlatformSpecificAdapterContribution | undefined;
if (platform.isWindows && !process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) {
platformInfo = result.winx86 || result.win || result.windows;
platformInfo = result.win || result.windows;
platformInfo = result.osx;
platformInfo = result.linux;
}
platformInfo = platformInfo || result;
// these are the relevant attributes
const program = platformInfo.program || result.program;
const args = platformInfo.args || result.args;
const runtime = platformInfo.runtime || result.runtime;
const runtimeArgs = platformInfo.runtimeArgs || result.runtimeArgs;
if (runtime) {
return {
type: 'executable',
command: runtime,
args: (runtimeArgs || []).concat(typeof program === 'string' ? [program] : []).concat(args || [])
};
return {
type: 'executable',
command: program,
args: args || []
};
}
// nothing found
return undefined;