Skip to content

Commit

Permalink
Extend support for data breakpoints in VS Code API
Browse files Browse the repository at this point in the history
- Allow the user to manage data breakpoints through vscode.debug API
- Add methods to gain information about a potential data breakpoint
- Allow data breakpoints to have different sources
-- Keep use case where data id is already known (current)
-- Add infrastructure for already existing address resolution
-- Extend for dynamic variables resolved for a session
--- Ensure dynamic variables are resolved in the debug model

Communication:
- Adapt DataBreakpoint with source between extension host and main
- Expose DataBreakpoint in VS Code API, previously not exposed

Minor:
- Make bytes optional in data bytes info, as it is the same in the DAP

Fixes microsoft#195151
  • Loading branch information
martin-fleck-at committed Sep 4, 2024
1 parent 65af771 commit ede5a2a
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 27 deletions.
61 changes: 60 additions & 1 deletion extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import * as assert from 'assert';
import { basename } from 'path';
import { commands, debug, Disposable, FunctionBreakpoint, window, workspace } from 'vscode';
import { commands, DataBreakpoint, debug, Disposable, FunctionBreakpoint, window, workspace } from 'vscode';
import { assertNoRpc, createRandomFile, disposeAll } from '../utils';

suite('vscode API - debug', function () {
Expand Down Expand Up @@ -60,6 +60,65 @@ suite('vscode API - debug', function () {
assert.strictEqual(functionBreakpoint.functionName, 'func');
});


test('data breakpoint - dataId', async function () {
debug.addBreakpoints([new DataBreakpoint({ type: 'variable', dataId: 'dataId' }, 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]);
const variableDbp = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint;
assert.strictEqual(variableDbp.condition, 'condition');
assert.strictEqual(variableDbp.hitCondition, 'hitCondition');
assert.strictEqual(variableDbp.logMessage, 'logMessage');
assert.strictEqual(variableDbp.enabled, false);
assert.strictEqual(variableDbp.label, 'data');
assert.strictEqual(variableDbp.source.type, 'variable');
assert.strictEqual(variableDbp.source.dataId, 'dataId');
assert.strictEqual(variableDbp.canPersist, false);
assert.strictEqual(variableDbp.accessType, 'readWrite');
});

test('data breakpoint - variable', async function () {
debug.addBreakpoints([new DataBreakpoint('dataId', 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]);
const dataIdDbp = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint;
assert.strictEqual(dataIdDbp.condition, 'condition');
assert.strictEqual(dataIdDbp.hitCondition, 'hitCondition');
assert.strictEqual(dataIdDbp.logMessage, 'logMessage');
assert.strictEqual(dataIdDbp.enabled, false);
assert.strictEqual(dataIdDbp.label, 'data');
assert.strictEqual(dataIdDbp.source.type, 'variable');
assert.strictEqual(dataIdDbp.source.dataId, 'dataId');
assert.strictEqual(dataIdDbp.canPersist, false);
assert.strictEqual(dataIdDbp.accessType, 'readWrite');
});

test('data breakpoint - address', async function () {
debug.addBreakpoints([new DataBreakpoint({ type: 'address', address: '0x00000', bytes: 4 }, 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]);
const addressDbp = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint;
assert.strictEqual(addressDbp.condition, 'condition');
assert.strictEqual(addressDbp.hitCondition, 'hitCondition');
assert.strictEqual(addressDbp.logMessage, 'logMessage');
assert.strictEqual(addressDbp.enabled, false);
assert.strictEqual(addressDbp.label, 'data');
assert.strictEqual(addressDbp.source.type, 'address');
assert.strictEqual(addressDbp.source.address, '0x00000');
assert.strictEqual(addressDbp.source.bytes, 4);
assert.strictEqual(addressDbp.canPersist, false);
assert.strictEqual(addressDbp.accessType, 'readWrite');
});

test('data breakpoint - dynamic variable', async function () {
debug.addBreakpoints([new DataBreakpoint({ type: 'dynamicVariable', name: 'i', variablesReference: 1000 }, 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]);
const dynamicVariableDbp = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint;
assert.strictEqual(dynamicVariableDbp.condition, 'condition');
assert.strictEqual(dynamicVariableDbp.hitCondition, 'hitCondition');
assert.strictEqual(dynamicVariableDbp.logMessage, 'logMessage');
assert.strictEqual(dynamicVariableDbp.enabled, false);
assert.strictEqual(dynamicVariableDbp.label, 'data');
assert.strictEqual(dynamicVariableDbp.source.type, 'dynamicVariable');
assert.strictEqual(dynamicVariableDbp.source.name, 'i');
assert.strictEqual(dynamicVariableDbp.source.variablesReference, 1000);
assert.strictEqual(dynamicVariableDbp.canPersist, false);
assert.strictEqual(dynamicVariableDbp.accessType, 'readWrite');
});

test('start debugging', async function () {
let stoppedEvents = 0;
let variablesReceived: () => void;
Expand Down
33 changes: 29 additions & 4 deletions src/vs/workbench/api/browser/mainThreadDebugService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { URI as uri, UriComponents } from '../../../base/common/uri.js';
import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind, IDebugVisualization, DataBreakpointSetType } from '../../contrib/debug/common/debug.js';
import {
ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext,
IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto, IStartDebuggingOptions, IDebugConfiguration, IThreadFocusDto, IStackFrameFocusDto
IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto, IStartDebuggingOptions, IDebugConfiguration, IThreadFocusDto, IStackFrameFocusDto,
IDataBreakpointInfo
} from '../common/extHost.protocol.js';
import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
import severity from '../../../base/common/severity.js';
Expand Down Expand Up @@ -173,7 +174,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb
const bps = this.debugService.getModel().getBreakpoints();
const fbps = this.debugService.getModel().getFunctionBreakpoints();
const dbps = this.debugService.getModel().getDataBreakpoints();
if (bps.length > 0 || fbps.length > 0) {
if (bps.length > 0 || fbps.length > 0 || dbps.length > 0) {
this._proxy.$acceptBreakpointsDelta({
added: this.convertToDto(bps).concat(this.convertToDto(fbps)).concat(this.convertToDto(dbps))
});
Expand Down Expand Up @@ -234,10 +235,16 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb
} else if (dto.type === 'data') {
this.debugService.addDataBreakpoint({
description: dto.label,
src: { type: DataBreakpointSetType.Variable, dataId: dto.dataId },
src: dto.source.type === 'variable' ? { type: DataBreakpointSetType.Variable, dataId: dto.source.dataId }
: dto.source.type === 'dynamicVariable' ? { type: DataBreakpointSetType.DynamicVariable, name: dto.source.name, variablesReference: dto.source.variablesReference }
: { type: DataBreakpointSetType.Address, address: dto.source.address, bytes: dto.source.bytes },
condition: dto.condition,
enabled: dto.enabled,
hitCondition: dto.hitCondition,
canPersist: dto.canPersist,
accessTypes: dto.accessTypes,
accessType: dto.accessType,
logMessage: dto.logMessage,
mode: dto.mode
});
}
Expand Down Expand Up @@ -369,6 +376,22 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb
return Promise.reject(new ErrorNoTelemetry('debug session not found'));
}

public $getDataBreakpointInfo(sessionId: DebugSessionUUID, name: string, variablesReference?: number): Promise<IDataBreakpointInfo | undefined> {
const session = this.debugService.getModel().getSession(sessionId, true);
if (session) {
return Promise.resolve(session.dataBreakpointInfo(name, variablesReference));
}
return Promise.reject(new ErrorNoTelemetry('debug session not found'));
}

public $getDataBytesBreakpointInfo(sessionId: DebugSessionUUID, address: string, bytes?: number): Promise<IDataBreakpointInfo | undefined> {
const session = this.debugService.getModel().getSession(sessionId, true);
if (session) {
return Promise.resolve(session.dataBytesBreakpointInfo(address, bytes));
}
return Promise.reject(new ErrorNoTelemetry('debug session not found'));
}

public $stopDebugging(sessionId: DebugSessionUUID | undefined): Promise<void> {
if (sessionId) {
const session = this.debugService.getModel().getSession(sessionId, true);
Expand Down Expand Up @@ -457,7 +480,9 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb
return {
type: 'data',
id: dbp.getId(),
dataId: dbp.src.type === DataBreakpointSetType.Variable ? dbp.src.dataId : dbp.src.address,
source: dbp.src.type === DataBreakpointSetType.Variable ? { type: 'variable', dataId: dbp.src.dataId }
: dbp.src.type === DataBreakpointSetType.DynamicVariable ? { type: 'dynamicVariable', name: dbp.src.name, variablesReference: dbp.src.variablesReference }
: { type: 'address', address: dbp.src.address, bytes: dbp.src.bytes },
enabled: dbp.enabled,
condition: dbp.condition,
hitCondition: dbp.hitCondition,
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1562,6 +1562,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
CompletionTriggerKind: extHostTypes.CompletionTriggerKind,
ConfigurationTarget: extHostTypes.ConfigurationTarget,
CustomExecution: extHostTypes.CustomExecution,
DataBreakpoint: extHostTypes.DataBreakpoint,
DebugAdapterExecutable: extHostTypes.DebugAdapterExecutable,
DebugAdapterInlineImplementation: extHostTypes.DebugAdapterInlineImplementation,
DebugAdapterNamedPipeServer: extHostTypes.DebugAdapterNamedPipeServer,
Expand Down
12 changes: 8 additions & 4 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from '../../servic
import * as search from '../../services/search/common/search.js';
import { TextSearchCompleteMessage } from '../../services/search/common/searchExtTypes.js';
import { ISaveProfileResult } from '../../services/userDataProfile/common/userDataProfile.js';
import type { TerminalShellExecutionCommandLineConfidence } from 'vscode';
import type * as vscode from 'vscode';

export interface IWorkspaceData extends IStaticWorkspaceData {
folders: { uri: UriComponents; name: string; index: number }[];
Expand Down Expand Up @@ -1632,6 +1632,8 @@ export interface MainThreadDebugServiceShape extends IDisposable {
$setDebugSessionName(id: DebugSessionUUID, name: string): void;
$customDebugAdapterRequest(id: DebugSessionUUID, command: string, args: any): Promise<any>;
$getDebugProtocolBreakpoint(id: DebugSessionUUID, breakpoinId: string): Promise<DebugProtocol.Breakpoint | undefined>;
$getDataBreakpointInfo(id: DebugSessionUUID, name: string, variablesReference?: number): Promise<IDataBreakpointInfo | undefined>;
$getDataBytesBreakpointInfo(id: DebugSessionUUID, address: string, bytes?: number): Promise<IDataBreakpointInfo | undefined>;
$appendDebugConsole(value: string): void;
$registerBreakpoints(breakpoints: Array<ISourceMultiBreakpointDto | IFunctionBreakpointDto | IDataBreakpointDto>): Promise<void>;
$unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[], dataBreakpointIds: string[]): Promise<void>;
Expand Down Expand Up @@ -2338,8 +2340,8 @@ export interface ExtHostTerminalServiceShape {

export interface ExtHostTerminalShellIntegrationShape {
$shellIntegrationChange(instanceId: number): void;
$shellExecutionStart(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, cwd: UriComponents | undefined): void;
$shellExecutionEnd(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, exitCode: number | undefined): void;
$shellExecutionStart(instanceId: number, commandLineValue: string, commandLineConfidence: vscode.TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, cwd: UriComponents | undefined): void;
$shellExecutionEnd(instanceId: number, commandLineValue: string, commandLineConfidence: vscode.TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, exitCode: number | undefined): void;
$shellExecutionData(instanceId: number, data: string): void;
$cwdChange(instanceId: number, cwd: UriComponents | undefined): void;
$closeTerminal(instanceId: number): void;
Expand Down Expand Up @@ -2392,9 +2394,11 @@ export interface IFunctionBreakpointDto extends IBreakpointDto {
mode?: string;
}

export type IDataBreakpointInfo = DebugProtocol.DataBreakpointInfoResponse['body'];

export interface IDataBreakpointDto extends IBreakpointDto {
type: 'data';
dataId: string;
source: vscode.DataBreakpointSource;
canPersist: boolean;
label: string;
accessTypes?: DebugProtocol.DataBreakpointAccessType[];
Expand Down
22 changes: 19 additions & 3 deletions src/vs/workbench/api/common/extHostDebugService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/ex
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
import { ISignService } from '../../../platform/sign/common/sign.js';
import { IWorkspaceFolder } from '../../../platform/workspace/common/workspace.js';
import { DebugSessionUUID, ExtHostDebugServiceShape, IBreakpointsDeltaDto, IThreadFocusDto, IStackFrameFocusDto, IDebugSessionDto, IFunctionBreakpointDto, ISourceMultiBreakpointDto, MainContext, MainThreadDebugServiceShape } from './extHost.protocol.js';
import { DebugSessionUUID, ExtHostDebugServiceShape, IBreakpointsDeltaDto, IThreadFocusDto, IStackFrameFocusDto, IDebugSessionDto, IFunctionBreakpointDto, ISourceMultiBreakpointDto, MainContext, MainThreadDebugServiceShape, IDataBreakpointDto } from './extHost.protocol.js';
import { IExtHostEditorTabs } from './extHostEditorTabs.js';
import { IExtHostExtensionService } from './extHostExtensionService.js';
import { IExtHostRpcService } from './extHostRpcService.js';
Expand Down Expand Up @@ -411,7 +411,7 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I
this.fireBreakpointChanges(breakpoints, [], []);

// convert added breakpoints to DTOs
const dtos: Array<ISourceMultiBreakpointDto | IFunctionBreakpointDto> = [];
const dtos: Array<ISourceMultiBreakpointDto | IFunctionBreakpointDto | IDataBreakpointDto> = [];
const map = new Map<string, ISourceMultiBreakpointDto>();
for (const bp of breakpoints) {
if (bp instanceof SourceBreakpoint) {
Expand Down Expand Up @@ -728,7 +728,7 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I
if (bpd.type === 'function') {
bp = new FunctionBreakpoint(bpd.functionName, bpd.enabled, bpd.condition, bpd.hitCondition, bpd.logMessage, bpd.mode);
} else if (bpd.type === 'data') {
bp = new DataBreakpoint(bpd.label, bpd.dataId, bpd.canPersist, bpd.enabled, bpd.hitCondition, bpd.condition, bpd.logMessage, bpd.mode);
bp = new DataBreakpoint(bpd.source, bpd.accessType, bpd.canPersist, bpd.label, bpd.enabled, bpd.hitCondition, bpd.condition, bpd.logMessage, bpd.mode);
} else {
const uri = URI.revive(bpd.uri);
bp = new SourceBreakpoint(new Location(uri, new Position(bpd.line, bpd.character)), bpd.enabled, bpd.condition, bpd.hitCondition, bpd.logMessage, bpd.mode);
Expand Down Expand Up @@ -769,6 +769,16 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I
sbp.hitCondition = bpd.hitCondition;
sbp.logMessage = bpd.logMessage;
sbp.location = new Location(URI.revive(bpd.uri), new Position(bpd.line, bpd.character));
} else if (bp instanceof DataBreakpoint && bpd.type === 'data') {
const dbp = <any>bp;
dbp.enabled = bpd.enabled;
dbp.condition = bpd.condition;
dbp.hitCondition = bpd.hitCondition;
dbp.logMessage = bpd.logMessage;
dbp.label = bpd.label;
dbp.source = bpd.source;
dbp.canPersist = bpd.canPersist;
dbp.accessType = bpd.accessType;
}
c.push(bp);
}
Expand Down Expand Up @@ -1133,6 +1143,12 @@ export class ExtHostDebugSession {
},
getDebugProtocolBreakpoint(breakpoint: vscode.Breakpoint): Promise<vscode.DebugProtocolBreakpoint | undefined> {
return that._debugServiceProxy.$getDebugProtocolBreakpoint(that._id, breakpoint.id);
},
getDataBreakpointInfo(name: string, variablesReference?: number): Promise<vscode.DataBreakpointInfo | undefined> {
return that._debugServiceProxy.$getDataBreakpointInfo(that._id, name, variablesReference);
},
getDataBytesBreakpointInfo(address: string, bytes?: number): Promise<vscode.DataBreakpointInfo | undefined> {
return that._debugServiceProxy.$getDataBytesBreakpointInfo(that._id, address, bytes);
}
});
}
Expand Down
20 changes: 11 additions & 9 deletions src/vs/workbench/api/common/extHostTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3076,17 +3076,19 @@ export class FunctionBreakpoint extends Breakpoint {
@es5ClassCompat
export class DataBreakpoint extends Breakpoint {
readonly label: string;
readonly dataId: string;
readonly source: vscode.DataBreakpointSource;
readonly canPersist: boolean;
readonly accessType: vscode.DataBreakpointAccessType;

constructor(label: string, dataId: string, canPersist: boolean, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string, mode?: string) {
constructor(source: vscode.DataBreakpointSource | string, accessType: vscode.DataBreakpointAccessType, canPersist?: boolean, label?: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string, mode?: string) {
super(enabled, condition, hitCondition, logMessage, mode);
if (!dataId) {
throw illegalArgument('dataId');
}
this.label = label;
this.dataId = dataId;
this.canPersist = canPersist;
this.source = typeof source === 'string' ? { type: 'variable', dataId: source } : source;
this.accessType = accessType;
this.canPersist = canPersist ?? false;
this.label = label ? label
: this.source.type === 'variable' ? `DataId '${this.source.dataId}'`
: this.source.type === 'address' ? `Address '${this.source.address}${this.source.bytes ? `,${this.source.bytes}'` : ''}`
: `Variable '${this.source.name}${this.source.variablesReference ? `,${this.source.variablesReference}` : ''}'`;
}
}

Expand Down Expand Up @@ -4127,7 +4129,7 @@ export function validateTestCoverageCount(cc?: vscode.TestCoverageCount) {
}

if (cc.covered > cc.total) {
throw new Error(`The total number of covered items (${cc.covered}) cannot be greater than the total (${cc.total})`);
throw new Error(`The total number of covered items (${cc.covered}) cannot be greater than the total(${cc.total})`);
}

if (cc.total < 0) {
Expand Down
6 changes: 3 additions & 3 deletions src/vs/workbench/contrib/debug/browser/debugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,19 +545,19 @@ export class DebugSession implements IDebugSession, IDisposable {
}
}

dataBytesBreakpointInfo(address: string, bytes: number): Promise<IDataBreakpointInfoResponse | undefined> {
dataBytesBreakpointInfo(address: string, bytes?: number): Promise<IDataBreakpointInfoResponse | undefined> {
if (this.raw?.capabilities.supportsDataBreakpointBytes === false) {
throw new Error(localize('sessionDoesNotSupporBytesBreakpoints', "Session does not support breakpoints with bytes"));
}

return this._dataBreakpointInfo({ name: address, bytes, asAddress: true });
}

dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> {
dataBreakpointInfo(name: string, variablesReference?: number): Promise<IDataBreakpointInfoResponse | undefined> {
return this._dataBreakpointInfo({ name, variablesReference });
}

private async _dataBreakpointInfo(args: DebugProtocol.DataBreakpointInfoArguments): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> {
private async _dataBreakpointInfo(args: DebugProtocol.DataBreakpointInfoArguments): Promise<IDataBreakpointInfoResponse | undefined> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'data breakpoints info'));
}
Expand Down
Loading

0 comments on commit ede5a2a

Please sign in to comment.