11import { Scope } from "@vscode/debugadapter" ;
2- import { ChildProcessWithoutNullStreams , spawn } from "child_process" ;
32import { EventEmitter } from "events" ;
43import * as Net from "net" ;
54import { Signal } from "./signals.js" ;
65import { assert } from "console" ;
6+ import { Terminal , TerminalShellExecution , window } from "vscode" ;
77
88export interface FileAccessor {
99 isWindows : boolean ;
@@ -54,21 +54,45 @@ export interface IStarlingMonkeyRuntimeConfig {
5454}
5555
5656class ComponentRuntimeInstance {
57- private static _componentRuntime : ChildProcessWithoutNullStreams ;
58- static running : boolean ;
59- static workspaceFolder : string ;
60- static _server : Net . Server ;
61- static _nextSessionPort : number | undefined ;
57+ private static _workspaceFolder ?: string ;
58+ private static _component ?: string ;
59+ private static _server ?: Net . Server ;
60+ private static _terminal ?: Terminal ;
61+ private static _nextSessionPort ?: number ;
62+ private static _runtimeExecution ?: TerminalShellExecution ;
6263
6364 static setNextSessionPort ( port : number ) {
6465 this . _nextSessionPort = port ;
6566 }
6667
6768 static async start ( workspaceFolder : string , component : string , config : IStarlingMonkeyRuntimeConfig ) {
68- assert ( ! this . running , "ComponentRuntime is already running" ) ;
69- this . running = true ;
70- this . workspaceFolder = workspaceFolder ;
69+ if ( this . _workspaceFolder && this . _workspaceFolder !== workspaceFolder ||
70+ this . _component && this . _component !== component
71+ ) {
72+ this . reset ( ) ;
73+ }
74+
75+ this . _workspaceFolder = workspaceFolder ;
76+ this . _component = component ;
77+
78+ this . ensureServer ( ) ;
79+ this . ensureHostRuntime ( config , workspaceFolder , component ) ;
80+ }
81+ static reset ( ) {
82+ this . _workspaceFolder = undefined ;
83+ this . _component = undefined ;
84+ this . _nextSessionPort = undefined ;
85+ this . _server ?. close ( ) ;
86+ this . _server = undefined ;
87+ this . _terminal ?. dispose ( ) ;
88+ this . _terminal = undefined ;
89+ this . _runtimeExecution = undefined ;
90+ }
7191
92+ static ensureServer ( ) {
93+ if ( this . _server ) {
94+ return ;
95+ }
7296 this . _server = Net . createServer ( ( socket ) => {
7397 socket . on ( "data" , ( data ) => {
7498 assert (
@@ -89,11 +113,21 @@ class ComponentRuntimeInstance {
89113 this . _nextSessionPort = undefined ;
90114 }
91115 } ) ;
116+ socket . on ( "close" , ( ) => {
117+ console . debug ( "ComponentRuntime disconnected" ) ;
118+ } ) ;
92119 } ) . listen ( ) ;
93- let port = ( < Net . AddressInfo > this . _server . address ( ) ) . port ;
94- console . info ( `waiting for debug protocol on port ${ port } ` ) ;
120+ }
121+
122+ private static serverPort ( ) {
123+ return ( < Net . AddressInfo > this . _server ! . address ( ) ) . port ;
124+ }
125+
126+ private static async ensureHostRuntime ( config : IStarlingMonkeyRuntimeConfig , workspaceFolder : string , component : string ) {
127+ if ( this . _runtimeExecution ) {
128+ return ;
129+ }
95130
96- // Start componentRuntime as a new process
97131 let componentRuntimeArgs = Array . from ( config . componentRuntime . options ) . map ( opt => {
98132 return opt
99133 . replace ( "${workspaceFolder}" , workspaceFolder )
@@ -104,28 +138,69 @@ class ComponentRuntimeInstance {
104138 `STARLINGMONKEY_CONFIG="${ config . jsRuntimeOptions . join ( " " ) } "`
105139 ) ;
106140 componentRuntimeArgs . push ( config . componentRuntime . envOption ) ;
107- componentRuntimeArgs . push ( `DEBUGGER_PORT=${ port } ` ) ;
141+ componentRuntimeArgs . push ( `DEBUGGER_PORT=${ this . serverPort ( ) } ` ) ;
142+
108143 console . debug (
109144 `${ config . componentRuntime . executable } ${ componentRuntimeArgs . join ( " " ) } `
110145 ) ;
111- this . _componentRuntime = spawn (
112- config . componentRuntime . executable ,
113- componentRuntimeArgs ,
114- { cwd : workspaceFolder }
115- ) ;
116146
117- this . _componentRuntime . stdout . on ( "data" , ( data ) => {
118- console . log ( `componentRuntime ${ data } ` ) ;
119- } ) ;
147+ await this . ensureTerminal ( ) ;
120148
121- this . _componentRuntime . stderr . on ( "data" , ( data ) => {
122- console . error ( `componentRuntime ${ data } ` ) ;
123- } ) ;
149+ if ( this . _terminal ! . shellIntegration ) {
150+ this . _runtimeExecution = this . _terminal ! . shellIntegration . executeCommand (
151+ config . componentRuntime . executable ,
152+ componentRuntimeArgs
153+ ) ;
154+ let disposable = window . onDidEndTerminalShellExecution ( ( event ) => {
155+ if ( event . execution === this . _runtimeExecution ) {
156+ this . _runtimeExecution = undefined ;
157+ disposable . dispose ( ) ;
158+ console . log ( `Component host runtime exited with code ${ event . exitCode } ` ) ;
159+ }
160+ } ) ;
161+ } else {
162+ // Fallback to sendText if there is no shell integration.
163+ // Send Ctrl+C to kill any existing component runtime first.
164+ this . _terminal ! . sendText ( '\x03' , false ) ;
165+ this . _terminal ! . sendText (
166+ `${ config . componentRuntime . executable } ${ componentRuntimeArgs . join ( " " ) } ` ,
167+ true
168+ ) ;
169+ }
170+ }
124171
125- this . _componentRuntime . on ( "close" , ( code ) => {
126- console . info ( `child process exited with code ${ code } ` ) ;
127- this . running = false ;
172+ private static async ensureTerminal ( ) {
173+ if ( this . _terminal && this . _terminal . exitStatus === undefined ) {
174+ return ;
175+ }
176+
177+ let signal = new Signal < void , void > ( ) ;
178+ this . _terminal = window . createTerminal ( ) ;
179+ let terminalCloseDisposable = window . onDidCloseTerminal ( ( terminal ) => {
180+ if ( terminal === this . _terminal ) {
181+ signal . resolve ( ) ;
182+ this . _terminal = undefined ;
183+ this . _runtimeExecution = undefined ;
184+ terminalCloseDisposable . dispose ( ) ;
185+ }
128186 } ) ;
187+
188+ let shellIntegrationDisposable = window . onDidChangeTerminalShellIntegration (
189+ async ( { terminal } ) => {
190+ if ( terminal === this . _terminal ) {
191+ clearTimeout ( timeout ) ;
192+ shellIntegrationDisposable . dispose ( ) ;
193+ signal . resolve ( ) ;
194+ }
195+ }
196+ ) ;
197+ // Fallback to sendText if there is no shell integration within 3 seconds of launching
198+ let timeout = setTimeout ( ( ) => {
199+ shellIntegrationDisposable . dispose ( ) ;
200+ signal . resolve ( ) ;
201+ } , 3000 ) ;
202+
203+ await signal . wait ( ) ;
129204 }
130205}
131206
@@ -158,7 +233,7 @@ export class StarlingMonkeyRuntime extends EventEmitter {
158233 }
159234
160235 public async start ( program : string , component : string , stopOnEntry : boolean , debug : boolean ) : Promise < void > {
161- await this . startComponentRuntime ( component ) ;
236+ await ComponentRuntimeInstance . start ( this . _workspaceDir , component , this . _config ) ;
162237 this . startSessionServer ( ) ;
163238 // TODO: tell StarlingMonkey not to debug if this is false.
164239 this . _debug = debug ;
@@ -169,7 +244,7 @@ export class StarlingMonkeyRuntime extends EventEmitter {
169244 message . type === "connect" ,
170245 `expected "connect" message, got "${ message . type } "`
171246 ) ;
172- this . sendMessage ( "startDebugLogging" ) ;
247+ // this.sendMessage("startDebugLogging");
173248 message = await this . sendAndReceiveMessage ( "loadProgram" , this . _sourceFile ) ;
174249 assert (
175250 message . type === "programLoaded" ,
@@ -247,7 +322,7 @@ export class StarlingMonkeyRuntime extends EventEmitter {
247322 let message = partialMessage . slice ( 0 , expectedLength ) ;
248323 try {
249324 let parsed = JSON . parse ( message ) ;
250- console . debug ( `received message ${ partialMessage } ` ) ;
325+ // console.debug(`received message ${partialMessage}`);
251326 resetMessageState ( ) ;
252327 this . _messageReceived . resolve ( parsed ) ;
253328 } catch ( e ) {
@@ -266,25 +341,14 @@ export class StarlingMonkeyRuntime extends EventEmitter {
266341 ComponentRuntimeInstance . setNextSessionPort ( port ) ;
267342 }
268343
269- private async startComponentRuntime ( component : string ) {
270- if ( ComponentRuntimeInstance . running ) {
271- assert (
272- ComponentRuntimeInstance . workspaceFolder === this . _workspaceDir ,
273- "ComponentRuntime is already running in a different workspace"
274- ) ;
275- return ;
276- }
277- await ComponentRuntimeInstance . start ( this . _workspaceDir , component , this . _config ) ;
278- }
279-
280344 private sendMessage ( type : string , value ?: any , useRawValue = false ) {
281345 let message : string ;
282346 if ( useRawValue ) {
283347 message = `{"type": "${ type } ", "value": ${ value } }` ;
284348 } else {
285349 message = JSON . stringify ( { type, value } ) ;
286350 }
287- console . debug ( `sending message to runtime: ${ message } ` ) ;
351+ // console.debug(`sending message to runtime: ${message}`);
288352 this . _socket . write ( `${ message . length } \n${ message } ` ) ;
289353 }
290354
0 commit comments