Skip to content

Commit 1717482

Browse files
committed
[release] src/goDebugConfiguration: massage launch config for debug/test
When the current build directory is resolved to a path different from the program's directory due to soft/symbolic links, the go command can be confused and complain that the absolute path in program is outside the module of the current directory. This CL avoids such problem by making dlv always use a relative path as the build target for build/test mode (and auto for testing). Before starting the debug session, we massage the launch config: program: /foo/bar.go -> program: ./bar.go, __buildDir: /foo program: /foo/bar -> program: ., __buildDir: /foo/bar Previously we find the package directory just before spawning dlv dap and spawn the dlv dap process from the package directory. With this CL, we find the package directory when resolving the debug configuration before debug session. This introduces __buildDir attribute which is internal. (There is an ongoing work to introduce 'buildDir' to dlv DAP so we use internal attribute to make it clear.) Also, this made the resolveDebugConfigurationWithSubstitutedVariables accept relative paths without workspace root. Just the behavior is undefined. (The motivation of change in this part is the testing. We have 'output' property or some others, that are like relative path. I guess most users wouldn't care much. Delve is cool with relative paths so we do our best to resolve that wrt the workspace root. Otherwise, just let delve decide. Fixes #1713 Fixes #1680 Fixes #1677 Updates #1348 Change-Id: I434c43131b27d9c58058450c502e1b30c58ea690 Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/344790 Trust: Hyang-Ah Hana Kim <[email protected]> Trust: Suzy Mueller <[email protected]> Run-TryBot: Hyang-Ah Hana Kim <[email protected]> TryBot-Result: kokoro <[email protected]> Reviewed-by: Suzy Mueller <[email protected]> Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/346095
1 parent ad27422 commit 1717482

File tree

4 files changed

+155
-48
lines changed

4 files changed

+155
-48
lines changed

