Files
factorio-clusterio/test/host/server_stop.js
T
bbassieandClaude Opus 5 49e8d6ecc5 Fix sendRcon hanging if Factorio exits before RCON connects
sendRcon waits for the rcon-ready event when RCON has not connected yet,
which is the same root cause as the stop() hang. events.once rejects on
error events, but _watchExit skips emitting error while the state is
stopping, and kill() sets that state before sending SIGKILL. exportData
does exactly that, so an RCON call racing the export never settles and
the request stays pending until the session closes.

Wait for exit as well, which leaves the existing RCON client check to
reject the call with "RCON connection lost" instead. Both call sites now
share a helper as the paired wait is identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 13:49:58 +00:00

78 lines
2.4 KiB
JavaScript

import assert from "node:assert/strict";
import events from "node:events";
import path from "node:path";
import { FactorioServer } from "@clusterio/host/dist/node/src/server.js";
// Stand-in for the Factorio child process, only exit and kill are used here.
class FakeProcess extends events.EventEmitter {
killed = false;
kill() {
this.killed = true;
}
}
function createStartedServer() {
const server = new FactorioServer(path.join("test", "file", "factorio"), path.join("temp", "test", "server"), {});
server._state = "running";
server._rconReady = false;
server._server = new FakeProcess();
server._watchExit();
return server;
}
// Fail instead of hanging if the call never settles.
async function withTimeout(promise, message) {
let timeoutId;
const timeout = new Promise((resolve, reject) => {
timeoutId = setTimeout(() => reject(new Error(message)), 500);
});
try {
return await Promise.race([promise, timeout]);
} finally {
clearTimeout(timeoutId);
}
}
describe("host/src/server", function() {
describe("FactorioServer.stop()", function() {
it("returns if the process exits before RCON is ready", async function() {
const server = createStartedServer();
const stopped = server.stop();
server._server.emit("exit", 1, null);
await withTimeout(stopped, "stop() did not resolve");
assert.equal(server._state, "init");
assert.equal(server.listenerCount("rcon-ready"), 0);
});
it("stops the server if RCON becomes ready", async function() {
const server = createStartedServer();
const stopped = server.stop();
server._rconReady = true;
server.emit("rcon-ready");
await new Promise(resolve => setImmediate(resolve));
assert.equal(server.listenerCount("exit"), 0);
const fakeProcess = server._server;
assert.equal(fakeProcess.killed, true);
fakeProcess.emit("exit", 0, null);
await withTimeout(stopped, "stop() did not resolve");
});
});
describe("FactorioServer.sendRcon()", function() {
it("throws if the process exits before RCON is ready", async function() {
const server = createStartedServer();
const sent = server.sendRcon("/version");
// kill() sets the state before killing, which suppresses the error event.
server._state = "stopping";
server._server.emit("exit", null, "SIGKILL");
await assert.rejects(
withTimeout(sent, "sendRcon() did not settle"),
new Error("RCON connection lost"),
);
assert.equal(server.listenerCount("rcon-ready"), 0);
});
});
});