Assign game port from host configured port ranges

Add host.factorio_port_range config to hosts that specify a pool of
ports that gets automatically assigned to instances which do not have a
game_port set on them.  This allows setting up a port range that's
forwarded to the host on the router and then configure that port range
on the host and have instances automatically get assigned an available
port when they start up.

Resolves: #448
This commit is contained in:
Hornwitser committed 2024-01-11 00:19:19 +01:00
1 parent 9742eb1645
commit 8733575255
5 files changed
+76 -6

No files matched your search

+1
View File
@@ -288,6 +288,7 @@ module.exports = {
":": "before",
"||": "before",
"&&": "before",
"??": "before",
},
}],
"padded-blocks": "off",
+56 -2
View File
@@ -599,7 +599,9 @@ export default class Host extends lib.Link {
let instanceConnection = this.instanceConnections.get(instanceId);
this.send(
new lib.InstanceStatusChangedEvent(
instanceId, instanceConnection ? instanceConnection.instance.status : "stopped", undefined
instanceId,
instanceConnection ? instanceConnection.instance.status : "stopped",
instanceConnection ? instanceConnection.instance.server.gamePort : this.gamePort(instanceId),
)
);
@@ -607,6 +609,10 @@ export default class Host extends lib.Link {
await this.sendSaveListUpdate(instanceId, path.join(instanceInfo.path, "saves"));
// save a copy of the instance config
await this.saveInstanceConfig(instanceInfo);
}
async saveInstanceConfig(instanceInfo: { path: string, config: lib.InstanceConfig }) {
let warnedOutput = {
_warning: "Changes to this file will be overwritten by the controller's copy.",
...instanceInfo.config.toJSON(),
@@ -647,6 +653,50 @@ export default class Host extends lib.Link {
return instanceInfo;
}
/**
* Retrieved assigned port of the given instance
* @param instanceId - ID of the instance to get the assigned game port of:
* @returns Assigned game port or undefined if it does not exist.
*/
gamePort(instanceId: number) {
const instance = this.discoveredInstanceInfos.get(instanceId);
if (!instance) {
return undefined;
}
return (
instance.config.get("factorio.game_port")
?? instance.config.get("factorio.host_assigned_game_port")
?? undefined
);
}
assignGamePort(instanceId: number) {
const availablePorts = lib.parseRanges(this.config.get("host.factorio_port_range"), 1, 2**16 - 1);
for (const [id, instance] of this.instanceInfos) {
if (id === instanceId) {
continue;
}
const port = (
instance.config.get("factorio.game_port")
?? instance.config.get("factorio.host_assigned_game_port")
);
if (port !== null) {
availablePorts.delete(port);
}
}
const instanceToAssign = this.instanceInfos.get(instanceId)!;
const assignedPort = instanceToAssign.config.get("factorio.host_assigned_game_port");
if (assignedPort !== null && availablePorts.has(assignedPort)) {
return assignedPort;
}
if (!availablePorts.size) {
throw new lib.RequestError("No available port left to assign from host.factorio_port_range");
}
const [newPort] = availablePorts;
instanceToAssign.config.set("factorio.host_assigned_game_port", newPort);
return newPort;
}
/**
* Initialize and connect an unloaded instance
*
@@ -923,7 +973,7 @@ export default class Host extends lib.Link {
list.push(new lib.HostInstanceUpdate(
instanceInfo.config.toRemote("controller"),
instanceConnection ? instanceConnection.instance.status : "stopped",
instanceConnection ? instanceConnection.instance.server.gamePort : undefined,
instanceConnection ? instanceConnection.instance.server.gamePort : this.gamePort(instanceId),
));
}
await this.send(new lib.InstancesUpdateRequest(list));
@@ -986,6 +1036,10 @@ export default class Host extends lib.Link {
logger.info("Saving config");
await lib.safeOutputFile(this.configPath, JSON.stringify(this.config, null, "\t"));
for (const instanceInfo of this.instanceInfos.values()) {
// Save in case host_assigned_game_port changed in the config.
await this.saveInstanceConfig(instanceInfo);
}
try {
// Clear silly interval in pidfile library.
+2 -2
View File
@@ -169,7 +169,7 @@ export default class Instance extends lib.Link {
let serverOptions = {
logger: this.logger,
version: this.config.get("factorio.version"),
gamePort: this.config.get("factorio.game_port") ?? undefined,
gamePort: this.config.get("factorio.game_port") ?? host.assignGamePort(this.id),
rconPort: this.config.get("factorio.rcon_port") ?? undefined,
rconPassword: this.config.get("factorio.rcon_password") ?? undefined,
enableWhitelist: this.config.get("factorio.enable_whitelist"),
@@ -449,7 +449,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
new lib.InstanceStatusChangedEvent(
this.id,
status,
status !== "stopped" ? this.server.gamePort : undefined,
this.server.gamePort,
),
);
}
+16 -1
View File
@@ -158,6 +158,7 @@ export interface HostConfigFields {
"host.controller_token": string;
"host.tls_ca": string | null;
"host.public_address": string;
"host.factorio_port_range": string;
"host.max_reconnect_delay": number;
}
@@ -220,6 +221,14 @@ export class HostConfig extends classes.Config<HostConfigFields> {
type: "string",
initialValue: "localhost",
},
"host.factorio_port_range": {
title: "Factorio port range",
description:
"Range of UDP ports to use for game connections. Supports both comma separated values and " +
"ranges separated with a dash.",
type: "string",
initialValue: "34100-34199",
},
"host.max_reconnect_delay": {
title: "Max Reconnect Delay",
description: "Maximum delay to wait before attempting to reconnect WebSocket",
@@ -237,6 +246,7 @@ export interface InstanceConfigFields {
"factorio.version": string;
"factorio.game_port": number | null;
"factorio.host_assigned_game_port": number | null;
"factorio.rcon_port": number | null;
"factorio.rcon_password": string | null;
"factorio.player_online_autosave_slots": number;
@@ -286,11 +296,16 @@ export class InstanceConfig extends classes.Config<InstanceConfigFields> {
initialValue: "latest",
},
"factorio.game_port": {
description: "UDP port to run game on, uses a random port if null",
description: "UDP port to run game on, uses a port in host.factorio_port_range if null",
restartRequired: true,
type: "number",
optional: true,
},
"factorio.host_assigned_game_port": {
access: ["host"],
type: "number",
optional: true,
},
"factorio.rcon_port": {
description: "TCP port to run RCON on, uses a random port if null",
restartRequired: true,
+1 -1
View File
@@ -20,7 +20,7 @@ describe("class Instance", function() {
instanceConfig.set("instance.name", "foo");
src = addr({ instanceId: instanceConfig.get("instance.id") });
connector = new MockConnector(src, dst);
instance = new Instance({}, connector, "dir", "factorioDir", instanceConfig);
instance = new Instance({ assignGamePort: () => 1 }, connector, "dir", "factorioDir", instanceConfig);
instance.server = new MockServer();
});