From ae86781b0dbfe9a782e28d555be974f15ba20982 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 3 Apr 2020 13:40:48 +0200 Subject: [PATCH 01/23] Add support for multiple Factorio installs Allow the factorio_directory specified in the slave config to contain multiple Factorio installs and add factior.version instance option to select which install to use for the given instance. --- CHANGELOG.md | 2 + README.md | 18 ++++-- client.js | 3 +- lib/config/definitions.js | 6 ++ lib/factorio/server.js | 106 +++++++++++++++++++++++++++++++++--- test/integration/server.js | 2 +- test/lib/factorio/server.js | 69 +++++++++++++++++++++-- 7 files changed, 186 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53365239..16d7a559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ Version 2.0.0 - Added export of pollution statitics. - Reconnection logic that esures no data is dropped talking to the master server provided the session can be resumed. +- Added support for having multiple Factorio installs and selecting which + version to use on a per instance basis. ### Changes diff --git a/README.md b/README.md index 1ab89f2a..c7a96caf 100644 --- a/README.md +++ b/README.md @@ -167,19 +167,25 @@ reboot when you are done, then proceed to the next steps. *reboots matter* - Via the stand alone version on from their website - 1. Download the MS Windows (64-bit zip package) from https://www.factorio.com/download + 1. Create a new folder named "factorio" in the the factorioClusterio + folder. - 2. Open the zip file and drag the folder called "Factorio_0.17.x" into the factorioClusterio folder + 2. Download the MS Windows (64-bit zip package) from + https://www.factorio.com/download . - 3. Rename the folder to "factorio" + 3. Open the zip file and drag the folder called "Factorio_0.x.y" into + the factorio folder created in step 1. - Via steam installation - 1. Locate the game files by right clicking the game in steam, selecting properties, then Local Files, then Browse local files. + 1. Create a new folder named "factorio" in the the factorioClusterio + folder. - 2. Go to the parent folder of the folder that Steam opened and copy the Factorio folder into the factorioClusterio folder + 2. Locate the game files by right clicking the game in steam, + selecting properties, then Local Files, then Browse local files. - 3. Rename the folder to "factorio" + 3. Go to the parent folder of the folder that Steam opened and copy + the Factorio folder into the factorio folder created in step 1. ## Running Clusterio diff --git a/client.js b/client.js index 91b960ab..1dd53343 100644 --- a/client.js +++ b/client.js @@ -29,13 +29,14 @@ class Instance extends link.Link{ this.config = instanceConfig; let serverOptions = { + version: this.config.get("factorio.version"), gamePort: this.config.get('factorio.game_port'), rconPort: this.config.get('factorio.rcon_port'), rconPassword: this.config.get('factorio.rcon_password'), }; this.server = new factorio.FactorioServer( - path.join(factorioDir, "data"), this._dir, serverOptions + factorioDir, this._dir, serverOptions ); this.server.on('output', (output) => { diff --git a/lib/config/definitions.js b/lib/config/definitions.js index b52c7603..576c7085 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -165,6 +165,12 @@ InstanceGroup.finalize(); class FactorioGroup extends classes.ConfigGroup { } FactorioGroup.groupName = "factorio"; +FactorioGroup.define({ + name: "version", + description: "Version of the game to run, use latest to run the latest installed version", + type: "string", + initial_value: "latest", +}); FactorioGroup.define({ name: "game_port", description: "UDP port to run game on, uses a random port if null", diff --git a/lib/factorio/server.js b/lib/factorio/server.js index 27da529d..028ee217 100644 --- a/lib/factorio/server.js +++ b/lib/factorio/server.js @@ -22,7 +22,16 @@ const errors = require("lib/errors"); * reading the changelog.txt in it. */ async function getVersion(changelogPath) { - let changelog = await fs.readFile(changelogPath, "utf-8"); + let changelog; + try { + changelog = await fs.readFile(changelogPath, "utf-8"); + } catch (err) { + if (err.code === "ENOENT") { + return null; + } + throw err; + } + for (let line of changelog.split(/[\r\n]+/)) { let index = line.indexOf(":"); if (index !== -1) { @@ -33,7 +42,80 @@ async function getVersion(changelogPath) { } } - throw new Error("Unable to determine the version of Factorio"); + return null; +} + +/** + * Comparison function for sorting Factorio version strings + */ +function versionOrder(a, b) { + let aParts = a.split(".").map(s => Number.parseInt(s, 10)); + let bParts = b.split(".").map(s => Number.parseInt(s, 10)); + + for (let i = 0; i < aParts.length; i++) { + if (aParts[i] === bParts[i]) { + continue; + } else if (aParts[i] < bParts[i]) { + return 1; + } else { + return -1; + } + } + + return 0; +} + +/** + * Find factorio data directory of the given version + * + * Searches the given factorio dir for an installation of Factorio of + * the target version. + */ +async function findVersion(factorioDir, targetVersion) { + + // 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 || targetVersion === "latest") { + return [path.join(factorioDir, "data"), simpleVersion]; + } + + 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()) { + continue; + } + + let version = await getVersion(path.join(factorioDir, entry.name, "data", "changelog.txt")); + if (version === null) { + continue; + } + + if (version === targetVersion) { + return [path.join(factorioDir, entry.name, "data"), version]; + } + + versions.set(version, entry.name); + } + + if (!versions.size) { + throw new Error(`Unable to find any Factorio install in ${factorioDir}`); + } + + if (targetVersion === "latest") { + let latest = [...versions.keys()].sort(versionOrder)[0]; + return [path.join(factorioDir, versions.get(latest), "data"), latest]; + } + + throw new Error(`Unable to find Factorio version ${targetVersion}`); } /** @@ -343,16 +425,21 @@ class FactorioServer extends events.EventEmitter { /** * Create a Factorio server interface * - * @param {string} dataDir - Data directory of the Factorio install. + * @param {string} factorioDir - Directory of the Factorio install(s). * @param {string} writeDir - Directory to write runtime data to. * @param {object} options - Optional parameters. */ - constructor(dataDir, writeDir, options) { + constructor(factorioDir, writeDir, options) { super(); - this._dataDir = dataDir; + this._factorioDir = factorioDir; this._writeDir = writeDir; + // Resolved in init + this._version = null; + this._dataDir = null; + + this._targetVersion = options.version || "latest"; this.gamePort = options.gamePort || randomDynamicPort(); this.rconPort = options.rconPort || randomDynamicPort(); this.rconPassword = options.rconPassword; @@ -489,7 +576,7 @@ class FactorioServer extends events.EventEmitter { */ async init() { this._check(["new"]); - this._version = await getVersion(this.dataPath("changelog.txt")); + [this._dataDir, this._version] = await findVersion(this._factorioDir, this._targetVersion); this.rconPassword = this.rconPassword || await generatePassword(10); this._state = "init"; } @@ -788,7 +875,8 @@ class FactorioServer extends events.EventEmitter { * 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. + * the data directory of the Factorio server. Not valid before init has + * been called. */ dataPath(...parts) { return path.join(this._dataDir, ...parts); @@ -811,7 +899,7 @@ class FactorioServer extends events.EventEmitter { * * @returns {object} the parsed server-settings. */ - async exampleSettings(dataDir) { + async exampleSettings() { return JSON.parse(await fs.readFile(this.dataPath("server-settings.example.json"), "utf-8")); } @@ -832,6 +920,8 @@ module.exports = { // For testing only _getVersion: getVersion, + _versionOrder: versionOrder, + _findVersion: findVersion, _randomDynamicPort: randomDynamicPort, _generatePassword: generatePassword, _LineSplitter: LineSplitter, diff --git a/test/integration/server.js b/test/integration/server.js index 10d400b4..3952bf0c 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -21,7 +21,7 @@ describe("Integration of lib/factorio/server", function() { describe("class FactorioServer", function() { let writePath = path.join("test", "temp", "integration"); - let server = new factorio.FactorioServer(path.join("factorio", "data"), writePath, {}); + let server = new factorio.FactorioServer("factorio", writePath, {}); let logFile; before(async function() { diff --git a/test/lib/factorio/server.js b/test/lib/factorio/server.js index f891e835..e1b400e1 100644 --- a/test/lib/factorio/server.js +++ b/test/lib/factorio/server.js @@ -13,10 +13,71 @@ describe("lib/factorio/server", function() { let version = await factorio._getVersion(path.join("test", "file", "changelog-test.txt")); assert.equal(version, "0.1.1"); }); - it("should throw if unable to find the version", async function() { + it("should return null if unable to find the version", async function() { + let version = await factorio._getVersion(path.join("test", "file", "changelog-bad.txt")); + assert.equal(version, null); + }); + it("should return null if file does not exist", async function() { + let version = await factorio._getVersion(path.join("test", "file", "does-not-exist.txt")); + assert.equal(version, null); + }); + }); + + describe("_versionOrder()", function() { + it("should sort an array of versions", function() { + let versions = ["1.2.3", "0.1.4", "0.1.2", "1.2.3", "0.1.5", "1.10.2"]; + versions.sort(factorio._versionOrder); + assert.deepEqual( + versions, + ["1.10.2", "1.2.3", "1.2.3", "0.1.5", "0.1.4", "0.1.2"] + ); + }); + }); + + 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 factorio._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 factorio._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( - factorio._getVersion(path.join("test", "file", "changelog-bad.txt")), - new Error("Unable to determine the version of Factorio") + factorio._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 factorio._findVersion(installDir, "latest"); + assert.equal(dir, path.join(installDir, "factorio", "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 factorio._findVersion(installDir, "0.1.1"); + assert.equal(dir, path.join(installDir, "factorio", "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"); + await assert.rejects( + factorio._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"); + await assert.rejects( + factorio._findVersion(installDir, "latest"), + new Error(`Unable to find any Factorio install in ${installDir}`) ); }); }); @@ -106,7 +167,7 @@ describe("lib/factorio/server", function() { describe("class FactorioServer", function() { let writePath = path.join("test", "temp", "should_not_exist"); - let server = new factorio.FactorioServer(path.join("test", "file", "factorio", "data"), writePath, {}); + let server = new factorio.FactorioServer(path.join("test", "file", "factorio"), writePath, {}); describe(".init()", function() { it("should not throw on first call", async function() { From 028f00e58a0f30ca03e9b4b60a342988fa2ecd48 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 3 Apr 2020 14:26:08 +0200 Subject: [PATCH 02/23] Rename client.js to slave.js Rename the client.js script to slave.js, and changed places where it's refered to as client into slave. --- CHANGELOG.md | 1 + README.md | 14 +++++++------- lib/manager/modManager.js | 2 +- package.json | 1 - client.js => slave.js | 22 +++++++++++----------- test/integration/index.js | 12 ++++++------ test/{client.js => slave.js} | 22 +++++++++++----------- 7 files changed, 37 insertions(+), 37 deletions(-) rename client.js => slave.js (98%) rename test/{client.js => slave.js} (86%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16d7a559..f3e180a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,7 @@ Version 2.0.0 - Removed UPSdisplay plugin. UPS statistics is exported by the statistics exporter plugin. - Master server now defaults to hosting on https on port 8443. +- Renamed client to slave. ### Breaking Changes diff --git a/README.md b/README.md index c7a96caf..116f9b94 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Clusterio has *very* limited support for using docker. sudo docker run --name master -e MODE=master -p 1234:8080 -d -it --restart=unless-stopped danielvestol/clusterio - sudo docker run --name slave -e MODE=client -e INSTANCE=world1 -v /srv/clusterio/instances:/factorioClusterio/instances -p 1235:34167 -it --restart=unless-stopped danielvestol/clusterio + sudo docker run --name slave -e MODE=slave -e INSTANCE=world1 -v /srv/clusterio/instances:/factorioClusterio/instances -p 1235:34167 -it --restart=unless-stopped danielvestol/clusterio The -v flag is used to specify the instance directory. Your instances (save files etc) will be stored there. --> @@ -142,7 +142,7 @@ Clusterio is built up of multiple parts. Here is a quick guide: Master = master.js -Server host (Slave) = client.js + factorio server +Server host (Slave) = slave.js + factorio server Game Client = The people connecting to the server @@ -213,20 +213,20 @@ up. To set up the configuration for a new local slave run the following. - node client config set slave.name "Local" - node client config set slave.master_token "" + node slave config set slave.name "Local" + node slave config set slave.master_token "" This will write a new `config-slave.json` file in the current directory (you can change the location with the `--config` option) with the name and token provided. If you are connecting to a remote master server you will also need to set the `slave.master_url` option to that url. -You can list the config of a slave with the `node client config list` -command. Use `node client config --help` for more information. +You can list the config of a slave with the `node slave config list` +command. Use `node slave config --help` for more information. Once the config is set up run the slave with - node client start + node slave run ### Instances diff --git a/lib/manager/modManager.js b/lib/manager/modManager.js index 296d4a2e..516873ab 100644 --- a/lib/manager/modManager.js +++ b/lib/manager/modManager.js @@ -36,7 +36,7 @@ function formatBytes(size, decimals) { /** Lists mods for an instance as a table. Warning, handles printing internally. -@param {Instance} instance - Instance object, see client.js +@param {Instance} instance - Instance object, see slave.js @example var modManager = require("modManager"); diff --git a/package.json b/package.json index 9c871792..81468226 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "cover": "nyc npm test", "fast-cover": "FAST_TEST=y nyc npm test", "ci-cover": "nyc --reporter=lcovonly npm run-script test", - "start": "echo Run node client start [instance] to start slaves && node master", "install": "node lib/npmPostinstall", "bundle-dependencies": "bundle-dependencies" }, diff --git a/client.js b/slave.js similarity index 98% rename from client.js rename to slave.js index 1dd53343..15e4bfeb 100644 --- a/client.js +++ b/slave.js @@ -761,13 +761,13 @@ async function symlinkMods(instance, sharedMods, logger) { } } -async function startClient() { +async function startSlave() { // add better stack traces on promise rejection process.on('unhandledRejection', r => console.log(r)); // argument parsing const args = yargs - .scriptName("client") + .scriptName("slave") .usage("$0 [options]") .option('config', { nargs: 1, @@ -776,7 +776,7 @@ async function startClient() { type: 'string', }) .command("config", "Manage Slave config", config.configCommand) - .command('start', "Start slave") + .command("run", "Run slave") .demandCommand(1, "You need to specify a command to run") .strict() .argv @@ -808,7 +808,7 @@ async function startClient() { return; } - // If we get here the command was start + // If we get here the command was run await fs.ensureDir(slaveConfig.get("slave.instances_directory")); await fs.ensureDir("sharedMods"); @@ -816,14 +816,14 @@ async function startClient() { // Set the process title, shows up as the title of the CMD window on windows // and as the process name in ps/top on linux. - process.title = "clusterioClient"; + process.title = "clusterioSlave"; // make sure we have the master access token if (slaveConfig.get("slave.master_token") === "enter token here") { console.error("ERROR invalid config!"); console.error( "Master server now needs an access token for write operations. As clusterio\n"+ - "slaves depends upon this, please set your token using the command node client\n"+ + "slaves depends upon this, please set your token using the command node slave\n"+ "config set slave.master_token . You can retrieve your auth token from\n"+ "the master in secret-api-token.txt after running it once." ); @@ -916,12 +916,12 @@ I version of clusterio. Expect things to break. I +==========================================================+ ` ); - startClient().catch(err => { + startSlave().catch(err => { console.error(` -+---------------------------------------------------------------+ -| Unexpected error occured while starting client, please report | -| it to https://github.com/clusterio/factorioClusterio/issues | -+---------------------------------------------------------------+` ++--------------------------------------------------------------+ +| Unexpected error occured while starting slave, please report | +| it to https://github.com/clusterio/factorioClusterio/issues | ++--------------------------------------------------------------+` ); console.error(err); diff --git a/test/integration/index.js b/test/integration/index.js index 0dd11bae..1fd04967 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -79,17 +79,17 @@ before(async function() { await exec(`node master --config ${masterConfigPath} config set master.https_port 4443`); await exec(`node master --config ${masterConfigPath} config set master.heartbeat_interval 0.25`); - await exec(`node client --config ${slaveConfigPath} config set slave.id 4`); - await exec(`node client --config ${slaveConfigPath} config set slave.name slave`); - await exec(`node client --config ${slaveConfigPath} config set slave.instances_directory ${instancesDir}`); - await exec(`node client --config ${slaveConfigPath} config set slave.master_url "${url}"`); - await exec(`node client --config ${slaveConfigPath} config set slave.master_token "${token}"`); + await exec(`node slave --config ${slaveConfigPath} config set slave.id 4`); + await exec(`node slave --config ${slaveConfigPath} config set slave.name slave`); + await exec(`node slave --config ${slaveConfigPath} config set slave.instances_directory ${instancesDir}`); + await exec(`node slave --config ${slaveConfigPath} config set slave.master_url "${url}"`); + await exec(`node slave --config ${slaveConfigPath} config set slave.master_token "${token}"`); await exec(`node clusterctl --config ${controlConfigPath} control-config set control.master_url "https://localhost:4443/"`); await exec(`node clusterctl --config ${controlConfigPath} control-config set control.master_token "${token}"`); masterProcess = await spawn("master:", `node master --config ${masterConfigPath} run`, "All plugins loaded"); - slaveProcess = await spawn("slave:", `node client --config ${slaveConfigPath} start`, "SOCKET | received ready from master"); + slaveProcess = await spawn("slave:", `node slave --config ${slaveConfigPath} run`, "SOCKET | received ready from master"); }); after(async function() { diff --git a/test/client.js b/test/slave.js similarity index 86% rename from test/client.js rename to test/slave.js index ebc5bb2c..424f475f 100644 --- a/test/client.js +++ b/test/slave.js @@ -4,9 +4,9 @@ const fs = require('fs-extra'); const link = require("lib/link"); const config = require("lib/config"); -const client = require("../client"); +const slave = require("../slave"); -describe("Client testing", function() { +describe("Slave testing", function() { before(function() { config.InstanceConfig.finalize(); }); @@ -17,7 +17,7 @@ describe("Client testing", function() { let instanceConfig = new config.InstanceConfig(); await instanceConfig.init(); instanceConfig.set("instance.name", "foo"); - instance = new client._Instance(new link.VirtualConnector(), "dir", "factorioDir", instanceConfig); + instance = new slave._Instance(new link.VirtualConnector(), "dir", "factorioDir", instanceConfig); }); describe(".name", function() { @@ -38,11 +38,11 @@ describe("Client testing", function() { describe("checkFilename()", function() { it("should allow a basic name", function() { - client._checkFilename("file"); + slave._checkFilename("file"); }); function check(item, msg) { - assert.throws(() => client._checkFilename(item), new Error(msg)); + assert.throws(() => slave._checkFilename(item), new Error(msg)); } it("should throw on non-string", function() { @@ -108,14 +108,14 @@ describe("Client testing", function() { let instanceConfig = new config.InstanceConfig(); await instanceConfig.init(); instanceConfig.set("instance.name", "test"); - instance = new client._Instance( + instance = new slave._Instance( new link.VirtualConnector(), path.join(testDir, "instance"), "factorioDir", instanceConfig ); await fs.outputFile(instance.path("mods", "mod_i.zip"), "i"); }); it("should link mods and data files", async function() { - await client._symlinkMods(instance, path.join(testDir, "shared"), discardingLogger); + await slave._symlinkMods(instance, path.join(testDir, "shared"), discardingLogger); assert.equal(await fs.readFile(instance.path("mods", "mod_a.zip"), "utf-8"), "a"); assert.equal(await fs.readFile(instance.path("mods", "mod_b.zip"), "utf-8"), "b"); @@ -125,14 +125,14 @@ describe("Client testing", function() { it("should ignore directories", async function() { await fs.ensureDir(path.join(testDir, "shared", "dir")); - await client._symlinkMods(instance, path.join(testDir, "shared"), discardingLogger); + await slave._symlinkMods(instance, path.join(testDir, "shared"), discardingLogger); assert(!await fs.exists(instance.path("mods", "dir"), "utf-8"), "dir was unxpectedly linked"); }); it("should ignore files", async function() { await fs.outputFile(path.join(testDir, "shared", "file"), "f"); - await client._symlinkMods(instance, path.join(testDir, "shared"), discardingLogger); + await slave._symlinkMods(instance, path.join(testDir, "shared"), discardingLogger); assert(!await fs.exists(instance.path("mods", "file"), "utf-8"), "dir was unxpectedly linked"); }); @@ -144,7 +144,7 @@ describe("Client testing", function() { } await fs.unlink(path.join(testDir, "shared", "mod_a.zip")); - await client._symlinkMods(instance, path.join(testDir, "shared"), discardingLogger); + await slave._symlinkMods(instance, path.join(testDir, "shared"), discardingLogger); await assert.rejects(fs.lstat(instance.path("mods", "mod_a.zip")), { code: "ENOENT" }); assert.equal(await fs.readFile(instance.path("mods", "mod_b.zip"), "utf-8"), "b"); @@ -157,7 +157,7 @@ describe("Client testing", function() { it("should discover test instance", async function() { let logger = { log: () => {}, error: () => {} }; let instancePath = path.join("test", "file", "instances"); - let instanceInfos = await client._discoverInstances(instancePath, logger); + let instanceInfos = await slave._discoverInstances(instancePath, logger); let referenceConfig = new config.InstanceConfig(); await referenceConfig.init(); From f9f0e5b5f7d7ededdf0ee3b0e1b8d738f4601ba9 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 3 Apr 2020 14:42:56 +0200 Subject: [PATCH 03/23] Move test/temp to temp/test Move the temporary files generated by the the tests to the top level directory temp. --- .gitignore | 2 +- .travis.yml | 2 +- test/integration/index.js | 10 +++++----- test/integration/patch.js | 2 +- test/integration/server.js | 2 +- test/lib/database.js | 5 +++-- test/lib/factorio/patch.js | 2 +- test/lib/factorio/server.js | 2 +- test/lib/fileOps/fileOps.js | 2 +- test/lib/generateSSLcert.js | 2 +- test/lib/plugin.js | 2 +- test/slave.js | 2 +- 12 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index adb87bc8..87661d5e 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,7 @@ static/pictures/spritesheet.js static/pictures/spritesheet.png static/external saves -/test/temp/ +/temp/ # weird pattern to ignore sharedPlugins except not ignore some core modules !sharedPlugins/ diff --git a/.travis.yml b/.travis.yml index 49ad77c6..b915da86 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,5 +7,5 @@ before_install: - npm install -g codecov script: - npm run-script ci-cover - - cat test/temp/integration/log.txt + - cat temp/test/integration/log.txt - codecov -f coverage/lcov.info diff --git a/test/integration/index.js b/test/integration/index.js index 1fd04967..8173bf6e 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -32,11 +32,11 @@ let slaveProcess; let url = "https://localhost:4443/"; let token = jwt.sign({ id: "api" }, "TestSecretDoNotUse"); -let instancesDir = path.join("test", "temp", "instances"); -let databaseDir = path.join("test", "temp", "databse"); -let masterConfigPath = path.join("test", "temp", "master-integration.json"); -let slaveConfigPath = path.join("test", "temp", "slave-integration.json"); -let controlConfigPath = path.join("test", "temp", "control-integration.json"); +let instancesDir = path.join("temp", "test", "instances"); +let databaseDir = path.join("temp", "test", "databse"); +let masterConfigPath = path.join("temp", "test", "master-integration.json"); +let slaveConfigPath = path.join("temp", "test", "slave-integration.json"); +let controlConfigPath = path.join("temp", "test", "control-integration.json"); async function exec(...args) { console.log(args[0]); diff --git a/test/integration/patch.js b/test/integration/patch.js index 0d102dcb..1101e147 100644 --- a/test/integration/patch.js +++ b/test/integration/patch.js @@ -13,7 +13,7 @@ require("./server") describe("Integration of lib/factorio/patch", function() { describe("patch()", function() { - let savePath = path.join("test", "temp", "integration", "saves", "test.zip"); + let savePath = path.join("temp", "test", "integration", "saves", "test.zip"); it("should patch a freeplay game", async function() { slowTest(this); await factorio.patch(savePath, [{ diff --git a/test/integration/server.js b/test/integration/server.js index 3952bf0c..815fa852 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -20,7 +20,7 @@ describe("Integration of lib/factorio/server", function() { }); describe("class FactorioServer", function() { - let writePath = path.join("test", "temp", "integration"); + let writePath = path.join("temp", "test", "integration"); let server = new factorio.FactorioServer("factorio", writePath, {}); let logFile; diff --git a/test/lib/database.js b/test/lib/database.js index ad78688c..16d0cce1 100644 --- a/test/lib/database.js +++ b/test/lib/database.js @@ -1,5 +1,6 @@ const assert = require("assert").strict; const fs = require("fs-extra"); +const path = require("path"); const database = require("lib/database"); @@ -60,7 +61,7 @@ describe("lib/database", function() { describe("saveMapAsJson()", function() { it("should save a mapping as JSON", async function() { - let testFile = 'test/temp/save_map.json'; + let testFile = path.join("temp", "test", "save_map.json"); async function deleteTestFile() { try { await fs.unlink(testFile); @@ -134,7 +135,7 @@ describe("lib/database", function() { describe("saveMapAsJsonArray()", function() { it("should save a mapping as JSON", async function() { - let testFile = 'test/temp/save_array_map.json'; + let testFile = path.join("temp", "test", "save_array_map.json"); async function deleteTestFile() { try { await fs.unlink(testFile); diff --git a/test/lib/factorio/patch.js b/test/lib/factorio/patch.js index 3a1f890e..79eeb422 100644 --- a/test/lib/factorio/patch.js +++ b/test/lib/factorio/patch.js @@ -212,7 +212,7 @@ describe("lib/factorio", function() { it("should throw on unknown scenario", async function() { let zip = new jszip(); zip.file("world/control.lua", "-- unknown\n"); - let zipPath = path.join("test", "temp", "patch.zip"); + let zipPath = path.join("temp", "test", "patch.zip"); await fs.outputFile(zipPath, await zip.generateAsync({type: "nodebuffer"})) await assert.rejects( diff --git a/test/lib/factorio/server.js b/test/lib/factorio/server.js index e1b400e1..a004f488 100644 --- a/test/lib/factorio/server.js +++ b/test/lib/factorio/server.js @@ -166,7 +166,7 @@ describe("lib/factorio/server", function() { }); describe("class FactorioServer", function() { - let writePath = path.join("test", "temp", "should_not_exist"); + let writePath = path.join("temp", "test", "server"); let server = new factorio.FactorioServer(path.join("test", "file", "factorio"), writePath, {}); describe(".init()", function() { diff --git a/test/lib/fileOps/fileOps.js b/test/lib/fileOps/fileOps.js index a97ef57b..d507b1ab 100644 --- a/test/lib/fileOps/fileOps.js +++ b/test/lib/fileOps/fileOps.js @@ -5,7 +5,7 @@ const path = require('path'); const fileOps = require('lib/fileOps'); describe("fileOps.js", function(){ - let baseDir = path.join("test", "temp", "fileOps"); + let baseDir = path.join("temp", "test", "fileOps"); async function setupTestingEnv() { await fs.ensureDir(path.join(baseDir, "test", "folder")); await fs.ensureDir(path.join(baseDir, "test", "another folder")); diff --git a/test/lib/generateSSLcert.js b/test/lib/generateSSLcert.js index c4356262..75f43b57 100644 --- a/test/lib/generateSSLcert.js +++ b/test/lib/generateSSLcert.js @@ -5,7 +5,7 @@ const path = require("path"); const generateSSLcert = require("lib/generateSSLcert") describe("generateSSLcert.js(options)", ()=>{ - let testDir = "test/temp/certTest"; + let testDir = path.join("temp", "test", "certTest"); before(async function() { await fs.remove(testDir); }); diff --git a/test/lib/plugin.js b/test/lib/plugin.js index 9c66aed9..632708b4 100644 --- a/test/lib/plugin.js +++ b/test/lib/plugin.js @@ -35,7 +35,7 @@ describe("lib/plugin", function() { }); describe("loadPluginInfos()", function() { - let baseDir = path.join('test', 'temp', 'plugin'); + let baseDir = path.join("temp", "test", "plugin"); let emptyDir = path.join(baseDir, 'emptyDir'); let emptyPlugin = path.join(baseDir, 'emptyPlugin'); let testPlugin = path.join(baseDir, 'testPlugin'); diff --git a/test/slave.js b/test/slave.js index 424f475f..47a146b9 100644 --- a/test/slave.js +++ b/test/slave.js @@ -89,7 +89,7 @@ describe("Slave testing", function() { }); describe("symlinkMods()", function() { - let testDir = path.join("test", "temp", "symlink"); + let testDir = path.join("temp", "test", "symlink"); let discardingLogger = { warning: function() { }, log: function() { }, From c2df80a17f7d223cf58e6e16f3d60c7730e93525 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 3 Apr 2020 14:56:35 +0200 Subject: [PATCH 04/23] Move test/json to test/file/json Move the json test files for the lib/database tests to test/file/json to be consistent with the location of test files for other tests. --- test/{ => file}/json/array.json | 0 test/{ => file}/json/array_null.json | 0 test/{ => file}/json/array_object.json | 0 test/{ => file}/json/boolean.json | 0 test/{ => file}/json/load_array_map.json | 0 test/{ => file}/json/load_map.json | 0 test/{ => file}/json/null.json | 0 test/{ => file}/json/number.json | 0 test/{ => file}/json/object.json | 0 test/{ => file}/json/save_map.json | 0 test/{ => file}/json/string.json | 0 test/lib/database.js | 20 ++++++++++---------- 12 files changed, 10 insertions(+), 10 deletions(-) rename test/{ => file}/json/array.json (100%) rename test/{ => file}/json/array_null.json (100%) rename test/{ => file}/json/array_object.json (100%) rename test/{ => file}/json/boolean.json (100%) rename test/{ => file}/json/load_array_map.json (100%) rename test/{ => file}/json/load_map.json (100%) rename test/{ => file}/json/null.json (100%) rename test/{ => file}/json/number.json (100%) rename test/{ => file}/json/object.json (100%) rename test/{ => file}/json/save_map.json (100%) rename test/{ => file}/json/string.json (100%) diff --git a/test/json/array.json b/test/file/json/array.json similarity index 100% rename from test/json/array.json rename to test/file/json/array.json diff --git a/test/json/array_null.json b/test/file/json/array_null.json similarity index 100% rename from test/json/array_null.json rename to test/file/json/array_null.json diff --git a/test/json/array_object.json b/test/file/json/array_object.json similarity index 100% rename from test/json/array_object.json rename to test/file/json/array_object.json diff --git a/test/json/boolean.json b/test/file/json/boolean.json similarity index 100% rename from test/json/boolean.json rename to test/file/json/boolean.json diff --git a/test/json/load_array_map.json b/test/file/json/load_array_map.json similarity index 100% rename from test/json/load_array_map.json rename to test/file/json/load_array_map.json diff --git a/test/json/load_map.json b/test/file/json/load_map.json similarity index 100% rename from test/json/load_map.json rename to test/file/json/load_map.json diff --git a/test/json/null.json b/test/file/json/null.json similarity index 100% rename from test/json/null.json rename to test/file/json/null.json diff --git a/test/json/number.json b/test/file/json/number.json similarity index 100% rename from test/json/number.json rename to test/file/json/number.json diff --git a/test/json/object.json b/test/file/json/object.json similarity index 100% rename from test/json/object.json rename to test/file/json/object.json diff --git a/test/json/save_map.json b/test/file/json/save_map.json similarity index 100% rename from test/json/save_map.json rename to test/file/json/save_map.json diff --git a/test/json/string.json b/test/file/json/string.json similarity index 100% rename from test/json/string.json rename to test/file/json/string.json diff --git a/test/lib/database.js b/test/lib/database.js index 16d0cce1..f2f31fc9 100644 --- a/test/lib/database.js +++ b/test/lib/database.js @@ -31,7 +31,7 @@ describe("lib/database", function() { for (let type of badTypes) { it(`should reject on ${type} JSON`, async function() { await assert.rejects( - database.loadJsonAsMap(`test/json/${type}.json`), + database.loadJsonAsMap(path.join("test", "file", "json", `${type}.json`)), new Error(`Expected object but got ${type}`) ); }); @@ -39,21 +39,21 @@ describe("lib/database", function() { it("should work on empty object JSON", async function() { assert.deepEqual( - await database.loadJsonAsMap('test/json/object.json'), + await database.loadJsonAsMap(path.join("test", "file", "json", "object.json")), new Map() ); }); it("should work on object JSON", async function() { assert.deepEqual( - await database.loadJsonAsMap('test/json/load_map.json'), + await database.loadJsonAsMap(path.join("test", "file", "json", "load_map.json")), new Map([['a', 1], ['b', true]]) ); }); it("should give an empty Map for non-existant file", async function() { assert.deepEqual( - await database.loadJsonAsMap('test/json/does-not-exist'), + await database.loadJsonAsMap(path.join("test", "file", "json", "does-not-exist")), new Map() ); }); @@ -91,7 +91,7 @@ describe("lib/database", function() { for (let type of badTypes) { it(`should reject on ${type} JSON`, async function() { await assert.rejects( - database.loadJsonArrayAsMap(`test/json/${type}.json`), + database.loadJsonArrayAsMap(path.join("test", "file", "json", `${type}.json`)), new Error(`Expected array but got ${type}`) ); }); @@ -99,35 +99,35 @@ describe("lib/database", function() { it("should reject on null element", async function() { await assert.rejects( - database.loadJsonArrayAsMap('test/json/array_null.json'), + database.loadJsonArrayAsMap(path.join("test", "file", "json", "array_null.json")), new Error("Expected all elements to be objects") ); }); it("should reject on element missing id", async function() { await assert.rejects( - database.loadJsonArrayAsMap('test/json/array_object.json'), + database.loadJsonArrayAsMap(path.join("test", "file", "json", "array_object.json")), new Error("Expected all elements to have an id property") ); }); it("should work on empty array JSON", async function() { assert.deepEqual( - await database.loadJsonArrayAsMap('test/json/array.json'), + await database.loadJsonArrayAsMap(path.join("test", "file", "json", "array.json")), new Map() ); }); it("should work on array JSON", async function() { assert.deepEqual( - await database.loadJsonArrayAsMap('test/json/load_array_map.json'), + await database.loadJsonArrayAsMap(path.join("test", "file", "json", "load_array_map.json")), new Map([['a', {id: 'a'}], ['b', {id: 'b'}]]) ); }); it("should give an empty Map for non-existant file", async function() { assert.deepEqual( - await database.loadJsonArrayAsMap('test/json/does-not-exist'), + await database.loadJsonArrayAsMap(path.join("test", "file", "json", "does-not-exist")), new Map() ); }); From 3ff5e630ca2aa0fb73e4e858b7f35c5d1e91b44d Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 3 Apr 2020 14:57:49 +0200 Subject: [PATCH 05/23] Warn on unhandled Factorio ipc Send warning to console if an ipc message is received but there were no handlers registered to handle it. --- lib/factorio/server.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/factorio/server.js b/lib/factorio/server.js index 028ee217..d2934d9f 100644 --- a/lib/factorio/server.js +++ b/lib/factorio/server.js @@ -505,7 +505,9 @@ class FactorioServer extends events.EventEmitter { throw new Error(`Unknown IPC type '${type}'`); } - this.emit(`ipc-${channel}`, content); + if (!this.emit(`ipc-${channel}`, content)) { + console.warn(`Warning: Unhandled ipc-${channel}`, content); + } } _handleOutput(line, source) { From 9a4a535adde4657dc33db208eb6a206154b14abe Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Wed, 18 Mar 2020 15:38:39 +0100 Subject: [PATCH 06/23] Expose Slave to instance plugins Allow the slave object to be accessed by instance plugins through the this.slave property. --- lib/plugin.js | 24 +++++++++++++++++++++++- slave.js | 6 +++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/plugin.js b/lib/plugin.js index e08fff22..3f88d499 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -1,3 +1,7 @@ +/** + * Plugin interfaces and utilities. + * @module + */ "use strict"; const fs = require("fs-extra"); const path = require("path"); @@ -9,9 +13,27 @@ const errors = require("lib/errors"); * Base class for instance plugins */ class BaseInstancePlugin { - constructor(info, instance) { + constructor(info, instance, slave) { + /** + * The plugin's own info module + */ this.info = info; + + /** + * Instance the plugin started for + * @type {Instance} + */ this.instance = instance; + + /** + * Slave running the instance + * + * With the exepction of accessing the slave's config you should + * avoid ineracting with the slave object directly. + * + * @type {Slave} + */ + this.slave = slave; } /** diff --git a/slave.js b/slave.js index 15e4bfeb..464e340d 100644 --- a/slave.js +++ b/slave.js @@ -50,7 +50,7 @@ class Instance extends link.Link{ }); } - async init(pluginInfos) { + async init(pluginInfos, slave) { await this.server.init(); // load plugins @@ -62,7 +62,7 @@ class Instance extends link.Link{ // require plugin class and initialize it let pluginLoadStarted = Date.now(); let { InstancePlugin } = require(`./plugins/${pluginInfo.name}/${pluginInfo.instanceEntrypoint}`); - let instancePlugin = new InstancePlugin(pluginInfo, this); + let instancePlugin = new InstancePlugin(pluginInfo, this, slave); await instancePlugin.init(); this.plugins.set(pluginInfo.name, instancePlugin); plugin.attachPluginMessages(this, pluginInfo, instancePlugin); @@ -543,7 +543,7 @@ class Slave extends link.Link { let instance = new Instance( connectionClient, instanceInfo.path, this.config.get("slave.factorio_directory"), instanceInfo.config ); - await instance.init(this.pluginInfos); + await instance.init(this.pluginInfos, this); // XXX: race condition on multiple simultanious calls this.instanceConnections.set(instanceId, instanceConnection); From f05100c94d933e28c9d47033221a05b996ddbd89 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 3 Apr 2020 15:17:25 +0200 Subject: [PATCH 07/23] Remove unused files Remove left over files that are no longer in use. --- config.json.dist | 32 ------------------- lib/isFactorioCommand/index.js | 1 - lib/isFactorioCommand/isFactorioCommand.js | 23 ------------- lib/typedef.js | 11 ------- static/main.js | 1 - static/slaveDisplay.js | 26 --------------- .../isFactorioCommand/isFactorioCommand.js | 18 ----------- test/missing.js | 1 - views/nodes.ejs | 1 - 9 files changed, 114 deletions(-) delete mode 100644 config.json.dist delete mode 100644 lib/isFactorioCommand/index.js delete mode 100644 lib/isFactorioCommand/isFactorioCommand.js delete mode 100644 lib/typedef.js delete mode 100644 static/slaveDisplay.js delete mode 100644 test/lib/isFactorioCommand/isFactorioCommand.js diff --git a/config.json.dist b/config.json.dist deleted file mode 100644 index b5faadc2..00000000 --- a/config.json.dist +++ /dev/null @@ -1,32 +0,0 @@ -{ - "__comment": "Where factorio folder. Should be in the same folder as this config file.", - "factorioDirectory": "./factorio", - "databaseDirectory": "./database", - - "__commentMasterPort": "HTTP port for the master to listen to", - "masterPort": 8080, - - "__commentsslPort": "HTTPS port for the master to listen to. Set to 0 or an empty string to not listen for HTTPS.", - "sslPort": 443, - "sslCert": "database/certificates/cert.crt", - "sslPrivKey": "database/certificates/cert.key", - - "__commentAuthSecret": "This is a long random string used for generating authentication tokens. Do not share. Regenerates when secret-api-token.txt is deleted.", - "masterAuthSecret": "", - - "__comment3": "Your factorio.com username for factorio MP services, also username will be admin on new instances", - "username": "", - "__comment4": "Token from your factorio.com profile page. Required for MP and mod manager services.", - "token": "", - - "__comment5": "General user settings", - "verify_user_identity": true, - "auto_pause": false, - "allow_commands": "admins-only", - "game_password": "", - "description": "This server runs Clusterio", - "visibility": { - "public": true, - "lan": true - }, -} diff --git a/lib/isFactorioCommand/index.js b/lib/isFactorioCommand/index.js deleted file mode 100644 index 408a2086..00000000 --- a/lib/isFactorioCommand/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("./isFactorioCommand"); diff --git a/lib/isFactorioCommand/isFactorioCommand.js b/lib/isFactorioCommand/isFactorioCommand.js deleted file mode 100644 index 239bbc59..00000000 --- a/lib/isFactorioCommand/isFactorioCommand.js +++ /dev/null @@ -1,23 +0,0 @@ -const luamin = require("luamin"); -function isFactorioCommand(command){ - if(typeof command != "string"){ - return false; - } else if(!command.includes("/silent-command ") && !command.includes("/c ")){ - return false; - } else if(command[0] != "/"){ - return false; - } else { - // check that what follows /c is valid LUA - let x = command.replace("/silent-command ", "").replace("/c ",""); - let y - try{ - y = luamin.minify(x); - } catch (e) { - return false; - } - - // by this point it is surely a LUA command - return true; - } -} -module.exports = isFactorioCommand; \ No newline at end of file diff --git a/lib/typedef.js b/lib/typedef.js deleted file mode 100644 index 1e888ab7..00000000 --- a/lib/typedef.js +++ /dev/null @@ -1,11 +0,0 @@ -/** -An item with a count specified - -@typedef {object} itemStack -@property {string} name The name of the item in the stack -@property {(number|string)} count The amount of items in the stack - -@example -itemStack = {name:"iron-plate", count:"12"} -itemStack = {name:"copper-plate", count:972} -*/ \ No newline at end of file diff --git a/static/main.js b/static/main.js index 65b7a1e8..deceacfe 100644 --- a/static/main.js +++ b/static/main.js @@ -1,6 +1,5 @@ (function (){ - ensureSetting("Display offline slaves", "boolean", false); ensureSetting("Periodically update storage screen", "boolean", true); }()) diff --git a/static/slaveDisplay.js b/static/slaveDisplay.js deleted file mode 100644 index 6a0a5dc5..00000000 --- a/static/slaveDisplay.js +++ /dev/null @@ -1,26 +0,0 @@ - -// ask master for slaves and render them nicely on a page with production graphs -setTimeout(function() { - var xmlhttp = new XMLHttpRequest(); - xmlhttp.onreadystatechange = function() { - if(xmlhttp.readyState == 4 && xmlhttp.status == 200) { - var slaveData = JSON.parse(xmlhttp.responseText); - var HTML = "