src/goDebugConfiguration.ts

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import path = require('path');
1111
import vscode = require('vscode');
1212
import { getGoConfig } from './config';
13+
import { parseProgramArgSync } from './goDebugFactory';
1314
import { toolExecutionEnvironment } from './goEnv';
1415
import {
1516
declinedToolInstall,
@@ -243,8 +244,6 @@ export class GoDebugConfigurationProvider implements vscode.DebugConfigurationPr
243244

244245
const dlvToolPath = getBinPath(debugAdapter);
245246
if (!path.isAbsolute(dlvToolPath)) {
246-
const tool = getTool(debugAdapter);
247-
248247
// If user has not already declined to install this tool,
249248
// prompt for it. Otherwise continue and have the lack of
250249
// dlv binary be caught later.
@@ -382,28 +381,49 @@ export class GoDebugConfigurationProvider implements vscode.DebugConfigurationPr
382381
const entriesWithRelativePaths = ['cwd', 'output', 'program'].filter(
383382
(attr) => debugConfiguration[attr] && !path.isAbsolute(debugConfiguration[attr])
384383
);
385-
if (debugConfiguration['debugAdapter'] === 'dlv-dap' && entriesWithRelativePaths.length > 0) {
386-
const workspaceRoot = folder?.uri.fsPath;
387-
if (!workspaceRoot) {
388-
this.showWarning(
389-
'relativePathsWithoutWorkspaceFolder',
390-
'Relative paths without a workspace folder for `cwd`, `program`, or `output` are not allowed.'
391-
);
392-
return null;
384+
if (debugConfiguration['debugAdapter'] === 'dlv-dap') {
385+
// relative paths -> absolute paths
386+
if (entriesWithRelativePaths.length > 0) {
387+
const workspaceRoot = folder?.uri.fsPath;
388+
if (workspaceRoot) {
389+
entriesWithRelativePaths.forEach((attr) => {
390+
debugConfiguration[attr] = path.join(workspaceRoot, debugConfiguration[attr]);
391+
});
392+
} else {
393+
this.showWarning(
394+
'relativePathsWithoutWorkspaceFolder',
395+
'Behavior when using relative paths without a workspace folder for `cwd`, `program`, or `output` is undefined.'
396+
);
397+
}
398+
}
399+
// compute build dir, and translate the dirname in program to a path relative to buildDir.
400+
if (debugConfiguration.request === 'launch') {
401+
const mode = debugConfiguration['mode'] || 'debug';
402+
if (['debug', 'test', 'auto'].includes(mode)) {
403+
// Massage config to build the target from the package directory
404+
// with a relative path. (https://github.com/golang/vscode-go/issues/1713)
405+
try {
406+
const { program, dirname, programIsDirectory } = parseProgramArgSync(debugConfiguration);
407+
if (dirname) {
408+
debugConfiguration['__buildDir'] = dirname;
409+
debugConfiguration['program'] = programIsDirectory
410+
? '.'
411+
: '.' + path.sep + path.relative(dirname, program);
412+
}
413+
} catch (e) {
414+
this.showWarning('invalidProgramArg', `Invalid 'program': ${e}`);
415+
// keep going - just in case dlv knows how to handle this better.
416+
}
417+
}
393418
}
394-
entriesWithRelativePaths.forEach((attr) => {
395-
debugConfiguration[attr] = path.join(workspaceRoot, debugConfiguration[attr]);
396-
});
397419
}
398-
399420
if (debugConfiguration.request === 'attach' && debugConfiguration['mode'] === 'local') {
400421
// processId needs to be an int, but the substituted variables from pickGoProcess and pickProcess
401422
// become a string. Convert any strings to integers.
402423
if (typeof debugConfiguration['processId'] === 'string') {
403424
debugConfiguration['processId'] = parseInt(debugConfiguration['processId'], 10);
404425
}
405426
}
406-
407427
return debugConfiguration;
408428
}
409429

src/goDebugFactory.ts

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export class GoDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescr
3838
configuration: vscode.DebugConfiguration
3939
): Promise<vscode.ProviderResult<vscode.DebugAdapterDescriptor>> {
4040
const logger = new TimestampedLogger(configuration.trace, this.outputChannel);
41+
logger.debug(`Config: ${JSON.stringify(configuration)}`);
4142
const d = new DelveDAPOutputAdapter(configuration, logger);
4243
return new vscode.DebugAdapterInlineImplementation(d);
4344
}
@@ -356,13 +357,9 @@ function spawnDlvDapServerProcess(
356357
throw new Error('Cannot find Delve debugger (dlv dap)');
357358
}
358359
let dir = getWorkspaceFolderPath();
359-
if (launchAttachArgs.request === 'launch') {
360-
try {
361-
dir = parseProgramArgSync(launchAttachArgs).dirname;
362-
} catch (err) {
363-
logErr(`Program arg: ${launchAttachArgs.program}\n${err}\n`);
364-
throw err; // rethrow so the caller knows it failed.
365-
}
360+
if (launchAttachArgs.request === 'launch' && launchAttachArgs['__buildDir']) {
361+
// __buildDir is the directory determined during resolving debug config
362+
dir = launchAttachArgs['__buildDir'];
366363
}
367364

368365
const dlvArgs = new Array<string>();
@@ -408,7 +405,7 @@ function spawnDlvDapServerProcess(
408405

409406
const logDestStream = logDest ? fs.createWriteStream(logDest) : undefined;
410407

411-
logConsole(`Starting: ${dlvPath} ${dlvArgs.join(' ')}\n`);
408+
logConsole(`Starting: ${dlvPath} ${dlvArgs.join(' ')} from ${dir}\n`);
412409

413410
// TODO(hyangah): In module-module workspace mode, the program should be build in the super module directory
414411
// where go.work (gopls.mod) file is present. Where dlv runs determines the build directory currently. Two options:
@@ -491,27 +488,37 @@ function spawnDlvDapServerProcess(
491488
export function parseProgramArgSync(
492489
launchAttachArgs: vscode.DebugConfiguration
493490
): { program: string; dirname: string; programIsDirectory: boolean } {
494-
const program = launchAttachArgs.program;
495-
let programIsDirectory = false;
491+
// attach request:
492+
// irrelevant
493+
if (launchAttachArgs.request !== 'launch') return;
496494

497-
if (launchAttachArgs.mode === 'replay') {
498-
// Skip program parsing on modes that do not require a program
499-
return { program: '', dirname: '', programIsDirectory: programIsDirectory };
500-
}
495+
const mode = launchAttachArgs.mode || 'debug';
496+
const program = launchAttachArgs.program;
501497

502498
if (!program) {
503499
throw new Error('The program attribute is missing in the debug configuration in launch.json');
504500
}
505501

506-
try {
507-
programIsDirectory = fs.lstatSync(program).isDirectory();
508-
} catch (e) {
509-
// TODO(hyangah): why can't the program be a package name?
510-
throw new Error('The program attribute must point to valid directory, .go file or executable.');
511-
}
512-
if (!programIsDirectory && launchAttachArgs.mode !== 'exec' && path.extname(program) !== '.go') {
513-
throw new Error('The program attribute must be a directory or .go file in debug and test mode');
502+
// debug, test, auto mode in launch request:
503+
// program ends with .go file -> file, otherwise -> programIsDirectory.
504+
// exec mode
505+
// program should be executable.
506+
// other modes:
507+
// not relevant
508+
if (['debug', 'test', 'auto'].includes(mode)) {
509+
// `auto` shouldn't happen other than in testing.
510+
const ext = path.extname(program);
511+
if (ext === '') {
512+
// the last path element doesn't have . or the first char is .
513+
// Treat this like a directory.
514+
return { program, dirname: program, programIsDirectory: true };
515+
}
516+
if (ext === '.go') {
517+
return { program, dirname: path.dirname(program), programIsDirectory: false };
518+
} else {
519+
throw new Error('The program attribute must be a directory or .go file in debug and test mode');
520+
}
514521
}
515-
const dirname = programIsDirectory ? program : path.dirname(program);
516-
return { program, dirname, programIsDirectory };
522+
// Otherwise, let delve handle.
523+
return { program, dirname: '', programIsDirectory: false };
517524
}

test/integration/goDebug.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2081,6 +2081,41 @@ const testAll = (ctx: Mocha.Context, isDlvDap: boolean) => {
20812081
await new Promise((resolve) => setTimeout(resolve, 2_000));
20822082
});
20832083
});
2084+
2085+
suite('substitutePath with symlink', () => {
2086+
let realPath: string;
2087+
let symlinkPath: string;
2088+
2089+
suiteSetup(() => {
2090+
realPath = copyDirectory('baseTest');
2091+
symlinkPath = path.join(tmpDir, 'symlinked');
2092+
fs.symlinkSync(realPath, symlinkPath, 'dir');
2093+
});
2094+
suiteTeardown(() => {
2095+
fs.unlinkSync(symlinkPath);
2096+
rmdirRecursive(realPath);
2097+
});
2098+
test('should stop on a breakpoint', async function () {
2099+
if (!isDlvDap) this.skip(); // BUG: the legacy adapter fails with 'breakpoint verification mismatch' error.
2100+
const FILE = path.join(symlinkPath, 'test.go');
2101+
const BREAKPOINT_LINE = 11;
2102+
const config = {
2103+
name: 'Launch',
2104+
type: 'go',
2105+
request: 'launch',
2106+
mode: 'debug',
2107+
program: FILE,
2108+
substitutePath: [
2109+
{
2110+
from: symlinkPath,
2111+
to: realPath
2112+
}
2113+
]
2114+
};
2115+
const debugConfig = await initializeDebugConfig(config);
2116+
await dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE));
2117+
});
2118+
});
20842119
});
20852120

