mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 23:14:53 +09:00
Merge pull request #1067 from ILLISIS/fix-game-port-in-use
Replace a stored game port that another process holds
This commit is contained in:
1 parent
48e8c53776
commit
137211d55d
3 files changed
+100
-2
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import dgram from "node:dgram";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "path";
|
||||
import pidusage from "pidusage";
|
||||
@@ -25,6 +26,35 @@ function checkRequestSaveName(name: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that no socket is bound to the given UDP port, on IPv4 or IPv6
|
||||
*
|
||||
* Factorio binds the game port on both 0.0.0.0 and [::].
|
||||
*
|
||||
* @param port - UDP port to check.
|
||||
* @returns true if the port can be bound.
|
||||
* @internal
|
||||
*/
|
||||
async function udpPortFree(port: number) {
|
||||
for (const type of ["udp4", "udp6"] as const) {
|
||||
const socket = dgram.createSocket({ type, ipv6Only: type === "udp6" });
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.once("error", reject);
|
||||
socket.bind(port, resolve);
|
||||
});
|
||||
} catch (err: any) {
|
||||
// Other errors, such as no IPv6 support on the host, don't stop Factorio from binding the port.
|
||||
if (err.code === "EADDRINUSE") {
|
||||
return false;
|
||||
}
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for instances in the provided directory
|
||||
*
|
||||
@@ -791,8 +821,11 @@ export default class Host extends lib.Link {
|
||||
);
|
||||
}
|
||||
|
||||
assignGamePort(instanceId: number) {
|
||||
assignGamePort(instanceId: number, inUse = new Set<number>()) {
|
||||
const availablePorts = lib.parseRanges(this.config.get("host.factorio_port_range"), 1, 2 ** 16 - 1);
|
||||
for (const port of inUse) {
|
||||
availablePorts.delete(port);
|
||||
}
|
||||
for (const [id, instance] of this.assignedInstances) {
|
||||
if (id === instanceId) {
|
||||
continue;
|
||||
@@ -811,13 +844,36 @@ export default class Host extends lib.Link {
|
||||
return assignedPort;
|
||||
}
|
||||
if (!availablePorts.size) {
|
||||
throw new lib.RequestError("No available port left to assign from host.factorio_port_range");
|
||||
const others = inUse.size ? ` (${inUse.size} in use by other processes)` : "";
|
||||
throw new lib.RequestError(`No available port left to assign from host.factorio_port_range${others}`);
|
||||
}
|
||||
const [newPort] = availablePorts;
|
||||
instanceToAssign.config.set("factorio.host_assigned_game_port", newPort);
|
||||
return newPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a game port that no other process is bound to
|
||||
*
|
||||
* Replaces the stored port if it is taken, so a port another process
|
||||
* holds is not reused on every start.
|
||||
*
|
||||
* @param instanceId - ID of the instance to assign a game port to.
|
||||
* @returns the assigned port.
|
||||
*/
|
||||
async assignFreeGamePort(instanceId: number) {
|
||||
const inUse = new Set<number>();
|
||||
for (;;) {
|
||||
// Picking and storing the port before checking it keeps concurrent assignments from picking the same one.
|
||||
const port = this.assignGamePort(instanceId, inUse);
|
||||
if (await udpPortFree(port)) {
|
||||
return port;
|
||||
}
|
||||
logger.warn(`Game port ${port} is in use by another process`, this.instanceLogMeta(instanceId));
|
||||
inUse.add(port);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and connect an unloaded instance
|
||||
*
|
||||
|
||||
@@ -678,6 +678,9 @@ end`.replace(/\r?\n/g, " ");
|
||||
// After changing the status this function is responsible for calling notifyExit if it throws.
|
||||
this.notifyStatus("starting");
|
||||
try {
|
||||
if (this.config.get("factorio.game_port") === null) {
|
||||
this.server.gamePort = await this._host.assignFreeGamePort(this.id);
|
||||
}
|
||||
const factorioVersions = await this.sendTo("controller", new lib.FactorioVersionsRequest());
|
||||
await this._loadStats();
|
||||
// Resolve a release channel target (e.g. "stable") to a concrete
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import dgram from "node:dgram";
|
||||
import events from "node:events";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -208,6 +210,43 @@ describe("host/src/Host", function() {
|
||||
assert.equal(host.config.restartRequired, true);
|
||||
});
|
||||
});
|
||||
describe(".assignFreeGamePort()", function() {
|
||||
let host;
|
||||
let held;
|
||||
let instanceConfig;
|
||||
beforeEach(async function() {
|
||||
const hostConfig = new lib.HostConfig("host");
|
||||
host = new Host(new HostConnector(hostConfig, []), hostConfig, []);
|
||||
instanceConfig = new lib.InstanceConfig("host", { "instance.id": 1 });
|
||||
host.assignedInstances.set(1, { config: instanceConfig, path: "" });
|
||||
held = dgram.createSocket("udp4");
|
||||
held.bind(0, "127.0.0.1");
|
||||
await events.once(held, "listening");
|
||||
});
|
||||
afterEach(function() {
|
||||
held.close();
|
||||
});
|
||||
it("replaces a stored port another process is bound to", async function() {
|
||||
const port = held.address().port;
|
||||
host.config.set("host.factorio_port_range", `${port}-${port + 1}`);
|
||||
instanceConfig.set("factorio.host_assigned_game_port", port);
|
||||
assert.equal(await host.assignFreeGamePort(1), port + 1);
|
||||
assert.equal(instanceConfig.get("factorio.host_assigned_game_port"), port + 1);
|
||||
});
|
||||
it("keeps a stored port that is free", async function() {
|
||||
const port = held.address().port;
|
||||
host.config.set("host.factorio_port_range", `${port}-${port + 1}`);
|
||||
instanceConfig.set("factorio.host_assigned_game_port", port + 1);
|
||||
assert.equal(await host.assignFreeGamePort(1), port + 1);
|
||||
});
|
||||
it("rejects when every port in the range is taken", async function() {
|
||||
host.config.set("host.factorio_port_range", String(held.address().port));
|
||||
await assert.rejects(
|
||||
host.assignFreeGamePort(1),
|
||||
/No available port left to assign from host.factorio_port_range \(1 in use by other processes\)/
|
||||
);
|
||||
});
|
||||
});
|
||||
describe(".handlePluginInstallRequest()", function() {
|
||||
let host;
|
||||
let _fetch;
|
||||
|
||||
Reference in new issue
Block a user