Remove serverport from project file (#1957)

* feat: get server port from app satate or env

optional startup port from env will always override

function for parsing port from env

populate default port in app state

add test for migration

* bump version
This commit is contained in:
Alex Christoffer Rasmussen
2026-03-02 06:02:29 -08:00
committed by GitHub
parent 1fe58e21be
commit c1fcdf7065
38 changed files with 593 additions and 193 deletions
@@ -72,14 +72,12 @@ describe('safeMerge', () => {
it('merges the settings key', () => {
const mergedData = safeMerge(baseDb, {
settings: {
serverPort: 3000,
language: 'pt',
version: 'new',
} as Settings,
});
expect(mergedData.settings).toStrictEqual({
version: 'new',
serverPort: 3000,
operatorKey: null,
editorKey: null,
timeFormat: baseDb.settings.timeFormat,
@@ -0,0 +1,140 @@
import { Server } from "http";
import { config } from "../../setup/config.js";
import { envPort, isDocker, isOntimeCloud } from "../../setup/environment.js";
import * as appState from "../../services/app-state-service/AppStateService.js";
import { logger } from "../Logger.js";
import { LogOrigin, MaybeNumber } from "ontime-types";
import { isAddressInfo, isPortInUseError } from "./PortManager.utils.js";
import { shouldCrashDev } from "../../utils/development.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();
@@ -0,0 +1,15 @@
import { AddressInfo } from 'net';
/**
* Checks whether a given error is a port in use error
*/
export function isPortInUseError(err: Error): boolean {
return typeof err === 'object' && err !== null && 'code' in err && err.code === 'EADDRINUSE';
}
/**
* Guard verifies that the given address is a usable AddressInfo object
*/
export function isAddressInfo(address: string | AddressInfo | null): address is AddressInfo {
return typeof address === 'object' && address !== null;
}