diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 0b01a318cd2..28875477a11 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -517,6 +517,21 @@ declare module 'vscode' { export type DebugAdapterDescriptor = DebugAdapterExecutable | DebugAdapterServer | DebugAdapterImplementation; + /** + * A Debug Adapter Tracker is a means to track the communication between VS Code and a Debug Adapter. + */ + export interface IDebugAdapterTracker { + // VS Code -> Debug Adapter + startDebugAdapter?(): void; + toDebugAdapter?(message: any): void; + stopDebugAdapter?(): void; + + // Debug Adapter -> VS Code + fromDebugAdapter?(message: any): void; + debugAdapterError?(error: Error): void; + debugAdapterExit?(code?: number, signal?: string): void; + } + export interface DebugConfigurationProvider { /** * The optional method 'provideDebugAdapter' is called at the start of a debug session to provide details about the debug adapter to use. @@ -542,6 +557,15 @@ declare module 'vscode' { */ provideDebugAdapter?(session: DebugSession, folder: WorkspaceFolder | undefined, executable: DebugAdapterExecutable | undefined, config: DebugConfiguration, token?: CancellationToken): ProviderResult; + /** + * The optional method 'provideDebugAdapterTracker' is called at the start of a debug session to provide a tracker that gives access to the communication between VS Code and a Debug Adapter. + * @param session The [debug session](#DebugSession) for which the tracker will be used. + * @param folder The workspace folder from which the configuration originates from or undefined for a folderless setup. + * @param config The resolved debug configuration. + * @param token A cancellation token. + */ + provideDebugAdapterTracker?(session: DebugSession, folder: WorkspaceFolder | undefined, config: DebugConfiguration, token?: CancellationToken): ProviderResult; + /** * Deprecated, use DebugConfigurationProvider.provideDebugAdapter instead. * @deprecated Use DebugConfigurationProvider.provideDebugAdapter instead diff --git a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts index 4f35e6c05e6..3597e23fbbe 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts @@ -149,10 +149,11 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } - public $registerDebugConfigurationProvider(debugType: string, hasProvide: boolean, hasResolve: boolean, hasProvideDebugAdapter: boolean, handle: number): Thenable { + public $registerDebugConfigurationProvider(debugType: string, hasProvide: boolean, hasResolve: boolean, hasProvideDebugAdapter: boolean, hasTracker: boolean, handle: number): Thenable { const provider = { - type: debugType + type: debugType, + hasTracker: hasTracker }; if (hasProvide) { provider.provideDebugConfigurations = (folder) => { diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 46a8a5c7770..7173cd13814 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -577,7 +577,7 @@ export interface MainThreadDebugServiceShape extends IDisposable { $acceptDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): void; $acceptDAError(handle: number, name: string, message: string, stack: string): void; $acceptDAExit(handle: number, code: number, signal: string): void; - $registerDebugConfigurationProvider(type: string, hasProvideMethod: boolean, hasResolveMethod: boolean, hasDebugAdapterExecutable: boolean, handle: number): Thenable; + $registerDebugConfigurationProvider(type: string, hasProvideMethod: boolean, hasResolveMethod: boolean, hasProvideDaMethod: boolean, hasProvideTrackerMethod: boolean, handle: number): Thenable; $unregisterDebugConfigurationProvider(handle: number): Thenable; $startDebugging(folder: UriComponents | undefined, nameOrConfig: string | vscode.DebugConfiguration): Thenable; $customDebugAdapterRequest(id: DebugSessionUUID, command: string, args: any): Thenable; diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 348136acf22..a6afb9abfee 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -38,9 +38,10 @@ import { IExtensionDescription } from 'vs/workbench/services/extensions/common/e export class ExtHostDebugService implements ExtHostDebugServiceShape { - private _handleCounter: number; + private _providerHandleCounter: number; private _providerByHandle: Map; private _providerByType: Map; + private _providers: TypeProviderPair[]; private _debugServiceProxy: MainThreadDebugServiceShape; private _debugSessions: Map = new Map(); @@ -70,6 +71,7 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { private _aexCommands: Map; private _debugAdapters: Map; + private _debugAdaptersTrackers: Map; private _variableResolver: IConfigurationResolverService; @@ -85,10 +87,14 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { private _terminalService: ExtHostTerminalService, private _commandService: ExtHostCommands ) { - this._aexCommands = new Map(); - this._handleCounter = 0; + this._providerHandleCounter = 0; this._providerByHandle = new Map(); this._providerByType = new Map(); + this._providers = []; + + this._aexCommands = new Map(); + this._debugAdapters = new Map(); + this._debugAdaptersTrackers = new Map(); this._onDidStartDebugSession = new Emitter(); this._onDidTerminateDebugSession = new Emitter(); @@ -108,7 +114,6 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { this._breakpoints = new Map(); this._breakpointEventsActive = false; - this._debugAdapters = new Map(); // register all debug extensions const debugTypes: string[] = []; @@ -262,17 +267,20 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { } } - let handle = this._handleCounter++; + let handle = this._providerHandleCounter++; this._providerByHandle.set(handle, provider); + this._providers.push({ type, provider }); this._debugServiceProxy.$registerDebugConfigurationProvider(type, !!provider.provideDebugConfigurations, !!provider.resolveDebugConfiguration, - !!provider.debugAdapterExecutable || !!provider.provideDebugAdapter, handle); + !!provider.debugAdapterExecutable || !!provider.provideDebugAdapter, + !!provider.provideDebugAdapterTracker, handle); return new Disposable(() => { this._providerByHandle.delete(handle); this._providerByType.delete(type); + this._providers = this._providers.filter(p => p.provider !== provider); // remove this._debugServiceProxy.$unregisterDebugConfigurationProvider(handle); }); } @@ -375,18 +383,43 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { if (da) { this._debugAdapters.set(handle, da); - da.onMessage(message => { - // DA -> VS Code - convertToVSCPaths(message, source => { - if (paths.isAbsolute(source.path)) { - (source).path = URI.file(source.path); + + return this.getDebugAdapterTrackers(sessionDto, folderUri, config).then(tracker => { + + if (tracker) { + this._debugAdaptersTrackers.set(handle, tracker); + } + + da.onMessage(message => { + + if (tracker) { + tracker.fromDebugAdapter(message); } + + // DA -> VS Code + convertToVSCPaths(message, source => { + if (paths.isAbsolute(source.path)) { + (source).path = URI.file(source.path); + } + }); + mythis._debugServiceProxy.$acceptDAMessage(handle, message); }); - mythis._debugServiceProxy.$acceptDAMessage(handle, message); + da.onError(err => { + tracker.debugAdapterError(err); + this._debugServiceProxy.$acceptDAError(handle, err.name, err.message, err.stack); + }); + da.onExit(code => { + tracker.debugAdapterExit(code, null); + this._debugServiceProxy.$acceptDAExit(handle, code, null); + }); + + if (tracker) { + tracker.startDebugAdapter(); + } + + return da.startSession(); }); - da.onError(err => this._debugServiceProxy.$acceptDAError(handle, err.name, err.message, err.stack)); - da.onExit(code => this._debugServiceProxy.$acceptDAExit(handle, code, null)); - return da.startSession(); + } return undefined; }); @@ -399,6 +432,12 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { source.path = URI.revive(source.path).fsPath; } }); + + const tracker = this._debugAdaptersTrackers.get(handle); + if (tracker) { + tracker.toDebugAdapter(message); + } + const da = this._debugAdapters.get(handle); if (da) { da.sendMessage(message); @@ -407,12 +446,22 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { } public $stopDASession(handle: number): TPromise { + + const tracker = this._debugAdaptersTrackers.get(handle); + this._debugAdaptersTrackers.delete(handle); + if (tracker) { + tracker.stopDebugAdapter(); + } + const da = this._debugAdapters.get(handle); this._debugAdapters.delete(handle); - return da ? da.stopSession() : void 0; + if (da) { + return da.stopSession(); + } else { + return void 0; + } } - public $acceptBreakpointsDelta(delta: IBreakpointsDeltaDto): void { let a: vscode.Breakpoint[] = []; @@ -474,7 +523,6 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { this.fireBreakpointChanges(a, r, c); } - public $provideDebugConfigurations(handle: number, folderUri: UriComponents | undefined): Thenable { let provider = this._providerByHandle.get(handle); if (!provider) { @@ -554,6 +602,24 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { return false; } + private getDebugAdapterTrackers(sessionDto: IDebugSessionDto, folderUri: UriComponents | undefined, config: vscode.DebugConfiguration): TPromise { + + const session = this.getSession(sessionDto); + const folder = this.getFolder(folderUri); + + const type = config.type; + const promises = this._providers + .filter(pair => pair.provider.provideDebugAdapterTracker && (pair.type === type || pair.type === '*')) + .map(pair => pair.provider.provideDebugAdapterTracker(session, folder, config, CancellationToken.None)); + + return TPromise.join(promises).then(trackers => { + if (trackers.length > 0) { + return new MultiTracker(trackers); + } + return undefined; + }); + } + private getAdapterDescriptor(debugConfigProvider, sessionDto: IDebugSessionDto, folderUri: UriComponents | undefined, config: vscode.DebugConfiguration): Thenable { // a "debugServer" attribute in the launch config takes precedence @@ -722,12 +788,47 @@ export class ExtHostVariableResolverService extends AbstractVariableResolverServ } } +interface TypeProviderPair { + type: string; + provider: vscode.DebugConfigurationProvider; +} + interface IDapTransport { start(cb: (msg: DebugProtocol.ProtocolMessage) => void, errorcb: (event: DebugProtocol.Event) => void); send(message: DebugProtocol.ProtocolMessage); stop(): void; } +class MultiTracker implements vscode.IDebugAdapterTracker { + + constructor(private trackers: vscode.IDebugAdapterTracker[]) { + } + + startDebugAdapter(): void { + this.trackers.forEach(t => t.startDebugAdapter ? t.startDebugAdapter() : void 0); + } + + toDebugAdapter(message: any): void { + this.trackers.forEach(t => t.toDebugAdapter ? t.toDebugAdapter(message) : void 0); + } + + fromDebugAdapter(message: any): void { + this.trackers.forEach(t => t.fromDebugAdapter ? t.fromDebugAdapter(message) : void 0); + } + + debugAdapterError(error: Error): void { + this.trackers.forEach(t => t.debugAdapterError ? t.debugAdapterError(error) : void 0); + } + + debugAdapterExit(code: number, signal: string): void { + this.trackers.forEach(t => t.debugAdapterExit ? t.debugAdapterExit(code, signal) : void 0); + } + + stopDebugAdapter(): void { + this.trackers.forEach(t => t.stopDebugAdapter ? t.stopDebugAdapter() : void 0); + } +} + class DirectTransport implements IDapTransport { private _sendUp: (msg: DebugProtocol.ProtocolMessage) => void; diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index 293f9e5f730..69ac24b9319 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -518,6 +518,7 @@ export interface IDebugConfigurationProvider { resolveDebugConfiguration?(folderUri: uri | undefined, debugConfiguration: IConfig): TPromise; provideDebugConfigurations?(folderUri: uri | undefined): TPromise; provideDebugAdapter?(session: IDebugSession, folderUri: uri | undefined, config: IConfig): TPromise; + hasTracker: boolean; } export interface ITerminalLauncher { @@ -564,6 +565,8 @@ export interface IConfigurationManager { */ onDidSelectConfiguration: Event; + needsToRunInExtHost(debugType: string): boolean; + registerDebugConfigurationProvider(handle: number, debugConfigurationProvider: IDebugConfigurationProvider): void; unregisterDebugConfigurationProvider(handle: number): void; diff --git a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts index 73b8b7f5caf..9de3937b766 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts @@ -92,6 +92,12 @@ export class ConfigurationManager implements IConfigurationManager { } } + public needsToRunInExtHost(debugType: string): boolean { + // if the given debugType matches any registered provider that has a provideTracker method, we need to run the DA in the EH + const providers = this.providers.filter(p => p.hasTracker && (p.type === debugType || p.type === '*')); + return providers.length > 0; + } + public unregisterDebugConfigurationProvider(handle: number): void { this.providers = this.providers.filter(p => p.handle !== handle); } diff --git a/src/vs/workbench/parts/debug/node/debugger.ts b/src/vs/workbench/parts/debug/node/debugger.ts index 375af5227d7..f2d2e74dc3e 100644 --- a/src/vs/workbench/parts/debug/node/debugger.ts +++ b/src/vs/workbench/parts/debug/node/debugger.ts @@ -40,7 +40,7 @@ export class Debugger implements IDebugger { public hasConfigurationProvider = false; public createDebugAdapter(session: IDebugSession, root: IWorkspaceFolder, config: IConfig, outputService: IOutputService): TPromise { - if (this.inEH()) { + if (this.inExtHost()) { return TPromise.as(this.configurationManager.createDebugAdapter(session, root, config)); } else { return this.getAdapterDescriptor(session, root, config).then(adapterDescriptor => { @@ -91,7 +91,7 @@ export class Debugger implements IDebugger { } public substituteVariables(folder: IWorkspaceFolder, config: IConfig): TPromise { - if (this.inEH()) { + if (this.inExtHost()) { return this.configurationManager.substituteVariables(this.type, folder, config).then(config => { return this.configurationResolverService.resolveWithCommands(folder, config, this.variables); }); @@ -102,12 +102,12 @@ export class Debugger implements IDebugger { public runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): TPromise { const config = this.configurationService.getValue('terminal'); - return this.configurationManager.runInTerminal(this.inEH() ? this.type : '*', args, config); + return this.configurationManager.runInTerminal(this.inExtHost() ? this.type : '*', args, config); } - private inEH(): boolean { + private inExtHost(): boolean { const debugConfigs = this.configurationService.getValue('debug'); - return debugConfigs.extensionHostDebugAdapter || this.extensionDescription.extensionLocation.scheme !== 'file'; + return debugConfigs.extensionHostDebugAdapter || this.configurationManager.needsToRunInExtHost(this.type) || this.extensionDescription.extensionLocation.scheme !== 'file'; } public get label(): string {