Skip to content

Commit 3d9a114

Browse files
committed
Add logic to infer path
1 parent e8e97e5 commit 3d9a114

File tree

1 file changed

+290
-8
lines changed

1 file changed

+290
-8
lines changed

src/debugAdapter/goDebug.ts

+290-8
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
*--------------------------------------------------------*/
55

66
import { ChildProcess, execFile, execSync, spawn, spawnSync } from 'child_process';
7+
import { EventEmitter } from 'events';
78
import * as fs from 'fs';
89
import { existsSync, lstatSync } from 'fs';
10+
import * as glob from 'glob';
911
import { Client, RPCConnection } from 'json-rpc2';
1012
import * as os from 'os';
1113
import * as path from 'path';
@@ -88,6 +90,20 @@ interface DebuggerState {
8890
Running: boolean;
8991
}
9092

93+
interface PackageBuildInfo {
94+
ImportPath: string;
95+
DirectoryPath: string;
96+
Files: string[];
97+
}
98+
99+
interface ListPackagesBuildInfoOut {
100+
List: PackageBuildInfo[];
101+
}
102+
103+
interface ListSourcesOut {
104+
Sources: string[];
105+
}
106+
91107
interface CreateBreakpointOut {
92108
Breakpoint: DebugBreakpoint;
93109
}
@@ -319,6 +335,10 @@ function normalizePath(filePath: string) {
319335
return filePath;
320336
}
321337

