@@ -6,6 +6,7 @@ import { setTimeout } from 'timers/promises';
66import Electron from 'electron' ;
77
88import K3sHelper from '@pkg/backend/k3sHelper' ;
9+ import Latch from '@pkg/utils/latch' ;
910import Logging from '@pkg/utils/logging' ;
1011import paths from '@pkg/utils/paths' ;
1112
@@ -16,10 +17,10 @@ const console = Logging.steve;
1617 */
1718export class Steve {
1819 private static instance : Steve ;
19- private process ! : ChildProcess ;
20+ private process : ChildProcess | undefined ;
2021
2122 private isRunning : boolean ;
22- private httpsPort = 0 ;
23+ #port = 0 ;
2324
2425 private constructor ( ) {
2526 this . isRunning = false ;
@@ -40,19 +41,17 @@ export class Steve {
4041 /**
4142 * @description Starts the Steve API if one is not already running.
4243 * Returns only after Steve is ready to accept connections.
43- * @param httpsPort The HTTPS port for Steve to listen on.
44+ * @returns The port Steve is listening on.
4445 */
45- public async start ( httpsPort : number ) {
46+ public async start ( ) : Promise < number > {
4647 const { pid } = this . process || { } ;
4748
4849 if ( this . isRunning && pid ) {
4950 console . debug ( `Steve is already running with pid: ${ pid } ` ) ;
5051
51- return ;
52+ return this . #port ;
5253 }
5354
54- this . httpsPort = httpsPort ;
55-
5655 const osSpecificName = / ^ w i n / i. test ( os . platform ( ) ) ? 'steve.exe' : 'steve' ;
5756 const stevePath = path . join ( paths . resources , os . platform ( ) , 'internal' , osSpecificName ) ;
5857 const env = Object . assign ( { } , process . env ) ;
@@ -62,33 +61,37 @@ export class Steve {
6261 } catch {
6362 // do nothing
6463 }
64+ console . debug ( `Starting Steve with KUBECONFIG=${ env . KUBECONFIG } ` ) ;
6565 this . process = spawn (
6666 stevePath ,
6767 [
6868 '--context' ,
6969 'rancher-desktop' ,
70- '--ui-path' ,
71- path . join ( paths . resources , 'rancher-dashboard' ) ,
7270 '--offline' ,
7371 'true' ,
74- '--https-listen-port' ,
75- String ( httpsPort ) ,
76- '--http-listen-port' ,
77- '0' , // Disable HTTP support; it does not work correctly anyway.
7872 ] ,
79- { env } ,
73+ { env, stdio : [ 'ignore' , 'pipe' , 'pipe' ] , windowsHide : true } ,
8074 ) ;
8175
8276 const { stdout, stderr } = this . process ;
77+ let portBuffer = '' ;
78+ const portLatch = Latch < number > ( ) ;
8379
8480 if ( ! stdout || ! stderr ) {
8581 console . error ( 'Unable to get child process...' ) ;
8682
87- return ;
83+ throw new Error ( `Failed to start Steve: could not get output` ) ;
8884 }
8985
86+ console . debug ( 'Waiting for Steve to output port...' ) ;
9087 stdout . on ( 'data' , ( data : any ) => {
91- console . log ( `stdout: ${ data } ` ) ;
88+ portBuffer += data . toString ( ) ;
89+ } ) ;
90+ stdout . on ( 'end' , ( ) => {
91+ const port = parseInt ( portBuffer , 10 ) ;
92+ if ( port ) {
93+ portLatch . resolve ( port ) ;
94+ }
9295 } ) ;
9396
9497 stderr . on ( 'data' , ( data : any ) => {
@@ -101,23 +104,32 @@ export class Steve {
101104 } ) ;
102105
103106 await new Promise < void > ( ( resolve , reject ) => {
104- this . process . once ( 'spawn' , ( ) => {
107+ this . process ? .once ( 'spawn' , ( ) => {
105108 this . isRunning = true ;
106- console . debug ( `Spawned child pid: ${ this . process . pid } ` ) ;
109+ console . debug ( `Spawned child pid: ${ this . process ? .pid } ` ) ;
107110 resolve ( ) ;
108111 } ) ;
109- this . process . once ( 'error' , ( err ) => {
112+ this . process ? .once ( 'error' , ( err ) => {
110113 reject ( new Error ( `Failed to spawn Steve: ${ err . message } ` , { cause : err } ) ) ;
111114 } ) ;
115+ setTimeout ( 10_000 ) . then ( ( ) => reject ( new Error ( 'Timed out waiting for Steve to start' ) ) ) ;
112116 } ) ;
117+ this . #port = await portLatch ;
118+ console . debug ( `Steve is listening on port: ${ this . #port } ` ) ;
113119
114- await this . waitForReady ( ) ;
120+ await this . waitForReady ( this . #port) ;
121+
122+ return this . #port;
123+ }
124+
125+ public get port ( ) {
126+ return this . #port;
115127 }
116128
117129 /**
118130 * Wait for Steve to be ready to serve API requests.
119131 */
120- private async waitForReady ( ) : Promise < void > {
132+ private async waitForReady ( port : number ) : Promise < void > {
121133 const maxAttempts = 60 ;
122134 const delayMs = 500 ;
123135
@@ -126,7 +138,7 @@ export class Steve {
126138 throw new Error ( 'Steve process exited before becoming ready' ) ;
127139 }
128140
129- if ( await this . isPortReady ( ) ) {
141+ if ( await this . isPortReady ( port ) ) {
130142 console . debug ( `Steve is ready after ${ attempt } / ${ maxAttempts } attempt(s)` ) ;
131143
132144 return ;
@@ -146,55 +158,17 @@ export class Steve {
146158 * we probe a core resource endpoint that returns 404 until the
147159 * schema controller has registered it.
148160 */
149- private isPortReady ( ) : Promise < boolean > {
150- // Steve's HTTP port just redirects to HTTPS, so we might as well go to the
151- // HTTPS port directly. We will need to ignore certificate errors; however,
152- // neither the NodeJS stack nor Electron.net.request() would pass through
153- // the `Electron.app.on('certificate-error', ...)` handler, so we cannot use
154- // the normal certificate handling for this health check. Instead, we
155- // create a temporary session with a certificate verify proc that ignores
156- // errors, and use that session for the health check request.
157- return new Promise ( ( resolve ) => {
158- const session = Electron . session . fromPartition ( 'steve-healthcheck' , { cache : false } ) ;
159-
160- session . setCertificateVerifyProc ( ( request , callback ) => {
161- if ( request . hostname === '127.0.0.1' ) {
162- // We do not have any more information to narrow down the certificate;
163- // given that we're doing this in a private partition, it should be
164- // safe to allow all localhost certificates. In particular, we do not
165- // get access to the port number, and all the Steve certificates have
166- // generic fields (e.g. subject).
167- callback ( 0 ) ;
168- } else {
169- // Unexpected request; not sure how this could happen in a new session,
170- // but we can at least pretend to do the right thing.
171- callback ( - 3 ) ; // Use Chromium's default verification.
172- }
173- } ) ;
174-
175- const req = Electron . net . request ( {
176- protocol : 'https:' ,
177- hostname : '127.0.0.1' ,
178- port : this . httpsPort ,
179- path : '/v1/namespaces' ,
180- method : 'GET' ,
181- redirect : 'error' ,
182- session,
183- } ) ;
184-
185- req . on ( 'response' , ( res ) => resolve ( res . statusCode === 200 ) ) ;
186- req . on ( 'error' , ( ) => resolve ( false ) ) ;
187- // Timeout if we don't get a response in a reasonable time.
188- setTimeout ( 1_000 ) . then ( ( ) => {
189- try {
190- req . abort ( ) ;
191- } catch {
192- // ignore
193- }
194- resolve ( false ) ;
195- } ) ;
196- req . end ( ) ;
197- } ) ;
161+ private async isPortReady ( port : number ) : Promise < boolean > {
162+ try {
163+ // Set up a short time out, so we don't wait too long.
164+ const signal = AbortSignal . timeout ( 1_000 ) ;
165+ const resp = await Electron . net . fetch (
166+ `http://127.0.0.1:${ port } /v1/namespaces` ,
167+ { redirect : 'error' , signal } ) ;
168+ return resp . ok ;
169+ } catch {
170+ return false ;
171+ }
198172 }
199173
200174 /**
@@ -205,6 +179,7 @@ export class Steve {
205179 return ;
206180 }
207181
208- this . process . kill ( 'SIGINT' ) ;
182+ this . process ?. kill ( 'SIGINT' ) ;
183+ this . #port = 0 ;
209184 }
210185}
0 commit comments