@@ -11,16 +11,22 @@ import * as fs from 'fs';
11
11
import net = require( 'net' ) ;
12
12
import * as os from 'os' ;
13
13
import * as path from 'path' ;
14
+ import kill = require( 'tree-kill' ) ;
14
15
15
16
import {
16
17
logger ,
17
18
Logger ,
18
19
LoggingDebugSession ,
20
+ OutputEvent ,
19
21
TerminatedEvent
20
22
} from 'vscode-debugadapter' ;
21
23
import { DebugProtocol } from 'vscode-debugprotocol' ;
22
24
23
- import { envPath } from '../goPath' ;
25
+ import {
26
+ envPath ,
27
+ getBinPathWithPreferredGopathGoroot ,
28
+ parseEnvFile
29
+ } from '../goPath' ;
24
30
import { DAPClient } from './dapClient' ;
25
31
26
32
interface LoadConfig {
@@ -133,7 +139,11 @@ export class GoDlvDapDebugSession extends LoggingDebugSession {
133
139
134
140
private logLevel : Logger . LogLevel = Logger . LogLevel . Error ;
135
141
136
- private dlvClient : DelveClient ;
142
+ private dlvClient : DelveClient = null ;
143
+
144
+ // Child process used to track debugee launched without debugging (noDebug
145
+ // mode). Either debugProcess or dlvClient are null.
146
+ private debugProcess : ChildProcess = null ;
137
147
138
148
public constructor ( ) {
139
149
super ( ) ;
@@ -185,19 +195,35 @@ export class GoDlvDapDebugSession extends LoggingDebugSession {
185
195
const logPath =
186
196
this . logLevel !== Logger . LogLevel . Error ? path . join ( os . tmpdir ( ) , 'vscode-godlvdapdebug.txt' ) : undefined ;
187
197
logger . setup ( this . logLevel , logPath ) ;
188
-
189
198
log ( 'launchRequest' ) ;
190
199
200
+ // In noDebug mode, we don't launch Delve.
201
+ // TODO: this logic is currently organized for compatibility with the
202
+ // existing DA. It's not clear what we should do in case noDebug is
203
+ // set and mode isn't 'debug'. Sending an error response could be
204
+ // a safe option.
205
+ if ( args . noDebug && args . mode === 'debug' ) {
206
+ try {
207
+ this . launchNoDebug ( args ) ;
208
+ } catch ( e ) {
209
+ logError ( `launchNoDebug failed: "${ e } "` ) ;
210
+ // TODO: define error constants
211
+ // https://github.com/golang/vscode-go/issues/305
212
+ this . sendErrorResponse (
213
+ response ,
214
+ 3000 ,
215
+ `Failed to launch "${ e } "` ) ;
216
+ }
217
+ return ;
218
+ }
219
+
191
220
if ( ! args . port ) {
192
221
args . port = this . DEFAULT_DELVE_PORT ;
193
222
}
194
223
if ( ! args . host ) {
195
224
args . host = this . DEFAULT_DELVE_HOST ;
196
225
}
197
226
198
- // TODO: if this is a noDebug launch request, don't launch Delve;
199
- // instead, run the program directly.
200
-
201
227
this . dlvClient = new DelveClient ( args ) ;
202
228
203
229
this . dlvClient . on ( 'stdout' , ( str ) => {
@@ -214,6 +240,8 @@ export class GoDlvDapDebugSession extends LoggingDebugSession {
214
240
215
241
this . dlvClient . on ( 'close' , ( rc ) => {
216
242
if ( rc !== 0 ) {
243
+ // TODO: define error constants
244
+ // https://github.com/golang/vscode-go/issues/305
217
245
this . sendErrorResponse (
218
246
response ,
219
247
3000 ,
@@ -248,7 +276,35 @@ export class GoDlvDapDebugSession extends LoggingDebugSession {
248
276
args : DebugProtocol . DisconnectArguments ,
249
277
request ?: DebugProtocol . Request
250
278
) : void {
251
- this . dlvClient . send ( request ) ;
279
+ log ( 'DisconnectRequest' ) ;
280
+ // How we handle DisconnectRequest depends on whether Delve was launched
281
+ // at all.
282
+ // * In noDebug node, the Go program was spawned directly without
283
+ // debugging: this.debugProcess will be non-null, and this.dlvClient
284
+ // will be null.
285
+ // * Otherwise, Delve was spawned: this.debugProcess will be null, and
286
+ // this.dlvClient will be non-null.
287
+ if ( this . debugProcess !== null ) {
288
+ log ( `killing debugee (pid: ${ this . debugProcess . pid } )...` ) ;
289
+
290
+ // Kill the debugee and notify the client when the killing is
291
+ // completed, to ensure a clean shutdown sequence.
292
+ killProcessTree ( this . debugProcess ) . then ( ( ) => {
293
+ super . disconnectRequest ( response , args ) ;
294
+ log ( 'DisconnectResponse' ) ;
295
+ } ) ;
296
+ } else if ( this . dlvClient !== null ) {
297
+ // Forward this DisconnectRequest to Delve.
298
+ this . dlvClient . send ( request ) ;
299
+ } else {
300
+ logError ( `both debug process and dlv client are null` ) ;
301
+ // TODO: define all error codes as constants
302
+ // https://github.com/golang/vscode-go/issues/305
303
+ this . sendErrorResponse (
304
+ response ,
305
+ 3000 ,
306
+ 'Failed to disconnect: Check the debug console for details.' ) ;
307
+ }
252
308
}
253
309
254
310
protected terminateRequest (
@@ -537,6 +593,73 @@ export class GoDlvDapDebugSession extends LoggingDebugSession {
537
593
) : void {
538
594
this . dlvClient . send ( request ) ;
539
595
}
596
+
597
+ // Launch the debugee process without starting a debugger.
598
+ // This implements the `Run > Run Without Debugger` functionality in vscode.
599
+ // Note: this method currently assumes launchArgs.mode === 'debug'.
600
+ private launchNoDebug ( launchArgs : LaunchRequestArguments ) : void {
601
+ const program = launchArgs . program ;
602
+ if ( ! program ) {
603
+ throw new Error ( 'The program attribute is missing in the debug configuration in launch.json' ) ;
604
+ }
605
+ let programIsDirectory = false ;
606
+ try {
607
+ programIsDirectory = fs . lstatSync ( program ) . isDirectory ( ) ;
608
+ } catch ( e ) {
609
+ throw new Error ( 'The program attribute must point to valid directory, .go file or executable.' ) ;
610
+ }
611
+ if ( ! programIsDirectory && path . extname ( program ) !== '.go' ) {
612
+ throw new Error ( 'The program attribute must be a directory or .go file in debug mode' ) ;
613
+ }
614
+
615
+ const goRunArgs = [ 'run' ] ;
616
+ if ( launchArgs . buildFlags ) {
617
+ goRunArgs . push ( launchArgs . buildFlags ) ;
618
+ }
619
+
620
+ if ( programIsDirectory ) {
621
+ goRunArgs . push ( '.' ) ;
622
+ } else {
623
+ goRunArgs . push ( program ) ;
624
+ }
625
+
626
+ if ( launchArgs . args ) {
627
+ goRunArgs . push ( ...launchArgs . args ) ;
628
+ }
629
+
630
+ // Read env from disk and merge into env variables.
631
+ const fileEnvs = [ ] ;
632
+ if ( typeof launchArgs . envFile === 'string' ) {
633
+ fileEnvs . push ( parseEnvFile ( launchArgs . envFile ) ) ;
634
+ }
635
+ if ( Array . isArray ( launchArgs . envFile ) ) {
636
+ launchArgs . envFile . forEach ( ( envFile ) => {
637
+ fileEnvs . push ( parseEnvFile ( envFile ) ) ;
638
+ } ) ;
639
+ }
640
+
641
+ const launchArgsEnv = launchArgs . env || { } ;
642
+ const programEnv = Object . assign ( { } , process . env , ...fileEnvs , launchArgsEnv ) ;
643
+
644
+ const dirname = programIsDirectory ? program : path . dirname ( program ) ;
645
+ const goExe = getBinPathWithPreferredGopathGoroot ( 'go' , [ ] ) ;
646
+ log ( `Current working directory: ${ dirname } ` ) ;
647
+ log ( `Running: ${ goExe } ${ goRunArgs . join ( ' ' ) } ` ) ;
648
+
649
+ this . debugProcess = spawn ( goExe , goRunArgs , {
650
+ cwd : dirname ,
651
+ env : programEnv
652
+ } ) ;
653
+ this . debugProcess . stderr . on ( 'data' , ( str ) => {
654
+ this . sendEvent ( new OutputEvent ( str . toString ( ) , 'stderr' ) ) ;
655
+ } ) ;
656
+ this . debugProcess . stdout . on ( 'data' , ( str ) => {
657
+ this . sendEvent ( new OutputEvent ( str . toString ( ) , 'stdout' ) ) ;
658
+ } ) ;
659
+ this . debugProcess . on ( 'close' , ( rc ) => {
660
+ this . sendEvent ( new TerminatedEvent ( ) ) ;
661
+ } ) ;
662
+ }
540
663
}
541
664
542
665
// DelveClient provides a DAP client to talk to a DAP server in Delve.
@@ -643,3 +766,25 @@ class DelveClient extends DAPClient {
643
766
} , 200 ) ;
644
767
}
645
768
}
769
+
770
+ // TODO: refactor this function into util.ts so it could be reused with
771
+ // the existing DA. Problem: it currently uses log() and logError() which makes
772
+ // this more difficult.
773
+ // We'll want a separate util.ts for the DA, because the current utils.ts pulls
774
+ // in vscode as a dependency, which shouldn't be done in a DA.
775
+ function killProcessTree ( p : ChildProcess ) : Promise < void > {
776
+ if ( ! p || ! p . pid ) {
777
+ log ( `no process to kill` ) ;
778
+ return Promise . resolve ( ) ;
779
+ }
780
+ return new Promise ( ( resolve ) => {
781
+ kill ( p . pid , ( err ) => {
782
+ if ( err ) {
783
+ logError ( `Error killing process ${ p . pid } : ${ err } ` ) ;
784
+ } else {
785
+ log ( `killed process ${ p . pid } ` ) ;
786
+ }
787
+ resolve ( ) ;
788
+ } ) ;
789
+ } ) ;
790
+ }
0 commit comments