20862121
let testNumber = 0;
@@ -2110,7 +2145,12 @@ const testAll = (ctx: Mocha.Context, isDlvDap: boolean) => {
21102145
}
21112146
testNumber++;
21122147

2113-
const debugConfig = await debugConfigProvider.resolveDebugConfiguration(undefined, config);
2148+
let debugConfig = await debugConfigProvider.resolveDebugConfiguration(undefined, config);
2149+
debugConfig = await debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(
2150+
undefined,
2151+
debugConfig
2152+
);
2153+
21142154
if (isDlvDap) {
21152155
dlvDapAdapter = await DelveDAPDebugAdapterOnSocket.create(debugConfig);
21162156
const port = await dlvDapAdapter.serve();

test/integration/goDebugConfiguration.test.ts

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -452,23 +452,50 @@ suite('Debug Configuration Converts Relative Paths', () => {
452452
};
453453
}
454454

455-
test('resolve relative paths with workspace root in dlv-dap mode', () => {
455+
test('resolve relative paths with workspace root in dlv-dap mode, exec mode does not set __buildDir', () => {
456456
const config = debugConfig('dlv-dap');
457+
config.mode = 'exec';
458+
config.program = path.join('foo', 'bar.exe');
457459
const workspaceFolder = {
458460
uri: vscode.Uri.file(os.tmpdir()),
459461
name: 'test',
460462
index: 0
461463
};
462-
const { program, cwd, output } = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(
464+
const { program, cwd, __buildDir } = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(
463465
workspaceFolder,
464466
config
465467
);
466468
assert.deepStrictEqual(
467-
{ program, cwd, output },
469+
{ program, cwd, __buildDir },
470+
{
471+
program: path.join(os.tmpdir(), 'foo', 'bar.exe'),
472+
cwd: os.tmpdir(),
473+
__buildDir: undefined
474+
}
475+
);
476+
});
477+
478+
test('program and __buildDir are updated while resolving debug configuration in dlv-dap mode', () => {
479+
const config = debugConfig('dlv-dap');
480+
config.program = path.join('foo', 'bar', 'pkg');
481+
const workspaceFolder = {
482+
uri: vscode.Uri.file(os.tmpdir()),
483+
name: 'test',
484+
index: 0
485+
};
486+
const {
487+
program,
488+
cwd,
489+
output,
490+
__buildDir
491+
} = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(workspaceFolder, config);
492+
assert.deepStrictEqual(
493+
{ program, cwd, output, __buildDir },
468494
{
469-
program: path.join(os.tmpdir(), 'foo/bar.go'),
495+
program: '.',
470496
cwd: os.tmpdir(),
471-
output: path.join(os.tmpdir(), 'debug')
497+
output: path.join(os.tmpdir(), 'debug'),
498+
__buildDir: path.join(os.tmpdir(), 'foo', 'bar', 'pkg')
472499
}
473500
);
474501
});
@@ -498,10 +525,23 @@ suite('Debug Configuration Converts Relative Paths', () => {
498525
);
499526
});
500527

501-
test('disallow relative paths with no workspace root', () => {
528+
test('relative paths with no workspace root are not expanded', () => {
502529
const config = debugConfig('dlv-dap');
503-
const got = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, config);
504-
assert.strictEqual(got, null);
530+
const {
531+
program,
532+
cwd,
533+
output,
534+
__buildDir
535+
} = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, config);
536+
assert.deepStrictEqual(
537+
{ program, cwd, output, __buildDir },
538+
{
539+
program: '.' + path.sep + 'bar.go',
540+
cwd: '.',
541+
output: 'debug',
542+
__buildDir: 'foo'
543+
}
544+
);
505545
});
506546

507547
test('do not affect relative paths (workspace) in legacy mode', () => {

0 commit comments

Comments
 (0)