4
4
*--------------------------------------------------------*/
5
5
6
6
import { ChildProcess , execFile , execSync , spawn , spawnSync } from 'child_process' ;
7
+ import { EventEmitter } from 'events' ;
7
8
import * as fs from 'fs' ;
8
9
import { existsSync , lstatSync } from 'fs' ;
10
+ import * as glob from 'glob' ;
9
11
import { Client , RPCConnection } from 'json-rpc2' ;
10
12
import * as os from 'os' ;
11
13
import * as path from 'path' ;
@@ -88,6 +90,20 @@ interface DebuggerState {
88
90
Running : boolean ;
89
91
}
90
92
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
+
91
107
interface CreateBreakpointOut {
92
108
Breakpoint : DebugBreakpoint ;
93
109
}
@@ -319,6 +335,10 @@ function normalizePath(filePath: string) {
319
335
return filePath ;
320
336
}
321
337
338
+ function getBaseName ( filePath : string ) {
339
+ return filePath . includes ( '/' ) ? path . basename ( filePath ) : path . win32 . basename ( filePath ) ;
340
+ }
341
+
322
342
class Delve {
323
343
public program : string ;
324
344
public remotePath : string ;
@@ -729,6 +749,9 @@ class GoDebugSession extends LoggingDebugSession {
729
749
private stopOnEntry : boolean ;
730
750
private logLevel : Logger . LogLevel = Logger . LogLevel . Error ;
731
751
private readonly initdone = 'initdone·' ;
752
+ private remoteSourcesAndPackages = new RemoteSourcesAndPackages ( ) ;
753
+ private localToRemotePathMapping = new Map < string , string > ( ) ;
754
+ private remoteToLocalPathMapping = new Map < string , string > ( ) ;
732
755
733
756
private showGlobalVariables : boolean = false ;
734
757
@@ -827,19 +850,194 @@ class GoDebugSession extends LoggingDebugSession {
827
850
}
828
851
}
829
852
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 > {
831
909
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
+ }
832
922
return this . convertClientPathToDebugger ( filePath ) ;
833
923
}
924
+
834
925
// The filePath may have a different path separator than the localPath
835
926
// So, update it to use the same separator as the remote path to ease
836
927
// in replacing the local path in it with remote path
837
928
filePath = filePath . replace ( / \/ | \\ / g, this . remotePathSeparator ) ;
838
929
return filePath . replace ( this . delve . program . replace ( / \/ | \\ / g, this . remotePathSeparator ) , this . delve . remotePath ) ;
839
930
}
840
931
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
+ */
841
1032
protected toLocalPath ( pathToConvert : string ) : string {
842
1033
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
+ }
843
1041
return this . convertDebuggerPathToClient ( pathToConvert ) ;
844
1042
}
845
1043
@@ -968,7 +1166,7 @@ class GoDebugSession extends LoggingDebugSession {
968
1166
this . delve . call < DebugLocation [ ] | StacktraceOut > (
969
1167
this . delve . isApiV1 ? 'StacktraceGoroutine' : 'Stacktrace' ,
970
1168
[ stackTraceIn ] ,
971
- ( err , out ) => {
1169
+ async ( err , out ) => {
972
1170
if ( err ) {
973
1171
this . logDelveError ( err , 'Failed to produce stacktrace' ) ;
974
1172
return this . sendErrorResponse ( response , 2004 , 'Unable to produce stack trace: "{e}"' , {
@@ -977,6 +1175,9 @@ class GoDebugSession extends LoggingDebugSession {
977
1175
}
978
1176
const locations = this . delve . isApiV1 ? < DebugLocation [ ] > out : ( < StacktraceOut > out ) . Locations ;
979
1177
log ( 'locations' , locations ) ;
1178
+
1179
+ await this . initializeRemotePackagesAndSources ( ) ;
1180
+
980
1181
let stackFrames = locations . map ( ( location , frameId ) => {
981
1182
const uniqueStackFrameId = this . stackFrameHandles . create ( [ goroutineId , frameId ] ) ;
982
1183
return new StackFrame (
@@ -1400,8 +1601,8 @@ class GoDebugSession extends LoggingDebugSession {
1400
1601
args . remotePath = '' ;
1401
1602
}
1402
1603
1604
+ this . localPathSeparator = findPathSeparator ( localPath ) ;
1403
1605
if ( args . remotePath . length > 0 ) {
1404
- this . localPathSeparator = findPathSeparator ( localPath ) ;
1405
1606
this . remotePathSeparator = findPathSeparator ( args . remotePath ) ;
1406
1607
1407
1608
const llist = localPath . split ( / \/ | \\ / ) . reverse ( ) ;
@@ -1476,15 +1677,38 @@ class GoDebugSession extends LoggingDebugSession {
1476
1677
) ;
1477
1678
}
1478
1679
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 (
1480
1704
response : DebugProtocol . SetBreakpointsResponse ,
1481
1705
args : DebugProtocol . SetBreakpointsArguments
1482
- ) : Thenable < void > {
1706
+ ) : Promise < void > {
1483
1707
const file = normalizePath ( args . source . path ) ;
1484
1708
if ( ! this . breakpoints . get ( file ) ) {
1485
1709
this . breakpoints . set ( file , [ ] ) ;
1486
1710
}
1487
- const remoteFile = this . toDebuggerPath ( file ) ;
1711
+ const remoteFile = await this . toDebuggerPath ( file ) ;
1488
1712
1489
1713
return Promise . all (
1490
1714
this . breakpoints . get ( file ) . map ( ( existingBP ) => {
@@ -1591,12 +1815,13 @@ class GoDebugSession extends LoggingDebugSession {
1591
1815
) ;
1592
1816
}
1593
1817
1594
- private getPackageInfo ( debugState : DebuggerState ) : Thenable < string > {
1818
+ private async getPackageInfo ( debugState : DebuggerState ) : Promise < string > {
1595
1819
if ( ! debugState . currentThread || ! debugState . currentThread . file ) {
1596
1820
return Promise . resolve ( null ) ;
1597
1821
}
1822
+ await this . initializeRemotePackagesAndSources ( ) ;
1598
1823
const dir = path . dirname (
1599
- this . delve . remotePath . length
1824
+ this . delve . remotePath . length || this . delve . isRemoteDebugging
1600
1825
? this . toLocalPath ( debugState . currentThread . file )
1601
1826
: debugState . currentThread . file
1602
1827
) ;
@@ -1909,6 +2134,63 @@ class GoDebugSession extends LoggingDebugSession {
1909
2134
}
1910
2135
}
1911
2136
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
+
1912
2194
function random ( low : number , high : number ) : number {
1913
2195
return Math . floor ( Math . random ( ) * ( high - low ) + low ) ;
1914
2196
}
0 commit comments