Current connections:

" - for(i=0;i

" + slaveData[key].instanceName + "

ID: " + slaveData[key].unique + "

Last seen: "+seenDate+"

Online players: "+slaveData[key].playerCount+"

IP: "+slaveData[key].publicIP +":"+ slaveData[key].serverPort+"

" - HTML += "

Host: "+slaveData[key].mac+"

"; - HTML += ""; - } - } - document.querySelector("#slaves > #display").innerHTML = HTML - } - } - xmlhttp.open("GET", "api/slaves", true); - xmlhttp.send(); -}, 0) diff --git a/test/lib/isFactorioCommand/isFactorioCommand.js b/test/lib/isFactorioCommand/isFactorioCommand.js deleted file mode 100644 index 8faa141c..00000000 --- a/test/lib/isFactorioCommand/isFactorioCommand.js +++ /dev/null @@ -1,18 +0,0 @@ -const isFactorioCommand = require("lib/isFactorioCommand"); -const assert = require("assert"); - -describe("inventoryImports/isFactorioCommand.js()", function(){ - it("Returns false if input is not a factorio command", function(){ - assert(isFactorioCommand({}) == false, "This is not even a string so should return false"); - assert(isFactorioCommand("Howdy boys!") == false, "This is not a command so should return false"); - assert(isFactorioCommand("/c this ain't a command because its invalid LUA") == false, "This is not valid LUA so it should return false"); - assert(isFactorioCommand("Trying to trick the system /c game.print('Hello world!')") == false, "this is still not a command"); - - }); - it("Returns true if it is a valid command", function(){ - assert(isFactorioCommand("/c game.print('hello world')"), "This is a valid command, should output true not false") - }); - it("Fails to understand that leading spaces are valid", function(){ - assert(isFactorioCommand(" /c game.print('Hello world!')") == false, "This should be true but often returns false unintentionally. If you see this you probably fixed the bug so change this test, OK?") - }); -}); \ No newline at end of file diff --git a/test/missing.js b/test/missing.js index f87ddc85..35e53b8d 100644 --- a/test/missing.js +++ b/test/missing.js @@ -3,4 +3,3 @@ require('lib/manager/modManager'); require('lib/npmPostinstall.js'); -require('lib/typedef'); diff --git a/views/nodes.ejs b/views/nodes.ejs index 31606dbb..cb879213 100644 --- a/views/nodes.ejs +++ b/views/nodes.ejs @@ -1,7 +1,6 @@ <%- include("head") %> - From 65494a0a919c15630f4d28476893a199938e4140 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 3 Apr 2020 15:27:51 +0200 Subject: [PATCH 08/23] Move basicType to lib/helpers Move the basicType function from the config lib to a shared helpers lib. --- lib/config/classes.js | 17 ++--------------- lib/helpers.js | 23 +++++++++++++++++++++++ test/lib/helpers.js | 17 +++++++++++++++++ 3 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 lib/helpers.js create mode 100644 test/lib/helpers.js diff --git a/lib/config/classes.js b/lib/config/classes.js index acbaa212..c43ec6be 100644 --- a/lib/config/classes.js +++ b/lib/config/classes.js @@ -4,6 +4,8 @@ */ "use strict"; +const { basicType } = require("lib/helpers"); + /** * Invalid Value exception @@ -39,21 +41,6 @@ function splitOn(separator, string) { return [string.slice(0, index), string.slice(index + separator.length)]; } -/** - * Return a string describing the type of the value passed - * - * Works the same as typeof, excpet that null and array types get their - * own string. - * - * @param value - value to return the type of. - * @returns {string} basic type of the value passed. - */ -function basicType(value) { - if (value === null) { return "null"; } - if (value instanceof Array) { return "array"; } - return typeof value; -} - /** * Collection of config groups * diff --git a/lib/helpers.js b/lib/helpers.js new file mode 100644 index 00000000..afc1f9bf --- /dev/null +++ b/lib/helpers.js @@ -0,0 +1,23 @@ +/** + * Collection of small utilites that are useful in multiple places. + * @module + */ + +/** + * Return a string describing the type of the value passed + * + * Works the same as typeof, excpet that null and array types get their + * own string. + * + * @param value - value to return the type of. + * @returns {string} basic type of the value passed. + */ +function basicType(value) { + if (value === null) { return "null"; } + if (value instanceof Array) { return "array"; } + return typeof value; +} + +module.exports = { + basicType, +} diff --git a/test/lib/helpers.js b/test/lib/helpers.js new file mode 100644 index 00000000..e5586bd5 --- /dev/null +++ b/test/lib/helpers.js @@ -0,0 +1,17 @@ +const assert = require("assert").strict; + +const helpers = require("lib/helpers"); + + +describe("lib/helpers", function() { + describe("basicType()", function() { + it("should return the expected values", function() { + assert.equal(helpers.basicType("s"), "string"); + assert.equal(helpers.basicType(null), "null"); + assert.equal(helpers.basicType(undefined), "undefined"); + assert.equal(helpers.basicType([1, 2]), "array"); + assert.equal(helpers.basicType(7), "number"); + assert.equal(helpers.basicType({ a: 1 }), "object"); + }); + }); +}); From 3cc84d640085472115fd54248f7a7fb0dafde47a Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 3 Apr 2020 15:37:27 +0200 Subject: [PATCH 09/23] Use basicType helper in lib/database Simplify the checking logic in lib/database by using the basicType helper. --- lib/database.js | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/lib/database.js b/lib/database.js index 43a67f27..85894de0 100644 --- a/lib/database.js +++ b/lib/database.js @@ -8,6 +8,8 @@ "use strict"; const fs = require("fs-extra"); +const { basicType } = require("lib/helpers"); + /** * Converts a Map with only string keys to a JavaScript object. @@ -46,16 +48,8 @@ async function loadJsonAsMap(filePath) { let content = await fs.readFile(filePath); let parsed = JSON.parse(content); - if (parsed instanceof Array) { - throw new Error(`Expected object but got array`); - } - - if (parsed === null) { - throw new Error(`Expected object but got null`); - } - - if (typeof parsed !== "object") { - throw new Error(`Expected object but got ${typeof parsed}`); + if (basicType(parsed) !== "object") { + throw new Error(`Expected object but got ${basicType(parsed)}`); } return new Map(Object.entries(parsed)); @@ -104,21 +98,13 @@ async function loadJsonArrayAsMap(filePath) { let content = await fs.readFile(filePath); let parsed = JSON.parse(content); - if (parsed === null) { - throw new Error(`Expected array but got null`); - } - - if (typeof parsed !== "object") { - throw new Error(`Expected array but got ${typeof parsed}`); - } - - if (!(parsed instanceof Array)) { - throw new Error(`Expected array but got object`); + if (basicType(parsed) !== "array") { + throw new Error(`Expected array but got ${basicType(parsed)}`); } let map = new Map(); for (let element of parsed) { - if (element === null || element instanceof Array || typeof element !== "object") { + if (basicType(element) !== "object") { throw new Error(`Expected all elements to be objects`); } From ee063300ed1237128580003ddfa46d9230ddf732 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Wed, 18 Mar 2020 15:36:31 +0100 Subject: [PATCH 10/23] Check result of rcon commands in subspace_storage Validate that the commands sent don't error out by checking the returned result is empty. --- plugins/subspace_storage/instance.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/subspace_storage/instance.js b/plugins/subspace_storage/instance.js index 5e7c9848..c4e38269 100644 --- a/plugins/subspace_storage/instance.js +++ b/plugins/subspace_storage/instance.js @@ -22,13 +22,14 @@ class InstancePlugin extends plugin.BaseInstancePlugin { async onStart() { this.pingId = setInterval(() => { this.instance.server.sendRcon( - "/sc __subspace_storage__ global.ticksSinceMasterPinged = 0" + "/sc __subspace_storage__ global.ticksSinceMasterPinged = 0", true ).catch(unexpectedError); }, 5000); let response = await this.info.messages.getStorage.send(this.instance); + // TODO Diff with dump of invdata produce minimal command to sync let itemsJson = luaTools.escapeString(JSON.stringify(response.items)); - await this.instance.server.sendRcon(`/sc __subspace_storage__ UpdateInvData("${itemsJson}", true)`); + await this.instance.server.sendRcon(`/sc __subspace_storage__ UpdateInvData("${itemsJson}", true)`, true); } async onStop() { @@ -66,7 +67,7 @@ class InstancePlugin extends plugin.BaseInstancePlugin { } let itemsJson = luaTools.escapeString(JSON.stringify(response.items)); - await this.instance.server.sendRcon(`/sc __subspace_storage__ Import("${itemsJson}")`); + await this.instance.server.sendRcon(`/sc __subspace_storage__ Import("${itemsJson}")`, true); } // combinator signals --------------------------------------------------------- @@ -85,7 +86,7 @@ class InstancePlugin extends plugin.BaseInstancePlugin { items.push(["signal-unixtime", Math.floor(Date.now()/1000)]); let itemsJson = luaTools.escapeString(JSON.stringify(items)); - await this.instance.server.sendRcon(`/sc __subspace_storage__ UpdateInvData("${itemsJson}")`); + await this.instance.server.sendRcon(`/sc __subspace_storage__ UpdateInvData("${itemsJson}")`, true); } } From 5c16809b5b235b0b4cf5274713d5aac0da30e90a Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Wed, 4 Sep 2019 14:36:40 +0200 Subject: [PATCH 11/23] JSDoc use autodetected module name Change @module entries to just @module as JSDoc is able to automatically determine the module name. --- lib/manager/modManager.js | 2 +- lib/objectOps.js | 2 +- master.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/manager/modManager.js b/lib/manager/modManager.js index 516873ab..80e3b140 100644 --- a/lib/manager/modManager.js +++ b/lib/manager/modManager.js @@ -1,6 +1,6 @@ /** Provides tools to manage mods in a clusterio install -@module modManager +@module */ const asTable = require("as-table").configure({delimiter: ' | '}); diff --git a/lib/objectOps.js b/lib/objectOps.js index a4286c07..23053af3 100644 --- a/lib/objectOps.js +++ b/lib/objectOps.js @@ -1,6 +1,6 @@ /** Helper functions related to objects -@module objectOps +@module */ /** diff --git a/master.js b/master.js index 839af8fc..c8529f77 100644 --- a/master.js +++ b/master.js @@ -2,7 +2,7 @@ Clusterio master server. Facilitates communication between slaves through a webserver, storing data related to slaves like production graphs. -@module clusterioMaster +@module @author Danielv123 @example From 17973e767e2ce507a3694c7503ae0373b9812c0f Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 3 Apr 2020 16:10:04 +0200 Subject: [PATCH 12/23] Generate API docs with JSDoc Add npm docs script to generate api docs with JSDoc. --- .gitignore | 3 +++ docs/jsdoc.json | 23 +++++++++++++++++++++++ package.json | 2 ++ 3 files changed, 28 insertions(+) create mode 100644 docs/jsdoc.json diff --git a/.gitignore b/.gitignore index 87661d5e..861f8b58 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ coverage build/Release dist/ +# Autogenerated docs output +/docs/api + node_modules/* Factorio* diff --git a/docs/jsdoc.json b/docs/jsdoc.json new file mode 100644 index 00000000..88b1ded8 --- /dev/null +++ b/docs/jsdoc.json @@ -0,0 +1,23 @@ +{ + "plugins": [ + "plugins/markdown" + ], + "tags": { + "allowUnknownTags": false + }, + "opts": { + "destination": "docs/api", + "recurse": true + }, + "source": { + "include": [ + "master.js", + "slave.js", + "clusterctl.js", + "lib/", + "plugins/", + "routes/", + "routes.js" + ] + } +} diff --git a/package.json b/package.json index 81468226..8646ff7b 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "cover": "nyc npm test", "fast-cover": "FAST_TEST=y nyc npm test", "ci-cover": "nyc --reporter=lcovonly npm run-script test", + "docs": "jsdoc --configure docs/jsdoc.json", "install": "node lib/npmPostinstall", "bundle-dependencies": "bundle-dependencies" }, @@ -67,6 +68,7 @@ }, "devDependencies": { "html5-validator": "^1.2.1", + "jsdoc": "^3.6.3", "klaw": "^3.0.0", "mocha": "^6.2.0", "mocha-sinon": "^2.1.0", From 706ffd9ed278f635124a59d3dbc64d1b2369caae Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 3 Apr 2020 16:18:44 +0200 Subject: [PATCH 13/23] Detail usage of single and double quoted strings Add section to contributing guidelines detailing that single quoted strings should not be used. The use of it has only resulted in the addition of more inconsistency to the codebase. --- docs/contributing.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/contributing.md b/docs/contributing.md index c72548ea..7deb413b 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -176,7 +176,19 @@ Code Style ---------- The style of the code is a bit of a mixed bag at the moment. But there -are at least two things that have been agreed upon. +are at least a few things that have been agreed upon. + + +### Strings + +JavaScript and Lua code should use double quoted strings (`"string"`) +for all strings with the only exception that strings that contain double +quotes may use single quotes (i.e., `'String with "quotes"'`.) For +JavaScript template literals may also be used where appropriate. + +There are places in the codebase that are using `'single'` quoted +strings. Should you be modifying such code use double quotes on new and +modified lines and leave the rest untouched. ### Indenting From 59758f15761973a9513c7824f0cd53c368c52cb9 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 3 Apr 2020 21:14:30 +0200 Subject: [PATCH 14/23] Rename plugin Prometheus metrics Rename Prometheus metrics exported by plugins to follow the clusterio__ naming scheme. --- CHANGELOG.md | 17 +++++++++++++---- docs/writing-plugins.md | 13 ++++++++----- plugins/statistics_exporter/instance.js | 8 ++++---- plugins/subspace_storage/dole.js | 4 ++-- plugins/subspace_storage/master.js | 24 ++++++++++++------------ 5 files changed, 39 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3e180a7..c5f2946f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -133,11 +133,20 @@ Version 2.0.0 socket_io_transmit_bytes, - Removed clusterio_connected_instaces_gauge and added clusterio_master_connected_clients_count in its place. -- Renamed clusterio_player_count_gauge to clusterio_instance_player_count -- Removed clusterio_UPS_gauge and added clusterio_instance_game_ticks_total in - its place. +- Renamed clusterio_player_count_gauge to + clusterio_statistics_exporter_instance_player_count +- Removed clusterio_UPS_gauge and added + clusterio_statistics_exporter_instance_game_ticks_total in its place. - Renamed clusterio_endpoint_hit_gauge to clusterio_http_enpoint_hits_total -- Renamed clusterio_statistics_gauge to clusterio_instance_force_flows +- Renamed clusterio_statistics_gauge to + clusterio_statistics_exporter_instance_force_flows +- Renamed clusterio_nn_dole_gauge to clusterio_subspace_storage_nn_dole_gauge. +- Renamed clusterio_dole_factor_gauge to + clusterio_subspace_storage_dole_factor_gauge. +- Renamed clusterio_import_gauge to clusterio_subspace_storage_import_total. +- Renamed clusterio_export_gauge to clusterio_subspace_storage_export_total. +- Renamed clusterio_master_inventory_gauge to + clusterio_subspace_storage_master_inventory. - Removed clusterioMod plugin specific config options logItemTransfers, disableFairItemDistribution useNeuralNetDoleDivider, autosaveInterval, and disableImportsOfEverythingExceptElectricity from the master config. diff --git a/docs/writing-plugins.md b/docs/writing-plugins.md index 16d8865d..0a1404fd 100644 --- a/docs/writing-plugins.md +++ b/docs/writing-plugins.md @@ -459,21 +459,24 @@ the metric and updating it somewhere in the plugin code. For example: const { Counter } = require('lib/prometheus'); const fooMetric = new Counter( - 'clusterio_foo_metric', "Measures the level of foo", + 'clusterio_foo_frobber_foo_metric', "Measures the level of foo", ); // Somewhere in the master plugin code fooMetric.inc(); This works for master plugins, and the metric will be automatically -available through the /metric HTTP endpoint. For metrics that are -per-instance you must define an `instance_id` label and set it -accordingly, for example: +available through the /metric HTTP endpoint. It's recommended that +plugin metrics follow `clusterio__` as the +naming scheme. + +For metrics that are per-instance you must define an `instance_id` label and +set it accordingly, for example: const { Counter } = require('lib/prometheus'); const barMetric = new Gauge( - 'clusterio_bar_metric', "Bar instance level", + 'clusterio_foo_frobber_bar_metric', "Bar instance level", { labels: ['instance_id'] } ); diff --git a/plugins/statistics_exporter/instance.js b/plugins/statistics_exporter/instance.js index eee2e0ab..4ca365ad 100644 --- a/plugins/statistics_exporter/instance.js +++ b/plugins/statistics_exporter/instance.js @@ -3,19 +3,19 @@ const { Gauge } = require('lib/prometheus'); const instancePlayerCount = new Gauge( - 'clusterio_instance_player_count', "Amount of players connected to this cluster", + "clusterio_statistics_exporter_instance_player_count", "Amount of players connected to this cluster", { labels: ['instance_id'] } ); const instanceGameTicksTotal = new Gauge( - 'clusterio_instance_game_ticks_total', "Game tick an instance has progressed to", + "clusterio_statistics_exporter_instance_game_ticks_total", "Game tick an instance has progressed to", { labels: ['instance_id'] } ); const instanceForceFlowStatistics = new Gauge( - 'clusterio_instance_force_flow_statistics', "Items/fluids/enemies/buildings produced/built/killed by a force", + "clusterio_statistics_exporter_instance_force_flow_statistics", "Items/fluids/enemies/buildings produced/built/killed by a force", { labels: ['instance_id', 'force', 'statistic', 'direction', 'name'] }, ); const instanceGameFlowStatistics = new Gauge( - 'clusterio_instance_game_flow_statistics', "Pollution produced/consumed in the game", + "clusterio_statistics_exporter_instance_game_flow_statistics", "Pollution produced/consumed in the game", { labels: ['instance_id', 'statistic', 'direction', 'name'] }, ); diff --git a/plugins/subspace_storage/dole.js b/plugins/subspace_storage/dole.js index 7f2f2b0d..5a4d9306 100644 --- a/plugins/subspace_storage/dole.js +++ b/plugins/subspace_storage/dole.js @@ -4,12 +4,12 @@ const prometheus = require("lib/prometheus"); const prometheusNNDoleGauge = new prometheus.Gauge( - "clusterio_nn_dole_gauge", + "clusterio_subspace_storage_nn_dole_gauge", "Current demand being supplied by Neural Network ; 1 means all demand covered, 0.5 means about half of each supply is covered, 0 means no items are given", { labels: ["resource"] } ); const prometheusDoleFactorGauge = new prometheus.Gauge( - "clusterio_dole_factor_gauge", + "clusterio_subspace_storage_dole_factor_gauge", "The current dole division factor for this resource", { labels: ["resource"] } ); diff --git a/plugins/subspace_storage/master.js b/plugins/subspace_storage/master.js index 8fbd78dd..f2ce2d35 100644 --- a/plugins/subspace_storage/master.js +++ b/plugins/subspace_storage/master.js @@ -10,25 +10,25 @@ const routes = require("./routes"); const dole = require("./dole"); -const prometheusExportGauge = new prometheus.Gauge( - "clusterio_export_gauge", +const exportCounter = new prometheus.Counter( + "clusterio_subspace_storage_export_total", "Resources exported by instance", { labels: ["instance_id", "resource"] } ); -const prometheusImportGauge = new prometheus.Gauge( - "clusterio_import_gauge", +const importCounter = new prometheus.Counter( + "clusterio_subspace_storage_import_total", "Resources imported by instance", { labels: ["instance_id", "resource"] } ); +const masterInventoryGauge = new prometheus.Gauge( + "clusterio_subspace_storage_master_inventory", + "Amount of resources stored on master", + { labels: ["resource"] } +); class MasterPlugin extends plugin.BaseMasterPlugin { async init() { - this.prometheusMasterInventoryGauge = new prometheus.Gauge( - "clusterio_master_inventory_gauge", - "Amount of resources stored on master", - { labels: ["resource"] } - ); this.ui = { sidebar: [ @@ -91,7 +91,7 @@ class MasterPlugin extends plugin.BaseMasterPlugin { for (let item of message.data.items) { this.items.addItem(item[0], item[1]); - prometheusExportGauge.labels(String(instanceId), item[0]).inc(item[1]); + exportCounter.labels(String(instanceId), item[0]).inc(item[1]); } this.updateStorage(); @@ -152,7 +152,7 @@ class MasterPlugin extends plugin.BaseMasterPlugin { if (itemsRemoved.length) { for (let item of itemsRemoved) { - prometheusImportGauge.labels(String(instanceId), item[0]).inc(item[1]); + importCounter.labels(String(instanceId), item[0]).inc(item[1]); } this.updateStorage(); @@ -171,7 +171,7 @@ class MasterPlugin extends plugin.BaseMasterPlugin { onMetrics() { if (this.items) { for (let [key, count] of this.items._items) { - this.prometheusMasterInventoryGauge.labels(key).set(Number(count) || 0); + masterInventoryGauge.labels(key).set(Number(count) || 0); } } } From 541d2ad337f2d868430ada15bdebbc011e952e55 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 3 Apr 2020 21:15:57 +0200 Subject: [PATCH 15/23] Add missing changlog entry for 737c9924 Add entry to changelog noting that the clusterioMod plugin has been renamed to subspace_storage. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5f2946f..a3f6172d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -158,6 +158,7 @@ Version 2.0.0 arguments from the master server. - Implemented a new config system that replaces the old. Breaks all plugins. - Removed usage of socket.io entirely in favor of a plain WebSocket connection. +- Renamed clusterioMod plugin to subspace_storage. Version 1.2.4 From 3343d53d753d0099c8d4791fa052e41fe67e406d Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 3 Apr 2020 22:22:23 +0200 Subject: [PATCH 16/23] Allow deleting unassigned instances Handle delete instance requests for instances that are not assigned to a slave. --- master.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/master.js b/master.js index c8529f77..5bbb2f20 100644 --- a/master.js +++ b/master.js @@ -411,6 +411,18 @@ class ControlConnection extends BaseConnection { db.instances.set(instanceId, instanceConfig); } + async deleteInstanceRequestHandler(message, request) { + let instanceConfig = db.instances.get(message.data.instance_id); + if (!instanceConfig) { + throw new errors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`); + } + + if (instanceConfig.get('instance.assigned_slave') !== null) { + await this.forwardRequestToInstance(message, request); + } + db.instances.delete(message.data.instance_id); + } + async getInstanceConfigRequestHandler(message) { let instanceConfig = db.instances.get(message.data.instance_id); if (!instanceConfig) { From 5e9d9097e1199c7be6812778861e732d18067cfe Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 3 Apr 2020 22:32:12 +0200 Subject: [PATCH 17/23] Link mods when creating save Ensure the mods are linked in when creating a new save. --- slave.js | 1 + 1 file changed, 1 insertion(+) diff --git a/slave.js b/slave.js index 464e340d..2ebcd27d 100644 --- a/slave.js +++ b/slave.js @@ -278,6 +278,7 @@ class Instance extends link.Link{ await this.writeServerSettings(); console.log("Creating save ....."); + await symlinkMods(this, "sharedMods", console); await this.server.create("world"); console.log("Clusterio | Successfully created save"); From 8ebf0fc7543281aa7a9594d9015ae0882900feca Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 4 Apr 2020 10:05:37 +0200 Subject: [PATCH 18/23] Add master server Web Root config Add web_root option to the master server and use it create URLs for resources hosted by it. This permits hosting the master server web interface behind reverse proxy under a dedicated path. --- CHANGELOG.md | 2 ++ lib/config/definitions.js | 7 +++++++ master.js | 1 + plugins/subspace_storage/master.js | 3 ++- plugins/subspace_storage/static/storage.js | 2 +- plugins/subspace_storage/views/storage.ejs | 2 +- static/main.js | 4 ++-- views/head.ejs | 15 ++++++++------- views/nav.ejs | 6 +++--- 9 files changed, 27 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3f6172d..efe173b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ Version 2.0.0 server provided the session can be resumed. - Added support for having multiple Factorio installs and selecting which version to use on a per instance basis. +- Added config to specify which path the master interface is accessed under, + allowing it to be proxied behind web-server. ### Changes diff --git a/lib/config/definitions.js b/lib/config/definitions.js index 576c7085..95f3f737 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -42,6 +42,13 @@ MasterGroup.define({ optional: true, initial_value: 8443, }); +MasterGroup.define({ + name: "web_root", + title: "Web Root", + description: "Sub-path to where the web interface is hosted. Needed when proxying the web interface.", + type: "string", + initial_value: "/", +}); MasterGroup.define({ name: "tls_certificate", title: "TLS Certificate", diff --git a/master.js b/master.js index 5bbb2f20..977f391b 100644 --- a/master.js +++ b/master.js @@ -72,6 +72,7 @@ app.set('views', ['views', 'plugins']); // give ejs access to some interesting information app.use(function(req, res, next){ + res.locals.root = masterConfig.get("master.web_root"); res.locals.res = res; res.locals.req = req; res.locals.masterPlugins = masterPlugins; diff --git a/plugins/subspace_storage/master.js b/plugins/subspace_storage/master.js index f2ce2d35..3170af23 100644 --- a/plugins/subspace_storage/master.js +++ b/plugins/subspace_storage/master.js @@ -30,12 +30,13 @@ const masterInventoryGauge = new prometheus.Gauge( class MasterPlugin extends plugin.BaseMasterPlugin { async init() { + let root = this.master.config.get("master.web_root"); this.ui = { sidebar: [ { getHtml: () => ` `, }, ], diff --git a/plugins/subspace_storage/static/storage.js b/plugins/subspace_storage/static/storage.js index 9a99819d..b65326ae 100644 --- a/plugins/subspace_storage/static/storage.js +++ b/plugins/subspace_storage/static/storage.js @@ -62,7 +62,7 @@ function updateInventory() { drawcontents(data); } } - xmlhttp.open("GET", "/api/inventory", true); + xmlhttp.open("GET", `${root}api/inventory`, true); xmlhttp.send(); } if(JSON.parse(localStorage.settings)["Periodically update storage screen"]) { diff --git a/plugins/subspace_storage/views/storage.ejs b/plugins/subspace_storage/views/storage.ejs index da151dd7..f084cac4 100644 --- a/plugins/subspace_storage/views/storage.ejs +++ b/plugins/subspace_storage/views/storage.ejs @@ -1,7 +1,7 @@ <%- include("head") %> - + diff --git a/static/main.js b/static/main.js index deceacfe..a3330fbe 100644 --- a/static/main.js +++ b/static/main.js @@ -198,7 +198,7 @@ function getImageFromName(name){ return "https://wiki.factorio.com/images/" + capitalizeFirstLetter(replaceAll(factorioLocale["fluid-name"][name], ' ', '_').toLowerCase()) + ".png"; } else if(factorioLocale["equipment-name"] && factorioLocale["equipment-name"][name]){ return "https://wiki.factorio.com/images/" + capitalizeFirstLetter(replaceAll(factorioLocale["equipment-name"][name], ' ', '_').toLowerCase()) + ".png"; - } else return "/pictures/unknown-item.png"; + } else return `${root}pictures/unknown-item.png`; } window.factorioLocale = {}; $.getJSON("/api/getFactorioLocale", (locale) => { @@ -252,6 +252,6 @@ var imagelinks = { "battery-mk2-equipment": "/pictures/Battery_MK2.png", "personal-roboport-mk2-equipment": "/pictures/Personal_roboport_MK2.png", } -var populateImageLinks = async () => (await getJSON("/api/getPictures")).forEach(img => imagelinks[img.name] = img.path) +var populateImageLinks = async () => (await getJSON(`${root}api/getPictures`)).forEach(img => imagelinks[img.name] = img.path) populateImageLinks() diff --git a/views/head.ejs b/views/head.ejs index f2dec05e..dddb18dc 100644 --- a/views/head.ejs +++ b/views/head.ejs @@ -1,12 +1,13 @@ Clusterio - - - - - - - + + + + + + + + diff --git a/views/nav.ejs b/views/nav.ejs index a6b300a2..a6ca3760 100644 --- a/views/nav.ejs +++ b/views/nav.ejs @@ -1,12 +1,12 @@ From 31b66339a08858a2ee2d9b80fea9d5f90409af88 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 4 Apr 2020 10:15:27 +0200 Subject: [PATCH 19/23] Remove Google Analytics Remove hard coded Google Analytics integration in web interface. --- views/head.ejs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/views/head.ejs b/views/head.ejs index dddb18dc..6f61484b 100644 --- a/views/head.ejs +++ b/views/head.ejs @@ -27,13 +27,3 @@ - - - From 75067e992d798173ab8db6c5a2a4b73258682000 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 4 Apr 2020 12:03:50 +0200 Subject: [PATCH 20/23] Add WebSocket usage statistics Add Prometheus metrics for messages sent, connections made, connections rejected, active connections, and active slaves. --- CHANGELOG.md | 1 + master.js | 53 ++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efe173b3..db27eb9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Version 2.0.0 version to use on a per instance basis. - Added config to specify which path the master interface is accessed under, allowing it to be proxied behind web-server. +- Added WebSocket usage statistics. ### Changes diff --git a/master.js b/master.js index 977f391b..876d9bac 100644 --- a/master.js +++ b/master.js @@ -86,11 +86,38 @@ require("./routes/api/getPictures")(app); // Set folder to serve static content from (the website) app.use(express.static('static')); -const endpointHitCounter = new prometheus.Gauge( - 'clusterio_master_http_endpoint_hits_total', "How many requests a particular endpoint has gotten", +const endpointHitCounter = new prometheus.Counter( + "clusterio_master_http_endpoint_hits_total", + "How many requests a particular HTTP endpoint has gotten", { labels: ['route'] } ); +const wsMessageCounter = new prometheus.Counter( + "clusterio_master_websocket_message_total", + "How many messages have been received over WebSocket on the master server", + { labels: ["direction"] } +); + +const wsConnectionsCounter = new prometheus.Counter( + "clusterio_master_websocket_connections_total", + "How many WebSocket connections have been initiated on the master server" +); + +const wsRejectedConnectionsCounter = new prometheus.Counter( + "clusterio_master_websocket_rejected_connections_total", + "How many WebSocket connections have been rejected during the handshake on the master server" +); + +const wsActiveConnectionsGauge = new prometheus.Gauge( + "clusterio_master_websocket_active_connections", + "How many WebSocket connections are currently open to the master server" +); + +const wsActiveSlavesGauge = new prometheus.Gauge( + "clusterio_master_active_slaves", + "How many slaves are currently connected to the master" +); + // Prometheus polling endpoint async function getMetrics(req, res, next) { endpointHitCounter.labels(req.route.path).inc(); @@ -133,6 +160,10 @@ async function getMetrics(req, res, next) { results.push(prometheus.deserializeResult(result)); } + wsActiveConnectionsGauge.set(activeConnectors.size); + wsActiveSlavesGauge.set(slaveConnections.size); + + let text = await prometheus.exposition(results); res.set('Content-Type', prometheus.exposition.contentType); res.send(text); @@ -513,10 +544,6 @@ class SlaveConnection extends BaseConnection { version: this._version, }); - this.connector.on('message', () => { - // XXX prometheusWsUsageCounter.labels('message', "other").inc(); - }); - this.connector.on("close", () => { if (slaveConnections.get(this._id) === this) { slaveConnections.delete(this._id); @@ -744,6 +771,15 @@ let activeConnectors = new Map(); wss.on("connection", function (socket, req) { console.log(`SOCKET | new connection from ${req.socket.remoteAddress}`); + // Track statistics + wsConnectionsCounter.inc(); + socket.on("message", () => { wsMessageCounter.labels("in").inc(); }); + let originalSend = socket.send; + socket.send = (...args) => { + wsMessageCounter.labels("out").inc(); + return originalSend.call(socket, ...args); + }; + // Start connection handshake. socket.send(JSON.stringify({ seq: null, type: "hello", data: { version }})); @@ -752,6 +788,7 @@ wss.on("connection", function (socket, req) { let timeoutId = setTimeout(() => { console.log(`SOCKET | closing ${req.socket.remoteAddress} after timing out on handshake`); + wsRejectedConnectionsCounter.inc(); socket.close(1008, "Handshake timeout"); pendingSockets.delete(socket); }, 30*1000); @@ -769,6 +806,7 @@ wss.on("connection", function (socket, req) { +----------------------------------------------------------------+` ); console.error(err); + wsRejectedConnectionsCounter.inc(); socket.close(1011, "Unexpected error"); }); }); @@ -788,12 +826,14 @@ async function handleHandshake(message, socket, req, attachHandler) { message = JSON.parse(message); } catch (err) { console.log(`SOCKET | closing ${req.socket.remoteAddress} after receiving invalid JSON`); + wsRejectedConnectionsCounter.inc(); socket.close(1001, "Invalid JSON"); return; } if (!schema.clientHandshake(message)) { console.log(`SOCKET | closing ${req.socket.remoteAddress} after receiving invalid handshake:`, message); + wsRejectedConnectionsCounter.inc(); socket.close(1001, "Bad handshake"); return; } @@ -828,6 +868,7 @@ async function handleHandshake(message, socket, req, attachHandler) { if (!result.ok) { console.error(`SOCKET | authentication failed for ${req.socket.remoteAddress}`); + wsRejectedConnectionsCounter.inc(); socket.close(4003, "Authentication failed"); return; } From 99ac8f4f3365f3d6da18c4a43abccedc7a89b166 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 4 Apr 2020 12:18:26 +0200 Subject: [PATCH 21/23] Add commands sent statistic Track RCON commands sent. --- CHANGELOG.md | 1 + slave.js | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db27eb9a..b5bbfb88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Version 2.0.0 - Added config to specify which path the master interface is accessed under, allowing it to be proxied behind web-server. - Added WebSocket usage statistics. +- Added commands sent statistic. ### Changes diff --git a/slave.js b/slave.js index 2ebcd27d..a18575be 100644 --- a/slave.js +++ b/slave.js @@ -16,6 +16,12 @@ const luaTools = require('lib/luaTools'); const config = require('lib/config'); +const instanceRconCommandsCounter = new prometheus.Counter( + "clusterio_instance_rcon_commands_total", + "How many commands have been sent to the instance", + { labels: ["instance_id"] } +); + /** * Keeps track of the runtime parameters of an instance */ @@ -39,6 +45,12 @@ class Instance extends link.Link{ factorioDir, this._dir, serverOptions ); + let originalSendRcon = this.server.sendRcon; + this.server.sendRcon = (...args) => { + instanceRconCommandsCounter.labels(String(this.config.get("instance.id"))).inc(); + return originalSendRcon.call(this.server, ...args); + } + this.server.on('output', (output) => { link.messages.instanceOutput.send(this, { instance_id: this.config.get("instance.id"), output }) From 75cd6d38dbd93afd673a9ffb7056562d5f9fe351 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 4 Apr 2020 15:14:23 +0200 Subject: [PATCH 22/23] Fix instance exit notification Fix improper notification on Factorio server exit causing plugins and the instance tracking to fail. --- lib/factorio/server.js | 5 +-- lib/link/messages.js | 5 +++ lib/plugin.js | 4 +-- plugins/subspace_storage/instance.js | 2 +- slave.js | 47 +++++++++++++--------------- test/lib/plugin.js | 2 +- 6 files changed, 33 insertions(+), 32 deletions(-) diff --git a/lib/factorio/server.js b/lib/factorio/server.js index d2934d9f..0a765ddc 100644 --- a/lib/factorio/server.js +++ b/lib/factorio/server.js @@ -419,7 +419,7 @@ const outputHeuristics = [ * output - invoked with parsed output from Factorio * rcon-ready - invoked when the RCON client has connected * game-ready - invoked when the server is finished starting up - * stopped - invoked when the sterver has exited + * exit - invoked when the sterver has exited */ class FactorioServer extends events.EventEmitter { /** @@ -636,7 +636,7 @@ class FactorioServer extends events.EventEmitter { this._gameReady = false; this._unexpected = []; - this.emit('stopped'); + this.emit("exit"); }); } @@ -678,6 +678,7 @@ class FactorioServer extends events.EventEmitter { ); this._attachStdio(); + this._server.once("exit", () => this.emit("exit")); try { let [code, signal] = await events.once(this._server, 'exit'); diff --git a/lib/link/messages.js b/lib/link/messages.js index 9108cffb..05305053 100644 --- a/lib/link/messages.js +++ b/lib/link/messages.js @@ -484,6 +484,11 @@ class Event { } } +messages.instanceStopped = new Event({ + type: 'instance_stopped', + links: ['instance-slave'], +}); + messages.updateInstances = new Event({ type: 'update_instances', links: ['slave-master'], diff --git a/lib/plugin.js b/lib/plugin.js index 3f88d499..abd97ad6 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -72,9 +72,9 @@ class BaseInstancePlugin { async onStop() { } /** - * Called when the instance is shutting down + * Called when the Factorio server exited */ - async onExit() { } + onExit() { } /** * Called when the Factorio outputs a line diff --git a/plugins/subspace_storage/instance.js b/plugins/subspace_storage/instance.js index c4e38269..b0d40ab1 100644 --- a/plugins/subspace_storage/instance.js +++ b/plugins/subspace_storage/instance.js @@ -32,7 +32,7 @@ class InstancePlugin extends plugin.BaseInstancePlugin { await this.instance.server.sendRcon(`/sc __subspace_storage__ UpdateInvData("${itemsJson}", true)`, true); } - async onStop() { + onExit() { clearInterval(this.pingId); } diff --git a/slave.js b/slave.js index a18575be..8b748680 100644 --- a/slave.js +++ b/slave.js @@ -60,6 +60,19 @@ class Instance extends link.Link{ }); } }); + + this.server.on("error", err => { + console.log(`Error in instance ${this.name}:`, err); + }); + + this.server.on("exit", err => { + link.messages.instanceStopped.send(this); + + // Notify plugins of exit + for (let pluginInstance of this.plugins.values()) { + pluginInstance.onExit(); + } + }); } async init(pluginInfos, slave) { @@ -250,11 +263,6 @@ class Instance extends link.Link{ collector.removeAll({ instance_id: String(this.config.get("instance.id")) }); } } - - // Notify plugins of exit - for (let pluginInstance of this.plugins.values()) { - await pluginInstance.onExit(); - } } async getMetricsRequestHandler() { @@ -294,11 +302,6 @@ class Instance extends link.Link{ await this.server.create("world"); console.log("Clusterio | Successfully created save"); - - // Notify plugins of exit - for (let pluginInstance of this.plugins.values()) { - await pluginInstance.onExit(); - } } async stopInstanceRequestHandler() { @@ -373,9 +376,10 @@ async function discoverInstances(instancesDir, logger) { } class InstanceConnection extends link.Link { - constructor(connector, slave) { + constructor(connector, slave, instanceId) { super('slave', 'instance', connector); this.slave = slave; + this.instanceId = instanceId; link.attachAllMessages(this); for (let pluginInfo of slave.pluginInfos) { @@ -413,6 +417,10 @@ class InstanceConnection extends link.Link { event.send(instanceConnection, message.data); } } + + async instanceStoppedEventHandler(message) { + this.slave.instanceConnections.delete(this.instanceId); + } } class SlaveConnector extends link.WebSocketClientConnector { @@ -552,7 +560,7 @@ class Slave extends link.Link { } let [connectionClient, connectionServer] = link.VirtualConnector.makePair(); - let instanceConnection = new InstanceConnection(connectionServer, this); + let instanceConnection = new InstanceConnection(connectionServer, this, instanceId); let instance = new Instance( connectionClient, instanceInfo.path, this.config.get("slave.factorio_directory"), instanceInfo.config ); @@ -604,25 +612,12 @@ class Slave extends link.Link { async createSaveRequestHandler(message, request) { let instanceId = message.data.instance_id; let instanceConnection = await this._connectInstance(instanceId); - try { - await request.send(instanceConnection, message.data); - this.instanceConnections.delete(instanceId); - - } catch (err) { - await this.stopInstance(instanceId); - throw err; - } + await request.send(instanceConnection, message.data); } async stopInstance(instanceId) { let instanceConnection = this.instanceConnections.get(instanceId); await link.messages.stopInstance.send(instanceConnection, { instance_id: instanceId }); - this.instanceConnections.delete(instanceId); - } - - async stopInstanceRequestHandler(message, request) { - await this.forwardRequestToInstance(message, request); - this.instanceConnections.delete(message.data.instance_id); } async deleteInstanceRequestHandler(message) { diff --git a/test/lib/plugin.js b/test/lib/plugin.js index 632708b4..4072aebf 100644 --- a/test/lib/plugin.js +++ b/test/lib/plugin.js @@ -18,7 +18,7 @@ describe("lib/plugin", function() { it("should define defaults for hooks", async function() { await instancePlugin.onStart(); await instancePlugin.onStop(); - await instancePlugin.onExit(); + instancePlugin.onExit(); await instancePlugin.onOutput({}); }) }); From ccb07d6eab2a42fdaec9797f95f142a1ec0ea82f Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Mon, 6 Apr 2020 14:41:51 +0200 Subject: [PATCH 23/23] Add support for Factorio 0.18.x Add option to bump patch number and override factorio_version in the generated mod and update the serialize library to support the inventory bar access methods of both 0.17 and 0.18. --- .travis.yml | 2 ++ build-mod.js | 16 ++++++++++++++++ mod/changelog.txt | 4 ++++ mod/info.json | 2 +- modules/clusterio/serialize.lua | 20 ++++++++++++++++---- 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index b915da86..89adafcb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,8 @@ before_install: - tar -xf factorio.tar.gz - npm install -g codecov script: + - node build-mod --bump-patch --factorio-version 0.18 --output-dir sharedMods + - curl -o sharedMods/subspace_storage_1.99.1.zip -L https://github.com/clusterio/factorioClusterioMod/releases/download/1.99.1/subspace_storage_1.99.1.zip - npm run-script ci-cover - cat temp/test/integration/log.txt - codecov -f coverage/lcov.info diff --git a/build-mod.js b/build-mod.js index ef47f8a4..2d1cb81d 100644 --- a/build-mod.js +++ b/build-mod.js @@ -23,12 +23,24 @@ async function main() { 'build': { describe: "Build mod", type: 'boolean', default: true }, 'pack': { describe: "Pack into zip file", type: 'boolean', default: true }, 'output-dir': { describe: "Path to output built mod", nargs: 1, type: 'string', default: "dist" }, + "bump-patch": { describe: "Increment patch number of build", type: "boolean", default: false }, + "factorio-version": { describe: "Override factorio_version", type: "string" }, }) .argv ; let info = JSON.parse(await fs.readFile(path.join(sourceDir, "info.json"))); + if (args.bumpPatch) { + let [major, minor, patch] = info.version.split("."); + patch = String(Number.parseInt(patch, 10) + 1); + info.version = [major, minor, patch].join("."); + } + + if (args.factorioVersion) { + info.factorio_version = args.factorioVersion; + } + if (args.clean) { let splitter = /^(.*)_(\d+\.\d+\.\d+)(\.zip)?$/ for (let entry of await fs.readdir(args.outputDir)) { @@ -67,6 +79,8 @@ async function main() { zip.file(path.posix.join(modName, path.basename(filePath)), fs.createReadStream(filePath)); } + zip.file(path.posix.join(modName, "info.json"), JSON.stringify(info, null, 4)); + let modPath = path.join(args.outputDir, `${modName}.zip`); console.log(`Writing ${modPath}`); let writeStream = zip.generateNodeStream().pipe(fs.createWriteStream(modPath)); @@ -83,6 +97,8 @@ async function main() { for (let filePath of additionalFiles) { await fs.copy(filePath, path.join(modDir, path.basename(filePath))); } + + await fs.writeFile(path.join(modDir, "info.json"), JSON.stringify(info, null, 4)); } } } diff --git a/mod/changelog.txt b/mod/changelog.txt index 50e3310e..fdce82ea 100644 --- a/mod/changelog.txt +++ b/mod/changelog.txt @@ -1,4 +1,8 @@ --------------------------------------------------------------------------------------------------- +Version: 0.1.1 + Features: + - Added support for Factorio 0.18.x +--------------------------------------------------------------------------------------------------- Version: 0.1.0 Major Features: - API for mods to interact with Clusterio diff --git a/mod/info.json b/mod/info.json index e2c5ebe1..dd75e41f 100644 --- a/mod/info.json +++ b/mod/info.json @@ -1,6 +1,6 @@ { "name": "clusterio_lib", - "version": "0.1.0", + "version": "0.1.1", "title": "Clusterio Library (Alpha)", "author": "Clusterio Team", "homepage": "https://github.com/clusterio/factorioClusterio", diff --git a/modules/clusterio/serialize.lua b/modules/clusterio/serialize.lua index 1714e194..ee247e63 100644 --- a/modules/clusterio/serialize.lua +++ b/modules/clusterio/serialize.lua @@ -2,6 +2,18 @@ -- Based on code from playerManager and trainTeleports local serialize = {} +-- 0.17 compatibility +local supports_bar, get_bar, has_bar +if (pcall(function() local mods = script.active_mods end)) then + supports_bar = "supports_bar" + get_bar = "get_bar" + set_bar = "set_bar" +else + supports_bar = "hasbar" + get_bar = "getbar" + set_bar = "setbar" +end + -- Equipment Grids are serialized into an array of equipment entries -- where ench entry is a table with the following fields: -- n: name @@ -181,8 +193,8 @@ end -- It's also possible that the slot is empty but has a slot filter. function serialize.serialize_inventory(inventory) local serialized = {} - if inventory.hasbar() and inventory.getbar() <= #inventory then - serialized.b = inventory.getbar() + if inventory[supports_bar]() and inventory[get_bar]() <= #inventory then + serialized.b = inventory[get_bar]() end serialized.i = {} @@ -227,8 +239,8 @@ function serialize.serialize_inventory(inventory) end function serialize.deserialize_inventory(inventory, serialized) - if serialized.b and inventory.hasbar() then - inventory.setbar(serialized.b) + if serialized.b and inventory[supports_bar]() then + inventory[set_bar](serialized.b) end local last_slot_index = 0