@@ -343,6 +343,16 @@ function findPathSeparator(filePath: string) {
343343 return filePath . includes ( '/' ) ? '/' : '\\' ;
344344}
345345
346+ // Comparing two different file paths while ignoring any different path separators.
347+ function compareFilePathIgnoreSeparator ( firstFilePath : string , secondFilePath : string ) : boolean {
348+ const firstSeparator = findPathSeparator ( firstFilePath ) ;
349+ const secondSeparator = findPathSeparator ( secondFilePath ) ;
350+ if ( firstSeparator === secondSeparator ) {
351+ return firstFilePath === secondFilePath ;
352+ }
353+ return firstFilePath === secondFilePath . split ( secondSeparator ) . join ( firstSeparator ) ;
354+ }
355+
346356export function escapeGoModPath ( filePath : string ) {
347357 return filePath . replace ( / [ A - Z ] / g, ( match : string ) => `!${ match . toLocaleLowerCase ( ) } ` ) ;
348358}
@@ -376,6 +386,7 @@ export class Delve {
376386 public stackTraceDepth : number ;
377387 public isRemoteDebugging : boolean ;
378388 public goroot : string ;
389+ public delveConnectionClosed = false ;
379390 private localDebugeePath : string | undefined ;
380391 private debugProcess : ChildProcess ;
381392 private request : 'attach' | 'launch' ;
@@ -695,9 +706,10 @@ export class Delve {
695706 *
696707 * For attach debugging there are two scenarios; attaching to a local process by ID or connecting to a
697708 * remote delve server. For attach-local we start the delve process so will also terminate it however we
698- * detach from the debugee without killing it. For attach-remote we only detach from delve.
709+ * detach from the debugee without killing it. For attach-remote we only close the client connection,
710+ * but do not terminate the remote server.
699711 *
700- * The only way to detach from delve when it is running a program is to send a Halt request first.
712+ * For local debugging, the only way to detach from delve when it is running a program is to send a Halt request first.
701713 * Since the Halt request might sometimes take too long to complete, we have a timer in place to forcefully kill
702714 * the debug process and clean up the assets in case of local debugging
703715 */
@@ -713,18 +725,19 @@ export class Delve {
713725 await forceCleanup ( ) ;
714726 return Promise . resolve ( ) ;
715727 }
716- log ( 'HaltRequest' ) ;
717728 const isLocalDebugging : boolean = this . request === 'launch' && ! ! this . debugProcess ;
718729
719730 return new Promise ( async ( resolve ) => {
720- // For remote debugging, closing the connection would terminate the
721- // program as well so we just want to disconnect .
731+ // For remote debugging, we want to leave the remote dlv server running,
732+ // so instead of killing it via halt+detach, we just close the network connection .
722733 // See https://www.github.com/go-delve/delve/issues/1587
723734 if ( this . isRemoteDebugging ) {
735+ log ( 'Remote Debugging: close dlv connection.' ) ;
724736 const rpcConnection = await this . connection ;
725737 // tslint:disable-next-line no-any
726738 ( rpcConnection as any ) [ 'conn' ] [ 'end' ] ( ) ;
727- return ;
739+ this . delveConnectionClosed = true ;
740+ return resolve ( ) ;
728741 }
729742 const timeoutToken : NodeJS . Timer =
730743 isLocalDebugging &&
@@ -736,6 +749,7 @@ export class Delve {
736749
737750 let haltErrMsg : string ;
738751 try {
752+ log ( 'HaltRequest' ) ;
739753 await this . callPromise ( 'Command' , [ { name : 'halt' } ] ) ;
740754 } catch ( err ) {
741755 log ( 'HaltResponse' ) ;
@@ -854,30 +868,44 @@ export class GoDebugSession extends LoggingDebugSession {
854868 args : DebugProtocol . DisconnectArguments
855869 ) : Promise < void > {
856870 log ( 'DisconnectRequest' ) ;
857- if ( ! this . delve ) {
858- log ( 'DisconnectRequest to parent' ) ;
859- super . disconnectRequest ( response , args ) ;
860- log ( 'DisconnectResponse' ) ;
861- return ;
871+ if ( this . delve ) {
872+ // Since users want to reset when they issue a disconnect request,
873+ // we should have a timeout in case disconnectRequestHelper hangs.
874+ await Promise . race ( [
875+ this . disconnectRequestHelper ( response , args ) ,
876+ new Promise ( ( resolve ) => setTimeout ( ( ) => {
877+ log ( 'DisconnectRequestHelper timed out after 5s.' ) ;
878+ resolve ( ) ;
879+ } , 5_000 ) )
880+ ] ) ;
862881 }
863882
883+ this . shutdownProtocolServer ( response , args ) ;
884+ log ( 'DisconnectResponse' ) ;
885+ }
886+
887+ protected async disconnectRequestHelper (
888+ response : DebugProtocol . DisconnectResponse ,
889+ args : DebugProtocol . DisconnectArguments
890+ ) : Promise < void > {
864891 // For remote process, we have to issue a continue request
865892 // before disconnecting.
866893 if ( this . delve . isRemoteDebugging ) {
867- // We don't have to wait for continue call
868- // because we are not doing anything with the result.
869- // Also, DisconnectRequest will return before
870- // we get the result back from delve.
871- this . debugState = await this . delve . getDebugState ( ) ;
872- if ( ! this . debugState . Running ) {
894+ // There is a chance that a second disconnectRequest can come through
895+ // if users click detach multiple times. In that case, we want to
896+ // guard against talking to the closed Delve connection.
897+ if ( this . delve . delveConnectionClosed ) {
898+ log ( `Skip disconnectRequestHelper as Delve's connection is already closed.` ) ;
899+ return ;
900+ }
901+
902+ if ( ! ( await this . isDebuggeeRunning ( ) ) ) {
903+ log ( `Issuing a continue command before closing Delve's connection as the debuggee is not running.` ) ;
873904 this . continue ( ) ;
874905 }
875906 }
876- this . delve . close ( ) . then ( ( ) => {
877- log ( 'DisconnectRequest to parent' ) ;
878- super . disconnectRequest ( response , args ) ;
879- log ( 'DisconnectResponse' ) ;
880- } ) ;
907+ log ( 'Closing Delve.' ) ;
908+ await this . delve . close ( ) ;
881909 }
882910
883911 protected async configurationDoneRequest (
@@ -888,12 +916,9 @@ export class GoDebugSession extends LoggingDebugSession {
888916 if ( this . stopOnEntry ) {
889917 this . sendEvent ( new StoppedEvent ( 'entry' , 1 ) ) ;
890918 log ( 'StoppedEvent("entry")' ) ;
891- } else {
892- this . debugState = await this . delve . getDebugState ( ) ;
893- if ( ! this . debugState . Running ) {
894- log ( 'Changing DebugState from Halted to Running' ) ;
895- this . continue ( ) ;
896- }
919+ } else if ( ! await this . isDebuggeeRunning ( ) ) {
920+ log ( 'Changing DebugState from Halted to Running' ) ;
921+ this . continue ( ) ;
897922 }
898923 this . sendResponse ( response ) ;
899924 log ( 'ConfigurationDoneResponse' , response ) ;
@@ -1168,19 +1193,12 @@ export class GoDebugSession extends LoggingDebugSession {
11681193 args : DebugProtocol . SetBreakpointsArguments
11691194 ) : Promise < void > {
11701195 log ( 'SetBreakPointsRequest' ) ;
1171- try {
1172- // If a program is launched with --continue, the program is running
1173- // before we can run attach. So we would need to check the state.
1174- // We use NonBlocking so the call would return immediately.
1175- this . debugState = await this . delve . getDebugState ( ) ;
1176- } catch ( error ) {
1177- this . logDelveError ( error , 'Failed to get state' ) ;
1178- }
1179-
1180- if ( ! this . debugState . Running && ! this . continueRequestRunning ) {
1196+ if ( ! ( await this . isDebuggeeRunning ( ) ) ) {
1197+ log ( 'Debuggee is not running. Setting breakpoints without halting.' ) ;
11811198 await this . setBreakPoints ( response , args ) ;
11821199 } else {
11831200 this . skipStopEventOnce = this . continueRequestRunning ;
1201+ log ( `Halting before setting breakpoints. SkipStopEventOnce is ${ this . skipStopEventOnce } .` ) ;
11841202 this . delve . callPromise ( 'Command' , [ { name : 'halt' } ] ) . then (
11851203 ( ) => {
11861204 return this . setBreakPoints ( response , args ) . then ( ( ) => {
@@ -1203,9 +1221,9 @@ export class GoDebugSession extends LoggingDebugSession {
12031221 }
12041222 }
12051223
1206- protected threadsRequest ( response : DebugProtocol . ThreadsResponse ) : void {
1207- if ( this . continueRequestRunning ) {
1208- // Thread request to delve is syncronous and will block if a previous async continue request didn't return
1224+ protected async threadsRequest ( response : DebugProtocol . ThreadsResponse ) : Promise < void > {
1225+ if ( ( await this . isDebuggeeRunning ( ) ) ) {
1226+ // Thread request to delve is synchronous and will block if a previous async continue request didn't return
12091227 response . body = { threads : [ new Thread ( 1 , 'Dummy' ) ] } ;
12101228 return this . sendResponse ( response ) ;
12111229 }
@@ -1244,11 +1262,20 @@ export class GoDebugSession extends LoggingDebugSession {
12441262 } ) ;
12451263 }
12461264
1247- protected stackTraceRequest (
1265+ protected async stackTraceRequest (
12481266 response : DebugProtocol . StackTraceResponse ,
12491267 args : DebugProtocol . StackTraceArguments
1250- ) : void {
1268+ ) : Promise < void > {
12511269 log ( 'StackTraceRequest' ) ;
1270+ // For normal VSCode, this request doesn't get invoked when we send a Dummy thread
1271+ // in the scenario where the debuggee is running.
1272+ // For Theia, however, this does get invoked and so we should just send an error
1273+ // response that we cannot get the stack trace at this point since the debugggee is running.
1274+ if ( await this . isDebuggeeRunning ( ) ) {
1275+ this . sendErrorResponse ( response , 2004 , 'Unable to produce stack trace as the debugger is running' ) ;
1276+ return ;
1277+ }
1278+
12521279 // delve does not support frame paging, so we ask for a large depth
12531280 const goroutineId = args . threadId ;
12541281 const stackTraceIn = { id : goroutineId , depth : this . delve . stackTraceDepth } ;
@@ -1919,11 +1946,14 @@ export class GoDebugSession extends LoggingDebugSession {
19191946 return null ;
19201947 }
19211948 }
1949+
1950+ // Make sure that we compare the file names with the same separators.
19221951 const matchedBreakpoint = existingBreakpoints . find (
19231952 ( existingBreakpoint ) =>
1924- existingBreakpoint . line === breakpointIn . line &&
1925- existingBreakpoint . file === breakpointIn . file
1953+ existingBreakpoint . line === breakpointIn . line
1954+ && compareFilePathIgnoreSeparator ( existingBreakpoint . file , breakpointIn . file )
19261955 ) ;
1956+
19271957 if ( ! matchedBreakpoint ) {
19281958 log ( `Cannot match breakpoint ${ breakpointIn } with existing breakpoints.` ) ;
19291959 return null ;
@@ -2166,6 +2196,7 @@ export class GoDebugSession extends LoggingDebugSession {
21662196 }
21672197
21682198 private handleReenterDebug ( reason : string ) : void {
2199+ log ( `handleReenterDebug(${ reason } ).` ) ;
21692200 this . cleanupHandles ( ) ;
21702201
21712202 if ( this . debugState . exited ) {
@@ -2190,6 +2221,7 @@ export class GoDebugSession extends LoggingDebugSession {
21902221 }
21912222
21922223 if ( this . skipStopEventOnce ) {
2224+ log ( `Skipping stop event for ${ reason } . The current Go routines is ${ this . debugState ?. currentGoroutine } .` ) ;
21932225 this . skipStopEventOnce = false ;
21942226 return ;
21952227 }
@@ -2201,6 +2233,34 @@ export class GoDebugSession extends LoggingDebugSession {
22012233 } ) ;
22022234 }
22032235 }
2236+
2237+ // Returns true if the debuggee is running.
2238+ // The call getDebugState is non-blocking so it should return
2239+ // almost instantaneously. However, if we run into some errors,
2240+ // we will fall back to the internal tracking of the debug state.
2241+ // TODO: If Delve is not in multi-client state, we can simply
2242+ // track the running state with continueRequestRunning internally
2243+ // instead of issuing a getDebugState call to Delve. Perhaps we want to
2244+ // do that to improve performance in the future.
2245+ private async isDebuggeeRunning ( ) : Promise < boolean > {
2246+ try {
2247+ this . debugState = await this . delve . getDebugState ( ) ;
2248+ return this . debugState . Running ;
2249+ } catch ( error ) {
2250+ this . logDelveError ( error , 'Failed to get state' ) ;
2251+ // Fall back to the internal tracking.
2252+ return this . continueRequestRunning ;
2253+ }
2254+ }
2255+
2256+ private shutdownProtocolServer (
2257+ response : DebugProtocol . DisconnectResponse ,
2258+ args : DebugProtocol . DisconnectArguments
2259+ ) : void {
2260+ log ( 'DisconnectRequest to parent to shut down protocol server.' ) ;
2261+ super . disconnectRequest ( response , args ) ;
2262+ }
2263+
22042264 private continue ( calledWhenSettingBreakpoint ?: boolean ) : Thenable < void > {
22052265 this . continueEpoch ++ ;
22062266 const closureEpoch = this . continueEpoch ;
@@ -2364,13 +2424,13 @@ export class GoDebugSession extends LoggingDebugSession {
23642424 // Debugger may be stopped at this point but we still can (and need) to obtain state and stacktrace
23652425 let goroutineId = 0 ;
23662426 try {
2367- const stateCallResult = await this . delve . getDebugState ( ) ;
2427+ this . debugState = await this . delve . getDebugState ( ) ;
23682428 // In some fault scenarios there may not be a currentGoroutine available from the debugger state
23692429 // Use the current thread
2370- if ( ! stateCallResult . currentGoroutine ) {
2371- goroutineId = stateCallResult . currentThread . goroutineID ;
2430+ if ( ! this . debugState . currentGoroutine ) {
2431+ goroutineId = this . debugState . currentThread . goroutineID ;
23722432 } else {
2373- goroutineId = stateCallResult . currentGoroutine . id ;
2433+ goroutineId = this . debugState . currentGoroutine . id ;
23742434 }
23752435 } catch ( error ) {
23762436 logError ( 'dumpStacktrace - Failed to get debugger state ' + error ) ;
0 commit comments