Skip to content

Commit f53495b

Browse files
fix: Add retry logic to che-terminal machine-exec WebSocket connection
Previously, the extension opened a single WebSocket to machine-exec during activation with no retry. On page refresh the connection could fail or close before the server responded, causing silent activation failure and breaking dependent extensions like che-commands. Now init() retries up to 30 times (1s apart), properly handles close/error events, and reports a meaningful error if all attempts are exhausted. Signed-off-by: Roman Nikitenko <rnikiten@redhat.com> Assisted-by: Cursor <cursoragent@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e1d1a1c commit f53495b

1 file changed

Lines changed: 72 additions & 44 deletions

File tree

code/extensions/che-terminal/src/machine-exec-client.ts

Lines changed: 72 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**********************************************************************
2-
* Copyright (c) 2022 Red Hat, Inc.
2+
* Copyright (c) 2022-2026 Red Hat, Inc.
33
*
44
* This program and the accompanying materials are made
55
* available under the terms of the Eclipse Public License 2.0
@@ -20,58 +20,86 @@ import { getOutputChannel } from './extension';
2020
/** Client for the machine-exec server. */
2121
export class MachineExecClient implements vscode.Disposable {
2222

23-
/** WebSocket connection to the machine-exec server. */
24-
private connection: WebSocket;
23+
private static readonly MAX_RETRIES = 30;
24+
private static readonly RETRY_DELAY_MS = 1000;
2525

26-
private initPromise: Promise<void>;
26+
/** WebSocket connection to the machine-exec server. */
27+
private connection: WebSocket | undefined;
2728

2829
private onExitEmitter = new vscode.EventEmitter<TerminalExitEvent>();
2930

3031
private LIST_CONTAINERS_MESSAGE_ID = -5;
3132

32-
constructor() {
33-
let resolveInit: () => void;
34-
let rejectInit: (reason: any) => void;
35-
36-
this.connection = new WebSocket('ws://localhost:3333/connect');
37-
this.connection
38-
.on('message', async (data: WS.Data) => {
39-
// By default, VS Code communicates over WebSocket in a binary format (exchanging data frames).
40-
// For easier debugging, let's log all incoming messages in a text format to the output channel.
41-
getOutputChannel().appendLine(`[WebSocket] <<< ${data.toString()}`);
42-
43-
const message = JSON.parse(data.toString());
44-
if (message.method === 'connected') {
45-
// the machine-exec server responds `connected` once it's ready to serve the clients
46-
resolveInit();
47-
} else if (message.method === 'onExecExit') {
48-
this.onExitEmitter.fire({ sessionId: message.params.id, exitCode: 0 }); // normal exit
49-
} else if (message.method === 'onExecError') {
50-
this.onExitEmitter.fire({ sessionId: message.params.id, exitCode: 1 });// the process failed
33+
/**
34+
* Connects to the machine-exec server with retry logic.
35+
* Resolves once the server sends the `connected` message.
36+
* Rejects if all retry attempts are exhausted.
37+
*/
38+
async init(): Promise<void> {
39+
for (let attempt = 1; attempt <= MachineExecClient.MAX_RETRIES; attempt++) {
40+
try {
41+
await this.tryConnect();
42+
return;
43+
} catch (err: any) {
44+
getOutputChannel().appendLine(`[machine-exec] Connection attempt ${attempt}/${MachineExecClient.MAX_RETRIES} failed: ${err.message}`);
45+
if (attempt === MachineExecClient.MAX_RETRIES) {
46+
throw new Error(`Failed to connect to machine-exec after ${MachineExecClient.MAX_RETRIES} attempts: ${err.message}`);
5147
}
52-
})
53-
.on('error', (err: Error) => {
54-
getOutputChannel().appendLine(`[WebSocket] error: ${err.message}`);
55-
56-
rejectInit(err.message);
57-
});
48+
await new Promise(resolve => setTimeout(resolve, MachineExecClient.RETRY_DELAY_MS));
49+
}
50+
}
51+
}
5852

59-
this.initPromise = new Promise<void>((resolve, reject) => {
60-
resolveInit = resolve;
61-
rejectInit = reject;
53+
private tryConnect(): Promise<void> {
54+
return new Promise<void>((resolve, reject) => {
55+
let settled = false;
56+
57+
const ws = new WebSocket('ws://localhost:3333/connect');
58+
ws
59+
.on('message', async (data: WS.Data) => {
60+
getOutputChannel().appendLine(`[WebSocket] <<< ${data.toString()}`);
61+
62+
const message = JSON.parse(data.toString());
63+
if (message.method === 'connected') {
64+
settled = true;
65+
this.connection = ws;
66+
this.setupMessageHandler(ws);
67+
resolve();
68+
}
69+
})
70+
.on('close', (code: number, reason: Buffer) => {
71+
const msg = reason.toString() || `code ${code}`;
72+
getOutputChannel().appendLine(`[WebSocket] closed: ${msg}`);
73+
if (!settled) {
74+
settled = true;
75+
ws.removeAllListeners();
76+
reject(new Error(`WebSocket closed before ready: ${msg}`));
77+
}
78+
})
79+
.on('error', (err: Error) => {
80+
getOutputChannel().appendLine(`[WebSocket] error: ${err.message}`);
81+
if (!settled) {
82+
settled = true;
83+
ws.removeAllListeners();
84+
reject(new Error(err.message));
85+
}
86+
});
6287
});
6388
}
6489

65-
/**
66-
* Resolves once the machine-exec server is ready to serve the clients.
67-
* Rejects if an error occurred while establishing the WebSocket connection to machine-exec server.
68-
*/
69-
init(): Promise<void> {
70-
return this.initPromise;
90+
private setupMessageHandler(ws: WebSocket): void {
91+
ws.on('message', (data: WS.Data) => {
92+
const message = JSON.parse(data.toString());
93+
if (message.method === 'onExecExit') {
94+
this.onExitEmitter.fire({ sessionId: message.params.id, exitCode: 0 });
95+
} else if (message.method === 'onExecError') {
96+
this.onExitEmitter.fire({ sessionId: message.params.id, exitCode: 1 });
97+
}
98+
});
7199
}
72100

73101
dispose() {
74-
this.connection.terminate();
102+
this.connection?.terminate();
75103
}
76104

77105
/**
@@ -89,10 +117,10 @@ export class MachineExecClient implements vscode.Disposable {
89117

90118
const command = JSON.stringify(jsonCommand);
91119
getOutputChannel().appendLine(`[WebSocket] >>> ${command}`);
92-
this.connection.send(command);
120+
this.connection!.send(command);
93121

94122
return new Promise(resolve => {
95-
this.connection.once('message', (data: WS.Data) => {
123+
this.connection!.once('message', (data: WS.Data) => {
96124
const message = JSON.parse(data.toString());
97125
if (message.id === this.LIST_CONTAINERS_MESSAGE_ID) {
98126
const remoteContainers: string[] = message.result.map((containerInfo: any) => containerInfo.container);
@@ -160,10 +188,10 @@ export class MachineExecClient implements vscode.Disposable {
160188

161189
const command = JSON.stringify(jsonCommand);
162190
getOutputChannel().appendLine(`[WebSocket] >>> ${command}`);
163-
this.connection.send(command);
191+
this.connection!.send(command);
164192

165193
return new Promise(resolve => {
166-
this.connection.once('message', (data: WS.Data) => {
194+
this.connection!.once('message', (data: WS.Data) => {
167195
const message = JSON.parse(data.toString());
168196
const sessionID = message.result;
169197
if (Number.isFinite(sessionID)) {
@@ -196,7 +224,7 @@ export class MachineExecClient implements vscode.Disposable {
196224

197225
const command = JSON.stringify(jsonCommand);
198226
getOutputChannel().appendLine(`[WebSocket] >>> ${command}`);
199-
this.connection.send(command);
227+
this.connection!.send(command);
200228
}
201229

202230
get onExit(): vscode.Event<TerminalExitEvent> {

0 commit comments

Comments
 (0)