diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index f109d0b1aba..5864a2b631c 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -10899,6 +10899,21 @@ declare module 'vscode' { constructor(port: number, host?: string); } + /** + * Represents a debug adapter running as a Named Pipe (on Windows)/UNIX Domain Socket (on non-Windows) based server. + */ + export class DebugAdapterNamedPipeServer { + /** + * The path to the NamedPipe/UNIX Domain Socket. + */ + readonly path: string; + + /** + * Create a description for a debug adapter running as a socket based server. + */ + constructor(path: string); + } + /** * A debug adapter that implements the Debug Adapter Protocol can be registered with VS Code if it implements the DebugAdapter interface. */ @@ -10937,7 +10952,7 @@ declare module 'vscode' { constructor(implementation: DebugAdapter); } - export type DebugAdapterDescriptor = DebugAdapterExecutable | DebugAdapterServer | DebugAdapterInlineImplementation; + export type DebugAdapterDescriptor = DebugAdapterExecutable | DebugAdapterServer | DebugAdapterNamedPipeServer | DebugAdapterInlineImplementation; export interface DebugAdapterDescriptorFactory { /** diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 2d2a16538e9..f697d498e35 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1019,6 +1019,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ConfigurationTarget: extHostTypes.ConfigurationTarget, DebugAdapterExecutable: extHostTypes.DebugAdapterExecutable, DebugAdapterServer: extHostTypes.DebugAdapterServer, + DebugAdapterNamedPipeServer: extHostTypes.DebugAdapterNamedPipeServer, DebugAdapterInlineImplementation: extHostTypes.DebugAdapterInlineImplementation, DecorationRangeBehavior: extHostTypes.DecorationRangeBehavior, Diagnostic: extHostTypes.Diagnostic, diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index fcf987ff946..1e1643ee414 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -11,12 +11,12 @@ import { MainContext, MainThreadDebugServiceShape, ExtHostDebugServiceShape, DebugSessionUUID, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto } from 'vs/workbench/api/common/extHost.protocol'; -import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable, DataBreakpoint, DebugConsoleMode, DebugAdapterInlineImplementation } from 'vs/workbench/api/common/extHostTypes'; +import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable, DataBreakpoint, DebugConsoleMode, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer } from 'vs/workbench/api/common/extHostTypes'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { ExtHostDocumentsAndEditors, IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { IDebuggerContribution, IConfig, IDebugAdapter, IDebugAdapterServer, IDebugAdapterExecutable, IAdapterDescriptor, IDebugAdapterImpl } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebuggerContribution, IConfig, IDebugAdapter, IDebugAdapterServer, IDebugAdapterExecutable, IAdapterDescriptor, IDebugAdapterImpl, IDebugAdapterNamedPipeServer } from 'vs/workbench/contrib/debug/common/debug'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration'; @@ -737,6 +737,11 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E port: x.port, host: x.host }; + } else if (x instanceof DebugAdapterNamedPipeServer) { + return { + type: 'pipeServer', + path: x.path + }; } else if (x instanceof DebugAdapterInlineImplementation) { return { type: 'implementation', diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 4ba62a170c5..db91ee1e124 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2294,6 +2294,12 @@ export class DebugAdapterServer implements vscode.DebugAdapterServer { } } +@es5ClassCompat +export class DebugAdapterNamedPipeServer implements vscode.DebugAdapterNamedPipeServer { + constructor(public readonly path: string) { + } +} + @es5ClassCompat export class DebugAdapterInlineImplementation implements vscode.DebugAdapterInlineImplementation { readonly implementation: vscode.DebugAdapter; diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 7d3cc3b33c3..4206b06776a 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import type * as vscode from 'vscode'; import * as env from 'vs/base/common/platform'; import { DebugAdapterExecutable } from 'vs/workbench/api/common/extHostTypes'; -import { ExecutableDebugAdapter, SocketDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; +import { ExecutableDebugAdapter, SocketDebugAdapter, NamedPipeDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; @@ -49,6 +49,8 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { switch (adapter.type) { case 'server': return new SocketDebugAdapter(adapter); + case 'pipeServer': + return new NamedPipeDebugAdapter(adapter); case 'executable': return new ExecutableDebugAdapter(adapter, session.type); } diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 3a4a200b641..702a54bde49 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -573,6 +573,11 @@ export interface IDebugAdapterServer { readonly host?: string; } +export interface IDebugAdapterNamedPipeServer { + readonly type: 'pipeServer'; + readonly path: string; +} + export interface IDebugAdapterInlineImpl extends IDisposable { readonly onDidSendMessage: Event; handleMessage(message: DebugProtocol.Message): void; @@ -583,7 +588,7 @@ export interface IDebugAdapterImpl { readonly implementation: IDebugAdapterInlineImpl; } -export type IAdapterDescriptor = IDebugAdapterExecutable | IDebugAdapterServer | IDebugAdapterImpl; +export type IAdapterDescriptor = IDebugAdapterExecutable | IDebugAdapterServer | IDebugAdapterNamedPipeServer | IDebugAdapterImpl; export interface IPlatformSpecificAdapterContribution { program?: string; diff --git a/src/vs/workbench/contrib/debug/node/debugAdapter.ts b/src/vs/workbench/contrib/debug/node/debugAdapter.ts index 70ddc23e1b5..dc6d941a068 100644 --- a/src/vs/workbench/contrib/debug/node/debugAdapter.ts +++ b/src/vs/workbench/contrib/debug/node/debugAdapter.ts @@ -14,7 +14,7 @@ import * as objects from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; import { ExtensionsChannelId } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IOutputService } from 'vs/workbench/contrib/output/common/output'; -import { IDebugAdapterExecutable, IDebuggerContribution, IPlatformSpecificAdapterContribution, IDebugAdapterServer } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugAdapterExecutable, IDebuggerContribution, IPlatformSpecificAdapterContribution, IDebugAdapterServer, IDebugAdapterNamedPipeServer } from 'vs/workbench/contrib/debug/common/debug'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { AbstractDebugAdapter } from '../common/abstractDebugAdapter'; @@ -91,25 +91,22 @@ export abstract class StreamDebugAdapter extends AbstractDebugAdapter { } } -/** - * An implementation that connects to a debug adapter via a socket. -*/ -export class SocketDebugAdapter extends StreamDebugAdapter { +export abstract class NetworkDebugAdapter extends StreamDebugAdapter { - private socket?: net.Socket; + protected socket?: net.Socket; - constructor(private adapterServer: IDebugAdapterServer) { - super(); - } + protected abstract createConnection(connectionListener: () => void): net.Socket; startSession(): Promise { return new Promise((resolve, reject) => { let connected = false; - this.socket = net.createConnection(this.adapterServer.port, this.adapterServer.host || '127.0.0.1', () => { + + 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')); @@ -117,6 +114,7 @@ export class SocketDebugAdapter extends StreamDebugAdapter { reject(new Error('connection closed')); } }); + this.socket.on('error', error => { if (connected) { this._onError.fire(error); @@ -136,6 +134,34 @@ export class SocketDebugAdapter extends StreamDebugAdapter { } } +/** + * An implementation that connects to a debug adapter via a socket. +*/ +export class SocketDebugAdapter extends NetworkDebugAdapter { + + constructor(private adapterServer: IDebugAdapterServer) { + super(); + } + + protected createConnection(connectionListener: () => void): net.Socket { + return net.createConnection(this.adapterServer.port, this.adapterServer.host || '127.0.0.1', connectionListener); + } +} + +/** + * 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) { + super(); + } + + protected createConnection(connectionListener: () => void): net.Socket { + return net.createConnection(this.adapterServer.path, connectionListener); + } +} + /** * An implementation that launches the debug adapter as a separate process and communicates via stdin/stdout. */ diff --git a/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts new file mode 100644 index 00000000000..52977226fa5 --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as crypto from 'crypto'; +import * as net from 'net'; +import * as platform from 'vs/base/common/platform'; +import { tmpdir } from 'os'; +import { join } from 'vs/base/common/path'; +import { SocketDebugAdapter, NamedPipeDebugAdapter, StreamDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; + +function rndPort(): number { + const min = 8000; + const max = 9000; + return Math.floor(Math.random() * (max - min) + min); +} + +function sendInitializeRequest(debugAdapter: StreamDebugAdapter): Promise { + return new Promise((resolve, reject) => { + debugAdapter.sendRequest('initialize', { adapterID: 'test' }, (result) => { + resolve(result); + }); + }); +} + +function serverConnection(socket: net.Socket) { + socket.on('data', (data: Buffer) => { + const str = data.toString().split('\r\n')[2]; + const request = JSON.parse(str); + const response: any = { + seq: request.seq, + request_seq: request.seq, + type: 'response', + command: request.command + }; + if (request.arguments.adapterID === 'test') { + response.success = true; + } else { + response.success = false; + response.message = 'failed'; + } + + const responsePayload = JSON.stringify(response); + socket.write(`Content-Length: ${responsePayload.length}\r\n\r\n${responsePayload}`); + }); +} + +suite('Debug - StreamDebugAdapter', () => { + const port = rndPort(); + const pipeName = crypto.randomBytes(10).toString('utf8'); + const pipePath = platform.isWindows ? join('\\\\.\\pipe\\', pipeName) : join(tmpdir(), pipeName); + + const testCases: { testName: string, debugAdapter: StreamDebugAdapter, connectionDetail: string | number }[] = [ + { + testName: 'NamedPipeDebugAdapter', + debugAdapter: new NamedPipeDebugAdapter({ + type: 'pipeServer', + path: pipePath + }), + connectionDetail: pipePath + }, + { + testName: 'SocketDebugAdapter', + debugAdapter: new SocketDebugAdapter({ + type: 'server', + port + }), + connectionDetail: port + } + ]; + + for (const testCase of testCases) { + test(`StreamDebugAdapter (${testCase.testName}) can initialize a connection`, async () => { + const server = net.createServer(serverConnection).listen(testCase.connectionDetail); + const debugAdapter = testCase.debugAdapter; + try { + await debugAdapter.startSession(); + const response: DebugProtocol.Response = await sendInitializeRequest(debugAdapter); + assert.strictEqual(response.command, 'initialize'); + assert.strictEqual(response.request_seq, 1); + assert.strictEqual(response.success, true, response.message); + } finally { + await debugAdapter.stopSession(); + server.close(); + debugAdapter.dispose(); + } + }); + } +});