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. */
2121export 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