diff --git a/packages/host/src/server.ts b/packages/host/src/server.ts index 32d65b52..e117c4de 100644 --- a/packages/host/src/server.ts +++ b/packages/host/src/server.ts @@ -1405,7 +1405,19 @@ export class FactorioServer extends events.EventEmitter { // complete before the RCON connection can be used if (!this._rconReady) { // Not using events.once here to avoid throwing on error events. - await new Promise(resolve => this.once("rcon-ready", resolve)); + // The process may also exit before RCON connects. + await new Promise(resolve => { + const onRconReady = () => { + this.off("exit", onExit); + resolve(); + }; + const onExit = () => { + this.off("rcon-ready", onRconReady); + resolve(); + }; + this.once("rcon-ready", onRconReady); + this.once("exit", onExit); + }); } // The Factorio server may have decided to get ahead of us and diff --git a/test/host/server_stop.js b/test/host/server_stop.js new file mode 100644 index 00000000..abff051c --- /dev/null +++ b/test/host/server_stop.js @@ -0,0 +1,62 @@ +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 stop() never resolves. +async function waitForStop(stopped) { + let timeoutId; + const timeout = new Promise((resolve, reject) => { + timeoutId = setTimeout(() => reject(new Error("stop() did not resolve")), 500); + }); + try { + await Promise.race([stopped, 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 waitForStop(stopped); + 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 waitForStop(stopped); + }); + }); +});