diff --git a/packages/plugin/webpack/src/WebpackPlugin.ts b/packages/plugin/webpack/src/WebpackPlugin.ts index 602e462edf..344d86ad5a 100644 --- a/packages/plugin/webpack/src/WebpackPlugin.ts +++ b/packages/plugin/webpack/src/WebpackPlugin.ts @@ -101,7 +101,7 @@ export default class WebpackPlugin extends PluginBase { this.servers = []; for (const logger of this.loggers) { d('stopping logger'); - logger.stop(); + logger.stopPort(); } this.loggers = []; } @@ -569,9 +569,9 @@ the generated files). Instead, it is ${JSON.stringify(pj.main)}`); await fs.remove(this.baseDir); - const logger = new Logger(this.loggerPort); + const logger = new Logger(); this.loggers.push(logger); - await logger.start(); + this.loggerPort = await logger.startPort(this.loggerPort); return { tasks: [ diff --git a/packages/utils/core-utils/src/index.ts b/packages/utils/core-utils/src/index.ts index 2b4f1876ef..40bf4b806a 100644 --- a/packages/utils/core-utils/src/index.ts +++ b/packages/utils/core-utils/src/index.ts @@ -1,3 +1,4 @@ export * from './rebuild'; export * from './electron-version'; export * from './yarn-or-npm'; +export * from './port'; diff --git a/packages/utils/core-utils/src/port.ts b/packages/utils/core-utils/src/port.ts new file mode 100644 index 0000000000..2e6e2abad7 --- /dev/null +++ b/packages/utils/core-utils/src/port.ts @@ -0,0 +1,39 @@ +import http from 'http'; + +/** + * Check if a port is occupied. + * @returns boolean promise that resolves to true if the port is available, false otherwise. + */ +export const portOccupied = async (port: number): Promise => { + return new Promise((resolve, reject) => { + const server = http.createServer().listen(port); + server.on('listening', () => { + server.close(); + server.on('close', () => { + resolve(true); + }); + }); + + server.on('error', (_err) => { + reject(false); + }); + }); +}; + +/** + * Find an available port for web UI. + * @returns the port number. + */ +export const findAvailablePort = async (initialPort: number): Promise => { + const maxPort = initialPort + 10; + + for (let p = initialPort; p <= maxPort; p++) { + try { + await portOccupied(p); + return p; + } catch (_err) { + // Pass + } + } + throw new Error(`Could not find an available port between ${initialPort} and ${maxPort}. Please free up a port and try again.`); +}; diff --git a/packages/utils/core-utils/test/port_spec.ts b/packages/utils/core-utils/test/port_spec.ts new file mode 100644 index 0000000000..67285544be --- /dev/null +++ b/packages/utils/core-utils/test/port_spec.ts @@ -0,0 +1,68 @@ +import net from 'node:net'; + +import { expect } from 'chai'; + +import { findAvailablePort, portOccupied } from '../src/port'; + +const usePort = (port: number) => { + const server = net.createServer(); + server.on('error', () => { + // pass + }); + server.listen(port); + return () => { + server.close(); + }; +}; + +const usePorts = (port: number, endPort: number) => { + const releases: ReturnType[] = []; + for (let i = port; i <= endPort; i++) { + releases.push(usePort(i)); + } + return () => { + releases.forEach((release) => release()); + }; +}; + +describe('Port tests', () => { + describe('portOccupied', () => { + it('should resolve to true if the port is available', async () => { + const port = 49152; + const result = await portOccupied(port); + expect(result).to.not.throw; + }); + it('should reject if the port is occupied', async () => { + const port = 48143; + const releasePort = usePort(port); + try { + await portOccupied(port); + } catch (error) { + expect(error).to.equal(false); + } finally { + releasePort(); + } + }); + }); + + describe('findAvailablePort', () => { + it('should find an available port', async () => { + const initialPort = 51155; + const port = await findAvailablePort(initialPort); + expect(port).gte(initialPort); + }); + it('should throw an error if no available port is found', async () => { + const initialPort = 53024; + const releasePort = usePorts(initialPort, initialPort + 10); + try { + await findAvailablePort(initialPort); + } catch (error) { + expect((error as Error).message).to.equal( + `Could not find an available port between ${initialPort} and ${initialPort + 10}. Please free up a port and try again.` + ); + } finally { + releasePort(); + } + }); + }); +}); diff --git a/packages/utils/web-multi-logger/package.json b/packages/utils/web-multi-logger/package.json index 94fdbf8bb1..22c25ab90d 100644 --- a/packages/utils/web-multi-logger/package.json +++ b/packages/utils/web-multi-logger/package.json @@ -8,6 +8,7 @@ "main": "dist/Logger.js", "typings": "dist/Logger.d.ts", "dependencies": { + "@electron-forge/core-utils": "7.4.0", "express": "^4.17.1", "express-ws": "^5.0.2", "xterm": "^4.9.0", diff --git a/packages/utils/web-multi-logger/src/Logger.ts b/packages/utils/web-multi-logger/src/Logger.ts index f9633e2759..126f4dbe50 100644 --- a/packages/utils/web-multi-logger/src/Logger.ts +++ b/packages/utils/web-multi-logger/src/Logger.ts @@ -1,6 +1,7 @@ import http from 'http'; import path from 'path'; +import { findAvailablePort } from '@electron-forge/core-utils'; import express from 'express'; import ews from 'express-ws'; @@ -33,7 +34,6 @@ export default class Logger { // I assume this endpoint is just a no-op needed for some reason. }); } - /** * Creates a new tab with the given name, the name should be human readable * it will be used as the tab title in the front end. @@ -49,16 +49,16 @@ export default class Logger { * * @returns the port number */ - start(): Promise { - return new Promise((resolve) => { - this.server = this.app.listen(this.port, () => resolve(this.port)); - }); + async startPort(loggerPort: number): Promise { + loggerPort = await findAvailablePort(loggerPort); + this.server = this.app.listen(loggerPort); + return loggerPort; } /** * Stop the HTTP server hosting the web UI */ - stop(): void { + stopPort(): void { if (this.server) this.server.close(); } }