11import { ChildProcess , spawn } from 'child_process' ;
2- import net from 'net' ;
32import os from 'os' ;
43import path from 'path' ;
54import { setTimeout } from 'timers/promises' ;
65
6+ import Electron from 'electron' ;
7+
78import K3sHelper from '@pkg/backend/k3sHelper' ;
89import Logging from '@pkg/utils/logging' ;
910import paths from '@pkg/utils/paths' ;
1011
11- const STEVE_PORT = 9443 ;
12-
1312const console = Logging . steve ;
1413
1514/**
16- * @description Singleton that manages the lifecycle of the Steve API
15+ * @description Singleton that manages the lifecycle of the Steve API.
1716 */
1817export class Steve {
1918 private static instance : Steve ;
2019 private process ! : ChildProcess ;
2120
2221 private isRunning : boolean ;
22+ private httpsPort = 0 ;
2323
2424 private constructor ( ) {
2525 this . isRunning = false ;
@@ -40,8 +40,9 @@ export class Steve {
4040 /**
4141 * @description Starts the Steve API if one is not already running.
4242 * Returns only after Steve is ready to accept connections.
43+ * @param httpsPort The HTTPS port for Steve to listen on.
4344 */
44- public async start ( ) {
45+ public async start ( httpsPort : number ) {
4546 const { pid } = this . process || { } ;
4647
4748 if ( this . isRunning && pid ) {
@@ -50,6 +51,8 @@ export class Steve {
5051 return ;
5152 }
5253
54+ this . httpsPort = httpsPort ;
55+
5356 const osSpecificName = / ^ w i n / i. test ( os . platform ( ) ) ? 'steve.exe' : 'steve' ;
5457 const stevePath = path . join ( paths . resources , os . platform ( ) , 'internal' , osSpecificName ) ;
5558 const env = Object . assign ( { } , process . env ) ;
@@ -68,6 +71,10 @@ export class Steve {
6871 path . join ( paths . resources , 'rancher-dashboard' ) ,
6972 '--offline' ,
7073 'true' ,
74+ '--https-listen-port' ,
75+ String ( httpsPort ) ,
76+ '--http-listen-port' ,
77+ '0' , // Disable HTTP support; it does not work correctly anyway.
7178 ] ,
7279 { env } ,
7380 ) ;
@@ -93,26 +100,22 @@ export class Steve {
93100 this . isRunning = false ;
94101 } ) ;
95102
96- try {
97- await new Promise < void > ( ( resolve , reject ) => {
98- this . process . once ( 'spawn' , ( ) => {
99- this . isRunning = true ;
100- console . debug ( `Spawned child pid: ${ this . process . pid } ` ) ;
101- resolve ( ) ;
102- } ) ;
103- this . process . once ( 'error' , ( err ) => {
104- reject ( new Error ( `Failed to spawn Steve: ${ err . message } ` , { cause : err } ) ) ;
105- } ) ;
103+ await new Promise < void > ( ( resolve , reject ) => {
104+ this . process . once ( 'spawn' , ( ) => {
105+ this . isRunning = true ;
106+ console . debug ( `Spawned child pid: ${ this . process . pid } ` ) ;
107+ resolve ( ) ;
108+ } ) ;
109+ this . process . once ( 'error' , ( err ) => {
110+ reject ( new Error ( `Failed to spawn Steve: ${ err . message } ` , { cause : err } ) ) ;
106111 } ) ;
112+ } ) ;
107113
108- await this . waitForReady ( ) ;
109- } catch ( ex ) {
110- console . error ( ex ) ;
111- }
114+ await this . waitForReady ( ) ;
112115 }
113116
114117 /**
115- * Wait for Steve to be ready to accept connections .
118+ * Wait for Steve to be ready to serve API requests .
116119 */
117120 private async waitForReady ( ) : Promise < void > {
118121 const maxAttempts = 60 ;
@@ -124,7 +127,7 @@ export class Steve {
124127 }
125128
126129 if ( await this . isPortReady ( ) ) {
127- console . debug ( `Steve is ready after ${ attempt } attempt(s)` ) ;
130+ console . debug ( `Steve is ready after ${ attempt } / ${ maxAttempts } attempt(s)` ) ;
128131
129132 return ;
130133 }
@@ -136,26 +139,61 @@ export class Steve {
136139 }
137140
138141 /**
139- * Check if Steve is accepting connections on its port.
142+ * Check if Steve has finished initializing its API controllers.
143+ * Steve accepts HTTP connections and responds to /v1 before its
144+ * controllers have discovered all resource schemas from the K8s
145+ * API server. The dashboard fails if schemas are incomplete, so
146+ * we probe a core resource endpoint that returns 404 until the
147+ * schema controller has registered it.
140148 */
141149 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.
142157 return new Promise ( ( resolve ) => {
143- const socket = new net . Socket ( ) ;
144-
145- socket . setTimeout ( 1000 ) ;
146- socket . once ( 'connect' , ( ) => {
147- socket . destroy ( ) ;
148- resolve ( true ) ;
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+ }
149173 } ) ;
150- socket . once ( 'error' , ( ) => {
151- socket . destroy ( ) ;
152- resolve ( false ) ;
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,
153183 } ) ;
154- socket . once ( 'timeout' , ( ) => {
155- socket . destroy ( ) ;
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+ }
156194 resolve ( false ) ;
157195 } ) ;
158- socket . connect ( STEVE_PORT , '127.0.0.1' ) ;
196+ req . end ( ) ;
159197 } ) ;
160198 }
161199
0 commit comments