Skip to content

Commit 3e1df38

Browse files
committed
Check server port before app startup
This has two main benefits: * Nicer clearer errors when the port is in use * Clearer error reporting, as we don't get a 403 from the UI or extra desktop startup failure noise.
1 parent fb91e04 commit 3e1df38

File tree

1 file changed

+49
-9
lines changed

1 file changed

+49
-9
lines changed

src/index.ts

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ function reportError(error: Error | string) {
1515
import { spawn, exec, ChildProcess } from 'child_process';
1616
import * as os from 'os';
1717
import { promises as fs } from 'fs'
18+
import * as net from 'net';
1819
import * as path from 'path';
1920
import { promisify } from 'util';
2021
import * 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

Comments
 (0)