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:
parent
ab97087f2a
commit
2f37cb25a0
@ -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);
|
||||
});
|
||||
|
||||
|
||||
@ -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<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}`) });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user