Files
azuredatastudio/src/vs/workbench/contrib/debug/browser/debugSession.ts

1010 lines
32 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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 severity from 'vs/base/common/severity';
import { Event, Emitter } from 'vs/base/common/event';
import { Position, IPosition } from 'vs/editor/common/core/position';
import * as aria from 'vs/base/browser/ui/aria/aria';
import { IDebugSession, IConfig, IThread, IRawModelUpdate, IDebugService, IRawStoppedDetails, State, LoadedSourceEvent, IFunctionBreakpoint, IExceptionBreakpoint, IBreakpoint, IExceptionInfo, AdapterEndEvent, IDebugger, VIEWLET_ID, IDebugConfiguration, IReplElement, IStackFrame, IExpression, IReplElementSource, IDataBreakpoint, IDebugSessionOptions } from 'vs/workbench/contrib/debug/common/debug';
import { Source } from 'vs/workbench/contrib/debug/common/debugSource';
import { mixin } from 'vs/base/common/objects';
import { Thread, ExpressionContainer, DebugModel } from 'vs/workbench/contrib/debug/common/debugModel';
import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession';
import { IProductService } from 'vs/platform/product/common/productService';
import { IWorkspaceFolder, IWorkspaceContextService } 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 { IHostService } from 'vs/workbench/services/host/browser/host';
import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { normalizeDriveLetter } from 'vs/base/common/labels';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { ReplModel } from 'vs/workbench/contrib/debug/common/replModel';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { variableSetEmitter } from 'vs/workbench/contrib/debug/browser/variablesView';
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
import { distinct } from 'vs/base/common/arrays';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
export class DebugSession implements IDebugSession {
private id: string;
private _subId: string | undefined;
private raw: RawDebugSession | undefined;
private initialized = false;
private _options: IDebugSessionOptions;
private sources = new Map<string, Source>();
private threads = new Map<number, Thread>();
private cancellationMap = new Map<number, CancellationTokenSource[]>();
private rawListeners: IDisposable[] = [];
private fetchThreadsScheduler: RunOnceScheduler | undefined;
private repl: ReplModel;
private readonly _onDidChangeState = new Emitter<void>();
private readonly _onDidEndAdapter = new Emitter<AdapterEndEvent>();
private readonly _onDidLoadedSource = new Emitter<LoadedSourceEvent>();
private readonly _onDidCustomEvent = new Emitter<DebugProtocol.Event>();
private readonly _onDidChangeREPLElements = new Emitter<void>();
private name: string | undefined;
private readonly _onDidChangeName = new Emitter<string>();
constructor(
private _configuration: { resolved: IConfig, unresolved: IConfig | undefined },
public root: IWorkspaceFolder | undefined,
private model: DebugModel,
options: IDebugSessionOptions | undefined,
@IDebugService private readonly debugService: IDebugService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IHostService private readonly hostService: IHostService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IViewletService private readonly viewletService: IViewletService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IProductService private readonly productService: IProductService,
@IExtensionHostDebugService private readonly extensionHostDebugService: IExtensionHostDebugService,
@IOpenerService private readonly openerService: IOpenerService,
@INotificationService private readonly notificationService: INotificationService,
@ILifecycleService lifecycleService: ILifecycleService
) {
this.id = generateUuid();
this._options = options || {};
if (this.hasSeparateRepl()) {
this.repl = new ReplModel();
} else {
this.repl = (this.parentSession as DebugSession).repl;
}
const toDispose: IDisposable[] = [];
toDispose.push(this.repl.onDidChangeElements(() => this._onDidChangeREPLElements.fire()));
if (lifecycleService) {
toDispose.push(lifecycleService.onShutdown(() => {
this.shutdown();
dispose(toDispose);
}));
}
}
getId(): string {
return this.id;
}
setSubId(subId: string | undefined) {
this._subId = subId;
}
get subId(): string | undefined {
return this._subId;
}
get configuration(): IConfig {
return this._configuration.resolved;
}
get unresolvedConfiguration(): IConfig | undefined {
return this._configuration.unresolved;
}
get parentSession(): IDebugSession | undefined {
return this._options.parentSession;
}
setConfiguration(configuration: { resolved: IConfig, unresolved: IConfig | undefined }) {
this._configuration = configuration;
}
getLabel(): string {
const includeRoot = this.workspaceContextService.getWorkspace().folders.length > 1;
const name = this.name || this.configuration.name;
return includeRoot && this.root ? `${name} (${resources.basenameOrAuthority(this.root.uri)})` : name;
}
setName(name: string): void {
this.name = name;
this._onDidChangeName.fire(name);
}
get state(): State {
if (!this.initialized) {
return State.Initializing;
}
if (!this.raw) {
return State.Inactive;
}
const focusedThread = this.debugService.getViewModel().focusedThread;
if (focusedThread && focusedThread.session === this) {
return focusedThread.stopped ? State.Stopped : State.Running;
}
if (this.getAllThreads().some(t => t.stopped)) {
return State.Stopped;
}
return State.Running;
}
get capabilities(): DebugProtocol.Capabilities {
return this.raw ? this.raw.capabilities : Object.create(null);
}
//---- events
get onDidChangeState(): Event<void> {
return this._onDidChangeState.event;
}
get onDidEndAdapter(): Event<AdapterEndEvent> {
return this._onDidEndAdapter.event;
}
get onDidChangeReplElements(): Event<void> {
return this._onDidChangeREPLElements.event;
}
get onDidChangeName(): Event<string> {
return this._onDidChangeName.event;
}
//---- DAP events
get onDidCustomEvent(): Event<DebugProtocol.Event> {
return this._onDidCustomEvent.event;
}
get onDidLoadedSource(): Event<LoadedSourceEvent> {
return this._onDidLoadedSource.event;
}
//---- DAP requests
/**
* create and initialize a new debug adapter for this session
*/
async initialize(dbgr: IDebugger): Promise<void> {
if (this.raw) {
// if there was already a connection make sure to remove old listeners
this.shutdown();
}
try {
const customTelemetryService = await dbgr.getCustomTelemetryService();
const debugAdapter = await dbgr.createDebugAdapter(this);
this.raw = new RawDebugSession(debugAdapter, dbgr, this.telemetryService, customTelemetryService, this.extensionHostDebugService, this.openerService, this.notificationService);
await this.raw.start();
this.registerListeners();
await this.raw!.initialize({
clientID: 'vscode',
clientName: this.productService.nameLong,
adapterID: this.configuration.type,
pathFormat: 'path',
linesStartAt1: true,
columnsStartAt1: true,
supportsVariableType: true, // #8858
supportsVariablePaging: true, // #9537
supportsRunInTerminalRequest: true, // #10574
locale: platform.locale
});
this.initialized = true;
this._onDidChangeState.fire();
this.model.setExceptionBreakpoints(this.raw!.capabilities.exceptionBreakpointFilters || []);
} catch (err) {
this.initialized = true;
this._onDidChangeState.fire();
this.shutdown();
throw err;
}
}
/**
* launch or attach to the debuggee
*/
async launchOrAttach(config: IConfig): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
// __sessionID only used for EH debugging (but we add it always for now...)
config.__sessionId = this.getId();
try {
await this.raw.launchOrAttach(config);
} catch (err) {
this.shutdown();
throw err;
}
}
/**
* end the current debug adapter session
*/
async terminate(restart = false): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
this.cancelAllRequests();
if (this.raw.capabilities.supportsTerminateRequest && this._configuration.resolved.request === 'launch') {
await this.raw.terminate(restart);
} else {
await this.raw.disconnect(restart);
}
}
/**
* end the current debug adapter session
*/
async disconnect(restart = false): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
this.cancelAllRequests();
await this.raw.disconnect(restart);
}
/**
* restart debug adapter session
*/
async restart(): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
this.cancelAllRequests();
await this.raw.restart();
}
async sendBreakpoints(modelUri: URI, breakpointsToSend: IBreakpoint[], sourceModified: boolean): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
if (!this.raw.readyForBreakpoints) {
return Promise.resolve(undefined);
}
const rawSource = this.getRawSource(modelUri);
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
if (rawSource.path) {
rawSource.path = normalizeDriveLetter(rawSource.path);
}
const response = await this.raw.setBreakpoints({
source: rawSource,
lines: breakpointsToSend.map(bp => bp.sessionAgnosticData.lineNumber),
breakpoints: breakpointsToSend.map(bp => ({ line: bp.sessionAgnosticData.lineNumber, column: bp.sessionAgnosticData.column, condition: bp.condition, hitCondition: bp.hitCondition, logMessage: bp.logMessage })),
sourceModified
});
if (response && response.body) {
const data = new Map<string, DebugProtocol.Breakpoint>();
for (let i = 0; i < breakpointsToSend.length; i++) {
data.set(breakpointsToSend[i].getId(), response.body.breakpoints[i]);
}
this.model.setBreakpointSessionData(this.getId(), this.capabilities, data);
}
}
async sendFunctionBreakpoints(fbpts: IFunctionBreakpoint[]): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
if (this.raw.readyForBreakpoints) {
const response = await this.raw.setFunctionBreakpoints({ breakpoints: fbpts });
if (response && response.body) {
const data = new Map<string, DebugProtocol.Breakpoint>();
for (let i = 0; i < fbpts.length; i++) {
data.set(fbpts[i].getId(), response.body.breakpoints[i]);
}
this.model.setBreakpointSessionData(this.getId(), this.capabilities, data);
}
}
}
async sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
if (this.raw.readyForBreakpoints) {
await this.raw.setExceptionBreakpoints({ filters: exbpts.map(exb => exb.filter) });
}
}
async dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean }> {
if (!this.raw) {
throw new Error('no debug adapter');
}
if (!this.raw.readyForBreakpoints) {
throw new Error(nls.localize('sessionNotReadyForBreakpoints', "Session is not ready for breakpoints"));
}
const response = await this.raw.dataBreakpointInfo({ name, variablesReference });
return response.body;
}
async sendDataBreakpoints(dataBreakpoints: IDataBreakpoint[]): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
if (this.raw.readyForBreakpoints) {
const response = await this.raw.setDataBreakpoints({ breakpoints: dataBreakpoints });
if (response && response.body) {
const data = new Map<string, DebugProtocol.Breakpoint>();
for (let i = 0; i < dataBreakpoints.length; i++) {
data.set(dataBreakpoints[i].getId(), response.body.breakpoints[i]);
}
this.model.setBreakpointSessionData(this.getId(), this.capabilities, data);
}
}
}
async breakpointsLocations(uri: URI, lineNumber: number): Promise<IPosition[]> {
if (!this.raw) {
throw new Error('no debug adapter');
}
const source = this.getRawSource(uri);
const response = await this.raw.breakpointLocations({ source, line: lineNumber });
if (!response.body || !response.body.breakpoints) {
return [];
}
const positions = response.body.breakpoints.map(bp => ({ lineNumber: bp.line, column: bp.column || 1 }));
return distinct(positions, p => `${p.lineNumber}:${p.column}`);
}
customRequest(request: string, args: any): Promise<DebugProtocol.Response> {
if (!this.raw) {
throw new Error('no debug adapter');
}
return this.raw.custom(request, args);
}
stackTrace(threadId: number, startFrame: number, levels: number): Promise<DebugProtocol.StackTraceResponse> {
if (!this.raw) {
throw new Error('no debug adapter');
}
const token = this.getNewCancellationToken(threadId);
return this.raw.stackTrace({ threadId, startFrame, levels }, token);
}
async exceptionInfo(threadId: number): Promise<IExceptionInfo | undefined> {
if (!this.raw) {
throw new Error('no debug adapter');
}
const response = await this.raw.exceptionInfo({ threadId });
if (response) {
return {
id: response.body.exceptionId,
description: response.body.description,
breakMode: response.body.breakMode,
details: response.body.details
};
}
return undefined;
}
scopes(frameId: number, threadId: number): Promise<DebugProtocol.ScopesResponse> {
if (!this.raw) {
throw new Error('no debug adapter');
}
const token = this.getNewCancellationToken(threadId);
return this.raw.scopes({ frameId }, token);
}
variables(variablesReference: number, threadId: number | undefined, filter: 'indexed' | 'named' | undefined, start: number | undefined, count: number | undefined): Promise<DebugProtocol.VariablesResponse> {
if (!this.raw) {
throw new Error('no debug adapter');
}
const token = threadId ? this.getNewCancellationToken(threadId) : undefined;
return this.raw.variables({ variablesReference, filter, start, count }, token);
}
evaluate(expression: string, frameId: number, context?: string): Promise<DebugProtocol.EvaluateResponse> {
if (!this.raw) {
throw new Error('no debug adapter');
}
return this.raw.evaluate({ expression, frameId, context });
}
async restartFrame(frameId: number, threadId: number): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
await this.raw.restartFrame({ frameId }, threadId);
}
async next(threadId: number): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
await this.raw.next({ threadId });
}
async stepIn(threadId: number): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
await this.raw.stepIn({ threadId });
}
async stepOut(threadId: number): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
await this.raw.stepOut({ threadId });
}
async stepBack(threadId: number): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
await this.raw.stepBack({ threadId });
}
async continue(threadId: number): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
await this.raw.continue({ threadId });
}
async reverseContinue(threadId: number): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
await this.raw.reverseContinue({ threadId });
}
async pause(threadId: number): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
await this.raw.pause({ threadId });
}
async terminateThreads(threadIds?: number[]): Promise<void> {
if (!this.raw) {
throw new Error('no debug adapter');
}
await this.raw.terminateThreads({ threadIds });
}
setVariable(variablesReference: number, name: string, value: string): Promise<DebugProtocol.SetVariableResponse> {
if (!this.raw) {
throw new Error('no debug adapter');
}
return this.raw.setVariable({ variablesReference, name, value });
}
gotoTargets(source: DebugProtocol.Source, line: number, column?: number): Promise<DebugProtocol.GotoTargetsResponse> {
if (!this.raw) {
throw new Error('no debug adapter');
}
return this.raw.gotoTargets({ source, line, column });
}
goto(threadId: number, targetId: number): Promise<DebugProtocol.GotoResponse> {
if (!this.raw) {
throw new Error('no debug adapter');
}
return this.raw.goto({ threadId, targetId });
}
loadSource(resource: URI): Promise<DebugProtocol.SourceResponse> {
if (!this.raw) {
return Promise.reject(new Error('no debug adapter'));
}
const source = this.getSourceForUri(resource);
let rawSource: DebugProtocol.Source;
if (source) {
rawSource = source.raw;
} else {
// create a Source
const data = Source.getEncodedDebugData(resource);
rawSource = { path: data.path, sourceReference: data.sourceReference };
}
return this.raw.source({ sourceReference: rawSource.sourceReference || 0, source: rawSource });
}
async getLoadedSources(): Promise<Source[]> {
if (!this.raw) {
return Promise.reject(new Error('no debug adapter'));
}
const response = await this.raw.loadedSources({});
if (response.body && response.body.sources) {
return response.body.sources.map(src => this.getSource(src));
} else {
return [];
}
}
async completions(frameId: number | undefined, text: string, position: Position, overwriteBefore: number, token: CancellationToken): Promise<DebugProtocol.CompletionsResponse> {
if (!this.raw) {
return Promise.reject(new Error('no debug adapter'));
}
return this.raw.completions({
frameId,
text,
column: position.column,
line: position.lineNumber,
}, token);
}
//---- threads
getThread(threadId: number): Thread | undefined {
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 = undefined): void {
if (reference !== undefined && reference !== null) {
const thread = this.threads.get(reference);
if (thread) {
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 {
const threadIds: number[] = [];
data.threads.forEach(thread => {
threadIds.push(thread.id);
if (!this.threads.has(thread.id)) {
// A new thread came in, initialize it.
this.threads.set(thread.id, new Thread(this, thread.name, thread.id));
} else if (thread.name) {
// Just the thread name got updated #18244
const oldThread = this.threads.get(thread.id);
if (oldThread) {
oldThread.name = thread.name;
}
}
});
this.threads.forEach(t => {
// Remove all old threads which are no longer part of the update #75980
if (threadIds.indexOf(t.threadId) === -1) {
this.threads.delete(t.threadId);
}
});
const stoppedDetails = data.stoppedDetails;
if (stoppedDetails) {
// Set the availability of the threads' callstacks depending on
// whether the thread is stopped or not
if (stoppedDetails.allThreadsStopped) {
this.threads.forEach(thread => {
thread.stoppedDetails = thread.threadId === stoppedDetails.threadId ? stoppedDetails : { reason: undefined };
thread.stopped = true;
thread.clearCallStack();
});
} else {
const thread = typeof stoppedDetails.threadId === 'number' ? this.threads.get(stoppedDetails.threadId) : undefined;
if (thread) {
// One thread is stopped, only update that thread.
thread.stoppedDetails = stoppedDetails;
thread.clearCallStack();
thread.stopped = true;
}
}
}
}
private async fetchThreads(stoppedDetails?: IRawStoppedDetails): Promise<void> {
if (this.raw) {
const response = await this.raw.threads();
if (response && response.body && response.body.threads) {
this.model.rawUpdate({
sessionId: this.getId(),
threads: response.body.threads,
stoppedDetails
});
}
}
}
initializeForTest(raw: RawDebugSession): void {
this.raw = raw;
this.registerListeners();
}
//---- private
private registerListeners(): void {
if (!this.raw) {
return;
}
this.rawListeners.push(this.raw.onDidInitialize(async () => {
aria.status(nls.localize('debuggingStarted', "Debugging started."));
const sendConfigurationDone = async () => {
if (this.raw && this.raw.capabilities.supportsConfigurationDoneRequest) {
try {
await this.raw.configurationDone();
} catch (e) {
// Disconnect the debug session on configuration done error #10596
if (this.raw) {
this.raw.disconnect();
}
}
}
return undefined;
};
// Send all breakpoints
try {
await this.debugService.sendAllBreakpoints(this);
} finally {
await sendConfigurationDone();
await this.fetchThreads();
}
}));
this.rawListeners.push(this.raw.onDidStop(async event => {
await this.fetchThreads(event.body);
const thread = typeof event.body.threadId === 'number' ? this.getThread(event.body.threadId) : undefined;
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
const promises = this.model.fetchCallStack(<Thread>thread);
const focus = async () => {
if (!event.body.preserveFocusHint && thread.getCallStack().length) {
await this.debugService.focusStackFrame(undefined, thread);
if (thread.stoppedDetails) {
if (this.configurationService.getValue<IDebugConfiguration>('debug').openDebug === 'openOnDebugBreak') {
this.viewletService.openViewlet(VIEWLET_ID);
}
if (this.configurationService.getValue<IDebugConfiguration>('debug').focusWindowOnBreak) {
this.hostService.focus();
}
}
}
};
await promises.topCallStack;
focus();
await promises.wholeCallStack;
if (!this.debugService.getViewModel().focusedStackFrame) {
// The top stack frame can be deemphesized so try to focus again #68616
focus();
}
}
this._onDidChangeState.fire();
}));
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();
}, 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);
const viewModel = this.debugService.getViewModel();
const focusedThread = viewModel.focusedThread;
if (focusedThread && event.body.threadId === focusedThread.threadId) {
// De-focus the thread in case it was focused
this.debugService.focusStackFrame(undefined, undefined, viewModel.focusedSession, false);
}
}
}));
this.rawListeners.push(this.raw.onDidTerminateDebugee(async event => {
aria.status(nls.localize('debuggingStopped', "Debugging stopped."));
if (event.body && event.body.restart) {
await this.debugService.restartSession(this, event.body.restart);
} else if (this.raw) {
await this.raw.disconnect();
}
}));
this.rawListeners.push(this.raw.onDidContinued(event => {
const threadId = event.body.allThreadsContinued !== false ? undefined : event.body.threadId;
if (threadId) {
const tokens = this.cancellationMap.get(threadId);
this.cancellationMap.delete(threadId);
if (tokens) {
tokens.forEach(t => t.cancel());
}
} else {
this.cancelAllRequests();
}
this.model.clearThreads(this.getId(), false, threadId);
this._onDidChangeState.fire();
}));
let outpuPromises: Promise<void>[] = [];
this.rawListeners.push(this.raw.onDidOutput(async event => {
if (!event.body || !this.raw) {
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 = outpuPromises.slice();
const source = event.body.source && event.body.line ? {
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, undefined, event.body.variablesReference, generateUuid());
outpuPromises.push(container.getChildren().then(async children => {
await Promise.all(waitFor);
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)
(<any>child).name = null;
this.appendToRepl(child, outputSeverity, source);
});
}));
} else if (typeof event.body.output === 'string') {
await Promise.all(waitFor);
this.appendToRepl(event.body.output, outputSeverity, source);
}
await Promise.all(outpuPromises);
outpuPromises = [];
}));
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.getIdFromAdapter(this.getId()) === id).pop();
const functionBreakpoint = this.model.getFunctionBreakpoints().filter(bp => bp.getIdFromAdapter(this.getId()) === id).pop();
if (event.body.reason === 'new' && event.body.breakpoint.source && event.body.breakpoint.line) {
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) {
const data = new Map<string, DebugProtocol.Breakpoint>([[bps[0].getId(), event.body.breakpoint]]);
this.model.setBreakpointSessionData(this.getId(), this.capabilities, data);
}
}
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;
}
const data = new Map<string, DebugProtocol.Breakpoint>([[breakpoint.getId(), event.body.breakpoint]]);
this.model.setBreakpointSessionData(this.getId(), this.capabilities, data);
}
if (functionBreakpoint) {
const data = new Map<string, DebugProtocol.Breakpoint>([[functionBreakpoint.getId(), event.body.breakpoint]]);
this.model.setBreakpointSessionData(this.getId(), this.capabilities, data);
}
}
}));
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(event => {
this.initialized = true;
this.model.setBreakpointSessionData(this.getId(), this.capabilities, undefined);
this.shutdown();
this._onDidEndAdapter.fire(event);
}));
}
// Disconnects and clears state. Session can be initialized again for a new connection.
private shutdown(): void {
dispose(this.rawListeners);
if (this.raw) {
this.raw.disconnect();
this.raw.dispose();
this.raw = undefined;
}
this.fetchThreadsScheduler = undefined;
this.model.clearThreads(this.getId(), true);
this._onDidChangeState.fire();
}
//---- sources
getSourceForUri(uri: URI): Source | undefined {
return this.sources.get(this.getUriKey(uri));
}
getSource(raw?: DebugProtocol.Source): Source {
let source = new Source(raw, this.getId());
const uriKey = this.getUriKey(source.uri);
const found = this.sources.get(uriKey);
if (found) {
source = found;
// merge attributes of new into existing
source.raw = mixin(source.raw, raw);
if (source.raw && raw) {
// Always take the latest presentation hint from adapter #42139
source.raw.presentationHint = raw.presentationHint;
}
} else {
this.sources.set(uriKey, source);
}
return source;
}
private getRawSource(uri: URI): DebugProtocol.Source {
const source = this.getSourceForUri(uri);
if (source) {
return source.raw;
} else {
const data = Source.getEncodedDebugData(uri);
return { name: data.name, path: data.path, sourceReference: data.sourceReference };
}
}
private getNewCancellationToken(threadId: number): CancellationToken {
const tokenSource = new CancellationTokenSource();
const tokens = this.cancellationMap.get(threadId) || [];
tokens.push(tokenSource);
this.cancellationMap.set(threadId, tokens);
return tokenSource.token;
}
private cancelAllRequests(): void {
this.cancellationMap.forEach(tokens => tokens.forEach(t => t.cancel()));
this.cancellationMap.clear();
}
private getUriKey(uri: URI): string {
// TODO: the following code does not make sense if uri originates from a different platform
return platform.isLinux ? uri.toString() : uri.toString().toLowerCase();
}
// REPL
getReplElements(): IReplElement[] {
return this.repl.getReplElements();
}
hasSeparateRepl(): boolean {
return !this.parentSession || this._options.repl !== 'mergeWithParent';
}
removeReplExpressions(): void {
this.repl.removeReplExpressions();
}
async addReplExpression(stackFrame: IStackFrame | undefined, name: string): Promise<void> {
await this.repl.addReplExpression(this, stackFrame, name);
// Evaluate all watch expressions and fetch variables again since repl evaluation might have changed some.
variableSetEmitter.fire();
}
appendToRepl(data: string | IExpression, severity: severity, source?: IReplElementSource): void {
this.repl.appendToRepl(this, data, severity, source);
}
logToRepl(sev: severity, args: any[], frame?: { uri: URI, line: number, column: number }) {
this.repl.logToRepl(this, sev, args, frame);
}
}