Skip to content

Commit 227836f

Browse files
committed
Fix port conflicts by distributing custom server & mockttp configuration
1 parent e6b082d commit 227836f

7 files changed

Lines changed: 107 additions & 171 deletions

File tree

package-lock.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
"electron-context-menu": "^3.5.0",
159159
"electron-store": "^11.0.1",
160160
"electron-window-state": "^5.0.3",
161+
"get-port": "^7.2.0",
161162
"os-proxy-config": "^1.1.1",
162163
"semver": "^7.2.1",
163164
"tslib": "^2.8.1",

src/cache-cleanup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ interface LastRunData {
2020
timestamp: number;
2121
}
2222

23-
// Read previous run record, or null if we've never written one (first run, or a pre-v1.27 install).
23+
// Read previous run record, or null if we've never written one (first run, or older install).
2424
const readLastRun = async (filePath: string): Promise<LastRunData | null> => {
2525
let content: string;
2626
try {
@@ -42,7 +42,7 @@ const readLastRun = async (filePath: string): Promise<LastRunData | null> => {
4242
}
4343
};
4444

45-
// The window-state.json mtime, or null if absent. Used as a last-used fallback for pre-v1.27 installs,
45+
// The window-state.json mtime, or null if absent. Used as a last-used fallback for older installs,
4646
// which have no run record of their own but do leave this behind on every window move/resize/close.
4747
const readWindowStateMtime = async (userDataPath: string): Promise<number | null> => {
4848
try {

src/index.ts

Lines changed: 56 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ import { getDeferred } from '@httptoolkit/util';
2222

2323
import { getMenu, shouldAutoHideMenu } from './menu.ts';
2424
import { ContextMenuDefinition, openContextMenu } from './context-menu.ts';
25+
import getPort from 'get-port';
26+
2527
import { stopServer } from './stop-server.ts';
2628
import { shouldClearStaleUICache, clearUICache, recordUIRun } from './cache-cleanup.ts';
2729
import { getDeviceDetails } from './device.ts';
28-
import { SERVER_PORTS, checkPortsInUse, checkWindowsReservedPorts } from './port-checks.ts';
2930

3031
import packageJson from '../package.json' with { type: 'json' };
3132

@@ -54,6 +55,23 @@ let windows: Electron.BrowserWindow[] = [];
5455

5556
let 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+
5775
app.commandLine.appendSwitch('ignore-connections-limit', 'app.httptoolkit.tech');
5876
app.commandLine.appendSwitch('disable-renderer-backgrounding');
5977
app.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

158189
const amMainInstance = app.requestSingleInstanceLock();
159190
if (!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));
755760
ipcMain.handle('get-device-info', ipcHandler(() => getDeviceDetails()));
756761

757762
ipcMain.handle('set-component-versions', ipcHandler((versions: Record<string, string>) => {

src/port-checks.ts

Lines changed: 0 additions & 94 deletions
This file was deleted.

0 commit comments

Comments
 (0)