diff --git a/src/GDBDebugSession.ts b/src/GDBDebugSession.ts index 9a581788..3a60c2b0 100644 --- a/src/GDBDebugSession.ts +++ b/src/GDBDebugSession.ts @@ -103,6 +103,7 @@ export class GDBDebugSession extends LoggingDebugSession { protected frameHandles = new Handles(); protected variableHandles = new Handles(); + protected functionBreakpoints: string[] = []; protected logPointMessages: { [ key: string ]: string } = {}; protected threads: Thread[] = []; @@ -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); @@ -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(); + // 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(); - 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; } }); @@ -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((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) { + 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); @@ -352,6 +426,32 @@ export class GDBDebugSession extends LoggingDebugSession { } } + protected resolveBreakpoints(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 { try { @@ -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': diff --git a/src/integration-tests/breakpoints.spec.ts b/src/integration-tests/breakpoints.spec.ts index d461fad8..033a9757 100644 --- a/src/integration-tests/breakpoints.spec.ts +++ b/src/integration-tests/breakpoints.spec.ts @@ -9,6 +9,7 @@ *********************************************************************/ import * as path from 'path'; +import { expect } from 'chai'; import { LaunchRequestArguments } from '../GDBDebugSession'; import { CdtDebugClient } from './debugClient'; import { @@ -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); + }); }); diff --git a/src/integration-tests/evaluate.spec.ts b/src/integration-tests/evaluate.spec.ts index e35c2ac1..733f0666 100644 --- a/src/integration-tests/evaluate.spec.ts +++ b/src/integration-tests/evaluate.spec.ts @@ -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'); @@ -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'); diff --git a/src/integration-tests/functionBreakpoints.spec.ts b/src/integration-tests/functionBreakpoints.spec.ts new file mode 100644 index 00000000..b76d78d8 --- /dev/null +++ b/src/integration-tests/functionBreakpoints.spec.ts @@ -0,0 +1,70 @@ +/********************************************************************* + * Copyright (c) 2019 Arm and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ + +import { join } from 'path'; +import { expect } from 'chai'; +import { CdtDebugClient } from './debugClient'; +import { LaunchRequestArguments } from '../GDBDebugSession'; +import { + standardBeforeEach, + gdbPath, + testProgramsDir, + openGdbConsole, + getScopes, +} from './utils'; +import { StoppedEvent } from 'vscode-debugadapter'; + +describe('function breakpoints', async () => { + let dc: CdtDebugClient; + + beforeEach(async () => { + dc = await standardBeforeEach(); + + await dc.launchRequest({ + verbose: true, + gdb: gdbPath, + program: join(testProgramsDir, 'functions'), + openGdbConsole, + } as LaunchRequestArguments); + }); + + afterEach(async () => { + await dc.stop(); + }); + + it('hits the main function breakpoint', async () => { + await dc.setFunctionBreakpointsRequest({ + breakpoints: [ + { + name: 'main', + }, + ], + }); + await dc.configurationDoneRequest(); + dc.waitForEvent('stopped'); + const scope = await getScopes(dc); + expect(scope.frame.line).to.eq(6); + }); + + it('hits the sub function breakpoint', async () => { + await dc.setFunctionBreakpointsRequest({ + breakpoints: [ + { + name: 'sub', + }, + ], + }); + await dc.configurationDoneRequest(); + const event = await dc.waitForEvent('stopped') as StoppedEvent; + expect(event.body.reason).to.eq('function breakpoint'); + const scope = await getScopes(dc); + expect(scope.frame.line).to.eq(2); + }); +}); diff --git a/src/integration-tests/logpoints.spec.ts b/src/integration-tests/logpoints.spec.ts index ac9f2893..1aa5c06a 100644 --- a/src/integration-tests/logpoints.spec.ts +++ b/src/integration-tests/logpoints.spec.ts @@ -16,7 +16,7 @@ import { standardBeforeEach, gdbPath, testProgramsDir, - openGdbConsole + openGdbConsole, } from './utils'; describe('logpoints', async () => { @@ -54,7 +54,41 @@ describe('logpoints', async () => { ], }); await dc.configurationDoneRequest(); - let logEvent = await dc.waitForOutputEvent('console'); + const logEvent = await dc.waitForOutputEvent('console'); + expect(logEvent.body.output).to.eq(logMessage); + }); + + it('supports changing log messages', async () => { + const logMessage = 'log message'; + + await dc.setBreakpointsRequest({ + source: { + name: 'count.c', + path: join(testProgramsDir, 'count.c'), + }, + breakpoints: [ + { + column: 1, + line: 4, + logMessage: 'something uninteresting', + }, + ], + }); + await dc.setBreakpointsRequest({ + source: { + name: 'count.c', + path: join(testProgramsDir, 'count.c'), + }, + breakpoints: [ + { + column: 1, + line: 4, + logMessage, + }, + ], + }); + await dc.configurationDoneRequest(); + const logEvent = await dc.waitForOutputEvent('console'); expect(logEvent.body.output).to.eq(logMessage); }); }); diff --git a/src/integration-tests/test-programs/.gitignore b/src/integration-tests/test-programs/.gitignore index 1f5b7a73..6fce56a7 100644 --- a/src/integration-tests/test-programs/.gitignore +++ b/src/integration-tests/test-programs/.gitignore @@ -1,4 +1,5 @@ count +functions disassemble empty empty space diff --git a/src/integration-tests/test-programs/Makefile b/src/integration-tests/test-programs/Makefile index 202944ad..9d9f6ced 100644 --- a/src/integration-tests/test-programs/Makefile +++ b/src/integration-tests/test-programs/Makefile @@ -1,4 +1,4 @@ -BINS = empty empty\ space evaluate vars vars_cpp mem segv count disassemble +BINS = empty empty\ space evaluate vars vars_cpp mem segv count disassemble functions .PHONY: all all: $(BINS) @@ -8,6 +8,9 @@ CXX = g++ LINK = $(CC) -o $@ $^ LINK_CXX = $(CXX) -o $@ $^ +functions: functions.o + $(LINK) + count: count.o $(LINK) diff --git a/src/integration-tests/test-programs/functions.c b/src/integration-tests/test-programs/functions.c new file mode 100644 index 00000000..41dcccfc --- /dev/null +++ b/src/integration-tests/test-programs/functions.c @@ -0,0 +1,8 @@ +int sub() { + return 0; +} + +int main() { + sub(); + return 0; +} diff --git a/src/integration-tests/utils.ts b/src/integration-tests/utils.ts index 3d71472e..7af3d06f 100644 --- a/src/integration-tests/utils.ts +++ b/src/integration-tests/utils.ts @@ -16,8 +16,8 @@ import { DebugProtocol } from 'vscode-debugprotocol'; import { CdtDebugClient } from './debugClient'; export interface Scope { - threadId: number; - frameId: number; + thread: DebugProtocol.Thread; + frame: DebugProtocol.StackFrame; scopes: DebugProtocol.ScopesResponse; } @@ -29,13 +29,15 @@ export async function getScopes( // threads const threads = await dc.threadsRequest(); expect(threads.body.threads.length, 'There are fewer threads than expected.').to.be.at.least(threadIndex + 1); - const threadId = threads.body.threads[threadIndex].id; + const thread = threads.body.threads[threadIndex]; + const threadId = thread.id; // stack trace const stack = await dc.stackTraceRequest({ threadId }); expect(stack.body.stackFrames.length, 'There are fewer stack frames than expected.').to.be.at.least(stackIndex + 1); - const frameId = stack.body.stackFrames[stackIndex].id; + const frame = stack.body.stackFrames[stackIndex]; + const frameId = frame.id; const scopes = await dc.scopesRequest({ frameId }); - return Promise.resolve({ threadId, frameId, scopes }); + return Promise.resolve({ thread, frame, scopes }); } /** diff --git a/src/integration-tests/var.spec.ts b/src/integration-tests/var.spec.ts index 13558d3a..235d990a 100644 --- a/src/integration-tests/var.spec.ts +++ b/src/integration-tests/var.spec.ts @@ -78,7 +78,7 @@ describe('Variables Test Suite', function() { verifyVariable(vars.body.variables[0], 'a', 'int', '25'); verifyVariable(vars.body.variables[1], 'b', 'int', '10'); // step the program and see that the values were passed to the program and evaluated. - await dc.next({ threadId: scope.threadId }, { path: varsSrc, line: lineTags['STOP HERE'] + 1 }); + await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: lineTags['STOP HERE'] + 1 }); scope = await getScopes(dc); expect(scope.scopes.body.scopes.length, 'Unexpected number of scopes returned').to.equal(1); vr = scope.scopes.body.scopes[0].variablesReference; @@ -89,8 +89,8 @@ describe('Variables Test Suite', function() { it('can read and set struct variables in a program', async function() { // step past the initialization for the structure - await dc.next({ threadId: scope.threadId }, { path: varsSrc, line: lineTags['STOP HERE'] + 1 }); - await dc.next({ threadId: scope.threadId }, { path: varsSrc, line: lineTags['STOP HERE'] + 2 }); + await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: lineTags['STOP HERE'] + 1 }); + await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: lineTags['STOP HERE'] + 2 }); scope = await getScopes(dc); expect(scope.scopes.body.scopes.length, 'Unexpected number of scopes returned').to.equal(1); // assert we can see the struct and its elements @@ -119,7 +119,7 @@ describe('Variables Test Suite', function() { verifyVariable(children.body.variables[0], 'x', 'int', '25'); verifyVariable(children.body.variables[1], 'y', 'int', '10'); // step the program and see that the values were passed to the program and evaluated. - await dc.next({ threadId: scope.threadId }, { path: varsSrc, line: lineTags['STOP HERE'] + 3 }); + await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: lineTags['STOP HERE'] + 3 }); scope = await getScopes(dc); expect(scope.scopes.body.scopes.length, 'Unexpected number of scopes returned').to.equal(1); vr = scope.scopes.body.scopes[0].variablesReference; @@ -130,8 +130,8 @@ describe('Variables Test Suite', function() { it('can read and set nested struct variables in a program', async function() { // step past the initialization for the structure - await dc.next({ threadId: scope.threadId }, { path: varsSrc, line: lineTags['STOP HERE'] + 1 }); - await dc.next({ threadId: scope.threadId }, { path: varsSrc, line: lineTags['STOP HERE'] + 2 }); + await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: lineTags['STOP HERE'] + 1 }); + await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: lineTags['STOP HERE'] + 2 }); scope = await getScopes(dc); expect(scope.scopes.body.scopes.length, 'Unexpected number of scopes returned').to.equal(1); // assert we can see the 'foo' struct and its child 'bar' struct @@ -167,8 +167,8 @@ describe('Variables Test Suite', function() { verifyVariable(subChildren.body.variables[0], 'a', 'int', '25'); verifyVariable(subChildren.body.variables[1], 'b', 'int', '10'); // step the program and see that the values were passed to the program and evaluated. - await dc.next({ threadId: scope.threadId }, { path: varsSrc, line: lineTags['STOP HERE'] + 3 }); - await dc.next({ threadId: scope.threadId }, { path: varsSrc, line: lineTags['STOP HERE'] + 4 }); + await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: lineTags['STOP HERE'] + 3 }); + await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: lineTags['STOP HERE'] + 4 }); scope = await getScopes(dc); expect(scope.scopes.body.scopes.length, 'Unexpected number of scopes returned').to.equal(1); vr = scope.scopes.body.scopes[0].variablesReference; @@ -181,7 +181,7 @@ describe('Variables Test Suite', function() { // skip ahead to array initialization const br = await dc.setBreakpointsRequest({ source: { path: varsSrc }, breakpoints: [{ line: 24 }] }); expect(br.success).to.equal(true); - await dc.continueRequest({ threadId: scope.threadId }); + await dc.continueRequest({ threadId: scope.thread.id }); scope = await getScopes(dc); expect(scope.scopes.body.scopes.length, 'Unexpected number of scopes returned').to.equal(1); // assert we can see the array and its elements @@ -214,7 +214,7 @@ describe('Variables Test Suite', function() { verifyVariable(children.body.variables[1], '[1]', 'int', '22'); verifyVariable(children.body.variables[2], '[2]', 'int', '33'); // step the program and see that the values were passed to the program and evaluated. - await dc.next({ threadId: scope.threadId }, { path: varsSrc, line: 25 }); + await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: 25 }); scope = await getScopes(dc); expect(scope.scopes.body.scopes.length, 'Unexpected number of scopes returned').to.equal(1); vr = scope.scopes.body.scopes[0].variablesReference; diff --git a/src/mi/base.ts b/src/mi/base.ts index 042159b4..a2d373d3 100644 --- a/src/mi/base.ts +++ b/src/mi/base.ts @@ -30,7 +30,7 @@ export interface MIBreakpointInfo { line?: string; threadGroups: string[]; times: string; - originalLocation?: string; + 'original-location'?: string; cond?: string; // TODO there are a lot more fields here } diff --git a/src/mi/breakpoint.ts b/src/mi/breakpoint.ts index dd6fd248..c25d08ae 100644 --- a/src/mi/breakpoint.ts +++ b/src/mi/breakpoint.ts @@ -68,3 +68,8 @@ export function sendBreakDelete(gdb: GDBBackend, request: { export function sendBreakList(gdb: GDBBackend): Promise { return gdb.sendCommand('-break-list'); } + +export function sendBreakFunctionInsert(gdb: GDBBackend, fn: string): Promise { + const command = `-break-insert --function ${fn}`; + return gdb.sendCommand(command); +}