@@ -11,7 +11,7 @@ import { observatoryListeningBannerPattern, pleaseReportBug } from "../shared/co
1111import { LogCategory , LogSeverity } from "../shared/enums" ;
1212import { LogMessage , SpawnedProcess } from "../shared/interfaces" ;
1313import { PackageMap } from "../shared/pub/package_map" ;
14- import { errorString , flatMap , notUndefined , throttle , uniq , uriToFilePath } from "../shared/utils" ;
14+ import { errorString , flatMap , notUndefined , PromiseCompleter , throttle , uniq , uriToFilePath } from "../shared/utils" ;
1515import { sortBy } from "../shared/utils/array" ;
1616import { applyColor , grey , grey2 } from "../shared/utils/colors" ;
1717import { getRandomInt } from "../shared/utils/fs" ;
@@ -36,7 +36,7 @@ const messageWithUriPattern = new RegExp(`(.*?)((?:package|dart|file):.*\\.dart)
3636// completionsRequest(response: DebugProtocol.CompletionsResponse, args: DebugProtocol.CompletionsArguments): void;
3737export class DartDebugSession extends DebugSession {
3838 // TODO: Tidy all this up
39- protected childProcess ?: SpawnedProcess ;
39+ protected childProcess ?: SpawnedProcess | RemoteEditorTerminal ;
4040 protected additionalPidsToTerminate : number [ ] = [ ] ;
4141 // We normally track the pid from Observatory to terminate the VM afterwards, but for Flutter Run it's
4242 // a remote PID and therefore doesn't make sense to try and terminate.
@@ -57,20 +57,15 @@ export class DartDebugSession extends DebugSession {
5757 private logStream ?: fs . WriteStream ;
5858 public debugSdkLibraries = false ;
5959 public debugExternalLibraries = false ;
60- // Usually we buffer messages so we can scan whole lines for stack frames, but
61- // for terminal apps that accept input this can result in the rendered text
62- // being out of order if messages don't end with newlines. Since we don't support
63- // linking up stack frames for terminal anyway, it's safe to just disable the
64- // buffering there.
65- public allowMessageBuffering = true ;
66- public sendOutputAsCustomEvent = false ;
6760 public showDartDeveloperLogs = true ;
6861 public useFlutterStructuredErrors = false ;
6962 public evaluateGettersInDebugViews = false ;
7063 protected previewToStringInDebugViews = false ;
7164 protected useWriteServiceInfo = false ;
7265 protected vmServiceInfoFile ?: string ;
7366 private serviceInfoPollTimer ?: NodeJS . Timer ;
67+ private remoteEditorTerminalLaunchCompleter ?: PromiseCompleter < RemoteEditorTerminal > ;
68+ private remoteEditorTerminalClosedCompleter ?: PromiseCompleter < { code : number | undefined } > ;
7469 public debuggerHandlesPathsEverywhereForBreakpoints = false ;
7570 protected threadManager : ThreadManager ;
7671 public packageMap ?: PackageMap ;
@@ -79,7 +74,7 @@ export class DartDebugSession extends DebugSession {
7974 protected parseObservatoryUriFromStdOut : boolean = true ;
8075 protected requiresProgram : boolean = true ;
8176 protected pollforMemoryMs ?: number ; // If set, will poll for memory usage and send events back.
82- protected processExit : Promise < void > = Promise . resolve ( ) ;
77+ protected processExit : Promise < { code : number | null , signal : string | null } > = Promise . resolve ( { code : 0 , signal : null } ) ;
8378 protected maxLogLineLength : number = 1000 ; // This should always be overriden in launch/attach requests but we have it here for narrower types.
8479 protected shouldKillProcessOnTerminate = true ;
8580 protected logCategory = LogCategory . General ; // This isn't used as General, since both debuggers override it.
@@ -117,7 +112,7 @@ export class DartDebugSession extends DebugSession {
117112 this . sendResponse ( response ) ;
118113 }
119114
120- protected launchRequest ( response : DebugProtocol . LaunchResponse , args : DartLaunchRequestArguments ) : void {
115+ protected async launchRequest ( response : DebugProtocol . LaunchResponse , args : DartLaunchRequestArguments ) : Promise < void > {
121116 if ( ! args || ! args . dartPath || ( this . requiresProgram && ! args . program ) ) {
122117 this . logToUser ( "Unable to restart debugging. Please try ending the debug session and starting again.\n" ) ;
123118 this . sendEvent ( new TerminatedEvent ( ) ) ;
@@ -149,43 +144,51 @@ export class DartDebugSession extends DebugSession {
149144 this . evaluateGettersInDebugViews = args . evaluateGettersInDebugViews ;
150145 this . previewToStringInDebugViews = args . previewToStringInDebugViews ;
151146 this . useWriteServiceInfo = args . useWriteServiceInfo ;
152- this . sendOutputAsCustomEvent = args . console === "terminal" ;
153- this . allowMessageBuffering = args . console !== "terminal" ;
154147 this . debuggerHandlesPathsEverywhereForBreakpoints = args . debuggerHandlesPathsEverywhereForBreakpoints ;
155148 this . logFile = args . observatoryLogFile ;
156149 this . maxLogLineLength = args . maxLogLineLength ;
157150
158151 this . sendResponse ( response ) ;
159152
160- this . childProcess = this . spawnProcess ( args ) ;
161-
162- // Start watching for the service info file.
163- if ( this . vmServiceInfoFile ) {
153+ if ( this . useWriteServiceInfo ) {
154+ this . parseObservatoryUriFromStdOut = false ;
155+ this . vmServiceInfoFile = path . join ( os . tmpdir ( ) , `dart-vm-service-${ getRandomInt ( 0x1000 , 0x10000 ) . toString ( 16 ) } .json` ) ;
164156 this . startServiceFilePolling ( ) ;
165157 }
166158
167- const process = this . childProcess ;
168- this . processExited = false ;
169- this . processExit = new Promise ( ( resolve ) => process . on ( "exit" , resolve ) ) ;
170- process . stdout . setEncoding ( "utf8" ) ;
171- process . stdout . on ( "data" , ( data ) => {
172- let match : RegExpExecArray | null = null ;
173- if ( this . shouldConnectDebugger && this . parseObservatoryUriFromStdOut && ! this . observatory ) {
174- match = observatoryListeningBannerPattern . exec ( data . toString ( ) ) ;
175- }
176- if ( match ) {
177- this . initDebugger ( this . websocketUriForObservatoryUri ( match [ 1 ] ) ) ;
178- } else if ( this . sendStdOutToConsole )
179- this . logToUserBuffered ( data . toString ( ) , "stdout" ) ;
180- } ) ;
181- process . stderr . setEncoding ( "utf8" ) ;
182- process . stderr . on ( "data" , ( data ) => {
183- this . logToUserBuffered ( data . toString ( ) , "stderr" ) ;
184- } ) ;
185- process . on ( "error" , ( error ) => {
186- this . logToUser ( `${ error } \n` , "stderr" ) ;
187- } ) ;
188- process . on ( "exit" , async ( code , signal ) => {
159+ if ( args . console === "terminal" ) {
160+ this . childProcess = await this . spawnRemoteEditorProcess ( args ) ;
161+
162+ this . remoteEditorTerminalClosedCompleter = new PromiseCompleter < { code : number | undefined } > ( ) ;
163+ this . processExit = this . remoteEditorTerminalClosedCompleter . promise . then ( ( ( { code } ) => {
164+ return { code : code !== undefined ? code : null , signal : null } ;
165+ } ) ) ;
166+ } else {
167+ const process = this . spawnProcess ( args ) ;
168+
169+ this . childProcess = process ;
170+ this . processExited = false ;
171+ this . processExit = new Promise ( ( resolve ) => process . on ( "exit" , ( code , signal ) => resolve ( { code, signal } ) ) ) ;
172+ process . stdout . setEncoding ( "utf8" ) ;
173+ process . stdout . on ( "data" , ( data ) => {
174+ let match : RegExpExecArray | null = null ;
175+ if ( this . shouldConnectDebugger && this . parseObservatoryUriFromStdOut && ! this . observatory ) {
176+ match = observatoryListeningBannerPattern . exec ( data . toString ( ) ) ;
177+ }
178+ if ( match ) {
179+ this . initDebugger ( this . websocketUriForObservatoryUri ( match [ 1 ] ) ) ;
180+ } else if ( this . sendStdOutToConsole )
181+ this . logToUserBuffered ( data . toString ( ) , "stdout" ) ;
182+ } ) ;
183+ process . stderr . setEncoding ( "utf8" ) ;
184+ process . stderr . on ( "data" , ( data ) => {
185+ this . logToUserBuffered ( data . toString ( ) , "stderr" ) ;
186+ } ) ;
187+ process . on ( "error" , ( error ) => {
188+ this . logToUser ( `${ error } \n` , "stderr" ) ;
189+ } ) ;
190+ }
191+ this . processExit . then ( async ( { code, signal } ) => {
189192 this . stopServiceFilePolling ( ) ;
190193 this . processExited = true ;
191194 this . log ( `Process exited (${ signal ? `${ signal } ` . toLowerCase ( ) : code } )` ) ;
@@ -252,14 +255,45 @@ export class DartDebugSession extends DebugSession {
252255 }
253256
254257 protected spawnProcess ( args : DartLaunchRequestArguments ) {
258+ const appArgs = this . buildAppArgs ( args ) ;
259+
260+ this . log ( `Spawning ${ args . dartPath } with args ${ JSON . stringify ( appArgs ) } ` ) ;
261+ if ( args . cwd )
262+ this . log ( `.. in ${ args . cwd } ` ) ;
263+ const process = safeSpawn ( args . cwd , args . dartPath , appArgs , args . env ) ;
264+
265+ this . log ( ` PID: ${ process . pid } ` ) ;
266+
267+ return process ;
268+ }
269+
270+ protected async spawnRemoteEditorProcess ( args : DartLaunchRequestArguments ) : Promise < RemoteEditorTerminal > {
271+ const appArgs = this . buildAppArgs ( args ) ;
272+
273+ this . log ( `Spawning ${ args . dartPath } remotely with args ${ JSON . stringify ( appArgs ) } ` ) ;
274+ if ( args . cwd )
275+ this . log ( `.. in ${ args . cwd } ` ) ;
276+
277+ this . remoteEditorTerminalLaunchCompleter = new PromiseCompleter < RemoteEditorTerminal > ( ) ;
278+ this . sendEvent ( new Event ( "dart.startTerminalProcess" , {
279+ args : appArgs ,
280+ binPath : args . dartPath ,
281+ cwd : args . cwd ,
282+ env : args . env ,
283+ } ) ) ;
284+
285+ this . log ( ` PID: ${ process . pid } ` ) ;
286+
287+ return this . remoteEditorTerminalLaunchCompleter . promise ;
288+ }
289+
290+ private buildAppArgs ( args : DartLaunchRequestArguments ) {
255291 let appArgs = [ ] ;
256292 if ( this . shouldConnectDebugger ) {
257293 appArgs . push ( `--enable-vm-service=${ args . vmServicePort } ` ) ;
258294 appArgs . push ( "--pause_isolates_on_start=true" ) ;
259295 }
260296 if ( this . useWriteServiceInfo ) {
261- this . parseObservatoryUriFromStdOut = false ;
262- this . vmServiceInfoFile = path . join ( os . tmpdir ( ) , `dart-vm-service-${ getRandomInt ( 0x1000 , 0x10000 ) . toString ( 16 ) } .json` ) ;
263297 appArgs . push ( `--write-service-info=${ this . vmServiceInfoFile } ` ) ;
264298 appArgs . push ( "-DSILENT_OBSERVATORY=true" ) ;
265299 }
@@ -273,16 +307,7 @@ export class DartDebugSession extends DebugSession {
273307 if ( args . args ) {
274308 appArgs = appArgs . concat ( args . args ) ;
275309 }
276-
277- this . log ( `Spawning ${ args . dartPath } with args ${ JSON . stringify ( appArgs ) } ` ) ;
278- if ( args . cwd )
279- this . log ( `.. in ${ args . cwd } ` ) ;
280-
281- const process = safeSpawn ( args . cwd , args . dartPath , appArgs , args . env ) ;
282-
283- this . log ( ` PID: ${ process . pid } ` ) ;
284-
285- return process ;
310+ return appArgs ;
286311 }
287312
288313 protected websocketUriForObservatoryUri ( uri : string ) {
@@ -334,7 +359,7 @@ export class DartDebugSession extends DebugSession {
334359 }
335360 }
336361
337- private tryReadServiceFile ( ) {
362+ private async tryReadServiceFile ( ) : Promise < void > {
338363 if ( ! this . vmServiceInfoFile || ! fs . existsSync ( this . vmServiceInfoFile ) )
339364 return ;
340365
@@ -358,6 +383,12 @@ export class DartDebugSession extends DebugSession {
358383 url . pathname = `${ url . pathname } /ws` ;
359384
360385 this . stopServiceFilePolling ( ) ;
386+ // Ensure we don't try to start anything before we've finished
387+ // setting up the process when running remotely.
388+ if ( this . remoteEditorTerminalLaunchCompleter ) {
389+ await this . remoteEditorTerminalLaunchCompleter . promise ;
390+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
391+ }
361392 this . initDebugger ( url . toString ( ) ) ;
362393 } catch ( e ) {
363394 this . logger . error ( e , LogCategory . Observatory ) ;
@@ -542,12 +573,16 @@ export class DartDebugSession extends DebugSession {
542573 }
543574 }
544575 if ( ! this . processExited ) {
545- try {
546- this . log ( `${ request } : Terminating main process with ${ signal } ...` ) ;
547- this . childProcess . kill ( signal ) ;
548- } catch ( e ) {
549- // This tends to throw a lot because the shell process quit when we terminated the related
550- // VM process above, so just swallow the error.
576+ if ( this . childProcess . pid ) {
577+ try {
578+ this . log ( `${ request } : Terminating main process with ${ signal } ...` ) ;
579+ process . kill ( this . childProcess . pid , signal ) ;
580+ } catch ( e ) {
581+ // This tends to throw a lot because the shell process quit when we terminated the related
582+ // VM process above, so just swallow the error.
583+ }
584+ } else {
585+ this . log ( `${ request } : Process had no PID.` ) ;
551586 }
552587 } else {
553588 this . log ( `${ request } : Main process had already quit.` ) ;
@@ -1282,17 +1317,28 @@ export class DartDebugSession extends DebugSession {
12821317 await this . callService ( args . type , args . params ) ;
12831318 this . sendResponse ( response ) ;
12841319 break ;
1285- case "dart.userInput" :
1286- if ( this . childProcess && ! this . childProcess . killed && ! this . processExited )
1287- this . childProcess . stdin . write ( args . input ) ;
1288- this . sendResponse ( response ) ;
1289- break ;
12901320 case "updateDebugOptions" :
12911321 this . debugExternalLibraries = ! ! args . debugExternalLibraries ;
12921322 this . debugSdkLibraries = ! ! args . debugSdkLibraries ;
12931323 await this . threadManager . setLibrariesDuggableForAllIsolates ( ) ;
12941324 this . sendResponse ( response ) ;
12951325 break ;
1326+ case "remoteEditorTerminalLaunch" :
1327+ if ( ! this . remoteEditorTerminalLaunchCompleter ) {
1328+ this . log ( `Invalid ${ request } because no completer` , LogSeverity . Warn ) ;
1329+ return ;
1330+ }
1331+ this . remoteEditorTerminalLaunchCompleter . resolve ( new RemoteEditorTerminal ( args . pid ) ) ;
1332+ this . sendResponse ( response ) ;
1333+ break ;
1334+ case "remoteEditorTerminalClosed" :
1335+ if ( ! this . remoteEditorTerminalClosedCompleter ) {
1336+ this . log ( `Invalid ${ request } because no completer` , LogSeverity . Warn ) ;
1337+ return ;
1338+ }
1339+ this . remoteEditorTerminalClosedCompleter . resolve ( { code : args ?. code } ) ;
1340+ this . sendResponse ( response ) ;
1341+ break ;
12961342 // Flutter requests that may be sent during test runs or other places
12971343 // that we don't currently support. TODO: Fix this by moving all the
12981344 // service extension stuff out of Flutter to here, and making it not
@@ -1893,11 +1939,6 @@ export class DartDebugSession extends DebugSession {
18931939 /// [5:01:50 PM] [General] [Info] [stderr] main (file:///D:/a/
18941940 /// [5:01:50 PM] [General] [Info] [stderr] Dart-Code/Dart-Code/src/test/test_projects/hello_world/bin/broken.dart:2:3)
18951941 protected logToUserBuffered ( message : string , category : string ) {
1896- if ( ! this . allowMessageBuffering ) {
1897- this . logToUser ( message , category ) ;
1898- return ;
1899- }
1900-
19011942 this . logBuffer [ category ] = this . logBuffer [ category ] || "" ;
19021943 this . logBuffer [ category ] += message ;
19031944
@@ -1913,10 +1954,6 @@ export class DartDebugSession extends DebugSession {
19131954 // Logs a message back to the editor. Does not add its own newlines, you must
19141955 // provide them!
19151956 protected logToUser ( message : string , category ?: string , colorText = ( s : string ) => s ) {
1916- if ( this . sendOutputAsCustomEvent ) {
1917- this . sendEvent ( new Event ( "dart.output" , { message, category } ) ) ;
1918- return ;
1919- }
19201957 // Extract stack frames from the message so we can do nicer formatting of them.
19211958 const frame = this . getStackFrameData ( message ) || this . getWebStackFrameData ( message ) || this . getMessageWithUriData ( message ) ;
19221959
@@ -1974,3 +2011,9 @@ interface MessageWithUriData {
19742011 prefix : string ;
19752012 sourceUri : string ;
19762013}
2014+
2015+ class RemoteEditorTerminal {
2016+ public killed = false ;
2017+
2018+ constructor ( public readonly pid : number ) { }
2019+ }
0 commit comments