提交 64980ea1 编写于 作者: I isidor

debug: better cancelation support

fixes #80374
上级 aa0231b0
......@@ -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));
}
}
......@@ -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 {
......@@ -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;
......@@ -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;
......@@ -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;
});
......@@ -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'));
......@@ -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'));
}
......@@ -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'));
}
......@@ -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) {
......@@ -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();
}));
......@@ -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)
......@@ -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();
......
......@@ -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.
......@@ -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':
......@@ -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'));
}
......@@ -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> {
......@@ -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> {
......@@ -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
......@@ -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);
......@@ -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 {
......
......@@ -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 };
}
......
......@@ -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
};
......@@ -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 {
......@@ -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);
......
......@@ -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>;
......@@ -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[]>;
......@@ -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 {
......
......@@ -37,6 +37,7 @@ export class ExpressionContainer implements IExpressionContainer {
constructor(
protected session: IDebugSession | undefined,
protected threadId: number | undefined,
private _reference: number | undefined,
private id: string,
public namedVariables: number | undefined = 0,
......@@ -85,7 +86,7 @@ export class ExpressionContainer implements IExpressionContainer {
for (let i = 0; i < numberOfChunks; i++) {
const start = (this.startOfVariables || 0) + i * chunkSize;
const count = Math.min(chunkSize, this.indexedVariables - i * chunkSize);
children.push(new Variable(this.session, this, this.reference, `[${start}..${start + count - 1}]`, '', '', undefined, count, { kind: 'virtual' }, undefined, true, start));
children.push(new Variable(this.session, this.threadId, this, this.reference, `[${start}..${start + count - 1}]`, '', '', undefined, count, { kind: 'virtual' }, undefined, true, start));
}
return children;
......@@ -109,12 +110,12 @@ export class ExpressionContainer implements IExpressionContainer {
}
private fetchVariables(start: number | undefined, count: number | undefined, filter: 'indexed' | 'named' | undefined): Promise<Variable[]> {
return this.session!.variables(this.reference || 0, filter, start, count).then(response => {
return this.session!.variables(this.reference || 0, this.threadId, filter, start, count).then(response => {
return response && response.body && response.body.variables
? distinct(response.body.variables.filter(v => !!v && isString(v.name)), (v: DebugProtocol.Variable) => v.name).map((v: DebugProtocol.Variable) =>
new Variable(this.session, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.presentationHint, v.type))
new Variable(this.session, this.threadId, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.presentationHint, v.type))
: [];
}, (e: Error) => [new Variable(this.session, this, 0, e.message, e.message, '', 0, 0, { kind: 'virtual' }, undefined, false)]);
}, (e: Error) => [new Variable(this.session, this.threadId, this, 0, e.message, e.message, '', 0, 0, { kind: 'virtual' }, undefined, false)]);
}
// The adapter explicitly sents the children count of an expression only if there are lots of children which should be chunked.
......@@ -171,7 +172,7 @@ export class Expression extends ExpressionContainer implements IExpression {
public available: boolean;
constructor(public name: string, id = generateUuid()) {
super(undefined, 0, id);
super(undefined, undefined, 0, id);
this.available = false;
// name is not set if the expression is just being added
// in that case do not set default value to prevent flashing #14499
......@@ -196,6 +197,7 @@ export class Variable extends ExpressionContainer implements IExpression {
constructor(
session: IDebugSession | undefined,
threadId: number | undefined,
public parent: IExpressionContainer,
reference: number | undefined,
public name: string,
......@@ -208,7 +210,7 @@ export class Variable extends ExpressionContainer implements IExpression {
public available = true,
startOfVariables = 0
) {
super(session, reference, `variable:${parent.getId()}:${name}`, namedVariables, indexedVariables, startOfVariables);
super(session, threadId, reference, `variable:${parent.getId()}:${name}`, namedVariables, indexedVariables, startOfVariables);
this.value = value || '';
}
......@@ -248,7 +250,7 @@ export class Scope extends ExpressionContainer implements IScope {
indexedVariables?: number,
public range?: IRange
) {
super(stackFrame.thread.session, reference, `scope:${name}:${index}`, namedVariables, indexedVariables);
super(stackFrame.thread.session, stackFrame.thread.threadId, reference, `scope:${name}:${index}`, namedVariables, indexedVariables);
}
toString(): string {
......@@ -276,7 +278,7 @@ export class StackFrame implements IStackFrame {
getScopes(): Promise<IScope[]> {
if (!this.scopes) {
this.scopes = this.thread.session.scopes(this.frameId).then(response => {
this.scopes = this.thread.session.scopes(this.frameId, this.thread.threadId).then(response => {
return response && response.body && response.body.scopes ?
response.body.scopes.map((rs, index) => new Scope(this, index, rs.name, rs.variablesReference, rs.expensive, rs.namedVariables, rs.indexedVariables,
rs.line && rs.column && rs.endLine && rs.endColumn ? new Range(rs.line, rs.column, rs.endLine, rs.endColumn) : undefined)) : [];
......
......@@ -97,7 +97,7 @@ export class ReplEvaluationInput implements IReplElement {
export class ReplEvaluationResult extends ExpressionContainer implements IReplElement {
constructor() {
super(undefined, 0, generateUuid());
super(undefined, undefined, 0, generateUuid());
}
toString(): string {
......
......@@ -127,8 +127,8 @@ export class SocketDebugAdapter extends StreamDebugAdapter {
});
}
stopSession(): Promise<void> {
async stopSession(): Promise<void> {
await this.cancelPendingRequests();
if (this.socket) {
this.socket.end();
this.socket = undefined;
......@@ -249,7 +249,7 @@ export class ExecutableDebugAdapter extends StreamDebugAdapter {
}
}
stopSession(): Promise<void> {
async stopSession(): Promise<void> {
if (!this.serverProcess) {
return Promise.resolve(undefined);
......@@ -258,6 +258,7 @@ export class ExecutableDebugAdapter extends StreamDebugAdapter {
// when killing a process in windows its child
// processes are *not* killed but become root
// processes. Therefore we use TASKKILL.EXE
await this.cancelPendingRequests();
if (platform.isWindows) {
return new Promise<void>((c, e) => {
const killer = cp.exec(`taskkill /F /T /PID ${this.serverProcess!.pid}`, function (err, stdout, stderr) {
......
......@@ -76,7 +76,7 @@ suite('Debug - Base Debug View', () => {
const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: undefined!, endColumn: undefined! }, 0);
const scope = new Scope(stackFrame, 1, 'local', 1, false, 10, 10);
let variable = new Variable(session, scope, 2, 'foo', 'bar.foo', undefined!, 0, 0, {}, 'string');
let variable = new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined!, 0, 0, {}, 'string');
let expression = $('.');
let name = $('.');
let value = $('.');
......@@ -104,7 +104,7 @@ suite('Debug - Base Debug View', () => {
assert.ok(value.querySelector('a'));
assert.equal(value.querySelector('a')!.textContent, variable.value);
variable = new Variable(session, scope, 2, 'console', 'console', '5', 0, 0, { kind: 'virtual' });
variable = new Variable(session, 1, scope, 2, 'console', 'console', '5', 0, 0, { kind: 'virtual' });
expression = $('.');
name = $('.');
value = $('.');
......
......@@ -264,7 +264,7 @@ export class MockSession implements IDebugSession {
scopes(frameId: number): Promise<DebugProtocol.ScopesResponse> {
throw new Error('Method not implemented.');
}
variables(variablesReference: number, filter: 'indexed' | 'named', start: number, count: number): Promise<DebugProtocol.VariablesResponse> {
variables(variablesReference: number, threadId: number | undefined, filter: 'indexed' | 'named', start: number, count: number): Promise<DebugProtocol.VariablesResponse> {
throw new Error('Method not implemented.');
}
evaluate(expression: string, frameId: number, context?: string): Promise<DebugProtocol.EvaluateResponse> {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册