338+
function getBaseName(filePath: string) {
339+
return filePath.includes('/') ? path.basename(filePath) : path.win32.basename(filePath);
340+
}
341+
322342
class Delve {
323343
public program: string;
324344
public remotePath: string;
@@ -729,6 +749,9 @@ class GoDebugSession extends LoggingDebugSession {
729749
private stopOnEntry: boolean;
730750
private logLevel: Logger.LogLevel = Logger.LogLevel.Error;
731751
private readonly initdone = 'initdone·';
752+
private remoteSourcesAndPackages = new RemoteSourcesAndPackages();
753+
private localToRemotePathMapping = new Map<string, string>();
754+
private remoteToLocalPathMapping = new Map<string, string>();
732755

733756
private showGlobalVariables: boolean = false;
734757

@@ -827,19 +850,194 @@ class GoDebugSession extends LoggingDebugSession {
827850
}
828851
}
829852

830-
protected toDebuggerPath(filePath: string): string {
853+
/**
854+
* Given a potential list of paths in potentialPaths array, we will
855+
* find the path that has the longest suffix matching filePath.
856+
* For example, if filePath is /usr/local/foo/bar/main.go
857+
* and potentialPaths are abc/xyz/main.go, bar/main.go
858+
* then bar/main.go will be the result.
859+
*/
860+
protected findPathWithBestMatchingSuffix(filePath: string, potentialPaths: string[]) {
861+
if (!potentialPaths.length) {
862+
return;
863+
}
864+
865+
if (potentialPaths.length === 1) {
866+
return potentialPaths[0];
867+
}
868+
869+
const filePathSegments = filePath.split(/\/|\\/).reverse();
870+
let bestPathSoFar = potentialPaths[0];
871+
let bestSegmentsCount = 0;
872+
for (const potentialPath of potentialPaths) {
873+
const potentialPathSegments = potentialPath.split(/\/|\\/).reverse();
874+
let i = 0;
875+
for (; i < filePathSegments.length
876+
&& i < potentialPathSegments.length
877+
&& filePathSegments[i] === potentialPathSegments[i]; i++) {
878+
if (i > bestSegmentsCount) {
879+
bestSegmentsCount = i;
880+
bestPathSoFar = potentialPath;
881+
}
882+
}
883+
}
884+
return bestPathSoFar;
885+
}
886+
887+
/**
888+
* Given a local path, try to find matching file in the remote machine
889+
* using remote sources and remote packages info that we get from Delve.
890+
* The result would be cached in localToRemotePathMapping.
891+
*/
892+
protected inferRemotePathFromLocalPath(localPath: string): string|undefined {
893+
if (this.localToRemotePathMapping.has(localPath)) {
894+
return this.localToRemotePathMapping.get(localPath);
895+
}
896+
897+
const fileName = getBaseName(localPath);
898+
const potentialMatchingRemoteFiles = this.remoteSourcesAndPackages.remoteSourceFilesNameGrouping.get(fileName);
899+
const bestMatchingRemoteFile = this.findPathWithBestMatchingSuffix(localPath, potentialMatchingRemoteFiles);
900+
if (!bestMatchingRemoteFile) {
901+
return;
902+
}
903+
904+
this.localToRemotePathMapping.set(localPath, bestMatchingRemoteFile);
905+
return bestMatchingRemoteFile;
906+
}
907+
908+
protected async toDebuggerPath(filePath: string): Promise<string> {
831909
if (this.delve.remotePath.length === 0) {
910+
if (this.delve.isRemoteDebugging) {
911+
// The user trusts us to infer the remote path mapping!
912+
try {
913+
await this.initializeRemotePackagesAndSources();
914+
const matchedRemoteFile = this.inferRemotePathFromLocalPath(filePath);
915+
if (matchedRemoteFile) {
916+
return matchedRemoteFile;
917+
}
918+
} catch (error) {
919+
log(`Failing to initialize remote sources: ${error}`);
920+
}
921+
}
832922
return this.convertClientPathToDebugger(filePath);
833923
}
924+
834925
// The filePath may have a different path separator than the localPath
835926
// So, update it to use the same separator as the remote path to ease
836927
// in replacing the local path in it with remote path
837928
filePath = filePath.replace(/\/|\\/g, this.remotePathSeparator);
838929
return filePath.replace(this.delve.program.replace(/\/|\\/g, this.remotePathSeparator), this.delve.remotePath);
839930
}
840931

932+
/**
933+
* Given a remote path, try to infer the matching local path.
934+
* We attempt to find the path in local Go packages as well as workspaceFolder.
935+
* Cache the result in remoteToLocalPathMapping.
936+
*/
937+
protected inferLocalPathFromRemotePath(remotePath: string): string|undefined {
938+
if (this.remoteToLocalPathMapping.has(remotePath)) {
939+
return this.remoteToLocalPathMapping.get(remotePath);
940+
}
941+
942+
const convertedLocalPackageFile = this.inferLocalPathFromRemoteGoPackage(remotePath);
943+
if (convertedLocalPackageFile) {
944+
this.remoteToLocalPathMapping.set(remotePath, convertedLocalPackageFile);
945+
return convertedLocalPackageFile;
946+
}
947+
948+
// If we cannot find the path in packages, most likely it will be in the current directory.
949+
const fileName = getBaseName(remotePath);
950+
const globSync = glob.sync(fileName, {matchBase: true,
951+
cwd: this.delve.program });
952+
const bestMatchingLocalPath = this.findPathWithBestMatchingSuffix(remotePath, globSync);
953+
if (bestMatchingLocalPath) {
954+
const fullLocalPath = path.join(this.delve.program, bestMatchingLocalPath);
955+
this.remoteToLocalPathMapping.set(remotePath, fullLocalPath);
956+
return fullLocalPath;
957+
}
958+
}
959+
960+
/**
961+
* Given a remote path, we attempt to infer the local path by first checking
962+
* if it is in any remote packages. If so, then we attempt to find the matching
963+
* local package and find the local path from there.
964+
*/
965+
protected inferLocalPathFromRemoteGoPackage(remotePath: string): string|undefined {
966+
const remotePackage = this.remoteSourcesAndPackages.remotePackagesBuildInfo.find(
967+
(buildInfo) => remotePath.startsWith(buildInfo.DirectoryPath));
968+
// Since we know pathToConvert exists in a remote package, we can try to find
969+
// that same package in the local client. We can use import path to search for the package.
970+
if (!remotePackage) {
971+
return;
972+
}
973+
974+
if (!this.remotePathSeparator) {
975+
this.remotePathSeparator = findPathSeparator(remotePackage.DirectoryPath);
976+
}
977+
978+
// The remotePackage.DirectoryPath should be something like
979+
// <gopath|goroot|source>/<import-path>/xyz...
980+
// Directory Path can be like "/go/pkg/mod/github.com/google/[email protected]/cmp"
981+
// and Import Path can be like "github.com/google/go-cmp/cmp"
982+
const importPathIndex = remotePackage.DirectoryPath.replace(/@v(\d+\.)*(\d+)*/, '')
983+
.indexOf(remotePackage.ImportPath);
984+
if (importPathIndex < 0) {
985+
return;
986+
}
987+
988+
const localRelativeDirectoryPath = remotePackage.DirectoryPath
989+
.substr(importPathIndex)
990+
.split(this.remotePathSeparator)
991+
.join(this.localPathSeparator);
992+
993+
// Scenario 1: The package is inside the current working directory.
994+
const localWorkspacePath = path.join(this.delve.program, localRelativeDirectoryPath);
995+
if (fs.existsSync(localWorkspacePath)) {
996+
return path.join(this.delve.program,
997+
remotePath
998+
.substr(importPathIndex)
999+
.split(this.remotePathSeparator)
1000+
.join(this.localPathSeparator));
1001+
}
1002+
1003+
// Scenario 2: The package is inside GOPATH.
1004+
const pathToConvertWithLocalSeparator = remotePath.split(this.remotePathSeparator).join(this.localPathSeparator);
1005+
const indexGoModCache = pathToConvertWithLocalSeparator.indexOf(
1006+
`${this.localPathSeparator}pkg${this.localPathSeparator}mod${this.localPathSeparator}`
1007+
);
1008+
const gopath = (process.env['GOPATH'] || '').split(path.delimiter)[0];
1009+
const localGoPathImportPath = path.join(
1010+
gopath,
1011+
pathToConvertWithLocalSeparator.substr(indexGoModCache));
1012+
if (fs.existsSync(localGoPathImportPath)) {
1013+
return localGoPathImportPath;
1014+
}
1015+
1016+
// Scenario 3: The package is inside GOROOT.
1017+
const srcIndex = pathToConvertWithLocalSeparator.indexOf(`${this.localPathSeparator}src${this.localPathSeparator}`);
1018+
const goroot = process.env['GOROOT'];
1019+
const localGoRootImportPath = path.join(
1020+
goroot,
1021+
pathToConvertWithLocalSeparator
1022+
.substr(srcIndex));
1023+
if (fs.existsSync(localGoRootImportPath)) {
1024+
return localGoRootImportPath;
1025+
}
1026+
}
1027+
1028+
/**
1029+
* This functions assumes that remote packages and paths information
1030+
* have been initialized.
1031+
*/
8411032
protected toLocalPath(pathToConvert: string): string {
8421033
if (this.delve.remotePath.length === 0) {
1034+
// User trusts use to infer the path
1035+
if (this.delve.isRemoteDebugging) {
1036+
const inferredPath = this.inferLocalPathFromRemotePath(pathToConvert);
1037+
if (inferredPath) {
1038+
return inferredPath;
1039+
}
1040+
}
8431041
return this.convertDebuggerPathToClient(pathToConvert);
8441042
}
8451043

@@ -968,7 +1166,7 @@ class GoDebugSession extends LoggingDebugSession {
9681166
this.delve.call<DebugLocation[] | StacktraceOut>(
9691167
this.delve.isApiV1 ? 'StacktraceGoroutine' : 'Stacktrace',
9701168
[stackTraceIn],
971-
(err, out) => {
1169+
async (err, out) => {
9721170
if (err) {
9731171
this.logDelveError(err, 'Failed to produce stacktrace');
9741172
return this.sendErrorResponse(response, 2004, 'Unable to produce stack trace: "{e}"', {
@@ -977,6 +1175,9 @@ class GoDebugSession extends LoggingDebugSession {
9771175
}
9781176
const locations = this.delve.isApiV1 ? <DebugLocation[]>out : (<StacktraceOut>out).Locations;
9791177
log('locations', locations);
1178+
1179+
await this.initializeRemotePackagesAndSources();
1180+
9801181
let stackFrames = locations.map((location, frameId) => {
9811182
const uniqueStackFrameId = this.stackFrameHandles.create([goroutineId, frameId]);
9821183
return new StackFrame(
@@ -1400,8 +1601,8 @@ class GoDebugSession extends LoggingDebugSession {
14001601
args.remotePath = '';
14011602
}
14021603

1604+
this.localPathSeparator = findPathSeparator(localPath);
14031605
if (args.remotePath.length > 0) {
1404-
this.localPathSeparator = findPathSeparator(localPath);
14051606
this.remotePathSeparator = findPathSeparator(args.remotePath);
14061607

14071608
const llist = localPath.split(/\/|\\/).reverse();
@@ -1476,15 +1677,38 @@ class GoDebugSession extends LoggingDebugSession {
14761677
);
14771678
}
14781679

1479-
private setBreakPoints(
1680+
/**
1681+
* Initializing remote packages and sources.
1682+
* We use event model to prevent race conditions.
1683+
*/
1684+
private async initializeRemotePackagesAndSources(): Promise<void> {
1685+
if (this.remoteSourcesAndPackages.initializedRemoteSourceFiles) {
1686+
return;
1687+
}
1688+
1689+
if (!this.remoteSourcesAndPackages.initializingRemoteSourceFiles) {
1690+
await this.remoteSourcesAndPackages.initializeRemotePackagesAndSources(this.delve);
1691+
return;
1692+
}
1693+
1694+
if (this.remoteSourcesAndPackages.initializingRemoteSourceFiles) {
1695+
await new Promise((resolve) => {
1696+
this.remoteSourcesAndPackages.on(RemoteSourcesAndPackages.INITIALIZED, () => {
1697+
resolve();
1698+
});
1699+
});
1700+
}
1701+
}
1702+
1703+
private async setBreakPoints(
14801704
response: DebugProtocol.SetBreakpointsResponse,
14811705
args: DebugProtocol.SetBreakpointsArguments
1482-
): Thenable<void> {
1706+
): Promise<void> {
14831707
const file = normalizePath(args.source.path);
14841708
if (!this.breakpoints.get(file)) {
14851709
this.breakpoints.set(file, []);
14861710
}
1487-
const remoteFile = this.toDebuggerPath(file);
1711+
const remoteFile = await this.toDebuggerPath(file);
14881712

14891713
return Promise.all(
14901714
this.breakpoints.get(file).map((existingBP) => {
@@ -1591,12 +1815,13 @@ class GoDebugSession extends LoggingDebugSession {
15911815
);
15921816
}
15931817

1594-
private getPackageInfo(debugState: DebuggerState): Thenable<string> {
1818+
private async getPackageInfo(debugState: DebuggerState): Promise<string> {
15951819
if (!debugState.currentThread || !debugState.currentThread.file) {
15961820
return Promise.resolve(null);
15971821
}
1822+
await this.initializeRemotePackagesAndSources();
15981823
const dir = path.dirname(
1599-
this.delve.remotePath.length
1824+
this.delve.remotePath.length || this.delve.isRemoteDebugging
16001825
? this.toLocalPath(debugState.currentThread.file)
16011826
: debugState.currentThread.file
16021827
);
@@ -1909,6 +2134,63 @@ class GoDebugSession extends LoggingDebugSession {
19092134
}
19102135
}
19112136

2137+
// Class for fetching remote sources and packages
2138+
// in the remote program using Delve.
2139+
// tslint:disable-next-line:max-classes-per-file
2140+
class RemoteSourcesAndPackages extends EventEmitter {
2141+
public static readonly INITIALIZED = 'INITIALIZED';
2142+
2143+
public initializingRemoteSourceFiles = false;
2144+
public initializedRemoteSourceFiles = false;
2145+
2146+
public remotePackagesBuildInfo: PackageBuildInfo[] = [];
2147+
public remoteSourceFiles: string[] = [];
2148+
public remoteSourceFilesNameGrouping = new Map<string, string[]>();
2149+
2150+
/**
2151+
* Initialize and fill out remote packages build info and remote source files.
2152+
* Emits the INITIALIZED event once initialization is complete.
2153+
*/
2154+
public async initializeRemotePackagesAndSources(delve: Delve): Promise<void> {
2155+
this.initializingRemoteSourceFiles = true;
2156+
2157+
try {
2158+
// ListPackagesBuildInfo is not available on V1.
2159+
if (!delve.isApiV1 && this.remotePackagesBuildInfo.length === 0) {
2160+
const packagesBuildInfoResponse: ListPackagesBuildInfoOut = await delve.callPromise(
2161+
'ListPackagesBuildInfo', [{IncludeFiles: true}]
2162+
);
2163+
if (packagesBuildInfoResponse && packagesBuildInfoResponse.List) {
2164+
this.remotePackagesBuildInfo = packagesBuildInfoResponse.List;
2165+
}
2166+
}
2167+
2168+
// List sources will return all the source files used by Delve.
2169+
if (delve.isApiV1) {
2170+
this.remoteSourceFiles = await delve.callPromise('ListSources', []);
2171+
} else {
2172+
const listSourcesResponse: ListSourcesOut = await delve.callPromise('ListSources', [{}]);
2173+
if (listSourcesResponse && listSourcesResponse.Sources) {
2174+
this.remoteSourceFiles = listSourcesResponse.Sources;
2175+
}
2176+
}
2177+
2178+
// Group the source files by name for easy searching later.
2179+
this.remoteSourceFiles = this.remoteSourceFiles.filter((sourceFile) => !sourceFile.startsWith('<'));
2180+
this.remoteSourceFiles.forEach((sourceFile) => {
2181+
const fileName = getBaseName(sourceFile);
2182+
if (!this.remoteSourceFilesNameGrouping.has(fileName)) {
2183+
this.remoteSourceFilesNameGrouping.set(fileName, []);
2184+
}
2185+
this.remoteSourceFilesNameGrouping.get(fileName).push(sourceFile);
2186+
});
2187+
} finally {
2188+
this.emit(RemoteSourcesAndPackages.INITIALIZED);
2189+
this.initializedRemoteSourceFiles = true;
2190+
}
2191+
}
2192+
}
2193+
19122194
function random(low: number, high: number): number {
19132195
return Math.floor(Math.random() * (high - low) + low);
19142196
}

0 commit comments

Comments
 (0)