@@ -15,6 +15,7 @@ function reportError(error: Error | string) {
15
15
import { spawn , exec , ChildProcess } from 'child_process' ;
16
16
import * as os from 'os' ;
17
17
import { promises as fs } from 'fs'
18
+ import * as net from 'net' ;
18
19
import * as path from 'path' ;
19
20
import { promisify } from 'util' ;
20
21
import * as querystring from 'querystring' ;
@@ -311,6 +312,28 @@ if (!amMainInstance) {
311
312
}
312
313
}
313
314
315
+ // When run *before* the server starts, this allows us to check whether the port is already in use,
316
+ // so we can provide clear setup instructions and avoid confusing errors later.
317
+ function checkServerPortAvailable ( host : string , port : number ) : Promise < void > {
318
+ const conn = net . connect ( { host, port } ) ;
319
+
320
+ return Promise . race ( [
321
+ new Promise < void > ( ( resolve , reject ) => {
322
+ // If we can already connect to the local port, then it's not available for our server:
323
+ conn . on ( 'connect' , ( ) =>
324
+ reject ( new Error ( `Port ${ port } is already in use` ) )
325
+ ) ;
326
+ // If we fail to connect to the port, it's probably available:
327
+ conn . on ( 'error' , resolve ) ;
328
+ } ) ,
329
+ // After 100 ms with no connection, assume the port is available:
330
+ new Promise < void > ( ( resolve ) => setTimeout ( resolve , 100 ) )
331
+ ] )
332
+ . finally ( ( ) => {
333
+ conn . destroy ( ) ;
334
+ } ) ;
335
+ }
336
+
314
337
async function startServer ( retries = 2 ) {
315
338
const binName = isWindows ? 'httptoolkit-server.cmd' : 'httptoolkit-server' ;
316
339
const serverBinPath = path . join ( RESOURCES_PATH , 'httptoolkit-server' , 'bin' , binName ) ;
@@ -399,23 +422,40 @@ if (!amMainInstance) {
399
422
400
423
reportStartupEvents ( ) ;
401
424
402
- cleanupOldServers ( ) . catch ( console . log )
403
- . then ( ( ) =>
425
+ // Use a promise to organize events around 'ready', and ensure they never
426
+ // fire before, as Electron will refuse to do various things if they do.
427
+ const appReady = getDeferred ( ) ;
428
+ app . on ( 'ready' , ( ) => appReady . resolve ( ) ) ;
429
+
430
+ const portCheck = checkServerPortAvailable ( '127.0.0.1' , 45457 )
431
+ . catch ( async ( ) => {
432
+ await appReady . promise ;
433
+
434
+ showErrorAlert (
435
+ "HTTP Toolkit could not start" ,
436
+ "HTTP Toolkit's local management port (45457) is already in use.\n\n" +
437
+ "Do you have another HTTP Toolkit process running somewhere?\n" +
438
+ "Please close the other process using this port, and try again.\n\n" +
439
+ "(Having trouble? File an issue at github.com/httptoolkit/httptoolkit)"
440
+ ) ;
441
+
442
+ process . exit ( 2 ) ;
443
+ } ) ;
444
+
445
+ Promise . all ( [
446
+ cleanupOldServers ( ) . catch ( console . log ) ,
447
+ portCheck
448
+ ] ) . then ( ( ) =>
404
449
startServer ( )
405
450
) . catch ( ( err ) => {
406
451
console . error ( 'Failed to start server, exiting.' , err ) ;
407
452
408
453
// Hide immediately, shutdown entirely after a brief pause for Sentry
409
454
windows . forEach ( window => window . hide ( ) ) ;
410
- setTimeout ( ( ) => process . exit ( 1 ) , 500 ) ;
455
+ setTimeout ( ( ) => process . exit ( 3 ) , 500 ) ;
411
456
} ) ;
412
457
413
- // Use a promise to organize events around 'ready', and ensure they never
414
- // fire before, as Electron will refuse to do various things if they do.
415
- const appReady = getDeferred ( ) ;
416
- app . on ( 'ready' , ( ) => appReady . resolve ( ) ) ;
417
-
418
- appReady . promise . then ( ( ) => {
458
+ Promise . all ( [ appReady . promise , portCheck ] ) . then ( ( ) => {
419
459
Menu . setApplicationMenu ( menu ) ;
420
460
createWindow ( ) ;
421
461
} ) ;
0 commit comments