@@ -15,6 +15,7 @@ function reportError(error: Error | string) {
1515import { spawn , exec , ChildProcess } from 'child_process' ;
1616import * as os from 'os' ;
1717import { promises as fs } from 'fs'
18+ import * as net from 'net' ;
1819import * as path from 'path' ;
1920import { promisify } from 'util' ;
2021import * as querystring from 'querystring' ;
@@ -311,6 +312,28 @@ if (!amMainInstance) {
311312 }
312313 }
313314
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+
314337 async function startServer ( retries = 2 ) {
315338 const binName = isWindows ? 'httptoolkit-server.cmd' : 'httptoolkit-server' ;
316339 const serverBinPath = path . join ( RESOURCES_PATH , 'httptoolkit-server' , 'bin' , binName ) ;
@@ -399,23 +422,40 @@ if (!amMainInstance) {
399422
400423 reportStartupEvents ( ) ;
401424
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 ( ( ) =>
404449 startServer ( )
405450 ) . catch ( ( err ) => {
406451 console . error ( 'Failed to start server, exiting.' , err ) ;
407452
408453 // Hide immediately, shutdown entirely after a brief pause for Sentry
409454 windows . forEach ( window => window . hide ( ) ) ;
410- setTimeout ( ( ) => process . exit ( 1 ) , 500 ) ;
455+ setTimeout ( ( ) => process . exit ( 3 ) , 500 ) ;
411456 } ) ;
412457
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 ( ( ) => {
419459 Menu . setApplicationMenu ( menu ) ;
420460 createWindow ( ) ;
421461 } ) ;
0 commit comments