diff --git a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts index 1e2665dbe89..2d741ad475b 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts @@ -220,7 +220,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb public $customDebugAdapterRequest(sessionId: DebugSessionUUID, request: string, args: any): Thenable { const session = this.debugService.getSession(sessionId); if (session) { - return session.raw.custom(request, args).then(response => { + return session.customRequest(request, args).then(response => { if (response && response.success) { return response.body; } else { diff --git a/src/vs/workbench/parts/debug/browser/debugContentProvider.ts b/src/vs/workbench/parts/debug/browser/debugContentProvider.ts index a4fc29bec7d..ba1059a13ca 100644 --- a/src/vs/workbench/parts/debug/browser/debugContentProvider.ts +++ b/src/vs/workbench/parts/debug/browser/debugContentProvider.ts @@ -42,12 +42,10 @@ export class DebugContentProvider implements IWorkbenchContribution, ITextModelC public provideTextContent(resource: uri): TPromise { let session: IDebugSession; - let sourceRef: number; if (resource.query) { const data = Source.getEncodedDebugData(resource); session = this.debugService.getModel().getSessions().filter(p => p.getId() === data.sessionId).pop(); - sourceRef = data.sourceReference; } if (!session) { @@ -58,39 +56,21 @@ export class DebugContentProvider implements IWorkbenchContribution, ITextModelC if (!session) { return TPromise.wrapError(new Error(localize('unable', "Unable to resolve the resource without a debug session"))); } - const source = session.getSourceForUri(resource); - let rawSource: DebugProtocol.Source; - if (source) { - rawSource = source.raw; - if (!sourceRef) { - sourceRef = source.reference; - } - } else { - // create a Source - rawSource = { - path: resource.with({ scheme: '', query: '' }).toString(true), // Remove debug: scheme - sourceReference: sourceRef - }; - } - const createErrModel = (message: string) => { this.debugService.sourceIsNotAvailable(resource); const modePromise = this.modeService.getOrCreateMode(MIME_TEXT); - const model = this.modelService.createModel(message, modePromise, resource); - - return model; + return this.modelService.createModel(message, modePromise, resource); }; - return session.raw.source({ sourceReference: sourceRef, source: rawSource }).then(response => { + return session.loadSource(resource).then(response => { if (!response) { return createErrModel(localize('canNotResolveSource', "Could not resolve resource {0}, no response from debug extension.", resource.toString())); } const mime = response.body.mimeType || guessMimeTypes(resource.path)[0]; const modePromise = this.modeService.getOrCreateMode(mime); - const model = this.modelService.createModel(response.body.content, modePromise, resource); + return this.modelService.createModel(response.body.content, modePromise, resource); - return model; }, (err: DebugProtocol.ErrorResponse) => createErrModel(err.message)); } } diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index 7f94b2fd334..eec024ae381 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -24,6 +24,8 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { IViewContainersRegistry, ViewContainer, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { TaskIdentifier } from 'vs/workbench/parts/tasks/common/tasks'; +import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; +import { IOutputService } from 'vs/workbench/parts/output/common/output'; export const VIEWLET_ID = 'workbench.view.debug'; export const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer(VIEWLET_ID); @@ -108,79 +110,95 @@ export interface IExpression extends IReplElement, IExpressionContainer { readonly type?: string; } -export interface IRawDebugSession { +export interface IDebugger { + createDebugAdapter(root: IWorkspaceFolder, outputService: IOutputService, debugPort?: number): TPromise; + runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): TPromise; + getCustomTelemetryService(): TPromise; +} - capabilities: DebugProtocol.Capabilities; - disconnected: boolean; +export type ActualBreakpoints = { [id: string]: DebugProtocol.Breakpoint }; - readyForBreakpoints: boolean; - emittedStopped: boolean; +export enum State { + Inactive, + Initializing, + Stopped, + Running +} + +export class AdapterEndEvent { + error?: Error; sessionLengthInSeconds: number; + emittedStopped: boolean; +} - launchOrAttach(args: IConfig): TPromise; - terminate(restart?: boolean): TPromise; - disconnect(restart?: boolean): TPromise; - - setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): TPromise; - setFunctionBreakpoints(args: DebugProtocol.SetFunctionBreakpointsArguments): TPromise; - setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): TPromise; - - stackTrace(args: DebugProtocol.StackTraceArguments): TPromise; - exceptionInfo(args: DebugProtocol.ExceptionInfoArguments): TPromise; - scopes(args: DebugProtocol.ScopesArguments): TPromise; - variables(args: DebugProtocol.VariablesArguments): TPromise; - evaluate(args: DebugProtocol.EvaluateArguments): TPromise; - custom(request: string, args: any): TPromise; - restartFrame(args: DebugProtocol.RestartFrameArguments, threadId: number): TPromise; - - next(args: DebugProtocol.NextArguments): TPromise; - stepIn(args: DebugProtocol.StepInArguments): TPromise; - stepOut(args: DebugProtocol.StepOutArguments): TPromise; - continue(args: DebugProtocol.ContinueArguments): TPromise; - pause(args: DebugProtocol.PauseArguments): TPromise; - terminateThreads(args: DebugProtocol.TerminateThreadsArguments): TPromise; - stepBack(args: DebugProtocol.StepBackArguments): TPromise; - reverseContinue(args: DebugProtocol.ReverseContinueArguments): TPromise; - - completions(args: DebugProtocol.CompletionsArguments): TPromise; - setVariable(args: DebugProtocol.SetVariableArguments): TPromise; - source(args: DebugProtocol.SourceArguments): TPromise; - loadedSources(args: DebugProtocol.LoadedSourcesArguments): TPromise; +export interface LoadedSourceEvent { + reason: string; + source: Source; } export interface IDebugSession extends ITreeElement, IDisposable { readonly configuration: IConfig; - readonly raw: IRawDebugSession; + readonly unresolvedConfiguration: IConfig; readonly state: State; readonly root: IWorkspaceFolder; - readonly capabilities: DebugProtocol.Capabilities; getName(includeRoot: boolean): string; + getSourceForUri(modelUri: uri): Source; - getThread(threadId: number): IThread; - getAllThreads(): ReadonlyArray; getSource(raw: DebugProtocol.Source): Source; - getLoadedSources(): TPromise; - completions(frameId: number, text: string, position: Position, overwriteBefore: number): TPromise; - clearThreads(removeThreads: boolean, reference?: number): void; rawUpdate(data: IRawModelUpdate): void; - /** - * Allows to register on loaded source events. - */ - onDidLoadedSource: Event; + // session events + onDidEndAdapter: Event; + onDidChangeState: Event; - /** - * Allows to register on custom DAP events. - */ + // DA capabilities + readonly capabilities: DebugProtocol.Capabilities; + + // DAP events + + onDidLoadedSource: Event; onDidCustomEvent: Event; - /** - * Allows to register on DA events. - */ - onDidExitAdapter: Event; + // DAP request + + initialize(dbgr: IDebugger): TPromise; + launchOrAttach(config: IConfig): TPromise; + restart(): TPromise; + terminate(restart?: boolean /* false */): TPromise; + disconnect(restart?: boolean /* false */): TPromise; + + sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): TPromise; + sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): TPromise; + sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): TPromise; + + stackTrace(threadId: number, startFrame: number, levels: number): TPromise; + exceptionInfo(threadId: number): TPromise; + scopes(frameId: number): TPromise; + variables(variablesReference: number, filter: 'indexed' | 'named', start: number, count: number): TPromise; + evaluate(expression: string, frameId?: number, context?: string): TPromise; + customRequest(request: string, args: any): TPromise; + + restartFrame(frameId: number, threadId: number): TPromise; + next(threadId: number): TPromise; + stepIn(threadId: number): TPromise; + stepOut(threadId: number): TPromise; + stepBack(threadId: number): TPromise; + continue(threadId: number): TPromise; + reverseContinue(threadId: number): TPromise; + pause(threadId: number): TPromise; + terminateThreads(threadIds: number[]): TPromise; + + completions(frameId: number, text: string, position: Position, overwriteBefore: number): TPromise; + setVariable(variablesReference: number, name: string, value: string): TPromise; + loadSource(resource: uri): TPromise; + getLoadedSources(): TPromise; + + getThread(threadId: number): IThread; + getAllThreads(): ReadonlyArray; + clearThreads(removeThreads: boolean, reference?: number): void; } export interface IThread extends ITreeElement { @@ -369,15 +387,6 @@ export interface IBreakpointsChangeEvent { sessionOnly?: boolean; } -// Debug enums - -export enum State { - Inactive, - Initializing, - Stopped, - Running -} - // Debug configuration interfaces export interface IDebugConfiguration { @@ -438,7 +447,7 @@ export interface IDebugAdapter extends IDisposable { startSession(): TPromise; sendMessage(message: DebugProtocol.ProtocolMessage): void; sendResponse(response: DebugProtocol.Response): void; - sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void): void; + sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void, timemout?: number): void; stopSession(): TPromise; } @@ -598,11 +607,6 @@ export interface ILaunch { export const IDebugService = createDecorator(DEBUG_SERVICE_ID); -export interface LoadedSourceEvent { - reason: string; - source: Source; -} - export interface IDebugService { _serviceBrand: any; diff --git a/src/vs/workbench/parts/debug/common/debugModel.ts b/src/vs/workbench/parts/debug/common/debugModel.ts index 6a2f98f6bf2..ec1f0121bd1 100644 --- a/src/vs/workbench/parts/debug/common/debugModel.ts +++ b/src/vs/workbench/parts/debug/common/debugModel.ts @@ -185,16 +185,12 @@ export class ExpressionContainer implements IExpressionContainer { } private fetchVariables(start: number, count: number, filter: 'indexed' | 'named'): TPromise { - return this.session.raw ? this.session.raw.variables({ - variablesReference: this.reference, - start, - count, - filter - }).then(response => { - return response && response.body && response.body.variables ? distinct(response.body.variables.filter(v => !!v && isString(v.name)), v => v.name).map( - v => new Variable(this.session, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.presentationHint, v.type) - ) : []; - }, (e: Error) => [new Variable(this.session, this, 0, e.message, e.message, '', 0, 0, { kind: 'virtual' }, null, false)]) : TPromise.as([]); + return this.session.variables(this.reference, filter, start, count).then(response => { + return response && response.body && response.body.variables + ? distinct(response.body.variables.filter(v => !!v && isString(v.name)), v => v.name).map( + v => new Variable(this.session, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.presentationHint, v.type)) + : []; + }, (e: Error) => [new Variable(this.session, this, 0, e.message, e.message, '', 0, 0, { kind: 'virtual' }, null, false)]); } // The adapter explicitly sents the children count of an expression only if there are lots of children which should be chunked. @@ -240,11 +236,7 @@ export class Expression extends ExpressionContainer implements IExpression { } this.session = session; - return session.raw.evaluate({ - expression: this.name, - frameId: stackFrame ? stackFrame.frameId : undefined, - context - }).then(response => { + return session.evaluate(this.name, stackFrame ? stackFrame.frameId : undefined, context).then(response => { this.available = !!(response && response.body); if (response && response.body) { this.value = response.body.result; @@ -289,11 +281,7 @@ export class Variable extends ExpressionContainer implements IExpression { } public setVariable(value: string): TPromise { - return this.session.raw.setVariable({ - name: this.name, - value, - variablesReference: (this.parent).reference - }).then(response => { + return this.session.setVariable((this.parent).reference, name, value).then(response => { if (response && response.body) { this.value = response.body.value; this.type = response.body.type || this.type; @@ -349,7 +337,7 @@ export class StackFrame implements IStackFrame { public getScopes(): TPromise { if (!this.scopes) { - this.scopes = this.thread.session.raw.scopes({ frameId: this.frameId }).then(response => { + this.scopes = this.thread.session.scopes(this.frameId).then(response => { return response && response.body && response.body.scopes ? response.body.scopes.map((rs, index) => new Scope(this, index, rs.name, rs.variablesReference, rs.expensive, rs.namedVariables, rs.indexedVariables, rs.line && rs.column && rs.endLine && rs.endColumn ? new Range(rs.line, rs.column, rs.endLine, rs.endColumn) : null)) : []; @@ -390,7 +378,7 @@ export class StackFrame implements IStackFrame { } public restart(): TPromise { - return this.thread.session.raw.restartFrame({ frameId: this.frameId }, this.thread.threadId); + return this.thread.session.restartFrame(this.frameId, this.thread.threadId); } public toString(): string { @@ -458,7 +446,7 @@ export class Thread implements IThread { } private getCallStackImpl(startFrame: number, levels: number): TPromise { - return this.session.raw.stackTrace({ threadId: this.threadId, startFrame, levels }).then(response => { + return this.session.stackTrace(this.threadId, startFrame, levels).then(response => { if (!response || !response.body) { return []; } @@ -491,60 +479,47 @@ export class Thread implements IThread { */ public get exceptionInfo(): TPromise { if (this.stoppedDetails && this.stoppedDetails.reason === 'exception') { - if (!this.session.capabilities.supportsExceptionInfoRequest) { - return TPromise.as({ - description: this.stoppedDetails.text, - breakMode: null - }); + if (this.session.capabilities.supportsExceptionInfoRequest) { + return this.session.exceptionInfo(this.threadId); } - - return this.session.raw.exceptionInfo({ threadId: this.threadId }).then(exception => { - if (!exception) { - return null; - } - - return { - id: exception.body.exceptionId, - description: exception.body.description, - breakMode: exception.body.breakMode, - details: exception.body.details - }; + return TPromise.as({ + description: this.stoppedDetails.text, + breakMode: null }); } - return TPromise.as(null); } public next(): TPromise { - return this.session.raw.next({ threadId: this.threadId }); + return this.session.next(this.threadId); } public stepIn(): TPromise { - return this.session.raw.stepIn({ threadId: this.threadId }); + return this.session.stepIn(this.threadId); } public stepOut(): TPromise { - return this.session.raw.stepOut({ threadId: this.threadId }); + return this.session.stepOut(this.threadId); } public stepBack(): TPromise { - return this.session.raw.stepBack({ threadId: this.threadId }); + return this.session.stepBack(this.threadId); } public continue(): TPromise { - return this.session.raw.continue({ threadId: this.threadId }); + return this.session.continue(this.threadId); } public pause(): TPromise { - return this.session.raw.pause({ threadId: this.threadId }); + return this.session.pause(this.threadId); } public terminate(): TPromise { - return this.session.raw.terminateThreads({ threadIds: [this.threadId] }); + return this.session.terminateThreads([this.threadId]); } public reverseContinue(): TPromise { - return this.session.raw.reverseContinue({ threadId: this.threadId }); + return this.session.reverseContinue(this.threadId); } } diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index 012e0df15f9..c9aa576f4b9 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -40,16 +40,14 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL, EXTENSION_RELOAD_BROADCAST_CHANNEL, EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost'; import { IBroadcastService } from 'vs/platform/broadcast/electron-browser/broadcastService'; import { IRemoteConsoleLog, parse, getFirstFrame } from 'vs/base/node/console'; -import { Source } from 'vs/workbench/parts/debug/common/debugSource'; import { TaskEvent, TaskEventKind, TaskIdentifier } from 'vs/workbench/parts/tasks/common/tasks'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IAction, Action } from 'vs/base/common/actions'; -import { normalizeDriveLetter } from 'vs/base/common/labels'; import { deepClone, equals } from 'vs/base/common/objects'; import { DebugSession } from 'vs/workbench/parts/debug/electron-browser/debugSession'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { IDebugService, State, IDebugSession, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_MODE, IThread, IDebugConfiguration, VIEWLET_ID, REPL_ID, IConfig, ILaunch, IViewModel, IConfigurationManager, IModel, IReplElementSource, IEnablement, IBreakpoint, IBreakpointData, IExpression, ICompound, IGlobalConfig, IStackFrame } from 'vs/workbench/parts/debug/common/debug'; +import { IDebugService, State, IDebugSession, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_MODE, IThread, IDebugConfiguration, VIEWLET_ID, REPL_ID, IConfig, ILaunch, IViewModel, IConfigurationManager, IModel, IReplElementSource, IEnablement, IBreakpoint, IBreakpointData, IExpression, ICompound, IGlobalConfig, IStackFrame, AdapterEndEvent } from 'vs/workbench/parts/debug/common/debug'; import { isExtensionHostDebugging } from 'vs/workbench/parts/debug/common/debugUtils'; const DEBUG_BREAKPOINTS_KEY = 'debug.breakpoint'; @@ -127,7 +125,7 @@ export class DebugService implements IDebugService { this.lifecycleService.onShutdown(this.dispose, this); this.toDispose.push(this.broadcastService.onBroadcast(broadcast => { - const session = this.getSession(broadcast.payload.debugId); + const session = this.getSession(broadcast.payload.debugId); if (session) { switch (broadcast.channel) { @@ -137,10 +135,8 @@ export class DebugService implements IDebugService { break; case EXTENSION_TERMINATE_BROADCAST_CHANNEL: - // EH was terminated -> terminate debug session - if (session.raw) { - session.raw.terminate(); - } + // EH was terminated + session.disconnect(); break; case EXTENSION_LOG_BROADCAST_CHANNEL: @@ -193,14 +189,14 @@ export class DebugService implements IDebugService { return this.initializing ? State.Initializing : State.Inactive; } - private startInitializing() { + private startInitializingState() { if (!this.initializing) { this.initializing = true; this.onStateChange(); } } - private endInitializing() { + private endInitializingState() { if (this.initializing) { this.initializing = false; this.onStateChange(); @@ -244,112 +240,114 @@ export class DebugService implements IDebugService { startDebugging(launch: ILaunch, configOrName?: IConfig | string, noDebug = false, unresolvedConfiguration?: IConfig, ): TPromise { // make sure to save all files and that the configuration is up to date - return this.extensionService.activateByEvent('onDebug').then(() => this.textFileService.saveAll().then(() => this.configurationService.reloadConfiguration(launch ? launch.workspace : undefined).then(() => - this.extensionService.whenInstalledExtensionsRegistered().then(() => { + return this.extensionService.activateByEvent('onDebug').then(() => + this.textFileService.saveAll().then(() => + this.configurationService.reloadConfiguration(launch ? launch.workspace : undefined).then(() => + this.extensionService.whenInstalledExtensionsRegistered().then(() => { - if (this.model.getSessions().length === 0) { - this.removeReplExpressions(); - this.allSessions.clear(); - } - - let config: IConfig, compound: ICompound; - if (!configOrName) { - configOrName = this.configurationManager.selectedConfiguration.name; - } - if (typeof configOrName === 'string' && launch) { - config = launch.getConfiguration(configOrName); - compound = launch.getCompound(configOrName); - - const sessions = this.model.getSessions(); - const alreadyRunningMessage = nls.localize('configurationAlreadyRunning', "There is already a debug configuration \"{0}\" running.", configOrName); - if (sessions.some(s => s.getName(false) === configOrName && (!launch || !launch.workspace || !s.root || s.root.uri.toString() === launch.workspace.uri.toString()))) { - return TPromise.wrapError(new Error(alreadyRunningMessage)); - } - if (compound && compound.configurations && sessions.some(p => compound.configurations.indexOf(p.getName(false)) !== -1)) { - return TPromise.wrapError(new Error(alreadyRunningMessage)); - } - } else if (typeof configOrName !== 'string') { - config = configOrName; - } - - if (compound) { - if (!compound.configurations) { - return TPromise.wrapError(new Error(nls.localize({ key: 'compoundMustHaveConfigurations', comment: ['compound indicates a "compounds" configuration item', '"configurations" is an attribute and should not be localized'] }, - "Compound must have \"configurations\" attribute set in order to start multiple configurations."))); - } - - return TPromise.join(compound.configurations.map(configData => { - const name = typeof configData === 'string' ? configData : configData.name; - if (name === compound.name) { - return TPromise.as(null); + if (this.model.getSessions().length === 0) { + this.removeReplExpressions(); + this.allSessions.clear(); } - let launchForName: ILaunch; - if (typeof configData === 'string') { - const launchesContainingName = this.configurationManager.getLaunches().filter(l => !!l.getConfiguration(name)); - if (launchesContainingName.length === 1) { - launchForName = launchesContainingName[0]; - } else if (launchesContainingName.length > 1 && launchesContainingName.indexOf(launch) >= 0) { - // If there are multiple launches containing the configuration give priority to the configuration in the current launch - launchForName = launch; - } else { - return TPromise.wrapError(new Error(launchesContainingName.length === 0 ? nls.localize('noConfigurationNameInWorkspace', "Could not find launch configuration '{0}' in the workspace.", name) - : nls.localize('multipleConfigurationNamesInWorkspace', "There are multiple launch configurations '{0}' in the workspace. Use folder name to qualify the configuration.", name))); + let config: IConfig, compound: ICompound; + if (!configOrName) { + configOrName = this.configurationManager.selectedConfiguration.name; + } + if (typeof configOrName === 'string' && launch) { + config = launch.getConfiguration(configOrName); + compound = launch.getCompound(configOrName); + + const sessions = this.model.getSessions(); + const alreadyRunningMessage = nls.localize('configurationAlreadyRunning', "There is already a debug configuration \"{0}\" running.", configOrName); + if (sessions.some(s => s.getName(false) === configOrName && (!launch || !launch.workspace || !s.root || s.root.uri.toString() === launch.workspace.uri.toString()))) { + return TPromise.wrapError(new Error(alreadyRunningMessage)); } - } else if (configData.folder) { - const launchesMatchingConfigData = this.configurationManager.getLaunches().filter(l => l.workspace && l.workspace.name === configData.folder && !!l.getConfiguration(configData.name)); - if (launchesMatchingConfigData.length === 1) { - launchForName = launchesMatchingConfigData[0]; - } else { - return TPromise.wrapError(new Error(nls.localize('noFolderWithName', "Can not find folder with name '{0}' for configuration '{1}' in compound '{2}'.", configData.folder, configData.name, compound.name))); + if (compound && compound.configurations && sessions.some(p => compound.configurations.indexOf(p.getName(false)) !== -1)) { + return TPromise.wrapError(new Error(alreadyRunningMessage)); } + } else if (typeof configOrName !== 'string') { + config = configOrName; } - return this.startDebugging(launchForName, name, noDebug, unresolvedConfiguration); - })); - } - if (configOrName && !config) { - const message = !!launch ? nls.localize('configMissing', "Configuration '{0}' is missing in 'launch.json'.", typeof configOrName === 'string' ? configOrName : JSON.stringify(configOrName)) : - nls.localize('launchJsonDoesNotExist', "'launch.json' does not exist."); - return TPromise.wrapError(new Error(message)); - } + if (compound) { + if (!compound.configurations) { + return TPromise.wrapError(new Error(nls.localize({ key: 'compoundMustHaveConfigurations', comment: ['compound indicates a "compounds" configuration item', '"configurations" is an attribute and should not be localized'] }, + "Compound must have \"configurations\" attribute set in order to start multiple configurations."))); + } - // We keep the debug type in a separate variable 'type' so that a no-folder config has no attributes. - // Storing the type in the config would break extensions that assume that the no-folder case is indicated by an empty config. - let type: string; - if (config) { - type = config.type; - } else { - // a no-folder workspace has no launch.config - config = {}; - } - unresolvedConfiguration = unresolvedConfiguration || deepClone(config); + return TPromise.join(compound.configurations.map(configData => { + const name = typeof configData === 'string' ? configData : configData.name; + if (name === compound.name) { + return TPromise.as(null); + } - if (noDebug) { - config.noDebug = true; - } + let launchForName: ILaunch; + if (typeof configData === 'string') { + const launchesContainingName = this.configurationManager.getLaunches().filter(l => !!l.getConfiguration(name)); + if (launchesContainingName.length === 1) { + launchForName = launchesContainingName[0]; + } else if (launchesContainingName.length > 1 && launchesContainingName.indexOf(launch) >= 0) { + // If there are multiple launches containing the configuration give priority to the configuration in the current launch + launchForName = launch; + } else { + return TPromise.wrapError(new Error(launchesContainingName.length === 0 ? nls.localize('noConfigurationNameInWorkspace', "Could not find launch configuration '{0}' in the workspace.", name) + : nls.localize('multipleConfigurationNamesInWorkspace', "There are multiple launch configurations '{0}' in the workspace. Use folder name to qualify the configuration.", name))); + } + } else if (configData.folder) { + const launchesMatchingConfigData = this.configurationManager.getLaunches().filter(l => l.workspace && l.workspace.name === configData.folder && !!l.getConfiguration(configData.name)); + if (launchesMatchingConfigData.length === 1) { + launchForName = launchesMatchingConfigData[0]; + } else { + return TPromise.wrapError(new Error(nls.localize('noFolderWithName', "Can not find folder with name '{0}' for configuration '{1}' in compound '{2}'.", configData.folder, configData.name, compound.name))); + } + } - return (type ? TPromise.as(null) : this.configurationManager.guessDebugger().then(a => type = a && a.type)).then(() => - this.configurationManager.resolveConfigurationByProviders(launch && launch.workspace ? launch.workspace.uri : undefined, type, config).then(config => { - // a falsy config indicates an aborted launch - if (config && config.type) { - return this.createSession(launch, config, unresolvedConfiguration); + return this.startDebugging(launchForName, name, noDebug, unresolvedConfiguration); + })); + } + if (configOrName && !config) { + const message = !!launch ? nls.localize('configMissing', "Configuration '{0}' is missing in 'launch.json'.", typeof configOrName === 'string' ? configOrName : JSON.stringify(configOrName)) : + nls.localize('launchJsonDoesNotExist', "'launch.json' does not exist."); + return TPromise.wrapError(new Error(message)); } - if (launch && type) { - return launch.openConfigFile(false, true, type).then(() => undefined); + // We keep the debug type in a separate variable 'type' so that a no-folder config has no attributes. + // Storing the type in the config would break extensions that assume that the no-folder case is indicated by an empty config. + let type: string; + if (config) { + type = config.type; + } else { + // a no-folder workspace has no launch.config + config = {}; + } + unresolvedConfiguration = unresolvedConfiguration || deepClone(config); + + if (noDebug) { + config.noDebug = true; } - return undefined; + return (type ? TPromise.as(null) : this.configurationManager.guessDebugger().then(a => type = a && a.type)).then(() => + this.configurationManager.resolveConfigurationByProviders(launch && launch.workspace ? launch.workspace.uri : undefined, type, config).then(config => { + // a falsy config indicates an aborted launch + if (config && config.type) { + return this.createSession(launch, config, unresolvedConfiguration); + } + + if (launch && type) { + return launch.openConfigFile(false, true, type).then(() => undefined); + } + + return undefined; + }) + ).then(() => undefined); }) - ).then(() => undefined); - }) - ))); + ))); } private createSession(launch: ILaunch, config: IConfig, unresolvedConfig: IConfig): TPromise { - this.startInitializing(); + this.startInitializingState(); return this.textFileService.saveAll().then(() => this.substituteVariables(launch, config).then(resolvedConfig => { @@ -391,21 +389,21 @@ export class DebugService implements IDebugService { return launch && launch.openConfigFile(false, true).then(editor => void 0); }) ).then(() => { - this.endInitializing(); + this.endInitializingState(); }, err => { - this.endInitializing(); + this.endInitializingState(); return TPromise.wrapError(err); }); } - private attachExtensionHost(session: DebugSession, port: number): TPromise { + private attachExtensionHost(session: IDebugSession, port: number): TPromise { session.configuration.request = 'attach'; session.configuration.port = port; const dbgr = this.configurationManager.getDebugger(session.configuration.type); return session.initialize(dbgr).then(() => { - session.raw.launchOrAttach(session.configuration).then(result => { + session.launchOrAttach(session.configuration).then(() => { this.focusStackFrame(undefined, undefined, session); }); }); @@ -413,7 +411,7 @@ export class DebugService implements IDebugService { private doCreateSession(root: IWorkspaceFolder, configuration: { resolved: IConfig, unresolved: IConfig }): TPromise { - const session = this.instantiationService.createInstance(DebugSession, configuration, root, this.model); + const session = this.instantiationService.createInstance(DebugSession, configuration, root, this.model); this.allSessions.set(session.getId(), session); // register listeners as the very first thing! @@ -428,14 +426,7 @@ export class DebugService implements IDebugService { return session.initialize(dbgr).then(() => { - const raw = session.raw; - - // pass the sessionID for EH debugging - if (isExtensionHostDebugging(resolved)) { - resolved.__sessionId = session.getId(); - } - - return raw.launchOrAttach(resolved).then(result => { + return session.launchOrAttach(resolved).then(() => { this.focusStackFrame(undefined, undefined, session); @@ -468,8 +459,8 @@ export class DebugService implements IDebugService { } if (errors.isPromiseCanceledError(error)) { - // Do not show 'canceled' error messages to the user #7906 - return TPromise.as(null); + // don't show 'canceled' error messages to the user #7906 + return TPromise.as(undefined); } // Show the repl if some error got logged there #5870 @@ -484,7 +475,7 @@ export class DebugService implements IDebugService { this.telemetryDebugMisconfiguration(resolved ? resolved.type : undefined, errorMessage); this.showError(errorMessage, errors.isErrorWithActions(error) ? error.actions : []); } - return undefined; + return TPromise.as(undefined); }); }).then(undefined, error => { @@ -494,14 +485,14 @@ export class DebugService implements IDebugService { } if (errors.isPromiseCanceledError(error)) { - // Do not show 'canceled' error messages to the user #7906 + // don't show 'canceled' error messages to the user #7906 return TPromise.as(null); } return TPromise.wrapError(error); }); } - private registerSessionListeners(session: DebugSession): void { + private registerSessionListeners(session: IDebugSession): void { this.toDispose.push(session.onDidChangeState((state) => { if (state === State.Running && this.viewModel.focusedSession && this.viewModel.focusedSession.getId() === session.getId()) { @@ -510,10 +501,10 @@ export class DebugService implements IDebugService { this.onStateChange(); })); - this.toDispose.push(session.onDidExitAdapter(err => { + this.toDispose.push(session.onDidEndAdapter(adapterExitEvent => { - if (err) { - this.notificationService.error(nls.localize('debugAdapterCrash', "Debug adapter process has terminated unexpectedly ({0})", err.message || err.toString())); + if (adapterExitEvent.error) { + this.notificationService.error(nls.localize('debugAdapterCrash', "Debug adapter process has terminated unexpectedly ({0})", adapterExitEvent.error.message || adapterExitEvent.error.toString())); } // 'Run without debugging' mode VSCode must terminate the extension host. More details: #3905 @@ -524,7 +515,7 @@ export class DebugService implements IDebugService { }); } - this.telemetryDebugSessionStop(session); + this.telemetryDebugSessionStop(session, adapterExitEvent); if (session.configuration.postDebugTask) { this.doRunTask(session.root, session.configuration.postDebugTask).then(undefined, err => @@ -552,18 +543,23 @@ export class DebugService implements IDebugService { } restartSession(session: IDebugSession, restartData?: any): TPromise { + + return this.textFileService.saveAll().then(() => { - const unresolvedConfiguration = (session).unresolvedConfiguration; + + const unresolvedConfiguration = session.unresolvedConfiguration; if (session.capabilities.supportsRestartRequest) { return this.runTask(session.root, session.configuration.postDebugTask, session.configuration, unresolvedConfiguration) .then(success => success ? this.runTask(session.root, session.configuration.preLaunchTask, session.configuration, unresolvedConfiguration) - .then(success => success ? session.raw.custom('restart', null) : undefined) : TPromise.as(undefined)); + .then(success => success ? session.restart() : undefined) : TPromise.as(undefined)); } const focusedSession = this.viewModel.focusedSession; const preserveFocus = focusedSession && session.getId() === focusedSession.getId(); + // Do not run preLaunch and postDebug tasks for automatic restarts - this.skipRunningTask = !!restartData; + const isAutoRestart = !!restartData; + this.skipRunningTask = isAutoRestart; if (isExtensionHostDebugging(session.configuration) && session.root) { return this.broadcastService.broadcast({ @@ -572,8 +568,8 @@ export class DebugService implements IDebugService { }); } - // If the restart is automatic disconnect, otherwise send the terminate signal #55064 - return (!!restartData ? session.raw.disconnect(true) : session.raw.terminate(true)).then(() => { + // If the restart is automatic -> disconnect, otherwise -> terminate #55064 + return (isAutoRestart ? session.disconnect(true) : session.terminate(true)).then(() => { return new TPromise((c, e) => { setTimeout(() => { @@ -591,7 +587,7 @@ export class DebugService implements IDebugService { } } configToUse.__restart = restartData; - this.skipRunningTask = !!restartData; + this.skipRunningTask = isAutoRestart; this.startDebugging(launch, configToUse, configToUse.noDebug, unresolvedConfiguration).then(() => c(null), err => e(err)); }, 300); }); @@ -610,15 +606,15 @@ export class DebugService implements IDebugService { stopSession(session: IDebugSession): TPromise { if (session) { - return session.raw.terminate(); + return session.terminate(); } const sessions = this.model.getSessions(); if (sessions.length) { - return TPromise.join(sessions.map(s => s.raw.terminate(false))); + return TPromise.join(sessions.map(s => s.terminate())); } - this._onDidChangeState.fire(); + this._onDidChangeState.fire(); // TODO@AW why state change? return undefined; } @@ -1000,93 +996,43 @@ export class DebugService implements IDebugService { private sendBreakpoints(modelUri: uri, sourceModified = false, session?: IDebugSession): TPromise { - const sendBreakpointsToSession = (session: IDebugSession): TPromise => { - const raw = session.raw; - if (!raw.readyForBreakpoints) { - return TPromise.as(null); - } + const breakpointsToSend = this.model.getBreakpoints({ uri: modelUri, enabledOnly: true }); - const breakpointsToSend = this.model.getBreakpoints({ uri: modelUri, enabledOnly: true }); - - const source = session.getSourceForUri(modelUri); - let rawSource: DebugProtocol.Source; - if (source) { - rawSource = source.raw; - } else { - const data = Source.getEncodedDebugData(modelUri); - rawSource = { name: data.name, path: data.path, sourceReference: data.sourceReference }; - } - - if (breakpointsToSend.length && !rawSource.adapterData) { - rawSource.adapterData = breakpointsToSend[0].adapterData; - } - // Normalize all drive letters going out from vscode to debug adapters so we are consistent with our resolving #43959 - rawSource.path = normalizeDriveLetter(rawSource.path); - - return raw.setBreakpoints({ - source: rawSource, - lines: breakpointsToSend.map(bp => bp.lineNumber), - breakpoints: breakpointsToSend.map(bp => ({ line: bp.lineNumber, column: bp.column, condition: bp.condition, hitCondition: bp.hitCondition, logMessage: bp.logMessage })), - sourceModified - }).then(response => { - if (!response || !response.body) { - return; + return this.sendToOneOrAllSessions(session, s => { + return s.sendBreakpoints(modelUri, breakpointsToSend, sourceModified).then(data => { + if (data) { + this.model.setBreakpointSessionData(s.getId(), data); } - - const data = Object.create(null); - for (let i = 0; i < breakpointsToSend.length; i++) { - data[breakpointsToSend[i].getId()] = response.body.breakpoints[i]; - } - this.model.setBreakpointSessionData(session.getId(), data); }); - }; - - return this.sendToOneOrAllSessions(session, sendBreakpointsToSession); + }); } private sendFunctionBreakpoints(session?: IDebugSession): TPromise { - const sendFunctionBreakpointsToSession = (session: IDebugSession): TPromise => { - const raw = session.raw; - if (!raw.readyForBreakpoints || !raw.capabilities.supportsFunctionBreakpoints) { - return TPromise.as(null); - } - const breakpointsToSend = this.model.getFunctionBreakpoints().filter(fbp => fbp.enabled && this.model.areBreakpointsActivated()); - return raw.setFunctionBreakpoints({ breakpoints: breakpointsToSend }).then(response => { - if (!response || !response.body) { - return; - } + const breakpointsToSend = this.model.getFunctionBreakpoints().filter(fbp => fbp.enabled && this.model.areBreakpointsActivated()); - const data = Object.create(null); - for (let i = 0; i < breakpointsToSend.length; i++) { - data[breakpointsToSend[i].getId()] = response.body.breakpoints[i]; + return this.sendToOneOrAllSessions(session, s => { + return s.sendFunctionBreakpoints(breakpointsToSend).then(data => { + if (data) { + this.model.setBreakpointSessionData(s.getId(), data); } - this.model.setBreakpointSessionData(session.getId(), data); }); - }; - - return this.sendToOneOrAllSessions(session, sendFunctionBreakpointsToSession); + }); } private sendExceptionBreakpoints(session?: IDebugSession): TPromise { - const sendExceptionBreakpointsToSession = (session: IDebugSession): TPromise => { - const raw = session.raw; - if (!raw.readyForBreakpoints || this.model.getExceptionBreakpoints().length === 0) { - return TPromise.as(null); - } - const enabledExceptionBps = this.model.getExceptionBreakpoints().filter(exb => exb.enabled); - return raw.setExceptionBreakpoints({ filters: enabledExceptionBps.map(exb => exb.filter) }); - }; + const enabledExceptionBps = this.model.getExceptionBreakpoints().filter(exb => exb.enabled); - return this.sendToOneOrAllSessions(session, sendExceptionBreakpointsToSession); + return this.sendToOneOrAllSessions(session, s => { + return s.sendExceptionBreakpoints(enabledExceptionBps); + }); } private sendToOneOrAllSessions(session: IDebugSession, send: (session: IDebugSession) => TPromise): TPromise { if (session) { return send(session); } - return TPromise.join(this.model.getSessions().map(s => send(s))).then(() => void 0); } @@ -1210,7 +1156,7 @@ export class DebugService implements IDebugService { }); } - private telemetryDebugSessionStop(session: IDebugSession): TPromise { + private telemetryDebugSessionStop(session: IDebugSession, adapterExitEvent: AdapterEndEvent): TPromise { const breakpoints = this.model.getBreakpoints(); @@ -1225,8 +1171,8 @@ export class DebugService implements IDebugService { */ return this.telemetryService.publicLog('debugSessionStop', { type: session && session.configuration.type, - success: session.raw.emittedStopped || breakpoints.length === 0, - sessionLengthInSeconds: session.raw.sessionLengthInSeconds, + success: adapterExitEvent.emittedStopped || breakpoints.length === 0, + sessionLengthInSeconds: adapterExitEvent.sessionLengthInSeconds, breakpointCount: breakpoints.length, watchExpressionsCount: this.model.getWatchExpressions().length }); diff --git a/src/vs/workbench/parts/debug/electron-browser/debugSession.ts b/src/vs/workbench/parts/debug/electron-browser/debugSession.ts index f340f6dcfdf..5e1b8eab69a 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugSession.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugSession.ts @@ -13,13 +13,12 @@ import { Event, Emitter } from 'vs/base/common/event'; import { ISuggestion } from 'vs/editor/common/modes'; import { Position } from 'vs/editor/common/core/position'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { IDebugSession, IConfig, IThread, IRawModelUpdate, IDebugService, IRawStoppedDetails, State, IRawDebugSession, LoadedSourceEvent } from 'vs/workbench/parts/debug/common/debug'; +import { IDebugSession, IConfig, IThread, IRawModelUpdate, IDebugService, IRawStoppedDetails, State, LoadedSourceEvent, IFunctionBreakpoint, IExceptionBreakpoint, ActualBreakpoints, IBreakpoint, IExceptionInfo, AdapterEndEvent, IDebugger } from 'vs/workbench/parts/debug/common/debug'; import { Source } from 'vs/workbench/parts/debug/common/debugSource'; import { mixin } from 'vs/base/common/objects'; import { Thread, ExpressionContainer, Model } from 'vs/workbench/parts/debug/common/debugModel'; import { RawDebugSession } from 'vs/workbench/parts/debug/electron-browser/rawDebugSession'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Debugger } from 'vs/workbench/parts/debug/node/debugger'; import product from 'vs/platform/node/product'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -27,21 +26,26 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { RunOnceScheduler } from 'vs/base/common/async'; import { generateUuid } from 'vs/base/common/uuid'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { normalizeDriveLetter } from 'vs/base/common/labels'; export class DebugSession implements IDebugSession { private id: string; + private _raw: RawDebugSession; + private _state: State; + private _nullCapabilities: DebugProtocol.Capabilities; private sources = new Map(); private threads = new Map(); private rawListeners: IDisposable[] = []; private fetchThreadsScheduler: RunOnceScheduler; - private _raw: RawDebugSession; - private _state: State; + + private readonly _onDidChangeState = new Emitter(); + private readonly _onDidEndAdapter = new Emitter(); + private readonly _onDidLoadedSource = new Emitter(); private readonly _onDidCustomEvent = new Emitter(); - private readonly _onDidChangeState = new Emitter(); - private readonly _onDidExitAdapter = new Emitter(); + constructor( private _configuration: { resolved: IConfig, unresolved: IConfig }, @@ -54,16 +58,13 @@ export class DebugSession implements IDebugSession { ) { this.id = generateUuid(); this.state = State.Initializing; + this._nullCapabilities = Object.create(null); } getId(): string { return this.id; } - get raw(): IRawDebugSession { - return this._raw; - } - get configuration(): IConfig { return this._configuration.resolved; } @@ -72,10 +73,6 @@ export class DebugSession implements IDebugSession { return this._configuration.unresolved; } - get capabilities(): DebugProtocol.Capabilities { - return this._raw ? this._raw.capabilities : Object.create(null); - } - getName(includeRoot: boolean): string { return includeRoot && this.root ? `${this.configuration.name} (${resources.basenameOrAuthority(this.root.uri)})` : this.configuration.name; } @@ -85,23 +82,43 @@ export class DebugSession implements IDebugSession { } set state(value: State) { - this._state = value; - this._onDidChangeState.fire(value); + if (this._state !== value) { + this._state = value; + this._onDidChangeState.fire(value); + } } + get capabilities(): DebugProtocol.Capabilities { + return this._raw ? this._raw.capabilities : this._nullCapabilities; + } + + //---- events + get onDidChangeState(): Event { return this._onDidChangeState.event; } + get onDidEndAdapter(): Event { + return this._onDidEndAdapter.event; + } + + //---- DAP events + get onDidCustomEvent(): Event { return this._onDidCustomEvent.event; } - get onDidExitAdapter(): Event { - return this._onDidExitAdapter.event; + get onDidLoadedSource(): Event { + return this._onDidLoadedSource.event; } - initialize(dbgr: Debugger): TPromise { + + //---- DAP requests + + /** + * create and initialize a new debug adapter for this session + */ + initialize(dbgr: IDebugger): TPromise { if (this._raw) { // if there was already a connection make sure to remove old listeners @@ -133,6 +150,409 @@ export class DebugSession implements IDebugSession { }); } + /** + * launch or attach to the debuggee + */ + launchOrAttach(config: IConfig): TPromise { + if (this._raw) { + + // __sessionID only used for EH debugging (but we add it always for now...) + config.__sessionId = this.getId(); + + return this._raw.launchOrAttach(config).then(result => { + return void 0; + }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + /** + * end the current debug adapter session + */ + terminate(restart = false): TPromise { + if (this._raw) { + if (this._raw.capabilities.supportsTerminateRequest && this._configuration.resolved.request === 'launch') { + return this._raw.terminate(restart).then(response => { + return void 0; + }); + } + return this._raw.disconnect(restart).then(response => { + return void 0; + }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + /** + * end the current debug adapter session + */ + disconnect(restart = false): TPromise { + if (this._raw) { + return this._raw.disconnect(restart).then(response => { + return void 0; + }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + /** + * restart debug adapter session + */ + restart(): TPromise { + if (this._raw) { + return this._raw.restart(); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + sendBreakpoints(modelUri: uri, breakpointsToSend: IBreakpoint[], sourceModified: boolean): TPromise { + + if (!this._raw) { + return TPromise.wrapError(new Error('no debug adapter')); + } + + if (!this._raw.readyForBreakpoints) { + return TPromise.as(undefined); + } + + const source = this.getSourceForUri(modelUri); + let rawSource: DebugProtocol.Source; + if (source) { + rawSource = source.raw; + } else { + const data = Source.getEncodedDebugData(modelUri); + rawSource = { name: data.name, path: data.path, sourceReference: data.sourceReference }; + } + + if (breakpointsToSend.length && !rawSource.adapterData) { + rawSource.adapterData = breakpointsToSend[0].adapterData; + } + // Normalize all drive letters going out from vscode to debug adapters so we are consistent with our resolving #43959 + rawSource.path = normalizeDriveLetter(rawSource.path); + + return this._raw.setBreakpoints({ + source: rawSource, + lines: breakpointsToSend.map(bp => bp.lineNumber), + breakpoints: breakpointsToSend.map(bp => ({ line: bp.lineNumber, column: bp.column, condition: bp.condition, hitCondition: bp.hitCondition, logMessage: bp.logMessage })), + sourceModified + }).then(response => { + + if (response && response.body) { + const data: ActualBreakpoints = Object.create(null); + for (let i = 0; i < breakpointsToSend.length; i++) { + data[breakpointsToSend[i].getId()] = response.body.breakpoints[i]; + } + this.model.setBreakpointSessionData(this.getId(), data); + return data; + } + return undefined; + }); + } + + sendFunctionBreakpoints(fbpts: IFunctionBreakpoint[]): TPromise { + if (this._raw) { + if (this._raw.readyForBreakpoints) { + return this._raw.setFunctionBreakpoints({ breakpoints: fbpts }).then(response => { + if (response && response.body) { + const data: ActualBreakpoints = Object.create(null); + for (let i = 0; i < fbpts.length; i++) { + data[fbpts[i].getId()] = response.body.breakpoints[i]; + } + return data; + } + return undefined; + }); + } + return TPromise.as(undefined); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): TPromise { + if (this._raw) { + if (this._raw.readyForBreakpoints && exbpts.length > 0) { + return this._raw.setExceptionBreakpoints({ filters: exbpts.map(exb => exb.filter) }); + } + return TPromise.as(null); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + customRequest(request: string, args: any): TPromise { + if (this._raw) { + return this._raw.custom(request, args); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + stackTrace(threadId: number, startFrame: number, levels: number): TPromise { + if (this._raw) { + return this._raw.stackTrace({ threadId, startFrame, levels }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + exceptionInfo(threadId: number): TPromise { + if (this._raw) { + return this._raw.exceptionInfo({ threadId }).then(response => { + if (response) { + return { + id: response.body.exceptionId, + description: response.body.description, + breakMode: response.body.breakMode, + details: response.body.details + }; + } + return null; + }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + scopes(frameId: number): TPromise { + if (this._raw) { + return this._raw.scopes({ frameId }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + variables(variablesReference: number, filter: 'indexed' | 'named', start: number, count: number): TPromise { + if (this._raw) { + return this._raw.variables({ variablesReference, filter, start, count }); + } + return TPromise.as(undefined); + } + + evaluate(expression: string, frameId: number, context?: string): TPromise { + if (this._raw) { + return this._raw.evaluate({ expression, frameId, context }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + restartFrame(frameId: number, threadId: number): TPromise { + if (this._raw) { + return this._raw.restartFrame({ frameId }, threadId); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + next(threadId: number): TPromise { + if (this._raw) { + return this._raw.next({ threadId }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + stepIn(threadId: number): TPromise { + if (this._raw) { + return this._raw.stepIn({ threadId }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + stepOut(threadId: number): TPromise { + if (this._raw) { + return this._raw.stepOut({ threadId }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + stepBack(threadId: number): TPromise { + if (this._raw) { + return this._raw.stepBack({ threadId }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + continue(threadId: number): TPromise { + if (this._raw) { + return this._raw.continue({ threadId }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + reverseContinue(threadId: number): TPromise { + if (this._raw) { + return this._raw.reverseContinue({ threadId }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + pause(threadId: number): TPromise { + if (this._raw) { + return this._raw.pause({ threadId }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + terminateThreads(threadIds?: number[]): TPromise { + if (this._raw) { + return this._raw.terminateThreads({ threadIds }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + setVariable(variablesReference: number, name: string, value: string): TPromise { + if (this._raw) { + return this._raw.setVariable({ variablesReference, name, value }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + loadSource(resource: uri): TPromise { + + if (!this._raw) { + return TPromise.wrapError(new Error('no debug adapter')); + } + + const source = this.getSourceForUri(resource); + let rawSource: DebugProtocol.Source; + if (source) { + rawSource = source.raw; + } else { + // create a Source + + let sourceRef: number; + if (resource.query) { + const data = Source.getEncodedDebugData(resource); + sourceRef = data.sourceReference; + } + + rawSource = { + path: resource.with({ scheme: '', query: '' }).toString(true), // Remove debug: scheme + sourceReference: sourceRef + }; + } + + return this._raw.source({ sourceReference: rawSource.sourceReference, source: rawSource }); + } + + getLoadedSources(): TPromise { + if (this._raw) { + return this._raw.loadedSources({}).then(response => { + return response.body.sources.map(src => this.getSource(src)); + }, () => { + return []; + }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + completions(frameId: number, text: string, position: Position, overwriteBefore: number): TPromise { + if (this._raw) { + return this._raw.completions({ + frameId, + text, + column: position.column, + line: position.lineNumber + }).then(response => { + + const result: ISuggestion[] = []; + if (response && response.body && response.body.targets) { + response.body.targets.forEach(item => { + if (item && item.label) { + result.push({ + label: item.label, + insertText: item.text || item.label, + type: item.type, + filterText: item.start && item.length && text.substr(item.start, item.length).concat(item.label), + overwriteBefore: item.length || overwriteBefore + }); + } + }); + } + + return result; + }); + } + return TPromise.wrapError(new Error('no debug adapter')); + } + + //---- threads + + getThread(threadId: number): Thread { + return this.threads.get(threadId); + } + + getAllThreads(): IThread[] { + const result: IThread[] = []; + this.threads.forEach(t => result.push(t)); + return result; + } + + clearThreads(removeThreads: boolean, reference: number = undefined): void { + if (reference !== undefined && reference !== null) { + if (this.threads.has(reference)) { + const thread = this.threads.get(reference); + thread.clearCallStack(); + thread.stoppedDetails = undefined; + thread.stopped = false; + + if (removeThreads) { + this.threads.delete(reference); + } + } + } else { + this.threads.forEach(thread => { + thread.clearCallStack(); + thread.stoppedDetails = undefined; + thread.stopped = false; + }); + + if (removeThreads) { + this.threads.clear(); + ExpressionContainer.allValues.clear(); + } + } + } + + rawUpdate(data: IRawModelUpdate): void { + + if (data.thread && !this.threads.has(data.threadId)) { + // A new thread came in, initialize it. + this.threads.set(data.threadId, new Thread(this, data.thread.name, data.thread.id)); + } else if (data.thread && data.thread.name) { + // Just the thread name got updated #18244 + this.threads.get(data.threadId).name = data.thread.name; + } + + if (data.stoppedDetails) { + // Set the availability of the threads' callstacks depending on + // whether the thread is stopped or not + if (data.stoppedDetails.allThreadsStopped) { + this.threads.forEach(thread => { + thread.stoppedDetails = thread.threadId === data.threadId ? data.stoppedDetails : { reason: undefined }; + thread.stopped = true; + thread.clearCallStack(); + }); + } else if (this.threads.has(data.threadId)) { + // One thread is stopped, only update that thread. + const thread = this.threads.get(data.threadId); + thread.stoppedDetails = data.stoppedDetails; + thread.clearCallStack(); + thread.stopped = true; + } + } + } + + private fetchThreads(stoppedDetails?: IRawStoppedDetails): TPromise { + return this._raw.threads().then(response => { + if (response && response.body && response.body.threads) { + response.body.threads.forEach(thread => { + this.model.rawUpdate({ + sessionId: this.getId(), + threadId: thread.id, + thread, + stoppedDetails: stoppedDetails && thread.id === stoppedDetails.threadId ? stoppedDetails : undefined + }); + }); + } + }); + } + + //---- private + private registerListeners(): void { this.rawListeners.push(this._raw.onDidInitialize(() => { @@ -292,8 +712,8 @@ export class DebugSession implements IDebugSession { this._onDidCustomEvent.fire(event); })); - this.rawListeners.push(this._raw.onDidExitAdapter(error => { - this._onDidExitAdapter.fire(error); + this.rawListeners.push(this._raw.onDidExitAdapter(event => { + this._onDidEndAdapter.fire(event); })); } @@ -302,7 +722,7 @@ export class DebugSession implements IDebugSession { this.model.clearThreads(this.getId(), true); this.model.removeSession(this.getId()); this.fetchThreadsScheduler = undefined; - if (this._raw && !this._raw.disconnected) { + if (this._raw) { this._raw.disconnect(); } this._raw = undefined; @@ -329,129 +749,4 @@ export class DebugSession implements IDebugSession { return source; } - getLoadedSources(): TPromise { - return this._raw.loadedSources({}).then(response => { - return response.body.sources.map(src => this.getSource(src)); - }, () => { - return []; - }); - } - - get onDidLoadedSource(): Event { - return this._onDidLoadedSource.event; - } - - //---- completions - - completions(frameId: number, text: string, position: Position, overwriteBefore: number): TPromise { - if (!this._raw.capabilities.supportsCompletionsRequest) { - return TPromise.as([]); - } - - return this._raw.completions({ - frameId, - text, - column: position.column, - line: position.lineNumber - }).then(response => { - const result: ISuggestion[] = []; - if (response && response.body && response.body.targets) { - response.body.targets.forEach(item => { - if (item && item.label) { - result.push({ - label: item.label, - insertText: item.text || item.label, - type: item.type, - filterText: item.start && item.length && text.substr(item.start, item.length).concat(item.label), - overwriteBefore: item.length || overwriteBefore - }); - } - }); - } - - return result; - }, () => []); - } - - //---- threads - - getThread(threadId: number): Thread { - return this.threads.get(threadId); - } - - getAllThreads(): IThread[] { - const result: IThread[] = []; - this.threads.forEach(t => result.push(t)); - return result; - } - - clearThreads(removeThreads: boolean, reference: number = undefined): void { - if (reference !== undefined && reference !== null) { - if (this.threads.has(reference)) { - const thread = this.threads.get(reference); - thread.clearCallStack(); - thread.stoppedDetails = undefined; - thread.stopped = false; - - if (removeThreads) { - this.threads.delete(reference); - } - } - } else { - this.threads.forEach(thread => { - thread.clearCallStack(); - thread.stoppedDetails = undefined; - thread.stopped = false; - }); - - if (removeThreads) { - this.threads.clear(); - ExpressionContainer.allValues.clear(); - } - } - } - - rawUpdate(data: IRawModelUpdate): void { - - if (data.thread && !this.threads.has(data.threadId)) { - // A new thread came in, initialize it. - this.threads.set(data.threadId, new Thread(this, data.thread.name, data.thread.id)); - } else if (data.thread && data.thread.name) { - // Just the thread name got updated #18244 - this.threads.get(data.threadId).name = data.thread.name; - } - - if (data.stoppedDetails) { - // Set the availability of the threads' callstacks depending on - // whether the thread is stopped or not - if (data.stoppedDetails.allThreadsStopped) { - this.threads.forEach(thread => { - thread.stoppedDetails = thread.threadId === data.threadId ? data.stoppedDetails : { reason: undefined }; - thread.stopped = true; - thread.clearCallStack(); - }); - } else if (this.threads.has(data.threadId)) { - // One thread is stopped, only update that thread. - const thread = this.threads.get(data.threadId); - thread.stoppedDetails = data.stoppedDetails; - thread.clearCallStack(); - thread.stopped = true; - } - } - } - - private fetchThreads(stoppedDetails?: IRawStoppedDetails): TPromise { - return this._raw.threads().then(response => { - if (response && response.body && response.body.threads) { - response.body.threads.forEach(thread => { - this.model.rawUpdate({ - sessionId: this.getId(), - threadId: thread.id, - thread, - stoppedDetails: stoppedDetails && thread.id === stoppedDetails.threadId ? stoppedDetails : undefined - }); - }); - } - }); - } } diff --git a/src/vs/workbench/parts/debug/electron-browser/electronDebugActions.ts b/src/vs/workbench/parts/debug/electron-browser/electronDebugActions.ts index c7745f86c64..ca825394e33 100644 --- a/src/vs/workbench/parts/debug/electron-browser/electronDebugActions.ts +++ b/src/vs/workbench/parts/debug/electron-browser/electronDebugActions.ts @@ -26,7 +26,7 @@ export class CopyValueAction extends Action { if (this.value instanceof Variable) { const frameId = this.debugService.getViewModel().focusedStackFrame.frameId; const session = this.debugService.getViewModel().focusedSession; - return session.raw.evaluate({ expression: this.value.evaluateName, frameId }).then(result => { + return session.evaluate(this.value.evaluateName, frameId).then(result => { clipboard.writeText(result.body.result); }, err => clipboard.writeText(this.value.value)); } diff --git a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts index e74cc626a63..473552f09e4 100644 --- a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts +++ b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts @@ -10,29 +10,28 @@ import { Action } from 'vs/base/common/actions'; import * as errors from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { Debugger } from 'vs/workbench/parts/debug/node/debugger'; import { IOutputService } from 'vs/workbench/parts/output/common/output'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { formatPII } from 'vs/workbench/parts/debug/common/debugUtils'; -import { SocketDebugAdapter } from 'vs/workbench/parts/debug/node/debugAdapter'; -import { IRawDebugSession, IDebugAdapter, IConfig } from 'vs/workbench/parts/debug/common/debug'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IDebugAdapter, IConfig, AdapterEndEvent, IDebugger } from 'vs/workbench/parts/debug/common/debug'; -export class RawDebugSession implements IRawDebugSession { +export class RawDebugSession { private debugAdapter: IDebugAdapter; private cachedInitDebugAdapterP: TPromise; - private startTime: number; - private terminated: boolean; - private cancellationTokens: CancellationTokenSource[]; private allThreadsContinued: boolean; - private isAttached: boolean; - - private _emittedStopped: boolean; private _readyForBreakpoints: boolean; - private _disconnected: boolean; private _capabilities: DebugProtocol.Capabilities; + // shutdown + private inShutdown: boolean; + private terminated: boolean; + private firedAdapterExitEvent: boolean; + + // telemetry + private startTime: number; + private emittedStopped: boolean; + // DAP events private readonly _onDidInitialize: Emitter; private readonly _onDidStop: Emitter; @@ -47,20 +46,23 @@ export class RawDebugSession implements IRawDebugSession { private readonly _onDidEvent: Emitter; // DA events - private readonly _onDidExitAdapter: Emitter; + private readonly _onDidExitAdapter: Emitter; constructor( private debugServerPort: number, - private _debugger: Debugger, + private _debugger: IDebugger, public customTelemetryService: ITelemetryService, private root: IWorkspaceFolder, @ITelemetryService private telemetryService: ITelemetryService, @IOutputService private outputService: IOutputService ) { - this._emittedStopped = false; this._readyForBreakpoints = false; + this.inShutdown = false; + this.firedAdapterExitEvent = false; + + this.emittedStopped = false; + this.allThreadsContinued = true; - this.cancellationTokens = []; this._onDidInitialize = new Emitter(); this._onDidStop = new Emitter(); @@ -74,33 +76,25 @@ export class RawDebugSession implements IRawDebugSession { this._onDidCustomEvent = new Emitter(); this._onDidEvent = new Emitter(); - this._onDidExitAdapter = new Emitter(); + this._onDidExitAdapter = new Emitter(); } - public get onDidExitAdapter(): Event { + public get onDidExitAdapter(): Event { return this._onDidExitAdapter.event; } - public get sessionLengthInSeconds(): number { - return (new Date().getTime() - this.startTime) / 1000; - } - public get capabilities(): DebugProtocol.Capabilities { return this._capabilities || {}; } + /** + * DA is ready to accepts setBreakpoint requests. + * Becomes true after "initialized" events has been received. + */ public get readyForBreakpoints(): boolean { return this._readyForBreakpoints; } - public get emittedStopped(): boolean { - return this._emittedStopped; - } - - public get disconnected(): boolean { - return this._disconnected; - } - //---- DAP events public get onDidInitialize(): Event { @@ -150,20 +144,26 @@ export class RawDebugSession implements IRawDebugSession { //---- DAP requests public initialize(args: DebugProtocol.InitializeRequestArguments): TPromise { - return this.send('initialize', args).then(response => { - this.readCapabilities(response); + return this.send('initialize', args).then((response: DebugProtocol.InitializeResponse) => { + this.mergeCapabilities(response.body); return response; }); } public launchOrAttach(config: IConfig): TPromise { - this.isAttached = config.request === 'attach'; - return this.send(this.isAttached ? 'attach' : 'launch', config).then(response => { - this.readCapabilities(response); + return this.send(config.request, config).then(response => { + this.mergeCapabilities(response.body); return response; }); } + public restart(): TPromise { + if (this.capabilities.supportsRestartRequest) { + return this.send('restart', null); + } + return TPromise.wrapError(new Error('restart not supported')); + } + public next(args: DebugProtocol.NextArguments): TPromise { return this.send('next', args).then(response => { this.fireSimulatedContinuedEvent(args.threadId); @@ -215,16 +215,31 @@ export class RawDebugSession implements IRawDebugSession { } public completions(args: DebugProtocol.CompletionsArguments): TPromise { - return this.send('completions', args); + if (this.capabilities.supportsCompletionsRequest) { + return this.send('completions', args); + } + return TPromise.wrapError(new Error('completions not supported')); } + /** + * Try terminate the debuggee softly + */ public terminate(restart = false): TPromise { - - if (this.capabilities.supportsTerminateRequest && !this.terminated && !this.isAttached) { - this.terminated = true; - return this.send('terminate', { restart }); + if (this.capabilities.supportsTerminateRequest) { + if (!this.terminated) { + this.terminated = true; + return this.send('terminate', { restart }); + } + return this.disconnect(restart); } - return this.disconnect(restart); + return TPromise.wrapError(new Error('terminated not supported')); + } + + /** + * Terminate the debuggee + */ + public disconnect(restart = false): TPromise { + return this.shutdown(undefined, restart); } public setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): TPromise { @@ -232,7 +247,10 @@ export class RawDebugSession implements IRawDebugSession { } public setFunctionBreakpoints(args: DebugProtocol.SetFunctionBreakpointsArguments): TPromise { - return this.send('setFunctionBreakpoints', args); + if (this.capabilities.supportsFunctionBreakpoints) { + return this.send('setFunctionBreakpoints', args); + } + return TPromise.wrapError(new Error('setFunctionBreakpoints not supported')); } public setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): TPromise { @@ -264,7 +282,10 @@ export class RawDebugSession implements IRawDebugSession { } public loadedSources(args: DebugProtocol.LoadedSourcesArguments): TPromise { - return this.send('loadedSources', args); + if (this.capabilities.supportsLoadedSourcesRequest) { + return this.send('loadedSources', args); + } + return TPromise.wrapError(new Error('loadedSources not supported')); } public threads(): TPromise { @@ -297,27 +318,6 @@ export class RawDebugSession implements IRawDebugSession { return this.send(request, args); } - public disconnect(restart = false): TPromise { - if (this.disconnected) { - return this.stopAdapter(); - } - - // Cancel all sent promises on disconnect so debug trees are not left in a broken state #3666. - // Give a 1s timeout to give a chance for some promises to complete. - setTimeout(() => { - this.cancellationTokens.forEach(token => token.cancel()); - this.cancellationTokens = []; - }, 1000); - - if (this.debugAdapter && !this.disconnected) { - // point of no return: from now on don't report any errors - this._disconnected = true; - return this.send('disconnect', { restart }, false).then(() => this.stopAdapter(), () => this.stopAdapter()); - } - - return TPromise.as(null); - } - //---- private private startAdapter(): TPromise { @@ -329,11 +329,19 @@ export class RawDebugSession implements IRawDebugSession { this.debugAdapter = debugAdapter; this.debugAdapter.onError(err => { - // TODO: don't stop server on this layer - this.stopAdapter(err); + this.shutdown(err); }); + + this.debugAdapter.onExit(code => { + if (code !== 0) { + this.shutdown(new Error(`exit code: ${code}`)); + } else { + // normal exit + this.shutdown(); + } + }); + this.debugAdapter.onEvent(event => this.onDapEvent(event)); - this.debugAdapter.onExit(code => this.onDebugAdapterExit(code)); this.debugAdapter.onRequest(request => this.dispatchRequest(request)); return this.debugAdapter.startSession(); @@ -349,6 +357,52 @@ export class RawDebugSession implements IRawDebugSession { return this.cachedInitDebugAdapterP; } + private shutdown(error?: Error, restart = false): TPromise { + if (!this.inShutdown) { + this.inShutdown = true; + if (this.debugAdapter) { + return this.send('disconnect', { restart }, 500).then(() => { + this.stopAdapter(error); + }, () => { + // ignore error + this.stopAdapter(error); + }); + } + return this.stopAdapter(error); + } + return TPromise.as(undefined); + } + + private stopAdapter(error?: Error): TPromise { + if (this.debugAdapter) { + const da = this.debugAdapter; + this.debugAdapter = null; + return da.stopSession().then(_ => { + this.fireAdapterExitEvent(error); + }, err => { + this.fireAdapterExitEvent(error); + }); + } else { + this.fireAdapterExitEvent(error); + } + return TPromise.as(undefined); + } + + private fireAdapterExitEvent(error?: Error): void { + if (!this.firedAdapterExitEvent) { + this.firedAdapterExitEvent = true; + + const e: AdapterEndEvent = { + emittedStopped: this.emittedStopped, + sessionLengthInSeconds: (new Date().getTime() - this.startTime) / 1000 + }; + if (error) { + e.error = error; + } + this._onDidExitAdapter.fire(e); + } + } + private onDapEvent(event: DebugProtocol.Event): void { switch (event.event) { @@ -362,11 +416,11 @@ export class RawDebugSession implements IRawDebugSession { case 'capabilities': if (event.body) { const capabilites = (event).body.capabilities; - this._capabilities = objects.mixin(this._capabilities, capabilites); + this.mergeCapabilities(capabilites); } break; case 'stopped': - this._emittedStopped = true; + this.emittedStopped = true; this._onDidStop.fire(event); break; case 'continued': @@ -395,16 +449,6 @@ export class RawDebugSession implements IRawDebugSession { this._onDidEvent.fire(event); } - private onDebugAdapterExit(code: number): void { - this.debugAdapter = null; - if (!this.disconnected && code !== 0) { - this._onDidExitAdapter.fire(new Error(`exit code: ${code}`)); - } else { - // normal exit - this._onDidExitAdapter.fire(); - } - } - private dispatchRequest(request: DebugProtocol.Request): void { const response: DebugProtocol.Response = { @@ -451,68 +495,53 @@ export class RawDebugSession implements IRawDebugSession { } } - private send(command: string, args: any, cancelOnDisconnect = true): TPromise { + private send(command: string, args: any, timeout?: number): TPromise { return this.startAdapter().then(() => { - const cancellationSource = new CancellationTokenSource(); - - const promise = this.internalSend(command, args, cancellationSource.token).then( - response => { - return response; - }, - (errorResponse: DebugProtocol.ErrorResponse) => { - - if (errors.isPromiseCanceledError(errorResponse)) { - return TPromise.wrapError(errorResponse); + return new TPromise((completeDispatch, errorDispatch) => { + this.debugAdapter.sendRequest(command, args, (response: R) => { + if (response.success) { + completeDispatch(response); + } else { + errorDispatch(response); } - - const error = errorResponse && errorResponse.body ? errorResponse.body.error : null; - const errorMessage = errorResponse ? errorResponse.message : ''; - - if (error && error.sendTelemetry) { - const telemetryMessage = error ? formatPII(error.format, true, error.variables) : errorMessage; - this.telemetryDebugProtocolErrorResponse(telemetryMessage); - } - - const userMessage = error ? formatPII(error.format, false, error.variables) : errorMessage; - if (error && error.url) { - const label = error.urlLabel ? error.urlLabel : nls.localize('moreInfo', "More Info"); - return TPromise.wrapError(errors.create(userMessage, { - actions: [new Action('debug.moreInfo', label, null, true, () => { - window.open(error.url); - return TPromise.as(null); - })] - })); - } - - return TPromise.wrapError(new Error(userMessage)); - } - ); - - if (cancelOnDisconnect) { - this.cancellationTokens.push(cancellationSource); - } - return promise; + }, timeout); + }).then(response => response, err => TPromise.wrapError(this.handleErrorResponse(err))); }); } - private internalSend(command: string, args: any, cancelationToken: CancellationToken): TPromise { - return new TPromise((completeDispatch, errorDispatch) => { - cancelationToken.onCancellationRequested(() => errorDispatch(errors.canceled())); - this.debugAdapter.sendRequest(command, args, (result: R) => { - if (result.success) { - completeDispatch(result); - } else { - errorDispatch(result); - } + private handleErrorResponse(errorResponse: DebugProtocol.Response): Error { + + if (errorResponse.command === 'canceled' && errorResponse.message === 'canceled') { + return errors.canceled(); + } + + const error = errorResponse && errorResponse.body ? errorResponse.body.error : null; + const errorMessage = errorResponse ? errorResponse.message : ''; + + if (error && error.sendTelemetry) { + const telemetryMessage = error ? formatPII(error.format, true, error.variables) : errorMessage; + this.telemetryDebugProtocolErrorResponse(telemetryMessage); + } + + const userMessage = error ? formatPII(error.format, false, error.variables) : errorMessage; + if (error && error.url) { + const label = error.urlLabel ? error.urlLabel : nls.localize('moreInfo', "More Info"); + return errors.create(userMessage, { + actions: [new Action('debug.moreInfo', label, null, true, () => { + window.open(error.url); + return TPromise.as(null); + })] }); - }); + } + + return new Error(userMessage); } - private readCapabilities(response: DebugProtocol.Response): void { - if (response) { - this._capabilities = objects.mixin(this._capabilities, response.body); + private mergeCapabilities(capabilities: DebugProtocol.Capabilities): void { + if (capabilities) { + this._capabilities = objects.mixin(this._capabilities, capabilities); } } @@ -528,21 +557,6 @@ export class RawDebugSession implements IRawDebugSession { }); } - private stopAdapter(error?: Error): TPromise { - - if (/* this.socket !== null */ this.debugAdapter instanceof SocketDebugAdapter) { - this.debugAdapter.stopSession(); - } - - this._onDidExitAdapter.fire(error); - this._disconnected = true; - if (!this.debugAdapter || this.debugAdapter instanceof SocketDebugAdapter) { - return TPromise.as(null); - } - - return this.debugAdapter.stopSession(); - } - private telemetryDebugProtocolErrorResponse(telemetryMessage: string) { /* __GDPR__ "debugProtocolErrorResponse" : { diff --git a/src/vs/workbench/parts/debug/electron-browser/repl.ts b/src/vs/workbench/parts/debug/electron-browser/repl.ts index ed14586db3a..9ebd5e49f77 100644 --- a/src/vs/workbench/parts/debug/electron-browser/repl.ts +++ b/src/vs/workbench/parts/debug/electron-browser/repl.ts @@ -180,16 +180,23 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati provideCompletionItems: (model: ITextModel, position: Position, _context: modes.SuggestContext, token: CancellationToken): Thenable => { // Disable history navigation because up and down are used to navigate through the suggest widget this.historyNavigationEnablement.set(false); - const word = this.replInput.getModel().getWordAtPosition(position); - const overwriteBefore = word ? word.word.length : 0; - const text = this.replInput.getModel().getLineContent(position.lineNumber); - const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; - const frameId = focusedStackFrame ? focusedStackFrame.frameId : undefined; + const focusedSession = this.debugService.getViewModel().focusedSession; - const completions = focusedSession ? focusedSession.completions(frameId, text, position, overwriteBefore) : TPromise.as([]); - return completions.then(suggestions => ({ - suggestions - })); + if (focusedSession && focusedSession.capabilities.supportsCompletionsRequest) { + + const word = this.replInput.getModel().getWordAtPosition(position); + const overwriteBefore = word ? word.word.length : 0; + const text = this.replInput.getModel().getLineContent(position.lineNumber); + const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; + const frameId = focusedStackFrame ? focusedStackFrame.frameId : undefined; + + return focusedSession.completions(frameId, text, position, overwriteBefore).then(suggestions => { + return { suggestions }; + }, err => { + return { suggestions: [] }; + }); + } + return TPromise.as({ suggestions: [] }); } }); diff --git a/src/vs/workbench/parts/debug/node/debugAdapter.ts b/src/vs/workbench/parts/debug/node/debugAdapter.ts index ed91f37b182..12bb2e8e956 100644 --- a/src/vs/workbench/parts/debug/node/debugAdapter.ts +++ b/src/vs/workbench/parts/debug/node/debugAdapter.ts @@ -35,7 +35,7 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { constructor() { this.sequence = 1; - this.pendingRequests = new Map void>(); + this.pendingRequests = new Map(); this._onError = new Emitter(); this._onExit = new Emitter(); @@ -79,7 +79,7 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { } } - public sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void): void { + public sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void, timeout?: number): void { const request: any = { command: command @@ -90,6 +90,25 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { this.internalSend('request', request); + if (typeof timeout === 'number') { + const timer = setTimeout(() => { + clearTimeout(timer); + const clb = this.pendingRequests.get(request.seq); + if (clb) { + this.pendingRequests.delete(request.seq); + const err: DebugProtocol.Response = { + type: 'response', + seq: 0, + request_seq: request.seq, + success: false, + command, + message: `timeout after ${timeout} ms` + }; + clb(err); + } + }, timeout); + } + if (clb) { // store callback for this request this.pendingRequests.set(request.seq, clb); @@ -126,6 +145,24 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { this.sendMessage(message); } + + protected cancelPending() { + const pending = this.pendingRequests; + this.pendingRequests = new Map(); + setTimeout(_ => { + pending.forEach((callback, request_seq) => { + const err: DebugProtocol.Response = { + type: 'response', + seq: 0, + request_seq, + success: false, + command: 'canceled', + message: 'canceled' + }; + callback(err); + }); + }, 1000); + } } /** @@ -145,7 +182,7 @@ export abstract class StreamDebugAdapter extends AbstractDebugAdapter { super(); } - public connect(readable: stream.Readable, writable: stream.Writable): void { + protected connect(readable: stream.Readable, writable: stream.Writable): void { this.outputStream = writable; this.rawData = Buffer.allocUnsafe(0); @@ -153,16 +190,16 @@ export abstract class StreamDebugAdapter extends AbstractDebugAdapter { readable.on('data', (data: Buffer) => this.handleData(data)); - // readable.on('close', () => { - // this._emitEvent(new Event('close')); - // }); - // readable.on('error', (error) => { - // this._emitEvent(new Event('error', 'readable error: ' + (error && error.message))); - // }); + readable.on('close', () => { + this._onError.fire(new Error('readable.close event')); + }); + readable.on('error', (error) => { + this._onError.fire(error); + }); - // writable.on('error', (error) => { - // this._emitEvent(new Event('error', 'writable error: ' + (error && error.message))); - // }); + writable.on('error', (error) => { + this._onError.fire(error); + }); } public sendMessage(message: DebugProtocol.ProtocolMessage): void { @@ -237,11 +274,15 @@ export class SocketDebugAdapter extends StreamDebugAdapter { } stopSession(): TPromise { + + // Cancel all sent promises on disconnect so debug trees are not left in a broken state #3666. + this.cancelPending(); + if (this.socket) { this.socket.end(); this.socket = undefined; } - return void 0; + return TPromise.as(undefined); } } @@ -322,6 +363,9 @@ export class DebugAdapter extends StreamDebugAdapter { stopSession(): TPromise { + // Cancel all sent promises on disconnect so debug trees are not left in a broken state #3666. + this.cancelPending(); + if (!this.serverProcess) { return TPromise.as(null); } diff --git a/src/vs/workbench/parts/debug/node/debugger.ts b/src/vs/workbench/parts/debug/node/debugger.ts index 257f14ceeaf..169c8e1fab5 100644 --- a/src/vs/workbench/parts/debug/node/debugger.ts +++ b/src/vs/workbench/parts/debug/node/debugger.ts @@ -11,7 +11,7 @@ import * as objects from 'vs/base/common/objects'; import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc'; import { IJSONSchema, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IConfig, IDebuggerContribution, IAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager, IDebugAdapter, IDebugConfiguration, ITerminalSettings } from 'vs/workbench/parts/debug/common/debug'; +import { IConfig, IDebuggerContribution, IAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager, IDebugAdapter, IDebugConfiguration, ITerminalSettings, IDebugger } from 'vs/workbench/parts/debug/common/debug'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -24,7 +24,7 @@ import { memoize } from 'vs/base/common/decorators'; import { TaskDefinitionRegistry } from 'vs/workbench/parts/tasks/common/taskDefinitionRegistry'; import { getPathFromAmdModule } from 'vs/base/common/amd'; -export class Debugger { +export class Debugger implements IDebugger { private mergedExtensionDescriptions: IExtensionDescription[]; diff --git a/src/vs/workbench/parts/debug/test/common/mockDebug.ts b/src/vs/workbench/parts/debug/test/common/mockDebug.ts index f1f4a4d9942..bd2be3b46d2 100644 --- a/src/vs/workbench/parts/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/parts/debug/test/common/mockDebug.ts @@ -8,7 +8,7 @@ import { Event } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { Position } from 'vs/editor/common/core/position'; -import { ILaunch, IDebugService, State, IDebugSession, IConfigurationManager, IStackFrame, IBreakpointData, IBreakpointUpdateData, IConfig, IModel, IViewModel, IRawDebugSession, IBreakpoint, LoadedSourceEvent, IThread, IRawModelUpdate } from 'vs/workbench/parts/debug/common/debug'; +import { ILaunch, IDebugService, State, IDebugSession, IConfigurationManager, IStackFrame, IBreakpointData, IBreakpointUpdateData, IConfig, IModel, IViewModel, IBreakpoint, LoadedSourceEvent, IThread, IRawModelUpdate, ActualBreakpoints, IFunctionBreakpoint, IExceptionBreakpoint, IDebugger, IExceptionInfo, AdapterEndEvent } from 'vs/workbench/parts/debug/common/debug'; import { Source } from 'vs/workbench/parts/debug/common/debugSource'; import { ISuggestion } from 'vs/editor/common/modes'; @@ -129,18 +129,19 @@ export class MockDebugService implements IDebugService { export class MockSession implements IDebugSession { configuration: IConfig = { type: 'mock', request: 'launch' }; - raw: IRawDebugSession = new MockRawSession(); + unresolvedConfiguration: IConfig = { type: 'mock', request: 'launch' }; state = State.Stopped; root: IWorkspaceFolder; + capabilities: DebugProtocol.Capabilities = {}; + + getId(): string { + return 'mock'; + } getName(includeRoot: boolean): string { return 'mockname'; } - public get capabilities(): DebugProtocol.Capabilities { - return {}; - } - getSourceForUri(modelUri: uri): Source { return null; } @@ -157,7 +158,11 @@ export class MockSession implements IDebugSession { return null; } - get onDidExitAdapter(): Event { + get onDidChangeState(): Event { + return null; + } + + get onDidEndAdapter(): Event { return null; } @@ -181,14 +186,87 @@ export class MockSession implements IDebugSession { rawUpdate(data: IRawModelUpdate): void { } - getId(): string { - return 'mock'; + initialize(dbgr: IDebugger): TPromise { + throw new Error('Method not implemented.'); + } + launchOrAttach(config: IConfig): TPromise { + throw new Error('Method not implemented.'); + } + restart(): TPromise { + throw new Error('Method not implemented.'); + } + sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): TPromise { + throw new Error('Method not implemented.'); + } + sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): TPromise { + throw new Error('Method not implemented.'); + } + sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): TPromise { + throw new Error('Method not implemented.'); + } + customRequest(request: string, args: any): TPromise { + throw new Error('Method not implemented.'); + } + stackTrace(threadId: number, startFrame: number, levels: number): TPromise { + throw new Error('Method not implemented.'); + } + exceptionInfo(threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + scopes(frameId: number): TPromise { + throw new Error('Method not implemented.'); + } + variables(variablesReference: number, filter: 'indexed' | 'named', start: number, count: number): TPromise { + throw new Error('Method not implemented.'); + } + evaluate(expression: string, frameId: number, context?: string): TPromise { + throw new Error('Method not implemented.'); + } + restartFrame(frameId: number, threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + next(threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + stepIn(threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + stepOut(threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + stepBack(threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + continue(threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + reverseContinue(threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + pause(threadId: number): TPromise { + throw new Error('Method not implemented.'); + } + terminateThreads(threadIds: number[]): TPromise { + throw new Error('Method not implemented.'); + } + setVariable(variablesReference: number, name: string, value: string): TPromise { + throw new Error('Method not implemented.'); + } + loadSource(resource: uri): TPromise { + throw new Error('Method not implemented.'); + } + + terminate(restart = false): TPromise { + throw new Error('Method not implemented.'); + } + disconnect(restart = false): TPromise { + throw new Error('Method not implemented.'); } dispose(): void { } } -export class MockRawSession implements IRawDebugSession { +export class MockRawSession { capabilities: DebugProtocol.Capabilities; disconnected: boolean;