mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Create FactorioServer and Instance through async factories
FactorioServer.fromOptions finds the Factorio install and generates the RCON password before constructing, so _version and _dataDir are never null and setTargetVersion and init are gone. checkForUpdates is now a module function called before fromOptions. Instance.fromConfig resolves the version, downloads Factorio if needed and builds the server, then constructs the instance with it. It runs before the instance is connected, so the starting and stopped statuses and the version requests go through the host link. FactorioVersionsRequest and LatestReleasesRequest now accept host as a source, and the host tracks starting instances so a second start during a download is rejected. Plugin loading stays a separate loadPlugins step after the instance is connected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3a81cfd94b
commit
abb04f3258
12 files changed
+364
-221
No files matched your search
@@ -322,6 +322,8 @@ export default class Host extends lib.Link {
|
||||
_startup = true;
|
||||
_disconnecting = false;
|
||||
_shuttingDown = false;
|
||||
/** Instances waiting on {@link Instance.fromConfig} */
|
||||
_startingInstances = new Set<number>();
|
||||
|
||||
startedAtMs = Date.now();
|
||||
|
||||
@@ -764,7 +766,7 @@ export default class Host extends lib.Link {
|
||||
this.send(
|
||||
new lib.InstanceStatusChangedEvent(
|
||||
instanceId,
|
||||
instanceConnection ? instanceConnection.instance.status : "stopped",
|
||||
instanceConnection ? instanceConnection.instance.status : this._unconnectedStatus(instanceId),
|
||||
instanceConnection ? instanceConnection.instance.server.gamePort : this.gamePort(instanceId),
|
||||
instanceConnection
|
||||
? instanceConnection.instance.server.version
|
||||
@@ -880,6 +882,11 @@ export default class Host extends lib.Link {
|
||||
}
|
||||
}
|
||||
|
||||
/** Status of an instance without a connection */
|
||||
_unconnectedStatus(instanceId: number) {
|
||||
return this._startingInstances.has(instanceId) ? "starting" as const : "stopped" as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and connect an unloaded instance
|
||||
*
|
||||
@@ -888,22 +895,27 @@ export default class Host extends lib.Link {
|
||||
*/
|
||||
async _connectInstance(instanceId: number) {
|
||||
let unloadedInstance = this.getRequestInstance(instanceId);
|
||||
if (this.instanceConnections.has(instanceId)) {
|
||||
if (this.instanceConnections.has(instanceId) || this._startingInstances.has(instanceId)) {
|
||||
throw new lib.RequestError(`Instance with ID ${instanceId} is running`);
|
||||
}
|
||||
|
||||
let hostAddress = new lib.Address(lib.Address.host, this.config.get("host.id"));
|
||||
let instanceAddress = new lib.Address(lib.Address.instance, instanceId);
|
||||
let [connectionClient, connectionServer] = lib.VirtualConnector.makePair(instanceAddress, hostAddress);
|
||||
let instance = new Instance(
|
||||
this, connectionClient,
|
||||
unloadedInstance.path, this.config.get("host.factorio_directory"),
|
||||
unloadedInstance.config
|
||||
);
|
||||
let instance: Instance;
|
||||
this._startingInstances.add(instanceId);
|
||||
try {
|
||||
instance = await Instance.fromConfig(
|
||||
unloadedInstance.config, this, connectionClient,
|
||||
unloadedInstance.path, this.config.get("host.factorio_directory"),
|
||||
);
|
||||
} finally {
|
||||
this._startingInstances.delete(instanceId);
|
||||
}
|
||||
let instanceConnection = new InstanceConnection(connectionServer, this, instance);
|
||||
|
||||
this.instanceConnections.set(instanceId, instanceConnection);
|
||||
await instance.init(this.pluginInfos);
|
||||
await instance.loadPlugins(this.pluginInfos);
|
||||
|
||||
return instanceConnection;
|
||||
}
|
||||
@@ -1259,7 +1271,7 @@ export default class Host extends lib.Link {
|
||||
let instanceConnection = this.instanceConnections.get(instanceId);
|
||||
list.push(new lib.HostInstanceUpdate(
|
||||
instance.config.toRemote("controller"),
|
||||
instanceConnection ? instanceConnection.instance.status : "stopped",
|
||||
instanceConnection ? instanceConnection.instance.status : this._unconnectedStatus(instanceId),
|
||||
instanceConnection ? instanceConnection.instance.server.gamePort : this.gamePort(instanceId),
|
||||
instanceConnection ? instanceConnection.instance.server.version : undefined,
|
||||
));
|
||||
|
||||
@@ -10,7 +10,7 @@ const execAsync = util.promisify(exec);
|
||||
// internal libraries
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
import { FactorioServer } from "./server.js";
|
||||
import { FactorioServer, checkForUpdates } from "./server.js";
|
||||
import { loadModules, patch } from "./patch.js";
|
||||
import { exportData } from "./export.js";
|
||||
import type Host from "./Host.js";
|
||||
@@ -32,6 +32,13 @@ const scriptCommands = [
|
||||
"/silent-command", "/sc",
|
||||
];
|
||||
|
||||
function instanceLogger(config: lib.InstanceConfig) {
|
||||
return lib.logger.child({
|
||||
instance_id: config.get("instance.id"),
|
||||
instance_name: config.get("instance.name"),
|
||||
});
|
||||
}
|
||||
|
||||
// Statuses the instance passes through on its way to running or stopped.
|
||||
const transitoryStatuses: ReadonlySet<Instance["_status"]> = new Set([
|
||||
"starting", "stopping", "creating_save", "exporting_data",
|
||||
@@ -191,8 +198,8 @@ export default class Instance extends lib.Link {
|
||||
host: Host,
|
||||
connector: lib.VirtualConnector,
|
||||
dir: string,
|
||||
factorioDir: string,
|
||||
instanceConfig: lib.InstanceConfig
|
||||
instanceConfig: lib.InstanceConfig,
|
||||
server: FactorioServer,
|
||||
) {
|
||||
super(connector);
|
||||
this._host = host;
|
||||
@@ -202,10 +209,7 @@ export default class Instance extends lib.Link {
|
||||
|
||||
this.id = this.config.get("instance.id");
|
||||
|
||||
this.logger = lib.logger.child({
|
||||
instance_id: this.id,
|
||||
instance_name: this.name,
|
||||
});
|
||||
this.logger = instanceLogger(instanceConfig);
|
||||
|
||||
this.hooks = new InstanceHooks(this.logger);
|
||||
|
||||
@@ -225,28 +229,7 @@ export default class Instance extends lib.Link {
|
||||
};
|
||||
this.config.on("fieldChanged", this._configFieldChanged);
|
||||
|
||||
let serverOptions = {
|
||||
logger: this.logger,
|
||||
// TODO: factorio.version is not validated
|
||||
version: this.config.get("factorio.version") as lib.TargetVersion,
|
||||
executablePath: this.config.get("factorio.executable_path") ?? 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,
|
||||
enableLuaUdp: this.config.get("factorio.enable_lua_udp"),
|
||||
luaUdpPort: this.config.get("factorio.lua_udp_port") ?? undefined,
|
||||
enableWhitelist: this.config.get("factorio.enable_whitelist"),
|
||||
enableAuthserverBans: this.config.get("factorio.enable_authserver_bans"),
|
||||
verboseLogging: this.config.get("factorio.verbose_logging"),
|
||||
consoleLogging: this.config.get("factorio.console_logging"),
|
||||
stripPaths: this.config.get("factorio.strip_paths"),
|
||||
maxConcurrentCommands: this.config.get("factorio.max_concurrent_commands"),
|
||||
shutdownTimeoutMs: this.config.get("factorio.shutdown_timeout") * 1000,
|
||||
};
|
||||
|
||||
this.server = new FactorioServer(
|
||||
factorioDir, this._dir, serverOptions
|
||||
);
|
||||
this.server = server;
|
||||
|
||||
this.server.on("output", (parsed, line) => {
|
||||
this.logger.log("server", { message: line, instance_id: this.id, parsed });
|
||||
@@ -670,20 +653,22 @@ end`.replace(/\r?\n/g, " ");
|
||||
this._saveStats().catch(err => this.logger.error(`Error saving stats:\n${err.stack}`));
|
||||
}
|
||||
|
||||
async _loadStats() {
|
||||
static async _readStats(statsPath: string) {
|
||||
let instanceStats;
|
||||
try {
|
||||
instanceStats = JSON.parse(await fs.readFile(this.path("instance-stats.json"), "utf8"));
|
||||
instanceStats = JSON.parse(await fs.readFile(statsPath, "utf8"));
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
return;
|
||||
return { playerStats: new Map<string, lib.PlayerStats>(), playerAutosaveSlot: 1 };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
this.playerStats = new Map(instanceStats["players"].map(
|
||||
([id, stats]: [number, Static<typeof lib.PlayerStats.jsonSchema>]) => [id, new lib.PlayerStats(stats)])
|
||||
);
|
||||
this._playerAutosaveSlot = instanceStats["player_autosave_slot"] || 1;
|
||||
return {
|
||||
playerStats: new Map<string, lib.PlayerStats>(instanceStats["players"].map(
|
||||
([id, stats]: [string, Static<typeof lib.PlayerStats.jsonSchema>]) => [id, new lib.PlayerStats(stats)]
|
||||
)),
|
||||
playerAutosaveSlot: (instanceStats["player_autosave_slot"] as number) || 1,
|
||||
};
|
||||
}
|
||||
|
||||
async _saveStats() {
|
||||
@@ -694,21 +679,35 @@ end`.replace(/\r?\n/g, " ");
|
||||
await lib.safeOutputFile(this.path("instance-stats.json"), content);
|
||||
}
|
||||
|
||||
async init(pluginInfos: lib.PluginNodeEnvInfo[]) {
|
||||
// After changing the status this function is responsible for calling notifyExit if it throws.
|
||||
this.notifyStatus("starting");
|
||||
/**
|
||||
* Create an instance with a Factorio server, downloading Factorio if needed
|
||||
*
|
||||
* The host reports the starting status for the instance while this runs.
|
||||
*/
|
||||
static async fromConfig(
|
||||
config: lib.InstanceConfig,
|
||||
host: Host,
|
||||
connector: lib.VirtualConnector,
|
||||
dir: string,
|
||||
factorioDir: string,
|
||||
) {
|
||||
const id = config.get("instance.id");
|
||||
// TODO: factorio.version is not validated
|
||||
const targetVersion = config.get("factorio.version") as lib.TargetVersion;
|
||||
const gamePort = config.get("factorio.game_port") ?? await host.assignFreeGamePort(id);
|
||||
host.send(new lib.InstanceStatusChangedEvent(id, "starting", gamePort, targetVersion));
|
||||
|
||||
let server: FactorioServer;
|
||||
let stats: Awaited<ReturnType<typeof Instance._readStats>>;
|
||||
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();
|
||||
const factorioVersions = await host.sendTo("controller", new lib.FactorioVersionsRequest());
|
||||
stats = await Instance._readStats(path.join(dir, "instance-stats.json"));
|
||||
// Resolve a release channel target (e.g. "stable") to a concrete
|
||||
// version using the controller's cached latest-releases data, so the
|
||||
// existing download/lookup logic can fetch and run it.
|
||||
const targetVersion = this.config.get("factorio.version") as string;
|
||||
let version = targetVersion;
|
||||
if (lib.isReleaseChannel(targetVersion)) {
|
||||
const releases = await this.sendTo("controller", new lib.LatestReleasesRequest());
|
||||
const releases = await host.sendTo("controller", new lib.LatestReleasesRequest());
|
||||
const resolved = lib.resolveReleaseChannel(releases, targetVersion);
|
||||
if (!resolved) {
|
||||
throw new Error(
|
||||
@@ -716,16 +715,46 @@ end`.replace(/\r?\n/g, " ");
|
||||
"latest-releases unavailable and no cached copy on the controller."
|
||||
);
|
||||
}
|
||||
this.server.setTargetVersion(resolved);
|
||||
version = resolved;
|
||||
}
|
||||
await this.server.checkForUpdates(factorioVersions);
|
||||
await this.server.init();
|
||||
const logger = instanceLogger(config);
|
||||
await checkForUpdates(factorioDir, version, factorioVersions, logger);
|
||||
server = await FactorioServer.fromOptions(factorioDir, dir, {
|
||||
logger,
|
||||
version,
|
||||
executablePath: config.get("factorio.executable_path") ?? undefined,
|
||||
gamePort,
|
||||
rconPort: config.get("factorio.rcon_port") ?? undefined,
|
||||
rconPassword: config.get("factorio.rcon_password") ?? undefined,
|
||||
enableLuaUdp: config.get("factorio.enable_lua_udp"),
|
||||
luaUdpPort: config.get("factorio.lua_udp_port") ?? undefined,
|
||||
enableWhitelist: config.get("factorio.enable_whitelist"),
|
||||
enableAuthserverBans: config.get("factorio.enable_authserver_bans"),
|
||||
verboseLogging: config.get("factorio.verbose_logging"),
|
||||
consoleLogging: config.get("factorio.console_logging"),
|
||||
stripPaths: config.get("factorio.strip_paths"),
|
||||
maxConcurrentCommands: config.get("factorio.max_concurrent_commands"),
|
||||
shutdownTimeoutMs: config.get("factorio.shutdown_timeout") * 1000,
|
||||
});
|
||||
} catch (err) {
|
||||
this.notifyExit();
|
||||
await this.sendSaveListUpdate();
|
||||
host.send(new lib.InstanceStatusChangedEvent(id, "stopped", gamePort, targetVersion));
|
||||
await host.sendSaveListUpdate(id, path.join(dir, "saves"));
|
||||
throw err;
|
||||
}
|
||||
|
||||
const instance = new this(host, connector, dir, config, server);
|
||||
instance._status = "starting";
|
||||
instance.playerStats = stats.playerStats;
|
||||
instance._playerAutosaveSlot = stats.playerAutosaveSlot;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the instance plugins and report the instance as initialised
|
||||
*
|
||||
* Must be called once the instance is connected to the host.
|
||||
*/
|
||||
async loadPlugins(pluginInfos: lib.PluginNodeEnvInfo[]) {
|
||||
// load plugins
|
||||
const plugins: Record<string, string> = {};
|
||||
for (let pluginInfo of pluginInfos) {
|
||||
|
||||
+98
-94
@@ -185,6 +185,73 @@ async function downloadAndExtractTar(url: string, targetDir: string) {
|
||||
// Factorio downloads in progress by target directory, shared by every server in the process
|
||||
const downloadsInProgress = new Map<string, Promise<void>>();
|
||||
|
||||
/**
|
||||
* Download the newest Factorio version matching the target if not installed
|
||||
*
|
||||
* Call before {@link FactorioServer.fromOptions} for it to find the new version.
|
||||
*
|
||||
* @param factorioDir - Path to Factorio installation dirs.
|
||||
* @param targetVersion - Version to look for, or "latest".
|
||||
* @param availableVersions - Versions available for download.
|
||||
* @param logger - Logger to report the download on.
|
||||
*/
|
||||
export async function checkForUpdates(
|
||||
factorioDir: string,
|
||||
targetVersion: lib.TargetVersion,
|
||||
availableVersions: lib.ExternalFactorioVersion[],
|
||||
logger: lib.Logger = lib.logger,
|
||||
) {
|
||||
|
||||
const latestVersion = availableVersions
|
||||
.filter(v => (
|
||||
targetVersion === "latest" || v.version === targetVersion || v.version.startsWith(targetVersion)
|
||||
))
|
||||
.sort((v1, v2) => versionOrder(v1.version, v2.version))
|
||||
[0];
|
||||
|
||||
if (!latestVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A version that is still being extracted can already look installed
|
||||
const dst = path.join(factorioDir, latestVersion.version);
|
||||
const installedVersions = await listFactorioVersions(factorioDir);
|
||||
if (!installedVersions.versions.has(latestVersion.version) || downloadsInProgress.has(dst)) {
|
||||
const v = latestVersion.version;
|
||||
// Can't download if not linux or this is a direct install
|
||||
if (installedVersions.direct) {
|
||||
logger.info(
|
||||
`A newer version of factorio is available (${v}) but cannot be downloaded automatically ` +
|
||||
`because ${factorioDir} is a direct (single-version) Factorio install. Use a versioned ` +
|
||||
`layout, where ${factorioDir} contains a subdirectory per version, to enable automatic ` +
|
||||
"downloads."
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (process.platform !== "linux") {
|
||||
logger.info(
|
||||
`A newer version of factorio is available (${v}) but must be manually downloaded ` +
|
||||
"(automatic downloads are only supported on Linux)."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Download the newer version, or wait for another server already downloading it
|
||||
let download = downloadsInProgress.get(dst);
|
||||
if (download) {
|
||||
logger.info(`A newer version of factorio is available (${v}), waiting for download...`);
|
||||
} else {
|
||||
logger.info(`A newer version of factorio is available (${v}), starting download...`);
|
||||
download = downloadAndExtractTar(latestVersion.headlessUrl, dst).finally(() => {
|
||||
downloadsInProgress.delete(dst);
|
||||
});
|
||||
downloadsInProgress.set(dst, download);
|
||||
}
|
||||
await download;
|
||||
logger.info(`Downloaded factorio version ${v}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find factorio data directory of the given version
|
||||
*
|
||||
@@ -670,9 +737,8 @@ export class FactorioServer extends events.EventEmitter<FactorioServerEvents> {
|
||||
_factorioDir: string;
|
||||
_writeDir: string;
|
||||
|
||||
// Resolved in init
|
||||
_version: lib.FullVersion | null = null;
|
||||
_dataDir: string | null = null;
|
||||
_version: lib.FullVersion;
|
||||
_dataDir: string;
|
||||
|
||||
// Due to inconsistencies in the factorio api, we must manually watch the whitelist
|
||||
// https://forums.factorio.com/viewtopic.php?t=123673
|
||||
@@ -680,8 +746,7 @@ export class FactorioServer extends events.EventEmitter<FactorioServerEvents> {
|
||||
_whitelist = new Set<string>();
|
||||
|
||||
_logger: lib.Logger;
|
||||
_targetVersion: lib.TargetVersion;
|
||||
_state: "new" | "init" | "create" | "running" | "stopping" = "new";
|
||||
_state: "init" | "create" | "running" | "stopping" = "init";
|
||||
_server: child_process.ChildProcessWithoutNullStreams | null = null;
|
||||
_rconClient: Rcon | null= null;
|
||||
_rconReady = false;
|
||||
@@ -697,27 +762,36 @@ export class FactorioServer extends events.EventEmitter<FactorioServerEvents> {
|
||||
|
||||
|
||||
/**
|
||||
* Create a Factorio server interface
|
||||
* Create a Factorio server interface for the version found by {@link fromOptions}
|
||||
*
|
||||
* @param factorioDir - Directory of the Factorio install(s).
|
||||
* @param writeDir - Directory to write runtime data to.
|
||||
* @param options - Optional parameters.
|
||||
* @param dataDir - Data directory of the Factorio version to run.
|
||||
* @param version - Factorio version to run.
|
||||
* @param options - Optional parameters, `version` is ignored.
|
||||
*/
|
||||
constructor(factorioDir: string, writeDir: string, options: FactorioServerOptions) {
|
||||
constructor(
|
||||
factorioDir: string,
|
||||
writeDir: string,
|
||||
dataDir: string,
|
||||
version: lib.FullVersion,
|
||||
options: FactorioServerOptions & { rconPassword: string },
|
||||
) {
|
||||
super();
|
||||
|
||||
this._factorioDir = factorioDir;
|
||||
this._writeDir = writeDir;
|
||||
this._dataDir = dataDir;
|
||||
this._version = version;
|
||||
|
||||
this._logger = options.logger || lib.logger;
|
||||
this._targetVersion = options.version || "latest";
|
||||
this.executablePath = options.executablePath;
|
||||
/** UDP port used for hosting the Factorio game server on */
|
||||
this.gamePort = options.gamePort || randomDynamicPort();
|
||||
/** TCP port used for RCON on the Factorio game server */
|
||||
this.rconPort = options.rconPort || randomDynamicPort();
|
||||
/** Password used for RCON on the Factorio game server */
|
||||
this.rconPassword = options.rconPassword as string; // init will generate one if not there
|
||||
this.rconPassword = options.rconPassword;
|
||||
/** Enable UDP messaging with the Factorio game server */
|
||||
this.enableLuaUdp = options.enableLuaUdp || false;
|
||||
/** UDP port the Factorio game server receives Lua UDP messages on */
|
||||
@@ -886,7 +960,7 @@ export class FactorioServer extends events.EventEmitter<FactorioServerEvents> {
|
||||
|
||||
// Factorio before 2.1.10 crashes when a packet is received on a headless server
|
||||
_checkLuaUdpVersion() {
|
||||
if (this.enableLuaUdp && lib.integerFullVersion(this._version!) < lib.integerFullVersion("2.1.10")) {
|
||||
if (this.enableLuaUdp && lib.integerFullVersion(this._version) < lib.integerFullVersion("2.1.10")) {
|
||||
throw new Error("Lua UDP requires Factorio 2.1.10 or later");
|
||||
}
|
||||
}
|
||||
@@ -1075,94 +1149,25 @@ export class FactorioServer extends events.EventEmitter<FactorioServerEvents> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the target version of Factorio to run
|
||||
* Create a Factorio server interface for an installed version
|
||||
*
|
||||
* Used to substitute a concrete version for a release channel target
|
||||
* (e.g. "stable" or "experimental") before {@link checkForUpdates} and
|
||||
* {@link init} run. Must be called before the server is initialized.
|
||||
*/
|
||||
setTargetVersion(version: lib.TargetVersion) {
|
||||
this._check(["new"]);
|
||||
this._targetVersion = version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize class instance
|
||||
*
|
||||
* Must be called before instances of this class can be used.
|
||||
* @param factorioDir - Directory of the Factorio install(s).
|
||||
* @param writeDir - Directory to write runtime data to.
|
||||
* @param options - Optional parameters.
|
||||
* @throws {Error} if the requested version of Factorio was not found.
|
||||
*/
|
||||
async init() {
|
||||
this._check(["new"]);
|
||||
[this._dataDir, this._version] = await findVersion(this._factorioDir, this._targetVersion);
|
||||
this.rconPassword = this.rconPassword as string | undefined ?? await generatePassword(10);
|
||||
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];
|
||||
|
||||
if (!latestVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A version that is still being extracted can already look installed
|
||||
const dst = path.join(this._factorioDir, latestVersion.version);
|
||||
const installedVersions = await listFactorioVersions(this._factorioDir);
|
||||
if (!installedVersions.versions.has(latestVersion.version) || downloadsInProgress.has(dst)) {
|
||||
const v = latestVersion.version;
|
||||
// Can't download if not linux or this is a direct install
|
||||
if (installedVersions.direct) {
|
||||
this._logger.info(
|
||||
`A newer version of factorio is available (${v}) but cannot be downloaded automatically ` +
|
||||
`because ${this._factorioDir} is a direct (single-version) Factorio install. Use a versioned ` +
|
||||
`layout, where ${this._factorioDir} contains a subdirectory per version, to enable automatic ` +
|
||||
"downloads."
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (process.platform !== "linux") {
|
||||
this._logger.info(
|
||||
`A newer version of factorio is available (${v}) but must be manually downloaded ` +
|
||||
"(automatic downloads are only supported on Linux)."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Download the newer version, or wait for another server already downloading it
|
||||
let download = downloadsInProgress.get(dst);
|
||||
if (download) {
|
||||
this._logger.info(`A newer version of factorio is available (${v}), waiting for download...`);
|
||||
} else {
|
||||
this._logger.info(`A newer version of factorio is available (${v}), starting download...`);
|
||||
download = downloadAndExtractTar(latestVersion.headlessUrl, dst).finally(() => {
|
||||
downloadsInProgress.delete(dst);
|
||||
});
|
||||
downloadsInProgress.set(dst, download);
|
||||
}
|
||||
await download;
|
||||
this._logger.info(`Downloaded factorio version ${v}`);
|
||||
}
|
||||
static async fromOptions(factorioDir: string, writeDir: string, options: FactorioServerOptions) {
|
||||
const [dataDir, version] = await findVersion(factorioDir, options.version || "latest");
|
||||
const rconPassword = options.rconPassword ?? await generatePassword(10);
|
||||
return new this(factorioDir, writeDir, dataDir, version, { ...options, rconPassword });
|
||||
}
|
||||
|
||||
/**
|
||||
* The version of Factorio in use. This will be the actual version if
|
||||
* "latest" (the default) was specified as `factorioVersion` to the constructor.
|
||||
* This will be undefined before the server is initialized, or if init failed.
|
||||
* "latest" (the default) was passed to {@link fromOptions}.
|
||||
*/
|
||||
get version(): lib.FullVersion | undefined {
|
||||
return this._version ?? undefined;
|
||||
get version(): lib.FullVersion {
|
||||
return this._version;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1681,14 +1686,13 @@ export class FactorioServer extends events.EventEmitter<FactorioServerEvents> {
|
||||
* Return path in data directory
|
||||
*
|
||||
* Creates a path using path.join with the given parts that's relative to
|
||||
* the data directory of the Factorio server. Not valid before init has
|
||||
* been called.
|
||||
* the data directory of the Factorio server.
|
||||
*
|
||||
* @param parts - Extra parts to add to the data path.
|
||||
* @returns Data directory path.
|
||||
*/
|
||||
dataPath(...parts: string[]) {
|
||||
return path.join(this._dataDir!, ...parts);
|
||||
return path.join(this._dataDir, ...parts);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -432,7 +432,7 @@ export class DebugWsMessageEvent {
|
||||
export class FactorioVersionsRequest {
|
||||
declare ["constructor"]: typeof FactorioVersionsRequest;
|
||||
static type = "request" as const;
|
||||
static src = ["control", "instance"] as const;
|
||||
static src = ["control", "host", "instance"] as const;
|
||||
static dst = "controller" as const;
|
||||
static permission = "core.external.get_factorio_versions" as const;
|
||||
|
||||
@@ -454,7 +454,7 @@ export class FactorioVersionsRequest {
|
||||
export class LatestReleasesRequest {
|
||||
declare ["constructor"]: typeof LatestReleasesRequest;
|
||||
static type = "request" as const;
|
||||
static src = ["control", "instance"] as const;
|
||||
static src = ["control", "host", "instance"] as const;
|
||||
static dst = "controller" as const;
|
||||
static permission = "core.external.get_latest_releases" as const;
|
||||
|
||||
|
||||
@@ -77,6 +77,29 @@ describe("host/src/Host", function() {
|
||||
await fs.access(modsDirectory);
|
||||
});
|
||||
});
|
||||
describe("._connectInstance()", function() {
|
||||
const originalFromConfig = Instance.fromConfig;
|
||||
afterEach(function() {
|
||||
Instance.fromConfig = originalFromConfig;
|
||||
});
|
||||
it("should reject a second start while the instance is starting", async function() {
|
||||
const hostConfig = new lib.HostConfig("host");
|
||||
const host = new Host(new HostConnector(hostConfig, []), hostConfig, []);
|
||||
const instanceConfig = new lib.InstanceConfig("host", { "instance.id": 7 });
|
||||
host.assignedInstances.set(7, { path: "dir", config: instanceConfig });
|
||||
let fail;
|
||||
Instance.fromConfig = () => new Promise((resolve, reject) => { fail = reject; });
|
||||
|
||||
const first = host._connectInstance(7);
|
||||
await assert.rejects(host._connectInstance(7), /Instance with ID 7 is running/);
|
||||
assert.equal(host._unconnectedStatus(7), "starting");
|
||||
|
||||
fail(new Error("start failed"));
|
||||
await assert.rejects(first, /start failed/);
|
||||
assert.equal(host._unconnectedStatus(7), "stopped");
|
||||
assert.equal(host.instanceConnections.has(7), false);
|
||||
});
|
||||
});
|
||||
describe(".handleSyncUserListsEvent()", function() {
|
||||
const hostAddress = lib.Address.fromShorthand({ hostId: 1 });
|
||||
const instanceAddress = lib.Address.fromShorthand({ instanceId: 1 });
|
||||
|
||||
+75
-2
@@ -1,4 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
@@ -12,6 +13,79 @@ const addr = lib.Address.fromShorthand;
|
||||
|
||||
|
||||
describe("class Instance", function() {
|
||||
describe(".fromConfig()", function() {
|
||||
const factorioDir = path.join("test", "file", "factorio");
|
||||
const dir = path.join("temp", "test", "instance_from_config");
|
||||
let host;
|
||||
let sent;
|
||||
let saveListUpdates;
|
||||
let requests;
|
||||
function makeConfig(fields) {
|
||||
return new lib.InstanceConfig("host", { "instance.id": 5, "instance.name": "foo", ...fields });
|
||||
}
|
||||
function fromConfig(config) {
|
||||
const connector = new MockConnector(addr({ instanceId: 5 }), addr({ hostId: 1 }));
|
||||
return Instance.fromConfig(config, host, connector, dir, factorioDir);
|
||||
}
|
||||
beforeEach(async function() {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
sent = [];
|
||||
saveListUpdates = [];
|
||||
requests = [];
|
||||
host = {
|
||||
assignFreeGamePort: async () => 34100,
|
||||
send(event) { sent.push(event); },
|
||||
async sendTo(dst, request) {
|
||||
requests.push(request.constructor.name);
|
||||
if (request instanceof lib.FactorioVersionsRequest) {
|
||||
return [];
|
||||
}
|
||||
if (request instanceof lib.LatestReleasesRequest) {
|
||||
return { stable: { headless: "0.1.1" } };
|
||||
}
|
||||
throw new Error(`Unexpected request ${request.constructor.name}`);
|
||||
},
|
||||
async sendSaveListUpdate(instanceId, savesDir) { saveListUpdates.push([instanceId, savesDir]); },
|
||||
};
|
||||
});
|
||||
|
||||
it("should create an instance with a server for the installed version", async function() {
|
||||
const instance = await fromConfig(makeConfig({}));
|
||||
assert.equal(instance.status, "starting");
|
||||
assert.equal(instance.server.version, "0.1.2");
|
||||
assert.equal(instance.server.gamePort, 34100);
|
||||
assert.deepEqual(sent, [new lib.InstanceStatusChangedEvent(5, "starting", 34100, "latest")]);
|
||||
assert.deepEqual(requests, ["FactorioVersionsRequest"]);
|
||||
});
|
||||
it("should use the configured game port", async function() {
|
||||
const instance = await fromConfig(makeConfig({ "factorio.game_port": 34200 }));
|
||||
assert.equal(instance.server.gamePort, 34200);
|
||||
});
|
||||
it("should resolve a release channel through the controller", async function() {
|
||||
const instance = await fromConfig(makeConfig({ "factorio.version": "stable" }));
|
||||
assert.equal(instance.server.version, "0.1.1");
|
||||
assert.deepEqual(requests, ["FactorioVersionsRequest", "LatestReleasesRequest"]);
|
||||
});
|
||||
it("should load player stats", async function() {
|
||||
await fs.writeFile(path.join(dir, "instance-stats.json"), JSON.stringify({
|
||||
players: [["player", { join_count: 2 }]],
|
||||
player_autosave_slot: 3,
|
||||
}));
|
||||
const instance = await fromConfig(makeConfig({}));
|
||||
assert.equal(instance.playerStats.get("player").joinCount, 2);
|
||||
assert.equal(instance._playerAutosaveSlot, 3);
|
||||
});
|
||||
it("should report the instance stopped if the version is not installed", async function() {
|
||||
await assert.rejects(fromConfig(makeConfig({ "factorio.version": "0.1.5" })), /0\.1\.5/);
|
||||
assert.deepEqual(sent, [
|
||||
new lib.InstanceStatusChangedEvent(5, "starting", 34100, "0.1.5"),
|
||||
new lib.InstanceStatusChangedEvent(5, "stopped", 34100, "0.1.5"),
|
||||
]);
|
||||
assert.deepEqual(saveListUpdates, [[5, path.join(dir, "saves")]]);
|
||||
});
|
||||
});
|
||||
|
||||
let src;
|
||||
let dst = addr({ hostId: 1 });
|
||||
let instance;
|
||||
@@ -21,8 +95,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({ assignGamePort: () => 1 }, connector, "dir", "factorioDir", instanceConfig);
|
||||
instance.server = new MockServer();
|
||||
instance = new Instance({}, connector, "dir", instanceConfig, new MockServer());
|
||||
});
|
||||
|
||||
describe(".name", function() {
|
||||
|
||||
+1
-2
@@ -9,8 +9,7 @@ describe("host/src/export", function() {
|
||||
let testServer;
|
||||
before(async function() {
|
||||
let writePath = path.join("temp", "test", "server");
|
||||
testServer = new FactorioServer(path.join("test", "file", "factorio"), writePath, {});
|
||||
await testServer.init();
|
||||
testServer = await FactorioServer.fromOptions(path.join("test", "file", "factorio"), writePath, {});
|
||||
});
|
||||
|
||||
describe("exportLocale()", function() {
|
||||
|
||||
+52
-51
@@ -223,7 +223,10 @@ describe("host/server", function() {
|
||||
|
||||
describe("class FactorioServer", function() {
|
||||
let writePath = path.join("temp", "test", "server");
|
||||
let server = new hostServer.FactorioServer(path.join("test", "file", "factorio"), writePath, {});
|
||||
let server;
|
||||
before(async function() {
|
||||
server = await hostServer.FactorioServer.fromOptions(path.join("test", "file", "factorio"), writePath, {});
|
||||
});
|
||||
|
||||
describe("constructor()", function() {
|
||||
it("should handle dashes in write path with strapPaths enabled", function() {
|
||||
@@ -231,28 +234,37 @@ describe("host/server", function() {
|
||||
new hostServer.FactorioServer(
|
||||
path.join("test", "file", "factorio"),
|
||||
path.join("temp", "test", "server-1"),
|
||||
{ stripPaths: true }
|
||||
path.join("test", "file", "factorio", "0.1.2", "data"),
|
||||
"0.1.2",
|
||||
{ stripPaths: true, rconPassword: "" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".setTargetVersion()", function() {
|
||||
it("should override the target version before init", function() {
|
||||
const fresh = new hostServer.FactorioServer(
|
||||
path.join("test", "file", "factorio"), writePath, {}
|
||||
describe(".fromOptions()", function() {
|
||||
it("should resolve the requested version", async function() {
|
||||
const found = await hostServer.FactorioServer.fromOptions(
|
||||
path.join("test", "file", "factorio"), writePath, { version: "0.1.1" }
|
||||
);
|
||||
fresh.setTargetVersion("0.1.5");
|
||||
assert.equal(fresh._targetVersion, "0.1.5");
|
||||
assert.equal(found.version, "0.1.1");
|
||||
assert.equal(found.dataPath(), path.join("test", "file", "factorio", "0.1.1", "data"));
|
||||
assert.equal(found._state, "init");
|
||||
});
|
||||
});
|
||||
|
||||
describe(".init()", function() {
|
||||
it("should not throw on first call", async function() {
|
||||
await server.init();
|
||||
it("should generate an RCON password if none is given", async function() {
|
||||
assert.equal(typeof server.rconPassword, "string");
|
||||
assert.equal(server.rconPassword.length, 10);
|
||||
const given = await hostServer.FactorioServer.fromOptions(
|
||||
path.join("test", "file", "factorio"), writePath, { rconPassword: "secret" }
|
||||
);
|
||||
assert.equal(given.rconPassword, "secret");
|
||||
});
|
||||
|
||||
it("should throw if called twice", async function() {
|
||||
await assert.rejects(server.init(), new Error("Expected state new but state is init"));
|
||||
it("should reject if the version is not installed", async function() {
|
||||
await assert.rejects(
|
||||
hostServer.FactorioServer.fromOptions(
|
||||
path.join("test", "file", "factorio"), writePath, { version: "0.1.5" }
|
||||
),
|
||||
/Unable to find Factorio version 0.1.5/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -314,7 +326,7 @@ describe("host/server", function() {
|
||||
describe(".handle()", function() {
|
||||
it("should log validation errors from ipc handlers", async function() {
|
||||
let logged = [];
|
||||
let ipcServer = new hostServer.FactorioServer(
|
||||
let ipcServer = await hostServer.FactorioServer.fromOptions(
|
||||
path.join("test", "file", "factorio"), writePath,
|
||||
{ logger: { error: msg => { logged.push(msg); } } }
|
||||
);
|
||||
@@ -571,7 +583,7 @@ describe("host/server", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe(".checkForUpdates()", function() {
|
||||
describe("checkForUpdates()", function() {
|
||||
let _fetch;
|
||||
let fetchCalledWith;
|
||||
let _platform = process.platform;
|
||||
@@ -589,7 +601,6 @@ describe("host/server", function() {
|
||||
});
|
||||
afterEach(function() {
|
||||
global.fetch = _fetch;
|
||||
server._factorioDir = path.join("test", "file", "factorio");
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: _platform,
|
||||
});
|
||||
@@ -602,18 +613,17 @@ describe("host/server", function() {
|
||||
]) {
|
||||
/* eslint-disable no-loop-func */
|
||||
describe(suite, function() {
|
||||
const factorioDir = path.join("test", "file", "factorio");
|
||||
// A target that matches the 0.1.5 download but not the installed 0.1.1
|
||||
const newerTarget = target === "0.1.1" ? "0.1.5" : target;
|
||||
it("should do nothing when there are no versions", async function() {
|
||||
server._factorioDir = path.join("test", "file", "factorio");
|
||||
server._targetVersion = target;
|
||||
await server.checkForUpdates([]);
|
||||
await hostServer.checkForUpdates(factorioDir, target, []);
|
||||
|
||||
assert.equal(fetchCalledWith, null);
|
||||
});
|
||||
if (target !== "latest") {
|
||||
it("should do nothing when no version matches", async function() {
|
||||
server._factorioDir = path.join("test", "file", "factorio");
|
||||
server._targetVersion = target;
|
||||
await server.checkForUpdates([{
|
||||
await hostServer.checkForUpdates(factorioDir, target, [{
|
||||
stable: true,
|
||||
version: "0.2.1",
|
||||
headlessUrl: "test1",
|
||||
@@ -627,9 +637,7 @@ describe("host/server", function() {
|
||||
});
|
||||
}
|
||||
it("should do nothing when there is no newer version", async function() {
|
||||
server._factorioDir = path.join("test", "file", "factorio");
|
||||
server._targetVersion = target;
|
||||
await server.checkForUpdates([{
|
||||
await hostServer.checkForUpdates(factorioDir, target, [{
|
||||
stable: true,
|
||||
version: "0.1.1",
|
||||
headlessUrl: "test1",
|
||||
@@ -642,9 +650,7 @@ describe("host/server", function() {
|
||||
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 = target === "0.1.1" ? "0.1.5" : target;
|
||||
await server.checkForUpdates([{
|
||||
await hostServer.checkForUpdates(path.join(factorioDir, "0.1.1"), newerTarget, [{
|
||||
stable: true,
|
||||
version: "0.1.5",
|
||||
headlessUrl: "test1",
|
||||
@@ -662,12 +668,10 @@ describe("host/server", function() {
|
||||
});
|
||||
it("should do nothing when on windows", async function() {
|
||||
let logLine = null;
|
||||
server._logger = { info: line => { logLine = line; } };
|
||||
const logger = { info: line => { logLine = line; } };
|
||||
Object.defineProperty(process, "platform", { value: "win32" });
|
||||
|
||||
server._factorioDir = path.join("test", "file", "factorio");
|
||||
server._targetVersion = target === "0.1.1" ? "0.1.5" : target;
|
||||
await server.checkForUpdates([{
|
||||
await hostServer.checkForUpdates(factorioDir, newerTarget, [{
|
||||
stable: true,
|
||||
version: "0.1.5",
|
||||
headlessUrl: "test1",
|
||||
@@ -679,7 +683,7 @@ describe("host/server", function() {
|
||||
stable: false,
|
||||
version: "0.1.0",
|
||||
headlessUrl: "test3",
|
||||
}]);
|
||||
}], logger);
|
||||
|
||||
assert.equal(fetchCalledWith, null);
|
||||
assert.ok(logLine !== null);
|
||||
@@ -687,12 +691,10 @@ describe("host/server", function() {
|
||||
});
|
||||
it("should do attempt to download on linux", async function() {
|
||||
let logLine = null;
|
||||
server._logger = { info: line => { logLine = line; } };
|
||||
const logger = { info: line => { logLine = line; } };
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
|
||||
server._factorioDir = path.join("test", "file", "factorio");
|
||||
server._targetVersion = target === "0.1.1" ? "0.1.5" : target;
|
||||
await assert.rejects(server.checkForUpdates([{
|
||||
await assert.rejects(hostServer.checkForUpdates(factorioDir, newerTarget, [{
|
||||
stable: true,
|
||||
version: "0.1.5",
|
||||
headlessUrl: "test1",
|
||||
@@ -704,7 +706,7 @@ describe("host/server", function() {
|
||||
stable: false,
|
||||
version: "0.1.0",
|
||||
headlessUrl: "test3",
|
||||
}]), new Error("Failed to fetch test1: -1 Fetch called"));
|
||||
}], logger), new Error("Failed to fetch test1: -1 Fetch called"));
|
||||
|
||||
assert.equal(fetchCalledWith, "test1");
|
||||
assert.ok(logLine !== null);
|
||||
@@ -716,7 +718,7 @@ describe("host/server", function() {
|
||||
describe("concurrent downloads", function() {
|
||||
const versions = [{ stable: true, version: "0.1.5", headlessUrl: "test1" }];
|
||||
|
||||
// Holds the mocked download open until release() and resolves waiting once a server waits for it
|
||||
// Holds the mocked download open until release() and resolves waiting once a check waits for it
|
||||
function holdDownload(factorioDir) {
|
||||
const held = { fetches: 0 };
|
||||
const release = new Promise(resolve => { held.release = resolve; });
|
||||
@@ -731,7 +733,7 @@ describe("host/server", function() {
|
||||
let waited;
|
||||
held.waiting = new Promise(resolve => { waited = resolve; });
|
||||
const logger = { info: line => line.endsWith("waiting for download...") && waited() };
|
||||
held.servers = [0, 1].map(() => new hostServer.FactorioServer(factorioDir, writePath, { logger }));
|
||||
held.check = () => hostServer.checkForUpdates(factorioDir, "latest", versions, logger);
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
return held;
|
||||
}
|
||||
@@ -739,7 +741,7 @@ describe("host/server", function() {
|
||||
it("should download once when servers check at the same time", async function() {
|
||||
const held = holdDownload(path.join("test", "file", "factorio"));
|
||||
let settled = 0;
|
||||
const checks = held.servers.map(s => s.checkForUpdates(versions).finally(() => { settled += 1; }));
|
||||
const checks = [0, 1].map(() => held.check().finally(() => { settled += 1; }));
|
||||
|
||||
await held.waiting;
|
||||
assert.equal(held.fetches, 1);
|
||||
@@ -754,14 +756,14 @@ describe("host/server", function() {
|
||||
await fs.rm(factorioDir, { force: true, recursive: true });
|
||||
await fs.mkdir(factorioDir, { recursive: true });
|
||||
const held = holdDownload(factorioDir);
|
||||
const first = held.servers[0].checkForUpdates(versions);
|
||||
const first = held.check();
|
||||
await held.fetching;
|
||||
|
||||
// The extraction has written the changelog but has not finished
|
||||
const dataDir = path.join(factorioDir, "0.1.5", "data");
|
||||
await fs.mkdir(dataDir, { recursive: true });
|
||||
await fs.writeFile(path.join(dataDir, "changelog.txt"), "Version: 0.1.5\n");
|
||||
const second = held.servers[1].checkForUpdates(versions);
|
||||
const second = held.check();
|
||||
|
||||
await held.waiting;
|
||||
held.release();
|
||||
@@ -776,12 +778,11 @@ describe("host/server", function() {
|
||||
this.skip();
|
||||
}
|
||||
|
||||
server._factorioDir = path.join("temp", "test", "factorioDownload");
|
||||
server._targetVersion = "latest";
|
||||
const factorioDir = path.join("temp", "test", "factorioDownload");
|
||||
global.fetch = _fetch;
|
||||
await fs.rm(server._factorioDir, { force: true, recursive: true, maxRetries: 10 });
|
||||
await fs.mkdir(server._factorioDir, { recursive: true });
|
||||
await server.checkForUpdates([{
|
||||
await fs.rm(factorioDir, { force: true, recursive: true, maxRetries: 10 });
|
||||
await fs.mkdir(factorioDir, { recursive: true });
|
||||
await hostServer.checkForUpdates(factorioDir, "latest", [{
|
||||
stable: true,
|
||||
version: "2.0.73",
|
||||
headlessUrl: "https://www.factorio.com/get-download/2.0.73/headless/linux64",
|
||||
|
||||
@@ -15,7 +15,10 @@ class FakeProcess extends events.EventEmitter {
|
||||
}
|
||||
|
||||
function createStartedServer() {
|
||||
const server = new FactorioServer(path.join("test", "file", "factorio"), path.join("temp", "test", "server"), {});
|
||||
const server = new FactorioServer(
|
||||
path.join("test", "file", "factorio"), path.join("temp", "test", "server"),
|
||||
path.join("test", "file", "factorio", "0.1.2", "data"), "0.1.2", { rconPassword: "" },
|
||||
);
|
||||
server._state = "running";
|
||||
server._rconReady = false;
|
||||
server._server = new FakeProcess();
|
||||
|
||||
@@ -12,8 +12,7 @@ describe("Integration of lib/factorio/export", function() {
|
||||
requiresFactorio(this);
|
||||
it("returns a nested Map with base game locale information", async function() {
|
||||
let writePath = path.join("temp", "test", "server");
|
||||
let testServer = new FactorioServer(factorioDir, writePath, {});
|
||||
await testServer.init();
|
||||
let testServer = await FactorioServer.fromOptions(factorioDir, writePath, {});
|
||||
|
||||
let locale = await _exportLocale(testServer, new Map(), new Map(), ["base"], "en");
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { Host, Instance, InstanceConnection } from "@clusterio/host";
|
||||
import { ControlConnection, Controller, HostConnection, InstanceRecord } from "@clusterio/controller";
|
||||
import { MockServer } from "../mock.js";
|
||||
|
||||
// Although there are no imports, the file has side-effects such as "before" which can effect tests
|
||||
import "./index.js";
|
||||
@@ -39,7 +40,7 @@ function connectInstance(controller, host, instanceId) {
|
||||
instanceConfig.set("instance.assigned_host", host.config.get("host.id"));
|
||||
host.assignedInstances.set(instanceId, { path: "invalid", config: instanceConfig });
|
||||
controller.instances.records.set(new InstanceRecord(instanceConfig, "stopped"));
|
||||
const instance = new Instance(host, instanceSide, "invalid", "invalid", instanceConfig);
|
||||
const instance = new Instance(host, instanceSide, "invalid", instanceConfig, new MockServer());
|
||||
const instanceConnection = new InstanceConnection(hostSide, host, instance);
|
||||
host.instanceConnections.set(instanceId, instanceConnection);
|
||||
return instance;
|
||||
|
||||
@@ -40,12 +40,14 @@ describe("Integration of host/src/server", function() {
|
||||
|
||||
describe("class FactorioServer", function() {
|
||||
let writePath = path.join("temp", "test", "integration");
|
||||
let server = new FactorioServer(factorioDir, writePath, {});
|
||||
// Let Factorio bind an OS-assigned UDP port instead of guessing a free one.
|
||||
server.gamePort = 0;
|
||||
let server;
|
||||
let logFile;
|
||||
|
||||
before(async function() {
|
||||
server = await FactorioServer.fromOptions(factorioDir, writePath, {});
|
||||
// Let Factorio bind an OS-assigned UDP port instead of guessing a free one.
|
||||
server.gamePort = 0;
|
||||
|
||||
// Delete result from previous run of these tests
|
||||
await fs.rm(writePath, { force: true, recursive: true, maxRetries: 10 });
|
||||
|
||||
@@ -63,9 +65,6 @@ describe("Integration of host/src/server", function() {
|
||||
}
|
||||
logFile.write(`${JSON.stringify(output)}\n`);
|
||||
});
|
||||
|
||||
// Initialize sever.
|
||||
await server.init();
|
||||
});
|
||||
|
||||
after(function() {
|
||||
|
||||
Reference in new issue
Block a user