From 1c7a32113a855023d6e7d60960bdf13391684fd1 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:16:56 +0000 Subject: [PATCH 1/9] Remove single dir factorio install --- packages/host/host.ts | 26 ++++++++++ packages/host/src/server.ts | 48 +++++++----------- .../bad/data/changelog.txt} | 0 .../good/data/changelog.txt} | 0 .../{ => 0.1.1}/data/base/locale/en/base.cfg | 0 .../factorio/{ => 0.1.1}/data/changelog.txt | 0 .../{ => 0.1.1}/data/core/locale/en/core.cfg | 0 test/file/factorio/not-a-dir | 0 .../factorio/not-an-install/not-an-install | 0 test/host/server.js | 49 +++++++------------ test/integration/server.js | 8 +-- 11 files changed, 66 insertions(+), 65 deletions(-) rename test/file/{changelog-bad.txt => changelogs/bad/data/changelog.txt} (100%) rename test/file/{changelog-test.txt => changelogs/good/data/changelog.txt} (100%) rename test/file/factorio/{ => 0.1.1}/data/base/locale/en/base.cfg (100%) rename test/file/factorio/{ => 0.1.1}/data/changelog.txt (100%) rename test/file/factorio/{ => 0.1.1}/data/core/locale/en/core.cfg (100%) create mode 100644 test/file/factorio/not-a-dir create mode 100644 test/file/factorio/not-an-install/not-an-install diff --git a/packages/host/host.ts b/packages/host/host.ts index 19127713..6f9205ee 100755 --- a/packages/host/host.ts +++ b/packages/host/host.ts @@ -25,6 +25,7 @@ import * as lib from "@clusterio/lib"; import { ConsoleTransport, levels, logger } from "@clusterio/lib"; import Host from "./src/Host"; +import { getFactorioVersion } from "./src/server"; let host: Host | undefined; @@ -236,6 +237,31 @@ async function startHost() { tlsCa = await fs.readFile(tlsCaPath, "utf8"); } + // Migrate from single factorio version to multiple + const factorioDir = hostConfig.get("host.factorio_directory"); + const singleFactorioVersion = await getFactorioVersion(factorioDir); + if (singleFactorioVersion !== null) { + const dst = path.join(factorioDir, singleFactorioVersion); + logger.info(`Found single factorio version in ${factorioDir} moving to ${dst}`); + const stats = await fs.lstat(factorioDir); + if (stats.isSymbolicLink()) { + // For symlinks we want to move the file, but cant move it into itself so need a tmp move + const tmpPath = `${factorioDir}.tmp`; + await fs.move(factorioDir, tmpPath); + await fs.ensureDir(factorioDir); + await fs.move(tmpPath, dst); + } else { + // It is a directory here, otherwise no version would have been found + // We will only move known files in case a version was incorrectly installed in the dir + await fs.ensureDir(dst); + await Promise.all( + ["bin", "data", "config-path.cfg"].map(name => ( + fs.move(path.join(factorioDir, name), path.join(dst, name)) + )) + ); + } + } + let hostConnector = new HostConnector(hostConfig, tlsCa, pluginInfos); host = new Host( hostConnector, diff --git a/packages/host/src/server.ts b/packages/host/src/server.ts index dc45d101..89aacb05 100644 --- a/packages/host/src/server.ts +++ b/packages/host/src/server.ts @@ -14,11 +14,11 @@ import * as lib from "@clusterio/lib"; * Determines the version of Factorio the datadir is pointing to by * reading the changelog.txt in it. * - * @param changelogPath - Path to changelog.txt. - * @internal + * @param factorioDir - Path factorio dir containing data/changelog.txt. */ -async function getVersion(changelogPath: string) { +export async function getFactorioVersion(factorioDir: string) { let changelog; + const changelogPath = path.join(factorioDir, "data", "changelog.txt"); try { changelog = await fs.readFile(changelogPath, "utf-8"); } catch (err: any) { @@ -28,10 +28,10 @@ async function getVersion(changelogPath: string) { throw err; } - for (let line of changelog.split(/[\r\n]+/)) { - let index = line.indexOf(":"); + for (const line of changelog.split(/[\r\n]+/)) { + const index = line.indexOf(":"); if (index !== -1) { - let nameĀ = line.slice(0, index).trim(); + const name = line.slice(0, index).trim(); if (name.toLowerCase() === "version") { return line.slice(index + 1).trim(); } @@ -80,40 +80,29 @@ function versionOrder(a: string, b: string) { * @internal */ async function findVersion(factorioDir: string, targetVersion: lib.TargetVersion): Promise<[string, lib.FullVersion]> { - - // There are two supported setups: having the factorio dir be the actual - // install directory, and having the factorio dir be a folder containing - // multiple install directories - - let simpleVersion = await getVersion(path.join(factorioDir, "data", "changelog.txt")); - if (simpleVersion !== null) { - if (simpleVersion === targetVersion || simpleVersion.startsWith(targetVersion) || targetVersion === "latest") { - if (lib.isFullVersion(simpleVersion)) { - return [path.join(factorioDir, "data"), simpleVersion]; + const versions = new Map(); + for (const name of await fs.readdir(factorioDir)) { + // We do not use withFileTypes or lstat because we want to follow symlinks + const fullPath = path.join(factorioDir, name); + try { + const stats = await fs.stat(fullPath); + if (!stats.isDirectory()) { + continue; } - } - - throw new Error( - `Factorio version ${targetVersion} was requested, but install directory contains ${simpleVersion}` - ); - } - - let versions = new Map(); - for (let entry of await fs.readdir(factorioDir, { withFileTypes: true })) { - if (!entry.isDirectory()) { + } catch { continue; } - let version = await getVersion(path.join(factorioDir, entry.name, "data", "changelog.txt")); + const version = await getFactorioVersion(fullPath); if (version === null || !lib.isFullVersion(version)) { continue; } if (version === targetVersion) { - return [path.join(factorioDir, entry.name, "data"), version]; + return [path.join(factorioDir, name, "data"), version]; } - versions.set(version, entry.name); + versions.set(version, name); } if (!versions.size) { @@ -1421,7 +1410,6 @@ export class FactorioServer extends events.EventEmitter { // For testing only -export const _getVersion = getVersion; export const _versionOrder = versionOrder; export const _findVersion = findVersion; export const _randomDynamicPort = randomDynamicPort; diff --git a/test/file/changelog-bad.txt b/test/file/changelogs/bad/data/changelog.txt similarity index 100% rename from test/file/changelog-bad.txt rename to test/file/changelogs/bad/data/changelog.txt diff --git a/test/file/changelog-test.txt b/test/file/changelogs/good/data/changelog.txt similarity index 100% rename from test/file/changelog-test.txt rename to test/file/changelogs/good/data/changelog.txt diff --git a/test/file/factorio/data/base/locale/en/base.cfg b/test/file/factorio/0.1.1/data/base/locale/en/base.cfg similarity index 100% rename from test/file/factorio/data/base/locale/en/base.cfg rename to test/file/factorio/0.1.1/data/base/locale/en/base.cfg diff --git a/test/file/factorio/data/changelog.txt b/test/file/factorio/0.1.1/data/changelog.txt similarity index 100% rename from test/file/factorio/data/changelog.txt rename to test/file/factorio/0.1.1/data/changelog.txt diff --git a/test/file/factorio/data/core/locale/en/core.cfg b/test/file/factorio/0.1.1/data/core/locale/en/core.cfg similarity index 100% rename from test/file/factorio/data/core/locale/en/core.cfg rename to test/file/factorio/0.1.1/data/core/locale/en/core.cfg diff --git a/test/file/factorio/not-a-dir b/test/file/factorio/not-a-dir new file mode 100644 index 00000000..e69de29b diff --git a/test/file/factorio/not-an-install/not-an-install b/test/file/factorio/not-an-install/not-an-install new file mode 100644 index 00000000..e69de29b diff --git a/test/host/server.js b/test/host/server.js index 1dc541cd..42e32cde 100644 --- a/test/host/server.js +++ b/test/host/server.js @@ -11,17 +11,17 @@ const { testLines } = require("../lib/factorio/lines"); describe("host/server", function() { - describe("_getVersion()", function() { + describe("getFactorioVersion()", function() { it("should get the version from a changelog", async function() { - let version = await hostServer._getVersion(path.join("test", "file", "changelog-test.txt")); + let version = await hostServer.getFactorioVersion(path.join("test", "file", "changelogs", "good")); assert.equal(version, "0.1.1"); }); it("should return null if unable to find the version", async function() { - let version = await hostServer._getVersion(path.join("test", "file", "changelog-bad.txt")); + let version = await hostServer.getFactorioVersion(path.join("test", "file", "changelogs", "bad")); assert.equal(version, null); }); it("should return null if file does not exist", async function() { - let version = await hostServer._getVersion(path.join("test", "file", "does-not-exist.txt")); + let version = await hostServer.getFactorioVersion(path.join("test", "file", "changelogs", "not-exists")); assert.equal(version, null); }); }); @@ -38,46 +38,33 @@ describe("host/server", function() { }); describe("_findVersion()", function() { - it("should find a given install dir with latest as target", async function() { - let installDir = path.join("test", "file", "factorio"); - let [dir, version] = await hostServer._findVersion(installDir, "latest"); - assert.equal(dir, path.join(installDir, "data")); - assert.equal(version, "0.1.1"); - }); - it("should find a given install dir with correct version as target", async function() { - let installDir = path.join("test", "file", "factorio"); - let [dir, version] = await hostServer._findVersion(installDir, "0.1.1"); - assert.equal(dir, path.join(installDir, "data")); - assert.equal(version, "0.1.1"); - }); - it("should reject if the install dir version does not match target version", async function() { - let installDir = path.join("test", "file", "factorio"); - await assert.rejects( - hostServer._findVersion(installDir, "0.1.2"), - new Error("Factorio version 0.1.2 was requested, but install directory contains 0.1.1") - ); - }); it("should search given directory for latest Factorio install", async function() { - let installDir = path.join("test", "file"); - let [dir, version] = await hostServer._findVersion(installDir, "latest"); - assert.equal(dir, path.join(installDir, "factorio", "data")); + const installDir = path.join("test", "file", "factorio"); + const [dir, version] = await hostServer._findVersion(installDir, "latest"); + assert.equal(dir, path.join(installDir, "0.1.1", "data")); assert.equal(version, "0.1.1"); }); it("should search given directory for given Factorio install", async function() { - let installDir = path.join("test", "file"); - let [dir, version] = await hostServer._findVersion(installDir, "0.1.1"); - assert.equal(dir, path.join(installDir, "factorio", "data")); + const installDir = path.join("test", "file", "factorio"); + const [dir, version] = await hostServer._findVersion(installDir, "0.1.1"); + assert.equal(dir, path.join(installDir, "0.1.1", "data")); + assert.equal(version, "0.1.1"); + }); + it("should search given directory for partly given Factorio install", async function() { + const installDir = path.join("test", "file", "factorio"); + const [dir, version] = await hostServer._findVersion(installDir, "0.1"); + assert.equal(dir, path.join(installDir, "0.1.1", "data")); assert.equal(version, "0.1.1"); }); it("should reject if no factorio install with the given version was found", async function() { - let installDir = path.join("test", "file"); + let installDir = path.join("test", "file", "factorio"); await assert.rejects( hostServer._findVersion(installDir, "0.1.2"), new Error("Unable to find Factorio version 0.1.2") ); }); it("should reject if no factorio install was found", async function() { - let installDir = path.join("test", "file", "instances"); + let installDir = path.join("test", "file"); await assert.rejects( hostServer._findVersion(installDir, "latest"), new Error(`Unable to find any Factorio install in ${installDir}`) diff --git a/test/integration/server.js b/test/integration/server.js index 1035c16e..f843700d 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -5,14 +5,14 @@ const fs = require("fs-extra"); const path = require("path"); const lib = require("@clusterio/lib"); -const { FactorioServer, _getVersion } = require("@clusterio/host/dist/node/src/server"); +const { FactorioServer, getFactorioVersion } = require("@clusterio/host/dist/node/src/server"); const { logger } = lib; const { slowTest, factorioDir } = require("./index"); describe("Integration of host/src/server", function() { - describe("_getVersion()", function() { + describe("getFactorioVersion()", function() { it("should get a version from factorio's changelog.txt", async function() { function checkVersion(version) { if (!/^\d+\.\d+\.\d+$/.test(version)) { @@ -20,7 +20,7 @@ describe("Integration of host/src/server", function() { } } - let version = await _getVersion(path.join(factorioDir, "data", "changelog.txt")); + let version = await getFactorioVersion(factorioDir); if (version !== null) { checkVersion(version); } else { @@ -30,7 +30,7 @@ describe("Integration of host/src/server", function() { } checkVersion( - await _getVersion(path.join(factorioDir, entry.name, "data", "changelog.txt")) + await getFactorioVersion(path.join(factorioDir, entry.name)) ); } } From 44abeace88323f8f86bce8f643721b701d486114 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Thu, 5 Feb 2026 00:45:53 +0000 Subject: [PATCH 2/9] Add automatic headless download and update --- packages/controller/src/HostConnection.ts | 5 + packages/host/src/Instance.ts | 2 + packages/host/src/server.ts | 129 +++++++++++++++++- packages/lib/src/data/messages_controller.ts | 2 +- packages/lib/src/external/FactorioVersions.ts | 2 +- .../0.1.2/data/base/locale/en/base.cfg | 4 + test/file/factorio/0.1.2/data/changelog.txt | 3 + .../0.1.2/data/core/locale/en/core.cfg | 4 + .../data/base/locale/en/base.cfg | 4 + .../not-full-version/data/changelog.txt | 3 + .../data/core/locale/en/core.cfg | 4 + test/host/server.js | 22 ++- test/lib/external/FactorioVersions.test.js | 6 +- 13 files changed, 177 insertions(+), 13 deletions(-) create mode 100644 test/file/factorio/0.1.2/data/base/locale/en/base.cfg create mode 100644 test/file/factorio/0.1.2/data/changelog.txt create mode 100644 test/file/factorio/0.1.2/data/core/locale/en/core.cfg create mode 100644 test/file/factorio/not-full-version/data/base/locale/en/base.cfg create mode 100644 test/file/factorio/not-full-version/data/changelog.txt create mode 100644 test/file/factorio/not-full-version/data/core/locale/en/core.cfg diff --git a/packages/controller/src/HostConnection.ts b/packages/controller/src/HostConnection.ts index c50c5f3d..688130dc 100644 --- a/packages/controller/src/HostConnection.ts +++ b/packages/controller/src/HostConnection.ts @@ -81,6 +81,7 @@ export default class HostConnection extends BaseConnection { this.handle(lib.InstanceSaveDetailsUpdatesEvent, this.handleInstanceSaveDetailsUpdatesEvent.bind(this)); this.handle(lib.LogMessageEvent, this.handleLogMessageEvent.bind(this)); this.handle(lib.InstancePlayerUpdateEvent, this.handleInstancePlayerUpdateEvent.bind(this)); + this.handle(lib.FactorioVersionsRequest, this.handleFactorioVersionsRequest.bind(this)); this.snoopEvent(lib.InstanceAdminlistUpdateEvent, this.snoopInstanceAdminlistUpdateEvent.bind(this)); this.snoopEvent(lib.InstanceBanlistUpdateEvent, this.snoopInstanceBanlistUpdateEvent.bind(this)); this.snoopEvent(lib.InstanceWhitelistUpdateEvent, this.snoopInstanceWhitelistUpdateEvent.bind(this)); @@ -363,6 +364,10 @@ export default class HostConnection extends BaseConnection { }); } + async handleFactorioVersionsRequest(request: lib.FactorioVersionsRequest) { + return await this._controller.factorioVersions.get(request.maxAgeMs); + } + async snoopInstanceAdminlistUpdateEvent(event: lib.InstanceAdminlistUpdateEvent, src: lib.Address) { if (src.type !== lib.Address.instance) { return; diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index 88cf50ae..af5b9ef4 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -639,8 +639,10 @@ end`.replace(/\r?\n/g, " "); async init(pluginInfos: lib.PluginNodeEnvInfo[]) { this.notifyStatus("starting"); + const factorioVersions = await this.sendTo("controller", new lib.FactorioVersionsRequest()); try { await this._loadStats(); + await this.server.checkForUpdates(factorioVersions); await this.server.init(); } catch (err) { this.notifyExit(); diff --git a/packages/host/src/server.ts b/packages/host/src/server.ts index 89aacb05..846dddfb 100644 --- a/packages/host/src/server.ts +++ b/packages/host/src/server.ts @@ -2,6 +2,7 @@ import fs from "fs-extra"; import child_process from "child_process"; import path from "path"; +import JSZip from "jszip"; import events from "events"; import util from "util"; import crypto from "crypto"; @@ -9,6 +10,7 @@ import { Rcon } from "rcon-client"; import * as lib from "@clusterio/lib"; +const execFileAsync = util.promisify(child_process.execFile); /** * Determines the version of Factorio the datadir is pointing to by @@ -66,13 +68,109 @@ function versionOrder(a: string, b: string) { return 0; } +/** + * Find all installed factorio versions and return them as a set + * + * @param factorioDir - Path to Factorio installation dirs. + * @returns Set of installed factorio versions + */ +async function listFactorioVersions(factorioDir: string) { + const versions = new Set(); + + for (const name of await fs.readdir(factorioDir)) { + // We do not use withFileTypes or lstat because we want to follow symlinks + const fullPath = path.join(factorioDir, name); + try { + const stats = await fs.stat(fullPath); + if (!stats.isDirectory()) { + continue; + } + } catch { + continue; + } + + const version = await getFactorioVersion(fullPath); + if (version === null || !lib.isFullVersion(version)) { + continue; + } + + versions.add(version); + } + + return versions; +} + +/** + * Downloads a ZIP file from a URL using native fetch and extracts it + * + * @param url - The URL of the ZIP file + * @param targetDir - Directory to extract the ZIP into + * @throws Network error if status of request is not ok + */ +async function downloadAndExtractZip(url: string, targetDir: string) { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`); + } + + // Load the ZIP with JSZip + const buffer = Buffer.from(await res.arrayBuffer()); + const zip = await JSZip.loadAsync(buffer); + + // Extract each file/folder + await fs.ensureDir(targetDir); + await Promise.all( + Object.values(zip.files).map(async (entry) => { + const entryPath = path.join(targetDir, entry.name); + if (entry.dir) { + await fs.ensureDir(entryPath); + } else { + await fs.ensureDir(path.dirname(entryPath)); + await fs.writeFile(entryPath, await entry.async("nodebuffer")); + } + }) + ); +} + +/** + * Downloads a tar.gz file from a URL using native fetch and extracts it + * + * @param url - The URL of the ZIP file + * @param targetDir - Directory to extract the ZIP into + * @throws Network error if status of request is not ok + */ +async function downloadAndExtractTarGz(url: string, targetDir: string) { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`); + } + + // Write the buffer to a temp file + const tmpFilePath = `${targetDir}.tmp`; + const buffer = Buffer.from(await res.arrayBuffer()); + await fs.writeFile(tmpFilePath, buffer); + + // Execute tar CLI to extract + try { + await execFileAsync("tar", ["-xzf", tmpFilePath, "-C", targetDir, "--strip-components", "1"]); + } catch (err: any) { + if (err.code === "ENOENT") { + throw new Error("error executing tar command, do you have 'xz-utils' installed?"); + } + throw err; + } finally { + // Clean up temp file + await fs.remove(tmpFilePath); + } +} + /** * Find factorio data directory of the given version * * Searches the given factorio dir for an installation of Factorio of * the target version. * - * @param factorioDir - Path to Factorio installation dir(s). + * @param factorioDir - Path to Factorio installation dirs. * @param targetVersion - * Version to look for, supports the special value "latest" for the * latest version available. @@ -860,6 +958,34 @@ export class FactorioServer extends events.EventEmitter { this._state = "init"; } + /** + * Check of updated versions to download + * + * Should be called before init for the new version to be detected during init + */ + async checkForUpdates(availableVersions: lib.ExternalFactorioVersion[]) { + const targetVersion = this._targetVersion; + const latestVersion = availableVersions + .filter(v => ( + targetVersion === "latest" || v.version === targetVersion || v.version.startsWith(targetVersion) + )) + .sort((v1, v2) => versionOrder(v1.version, v2.version)) + [0]; + + const installedVersions = await listFactorioVersions(this._factorioDir); + if (!installedVersions.has(latestVersion.version)) { + // Download the newer version + const v = latestVersion.version; + if (process.platform === "linux") { + this._logger.info(`A newer version of factorio is available (${v}), starting download...`); + const dst = path.join(this._factorioDir, latestVersion.version); + await downloadAndExtractTarGz(latestVersion.headlessUrl, dst); + } else { + this._logger.info(`A newer version of factorio is available (${v}) but must be manually downloaded`); + } + } + } + /** * The version of Factorio in use. This will be the actual version if * "latest" (the default) was specified as `factorioVersion` to the constructor. @@ -1412,6 +1538,7 @@ export class FactorioServer extends events.EventEmitter { // For testing only export const _versionOrder = versionOrder; export const _findVersion = findVersion; +export const _listFactorioVersions = listFactorioVersions; export const _randomDynamicPort = randomDynamicPort; export const _generatePassword = generatePassword; export const _parseOutput = parseOutput; diff --git a/packages/lib/src/data/messages_controller.ts b/packages/lib/src/data/messages_controller.ts index 53c48e74..2c74b4c2 100644 --- a/packages/lib/src/data/messages_controller.ts +++ b/packages/lib/src/data/messages_controller.ts @@ -402,7 +402,7 @@ export class DebugWsMessageEvent { export class FactorioVersionsRequest { declare ["constructor"]: typeof FactorioVersionsRequest; static type = "request" as const; - static src = "control" as const; + static src = ["control", "instance"] as const; static dst = "controller" as const; static permission = "core.external.get_factorio_versions" as const; diff --git a/packages/lib/src/external/FactorioVersions.ts b/packages/lib/src/external/FactorioVersions.ts index b9041fc0..45125433 100644 --- a/packages/lib/src/external/FactorioVersions.ts +++ b/packages/lib/src/external/FactorioVersions.ts @@ -60,7 +60,7 @@ function parseFactorioVersions(html: string): ExternalFactorioVersion[] { versions.set(version, { version: version, stable: kind === "stable", - headlessUrl: `www.factorio.com/get-download/${version}/headless/linux64`, + headlessUrl: `https://www.factorio.com/get-download/${version}/headless/linux64`, }); } } diff --git a/test/file/factorio/0.1.2/data/base/locale/en/base.cfg b/test/file/factorio/0.1.2/data/base/locale/en/base.cfg new file mode 100644 index 00000000..ae82115b --- /dev/null +++ b/test/file/factorio/0.1.2/data/base/locale/en/base.cfg @@ -0,0 +1,4 @@ +; Test data for exportLocale +[test] +base-a=1 +base-b=2 diff --git a/test/file/factorio/0.1.2/data/changelog.txt b/test/file/factorio/0.1.2/data/changelog.txt new file mode 100644 index 00000000..2b12ff4b --- /dev/null +++ b/test/file/factorio/0.1.2/data/changelog.txt @@ -0,0 +1,3 @@ +--------------------------------------------------------------------------------------------------- +Version: 0.1.2 +About: Dummy changelog.txt for FactorioServer tests diff --git a/test/file/factorio/0.1.2/data/core/locale/en/core.cfg b/test/file/factorio/0.1.2/data/core/locale/en/core.cfg new file mode 100644 index 00000000..f414b76f --- /dev/null +++ b/test/file/factorio/0.1.2/data/core/locale/en/core.cfg @@ -0,0 +1,4 @@ +; Test data for exportLocale +[test] +core-a=1 +core-b=2 diff --git a/test/file/factorio/not-full-version/data/base/locale/en/base.cfg b/test/file/factorio/not-full-version/data/base/locale/en/base.cfg new file mode 100644 index 00000000..ae82115b --- /dev/null +++ b/test/file/factorio/not-full-version/data/base/locale/en/base.cfg @@ -0,0 +1,4 @@ +; Test data for exportLocale +[test] +base-a=1 +base-b=2 diff --git a/test/file/factorio/not-full-version/data/changelog.txt b/test/file/factorio/not-full-version/data/changelog.txt new file mode 100644 index 00000000..bc1a9058 --- /dev/null +++ b/test/file/factorio/not-full-version/data/changelog.txt @@ -0,0 +1,3 @@ +--------------------------------------------------------------------------------------------------- +Version: 0.1 +About: Dummy changelog.txt for FactorioServer tests diff --git a/test/file/factorio/not-full-version/data/core/locale/en/core.cfg b/test/file/factorio/not-full-version/data/core/locale/en/core.cfg new file mode 100644 index 00000000..f414b76f --- /dev/null +++ b/test/file/factorio/not-full-version/data/core/locale/en/core.cfg @@ -0,0 +1,4 @@ +; Test data for exportLocale +[test] +core-a=1 +core-b=2 diff --git a/test/host/server.js b/test/host/server.js index 42e32cde..d75e5568 100644 --- a/test/host/server.js +++ b/test/host/server.js @@ -41,8 +41,8 @@ describe("host/server", function() { it("should search given directory for latest Factorio install", async function() { const installDir = path.join("test", "file", "factorio"); const [dir, version] = await hostServer._findVersion(installDir, "latest"); - assert.equal(dir, path.join(installDir, "0.1.1", "data")); - assert.equal(version, "0.1.1"); + assert.equal(dir, path.join(installDir, "0.1.2", "data")); + assert.equal(version, "0.1.2"); }); it("should search given directory for given Factorio install", async function() { const installDir = path.join("test", "file", "factorio"); @@ -53,14 +53,14 @@ describe("host/server", function() { it("should search given directory for partly given Factorio install", async function() { const installDir = path.join("test", "file", "factorio"); const [dir, version] = await hostServer._findVersion(installDir, "0.1"); - assert.equal(dir, path.join(installDir, "0.1.1", "data")); - assert.equal(version, "0.1.1"); + assert.equal(dir, path.join(installDir, "0.1.2", "data")); + assert.equal(version, "0.1.2"); }); it("should reject if no factorio install with the given version was found", async function() { let installDir = path.join("test", "file", "factorio"); await assert.rejects( - hostServer._findVersion(installDir, "0.1.2"), - new Error("Unable to find Factorio version 0.1.2") + hostServer._findVersion(installDir, "0.1.3"), + new Error("Unable to find Factorio version 0.1.3") ); }); it("should reject if no factorio install was found", async function() { @@ -72,6 +72,14 @@ describe("host/server", function() { }); }); + describe("_listFactorioVersions()", function() { + it("should list all versions in a directory", async function() { + const installDir = path.join("test", "file", "factorio"); + const installedVersions = await hostServer._listFactorioVersions(installDir); + assert.deepEqual(installedVersions, new Set(["0.1.1", "0.1.2"])); + }); + }); + describe("randomDynamicPort()", function() { it("should return a port number", function() { let port = hostServer._randomDynamicPort(); @@ -144,7 +152,7 @@ describe("host/server", function() { describe(".version", function() { it("should return the version detected", function() { - assert.equal(server.version, "0.1.1"); + assert.equal(server.version, "0.1.2"); }); }); diff --git a/test/lib/external/FactorioVersions.test.js b/test/lib/external/FactorioVersions.test.js index 32b86dad..9058e6e0 100644 --- a/test/lib/external/FactorioVersions.test.js +++ b/test/lib/external/FactorioVersions.test.js @@ -54,12 +54,12 @@ describe("FactorioVersions", function() { { stable: false, version: "2.0.73", - headlessUrl: "www.factorio.com/get-download/2.0.73/headless/linux64", + headlessUrl: "https://www.factorio.com/get-download/2.0.73/headless/linux64", }, { stable: true, version: "2.0.72", - headlessUrl: "www.factorio.com/get-download/2.0.72/headless/linux64", + headlessUrl: "https://www.factorio.com/get-download/2.0.72/headless/linux64", }, ]); }); @@ -90,7 +90,7 @@ describe("FactorioVersions", function() { { stable: true, version: "2.0.73", - headlessUrl: "www.factorio.com/get-download/2.0.73/headless/linux64", + headlessUrl: "https://www.factorio.com/get-download/2.0.73/headless/linux64", }, ]); }); From 4cbc30f62798ac5d69d1fb3598e3ecd7dfb8b546 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:16:03 +0000 Subject: [PATCH 3/9] Fix tar expecting gz when xz was provided --- packages/host/src/server.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/host/src/server.ts b/packages/host/src/server.ts index 846dddfb..1c90a9c1 100644 --- a/packages/host/src/server.ts +++ b/packages/host/src/server.ts @@ -139,7 +139,7 @@ async function downloadAndExtractZip(url: string, targetDir: string) { * @param targetDir - Directory to extract the ZIP into * @throws Network error if status of request is not ok */ -async function downloadAndExtractTarGz(url: string, targetDir: string) { +async function downloadAndExtractTar(url: string, targetDir: string) { const res = await fetch(url); if (!res.ok) { throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`); @@ -152,7 +152,7 @@ async function downloadAndExtractTarGz(url: string, targetDir: string) { // Execute tar CLI to extract try { - await execFileAsync("tar", ["-xzf", tmpFilePath, "-C", targetDir, "--strip-components", "1"]); + await execFileAsync("tar", ["-xf", tmpFilePath, "-C", targetDir, "--strip-components", "1"]); } catch (err: any) { if (err.code === "ENOENT") { throw new Error("error executing tar command, do you have 'xz-utils' installed?"); @@ -979,7 +979,7 @@ export class FactorioServer extends events.EventEmitter { if (process.platform === "linux") { this._logger.info(`A newer version of factorio is available (${v}), starting download...`); const dst = path.join(this._factorioDir, latestVersion.version); - await downloadAndExtractTarGz(latestVersion.headlessUrl, dst); + await downloadAndExtractTar(latestVersion.headlessUrl, dst); } else { this._logger.info(`A newer version of factorio is available (${v}) but must be manually downloaded`); } From b4338a209c338249dc9e1f983bd1e1ad80f06fd7 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:30:48 +0000 Subject: [PATCH 4/9] Readd support for direct installs --- packages/host/host.ts | 26 ---------- packages/host/src/server.ts | 44 ++++++++++++----- test/host/server.js | 96 ++++++++++++++++++++++++++----------- test/integration/server.js | 6 +-- 4 files changed, 103 insertions(+), 69 deletions(-) diff --git a/packages/host/host.ts b/packages/host/host.ts index 6f9205ee..19127713 100755 --- a/packages/host/host.ts +++ b/packages/host/host.ts @@ -25,7 +25,6 @@ import * as lib from "@clusterio/lib"; import { ConsoleTransport, levels, logger } from "@clusterio/lib"; import Host from "./src/Host"; -import { getFactorioVersion } from "./src/server"; let host: Host | undefined; @@ -237,31 +236,6 @@ async function startHost() { tlsCa = await fs.readFile(tlsCaPath, "utf8"); } - // Migrate from single factorio version to multiple - const factorioDir = hostConfig.get("host.factorio_directory"); - const singleFactorioVersion = await getFactorioVersion(factorioDir); - if (singleFactorioVersion !== null) { - const dst = path.join(factorioDir, singleFactorioVersion); - logger.info(`Found single factorio version in ${factorioDir} moving to ${dst}`); - const stats = await fs.lstat(factorioDir); - if (stats.isSymbolicLink()) { - // For symlinks we want to move the file, but cant move it into itself so need a tmp move - const tmpPath = `${factorioDir}.tmp`; - await fs.move(factorioDir, tmpPath); - await fs.ensureDir(factorioDir); - await fs.move(tmpPath, dst); - } else { - // It is a directory here, otherwise no version would have been found - // We will only move known files in case a version was incorrectly installed in the dir - await fs.ensureDir(dst); - await Promise.all( - ["bin", "data", "config-path.cfg"].map(name => ( - fs.move(path.join(factorioDir, name), path.join(dst, name)) - )) - ); - } - } - let hostConnector = new HostConnector(hostConfig, tlsCa, pluginInfos); host = new Host( hostConnector, diff --git a/packages/host/src/server.ts b/packages/host/src/server.ts index 1c90a9c1..35b1028a 100644 --- a/packages/host/src/server.ts +++ b/packages/host/src/server.ts @@ -18,7 +18,7 @@ const execFileAsync = util.promisify(child_process.execFile); * * @param factorioDir - Path factorio dir containing data/changelog.txt. */ -export async function getFactorioVersion(factorioDir: string) { +async function getFactorioVersion(factorioDir: string) { let changelog; const changelogPath = path.join(factorioDir, "data", "changelog.txt"); try { @@ -35,7 +35,8 @@ export async function getFactorioVersion(factorioDir: string) { if (index !== -1) { const name = line.slice(0, index).trim(); if (name.toLowerCase() === "version") { - return line.slice(index + 1).trim(); + const version = line.slice(index + 1).trim(); + return lib.isFullVersion(version) ? version : null; } } } @@ -77,6 +78,12 @@ function versionOrder(a: string, b: string) { async function listFactorioVersions(factorioDir: string) { const versions = new Set(); + const directVersion = await getFactorioVersion(factorioDir); + if (directVersion !== null) { + versions.add(directVersion); + return { direct: true, versions }; + } + for (const name of await fs.readdir(factorioDir)) { // We do not use withFileTypes or lstat because we want to follow symlinks const fullPath = path.join(factorioDir, name); @@ -90,14 +97,14 @@ async function listFactorioVersions(factorioDir: string) { } const version = await getFactorioVersion(fullPath); - if (version === null || !lib.isFullVersion(version)) { + if (version === null) { continue; } versions.add(version); } - return versions; + return { direct: false, versions }; } /** @@ -178,6 +185,16 @@ async function downloadAndExtractTar(url: string, targetDir: string) { * @internal */ async function findVersion(factorioDir: string, targetVersion: lib.TargetVersion): Promise<[string, lib.FullVersion]> { + // Check if this is a direct installation dir + const directVersion = await getFactorioVersion(factorioDir); + if (directVersion !== null) { + if (targetVersion === "latest" || directVersion === targetVersion || directVersion.startsWith(targetVersion)) { + return [path.join(factorioDir, "data"), directVersion]; + } + throw new Error(`Unable to find Factorio version ${targetVersion}`); + } + + // Otherwise assume it is a directory of multiple versions const versions = new Map(); for (const name of await fs.readdir(factorioDir)) { // We do not use withFileTypes or lstat because we want to follow symlinks @@ -192,7 +209,7 @@ async function findVersion(factorioDir: string, targetVersion: lib.TargetVersion } const version = await getFactorioVersion(fullPath); - if (version === null || !lib.isFullVersion(version)) { + if (version === null) { continue; } @@ -973,16 +990,18 @@ export class FactorioServer extends events.EventEmitter { [0]; const installedVersions = await listFactorioVersions(this._factorioDir); - if (!installedVersions.has(latestVersion.version)) { - // Download the newer version + if (!installedVersions.versions.has(latestVersion.version)) { const v = latestVersion.version; - if (process.platform === "linux") { - this._logger.info(`A newer version of factorio is available (${v}), starting download...`); - const dst = path.join(this._factorioDir, latestVersion.version); - await downloadAndExtractTar(latestVersion.headlessUrl, dst); - } else { + // Can't download if not linux or this is a direct install + if (installedVersions.direct || process.platform !== "linux") { this._logger.info(`A newer version of factorio is available (${v}) but must be manually downloaded`); } + + // Download the newer version + this._logger.info(`A newer version of factorio is available (${v}), starting download...`); + const dst = path.join(this._factorioDir, latestVersion.version); + await downloadAndExtractTar(latestVersion.headlessUrl, dst); + this._logger.info(`Downloaded factorio version ${v}`); } } @@ -1536,6 +1555,7 @@ export class FactorioServer extends events.EventEmitter { // For testing only +export const _getFactorioVersion = getFactorioVersion; export const _versionOrder = versionOrder; export const _findVersion = findVersion; export const _listFactorioVersions = listFactorioVersions; diff --git a/test/host/server.js b/test/host/server.js index d75e5568..3ca40064 100644 --- a/test/host/server.js +++ b/test/host/server.js @@ -11,17 +11,17 @@ const { testLines } = require("../lib/factorio/lines"); describe("host/server", function() { - describe("getFactorioVersion()", function() { + describe("_getFactorioVersion()", function() { it("should get the version from a changelog", async function() { - let version = await hostServer.getFactorioVersion(path.join("test", "file", "changelogs", "good")); + let version = await hostServer._getFactorioVersion(path.join("test", "file", "changelogs", "good")); assert.equal(version, "0.1.1"); }); it("should return null if unable to find the version", async function() { - let version = await hostServer.getFactorioVersion(path.join("test", "file", "changelogs", "bad")); + let version = await hostServer._getFactorioVersion(path.join("test", "file", "changelogs", "bad")); assert.equal(version, null); }); it("should return null if file does not exist", async function() { - let version = await hostServer.getFactorioVersion(path.join("test", "file", "changelogs", "not-exists")); + let version = await hostServer._getFactorioVersion(path.join("test", "file", "changelogs", "not-exists")); assert.equal(version, null); }); }); @@ -38,30 +38,59 @@ describe("host/server", function() { }); describe("_findVersion()", function() { - it("should search given directory for latest Factorio install", async function() { - const installDir = path.join("test", "file", "factorio"); - const [dir, version] = await hostServer._findVersion(installDir, "latest"); - assert.equal(dir, path.join(installDir, "0.1.2", "data")); - assert.equal(version, "0.1.2"); + describe("direct install", function() { + it("should search given directory for latest Factorio install", async function() { + const installDir = path.join("test", "file", "factorio", "0.1.2"); + const [dir, version] = await hostServer._findVersion(installDir, "latest"); + assert.equal(dir, path.join(installDir, "data")); + assert.equal(version, "0.1.2"); + }); + it("should search given directory for given Factorio install", async function() { + const installDir = path.join("test", "file", "factorio", "0.1.1"); + const [dir, version] = await hostServer._findVersion(installDir, "0.1.1"); + assert.equal(dir, path.join(installDir, "data")); + assert.equal(version, "0.1.1"); + }); + it("should search given directory for partly given Factorio install", async function() { + const installDir = path.join("test", "file", "factorio", "0.1.2"); + const [dir, version] = await hostServer._findVersion(installDir, "0.1"); + assert.equal(dir, path.join(installDir, "data")); + assert.equal(version, "0.1.2"); + }); + it("should search given directory for latest Factorio install", async function() { + const installDir = path.join("test", "file", "factorio"); + const [dir, version] = await hostServer._findVersion(installDir, "latest"); + assert.equal(dir, path.join(installDir, "0.1.2", "data")); + assert.equal(version, "0.1.2"); + }); }); - it("should search given directory for given Factorio install", async function() { - const installDir = path.join("test", "file", "factorio"); - const [dir, version] = await hostServer._findVersion(installDir, "0.1.1"); - assert.equal(dir, path.join(installDir, "0.1.1", "data")); - assert.equal(version, "0.1.1"); - }); - it("should search given directory for partly given Factorio install", async function() { - const installDir = path.join("test", "file", "factorio"); - const [dir, version] = await hostServer._findVersion(installDir, "0.1"); - assert.equal(dir, path.join(installDir, "0.1.2", "data")); - assert.equal(version, "0.1.2"); - }); - it("should reject if no factorio install with the given version was found", async function() { - let installDir = path.join("test", "file", "factorio"); - await assert.rejects( - hostServer._findVersion(installDir, "0.1.3"), - new Error("Unable to find Factorio version 0.1.3") - ); + describe("mutli install", function() { + it("should reject if no factorio install with the given version was found", async function() { + let installDir = path.join("test", "file", "factorio"); + await assert.rejects( + hostServer._findVersion(installDir, "0.1.3"), + new Error("Unable to find Factorio version 0.1.3") + ); + }); + it("should search given directory for given Factorio install", async function() { + const installDir = path.join("test", "file", "factorio"); + const [dir, version] = await hostServer._findVersion(installDir, "0.1.1"); + assert.equal(dir, path.join(installDir, "0.1.1", "data")); + assert.equal(version, "0.1.1"); + }); + it("should search given directory for partly given Factorio install", async function() { + const installDir = path.join("test", "file", "factorio"); + const [dir, version] = await hostServer._findVersion(installDir, "0.1"); + assert.equal(dir, path.join(installDir, "0.1.2", "data")); + assert.equal(version, "0.1.2"); + }); + it("should reject if no factorio install with the given version was found", async function() { + let installDir = path.join("test", "file", "factorio"); + await assert.rejects( + hostServer._findVersion(installDir, "0.1.3"), + new Error("Unable to find Factorio version 0.1.3") + ); + }); }); it("should reject if no factorio install was found", async function() { let installDir = path.join("test", "file"); @@ -73,10 +102,21 @@ describe("host/server", function() { }); describe("_listFactorioVersions()", function() { + it("should list the version in a direct install", async function() { + const installDir = path.join("test", "file", "factorio", "0.1.1"); + const installedVersions = await hostServer._listFactorioVersions(installDir); + assert.deepEqual(installedVersions, { + direct: true, + versions: new Set(["0.1.1"]), + }); + }); it("should list all versions in a directory", async function() { const installDir = path.join("test", "file", "factorio"); const installedVersions = await hostServer._listFactorioVersions(installDir); - assert.deepEqual(installedVersions, new Set(["0.1.1", "0.1.2"])); + assert.deepEqual(installedVersions, { + direct: false, + versions: new Set(["0.1.1", "0.1.2"]), + }); }); }); diff --git a/test/integration/server.js b/test/integration/server.js index f843700d..aa5eff48 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -5,7 +5,7 @@ const fs = require("fs-extra"); const path = require("path"); const lib = require("@clusterio/lib"); -const { FactorioServer, getFactorioVersion } = require("@clusterio/host/dist/node/src/server"); +const { FactorioServer, _getFactorioVersion } = require("@clusterio/host/dist/node/src/server"); const { logger } = lib; const { slowTest, factorioDir } = require("./index"); @@ -20,7 +20,7 @@ describe("Integration of host/src/server", function() { } } - let version = await getFactorioVersion(factorioDir); + let version = await _getFactorioVersion(factorioDir); if (version !== null) { checkVersion(version); } else { @@ -30,7 +30,7 @@ describe("Integration of host/src/server", function() { } checkVersion( - await getFactorioVersion(path.join(factorioDir, entry.name)) + await _getFactorioVersion(path.join(factorioDir, entry.name)) ); } } From 372c1bffe17e1c9e0a07f58ba1426ea8dfcb81cd Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:43:28 +0000 Subject: [PATCH 5/9] Add more tests and missing return --- packages/host/src/server.ts | 1 + test/host/server.js | 283 ++++++++++++++++++++++++++++++++++++ 2 files changed, 284 insertions(+) diff --git a/packages/host/src/server.ts b/packages/host/src/server.ts index 35b1028a..8d3b31eb 100644 --- a/packages/host/src/server.ts +++ b/packages/host/src/server.ts @@ -995,6 +995,7 @@ export class FactorioServer extends events.EventEmitter { // Can't download if not linux or this is a direct install if (installedVersions.direct || process.platform !== "linux") { this._logger.info(`A newer version of factorio is available (${v}) but must be manually downloaded`); + return; } // Download the newer version diff --git a/test/host/server.js b/test/host/server.js index 3ca40064..86ac154f 100644 --- a/test/host/server.js +++ b/test/host/server.js @@ -270,5 +270,288 @@ describe("host/server", function() { await wait(21); // Wait until after shutdown timeout }); }); + + describe(".checkForUpdates()", function() { + let _fetch; + let fetchCalledWith; + let _platform = process.platform; + beforeEach(function() { + _fetch = global.fetch; + fetchCalledWith = null; + global.fetch = async function(url) { + fetchCalledWith = url; + return { + ok: false, + status: -1, + statusText: "Fetch called", + }; + }; + }); + afterEach(function() { + global.fetch = _fetch; + Object.defineProperty(process, "platform", { + value: _platform, + }); + }); + + describe("full version", function() { + it("should do nothing when there is no newer version", async function() { + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "0.1.1"; + await server.checkForUpdates([{ + stable: true, + version: "0.1.1", + headlessUrl: "test1", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test2", + }]); + + assert.equal(fetchCalledWith, null); + }); + it("should do nothing for direct installs", async function() { + server._factorioDir = path.join("test", "file", "factorio", "0.1.1"); + server._targetVersion = "0.1.5"; + await server.checkForUpdates([{ + stable: true, + version: "0.1.5", + headlessUrl: "test1", + }, { + stable: true, + version: "0.1.1", + headlessUrl: "test1", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test2", + }]); + + assert.equal(fetchCalledWith, null); + }); + it("should do nothing when on windows", async function() { + let logLine = null; + server._logger = { info: line => { logLine = line; } }; + Object.defineProperty(process, "platform", { value: "win32" }); + + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "0.1.5"; + await server.checkForUpdates([{ + stable: true, + version: "0.1.5", + headlessUrl: "test1", + }, { + stable: true, + version: "0.1.1", + headlessUrl: "test2", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test3", + }]); + + assert.equal(fetchCalledWith, null); + assert.ok(logLine !== null); + assert.ok(logLine.endsWith("but must be manually downloaded")); + }); + it("should do attempt to download on linux", async function() { + let logLine = null; + server._logger = { info: line => { logLine = line; } }; + Object.defineProperty(process, "platform", { value: "linux" }); + + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "0.1.5"; + await assert.rejects(server.checkForUpdates([{ + stable: true, + version: "0.1.5", + headlessUrl: "test1", + }, { + stable: true, + version: "0.1.1", + headlessUrl: "test2", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test3", + }]), new Error("Failed to fetch test1: -1 Fetch called")); + + assert.equal(fetchCalledWith, "test1"); + assert.ok(logLine !== null); + assert.ok(logLine.endsWith("starting download...")); + }); + }); + describe("partial version", function() { + it("should do nothing when there is no newer version", async function() { + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "0.1"; + await server.checkForUpdates([{ + stable: true, + version: "0.1.1", + headlessUrl: "test1", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test2", + }]); + + assert.equal(fetchCalledWith, null); + }); + it("should do nothing for direct installs", async function() { + server._factorioDir = path.join("test", "file", "factorio", "0.1.1"); + server._targetVersion = "0.1"; + await server.checkForUpdates([{ + stable: true, + version: "0.1.5", + headlessUrl: "test1", + }, { + stable: true, + version: "0.1.1", + headlessUrl: "test1", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test2", + }]); + + assert.equal(fetchCalledWith, null); + }); + it("should do nothing when on windows", async function() { + let logLine = null; + server._logger = { info: line => { logLine = line; } }; + Object.defineProperty(process, "platform", { value: "win32" }); + + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "0.1"; + await server.checkForUpdates([{ + stable: true, + version: "0.1.5", + headlessUrl: "test1", + }, { + stable: true, + version: "0.1.1", + headlessUrl: "test2", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test3", + }]); + + assert.equal(fetchCalledWith, null); + assert.ok(logLine !== null); + assert.ok(logLine.endsWith("but must be manually downloaded")); + }); + it("should do attempt to download on linux", async function() { + let logLine = null; + server._logger = { info: line => { logLine = line; } }; + Object.defineProperty(process, "platform", { value: "linux" }); + + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "0.1"; + await assert.rejects(server.checkForUpdates([{ + stable: true, + version: "0.1.5", + headlessUrl: "test1", + }, { + stable: true, + version: "0.1.1", + headlessUrl: "test2", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test3", + }]), new Error("Failed to fetch test1: -1 Fetch called")); + + assert.equal(fetchCalledWith, "test1"); + assert.ok(logLine !== null); + assert.ok(logLine.endsWith("starting download...")); + }); + }); + describe("latest version", function() { + it("should do nothing when there is no newer version", async function() { + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "latest"; + await server.checkForUpdates([{ + stable: true, + version: "0.1.1", + headlessUrl: "test1", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test2", + }]); + + assert.equal(fetchCalledWith, null); + }); + it("should do nothing for direct installs", async function() { + server._factorioDir = path.join("test", "file", "factorio", "0.1.1"); + server._targetVersion = "latest"; + await server.checkForUpdates([{ + stable: true, + version: "0.1.5", + headlessUrl: "test1", + }, { + stable: true, + version: "0.1.1", + headlessUrl: "test1", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test2", + }]); + + assert.equal(fetchCalledWith, null); + }); + it("should do nothing when on windows", async function() { + let logLine = null; + server._logger = { info: line => { logLine = line; } }; + Object.defineProperty(process, "platform", { value: "win32" }); + + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "latest"; + await server.checkForUpdates([{ + stable: true, + version: "0.1.5", + headlessUrl: "test1", + }, { + stable: true, + version: "0.1.1", + headlessUrl: "test2", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test3", + }]); + + assert.equal(fetchCalledWith, null); + assert.ok(logLine !== null); + assert.ok(logLine.endsWith("but must be manually downloaded")); + }); + it("should do attempt to download on linux", async function() { + let logLine = null; + server._logger = { info: line => { logLine = line; } }; + Object.defineProperty(process, "platform", { value: "linux" }); + + server._factorioDir = path.join("test", "file", "factorio"); + server._targetVersion = "latest"; + await assert.rejects(server.checkForUpdates([{ + stable: true, + version: "0.1.5", + headlessUrl: "test1", + }, { + stable: true, + version: "0.1.1", + headlessUrl: "test2", + }, { + stable: false, + version: "0.1.0", + headlessUrl: "test3", + }]), new Error("Failed to fetch test1: -1 Fetch called")); + + assert.equal(fetchCalledWith, "test1"); + assert.ok(logLine !== null); + assert.ok(logLine.endsWith("starting download...")); + }); + }); + }); }); }); From dc3205148e7ea8f4770c56c187544db1560d98d0 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:17:34 +0000 Subject: [PATCH 6/9] Add missing tests --- packages/host/src/server.ts | 11 ++++- test/host/server.js | 85 +++++++++++++++++++++++++++++++------ 2 files changed, 83 insertions(+), 13 deletions(-) diff --git a/packages/host/src/server.ts b/packages/host/src/server.ts index 8d3b31eb..23d46daa 100644 --- a/packages/host/src/server.ts +++ b/packages/host/src/server.ts @@ -128,7 +128,13 @@ async function downloadAndExtractZip(url: string, targetDir: string) { await fs.ensureDir(targetDir); await Promise.all( Object.values(zip.files).map(async (entry) => { - const entryPath = path.join(targetDir, entry.name); + const parts = entry.name.split("/").filter(Boolean); + const strippedPath = parts.slice(1).join("/"); + if (!strippedPath) { + return; // This copies the behaviour of tar --strip-components 1 + } + + const entryPath = path.join(targetDir, strippedPath); if (entry.dir) { await fs.ensureDir(entryPath); } else { @@ -156,6 +162,7 @@ async function downloadAndExtractTar(url: string, targetDir: string) { const tmpFilePath = `${targetDir}.tmp`; const buffer = Buffer.from(await res.arrayBuffer()); await fs.writeFile(tmpFilePath, buffer); + await fs.ensureDir(targetDir); // Execute tar CLI to extract try { @@ -1560,6 +1567,8 @@ export const _getFactorioVersion = getFactorioVersion; export const _versionOrder = versionOrder; export const _findVersion = findVersion; export const _listFactorioVersions = listFactorioVersions; +export const _downloadAndExtractZip = downloadAndExtractZip; +export const _downloadAndExtractTar = downloadAndExtractTar; export const _randomDynamicPort = randomDynamicPort; export const _generatePassword = generatePassword; export const _parseOutput = parseOutput; diff --git a/test/host/server.js b/test/host/server.js index 86ac154f..490b96a0 100644 --- a/test/host/server.js +++ b/test/host/server.js @@ -8,6 +8,7 @@ const hostServer = require("@clusterio/host/dist/node/src/server"); const lib = require("@clusterio/lib"); const { wait } = lib; const { testLines } = require("../lib/factorio/lines"); +const { slowTest } = require("../integration"); describe("host/server", function() { @@ -57,11 +58,12 @@ describe("host/server", function() { assert.equal(dir, path.join(installDir, "data")); assert.equal(version, "0.1.2"); }); - it("should search given directory for latest Factorio install", async function() { - const installDir = path.join("test", "file", "factorio"); - const [dir, version] = await hostServer._findVersion(installDir, "latest"); - assert.equal(dir, path.join(installDir, "0.1.2", "data")); - assert.equal(version, "0.1.2"); + it("should reject if the version does not match", async function() { + let installDir = path.join("test", "file", "0.1.1"); + await assert.rejects( + hostServer._findVersion(installDir, "0.1.2"), + new Error("Unable to find Factorio version 0.1.2") + ); }); }); describe("mutli install", function() { @@ -91,13 +93,13 @@ describe("host/server", function() { new Error("Unable to find Factorio version 0.1.3") ); }); - }); - it("should reject if no factorio install was found", async function() { - let installDir = path.join("test", "file"); - await assert.rejects( - hostServer._findVersion(installDir, "latest"), - new Error(`Unable to find any Factorio install in ${installDir}`) - ); + it("should reject if no factorio install was found", async function() { + let installDir = path.join("test", "file"); + await assert.rejects( + hostServer._findVersion(installDir, "0.0.0"), + new Error(`Unable to find any Factorio install in ${installDir}`) + ); + }); }); }); @@ -120,6 +122,50 @@ describe("host/server", function() { }); }); + describe("downloadAndExtractZip", function() { + let _fetch; + beforeEach(function() { + _fetch = global.fetch; + }); + afterEach(function() { + global.fetch = _fetch; + }); + + it("works", async function() { + const url = "https://github.com/clusterio/clusterio/archive/refs/tags/v2.0.0-alpha.22.zip"; + const downloads = path.join("temp", "test", "downloads"); + await fs.emptyDir(downloads); + await hostServer._downloadAndExtractZip(url, path.join(downloads, "zip")); + assert.ok(await fs.exists(path.join(downloads, "zip", "packages", "controller", "package.json"))); + }); + it("errors and bad status", async function() { + global.fetch = () => ({ ok: false, status: -1, statusText: "Fetch called" }); + await assert.rejects(hostServer._downloadAndExtractZip("url does not matter"), /-1 Fetch called/); + }); + }); + + describe("downloadAndExtractTar", function() { + let _fetch; + beforeEach(function() { + _fetch = global.fetch; + }); + afterEach(function() { + global.fetch = _fetch; + }); + + it("works", async function() { + const url = "https://github.com/clusterio/clusterio/archive/refs/tags/v2.0.0-alpha.22.tar.gz"; + const downloads = path.join("temp", "test", "downloads"); + await fs.emptyDir(downloads); + await hostServer._downloadAndExtractTar(url, path.join(downloads, "tar")); + assert.ok(await fs.exists(path.join(downloads, "tar", "packages", "controller", "package.json"))); + }); + it("errors and bad status", async function() { + global.fetch = () => ({ ok: false, status: -1, statusText: "Fetch called" }); + await assert.rejects(hostServer._downloadAndExtractZip("url does not matter"), /-1 Fetch called/); + }); + }); + describe("randomDynamicPort()", function() { it("should return a port number", function() { let port = hostServer._randomDynamicPort(); @@ -289,6 +335,7 @@ describe("host/server", function() { }); afterEach(function() { global.fetch = _fetch; + server._factorioDir = path.join("test", "file", "factorio"); Object.defineProperty(process, "platform", { value: _platform, }); @@ -552,6 +599,20 @@ describe("host/server", function() { assert.ok(logLine.endsWith("starting download...")); }); }); + it("should download a version correctly (live api)", async function() { + slowTest(this); + if (_platform !== "linux") { + this.skip(); + } + + server._factorioDir = path.join("test", "file", "factorioDownload"); + server._targetVersion = "latest"; + await server.checkForUpdates([{ + stable: true, + version: "2.0.73", + headlessUrl: "https://www.factorio.com/get-download/2.0.73/headless/linux64", + }]); + }); }); }); }); From fd43a0d2386419502a29fcd04f35cad88f3db21f Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:26:17 +0000 Subject: [PATCH 7/9] Fix two tests --- test/host/server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/host/server.js b/test/host/server.js index 490b96a0..3b20b1a8 100644 --- a/test/host/server.js +++ b/test/host/server.js @@ -59,7 +59,7 @@ describe("host/server", function() { assert.equal(version, "0.1.2"); }); it("should reject if the version does not match", async function() { - let installDir = path.join("test", "file", "0.1.1"); + let installDir = path.join("test", "file", "factorio", "0.1.1"); await assert.rejects( hostServer._findVersion(installDir, "0.1.2"), new Error("Unable to find Factorio version 0.1.2") @@ -607,6 +607,7 @@ describe("host/server", function() { server._factorioDir = path.join("test", "file", "factorioDownload"); server._targetVersion = "latest"; + await fs.emptyDir(server._factorioDir); await server.checkForUpdates([{ stable: true, version: "2.0.73", From 098aceaf108cca63c165c214df49437b828ec3e4 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:34:43 +0000 Subject: [PATCH 8/9] Forgot to restore fetch in live test --- test/host/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/host/server.js b/test/host/server.js index 3b20b1a8..4353c4ac 100644 --- a/test/host/server.js +++ b/test/host/server.js @@ -607,6 +607,7 @@ describe("host/server", function() { server._factorioDir = path.join("test", "file", "factorioDownload"); server._targetVersion = "latest"; + global.fetch = _fetch; await fs.emptyDir(server._factorioDir); await server.checkForUpdates([{ stable: true, From 0a5d0c374b1beb4ab760c28d6eab8abd84232b27 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sun, 8 Feb 2026 21:44:04 +0000 Subject: [PATCH 9/9] Response to comments - Increase timeout on download test - Fix wrong function being called --- test/host/server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/host/server.js b/test/host/server.js index 4353c4ac..28d21054 100644 --- a/test/host/server.js +++ b/test/host/server.js @@ -132,6 +132,7 @@ describe("host/server", function() { }); it("works", async function() { + slowTest(this); const url = "https://github.com/clusterio/clusterio/archive/refs/tags/v2.0.0-alpha.22.zip"; const downloads = path.join("temp", "test", "downloads"); await fs.emptyDir(downloads); @@ -162,7 +163,7 @@ describe("host/server", function() { }); it("errors and bad status", async function() { global.fetch = () => ({ ok: false, status: -1, statusText: "Fetch called" }); - await assert.rejects(hostServer._downloadAndExtractZip("url does not matter"), /-1 Fetch called/); + await assert.rejects(hostServer._downloadAndExtractTar("url does not matter"), /-1 Fetch called/); }); });