From 10a5d09ed518776df2f5f8df5ad851f76def0aee Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 23 Jul 2020 15:40:13 -0700 Subject: [PATCH 1/4] Add DebugAdapterNamedPipeServer --- src/vs/vscode.d.ts | 15 +++ .../workbench/api/common/extHost.api.impl.ts | 1 + .../api/common/extHostDebugService.ts | 9 +- src/vs/workbench/api/common/extHostTypes.ts | 6 ++ .../workbench/api/node/extHostDebugService.ts | 4 +- .../workbench/contrib/debug/common/debug.ts | 7 +- .../contrib/debug/node/debugAdapter.ts | 47 +++++++++- .../test/node/streamDebugAdapter.test.ts | 94 +++++++++++++++++++ 8 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index efd2b3b06e4..8df7d0ef548 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -10790,6 +10790,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. */ diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 036366842bf..db0d8cdd2c0 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1016,6 +1016,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 f9dcbecccef..3d302655fb1 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 447fa8092c6..66deb3afe2b 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2288,6 +2288,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 620831e332c..ce399761320 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -565,6 +565,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; @@ -575,7 +580,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..659e03c1a06 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'; @@ -136,6 +136,51 @@ export class SocketDebugAdapter extends StreamDebugAdapter { } } +/** + * An implementation that connects to a debug adapter via a NamedPipe (on Windows)/UNIX Domain Socket (on non-Windows). + */ +export class NamedPipeDebugAdapter extends StreamDebugAdapter { + + private socket?: net.Socket; + + constructor(private adapterServer: IDebugAdapterNamedPipeServer) { + super(); + } + + startSession(): Promise { + return new Promise((resolve, reject) => { + let connected = false; + this.socket = net.createConnection(this.adapterServer.path, () => { + 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 => { + if (connected) { + this._onError.fire(error); + } else { + reject(error); + } + }); + }); + } + + async stopSession(): Promise { + await this.cancelPendingRequests(); + if (this.socket) { + this.socket.end(); + this.socket = undefined; + } + } +} + /** * 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..1564c3d22f8 --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * 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 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 rndName(): string { + return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10); +} + +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 = rndName(); + 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(); + } + }); + } +}); From 0b103238b73423655376d5280005e42e13f1be57 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 23 Jul 2020 16:53:11 -0700 Subject: [PATCH 2/4] refactor to abstract class and use crypto.randomBytes --- .../contrib/debug/node/debugAdapter.ts | 39 ++++++++----------- .../test/node/streamDebugAdapter.test.ts | 7 +--- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/contrib/debug/node/debugAdapter.ts b/src/vs/workbench/contrib/debug/node/debugAdapter.ts index 659e03c1a06..debe9451071 100644 --- a/src/vs/workbench/contrib/debug/node/debugAdapter.ts +++ b/src/vs/workbench/contrib/debug/node/debugAdapter.ts @@ -91,12 +91,25 @@ export abstract class StreamDebugAdapter extends AbstractDebugAdapter { } } +export abstract class NetworkDebugAdapter extends StreamDebugAdapter { + + protected socket?: net.Socket; + + abstract startSession(): Promise; + + async stopSession(): Promise { + await this.cancelPendingRequests(); + if (this.socket) { + this.socket.end(); + this.socket = undefined; + } + } +} + /** * An implementation that connects to a debug adapter via a socket. */ -export class SocketDebugAdapter extends StreamDebugAdapter { - - private socket?: net.Socket; +export class SocketDebugAdapter extends NetworkDebugAdapter { constructor(private adapterServer: IDebugAdapterServer) { super(); @@ -126,22 +139,12 @@ export class SocketDebugAdapter extends StreamDebugAdapter { }); }); } - - async stopSession(): Promise { - await this.cancelPendingRequests(); - if (this.socket) { - this.socket.end(); - this.socket = undefined; - } - } } /** * An implementation that connects to a debug adapter via a NamedPipe (on Windows)/UNIX Domain Socket (on non-Windows). */ -export class NamedPipeDebugAdapter extends StreamDebugAdapter { - - private socket?: net.Socket; +export class NamedPipeDebugAdapter extends NetworkDebugAdapter { constructor(private adapterServer: IDebugAdapterNamedPipeServer) { super(); @@ -171,14 +174,6 @@ export class NamedPipeDebugAdapter extends StreamDebugAdapter { }); }); } - - async stopSession(): Promise { - await this.cancelPendingRequests(); - if (this.socket) { - this.socket.end(); - this.socket = undefined; - } - } } /** diff --git a/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts index 1564c3d22f8..52977226fa5 100644 --- a/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 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'; @@ -16,10 +17,6 @@ function rndPort(): number { return Math.floor(Math.random() * (max - min) + min); } -function rndName(): string { - return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10); -} - function sendInitializeRequest(debugAdapter: StreamDebugAdapter): Promise { return new Promise((resolve, reject) => { debugAdapter.sendRequest('initialize', { adapterID: 'test' }, (result) => { @@ -52,7 +49,7 @@ function serverConnection(socket: net.Socket) { suite('Debug - StreamDebugAdapter', () => { const port = rndPort(); - const pipeName = rndName(); + 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 }[] = [ From f8cf2fce66ff93bc1810c8403e33ee4a936ff6fb Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 23 Jul 2020 19:13:24 -0700 Subject: [PATCH 3/4] abstract more --- .../contrib/debug/node/debugAdapter.ts | 80 ++++++++----------- 1 file changed, 33 insertions(+), 47 deletions(-) diff --git a/src/vs/workbench/contrib/debug/node/debugAdapter.ts b/src/vs/workbench/contrib/debug/node/debugAdapter.ts index debe9451071..dc6d941a068 100644 --- a/src/vs/workbench/contrib/debug/node/debugAdapter.ts +++ b/src/vs/workbench/contrib/debug/node/debugAdapter.ts @@ -95,7 +95,35 @@ export abstract class NetworkDebugAdapter extends StreamDebugAdapter { protected socket?: net.Socket; - abstract startSession(): Promise; + protected abstract createConnection(connectionListener: () => void): net.Socket; + + startSession(): Promise { + return new Promise((resolve, reject) => { + 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 => { + if (connected) { + this._onError.fire(error); + } else { + reject(error); + } + }); + }); + } async stopSession(): Promise { await this.cancelPendingRequests(); @@ -115,29 +143,8 @@ export class SocketDebugAdapter extends NetworkDebugAdapter { super(); } - 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.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 => { - if (connected) { - this._onError.fire(error); - } else { - reject(error); - } - }); - }); + protected createConnection(connectionListener: () => void): net.Socket { + return net.createConnection(this.adapterServer.port, this.adapterServer.host || '127.0.0.1', connectionListener); } } @@ -150,29 +157,8 @@ export class NamedPipeDebugAdapter extends NetworkDebugAdapter { super(); } - startSession(): Promise { - return new Promise((resolve, reject) => { - let connected = false; - this.socket = net.createConnection(this.adapterServer.path, () => { - 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 => { - if (connected) { - this._onError.fire(error); - } else { - reject(error); - } - }); - }); + protected createConnection(connectionListener: () => void): net.Socket { + return net.createConnection(this.adapterServer.path, connectionListener); } } From c227faae4e4e9150c9f3c97922444d24667525f0 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Fri, 24 Jul 2020 13:45:47 -0700 Subject: [PATCH 4/4] add to descriptor --- src/vs/vscode.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 8df7d0ef548..5af2a0720ba 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -10843,7 +10843,7 @@ declare module 'vscode' { constructor(implementation: DebugAdapter); } - export type DebugAdapterDescriptor = DebugAdapterExecutable | DebugAdapterServer | DebugAdapterInlineImplementation; + export type DebugAdapterDescriptor = DebugAdapterExecutable | DebugAdapterServer | DebugAdapterNamedPipeServer | DebugAdapterInlineImplementation; export interface DebugAdapterDescriptorFactory { /**