diff --git a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts index b98563c70b1..3769f8b0c6b 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts @@ -29,14 +29,22 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb private _debugAdapters: Map; private _debugAdaptersHandleCounter = 1; - constructor( extHostContext: IExtHostContext, @IDebugService private debugService: IDebugService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDebugService); this._toDispose = []; - this._toDispose.push(debugService.onDidNewSession(proc => this._proxy.$acceptDebugSessionStarted(proc.getId(), proc.configuration.type, proc.getName(false)))); + this._toDispose.push(debugService.onDidNewSession(session => { + this._proxy.$acceptDebugSessionStarted(session.getId(), session.configuration.type, session.getName(false)); + this._toDispose.push(session.onDidCustomEvent(event => { + if (event && event.sessionId) { + if (process) { + this._proxy.$acceptDebugSessionCustomEvent(event.sessionId, session.configuration.type, session.configuration.name, event); + } + } + })); + })); this._toDispose.push(debugService.onDidEndSession(proc => this._proxy.$acceptDebugSessionTerminated(proc.getId(), proc.configuration.type, proc.getName(false)))); this._toDispose.push(debugService.getViewModel().onDidFocusSession(proc => { if (proc) { @@ -46,14 +54,6 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } })); - this._toDispose.push(debugService.onDidCustomEvent(event => { - if (event && event.sessionId) { - const process = this.debugService.getModel().getSessions().filter(p => p.getId() === event.sessionId).pop(); - if (process) { - this._proxy.$acceptDebugSessionCustomEvent(event.sessionId, process.configuration.type, process.configuration.name, event); - } - } - })); this._debugAdapters = new Map(); } diff --git a/src/vs/workbench/parts/debug/browser/debugActions.ts b/src/vs/workbench/parts/debug/browser/debugActions.ts index 6191db0921c..1d35bf5ea64 100644 --- a/src/vs/workbench/parts/debug/browser/debugActions.ts +++ b/src/vs/workbench/parts/debug/browser/debugActions.ts @@ -12,7 +12,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IFileService } from 'vs/platform/files/common/files'; -import { IDebugService, State, ISession, IThread, IEnablement, IBreakpoint, IStackFrame, REPL_ID, SessionState } +import { IDebugService, State, ISession, IThread, IEnablement, IBreakpoint, IStackFrame, REPL_ID } from 'vs/workbench/parts/debug/common/debug'; import { Variable, Expression, Thread, Breakpoint } from 'vs/workbench/parts/debug/common/debugModel'; import { IPartService } from 'vs/workbench/services/part/common/partService'; @@ -226,7 +226,7 @@ export class RestartAction extends AbstractDebugAction { } private setLabel(session: ISession): void { - this.updateLabel(session && session.state === SessionState.ATTACH ? RestartAction.RECONNECT_LABEL : RestartAction.LABEL); + this.updateLabel(session && session.configuration.request === 'attach' ? RestartAction.RECONNECT_LABEL : RestartAction.LABEL); } public run(session: ISession): TPromise { diff --git a/src/vs/workbench/parts/debug/browser/debugEditorActions.ts b/src/vs/workbench/parts/debug/browser/debugEditorActions.ts index d6332e2f2ca..3d00826481d 100644 --- a/src/vs/workbench/parts/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/parts/debug/browser/debugEditorActions.ts @@ -118,7 +118,7 @@ class RunToCursorAction extends EditorAction { } let breakpointToRemove: IBreakpoint; - const oneTimeListener = debugService.getViewModel().focusedSession.raw.onDidEvent(event => { + const oneTimeListener = debugService.getViewModel().focusedSession.onDidCustomEvent(event => { if (event.event === 'stopped' || event.event === 'exit') { if (breakpointToRemove) { debugService.removeBreakpoints(breakpointToRemove.getId()); diff --git a/src/vs/workbench/parts/debug/browser/loadedScriptsView.ts b/src/vs/workbench/parts/debug/browser/loadedScriptsView.ts index 780b5ebb659..b8bd358a6cf 100644 --- a/src/vs/workbench/parts/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/parts/debug/browser/loadedScriptsView.ts @@ -371,16 +371,18 @@ export class LoadedScriptsView extends TreeViewsViewletPanel { let timeout: number; - this.disposables.push(this.debugService.onDidLoadedSource(event => { - const sessionRoot = root.add(event.session); - sessionRoot.addPath(event.source); + this.disposables.push(this.debugService.onDidNewSession(session => { + this.disposables.push(session.onDidLoadedSource(event => { + const sessionRoot = root.add(session); + sessionRoot.addPath(event.source); - clearTimeout(timeout); - timeout = setTimeout(() => { - if (this.tree) { - this.tree.refresh(root, true); - } - }, 300); + clearTimeout(timeout); + timeout = setTimeout(() => { + if (this.tree) { + this.tree.refresh(root, true); + } + }, 300); + })); })); this.disposables.push(this.debugService.onDidEndSession(session => { diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index d527aef5481..d832d738ecb 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -109,7 +109,6 @@ export interface IExpression extends IReplElement, IExpressionContainer { } export interface IRawSession { - readonly root: IWorkspaceFolder; stackTrace(args: DebugProtocol.StackTraceArguments): TPromise; exceptionInfo(args: DebugProtocol.ExceptionInfoArguments): TPromise; scopes(args: DebugProtocol.ScopesArguments): TPromise; @@ -117,11 +116,9 @@ export interface IRawSession { evaluate(args: DebugProtocol.EvaluateArguments): TPromise; readonly capabilities: DebugProtocol.Capabilities; + disconnect(restart?: boolean): TPromise; terminate(restart?: boolean): TPromise; custom(request: string, args: any): TPromise; - onDidEvent: Event; - onDidInitialize: Event; - onDidExitAdapter: Event<{ sessionId: string }>; restartFrame(args: DebugProtocol.RestartFrameArguments, threadId: number): TPromise; next(args: DebugProtocol.NextArguments): TPromise; @@ -140,15 +137,11 @@ export interface IRawSession { } -export enum SessionState { - ATTACH, - LAUNCH -} - -export interface ISession extends ITreeElement { +export interface ISession extends ITreeElement, IDisposable { readonly configuration: IConfig; readonly raw: IRawSession; - readonly state: SessionState; + readonly state: State; + readonly root: IWorkspaceFolder; getName(includeRoot: boolean): string; getSourceForUri(modelUri: uri): Source; @@ -160,6 +153,18 @@ export interface ISession extends ITreeElement { clearThreads(removeThreads: boolean, reference?: number): void; rawUpdate(data: IRawModelUpdate): void; + + /** + * Allows to register on loaded source events. + */ + onDidLoadedSource: Event; + + /** + * Allows to register on custom DAP events. + */ + onDidCustomEvent: Event; + + onDidExitAdapter: Event; } export interface IThread extends ITreeElement { @@ -581,7 +586,6 @@ export interface DebugEvent extends DebugProtocol.Event { } export interface LoadedSourceEvent { - session: ISession; reason: string; source: Source; } @@ -609,16 +613,6 @@ export interface IDebugService { */ onDidEndSession: Event; - /** - * Allows to register on loaded source events. - */ - onDidLoadedSource: Event; - - /** - * Allows to register on custom DAP events. - */ - onDidCustomEvent: Event; - /** * Gets the current configuration manager. */ @@ -687,7 +681,7 @@ export interface IDebugService { /** * Appends the passed string to the debug repl. */ - logToRepl(value: string, sev?: severity): void; + logToRepl(value: string | IExpression, sev?: severity, source?: IReplElementSource): void; /** * Adds a new watch expression and evaluates it against the debug adapter. @@ -719,7 +713,7 @@ export interface IDebugService { /** * Restarts a session or creates a new one if there is no active session. */ - restartSession(session: ISession): TPromise; + restartSession(session: ISession, restartData?: any): TPromise; /** * Stops the session. If the session does not exist then stops all sessions. @@ -740,6 +734,11 @@ export interface IDebugService { * Gets the current view model. */ getViewModel(): IViewModel; + + /** + * Try to auto focus the top stack frame of the passed thread. + */ + tryToAutoFocusStackFrame(thread: IThread): TPromise; } // Editor interfaces diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index 73c6353846b..a360dabf1cc 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -4,13 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as lifecycle from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import * as resources from 'vs/base/common/resources'; -import * as strings from 'vs/base/common/strings'; import { generateUuid } from 'vs/base/common/uuid'; import uri from 'vs/base/common/uri'; -import * as platform from 'vs/base/common/platform'; import { first, distinct } from 'vs/base/common/arrays'; import { isObject, isUndefinedOrNull } from 'vs/base/common/types'; import * as errors from 'vs/base/common/errors'; @@ -26,9 +23,8 @@ import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/file import { IWindowService } from 'vs/platform/windows/common/windows'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import * as debug from 'vs/workbench/parts/debug/common/debug'; import { RawDebugSession } from 'vs/workbench/parts/debug/electron-browser/rawDebugSession'; -import { Model, ExceptionBreakpoint, FunctionBreakpoint, Breakpoint, Expression, RawObjectReplElement, ExpressionContainer, Thread } from 'vs/workbench/parts/debug/common/debugModel'; +import { Model, ExceptionBreakpoint, FunctionBreakpoint, Breakpoint, Expression, RawObjectReplElement } from 'vs/workbench/parts/debug/common/debugModel'; import { ViewModel } from 'vs/workbench/parts/debug/common/debugViewModel'; import * as debugactions from 'vs/workbench/parts/debug/browser/debugActions'; import { ConfigurationManager } from 'vs/workbench/parts/debug/electron-browser/debugConfigurationManager'; @@ -43,7 +39,7 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL, EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, EXTENSION_RELOAD_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost'; +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, IBroadcast } 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'; @@ -52,10 +48,11 @@ 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 { RunOnceScheduler } from 'vs/base/common/async'; -import product from 'vs/platform/node/product'; import { deepClone, equals } from 'vs/base/common/objects'; import { Session } from 'vs/workbench/parts/debug/electron-browser/debugSession'; +import { equalsIgnoreCase } from 'vs/base/common/strings'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { IDebugService, State, ISession, 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'; const DEBUG_BREAKPOINTS_KEY = 'debug.breakpoint'; const DEBUG_BREAKPOINTS_ACTIVATED_KEY = 'debug.breakpointactivated'; @@ -63,29 +60,24 @@ const DEBUG_FUNCTION_BREAKPOINTS_KEY = 'debug.functionbreakpoint'; const DEBUG_EXCEPTION_BREAKPOINTS_KEY = 'debug.exceptionbreakpoint'; const DEBUG_WATCH_EXPRESSIONS_KEY = 'debug.watchexpressions'; -export class DebugService implements debug.IDebugService { - public _serviceBrand: any; +export class DebugService implements IDebugService { + _serviceBrand: any; - private sessionStates: Map; - private readonly _onDidChangeState: Emitter; - private readonly _onDidNewSession: Emitter; - private readonly _onDidEndSession: Emitter; - private readonly _onDidLoadedSource: Emitter; - private readonly _onDidCustomEvent: Emitter; + private readonly _onDidChangeState: Emitter; + private readonly _onDidNewSession: Emitter; + private readonly _onDidEndSession: Emitter; private model: Model; private viewModel: ViewModel; - private allSessions: Map; private configurationManager: ConfigurationManager; - private toDispose: lifecycle.IDisposable[]; - private toDisposeOnSessionEnd: Map; + private allSessions = new Map(); + private toDispose: IDisposable[]; private debugType: IContextKey; private debugState: IContextKey; private inDebugMode: IContextKey; private breakpointsToSendOnResourceSaved: Set; private firstSessionStart: boolean; private skipRunningTask: boolean; - private previousState: debug.State; - private fetchThreadsSchedulers: Map; + private previousState: State; constructor( @IStorageService private storageService: IStorageService, @@ -110,22 +102,16 @@ export class DebugService implements debug.IDebugService { @IConfigurationService private configurationService: IConfigurationService, ) { this.toDispose = []; - this.toDisposeOnSessionEnd = new Map(); this.breakpointsToSendOnResourceSaved = new Set(); - this._onDidChangeState = new Emitter(); - this._onDidNewSession = new Emitter(); - this._onDidEndSession = new Emitter(); - this._onDidLoadedSource = new Emitter(); - this._onDidCustomEvent = new Emitter(); - this.sessionStates = new Map(); - this.allSessions = new Map(); - this.fetchThreadsSchedulers = new Map(); + this._onDidChangeState = new Emitter(); + this._onDidNewSession = new Emitter(); + this._onDidEndSession = new Emitter(); this.configurationManager = this.instantiationService.createInstance(ConfigurationManager); this.toDispose.push(this.configurationManager); - this.debugType = debug.CONTEXT_DEBUG_TYPE.bindTo(contextKeyService); - this.debugState = debug.CONTEXT_DEBUG_STATE.bindTo(contextKeyService); - this.inDebugMode = debug.CONTEXT_IN_DEBUG_MODE.bindTo(contextKeyService); + this.debugType = CONTEXT_DEBUG_TYPE.bindTo(contextKeyService); + this.debugState = CONTEXT_DEBUG_STATE.bindTo(contextKeyService); + this.inDebugMode = CONTEXT_IN_DEBUG_MODE.bindTo(contextKeyService); this.model = new Model(this.loadBreakpoints(), this.storageService.getBoolean(DEBUG_BREAKPOINTS_ACTIVATED_KEY, StorageScope.WORKSPACE, true), this.loadFunctionBreakpoints(), this.loadExceptionBreakpoints(), this.loadWatchExpressions()); @@ -144,18 +130,17 @@ export class DebugService implements debug.IDebugService { this.toDispose.push(this.viewModel.onDidFocusSession(s => { const id = s ? s.getId() : undefined; this.model.setBreakpointsSessionId(id); + this.onStateChange(); })); } private onBroadcast(broadcast: IBroadcast): void { - // attach: PH is ready to be attached to const session = this.allSessions.get(broadcast.payload.debugId); if (!session) { // Ignore attach events for sessions that never existed (wrong vscode windows) return; } - const raw = session.raw; if (broadcast.channel === EXTENSION_ATTACH_BROADCAST_CHANNEL) { const initialAttach = session.configuration.request === 'launch'; @@ -164,22 +149,20 @@ export class DebugService implements debug.IDebugService { session.configuration.port = broadcast.payload.port; // Do not end process on initial attach (since the request is still 'launch') if (initialAttach) { - const root = raw.root; - lifecycle.dispose(this.toDisposeOnSessionEnd[raw.getId()]); - this.initializeRawSession(root, { resolved: session.configuration, unresolved: session.unresolvedConfiguration }, session.getId(), session).then(session => { - (session.raw).attach(session.configuration); - }); + const dbgr = this.configurationManager.getDebugger(session.configuration.type); + session.initialize(dbgr).then(() => (session.raw).attach(session.configuration)); } else { - const root = raw.root; - raw.disconnect().done(undefined, errors.onUnexpectedError); - this.doCreateSession(root, { resolved: session.configuration, unresolved: session.unresolvedConfiguration }, session.getId()); + if (session.raw) { + session.raw.disconnect().done(undefined, errors.onUnexpectedError); + } + this.doCreateSession(session.root, { resolved: session.configuration, unresolved: session.unresolvedConfiguration }, session.getId()); } return; } if (broadcast.channel === EXTENSION_TERMINATE_BROADCAST_CHANNEL) { - raw.terminate().done(undefined, errors.onUnexpectedError); + session.raw.terminate().done(undefined, errors.onUnexpectedError); return; } @@ -189,7 +172,7 @@ export class DebugService implements debug.IDebugService { let sev = extensionOutput.severity === 'warn' ? severity.Warning : extensionOutput.severity === 'error' ? severity.Error : severity.Info; const { args, stack } = parse(extensionOutput); - let source: debug.IReplElementSource; + let source: IReplElementSource; if (stack) { const frame = getFirstFrame(stack); if (frame) { @@ -264,7 +247,7 @@ export class DebugService implements debug.IDebugService { } } - private tryToAutoFocusStackFrame(thread: debug.IThread): TPromise { + tryToAutoFocusStackFrame(thread: IThread): TPromise { const callStack = thread.getCallStack(); if (!callStack.length || (this.viewModel.focusedStackFrame && this.viewModel.focusedStackFrame.thread.getId() === thread.getId())) { return TPromise.as(null); @@ -278,8 +261,8 @@ export class DebugService implements debug.IDebugService { this.focusStackFrame(stackFrameToFocus); if (thread.stoppedDetails) { - if (this.configurationService.getValue('debug').openDebug === 'openOnDebugBreak') { - this.viewletService.openViewlet(debug.VIEWLET_ID).done(undefined, errors.onUnexpectedError); + if (this.configurationService.getValue('debug').openDebug === 'openOnDebugBreak') { + this.viewletService.openViewlet(VIEWLET_ID).done(undefined, errors.onUnexpectedError); } this.windowService.focusWindow(); aria.alert(nls.localize('debuggingPaused', "Debugging paused, reason {0}, {1} {2}", thread.stoppedDetails.reason, stackFrameToFocus.source ? stackFrameToFocus.source.name : '', stackFrameToFocus.range.startLineNumber)); @@ -288,199 +271,6 @@ export class DebugService implements debug.IDebugService { return stackFrameToFocus.openInEditor(this.editorService, true); } - private registerSessionListeners(session: debug.ISession, raw: RawDebugSession): void { - this.toDisposeOnSessionEnd.get(raw.getId()).push(raw.onDidInitialize(event => { - aria.status(nls.localize('debuggingStarted', "Debugging started.")); - const sendConfigurationDone = () => { - if (raw && raw.capabilities.supportsConfigurationDoneRequest) { - return raw.configurationDone().done(null, e => { - // Disconnect the debug session on configuration done error #10596 - if (raw) { - raw.disconnect().done(undefined, errors.onUnexpectedError); - } - this.notificationService.error(e.message); - }); - } - }; - - this.sendAllBreakpoints(session).then(sendConfigurationDone, sendConfigurationDone) - .done(() => this.fetchThreads(raw), errors.onUnexpectedError); - })); - - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidStop(event => { - this.updateStateAndEmit(session.getId(), debug.State.Stopped); - this.fetchThreads(raw, event.body).done(() => { - const thread = session && session.getThread(event.body.threadId); - if (thread) { - // Call fetch call stack twice, the first only return the top stack frame. - // Second retrieves the rest of the call stack. For performance reasons #25605 - this.model.fetchCallStack(thread).then(() => { - return !event.body.preserveFocusHint ? this.tryToAutoFocusStackFrame(thread) : undefined; - }); - } - }, errors.onUnexpectedError); - })); - - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidThread(event => { - if (event.body.reason === 'started') { - // debounce to reduce threadsRequest frequency and improve performance - let scheduler = this.fetchThreadsSchedulers.get(session.getId()); - if (!scheduler) { - scheduler = new RunOnceScheduler(() => { - this.fetchThreads(raw).done(undefined, errors.onUnexpectedError); - }, 100); - this.fetchThreadsSchedulers.set(session.getId(), scheduler); - this.toDisposeOnSessionEnd.get(session.getId()).push(scheduler); - } - if (!scheduler.isScheduled()) { - scheduler.schedule(); - } - } else if (event.body.reason === 'exited') { - this.model.clearThreads(session.getId(), true, event.body.threadId); - } - })); - - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidTerminateDebugee(event => { - aria.status(nls.localize('debuggingStopped', "Debugging stopped.")); - if (session && session.getId() === event.sessionId) { - if (event.body && event.body.restart && session) { - this.restartSession(session, event.body.restart).done(null, err => this.notificationService.error(err.message)); - } else { - raw.disconnect().done(undefined, errors.onUnexpectedError); - } - } - })); - - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidContinued(event => { - const threadId = event.body.allThreadsContinued !== false ? undefined : event.body.threadId; - this.model.clearThreads(session.getId(), false, threadId); - if (this.viewModel.focusedSession.getId() === session.getId()) { - this.focusStackFrame(undefined, this.viewModel.focusedThread, this.viewModel.focusedSession); - } - this.updateStateAndEmit(session.getId(), debug.State.Running); - })); - - let outputPromises: TPromise[] = []; - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidOutput(event => { - if (!event.body) { - return; - } - - const outputSeverity = event.body.category === 'stderr' ? severity.Error : event.body.category === 'console' ? severity.Warning : severity.Info; - if (event.body.category === 'telemetry') { - // only log telemetry events from debug adapter if the debug extension provided the telemetry key - // and the user opted in telemetry - if (raw.customTelemetryService && this.telemetryService.isOptedIn) { - // __GDPR__TODO__ We're sending events in the name of the debug extension and we can not ensure that those are declared correctly. - raw.customTelemetryService.publicLog(event.body.output, event.body.data); - } - - return; - } - - // Make sure to append output in the correct order by properly waiting on preivous promises #33822 - const waitFor = outputPromises.slice(); - const source = event.body.source ? { - lineNumber: event.body.line, - column: event.body.column ? event.body.column : 1, - source: session.getSource(event.body.source) - } : undefined; - if (event.body.variablesReference) { - const container = new ExpressionContainer(session, event.body.variablesReference, generateUuid()); - outputPromises.push(container.getChildren().then(children => { - return TPromise.join(waitFor).then(() => children.forEach(child => { - // Since we can not display multiple trees in a row, we are displaying these variables one after the other (ignoring their names) - child.name = null; - this.logToRepl(child, outputSeverity, source); - })); - })); - } else if (typeof event.body.output === 'string') { - TPromise.join(waitFor).then(() => this.logToRepl(event.body.output, outputSeverity, source)); - } - TPromise.join(outputPromises).then(() => outputPromises = []); - })); - - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidBreakpoint(event => { - const id = event.body && event.body.breakpoint ? event.body.breakpoint.id : undefined; - const breakpoint = this.model.getBreakpoints().filter(bp => bp.idFromAdapter === id).pop(); - const functionBreakpoint = this.model.getFunctionBreakpoints().filter(bp => bp.idFromAdapter === id).pop(); - - if (event.body.reason === 'new' && event.body.breakpoint.source) { - const source = session.getSource(event.body.breakpoint.source); - const bps = this.model.addBreakpoints(source.uri, [{ - column: event.body.breakpoint.column, - enabled: true, - lineNumber: event.body.breakpoint.line, - }], false); - if (bps.length === 1) { - this.model.updateBreakpoints({ [bps[0].getId()]: event.body.breakpoint }); - } - } - - if (event.body.reason === 'removed') { - if (breakpoint) { - this.model.removeBreakpoints([breakpoint]); - } - if (functionBreakpoint) { - this.model.removeFunctionBreakpoints(functionBreakpoint.getId()); - } - } - - if (event.body.reason === 'changed') { - if (breakpoint) { - if (!breakpoint.column) { - event.body.breakpoint.column = undefined; - } - this.model.setBreakpointSessionData(session.getId(), { [breakpoint.getId()]: event.body.breakpoint }); - } - if (functionBreakpoint) { - this.model.setBreakpointSessionData(session.getId(), { [functionBreakpoint.getId()]: event.body.breakpoint }); - } - } - })); - - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidExitAdapter(event => { - // 'Run without debugging' mode VSCode must terminate the extension host. More details: #3905 - if (strings.equalsIgnoreCase(session.configuration.type, 'extensionhost') && this.sessionStates.get(session.getId()) === debug.State.Running && - session && session.raw.root && session.configuration.noDebug) { - this.broadcastService.broadcast({ - channel: EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, - payload: [session.raw.root.uri.fsPath] - }); - } - if (session && session.getId() === event.sessionId) { - this.onExitAdapter(raw); - } - })); - - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidLoadedSource(event => { - this._onDidLoadedSource.fire({ - session: session, - reason: event.body.reason, - source: session.getSource(event.body.source) - }); - })); - - this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidCustomEvent(event => { - this._onDidCustomEvent.fire(event); - })); - } - - private fetchThreads(session: RawDebugSession, stoppedDetails?: debug.IRawStoppedDetails): TPromise { - return session.threads().then(response => { - if (response && response.body && response.body.threads) { - response.body.threads.forEach(thread => { - this.model.rawUpdate({ - sessionId: session.getId(), - threadId: thread.id, - thread, - stoppedDetails: stoppedDetails && thread.id === stoppedDetails.threadId ? stoppedDetails : undefined - }); - }); - } - }); - } - private loadBreakpoints(): Breakpoint[] { let result: Breakpoint[]; try { @@ -525,64 +315,28 @@ export class DebugService implements debug.IDebugService { return result || []; } - public get state(): debug.State { - const focusedThread = this.viewModel.focusedThread; - if (focusedThread && focusedThread.stopped) { - return debug.State.Stopped; - } + get state(): State { const focusedSession = this.viewModel.focusedSession; - if (focusedSession && this.sessionStates.has(focusedSession.getId())) { - return this.sessionStates.get(focusedSession.getId()); - } - if (this.sessionStates.size > 0) { - return debug.State.Initializing; + if (focusedSession) { + return focusedSession.state; } - return debug.State.Inactive; + return State.Inactive; } - public get onDidChangeState(): Event { + get onDidChangeState(): Event { return this._onDidChangeState.event; } - public get onDidNewSession(): Event { + get onDidNewSession(): Event { return this._onDidNewSession.event; } - public get onDidEndSession(): Event { + get onDidEndSession(): Event { return this._onDidEndSession.event; } - public get onDidLoadedSource(): Event { - return this._onDidLoadedSource.event; - } - - public get onDidCustomEvent(): Event { - return this._onDidCustomEvent.event; - } - - private updateStateAndEmit(sessionId?: string, newState?: debug.State): void { - if (sessionId) { - if (newState === debug.State.Inactive) { - this.sessionStates.delete(sessionId); - } else { - this.sessionStates.set(sessionId, newState); - } - } - - const state = this.state; - if (this.previousState !== state) { - const stateLabel = debug.State[state]; - if (stateLabel) { - this.debugState.set(stateLabel.toLowerCase()); - this.inDebugMode.set(state !== debug.State.Inactive); - } - this.previousState = state; - this._onDidChangeState.fire(state); - } - } - - public focusStackFrame(stackFrame: debug.IStackFrame, thread?: debug.IThread, session?: debug.ISession, explicit?: boolean): void { + focusStackFrame(stackFrame: IStackFrame, thread?: IThread, session?: ISession, explicit?: boolean): void { if (!session) { if (stackFrame || thread) { session = stackFrame ? stackFrame.thread.session : thread.session; @@ -609,10 +363,9 @@ export class DebugService implements debug.IDebugService { } this.viewModel.setFocus(stackFrame, thread, session, explicit); - this.updateStateAndEmit(); } - public enableOrDisableBreakpoints(enable: boolean, breakpoint?: debug.IEnablement): TPromise { + enableOrDisableBreakpoints(enable: boolean, breakpoint?: IEnablement): TPromise { if (breakpoint) { this.model.setEnablement(breakpoint, enable); if (breakpoint instanceof Breakpoint) { @@ -628,14 +381,14 @@ export class DebugService implements debug.IDebugService { return this.sendAllBreakpoints(); } - public addBreakpoints(uri: uri, rawBreakpoints: debug.IBreakpointData[]): TPromise { + addBreakpoints(uri: uri, rawBreakpoints: IBreakpointData[]): TPromise { const breakpoints = this.model.addBreakpoints(uri, rawBreakpoints); breakpoints.forEach(bp => aria.status(nls.localize('breakpointAdded', "Added breakpoint, line {0}, file {1}", bp.lineNumber, uri.fsPath))); return this.sendBreakpoints(uri).then(() => breakpoints); } - public updateBreakpoints(uri: uri, data: { [id: string]: DebugProtocol.Breakpoint }, sendOnResourceSaved: boolean): void { + updateBreakpoints(uri: uri, data: { [id: string]: DebugProtocol.Breakpoint }, sendOnResourceSaved: boolean): void { this.model.updateBreakpoints(data); if (sendOnResourceSaved) { this.breakpointsToSendOnResourceSaved.add(uri.toString()); @@ -644,7 +397,7 @@ export class DebugService implements debug.IDebugService { } } - public removeBreakpoints(id?: string): TPromise { + removeBreakpoints(id?: string): TPromise { const toRemove = this.model.getBreakpoints().filter(bp => !id || bp.getId() === id); toRemove.forEach(bp => aria.status(nls.localize('breakpointRemoved', "Removed breakpoint, line {0}, file {1}", bp.lineNumber, bp.uri.fsPath))); const urisToClear = distinct(toRemove, bp => bp.uri.toString()).map(bp => bp.uri); @@ -654,37 +407,37 @@ export class DebugService implements debug.IDebugService { return TPromise.join(urisToClear.map(uri => this.sendBreakpoints(uri))); } - public setBreakpointsActivated(activated: boolean): TPromise { + setBreakpointsActivated(activated: boolean): TPromise { this.model.setBreakpointsActivated(activated); return this.sendAllBreakpoints(); } - public addFunctionBreakpoint(name?: string, id?: string): void { + addFunctionBreakpoint(name?: string, id?: string): void { const newFunctionBreakpoint = this.model.addFunctionBreakpoint(name || '', id); this.viewModel.setSelectedFunctionBreakpoint(newFunctionBreakpoint); } - public renameFunctionBreakpoint(id: string, newFunctionName: string): TPromise { + renameFunctionBreakpoint(id: string, newFunctionName: string): TPromise { this.model.renameFunctionBreakpoint(id, newFunctionName); return this.sendFunctionBreakpoints(); } - public removeFunctionBreakpoints(id?: string): TPromise { + removeFunctionBreakpoints(id?: string): TPromise { this.model.removeFunctionBreakpoints(id); return this.sendFunctionBreakpoints(); } - public addReplExpression(name: string): TPromise { + addReplExpression(name: string): TPromise { return this.model.addReplExpression(this.viewModel.focusedSession, this.viewModel.focusedStackFrame, name) // Evaluate all watch expressions and fetch variables again since repl evaluation might have changed some. .then(() => this.focusStackFrame(this.viewModel.focusedStackFrame, this.viewModel.focusedThread, this.viewModel.focusedSession)); } - public removeReplExpressions(): void { + removeReplExpressions(): void { this.model.removeReplExpressions(); } - public logToRepl(value: string | debug.IExpression, sev = severity.Info, source?: debug.IReplElementSource): void { + logToRepl(value: string | IExpression, sev = severity.Info, source?: IReplElementSource): void { const clearAnsiSequence = '\u001b[2J'; if (typeof value === 'string' && value.indexOf(clearAnsiSequence) >= 0) { // [2J is the ansi escape sequence for clearing the display http://ascii-table.com/ansi-escape-sequences.php @@ -696,31 +449,25 @@ export class DebugService implements debug.IDebugService { this.model.appendToRepl(value, sev, source); } - public addWatchExpression(name: string): void { + addWatchExpression(name: string): void { const we = this.model.addWatchExpression(name); this.viewModel.setSelectedExpression(we); } - public renameWatchExpression(id: string, newName: string): void { + renameWatchExpression(id: string, newName: string): void { return this.model.renameWatchExpression(id, newName); } - public moveWatchExpression(id: string, position: number): void { + moveWatchExpression(id: string, position: number): void { this.model.moveWatchExpression(id, position); } - public removeWatchExpressions(id?: string): void { + removeWatchExpressions(id?: string): void { this.model.removeWatchExpressions(id); } - public startDebugging(launch: debug.ILaunch, configOrName?: debug.IConfig | string, noDebug = false, unresolvedConfiguration?: debug.IConfig, ): TPromise { + startDebugging(launch: ILaunch, configOrName?: IConfig | string, noDebug = false, unresolvedConfiguration?: IConfig, ): TPromise { const sessionId = generateUuid(); - this.updateStateAndEmit(sessionId, debug.State.Initializing); - const wrapUpState = () => { - if (this.sessionStates.get(sessionId) === debug.State.Initializing) { - this.updateStateAndEmit(sessionId, debug.State.Inactive); - } - }; // 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(() => @@ -730,7 +477,7 @@ export class DebugService implements debug.IDebugService { this.allSessions.clear(); } - let config: debug.IConfig, compound: debug.ICompound; + let config: IConfig, compound: ICompound; if (!configOrName) { configOrName = this.configurationManager.selectedConfiguration.name; } @@ -740,7 +487,7 @@ export class DebugService implements debug.IDebugService { const sessions = this.model.getSessions(); const alreadyRunningMessage = nls.localize('configurationAlreadyRunning', "There is already a debug configuration \"{0}\" running.", configOrName); - if (sessions.some(p => p.getName(false) === configOrName && (!launch || !launch.workspace || !p.raw.root || p.raw.root.uri.toString() === launch.workspace.uri.toString()))) { + 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)) { @@ -762,7 +509,7 @@ export class DebugService implements debug.IDebugService { return TPromise.as(null); } - let launchForName: debug.ILaunch; + let launchForName: ILaunch; if (typeof configData === 'string') { const launchesContainingName = this.configurationManager.getLaunches().filter(l => !!l.getConfiguration(name)); if (launchesContainingName.length === 1) { @@ -799,7 +546,7 @@ export class DebugService implements debug.IDebugService { type = config.type; } else { // a no-folder workspace has no launch.config - config = {}; + config = {}; } unresolvedConfiguration = unresolvedConfiguration || deepClone(config); @@ -820,13 +567,10 @@ export class DebugService implements debug.IDebugService { }) ).then(() => undefined); }) - ))).then(() => wrapUpState(), err => { - wrapUpState(); - return TPromise.wrapError(err); - }); + ))); } - private substituteVariables(launch: debug.ILaunch | undefined, config: debug.IConfig): TPromise { + private substituteVariables(launch: ILaunch | undefined, config: IConfig): TPromise { const dbg = this.configurationManager.getDebugger(config.type); if (dbg) { let folder: IWorkspaceFolder = undefined; @@ -848,7 +592,7 @@ export class DebugService implements debug.IDebugService { return TPromise.as(config); } - private createSession(launch: debug.ILaunch, config: debug.IConfig, unresolvedConfig: debug.IConfig, sessionId: string): TPromise { + private createSession(launch: ILaunch, config: IConfig, unresolvedConfig: IConfig, sessionId: string): TPromise { return this.textFileService.saveAll().then(() => this.substituteVariables(launch, config).then(resolvedConfig => { @@ -871,8 +615,6 @@ export class DebugService implements debug.IDebugService { return this.showError(message); } - this.toDisposeOnSessionEnd.set(sessionId, []); - const workspace = launch ? launch.workspace : undefined; return this.runTask(sessionId, workspace, resolvedConfig.preLaunchTask, resolvedConfig, unresolvedConfig, () => this.doCreateSession(workspace, { resolved: resolvedConfig, unresolved: unresolvedConfig }, sessionId) @@ -890,45 +632,16 @@ export class DebugService implements debug.IDebugService { ); } - private initializeRawSession(root: IWorkspaceFolder, configuration: { resolved: debug.IConfig, unresolved: debug.IConfig }, sessionId: string, session?: Session): TPromise { - const dbg = this.configurationManager.getDebugger(configuration.resolved.type); - return dbg.getCustomTelemetryService().then(customTelemetryService => { - - const raw = this.instantiationService.createInstance(RawDebugSession, sessionId, configuration.resolved.debugServer, dbg, customTelemetryService, root); - if (!session) { - session = new Session(configuration, raw); - this.model.addSession(session); - this.allSessions.set(session.getId(), session); - } else { - session.raw = raw; - } - this.registerSessionListeners(session, raw); - - return raw.initialize({ - clientID: 'vscode', - clientName: product.nameLong, - adapterID: configuration.resolved.type, - pathFormat: 'path', - linesStartAt1: true, - columnsStartAt1: true, - supportsVariableType: true, // #8858 - supportsVariablePaging: true, // #9537 - supportsRunInTerminalRequest: true, // #10574 - locale: platform.locale - }).then((result: DebugProtocol.InitializeResponse) => { - this.model.setExceptionBreakpoints(raw.capabilities.exceptionBreakpointFilters); - return session; - }); - }); - } - - private doCreateSession(root: IWorkspaceFolder, configuration: { resolved: debug.IConfig, unresolved: debug.IConfig }, sessionId: string): TPromise { + private doCreateSession(root: IWorkspaceFolder, configuration: { resolved: IConfig, unresolved: IConfig }, sessionId: string): TPromise { const resolved = configuration.resolved; resolved.__sessionId = sessionId; - const dbg = this.configurationManager.getDebugger(resolved.type); - return this.initializeRawSession(root, configuration, sessionId).then(session => { + const dbgr = this.configurationManager.getDebugger(resolved.type); + const session = this.instantiationService.createInstance(Session, sessionId, configuration, root, this.model); + this.allSessions.set(sessionId, session); + return session.initialize(dbgr).then(() => { + this.registerSessionListeners(session); const raw = session.raw; return (resolved.request === 'attach' ? raw.attach(resolved) : raw.launch(resolved)) .then((result: DebugProtocol.Response) => { @@ -938,15 +651,15 @@ export class DebugService implements debug.IDebugService { this.focusStackFrame(undefined, undefined, session); this._onDidNewSession.fire(session); - const internalConsoleOptions = resolved.internalConsoleOptions || this.configurationService.getValue('debug').internalConsoleOptions; + const internalConsoleOptions = resolved.internalConsoleOptions || this.configurationService.getValue('debug').internalConsoleOptions; if (internalConsoleOptions === 'openOnSessionStart' || (this.firstSessionStart && internalConsoleOptions === 'openOnFirstSessionStart')) { - this.panelService.openPanel(debug.REPL_ID, false).done(undefined, errors.onUnexpectedError); + this.panelService.openPanel(REPL_ID, false).done(undefined, errors.onUnexpectedError); } - const openDebug = this.configurationService.getValue('debug').openDebug; + const openDebug = this.configurationService.getValue('debug').openDebug; // Open debug viewlet based on the visibility of the side bar and openDebug setting if (openDebug === 'openOnSessionStart' || (openDebug === 'openOnFirstSessionStart' && this.firstSessionStart)) { - this.viewletService.openViewlet(debug.VIEWLET_ID); + this.viewletService.openViewlet(VIEWLET_ID); } this.firstSessionStart = false; @@ -954,7 +667,6 @@ export class DebugService implements debug.IDebugService { if (this.model.getSessions().length > 1) { this.viewModel.setMultiSessionView(true); } - this.updateStateAndEmit(raw.getId(), debug.State.Running); /* __GDPR__ "debugSessionStart" : { @@ -972,9 +684,9 @@ export class DebugService implements debug.IDebugService { breakpointCount: this.model.getBreakpoints().length, exceptionBreakpoints: this.model.getExceptionBreakpoints(), watchExpressionsCount: this.model.getWatchExpressions().length, - extensionName: dbg.extensionDescription.id, - isBuiltin: dbg.extensionDescription.isBuiltin, - launchJsonExists: root && !!this.configurationService.getValue('launch', { resource: root.uri }) + extensionName: dbgr.extensionDescription.id, + isBuiltin: dbgr.extensionDescription.isBuiltin, + launchJsonExists: root && !!this.configurationService.getValue('launch', { resource: root.uri }) }); }).then(() => session, (error: Error | string) => { if (errors.isPromiseCanceledError(error)) { @@ -990,16 +702,15 @@ export class DebugService implements debug.IDebugService { } */ this.telemetryService.publicLog('debugMisconfiguration', { type: resolved ? resolved.type : undefined, error: errorMessage }); - this.updateStateAndEmit(raw.getId(), debug.State.Inactive); if (!raw.disconnected) { raw.disconnect(); } else if (session) { - this.model.removeSession(session.getId()); + dispose(session); } // Show the repl if some error got logged there #5870 if (this.model.getReplElements().length > 0) { - this.panelService.openPanel(debug.REPL_ID, false).done(undefined, errors.onUnexpectedError); + this.panelService.openPanel(REPL_ID, false).done(undefined, errors.onUnexpectedError); } this.showError(errorMessage, errors.isErrorWithActions(error) ? error.actions : []); @@ -1008,6 +719,83 @@ export class DebugService implements debug.IDebugService { }); } + private onStateChange(): void { + const state = this.state; + if (this.previousState !== state) { + const stateLabel = State[state]; + if (stateLabel) { + this.debugState.set(stateLabel.toLowerCase()); + this.inDebugMode.set(state !== State.Inactive); + } + this.previousState = state; + this._onDidChangeState.fire(state); + } + } + + private registerSessionListeners(session: Session): void { + const toDispose: IDisposable[] = []; + + toDispose.push(session.onDidChangeState((state) => { + if (state === State.Running && this.viewModel.focusedSession.getId() === session.getId()) { + this.focusStackFrame(undefined); + } + this.onStateChange(); + })); + + toDispose.push(session.onDidExitAdapter(() => { + // 'Run without debugging' mode VSCode must terminate the extension host. More details: #3905 + if (equalsIgnoreCase(session.configuration.type, 'extensionhost') && session.state === State.Running && session.configuration.noDebug) { + this.broadcastService.broadcast({ + channel: EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, + payload: [session.root.uri.fsPath] + }); + } + + + const breakpoints = this.model.getBreakpoints(); + /* __GDPR__ + "debugSessionStop" : { + "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "success": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "sessionLengthInSeconds": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "breakpointCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "watchExpressionsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + } + */ + this.telemetryService.publicLog('debugSessionStop', { + type: session && session.configuration.type, + success: (session.raw).emittedStopped || breakpoints.length === 0, + sessionLengthInSeconds: (session.raw).getLengthInSeconds(), + breakpointCount: breakpoints.length, + watchExpressionsCount: this.model.getWatchExpressions().length + }); + + if (session.configuration.postDebugTask) { + this.doRunTask(session.getId(), session.root, session.configuration.postDebugTask).done(undefined, err => + this.notificationService.error(err) + ); + } + toDispose.push(session); + dispose(toDispose); + this._onDidEndSession.fire(session); + + const focusedSession = this.viewModel.focusedSession; + if (focusedSession && focusedSession.getId() === session.getId()) { + this.focusStackFrame(null); + } + + if (this.model.getSessions().length === 0) { + this.debugType.reset(); + this.viewModel.setMultiSessionView(false); + + if (this.partService.isVisible(Parts.SIDEBAR_PART) && this.configurationService.getValue('debug').openExplorerOnEnd) { + this.viewletService.openViewlet(EXPLORER_VIEWLET_ID).done(null, errors.onUnexpectedError); + } + } + + })); + } + private showError(message: string, actions: IAction[] = []): TPromise { const configureAction = this.instantiationService.createInstance(debugactions.ConfigureAction, debugactions.ConfigureAction.ID, debugactions.ConfigureAction.LABEL); actions.push(configureAction); @@ -1020,7 +808,7 @@ export class DebugService implements debug.IDebugService { }); } - private runTask(sessionId: string, root: IWorkspaceFolder, taskId: string | TaskIdentifier, config: debug.IConfig, unresolvedConfig: debug.IConfig, onSuccess: () => TPromise): TPromise { + private runTask(sessionId: string, root: IWorkspaceFolder, taskId: string | TaskIdentifier, config: IConfig, unresolvedConfig: IConfig, onSuccess: () => TPromise): TPromise { const debugAnywayAction = new Action('debug.debugAnyway', nls.localize('debugAnyway', "Debug Anyway"), undefined, true, () => { return this.doCreateSession(root, { resolved: config, unresolved: unresolvedConfig }, sessionId); }); @@ -1079,16 +867,12 @@ export class DebugService implements debug.IDebugService { // task is already running - nothing to do. return TPromise.as(null); } - this.toDisposeOnSessionEnd.get(sessionId).push( - once(TaskEventKind.Active, this.taskService.onDidStateChange)(() => { - taskStarted = true; - }) - ); + once(TaskEventKind.Active, this.taskService.onDidStateChange)((taskEvent) => { + taskStarted = true; + }); const taskPromise = this.taskService.run(task); if (task.isBackground) { - return new TPromise((c, e) => this.toDisposeOnSessionEnd.get(sessionId).push( - once(TaskEventKind.Inactive, this.taskService.onDidStateChange)(() => c(null))) - ); + return new TPromise((c, e) => once(TaskEventKind.Inactive, this.taskService.onDidStateChange)(() => c(null))); } return taskPromise; @@ -1112,16 +896,16 @@ export class DebugService implements debug.IDebugService { }); } - public sourceIsNotAvailable(uri: uri): void { + sourceIsNotAvailable(uri: uri): void { this.model.sourceIsNotAvailable(uri); } - public restartSession(session: debug.ISession, restartData?: any): TPromise { + restartSession(session: ISession, restartData?: any): TPromise { return this.textFileService.saveAll().then(() => { const unresolvedConfiguration = (session).unresolvedConfiguration; if (session.raw.capabilities.supportsRestartRequest) { - return this.runTask(session.getId(), session.raw.root, session.configuration.postDebugTask, session.configuration, unresolvedConfiguration, - () => this.runTask(session.getId(), session.raw.root, session.configuration.preLaunchTask, session.configuration, unresolvedConfiguration, + return this.runTask(session.getId(), session.root, session.configuration.postDebugTask, session.configuration, unresolvedConfiguration, + () => this.runTask(session.getId(), session.root, session.configuration.preLaunchTask, session.configuration, unresolvedConfiguration, () => session.raw.custom('restart', null))); } @@ -1131,11 +915,11 @@ export class DebugService implements debug.IDebugService { this.skipRunningTask = !!restartData; // If the restart is automatic disconnect, otherwise send the terminate signal #55064 - return (!!restartData ? (session.raw).disconnect(true) : session.raw.terminate(true)).then(() => { - if (strings.equalsIgnoreCase(session.configuration.type, 'extensionHost') && session.raw.root) { + return (!!restartData ? session.raw.disconnect(true) : session.raw.terminate(true)).then(() => { + if (equalsIgnoreCase(session.configuration.type, 'extensionHost') && session.root) { return this.broadcastService.broadcast({ channel: EXTENSION_RELOAD_BROADCAST_CHANNEL, - payload: [session.raw.root.uri.fsPath] + payload: [session.root.uri.fsPath] }); } @@ -1144,7 +928,7 @@ export class DebugService implements debug.IDebugService { // Read the configuration again if a launch.json has been changed, if not just use the inmemory configuration let configToUse = session.configuration; - const launch = session.raw.root ? this.configurationManager.getLaunch(session.raw.root.uri) : undefined; + const launch = session.root ? this.configurationManager.getLaunch(session.root.uri) : undefined; if (launch) { const config = launch.getConfiguration(session.configuration.name); if (config && !equals(config, unresolvedConfiguration)) { @@ -1171,7 +955,7 @@ export class DebugService implements debug.IDebugService { }); } - public stopSession(session: debug.ISession): TPromise { + stopSession(session: ISession): TPromise { if (session) { return session.raw.terminate(); } @@ -1181,80 +965,32 @@ export class DebugService implements debug.IDebugService { return TPromise.join(sessions.map(s => s.raw.terminate(false))); } - this.sessionStates.clear(); this._onDidChangeState.fire(); return undefined; } - private onExitAdapter(raw: RawDebugSession): void { - const breakpoints = this.model.getBreakpoints(); - const session = this.model.getSessions().filter(p => p.getId() === raw.getId()).pop(); - /* __GDPR__ - "debugSessionStop" : { - "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "success": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "sessionLengthInSeconds": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "breakpointCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "watchExpressionsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - } - */ - this.telemetryService.publicLog('debugSessionStop', { - type: session && session.configuration.type, - success: raw.emittedStopped || breakpoints.length === 0, - sessionLengthInSeconds: raw.getLengthInSeconds(), - breakpointCount: breakpoints.length, - watchExpressionsCount: this.model.getWatchExpressions().length - }); - - this.model.removeSession(raw.getId()); - if (session) { - this._onDidEndSession.fire(session); - if (session.configuration.postDebugTask) { - this.doRunTask(session.getId(), session.raw.root, session.configuration.postDebugTask).done(undefined, err => - this.notificationService.error(err) - ); - } - } - - lifecycle.dispose(this.toDisposeOnSessionEnd.get(raw.getId())); - const focusedSession = this.viewModel.focusedSession; - if (focusedSession && focusedSession.getId() === raw.getId()) { - this.focusStackFrame(null); - } - this.updateStateAndEmit(raw.getId(), debug.State.Inactive); - - if (this.model.getSessions().length === 0) { - this.debugType.reset(); - this.viewModel.setMultiSessionView(false); - - if (this.partService.isVisible(Parts.SIDEBAR_PART) && this.configurationService.getValue('debug').openExplorerOnEnd) { - this.viewletService.openViewlet(EXPLORER_VIEWLET_ID).done(null, errors.onUnexpectedError); - } - } - } - - public getModel(): debug.IModel { + getModel(): IModel { return this.model; } - public getViewModel(): debug.IViewModel { + getViewModel(): IViewModel { return this.viewModel; } - public getConfigurationManager(): debug.IConfigurationManager { + getConfigurationManager(): IConfigurationManager { return this.configurationManager; } - private sendAllBreakpoints(session?: debug.ISession): TPromise { + private sendAllBreakpoints(session?: ISession): TPromise { return TPromise.join(distinct(this.model.getBreakpoints(), bp => bp.uri.toString()).map(bp => this.sendBreakpoints(bp.uri, false, session))) .then(() => this.sendFunctionBreakpoints(session)) // send exception breakpoints at the end since some debug adapters rely on the order .then(() => this.sendExceptionBreakpoints(session)); } - private sendBreakpoints(modelUri: uri, sourceModified = false, session?: debug.ISession): TPromise { + private sendBreakpoints(modelUri: uri, sourceModified = false, session?: ISession): TPromise { - const sendBreakpointsToSession = (session: debug.ISession): TPromise => { + const sendBreakpointsToSession = (session: ISession): TPromise => { const raw = session.raw; if (!raw.readyForBreakpoints) { return TPromise.as(null); @@ -1291,15 +1027,15 @@ export class DebugService implements debug.IDebugService { for (let i = 0; i < breakpointsToSend.length; i++) { data[breakpointsToSend[i].getId()] = response.body.breakpoints[i]; } - this.model.setBreakpointSessionData(raw.getId(), data); + this.model.setBreakpointSessionData(session.getId(), data); }); }; return this.sendToOneOrAllSessions(session, sendBreakpointsToSession); } - private sendFunctionBreakpoints(session?: debug.ISession): TPromise { - const sendFunctionBreakpointsToSession = (session: debug.ISession): TPromise => { + private sendFunctionBreakpoints(session?: ISession): TPromise { + const sendFunctionBreakpointsToSession = (session: ISession): TPromise => { const raw = session.raw; if (!raw.readyForBreakpoints || !raw.capabilities.supportsFunctionBreakpoints) { return TPromise.as(null); @@ -1315,15 +1051,15 @@ export class DebugService implements debug.IDebugService { for (let i = 0; i < breakpointsToSend.length; i++) { data[breakpointsToSend[i].getId()] = response.body.breakpoints[i]; } - this.model.setBreakpointSessionData(raw.getId(), data); + this.model.setBreakpointSessionData(session.getId(), data); }); }; return this.sendToOneOrAllSessions(session, sendFunctionBreakpointsToSession); } - private sendExceptionBreakpoints(session?: debug.ISession): TPromise { - const sendExceptionBreakpointsToSession = (session: debug.ISession): TPromise => { + private sendExceptionBreakpoints(session?: ISession): TPromise { + const sendExceptionBreakpointsToSession = (session: ISession): TPromise => { const raw = session.raw; if (!raw.readyForBreakpoints || this.model.getExceptionBreakpoints().length === 0) { return TPromise.as(null); @@ -1336,7 +1072,7 @@ export class DebugService implements debug.IDebugService { return this.sendToOneOrAllSessions(session, sendExceptionBreakpointsToSession); } - private sendToOneOrAllSessions(session: debug.ISession, send: (session: debug.ISession) => TPromise): TPromise { + private sendToOneOrAllSessions(session: ISession, send: (session: ISession) => TPromise): TPromise { if (session) { return send(session); } @@ -1395,8 +1131,7 @@ export class DebugService implements debug.IDebugService { } } - public dispose(): void { - this.toDisposeOnSessionEnd.forEach(toDispose => lifecycle.dispose(toDispose)); - this.toDispose = lifecycle.dispose(this.toDispose); + dispose(): void { + this.toDispose = dispose(this.toDispose); } } diff --git a/src/vs/workbench/parts/debug/electron-browser/debugSession.ts b/src/vs/workbench/parts/debug/electron-browser/debugSession.ts index 75170c00238..1be9dbc2494 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugSession.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugSession.ts @@ -5,53 +5,95 @@ import uri from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; +import * as nls from 'vs/nls'; +import * as platform from 'vs/base/common/platform'; +import * as errors from 'vs/base/common/errors'; +import severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; +import { Event, Emitter } from 'vs/base/common/event'; import { ISuggestion } from 'vs/editor/common/modes'; import { Position } from 'vs/editor/common/core/position'; -import { ITreeElement, ISession, IConfig, IRawSession, IThread, IRawModelUpdate, SessionState } from 'vs/workbench/parts/debug/common/debug'; +import * as aria from 'vs/base/browser/ui/aria/aria'; +import { ISession, IConfig, IThread, IRawModelUpdate, IDebugService, IRawStoppedDetails, State, IRawSession, LoadedSourceEvent, DebugEvent } 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 } from 'vs/workbench/parts/debug/common/debugModel'; +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'; +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'; export class Session implements ISession { - private sources: Map; - private threads: Map; + private sources = new Map(); + private threads = new Map(); + private rawListeners: IDisposable[] = []; + private fetchThreadsScheduler: RunOnceScheduler; + private _raw: RawDebugSession; + private _state: State; + 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 }, private session: IRawSession & ITreeElement) { - this.threads = new Map(); - this.sources = new Map(); + constructor( + private id: string, + private _configuration: { resolved: IConfig, unresolved: IConfig }, + public root: IWorkspaceFolder, + private model: Model, + @IInstantiationService private instantiationService: IInstantiationService, + @INotificationService private notificationService: INotificationService, + @IDebugService private debugService: IDebugService, + @ITelemetryService private telemetryService: ITelemetryService, + ) { + this.model.addSession(this); + this.state = State.Initializing; } - public get configuration(): IConfig { + get raw(): IRawSession { + return this._raw; + } + + get configuration(): IConfig { return this._configuration.resolved; } - public get unresolvedConfiguration(): IConfig { + get unresolvedConfiguration(): IConfig { return this._configuration.unresolved; } - public get raw(): IRawSession & ITreeElement { - return this.session; + getName(includeRoot: boolean): string { + return includeRoot && this.root ? `${this.configuration.name} (${resources.basenameOrAuthority(this.root.uri)})` : this.configuration.name; } - public set raw(value: IRawSession & ITreeElement) { - this.session = value; + get state(): State { + return this._state; } - public getName(includeRoot: boolean): string { - return includeRoot && this.raw.root ? `${this.configuration.name} (${resources.basenameOrAuthority(this.raw.root.uri)})` : this.configuration.name; + set state(value: State) { + this._state = value; + this._onDidChangeState.fire(value); } - public get state(): SessionState { - return this.configuration.type === 'attach' ? SessionState.ATTACH : SessionState.LAUNCH; + get onDidChangeState(): Event { + return this._onDidChangeState.event; } - public getSourceForUri(modelUri: uri): Source { + getId(): string { + return this.id; + } + + getSourceForUri(modelUri: uri): Source { return this.sources.get(modelUri.toString()); } - public getSource(raw: DebugProtocol.Source): Source { + getSource(raw: DebugProtocol.Source): Source { let source = new Source(raw, this.getId()); if (this.sources.has(source.uri.toString())) { source = this.sources.get(source.uri.toString()); @@ -67,29 +109,37 @@ export class Session implements ISession { return source; } - public getThread(threadId: number): Thread { + getThread(threadId: number): Thread { return this.threads.get(threadId); } - public getAllThreads(): IThread[] { + getAllThreads(): IThread[] { const result: IThread[] = []; this.threads.forEach(t => result.push(t)); return result; } - public getLoadedSources(): TPromise { - return this.raw.loadedSources({}).then(response => { + getLoadedSources(): TPromise { + return this._raw.loadedSources({}).then(response => { return response.body.sources.map(src => this.getSource(src)); - }, error => { + }, () => { return []; }); } - public getId(): string { - return this.session.getId(); + get onDidLoadedSource(): Event { + return this._onDidLoadedSource.event; } - public rawUpdate(data: IRawModelUpdate): void { + get onDidCustomEvent(): Event { + return this._onDidCustomEvent.event; + } + + get onDidExitAdapter(): Event { + return this._onDidExitAdapter.event; + } + + rawUpdate(data: IRawModelUpdate): void { if (data.thread && !this.threads.has(data.threadId)) { // A new thread came in, initialize it. @@ -118,7 +168,7 @@ export class Session implements ISession { } } - public clearThreads(removeThreads: boolean, reference: number = undefined): void { + clearThreads(removeThreads: boolean, reference: number = undefined): void { if (reference !== undefined && reference !== null) { if (this.threads.has(reference)) { const thread = this.threads.get(reference); @@ -144,12 +194,12 @@ export class Session implements ISession { } } - public completions(frameId: number, text: string, position: Position, overwriteBefore: number): TPromise { - if (!this.raw.capabilities.supportsCompletionsRequest) { + completions(frameId: number, text: string, position: Position, overwriteBefore: number): TPromise { + if (!this._raw.capabilities.supportsCompletionsRequest) { return TPromise.as([]); } - return this.raw.completions({ + return this._raw.completions({ frameId, text, column: position.column, @@ -173,4 +223,214 @@ export class Session implements ISession { return result; }, () => []); } + + initialize(dbgr: Debugger): TPromise { + return dbgr.getCustomTelemetryService().then(customTelemetryService => { + if (this._raw) { + // If there was already a connection make sure to remove old listeners + dispose(this.rawListeners); + } + this._raw = this.instantiationService.createInstance(RawDebugSession, this.id, this._configuration.resolved.debugServer, dbgr, customTelemetryService, this.root); + this.registerListeners(); + + return this._raw.initialize({ + clientID: 'vscode', + clientName: product.nameLong, + adapterID: this.configuration.type, + pathFormat: 'path', + linesStartAt1: true, + columnsStartAt1: true, + supportsVariableType: true, // #8858 + supportsVariablePaging: true, // #9537 + supportsRunInTerminalRequest: true, // #10574 + locale: platform.locale + }).then(() => { + this.state = State.Running; + this.model.setExceptionBreakpoints(this._raw.capabilities.exceptionBreakpointFilters); + }); + }); + } + + private registerListeners(): void { + this.rawListeners.push(this._raw.onDidInitialize(() => { + aria.status(nls.localize('debuggingStarted', "Debugging started.")); + const sendConfigurationDone = () => { + if (this._raw && this._raw.capabilities.supportsConfigurationDoneRequest) { + return this._raw.configurationDone().done(null, e => { + // Disconnect the debug session on configuration done error #10596 + if (this._raw) { + this._raw.disconnect().done(undefined, errors.onUnexpectedError); + } + this.notificationService.error(e.message); + }); + } + }; + + // Send all breakpoints + this.debugService.setBreakpointsActivated(true).then(sendConfigurationDone, sendConfigurationDone) + .done(() => this.fetchThreads(), errors.onUnexpectedError); + })); + + this.rawListeners.push(this._raw.onDidStop(event => { + this.state = State.Stopped; + this.fetchThreads(event.body).done(() => { + const thread = this.getThread(event.body.threadId); + if (thread) { + // Call fetch call stack twice, the first only return the top stack frame. + // Second retrieves the rest of the call stack. For performance reasons #25605 + this.model.fetchCallStack(thread).then(() => { + return !event.body.preserveFocusHint ? this.debugService.tryToAutoFocusStackFrame(thread) : undefined; + }); + } + }, errors.onUnexpectedError); + })); + + this.rawListeners.push(this._raw.onDidThread(event => { + if (event.body.reason === 'started') { + // debounce to reduce threadsRequest frequency and improve performance + if (!this.fetchThreadsScheduler) { + this.fetchThreadsScheduler = new RunOnceScheduler(() => { + this.fetchThreads().done(undefined, errors.onUnexpectedError); + }, 100); + this.rawListeners.push(this.fetchThreadsScheduler); + } + if (!this.fetchThreadsScheduler.isScheduled()) { + this.fetchThreadsScheduler.schedule(); + } + } else if (event.body.reason === 'exited') { + this.model.clearThreads(this.getId(), true, event.body.threadId); + } + })); + + this.rawListeners.push(this._raw.onDidTerminateDebugee(event => { + aria.status(nls.localize('debuggingStopped', "Debugging stopped.")); + if (event.body && event.body.restart) { + this.debugService.restartSession(this, event.body.restart).done(null, err => this.notificationService.error(err.message)); + } else { + this._raw.disconnect().done(undefined, errors.onUnexpectedError); + } + })); + + this.rawListeners.push(this._raw.onDidContinued(event => { + const threadId = event.body.allThreadsContinued !== false ? undefined : event.body.threadId; + this.model.clearThreads(this.getId(), false, threadId); + this.state = State.Running; + })); + + let outputPromises: TPromise[] = []; + this.rawListeners.push(this._raw.onDidOutput(event => { + if (!event.body) { + return; + } + + const outputSeverity = event.body.category === 'stderr' ? severity.Error : event.body.category === 'console' ? severity.Warning : severity.Info; + if (event.body.category === 'telemetry') { + // only log telemetry events from debug adapter if the debug extension provided the telemetry key + // and the user opted in telemetry + if (this._raw.customTelemetryService && this.telemetryService.isOptedIn) { + // __GDPR__TODO__ We're sending events in the name of the debug extension and we can not ensure that those are declared correctly. + this._raw.customTelemetryService.publicLog(event.body.output, event.body.data); + } + + return; + } + + // Make sure to append output in the correct order by properly waiting on preivous promises #33822 + const waitFor = outputPromises.slice(); + const source = event.body.source ? { + lineNumber: event.body.line, + column: event.body.column ? event.body.column : 1, + source: this.getSource(event.body.source) + } : undefined; + if (event.body.variablesReference) { + const container = new ExpressionContainer(this, event.body.variablesReference, generateUuid()); + outputPromises.push(container.getChildren().then(children => { + return TPromise.join(waitFor).then(() => children.forEach(child => { + // Since we can not display multiple trees in a row, we are displaying these variables one after the other (ignoring their names) + child.name = null; + this.debugService.logToRepl(child, outputSeverity, source); + })); + })); + } else if (typeof event.body.output === 'string') { + TPromise.join(waitFor).then(() => this.debugService.logToRepl(event.body.output, outputSeverity, source)); + } + TPromise.join(outputPromises).then(() => outputPromises = []); + })); + + this.rawListeners.push(this._raw.onDidBreakpoint(event => { + const id = event.body && event.body.breakpoint ? event.body.breakpoint.id : undefined; + const breakpoint = this.model.getBreakpoints().filter(bp => bp.idFromAdapter === id).pop(); + const functionBreakpoint = this.model.getFunctionBreakpoints().filter(bp => bp.idFromAdapter === id).pop(); + + if (event.body.reason === 'new' && event.body.breakpoint.source) { + const source = this.getSource(event.body.breakpoint.source); + const bps = this.model.addBreakpoints(source.uri, [{ + column: event.body.breakpoint.column, + enabled: true, + lineNumber: event.body.breakpoint.line, + }], false); + if (bps.length === 1) { + this.model.updateBreakpoints({ [bps[0].getId()]: event.body.breakpoint }); + } + } + + if (event.body.reason === 'removed') { + if (breakpoint) { + this.model.removeBreakpoints([breakpoint]); + } + if (functionBreakpoint) { + this.model.removeFunctionBreakpoints(functionBreakpoint.getId()); + } + } + + if (event.body.reason === 'changed') { + if (breakpoint) { + if (!breakpoint.column) { + event.body.breakpoint.column = undefined; + } + this.model.setBreakpointSessionData(this.getId(), { [breakpoint.getId()]: event.body.breakpoint }); + } + if (functionBreakpoint) { + this.model.setBreakpointSessionData(this.getId(), { [functionBreakpoint.getId()]: event.body.breakpoint }); + } + } + })); + + this.rawListeners.push(this._raw.onDidLoadedSource(event => { + this._onDidLoadedSource.fire({ + reason: event.body.reason, + source: this.getSource(event.body.source) + }); + })); + + this.rawListeners.push(this._raw.onDidCustomEvent(event => { + this._onDidCustomEvent.fire(event); + })); + + this.rawListeners.push(this._raw.onDidExitAdapter(() => this._onDidExitAdapter.fire())); + } + + 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 + }); + }); + } + }); + } + + dispose(): void { + dispose(this.rawListeners); + this.model.removeSession(this.getId()); + if (!this._raw.disconnected) { + this._raw.disconnect().done(undefined, errors.onUnexpectedError); + } + this._raw = undefined; + } } diff --git a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts index 93891ff0b4e..c98bfcb2347 100644 --- a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts +++ b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts @@ -16,10 +16,9 @@ import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { formatPII } from 'vs/workbench/parts/debug/common/debugUtils'; import { SocketDebugAdapter } from 'vs/workbench/parts/debug/node/debugAdapter'; -import { SessionState, DebugEvent, IRawSession, IDebugAdapter } from 'vs/workbench/parts/debug/common/debug'; +import { DebugEvent, IRawSession, IDebugAdapter } from 'vs/workbench/parts/debug/common/debug'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; - export interface SessionExitedEvent extends DebugEvent { body: { exitCode: number, @@ -48,7 +47,7 @@ export class RawDebugSession implements IRawSession { private cancellationTokens: CancellationTokenSource[]; private _capabilities: DebugProtocol.Capabilities; private allThreadsContinued: boolean; - private state: SessionState = SessionState.LAUNCH; + private isAttached: boolean; private readonly _onDidInitialize: Emitter; private readonly _onDidStop: Emitter; @@ -64,11 +63,11 @@ export class RawDebugSession implements IRawSession { private readonly _onDidEvent: Emitter; constructor( - private id: string, + private sessionId: string, private debugServerPort: number, private _debugger: Debugger, public customTelemetryService: ITelemetryService, - public root: IWorkspaceFolder, + private root: IWorkspaceFolder, @INotificationService private notificationService: INotificationService, @ITelemetryService private telemetryService: ITelemetryService, @IOutputService private outputService: IOutputService @@ -92,10 +91,6 @@ export class RawDebugSession implements IRawSession { this._onDidEvent = new Emitter(); } - public getId(): string { - return this.id; - } - public get onDidInitialize(): Event { return this._onDidInitialize.event; } @@ -239,7 +234,7 @@ export class RawDebugSession implements IRawSession { } private onDapEvent(event: DebugEvent): void { - event.sessionId = this.id; + event.sessionId = this.sessionId; if (event.event === 'loadedSource') { // most frequent comes first this._onDidLoadedSource.fire(event); @@ -293,7 +288,7 @@ export class RawDebugSession implements IRawSession { } public attach(args: DebugProtocol.AttachRequestArguments): TPromise { - this.state = SessionState.ATTACH; + this.isAttached = true; return this.send('attach', args).then(response => this.readCapabilities(response)); } @@ -352,7 +347,7 @@ export class RawDebugSession implements IRawSession { } public terminate(restart = false): TPromise { - if (this.capabilities.supportsTerminateRequest && !this.terminated && this.state === SessionState.LAUNCH) { + if (this.capabilities.supportsTerminateRequest && !this.terminated && !this.isAttached) { this.terminated = true; return this.send('terminate', { restart }); } @@ -516,13 +511,12 @@ export class RawDebugSession implements IRawSession { this.cachedInitServerP = null; } - this._onDidExitAdapter.fire({ sessionId: this.getId() }); + this._onDidExitAdapter.fire({ sessionId: this.sessionId }); this.disconnected = true; if (!this.debugAdapter || this.debugAdapter instanceof SocketDebugAdapter) { return TPromise.as(null); } - return this.debugAdapter.stopSession(); } @@ -537,6 +531,6 @@ export class RawDebugSession implements IRawSession { if (!this.disconnected) { this.notificationService.error(nls.localize('debugAdapterCrash', "Debug adapter process has terminated unexpectedly")); } - this._onDidExitAdapter.fire({ sessionId: this.getId() }); + this._onDidExitAdapter.fire({ sessionId: this.sessionId }); } } diff --git a/src/vs/workbench/parts/debug/test/common/mockDebug.ts b/src/vs/workbench/parts/debug/test/common/mockDebug.ts index c9914760a19..6e272e451b8 100644 --- a/src/vs/workbench/parts/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/parts/debug/test/common/mockDebug.ts @@ -4,25 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import uri from 'vs/base/common/uri'; -import { Event, Emitter } from 'vs/base/common/event'; +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, DebugEvent, ISession, IConfigurationManager, IStackFrame, IBreakpointData, IBreakpointUpdateData, IConfig, IModel, IViewModel, IRawSession, IBreakpoint, LoadedSourceEvent, SessionState, IThread, IRawModelUpdate } from 'vs/workbench/parts/debug/common/debug'; +import { ILaunch, IDebugService, State, DebugEvent, ISession, IConfigurationManager, IStackFrame, IBreakpointData, IBreakpointUpdateData, IConfig, IModel, IViewModel, IRawSession, IBreakpoint, LoadedSourceEvent, IThread, IRawModelUpdate } from 'vs/workbench/parts/debug/common/debug'; import { Source } from 'vs/workbench/parts/debug/common/debugSource'; import { ISuggestion } from 'vs/editor/common/modes'; export class MockDebugService implements IDebugService { + public _serviceBrand: any; public get state(): State { return null; } - public get onDidCustomEvent(): Event { - return null; - } - public get onDidNewSession(): Event { return null; } @@ -35,10 +32,6 @@ export class MockDebugService implements IDebugService { return null; } - public get onDidLoadedSource(): Event { - return null; - } - public getConfigurationManager(): IConfigurationManager { return null; } @@ -115,12 +108,18 @@ export class MockDebugService implements IDebugService { public logToRepl(value: string): void { } public sourceIsNotAvailable(uri: uri): void { } + + public tryToAutoFocusStackFrame(thread: IThread): TPromise { + return TPromise.as(null); + } } export class MockSession implements ISession { + configuration: IConfig = { type: 'mock', request: 'launch' }; raw: IRawSession = new MockRawSession(); - state = SessionState.LAUNCH; + state = State.Stopped; + root: IWorkspaceFolder; getName(includeRoot: boolean): string { return 'mockname'; @@ -134,6 +133,18 @@ export class MockSession implements ISession { return null; } + get onDidCustomEvent(): Event { + return null; + } + + get onDidLoadedSource(): Event { + return null; + } + + get onDidExitAdapter(): Event { + return null; + } + getAllThreads(): ReadonlyArray { return []; } @@ -157,6 +168,8 @@ export class MockSession implements ISession { getId(): string { return 'mock'; } + + dispose(): void { } } export class MockRawSession implements IRawSession { @@ -164,12 +177,6 @@ export class MockRawSession implements IRawSession { public readyForBreakpoints = true; public emittedStopped = true; - public getId() { - return 'mockrawsession'; - } - - public root: IWorkspaceFolder; - public getLengthInSeconds(): number { return 100; } @@ -216,21 +223,6 @@ export class MockRawSession implements IRawSession { return {}; } - public get onDidEvent(): Event { - return null; - } - - public get onDidInitialize(): Event { - const emitter = new Emitter(); - return emitter.event; - } - - public get onDidExitAdapter(): Event<{ sessionId: string }> { - const emitter = new Emitter<{ sessionId: string }>(); - return emitter.event; - } - - public custom(request: string, args: any): TPromise { return TPromise.as(null); } @@ -239,6 +231,10 @@ export class MockRawSession implements IRawSession { return TPromise.as(null); } + public disconnect(restart?: boolean): TPromise { + return TPromise.as(null); + } + public threads(): TPromise { return TPromise.as(null); } diff --git a/src/vs/workbench/parts/debug/test/electron-browser/debugModel.test.ts b/src/vs/workbench/parts/debug/test/electron-browser/debugModel.test.ts index 3e448f864e7..5aa97ff564d 100644 --- a/src/vs/workbench/parts/debug/test/electron-browser/debugModel.test.ts +++ b/src/vs/workbench/parts/debug/test/electron-browser/debugModel.test.ts @@ -109,11 +109,11 @@ suite('Debug - Model', () => { test('threads simple', () => { const threadId = 1; const threadName = 'firstThread'; - const session = new Session({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, rawSession); - model.addSession(session); + const session = new Session('mock', { resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined, undefined, undefined); + assert.equal(model.getSessions().length, 1); model.rawUpdate({ - sessionId: rawSession.getId(), + sessionId: 'mock', threadId: threadId, thread: { id: threadId, @@ -140,10 +140,11 @@ suite('Debug - Model', () => { const stoppedReason = 'breakpoint'; // Add the threads - const session = new Session({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, rawSession); - model.addSession(session); + const session = new Session('mock', { resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined, undefined, undefined); + session['_raw'] = rawSession; + model.rawUpdate({ - sessionId: rawSession.getId(), + sessionId: 'mock', threadId: threadId1, thread: { id: threadId1, @@ -152,7 +153,7 @@ suite('Debug - Model', () => { }); model.rawUpdate({ - sessionId: rawSession.getId(), + sessionId: 'mock', threadId: threadId2, thread: { id: threadId2, @@ -162,7 +163,7 @@ suite('Debug - Model', () => { // Stopped event with all threads stopped model.rawUpdate({ - sessionId: rawSession.getId(), + sessionId: 'mock', threadId: threadId1, stoppedDetails: { reason: stoppedReason, @@ -230,11 +231,12 @@ suite('Debug - Model', () => { const runningThreadId = 2; const runningThreadName = 'runningThread'; const stoppedReason = 'breakpoint'; - const session = new Session({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, rawSession); - model.addSession(session); + const session = new Session('mock', { resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined, undefined, undefined); + session['_raw'] = rawSession; + // Add the threads model.rawUpdate({ - sessionId: rawSession.getId(), + sessionId: 'mock', threadId: stoppedThreadId, thread: { id: stoppedThreadId, @@ -243,7 +245,7 @@ suite('Debug - Model', () => { }); model.rawUpdate({ - sessionId: rawSession.getId(), + sessionId: 'mock', threadId: runningThreadId, thread: { id: runningThreadId, @@ -253,7 +255,7 @@ suite('Debug - Model', () => { // Stopped event with only one thread stopped model.rawUpdate({ - sessionId: rawSession.getId(), + sessionId: 'mock', threadId: stoppedThreadId, stoppedDetails: { reason: stoppedReason, @@ -341,7 +343,8 @@ suite('Debug - Model', () => { test('repl expressions', () => { assert.equal(model.getReplElements().length, 0); - const session = new Session({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, rawSession); + const session = new Session('mock', { resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined, undefined, undefined); + session['_raw'] = rawSession; const thread = new Thread(session, 'mockthread', 1); const stackFrame = new StackFrame(thread, 1, null, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 10 }, 1); model.addReplExpression(session, stackFrame, 'myVariable').done(); @@ -360,7 +363,7 @@ suite('Debug - Model', () => { }); test('stack frame get specific source name', () => { - const session = new Session({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, rawSession); + const session = new Session('mock', { resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined, model, undefined, undefined, undefined, undefined); let firstStackFrame: StackFrame; let secondStackFrame: StackFrame; const thread = new class extends Thread {