Path: blob/main/src/vs/workbench/contrib/debug/node/debugAdapter.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as cp from 'child_process';6import * as net from 'net';7import * as stream from 'stream';8import * as objects from '../../../../base/common/objects.js';9import * as path from '../../../../base/common/path.js';10import * as platform from '../../../../base/common/platform.js';11import * as strings from '../../../../base/common/strings.js';12import { Promises } from '../../../../base/node/pfs.js';13import * as nls from '../../../../nls.js';14import { IExtensionDescription } from '../../../../platform/extensions/common/extensions.js';15import { IDebugAdapterExecutable, IDebugAdapterNamedPipeServer, IDebugAdapterServer, IDebuggerContribution, IPlatformSpecificAdapterContribution } from '../common/debug.js';16import { AbstractDebugAdapter } from '../common/abstractDebugAdapter.js';17import { killTree } from '../../../../base/node/processes.js';1819/**20* An implementation that communicates via two streams with the debug adapter.21*/22export abstract class StreamDebugAdapter extends AbstractDebugAdapter {2324private static readonly TWO_CRLF = '\r\n\r\n';25private static readonly HEADER_LINESEPARATOR = /\r?\n/; // allow for non-RFC 2822 conforming line separators26private static readonly HEADER_FIELDSEPARATOR = /: */;2728private outputStream!: stream.Writable;29private rawData = Buffer.allocUnsafe(0);30private contentLength = -1;3132constructor() {33super();34}3536protected connect(readable: stream.Readable, writable: stream.Writable): void {3738this.outputStream = writable;39this.rawData = Buffer.allocUnsafe(0);40this.contentLength = -1;4142readable.on('data', (data: Buffer) => this.handleData(data));43}4445sendMessage(message: DebugProtocol.ProtocolMessage): void {4647if (this.outputStream) {48const json = JSON.stringify(message);49this.outputStream.write(`Content-Length: ${Buffer.byteLength(json, 'utf8')}${StreamDebugAdapter.TWO_CRLF}${json}`, 'utf8');50}51}5253private handleData(data: Buffer): void {5455this.rawData = Buffer.concat([this.rawData, data]);5657while (true) {58if (this.contentLength >= 0) {59if (this.rawData.length >= this.contentLength) {60const message = this.rawData.toString('utf8', 0, this.contentLength);61this.rawData = this.rawData.slice(this.contentLength);62this.contentLength = -1;63if (message.length > 0) {64try {65this.acceptMessage(<DebugProtocol.ProtocolMessage>JSON.parse(message));66} catch (e) {67this._onError.fire(new Error((e.message || e) + '\n' + message));68}69}70continue; // there may be more complete messages to process71}72} else {73const idx = this.rawData.indexOf(StreamDebugAdapter.TWO_CRLF);74if (idx !== -1) {75const header = this.rawData.toString('utf8', 0, idx);76const lines = header.split(StreamDebugAdapter.HEADER_LINESEPARATOR);77for (const h of lines) {78const kvPair = h.split(StreamDebugAdapter.HEADER_FIELDSEPARATOR);79if (kvPair[0] === 'Content-Length') {80this.contentLength = Number(kvPair[1]);81}82}83this.rawData = this.rawData.slice(idx + StreamDebugAdapter.TWO_CRLF.length);84continue;85}86}87break;88}89}90}9192export abstract class NetworkDebugAdapter extends StreamDebugAdapter {9394protected socket?: net.Socket;9596protected abstract createConnection(connectionListener: () => void): net.Socket;9798startSession(): Promise<void> {99return new Promise<void>((resolve, reject) => {100let connected = false;101102this.socket = this.createConnection(() => {103this.connect(this.socket!, this.socket!);104resolve();105connected = true;106});107108this.socket.on('close', () => {109if (connected) {110this._onError.fire(new Error('connection closed'));111} else {112reject(new Error('connection closed'));113}114});115116this.socket.on('error', error => {117// On ipv6 posix this can be an AggregateError which lacks a message. Use the first.118if (error instanceof AggregateError) {119error = error.errors[0];120}121122if (connected) {123this._onError.fire(error);124} else {125reject(error);126}127});128});129}130131async stopSession(): Promise<void> {132await this.cancelPendingRequests();133if (this.socket) {134this.socket.end();135this.socket = undefined;136}137}138}139140/**141* An implementation that connects to a debug adapter via a socket.142*/143export class SocketDebugAdapter extends NetworkDebugAdapter {144145constructor(private adapterServer: IDebugAdapterServer) {146super();147}148149protected createConnection(connectionListener: () => void): net.Socket {150return net.createConnection(this.adapterServer.port, this.adapterServer.host || '127.0.0.1', connectionListener);151}152}153154/**155* An implementation that connects to a debug adapter via a NamedPipe (on Windows)/UNIX Domain Socket (on non-Windows).156*/157export class NamedPipeDebugAdapter extends NetworkDebugAdapter {158159constructor(private adapterServer: IDebugAdapterNamedPipeServer) {160super();161}162163protected createConnection(connectionListener: () => void): net.Socket {164return net.createConnection(this.adapterServer.path, connectionListener);165}166}167168/**169* An implementation that launches the debug adapter as a separate process and communicates via stdin/stdout.170*/171export class ExecutableDebugAdapter extends StreamDebugAdapter {172173private serverProcess: cp.ChildProcess | undefined;174175constructor(private adapterExecutable: IDebugAdapterExecutable, private debugType: string) {176super();177}178179async startSession(): Promise<void> {180181const command = this.adapterExecutable.command;182const args = this.adapterExecutable.args;183const options = this.adapterExecutable.options || {};184185try {186// verify executables asynchronously187if (command) {188if (path.isAbsolute(command)) {189const commandExists = await Promises.exists(command);190if (!commandExists) {191throw new Error(nls.localize('debugAdapterBinNotFound', "Debug adapter executable '{0}' does not exist.", command));192}193} else {194// relative path195if (command.indexOf('/') < 0 && command.indexOf('\\') < 0) {196// no separators: command looks like a runtime name like 'node' or 'mono'197// TODO: check that the runtime is available on PATH198}199}200} else {201throw new Error(nls.localize({ key: 'debugAdapterCannotDetermineExecutable', comment: ['Adapter executable file not found'] },202"Cannot determine executable for debug adapter '{0}'.", this.debugType));203}204205let env = process.env;206if (options.env && Object.keys(options.env).length > 0) {207env = objects.mixin(objects.deepClone(process.env), options.env);208}209210if (command === 'node') {211if (Array.isArray(args) && args.length > 0) {212const isElectron = !!process.env['ELECTRON_RUN_AS_NODE'] || !!process.versions['electron'];213const forkOptions: cp.ForkOptions = {214env: env,215execArgv: isElectron ? ['-e', 'delete process.env.ELECTRON_RUN_AS_NODE;require(process.argv[1])'] : [],216silent: true217};218if (options.cwd) {219forkOptions.cwd = options.cwd;220}221const child = cp.fork(args[0], args.slice(1), forkOptions);222if (!child.pid) {223throw new Error(nls.localize('unableToLaunchDebugAdapter', "Unable to launch debug adapter from '{0}'.", args[0]));224}225this.serverProcess = child;226} else {227throw new Error(nls.localize('unableToLaunchDebugAdapterNoArgs', "Unable to launch debug adapter."));228}229} else {230let spawnCommand = command;231let spawnArgs = args;232const spawnOptions: cp.SpawnOptions = {233env: env234};235if (options.cwd) {236spawnOptions.cwd = options.cwd;237}238if (platform.isWindows && (command.endsWith('.bat') || command.endsWith('.cmd'))) {239// https://github.com/microsoft/vscode/issues/224184240spawnOptions.shell = true;241spawnCommand = `"${command}"`;242spawnArgs = args.map(a => {243a = a.replace(/"/g, '\\"'); // Escape existing double quotes with \244// Wrap in double quotes245return `"${a}"`;246});247}248249this.serverProcess = cp.spawn(spawnCommand, spawnArgs, spawnOptions);250}251252this.serverProcess.on('error', err => {253this._onError.fire(err);254});255this.serverProcess.on('exit', (code, signal) => {256this._onExit.fire(code);257});258259this.serverProcess.stdout!.on('close', () => {260this._onError.fire(new Error('read error'));261});262this.serverProcess.stdout!.on('error', error => {263this._onError.fire(error);264});265266this.serverProcess.stdin!.on('error', error => {267this._onError.fire(error);268});269270this.serverProcess.stderr!.resume();271272// finally connect to the DA273this.connect(this.serverProcess.stdout!, this.serverProcess.stdin!);274275} catch (err) {276this._onError.fire(err);277}278}279280async stopSession(): Promise<void> {281282if (!this.serverProcess) {283return Promise.resolve(undefined);284}285286// when killing a process in windows its child287// processes are *not* killed but become root288// processes. Therefore we use TASKKILL.EXE289await this.cancelPendingRequests();290if (platform.isWindows) {291return killTree(this.serverProcess!.pid!, true).catch(() => {292this.serverProcess?.kill();293});294} else {295this.serverProcess.kill('SIGTERM');296return Promise.resolve(undefined);297}298}299300private static extract(platformContribution: IPlatformSpecificAdapterContribution, extensionFolderPath: string): IDebuggerContribution | undefined {301if (!platformContribution) {302return undefined;303}304305const result: IDebuggerContribution = Object.create(null);306if (platformContribution.runtime) {307if (platformContribution.runtime.indexOf('./') === 0) { // TODO308result.runtime = path.join(extensionFolderPath, platformContribution.runtime);309} else {310result.runtime = platformContribution.runtime;311}312}313if (platformContribution.runtimeArgs) {314result.runtimeArgs = platformContribution.runtimeArgs;315}316if (platformContribution.program) {317if (!path.isAbsolute(platformContribution.program)) {318result.program = path.join(extensionFolderPath, platformContribution.program);319} else {320result.program = platformContribution.program;321}322}323if (platformContribution.args) {324result.args = platformContribution.args;325}326327const contribution = platformContribution as IDebuggerContribution;328329if (contribution.win) {330result.win = ExecutableDebugAdapter.extract(contribution.win, extensionFolderPath);331}332if (contribution.winx86) {333result.winx86 = ExecutableDebugAdapter.extract(contribution.winx86, extensionFolderPath);334}335if (contribution.windows) {336result.windows = ExecutableDebugAdapter.extract(contribution.windows, extensionFolderPath);337}338if (contribution.osx) {339result.osx = ExecutableDebugAdapter.extract(contribution.osx, extensionFolderPath);340}341if (contribution.linux) {342result.linux = ExecutableDebugAdapter.extract(contribution.linux, extensionFolderPath);343}344return result;345}346347static platformAdapterExecutable(extensionDescriptions: IExtensionDescription[], debugType: string): IDebugAdapterExecutable | undefined {348let result: IDebuggerContribution = Object.create(null);349debugType = debugType.toLowerCase();350351// merge all contributions into one352for (const ed of extensionDescriptions) {353if (ed.contributes) {354const debuggers = <IDebuggerContribution[]>ed.contributes['debuggers'];355if (debuggers && debuggers.length > 0) {356debuggers.filter(dbg => typeof dbg.type === 'string' && strings.equalsIgnoreCase(dbg.type, debugType)).forEach(dbg => {357// extract relevant attributes and make them absolute where needed358const extractedDbg = ExecutableDebugAdapter.extract(dbg, ed.extensionLocation.fsPath);359360// merge361result = objects.mixin(result, extractedDbg, ed.isBuiltin);362});363}364}365}366367// select the right platform368let platformInfo: IPlatformSpecificAdapterContribution | undefined;369if (platform.isWindows && !process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) {370platformInfo = result.winx86 || result.win || result.windows;371} else if (platform.isWindows) {372platformInfo = result.win || result.windows;373} else if (platform.isMacintosh) {374platformInfo = result.osx;375} else if (platform.isLinux) {376platformInfo = result.linux;377}378platformInfo = platformInfo || result;379380// these are the relevant attributes381const program = platformInfo.program || result.program;382const args = platformInfo.args || result.args;383const runtime = platformInfo.runtime || result.runtime;384const runtimeArgs = platformInfo.runtimeArgs || result.runtimeArgs;385386if (runtime) {387return {388type: 'executable',389command: runtime,390args: (runtimeArgs || []).concat(typeof program === 'string' ? [program] : []).concat(args || [])391};392} else if (program) {393return {394type: 'executable',395command: program,396args: args || []397};398}399400// nothing found401return undefined;402}403}404405406