Skip to content

Commit

Permalink
debug: better cancelation support
Browse files Browse the repository at this point in the history
fixes #80374
  • Loading branch information
isidorn committed Sep 13, 2019
1 parent aa0231b commit 64980ea
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 65 deletions.
11 changes: 6 additions & 5 deletions src/vs/workbench/api/browser/mainThreadDebugService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,23 +361,24 @@ class ExtensionHostDebugAdapter extends AbstractDebugAdapter {
super();
}

public fireError(handle: number, err: Error) {
fireError(handle: number, err: Error) {
this._onError.fire(err);
}

public fireExit(handle: number, code: number, signal: string) {
fireExit(handle: number, code: number, signal: string) {
this._onExit.fire(code);
}

public startSession(): Promise<void> {
startSession(): Promise<void> {
return Promise.resolve(this._proxy.$startDASession(this._handle, this._ds.getSessionDto(this._session)));
}

public sendMessage(message: DebugProtocol.ProtocolMessage): void {
sendMessage(message: DebugProtocol.ProtocolMessage): void {
this._proxy.$sendDAMessage(this._handle, convertToDAPaths(message, true));
}

public stopSession(): Promise<void> {
async stopSession(): Promise<void> {
await this.cancelPendingRequests();
return Promise.resolve(this._proxy.$stopDASession(this._handle));
}
}
50 changes: 41 additions & 9 deletions src/vs/workbench/contrib/debug/browser/debugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { INotificationService } from 'vs/platform/notification/common/notification';
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';

export class DebugSession implements IDebugSession {

Expand All @@ -43,6 +44,7 @@ export class DebugSession implements IDebugSession {

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;
Expand Down Expand Up @@ -235,6 +237,7 @@ export class DebugSession implements IDebugSession {
*/
terminate(restart = false): Promise<void> {
if (this.raw) {
this.cancelAllRequests();
if (this.raw.capabilities.supportsTerminateRequest && this._configuration.resolved.request === 'launch') {
return this.raw.terminate(restart).then(response => {
return undefined;
Expand All @@ -252,6 +255,7 @@ export class DebugSession implements IDebugSession {
*/
disconnect(restart = false): Promise<void> {
if (this.raw) {
this.cancelAllRequests();
return this.raw.disconnect(restart).then(response => {
return undefined;
});
Expand All @@ -264,6 +268,7 @@ export class DebugSession implements IDebugSession {
*/
restart(): Promise<void> {
if (this.raw) {
this.cancelAllRequests();
return this.raw.restart().then(() => undefined);
}
return Promise.reject(new Error('no debug adapter'));
Expand Down Expand Up @@ -380,7 +385,8 @@ export class DebugSession implements IDebugSession {

stackTrace(threadId: number, startFrame: number, levels: number): Promise<DebugProtocol.StackTraceResponse> {
if (this.raw) {
return this.raw.stackTrace({ threadId, startFrame, levels });
const token = this.getNewCancellationToken(threadId);
return this.raw.stackTrace({ threadId, startFrame, levels }, token);
}
return Promise.reject(new Error('no debug adapter'));
}
Expand All @@ -402,16 +408,18 @@ export class DebugSession implements IDebugSession {
return Promise.reject(new Error('no debug adapter'));
}

scopes(frameId: number): Promise<DebugProtocol.ScopesResponse> {
scopes(frameId: number, threadId: number): Promise<DebugProtocol.ScopesResponse> {
if (this.raw) {
return this.raw.scopes({ frameId });
const token = this.getNewCancellationToken(threadId);
return this.raw.scopes({ frameId }, token);
}
return Promise.reject(new Error('no debug adapter'));
}

variables(variablesReference: number, filter: 'indexed' | 'named' | undefined, start: number | undefined, count: number | undefined): Promise<DebugProtocol.VariablesResponse> {
variables(variablesReference: number, threadId: number | undefined, filter: 'indexed' | 'named' | undefined, start: number | undefined, count: number | undefined): Promise<DebugProtocol.VariablesResponse> {
if (this.raw) {
return this.raw.variables({ variablesReference, filter, start, count });
const token = threadId ? this.getNewCancellationToken(threadId) : undefined;
return this.raw.variables({ variablesReference, filter, start, count }, token);
}
return Promise.reject(new Error('no debug adapter'));
}
Expand Down Expand Up @@ -541,14 +549,14 @@ export class DebugSession implements IDebugSession {
return Promise.reject(new Error('no debug adapter'));
}

completions(frameId: number | undefined, text: string, position: Position, overwriteBefore: number): Promise<CompletionItem[]> {
completions(frameId: number | undefined, text: string, position: Position, overwriteBefore: number, token: CancellationToken): Promise<CompletionItem[]> {
if (this.raw) {
return this.raw.completions({
frameId,
text,
column: position.column,
line: position.lineNumber
}).then(response => {
line: position.lineNumber,
}, token).then(response => {

const result: CompletionItem[] = [];
if (response && response.body && response.body.targets) {
Expand Down Expand Up @@ -757,6 +765,16 @@ export class DebugSession implements IDebugSession {

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();
}));
Expand Down Expand Up @@ -787,7 +805,7 @@ export class DebugSession implements IDebugSession {
source: this.getSource(event.body.source)
} : undefined;
if (event.body.variablesReference) {
const container = new ExpressionContainer(this, event.body.variablesReference, generateUuid());
const container = new ExpressionContainer(this, undefined, event.body.variablesReference, generateUuid());
outpuPromises.push(container.getChildren().then(children => {
return Promise.all(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)
Expand Down Expand Up @@ -896,6 +914,20 @@ export class DebugSession implements IDebugSession {
return source;
}

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();
Expand Down
54 changes: 27 additions & 27 deletions src/vs/workbench/contrib/debug/browser/rawDebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { IProcessEnvironment } from 'vs/base/common/platform';
import { env as processEnv } from 'vs/base/common/process';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { CancellationToken } from 'vs/base/common/cancellation';

/**
* This interface represents a single command line argument split into a "prefix" and a "path" half.
Expand Down Expand Up @@ -112,8 +113,6 @@ export class RawDebugSession implements IDisposable {
}
}));

this.toDispose.push(this.onDidContinued(() => this.cancelPendingRequests()));

this.debugAdapter.onEvent(event => {
switch (event.event) {
case 'initialized':
Expand Down Expand Up @@ -346,9 +345,9 @@ export class RawDebugSession implements IDisposable {
return Promise.reject(new Error('restartFrame not supported'));
}

completions(args: DebugProtocol.CompletionsArguments): Promise<DebugProtocol.CompletionsResponse> {
completions(args: DebugProtocol.CompletionsArguments, token: CancellationToken): Promise<DebugProtocol.CompletionsResponse> {
if (this.capabilities.supportsCompletionsRequest) {
return this.send<DebugProtocol.CompletionsResponse>('completions', args);
return this.send<DebugProtocol.CompletionsResponse>('completions', args, token);
}
return Promise.reject(new Error('completions not supported'));
}
Expand Down Expand Up @@ -389,8 +388,8 @@ export class RawDebugSession implements IDisposable {
return Promise.reject(new Error('configurationDone not supported'));
}

stackTrace(args: DebugProtocol.StackTraceArguments): Promise<DebugProtocol.StackTraceResponse> {
return this.send<DebugProtocol.StackTraceResponse>('stackTrace', args);
stackTrace(args: DebugProtocol.StackTraceArguments, token: CancellationToken): Promise<DebugProtocol.StackTraceResponse> {
return this.send<DebugProtocol.StackTraceResponse>('stackTrace', args, token);
}

exceptionInfo(args: DebugProtocol.ExceptionInfoArguments): Promise<DebugProtocol.ExceptionInfoResponse> {
Expand All @@ -400,12 +399,12 @@ export class RawDebugSession implements IDisposable {
return Promise.reject(new Error('exceptionInfo not supported'));
}

scopes(args: DebugProtocol.ScopesArguments): Promise<DebugProtocol.ScopesResponse> {
return this.send<DebugProtocol.ScopesResponse>('scopes', args);
scopes(args: DebugProtocol.ScopesArguments, token: CancellationToken): Promise<DebugProtocol.ScopesResponse> {
return this.send<DebugProtocol.ScopesResponse>('scopes', args, token);
}

variables(args: DebugProtocol.VariablesArguments): Promise<DebugProtocol.VariablesResponse> {
return this.send<DebugProtocol.VariablesResponse>('variables', args);
variables(args: DebugProtocol.VariablesArguments, token?: CancellationToken): Promise<DebugProtocol.VariablesResponse> {
return this.send<DebugProtocol.VariablesResponse>('variables', args, token);
}

source(args: DebugProtocol.SourceArguments): Promise<DebugProtocol.SourceResponse> {
Expand Down Expand Up @@ -482,7 +481,7 @@ export class RawDebugSession implements IDisposable {
if (!this.inShutdown) {
this.inShutdown = true;
if (this.debugAdapter) {
return this.send('disconnect', { restart }, 500).then(() => {
return this.send('disconnect', { restart }, undefined, 500).then(() => {
this.stopAdapter(error);
}, () => {
// ignore error
Expand All @@ -494,23 +493,10 @@ export class RawDebugSession implements IDisposable {
return Promise.resolve(undefined);
}

private cancelPendingRequests(): void {
if (this.debugAdapter) {
if (this.capabilities.supportsCancelRequest) {
this.debugAdapter.getPendingRequestIds().forEach(requestId => {
this.cancel({ requestId });
});
} else {
this.debugAdapter.cancelPendingRequests();
}
}
}

private stopAdapter(error?: Error): Promise<any> {
if (this.debugAdapter) {
const da = this.debugAdapter;
this.debugAdapter = null;
this.cancelPendingRequests();
return da.stopSession().then(_ => {
this.debugAdapterStopped = true;
this.fireAdapterExitEvent(error);
Expand Down Expand Up @@ -642,20 +628,34 @@ export class RawDebugSession implements IDisposable {
return this.windowsService.openExtensionDevelopmentHostWindow(args, env);
}

private send<R extends DebugProtocol.Response>(command: string, args: any, timeout?: number): Promise<R> {
private send<R extends DebugProtocol.Response>(command: string, args: any, token?: CancellationToken, timeout?: number): Promise<R> {
return new Promise<R>((completeDispatch, errorDispatch) => {
if (!this.debugAdapter) {
errorDispatch(new Error('no debug adapter found'));
return;
}
this.debugAdapter.sendRequest(command, args, (response: R) => {
let cancelationListener: IDisposable;
const requestId = this.debugAdapter.sendRequest(command, args, (response: R) => {
if (cancelationListener) {
cancelationListener.dispose();
}

if (response.success) {
completeDispatch(response);
} else {
errorDispatch(response);
}
}, timeout);
}).then(response => response, err => Promise.reject(this.handleErrorResponse(err)));

if (token) {
cancelationListener = token.onCancellationRequested(() => {
cancelationListener.dispose();
if (this.capabilities.supportsCancelRequest) {
this.cancel({ requestId });
}
});
}
}).then(undefined, err => Promise.reject(this.handleErrorResponse(err)));
}

private handleErrorResponse(errorResponse: DebugProtocol.Response): Error {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/contrib/debug/browser/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati
const text = model.getLineContent(position.lineNumber);
const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame;
const frameId = focusedStackFrame ? focusedStackFrame.frameId : undefined;
const suggestions = await session.completions(frameId, text, position, overwriteBefore);
const suggestions = await session.completions(frameId, text, position, overwriteBefore, token);
return { suggestions };
}

Expand Down
10 changes: 8 additions & 2 deletions src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter {
}
}

sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void, timeout?: number): void {
sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void, timeout?: number): number {
const request: any = {
command: command
};
Expand Down Expand Up @@ -101,6 +101,8 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter {
// store callback for this request
this.pendingRequests.set(request.seq, clb);
}

return request.seq;
}

acceptMessage(message: DebugProtocol.ProtocolMessage): void {
Expand Down Expand Up @@ -137,7 +139,11 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter {
this.sendMessage(message);
}

async cancelPendingRequests(): Promise<void> {
protected async cancelPendingRequests(): Promise<void> {
if (this.pendingRequests.size === 0) {
return Promise.resolve();
}

const pending = new Map<number, (e: DebugProtocol.Response) => void>();
this.pendingRequests.forEach((value, key) => pending.set(key, value));
await timeout(500);
Expand Down
10 changes: 4 additions & 6 deletions src/vs/workbench/contrib/debug/common/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ export interface IDebugSession extends ITreeElement {

stackTrace(threadId: number, startFrame: number, levels: number): Promise<DebugProtocol.StackTraceResponse>;
exceptionInfo(threadId: number): Promise<IExceptionInfo | undefined>;
scopes(frameId: number): Promise<DebugProtocol.ScopesResponse>;
variables(variablesReference: number, filter: 'indexed' | 'named' | undefined, start: number | undefined, count: number | undefined): Promise<DebugProtocol.VariablesResponse>;
scopes(frameId: number, threadId: number): Promise<DebugProtocol.ScopesResponse>;
variables(variablesReference: number, threadId: number | undefined, filter: 'indexed' | 'named' | undefined, start: number | undefined, count: number | undefined): Promise<DebugProtocol.VariablesResponse>;
evaluate(expression: string, frameId?: number, context?: string): Promise<DebugProtocol.EvaluateResponse>;
customRequest(request: string, args: any): Promise<DebugProtocol.Response>;

Expand All @@ -225,7 +225,7 @@ export interface IDebugSession extends ITreeElement {
pause(threadId: number): Promise<void>;
terminateThreads(threadIds: number[]): Promise<void>;

completions(frameId: number | undefined, text: string, position: Position, overwriteBefore: number): Promise<CompletionItem[]>;
completions(frameId: number | undefined, text: string, position: Position, overwriteBefore: number, token: CancellationToken): Promise<CompletionItem[]>;
setVariable(variablesReference: number | undefined, name: string, value: string): Promise<DebugProtocol.SetVariableResponse>;
loadSource(resource: uri): Promise<DebugProtocol.SourceResponse>;
getLoadedSources(): Promise<Source[]>;
Expand Down Expand Up @@ -504,10 +504,8 @@ export interface IDebugAdapter extends IDisposable {
startSession(): Promise<void>;
sendMessage(message: DebugProtocol.ProtocolMessage): void;
sendResponse(response: DebugProtocol.Response): void;
sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void, timeout?: number): void;
sendRequest(command: string, args: any, clb: (result: DebugProtocol.Response) => void, timeout?: number): number;
stopSession(): Promise<void>;
cancelPendingRequests(): void;
getPendingRequestIds(): number[];
}

export interface IDebugAdapterFactory extends ITerminalLauncher {
Expand Down
Loading

1 comment on commit 64980ea

@ivankravets
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check #82354

Please sign in to comment.