@@ -22,10 +22,11 @@ import { getDeferred } from '@httptoolkit/util';
2222
2323import { getMenu , shouldAutoHideMenu } from './menu.ts' ;
2424import { ContextMenuDefinition , openContextMenu } from './context-menu.ts' ;
25+ import getPort from 'get-port' ;
26+
2527import { stopServer } from './stop-server.ts' ;
2628import { shouldClearStaleUICache , clearUICache , recordUIRun } from './cache-cleanup.ts' ;
2729import { getDeviceDetails } from './device.ts' ;
28- import { SERVER_PORTS , checkPortsInUse , checkWindowsReservedPorts } from './port-checks.ts' ;
2930
3031import packageJson from '../package.json' with { type : 'json' } ;
3132
@@ -54,6 +55,23 @@ let windows: Electron.BrowserWindow[] = [];
5455
5556let server : ChildProcess | null = null ;
5657
58+ interface ServerPorts {
59+ serverPort : number ;
60+ mockttpPort : number ;
61+ }
62+
63+ // Pick two random available ports in parallel. We deliberately don't constrain
64+ // to a fixed range: if there's a conflict or race, restarting the app picks
65+ // fresh ports and is likely to succeed.
66+ const pickServerPorts = async ( ) : Promise < ServerPorts > => {
67+ const [ serverPort , mockttpPort ] = await Promise . all ( [ getPort ( ) , getPort ( ) ] ) ;
68+ return { serverPort, mockttpPort } ;
69+ } ;
70+
71+ // Resolved before any window or server is started, to inject into the renderer
72+ // synchronously via additionalArguments and into the server via CLI flags.
73+ let serverPorts : ServerPorts ;
74+
5775app . commandLine . appendSwitch ( 'ignore-connections-limit' , 'app.httptoolkit.tech' ) ;
5876app . commandLine . appendSwitch ( 'disable-renderer-backgrounding' ) ;
5977app . commandLine . appendSwitch ( 'js-flags' , [
@@ -86,7 +104,15 @@ const createWindow = () => {
86104 webPreferences : {
87105 preload : path . join ( import . meta. dirname , 'preload.cjs' ) ,
88106 contextIsolation : true ,
89- nodeIntegration : false
107+ nodeIntegration : false ,
108+ // Pass startup-time values into the preload synchronously, so the
109+ // UI can read them via desktopApi getters without awaiting IPC.
110+ additionalArguments : [
111+ `--htk-desktop-version=${ DESKTOP_VERSION } ` ,
112+ `--htk-server-auth-token=${ AUTH_TOKEN } ` ,
113+ `--htk-server-port=${ serverPorts . serverPort } ` ,
114+ `--htk-mockttp-port=${ serverPorts . mockttpPort } `
115+ ]
90116 } ,
91117
92118 show : false
@@ -153,7 +179,12 @@ const writeLog = (message: string) => {
153179 stream . write ( message + '\n' ) ;
154180}
155181
156- const openNewWindow = ( ) => appReady . promise . then ( ( ) => createWindow ( ) ) ;
182+ // Resolved once serverPorts is populated, after the main-instance branch
183+ // initialises it. createWindow reads serverPorts to seed additionalArguments.
184+ const portsResolved = getDeferred < ServerPorts > ( ) ;
185+
186+ const openNewWindow = ( ) => Promise . all ( [ appReady . promise , portsResolved . promise ] )
187+ . then ( ( ) => createWindow ( ) ) ;
157188
158189const amMainInstance = app . requestSingleInstanceLock ( ) ;
159190if ( ! amMainInstance ) {
@@ -176,7 +207,7 @@ if (!amMainInstance) {
176207 serverKilled = true ;
177208
178209 try {
179- await stopServer ( server , AUTH_TOKEN ) ;
210+ await stopServer ( server , AUTH_TOKEN , serverPorts . serverPort ) ;
180211 } catch ( error ) {
181212 console . log ( 'Failed to kill server' , error ) ;
182213 logError ( error ) ;
@@ -413,7 +444,13 @@ if (!amMainInstance) {
413444 ] . join ( ' ' )
414445 }
415446
416- server = spawn ( serverBinCommand , [ 'start' ] , {
447+ const serverArgs = [
448+ 'start' ,
449+ '--server-port' , String ( serverPorts . serverPort ) ,
450+ '--mockttp-port' , String ( serverPorts . mockttpPort )
451+ ] ;
452+
453+ server = spawn ( serverBinCommand , serverArgs , {
417454 windowsHide : true ,
418455 stdio : [ 'inherit' , 'pipe' , 'pipe' ] ,
419456 shell : isWindows , // Required to spawn a .cmd script
@@ -526,47 +563,18 @@ if (!amMainInstance) {
526563 process . env . COMSPEC = path . join ( process . env . SystemRoot || 'C:\\Windows' , 'System32' , 'cmd.exe' ) ;
527564 }
528565
529- // Check if our required ports are already in use by another process
530- const portsInUseCheck = checkPortsInUse ( '127.0.0.1' , [ ...SERVER_PORTS ] )
531- . then ( async ( portsInUse ) => {
532- if ( portsInUse . length === 0 ) return ;
533- if ( DEV_MODE ) return ; // In full dev mode this is OK & expected
534-
535- await appReady . promise ;
536-
537- const portList = portsInUse . join ( ', ' ) ;
538- showErrorAlert (
539- "HTTP Toolkit could not start" ,
540- `HTTP Toolkit's required port${ portsInUse . length > 1 ? 's' : '' } (${ portList } ) ` +
541- `${ portsInUse . length > 1 ? 'are' : 'is' } already in use.\n\n` +
542- "Do you have another HTTP Toolkit process running somewhere?\n" +
543- "Please close the other process using this port, and try again.\n\n" +
544- "(Having trouble? File an issue at github.com/httptoolkit/httptoolkit)"
545- ) ;
546-
547- process . exit ( 2 ) ;
548- } ) ;
549-
550- // On Windows, check if Hyper-V/WSL has reserved our ports (separate from 'in use' above)
551- const reservedPortCheck = checkWindowsReservedPorts ( [ ...SERVER_PORTS ] )
552- . then ( async ( reservedPorts ) => {
553- if ( reservedPorts . length === 0 ) return ;
554- if ( DEV_MODE ) return ;
555-
556- await appReady . promise ;
557-
558- const portList = reservedPorts . join ( ', ' ) ;
559- await showErrorAlert (
560- "HTTP Toolkit could not start" ,
561- `HTTP Toolkit's required port${ reservedPorts . length > 1 ? 's' : '' } (${ portList } ) ` +
562- `${ reservedPorts . length > 1 ? 'have' : 'has' } been reserved by Windows.\n\n` +
563- "This is usually caused by Hyper-V or WSL reserving ports for its own use. " +
564- "This can be fixed by adjusting your Windows network configuration." ,
565- "https://httptoolkit.com/docs/guides/troubleshooting/#http-toolkit-conflicts-with-hyper-v"
566- ) ;
567-
568- process . exit ( 2 ) ;
569- } ) ;
566+ // Pick random available ports for the server & mockttp admin API. Random
567+ // (rather than a fixed range) means that a transient conflict or race is
568+ // self-healing: restarting the app will likely pick a different pair.
569+ // In DEV_MODE we use the historic defaults, so a separately-launched dev
570+ // server stays reachable.
571+ ( DEV_MODE
572+ ? Promise . resolve ( { serverPort : 45457 , mockttpPort : 45456 } )
573+ : pickServerPorts ( )
574+ ) . then ( ( ports ) => {
575+ serverPorts = ports ;
576+ portsResolved . resolve ( ports ) ;
577+ } , portsResolved . reject ) ;
570578
571579 // Check we're happy using the default proxy settings
572580 getSystemProxy ( )
@@ -623,8 +631,7 @@ if (!amMainInstance) {
623631
624632 Promise . all ( [
625633 cleanupOldServers ( ) . catch ( console . log ) ,
626- portsInUseCheck ,
627- reservedPortCheck
634+ portsResolved . promise
628635 ] ) . then ( ( ) =>
629636 startServer ( )
630637 ) . catch ( ( err ) => {
@@ -667,7 +674,7 @@ if (!amMainInstance) {
667674 } ) ;
668675 }
669676
670- Promise . all ( [ appReady . promise , portsInUseCheck , reservedPortCheck , uiCacheReady ] ) . then ( ( ) => {
677+ Promise . all ( [ appReady . promise , portsResolved . promise , uiCacheReady ] ) . then ( ( ) => {
671678 Menu . setApplicationMenu ( getMenu ( windows , openNewWindow ) ) ;
672679 createWindow ( ) ;
673680 } ) ;
@@ -750,8 +757,6 @@ ipcMain.handle('open-context-menu', ipcHandler((options: ContextMenuDefinition)
750757 openContextMenu ( options )
751758) ) ;
752759
753- ipcMain . handle ( 'get-desktop-version' , ipcHandler ( ( ) => DESKTOP_VERSION ) ) ;
754- ipcMain . handle ( 'get-server-auth-token' , ipcHandler ( ( ) => AUTH_TOKEN ) ) ;
755760ipcMain . handle ( 'get-device-info' , ipcHandler ( ( ) => getDeviceDetails ( ) ) ) ;
756761
757762ipcMain . handle ( 'set-component-versions' , ipcHandler ( ( versions : Record < string , string > ) => {
0 commit comments