fix(core): use next available port when the port for nx graph is in use (#31365)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->
Command would fail silently with no error message
## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Rather than erroring, Nx will find the next available port and use that.

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #30915
This commit is contained in:
Jason Jean 2025-05-29 09:47:49 -04:00 committed by GitHub
parent ab97087f2a
commit 2f37cb25a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 129 additions and 16 deletions

View File

@ -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<void>((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.]+:(?<port>\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);
});

View File

@ -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,7 +491,10 @@ export async function generateGraph(
!!args.file && args.file.endsWith('html') ? 'build' : 'serve'
);
const { app, url } = await startServer(
let app: Server;
let url: URL;
try {
const result = await startServer(
html,
environmentJs,
args.host || '127.0.0.1',
@ -501,6 +505,15 @@ export async function generateGraph(
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<number> {
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}`) });
});
});
}