From 4da450ba4a1750e92f3b377b7eb61ea10ac36a5a Mon Sep 17 00:00:00 2001 From: Jonah Graham Date: Thu, 12 Oct 2023 14:39:16 -0400 Subject: [PATCH] Add support for setting the cwd of the gdb process For some use cases the executable being debugged expects to find its sources relative to the current working directory ($cwd) instead of the compilation directory ($cdir). This can happen in a couple of circumstances: 1. The source and executable have been moved from the location it was compiled from. 2. The compiler did not save the compilation directory for the given file. The compilation directory can be examined with "info source" in the GDB debugger console. --- src/GDBBackend.ts | 31 ++++- src/GDBDebugSession.ts | 14 ++- src/GDBTargetDebugSession.ts | 8 +- src/integration-tests/gdbCwd.spec.ts | 125 +++++++++++++++++++ src/integration-tests/test-programs/Makefile | 17 ++- src/integration-tests/test-programs/cwd.c | 4 + src/util.ts | 25 +++- 7 files changed, 210 insertions(+), 14 deletions(-) create mode 100644 src/integration-tests/gdbCwd.spec.ts create mode 100644 src/integration-tests/test-programs/cwd.c diff --git a/src/GDBBackend.ts b/src/GDBBackend.ts index b1d3e554..fedc2352 100644 --- a/src/GDBBackend.ts +++ b/src/GDBBackend.ts @@ -19,7 +19,12 @@ import * as mi from './mi'; import { MIResponse } from './mi'; import { MIParser } from './MIParser'; import { VarManager } from './varManager'; -import { compareVersions, getGdbVersion, createEnvValues } from './util'; +import { + compareVersions, + getGdbVersion, + createEnvValues, + getGdbCwd, +} from './util'; export interface MIExecNextRequest { reverse?: boolean; @@ -73,7 +78,11 @@ export class GDBBackend extends events.EventEmitter { requestArgs: LaunchRequestArguments | AttachRequestArguments ) { const gdbPath = requestArgs.gdb || 'gdb'; - this.gdbVersion = await getGdbVersion(gdbPath, requestArgs.environment); + this.gdbVersion = await getGdbVersion( + gdbPath, + getGdbCwd(requestArgs), + requestArgs.environment + ); let args = ['--interpreter=mi2']; if (requestArgs.gdbArguments) { args = args.concat(requestArgs.gdbArguments); @@ -81,7 +90,10 @@ export class GDBBackend extends events.EventEmitter { const gdbEnvironment = requestArgs.environment ? createEnvValues(process.env, requestArgs.environment) : process.env; - this.proc = spawn(gdbPath, args, { env: gdbEnvironment }); + this.proc = spawn(gdbPath, args, { + cwd: getGdbCwd(requestArgs), + env: gdbEnvironment, + }); if (this.proc.stdin == null || this.proc.stdout == null) { throw new Error('Spawned GDB does not have stdout or stdin'); } @@ -103,7 +115,11 @@ export class GDBBackend extends events.EventEmitter { cb: (args: string[]) => Promise ) { const gdbPath = requestArgs.gdb || 'gdb'; - this.gdbVersion = await getGdbVersion(gdbPath, requestArgs.environment); + this.gdbVersion = await getGdbVersion( + gdbPath, + getGdbCwd(requestArgs), + requestArgs.environment + ); // Use dynamic import to remove need for natively building this adapter // Useful when 'spawnInClientTerminal' isn't needed, but adapter is distributed on multiple OS's const { Pty } = await import('./native/pty'); @@ -201,9 +217,14 @@ export class GDBBackend extends events.EventEmitter { public async supportsNewUi( gdbPath?: string, + gdbCwd?: string, environment?: Record ): Promise { - this.gdbVersion = await getGdbVersion(gdbPath || 'gdb', environment); + this.gdbVersion = await getGdbVersion( + gdbPath || 'gdb', + gdbCwd, + environment + ); return this.gdbVersionAtLeast('7.12'); } diff --git a/src/GDBDebugSession.ts b/src/GDBDebugSession.ts index 5722b74c..081a2660 100644 --- a/src/GDBDebugSession.ts +++ b/src/GDBDebugSession.ts @@ -34,16 +34,18 @@ import { } from './mi/data'; import { StoppedEvent } from './stoppedEvent'; import { VarObjType } from './varManager'; -import { createEnvValues } from './util'; +import { createEnvValues, getGdbCwd } from './util'; export interface RequestArguments extends DebugProtocol.LaunchRequestArguments { gdb?: string; gdbArguments?: string[]; gdbAsync?: boolean; gdbNonStop?: boolean; + // defaults to the environment of the process of the adapter environment?: Record; program: string; - cwd?: string; // TODO not implemented + // defaults to dirname of the program, if present or the cwd of the process of the adapter + cwd?: string; verbose?: boolean; logFile?: string; openGdbConsole?: boolean; @@ -442,7 +444,11 @@ export class GDBDebugSession extends LoggingDebugSession { 'cdt-gdb-adapter: openGdbConsole is not supported on this platform' ); } else if ( - !(await this.gdb.supportsNewUi(args.gdb, args.environment)) + !(await this.gdb.supportsNewUi( + args.gdb, + getGdbCwd(args), + args.environment + )) ) { logger.warn( `cdt-gdb-adapter: new-ui command not detected (${ @@ -478,7 +484,7 @@ export class GDBDebugSession extends LoggingDebugSession { 'runInTerminal', { kind: 'integrated', - cwd: process.cwd(), + cwd: getGdbCwd(requestArgs), env: gdbEnvironment, args: command, } as DebugProtocol.RunInTerminalRequestArguments, diff --git a/src/GDBTargetDebugSession.ts b/src/GDBTargetDebugSession.ts index 09622277..2e9829f0 100644 --- a/src/GDBTargetDebugSession.ts +++ b/src/GDBTargetDebugSession.ts @@ -21,7 +21,7 @@ import { DebugProtocol } from '@vscode/debugprotocol'; import { spawn, ChildProcess } from 'child_process'; import { SerialPort, ReadlineParser } from 'serialport'; import { Socket } from 'net'; -import { createEnvValues } from './util'; +import { createEnvValues, getGdbCwd } from './util'; interface UARTArguments { // Path to the serial port connected to the UART on the board. @@ -63,6 +63,7 @@ export interface TargetLaunchArguments extends TargetAttachArguments { // defaults to 'gdbserver --once :0 ${args.program}' (requires gdbserver >= 7.3) server?: string; serverParameters?: string[]; + // Specifies the working directory of gdbserver, defaults to environment in RequestArguments environment?: Record; // Regular expression to extract port from by examinging stdout/err of server. // Once server is launched, port will be set to this if port is not set. @@ -74,7 +75,7 @@ export interface TargetLaunchArguments extends TargetAttachArguments { serverStartupDelay?: number; // Automatically kill the launched server when client issues a disconnect (default: true) automaticallyKillServer?: boolean; - // Specifies the working directory of gdbserver + // Specifies the working directory of gdbserver, defaults to cwd in RequestArguments cwd?: string; } @@ -209,7 +210,8 @@ export class GDBTargetDebugSession extends GDBDebugSession { const target = args.target; const serverExe = target.server !== undefined ? target.server : 'gdbserver'; - const serverCwd = target.cwd !== undefined ? target.cwd : args.cwd; + const serverCwd = + target.cwd !== undefined ? target.cwd : getGdbCwd(args); const serverParams = target.serverParameters !== undefined ? target.serverParameters diff --git a/src/integration-tests/gdbCwd.spec.ts b/src/integration-tests/gdbCwd.spec.ts new file mode 100644 index 00000000..5cb63b74 --- /dev/null +++ b/src/integration-tests/gdbCwd.spec.ts @@ -0,0 +1,125 @@ +/********************************************************************* + * Copyright (c) 2023 Kichwa Coders Canada Inc. 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 * as path from 'path'; +import { CdtDebugClient } from './debugClient'; +import { + fillDefaults, + resolveLineTagLocations, + standardBeforeEach, + testProgramsDir, +} from './utils'; +import { expect } from 'chai'; + +/** + * To test that cwd is set properly we remove the compilation directory from the executable, + * see the makefile for that part, and then launch with a variety of executable/cwd locations + * to make sure that we can insert breakpoints when we expect to, and cannot insert breakpoints + * when we force gdb not to be able to find the source + */ +describe.only('gdb cwd', function () { + let dc: CdtDebugClient; + const program = path.join(testProgramsDir, 'cwd.exe'); + const programRelocated = path.join(testProgramsDir, 'Debug', 'cwd.exe'); + const src = path.join(testProgramsDir, 'cwd.c'); + const lineTags = { + 'STOP HERE': 0, + }; + + before(function () { + resolveLineTagLocations(src, lineTags); + }); + + beforeEach(async function () { + dc = await standardBeforeEach(); + }); + + afterEach(async function () { + await dc.stop(); + }); + + it('default cwd finds source in program directory', async function () { + await dc.launchRequest( + fillDefaults(this.test, { + program: program, + }) + ); + + const bps = await dc.setBreakpointsRequest({ + lines: [lineTags['STOP HERE']], + breakpoints: [{ line: lineTags['STOP HERE'], column: 1 }], + source: { path: src }, + }); + expect(bps.body.breakpoints[0].verified).to.eq(true); + }); + + it('explicit cwd finds source in program directory', async function () { + await dc.launchRequest( + fillDefaults(this.test, { + program: program, + cwd: testProgramsDir, + }) + ); + + const bps = await dc.setBreakpointsRequest({ + lines: [lineTags['STOP HERE']], + breakpoints: [{ line: lineTags['STOP HERE'], column: 1 }], + source: { path: src }, + }); + expect(bps.body.breakpoints[0].verified).to.eq(true); + }); + + it('default cwd does not find source with relocated program', async function () { + await dc.launchRequest( + fillDefaults(this.test, { + program: programRelocated, + }) + ); + + const bps = await dc.setBreakpointsRequest({ + lines: [lineTags['STOP HERE']], + breakpoints: [{ line: lineTags['STOP HERE'], column: 1 }], + source: { path: src }, + }); + expect(bps.body.breakpoints[0].verified).to.eq(false); + }); + + it('explicitly incorrect cwd does not finds source with relocated program', async function () { + await dc.launchRequest( + fillDefaults(this.test, { + program: programRelocated, + cwd: path.join(testProgramsDir, 'EmptyDir'), + }) + ); + + const bps = await dc.setBreakpointsRequest({ + lines: [lineTags['STOP HERE']], + breakpoints: [{ line: lineTags['STOP HERE'], column: 1 }], + source: { path: src }, + }); + expect(bps.body.breakpoints[0].verified).to.eq(false); + }); + + it('explicitly correct cwd does find source with relocated program', async function () { + await dc.launchRequest( + fillDefaults(this.test, { + program: programRelocated, + cwd: testProgramsDir, + }) + ); + + const bps = await dc.setBreakpointsRequest({ + lines: [lineTags['STOP HERE']], + breakpoints: [{ line: lineTags['STOP HERE'], column: 1 }], + source: { path: src }, + }); + expect(bps.body.breakpoints[0].verified).to.eq(true); + }); +}); diff --git a/src/integration-tests/test-programs/Makefile b/src/integration-tests/test-programs/Makefile index a3045ac9..605fd071 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 vars_env mem segv count disassemble functions loopforever MultiThread MultiThreadRunControl stderr bug275-测试 +BINS = empty empty\ space evaluate vars vars_cpp vars_env mem segv count disassemble functions loopforever MultiThread MultiThreadRunControl stderr bug275-测试 cwd.exe .PHONY: all all: $(BINS) @@ -26,6 +26,21 @@ count: count.o count_other.o count\ space.o count\ space.o: count\ space.c $(CC) -c "count space.c" -g3 -O0 +# the cwd tests need to move around source and binary +# in ways that mean gdb cannot find the source automatically +# in the normal $cdir:$cwd and needs additional information +# to be provided +# debug-prefix-map used like this is to put an "incorrect" +# DW_AT_comp_dir in the debug info +cwd.o: cwd.c + $(CC) -fdebug-prefix-map=$(PWD)=. -c $< -g3 -O0 + +cwd.exe: cwd.o + $(LINK) -fdebug-prefix-map=$(PWD)=. + mkdir -p Debug + cp cwd.exe Debug + mkdir -p EmptyDir + empty: empty.o $(LINK) diff --git a/src/integration-tests/test-programs/cwd.c b/src/integration-tests/test-programs/cwd.c new file mode 100644 index 00000000..85cd944e --- /dev/null +++ b/src/integration-tests/test-programs/cwd.c @@ -0,0 +1,4 @@ +int main(int argc, char *argv[]) +{ + return 0; // STOP HERE +} diff --git a/src/util.ts b/src/util.ts index 055f04d8..53bd9c34 100644 --- a/src/util.ts +++ b/src/util.ts @@ -10,6 +10,8 @@ import { execFile } from 'child_process'; import { platform } from 'os'; import { promisify } from 'util'; +import { dirname } from 'path'; + /** * This method actually launches 'gdb --version' to determine the version of * the GDB that is being used. @@ -19,6 +21,7 @@ import { promisify } from 'util'; */ export async function getGdbVersion( gdbPath: string, + gdbCwd?: string, environment?: Record ): Promise { const gdbEnvironment = environment @@ -27,7 +30,7 @@ export async function getGdbVersion( const { stdout, stderr } = await promisify(execFile)( gdbPath, ['--version'], - { env: gdbEnvironment } + { cwd: gdbCwd, env: gdbEnvironment } ); const gdbVersion = parseGdbVersionOutput(stdout); @@ -154,3 +157,23 @@ export function createEnvValues( } return result; } + +/** + * Calculate the CWD that should be used to launch gdb based on the program + * being debugged or the explicitly set cwd in the launch arguments. + * + * Note that launchArgs.program is optional here in preparation for + * debugging where no program is specified. See #262 + * + * @param launchArgs Launch Arguments to compute GDB cwd from + * @returns effective cwd to use + */ +export function getGdbCwd(launchArgs: { + program?: string; + cwd?: string; +}): string { + return ( + launchArgs.cwd || + (launchArgs.program ? dirname(launchArgs.program) : process.cwd()) + ); +}