Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for function breakpoints #136

Merged
merged 1 commit into from
Nov 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
186 changes: 144 additions & 42 deletions src/GDBDebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export class GDBDebugSession extends LoggingDebugSession {

protected frameHandles = new Handles<FrameReference>();
protected variableHandles = new Handles<VariableReference>();
protected functionBreakpoints: string[] = [];
protected logPointMessages: { [ key: string ]: string } = {};

protected threads: Thread[] = [];
Expand Down Expand Up @@ -139,6 +140,7 @@ export class GDBDebugSession extends LoggingDebugSession {
response.body.supportsConditionalBreakpoints = true;
response.body.supportsHitConditionalBreakpoints = true;
response.body.supportsLogPoints = true;
response.body.supportsFunctionBreakpoints = true;
// response.body.supportsSetExpression = true;
response.body.supportsDisassembleRequest = true;
this.sendResponse(response);
Expand Down Expand Up @@ -250,56 +252,65 @@ export class GDBDebugSession extends LoggingDebugSession {
await waitPromise;
}

// Reset logPoint messages
this.logPointMessages = {};

try {
// Need to get the list of current breakpoints in the file and then make sure
// that we end up with the requested set of breakpoints for that file
// deleting ones not requested and inserting new ones.

const result = await mi.sendBreakList(this.gdb);
const gdbbps = result.BreakpointTable.body.filter((gdbbp) => {
// Ignore function breakpoints
return this.functionBreakpoints.indexOf(gdbbp.number) === -1;
});

const file = args.source.path as string;
const breakpoints = args.breakpoints || [];
const { inserts, existing, deletes } = this.resolveBreakpoints(args.breakpoints || [], gdbbps,
(vsbp, gdbbp) => {

let inserts = breakpoints.slice();
const deletes = new Array<string>();
// Always invalidate hit conditions as they have a one-way mapping to gdb ignore and temporary
if (vsbp.hitCondition) {
return false;
}

// Ensure we can compare undefined and empty strings
const vsbpCond = vsbp.condition || undefined;
const gdbbpCond = gdbbp.cond || undefined;

// TODO probably need more thorough checks than just line number
return !!(gdbbp.fullname === file
&& gdbbp.line && parseInt(gdbbp.line, 10) === vsbp.line
&& vsbpCond === gdbbpCond);
});

// Delete before insert to avoid breakpoint clashes in gdb
if (deletes.length > 0) {
await mi.sendBreakDelete(this.gdb, { breakpoints: deletes });
deletes.forEach((breakpoint) => delete this.logPointMessages[breakpoint]);
}

// Reset logPoints
this.logPointMessages = {};

// Set up logpoint messages and return a formatted breakpoint for the response body
const createState = (vsbp: DebugProtocol.SourceBreakpoint, gdbbp: mi.MIBreakpointInfo)
: DebugProtocol.Breakpoint => {

if (vsbp.logMessage) {
this.logPointMessages[gdbbp.number] = vsbp.logMessage;
}

const actual = new Array<DebugProtocol.Breakpoint>();
const createActual = (breakpoint: mi.MIBreakpointInfo) => {
return {
id: parseInt(breakpoint.number, 10),
line: breakpoint.line ? parseInt(breakpoint.line, 10) : 0,
id: parseInt(gdbbp.number, 10),
line: vsbp.line || 0,
verified: true,
};
};

const result = await mi.sendBreakList(this.gdb);
result.BreakpointTable.body.forEach((gdbbp) => {
if (gdbbp.fullname === file && gdbbp.line) {
// TODO probably need more thorough checks than just line number
const line = parseInt(gdbbp.line, 10);
const breakpoint = breakpoints.find((vsbp) => vsbp.line === line);
if (!breakpoint) {
deletes.push(gdbbp.number);
}

inserts = inserts.filter((vsbp) => {
if (vsbp.line !== line) {
return true;
}
// Ensure we can compare undefined and empty strings
const insertCond = vsbp.condition || undefined;
const tableCond = gdbbp.cond || undefined;
if (insertCond !== tableCond) {
return true;
}
actual.push(createActual(gdbbp));
const actual = existing.map((bp) => createState(bp.vsbp, bp.gdbbp));

if (breakpoint && breakpoint.logMessage) {
this.logPointMessages[gdbbp.number] = breakpoint.logMessage;
}

return false;
});
existing.forEach((bp) => {
if (bp.vsbp.logMessage) {
this.logPointMessages[bp.gdbbp.number] = bp.vsbp.logMessage;
}
});

Expand Down Expand Up @@ -328,20 +339,83 @@ export class GDBDebugSession extends LoggingDebugSession {
temporary,
ignoreCount,
});
actual.push(createActual(gdbbp.bkpt));
if (vsbp.logMessage) {
this.logPointMessages[gdbbp.bkpt.number] = vsbp.logMessage;
}

actual.push(createState(vsbp, gdbbp.bkpt));
}

response.body = {
breakpoints: actual,
};

this.sendResponse(response);
} catch (err) {
this.sendErrorResponse(response, 1, err.message);
}

if (neededPause) {
mi.sendExecContinue(this.gdb);
}
}

protected async setFunctionBreakPointsRequest(response: DebugProtocol.SetFunctionBreakpointsResponse,
args: DebugProtocol.SetFunctionBreakpointsArguments) {

const neededPause = this.isRunning;
if (neededPause) {
// Need to pause first
const waitPromise = new Promise<void>((resolve) => {
this.waitPaused = resolve;
});
this.gdb.pause();
await waitPromise;
}

try {
const result = await mi.sendBreakList(this.gdb);
const gdbbps = result.BreakpointTable.body.filter((gdbbp) => {
// Only function breakpoints
return this.functionBreakpoints.indexOf(gdbbp.number) > -1;
});

const { inserts, existing, deletes } = this.resolveBreakpoints(args.breakpoints, gdbbps,
(vsbp, gdbbp) => {

// Always invalidate hit conditions as they have a one-way mapping to gdb ignore and temporary
if (vsbp.hitCondition) {
Copy link
Contributor

Choose a reason for hiding this comment

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

hitCondition and condition - VSCode does not yet have UI for this so it seems acceptable to not implement support for now, but we are at some point going to need our own fork of the debug protocol spec to document our differences and extensions and this is one of them.

return false;
}

// Ensure we can compare undefined and empty strings
const vsbpCond = vsbp.condition || undefined;
const gdbbpCond = gdbbp.cond || undefined;

return !!(gdbbp['original-location'] === `-function ${vsbp.name}`
&& vsbpCond === gdbbpCond);
});

// Delete before insert to avoid breakpoint clashes in gdb
if (deletes.length > 0) {
await mi.sendBreakDelete(this.gdb, { breakpoints: deletes });
this.functionBreakpoints = this.functionBreakpoints.filter((fnbp) => deletes.indexOf(fnbp) === -1);
}

const createActual = (breakpoint: mi.MIBreakpointInfo): DebugProtocol.Breakpoint => ({
id: parseInt(breakpoint.number, 10),
verified: true,
});

const actual = existing.map((bp) => createActual(bp.gdbbp));

for (const vsbp of inserts) {
const gdbbp = await mi.sendBreakFunctionInsert(this.gdb, vsbp.name);
this.functionBreakpoints.push(gdbbp.bkpt.number);
actual.push(createActual(gdbbp.bkpt));
}

response.body = {
breakpoints: actual,
};

this.sendResponse(response);
} catch (err) {
this.sendErrorResponse(response, 1, err.message);
Expand All @@ -352,6 +426,32 @@ export class GDBDebugSession extends LoggingDebugSession {
}
}

protected resolveBreakpoints<T>(vsbps: T[], gdbbps: mi.MIBreakpointInfo[],
matchFn: (vsbp: T, gdbbp: mi.MIBreakpointInfo) => boolean)
: { inserts: T[]; existing: Array<{vsbp: T, gdbbp: mi.MIBreakpointInfo}>; deletes: string[]; } {

const inserts = vsbps.filter((vsbp) => {
return !gdbbps.find((gdbbp) => matchFn(vsbp, gdbbp));
});

const existing: Array<{vsbp: T, gdbbp: mi.MIBreakpointInfo}> = [];
vsbps.forEach((vsbp) => {
const match = gdbbps.find((gdbbp) => matchFn(vsbp, gdbbp));
if (match) {
existing.push({
vsbp,
gdbbp: match,
});
}
});

const deletes = gdbbps.filter((gdbbp) => {
return !vsbps.find((vsbp) => matchFn(vsbp, gdbbp));
}).map((gdbbp) => gdbbp.number);

return { inserts, existing, deletes };
}

protected async configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse,
args: DebugProtocol.ConfigurationDoneArguments): Promise<void> {
try {
Expand Down Expand Up @@ -816,7 +916,9 @@ export class GDBDebugSession extends LoggingDebugSession {
this.sendEvent(new OutputEvent(this.logPointMessages[result.bkptno]));
mi.sendExecContinue(this.gdb);
} else {
this.sendStoppedEvent('breakpoint', getThreadId(result), getAllThreadsStopped(result));
const reason = (this.functionBreakpoints.indexOf(result.bkptno) > -1)
? 'function breakpoint' : 'breakpoint';
this.sendStoppedEvent(reason, getThreadId(result), getAllThreadsStopped(result));
}
break;
case 'end-stepping-range':
Expand Down
72 changes: 72 additions & 0 deletions src/integration-tests/breakpoints.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*********************************************************************/

import * as path from 'path';
import { expect } from 'chai';
import { LaunchRequestArguments } from '../GDBDebugSession';
import { CdtDebugClient } from './debugClient';
import {
Expand Down Expand Up @@ -120,4 +121,75 @@ describe('breakpoints', async () => {
const vars = await dc.variablesRequest({ variablesReference: vr });
verifyVariable(vars.body.variables[0], 'count', 'int', '4');
});

it('resolves breakpoints', async () => {
let response = await dc.setBreakpointsRequest({
source: {
name: 'count.c',
path: path.join(testProgramsDir, 'count.c'),
},
breakpoints: [
{
column: 1,
line: 2,
},
],
});
expect(response.body.breakpoints.length).to.eq(1);

await dc.configurationDoneRequest();
await dc.waitForEvent('stopped');

response = await dc.setBreakpointsRequest({
source: {
name: 'count.c',
path: path.join(testProgramsDir, 'count.c'),
},
breakpoints: [
{
column: 1,
line: 2,
},
{
column: 1,
line: 3,
},
],
});
expect(response.body.breakpoints.length).to.eq(2);

response = await dc.setBreakpointsRequest({
source: {
name: 'count.c',
path: path.join(testProgramsDir, 'count.c'),
},
breakpoints: [
{
column: 1,
line: 2,
condition: 'count == 5',
},
{
column: 1,
line: 3,
},
],
});
expect(response.body.breakpoints.length).to.eq(2);

response = await dc.setBreakpointsRequest({
source: {
name: 'count.c',
path: path.join(testProgramsDir, 'count.c'),
},
breakpoints: [
{
column: 1,
line: 2,
condition: 'count == 3',
},
],
});
expect(response.body.breakpoints.length).to.eq(1);
});
});
4 changes: 2 additions & 2 deletions src/integration-tests/evaluate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('evaluate request', function() {
const res = await dc.evaluateRequest({
context: 'repl',
expression: '2 + 2',
frameId: scope.frameId,
frameId: scope.frame.id,
});

expect(res.body.result).eq('4');
Expand All @@ -76,7 +76,7 @@ describe('evaluate request', function() {
const err = await expectRejection(dc.evaluateRequest({
context: 'repl',
expression: '2 +',
frameId: scope.frameId,
frameId: scope.frame.id,
}));

expect(err.message).eq('-var-create: unable to create variable object');
Expand Down