mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 18:33:53 +00:00
1849b4d39f
* chore: migrate eslint to oxlint * chore: migrate prettier to oxfmt * chore: migrate typescript * chore: toThrow should have a expected value * chore: cast test value as Day * chore: small title fix * chore: mocks should be hoisted * chore: incorrect async useage * chore: test should be inside description * chore: test sohuld include an expeced * chore: oxfmt --------- Co-authored-by: alex-Arc <omnivox@LAPTOP-RC5SNBVV.localdomain>
142 lines
4.5 KiB
TypeScript
142 lines
4.5 KiB
TypeScript
import { Server } from 'http';
|
|
|
|
import { LogOrigin, MaybeNumber } from 'ontime-types';
|
|
|
|
import * as appState from '../../services/app-state-service/AppStateService.js';
|
|
import { config } from '../../setup/config.js';
|
|
import { envPort, isDocker, isOntimeCloud } from '../../setup/environment.js';
|
|
import { shouldCrashDev } from '../../utils/development.js';
|
|
import { logger } from '../Logger.js';
|
|
import { isAddressInfo, isPortInUseError } from './PortManager.utils.js';
|
|
|
|
class PortManager {
|
|
private static port: number;
|
|
private static pendingRestart = false;
|
|
private static newPort: MaybeNumber = null;
|
|
|
|
public getPort() {
|
|
return {
|
|
port: PortManager.port,
|
|
pendingRestart: PortManager.pendingRestart,
|
|
newPort: PortManager.newPort,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* marks that a port change is requested and will be applied on next restart
|
|
* @throws if trying to change port inside docker
|
|
* @param newPort
|
|
* @returns {void}
|
|
*/
|
|
public changePort(newPort: number): void {
|
|
if (isDocker) throw new Error('Can not change port when running inside docker');
|
|
if (PortManager.port === newPort) return;
|
|
PortManager.newPort = newPort;
|
|
PortManager.pendingRestart = true;
|
|
}
|
|
|
|
public migratePortFromProjectFile(port: number) {
|
|
shouldCrashDev(
|
|
PortManager.port !== undefined,
|
|
'this function should not be called after `PortManager.port` has been initialized',
|
|
);
|
|
appState.setServerPort(port);
|
|
}
|
|
|
|
public async shutdown() {
|
|
if (PortManager.pendingRestart && PortManager.newPort != null) {
|
|
logger.info(
|
|
LogOrigin.Server,
|
|
`A port change to ${PortManager.newPort} is pending and will take effect on next start`,
|
|
);
|
|
await appState.setServerPort(PortManager.newPort);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @description tries to open the server with the desired port, and if getting a `EADDRINUSE` will change to a random port assigned by the OS
|
|
* @param {http.Server} server http server object
|
|
* @returns {Promise<number>} the resulting port number
|
|
* @throws any other server errors will result in a throw
|
|
*/
|
|
public async attachServer(server: Server): Promise<number> {
|
|
if (isOntimeCloud) {
|
|
PortManager.port = await this.forceCloudPort(server);
|
|
} else {
|
|
PortManager.port = this.parsePort(envPort) || (await appState.getServerPort()) || config.defaultServerPort;
|
|
PortManager.port = await this.tryServerPort(server);
|
|
}
|
|
await appState.setServerPort(PortManager.port);
|
|
return PortManager.port;
|
|
}
|
|
|
|
private parsePort(port: string | undefined) {
|
|
if (typeof port !== 'string') return null;
|
|
if (port === '') return null;
|
|
const maybePort = Number(port);
|
|
if (isNaN(maybePort)) return null;
|
|
return maybePort;
|
|
}
|
|
|
|
private async tryServerPort(server: Server): Promise<number> {
|
|
return new Promise((resolve, reject) => {
|
|
server.once('error', (error) => {
|
|
// we should only move ports if we are in a desktop environment
|
|
if (isDocker) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
|
|
if (!isPortInUseError(error)) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
|
|
// if we get an address in use error, we will try to open the server in an ephemeral port
|
|
// port 0 will assign an ephemeral port
|
|
server.listen(0, '0.0.0.0', () => {
|
|
const address = server.address();
|
|
if (!isAddressInfo(address)) {
|
|
reject(new Error('Unknown port type, unable to proceed'));
|
|
return;
|
|
}
|
|
logger.error(
|
|
LogOrigin.Server,
|
|
`Failed to open the desired port: ${PortManager.port} \nMoved to an Ephemeral port: ${address.port}`,
|
|
true,
|
|
);
|
|
|
|
resolve(address.port);
|
|
});
|
|
});
|
|
|
|
server.listen(PortManager.port, '0.0.0.0', () => {
|
|
const address = server.address();
|
|
if (!isAddressInfo(address)) {
|
|
reject(new Error('Unknown port type, unable to proceed'));
|
|
return;
|
|
}
|
|
resolve(address.port);
|
|
});
|
|
});
|
|
}
|
|
|
|
private forceCloudPort(server: Server): Promise<number> {
|
|
return new Promise((resolve, reject) => {
|
|
server.once('error', (error) => {
|
|
reject(error);
|
|
});
|
|
server.listen(config.defaultServerPort, '0.0.0.0', () => {
|
|
const address = server.address();
|
|
if (!isAddressInfo(address)) {
|
|
reject(new Error('Unknown port type, unable to proceed'));
|
|
return;
|
|
}
|
|
resolve(address.port);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
export const portManager = new PortManager();
|