diff --git a/e2e/nx/src/misc.test.ts b/e2e/nx/src/misc.test.ts index ce2c01254f..a9d603dacd 100644 --- a/e2e/nx/src/misc.test.ts +++ b/e2e/nx/src/misc.test.ts @@ -83,7 +83,7 @@ describe('Nx Commands', () => { runCLI(`generate @nx/web:app apps/${app}`); let url: string; let port: number; - const child_process = await runCommandUntil( + const childProcess = await runCommandUntil( `show project ${app} --web --open=false`, (output) => { console.log(output); @@ -102,7 +102,68 @@ describe('Nx Commands', () => { // Check that url is alive const response = await fetch(url); expect(response.status).toEqual(200); - await killProcessAndPorts(child_process.pid, port); + await killProcessAndPorts(childProcess.pid, port); + }, 700000); + + it('should find alternative port when default port is occupied', async () => { + const app = uniq('myapp'); + runCLI(`generate @nx/web:app apps/${app}`); + + const http = require('http'); + + // Create a server that occupies the default port 4211 + const blockingServer = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('blocking server'); + }); + + await new Promise((resolve) => { + blockingServer.listen(4211, '127.0.0.1', () => { + console.log('Blocking server started on port 4211'); + resolve(); + }); + }); + + let url: string; + let port: number; + let foundAlternativePort = false; + + try { + const childProcess = await runCommandUntil( + `show project ${app} --web --open=false`, + (output) => { + console.log(output); + // Should find alternative port and show message about port being in use + if (output.includes('Port 4211 was already in use, using port')) { + foundAlternativePort = true; + } + // output should contain 'Project graph started at http://127.0.0.1:{port}' + if (output.includes('Project graph started at http://')) { + const match = /https?:\/\/[\d.]+:(?\d+)/.exec(output); + if (match) { + port = parseInt(match.groups.port); + url = match[0]; + return true; + } + } + return false; + } + ); + + // Verify that an alternative port was found + expect(foundAlternativePort).toBe(true); + expect(port).not.toBe(4211); + expect(port).toBeGreaterThan(4211); + + // Check that url is alive + const response = await fetch(url); + expect(response.status).toEqual(200); + + await killProcessAndPorts(childProcess.pid, port); + } finally { + // Clean up the blocking server + blockingServer.close(); + } }, 700000); }); diff --git a/packages/nx/src/command-line/graph/graph.ts b/packages/nx/src/command-line/graph/graph.ts index 5e172ab617..0bd1a2f921 100644 --- a/packages/nx/src/command-line/graph/graph.ts +++ b/packages/nx/src/command-line/graph/graph.ts @@ -21,6 +21,7 @@ import { parse, relative, } from 'path'; +import * as net from 'net'; import { performance } from 'perf_hooks'; import { readNxJson, workspaceLayout } from '../../config/configuration'; import { @@ -490,17 +491,29 @@ export async function generateGraph( !!args.file && args.file.endsWith('html') ? 'build' : 'serve' ); - const { app, url } = await startServer( - html, - environmentJs, - args.host || '127.0.0.1', - args.port || 4211, - args.watch, - affectedProjects, - args.focus, - args.groupByFolder, - args.exclude - ); + let app: Server; + let url: URL; + try { + const result = await startServer( + html, + environmentJs, + args.host || '127.0.0.1', + args.port || 4211, + args.watch, + affectedProjects, + args.focus, + args.groupByFolder, + args.exclude + ); + app = result.app; + url = result.url; + } catch (err) { + output.error({ + title: 'Failed to start graph server', + bodyLines: [err.message], + }); + process.exit(1); + } url.pathname = args.view; @@ -539,6 +552,33 @@ export async function generateGraph( } } +function findAvailablePort( + startPort: number, + host: string = '127.0.0.1' +): Promise { + return new Promise((resolve, reject) => { + const server = net.createServer(); + + server.listen(startPort, host, () => { + const port = (server.address() as net.AddressInfo).port; + server.close(() => { + resolve(port); + }); + }); + + server.on('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE') { + // Port is in use, try the next one + findAvailablePort(startPort + 1, host) + .then(resolve) + .catch(reject); + } else { + reject(err); + } + }); + }); +} + async function startServer( html: string, environmentJs: string, @@ -676,9 +716,21 @@ async function startServer( process.on('SIGINT', () => handleTermination(128 + 2)); process.on('SIGTERM', () => handleTermination(128 + 15)); - return new Promise<{ app: Server; url: URL }>((res) => { - app.listen(port, host, () => { - res({ app, url: new URL(`http://${host}:${port}`) }); + // Find an available port starting from the requested port + const availablePort = await findAvailablePort(port, host); + + return new Promise<{ app: Server; url: URL }>((res, rej) => { + app.on('error', (err: NodeJS.ErrnoException) => { + rej(err); + }); + + app.listen(availablePort, host, () => { + if (availablePort !== port) { + output.note({ + title: `Port ${port} was already in use, using port ${availablePort} instead`, + }); + } + res({ app, url: new URL(`http://${host}:${availablePort}`) }); }); }); }