Skip to content

Commit 3b7954f

Browse files
committed
Make handling of component host runtime and debugging server more robust
1 parent 44471f6 commit 3b7954f

1 file changed

Lines changed: 106 additions & 42 deletions

File tree

debugger/vscode-dap-extension/src/starlingMonkeyRuntime.ts

Lines changed: 106 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Scope } from "@vscode/debugadapter";
2-
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
32
import { EventEmitter } from "events";
43
import * as Net from "net";
54
import { Signal } from "./signals.js";
65
import { assert } from "console";
6+
import { Terminal, TerminalShellExecution, window } from "vscode";
77

88
export interface FileAccessor {
99
isWindows: boolean;
@@ -54,21 +54,45 @@ export interface IStarlingMonkeyRuntimeConfig {
5454
}
5555

5656
class 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

Comments
 (0)