detect EADDRINUSE on server start (#1348)

* detect `EADDRINUSE` on server start

* @ts-expect-error

* send portError to electron

* use escalateErrorFn

* dont move in docker

* allow errors to be sendt to electron UI

* Log after the new port is found

* Update comment

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>

* update comment

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>

* update comment

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>

* rename to escalate

* extract `serverTryDesiredPort`

* type check server.address()

* request shutdown on `unrecoverable` `escalateError`

* combine network utils

* reject promise

* not unrecoverable by default

* unneeded assignment

---------

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
This commit is contained in:
Alex Christoffer Rasmussen
2024-12-22 16:23:47 +01:00
committed by GitHub
parent 1af394cd49
commit 29243e3f6c
6 changed files with 121 additions and 54 deletions
+81
View File
@@ -0,0 +1,81 @@
import { LogOrigin } from 'ontime-types';
import { logger } from '../classes/Logger.js';
import { isDocker } from '../externals.js';
import http from 'http';
import { networkInterfaces } from 'os';
/**
* @description Gets information on IPV4 non-internal interfaces
* @returns {array} - Array of objects {name: ip}
*/
export function getNetworkInterfaces(): { name: string; address: string }[] {
const nets = networkInterfaces();
const results: { name: string; address: string }[] = [];
for (const name of Object.keys(nets)) {
const netObjects = nets[name];
if (!netObjects) {
continue;
}
for (const net of netObjects) {
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
if (net.family === 'IPv4' && !net.internal) {
results.push({
name,
address: net.address,
});
}
}
}
return results;
}
/**
* @description tries to open the server with the desired port, and if getting a `EADDRINUSE` will change to an random port assigned by the OS
* @param {http.Server}server http server object
* @param {number}desiredPort the desired port
* @returns {number} the resulting port number
* @throws any other server errors will result in a throw
*/
export async function serverTryDesiredPort(server: http.Server, desiredPort: number): Promise<number> {
return new Promise((resolve, reject) => {
server.once('error', (e) => {
if (isDocker) reject(e); // we should only move ports if we are in a desktop environment
if (testForPortInUser(e)) {
server.listen(0, '0.0.0.0', () => {
const address = server.address();
if (typeof address !== 'object') {
reject('unknown port type, can not proceed');
return; // the return is needed here to let TS know that we wont continue
}
logger.error(
LogOrigin.Server,
`Failed open the desired port: ${desiredPort} \nMoved to an Ephemeral port: ${address.port}`,
true,
);
resolve(address.port);
});
} else {
reject(e);
}
});
server.listen(desiredPort, '0.0.0.0', () => {
const address = server.address();
if (typeof address !== 'object') {
reject('unknown port type, can not proceed');
return; // the return is needed here to let TS know that we wont continue
}
resolve(address.port);
});
});
}
function testForPortInUser(err: unknown) {
if (typeof err === 'object' && 'code' in err && err.code === 'EADDRINUSE') {
return true;
}
return false;
}
@@ -1,28 +0,0 @@
import { networkInterfaces } from 'os';
/**
* @description Gets information on IPV4 non-internal interfaces
* @returns {array} - Array of objects {name: ip}
*/
export function getNetworkInterfaces(): { name: string; address: string }[] {
const nets = networkInterfaces();
const results: { name: string; address: string }[] = [];
for (const name of Object.keys(nets)) {
const netObjects = nets[name];
if (!netObjects) {
continue;
}
for (const net of netObjects) {
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
if (net.family === 'IPv4' && !net.internal) {
results.push({
name,
address: net.address,
});
}
}
}
return results;
}