From 1b901f5d1cb07ef8c220926601ab4937e2627ed0 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Tue, 5 Jan 2021 22:28:08 +0100 Subject: [PATCH] Fix the remaning linter warnings (#353) Refactor the code base to remove all of the remaining warnings from ESLint and convert them into errors. --- .eslintrc.js | 40 +- packages/ctl/ctl.js | 82 ++-- packages/lib/RateLimiter.js | 2 +- packages/lib/build_mod.js | 117 +++--- packages/lib/config/classes.js | 7 +- packages/lib/config/index.js | 1 + packages/lib/database.js | 6 +- packages/lib/factorio/export.js | 189 +++++----- packages/lib/factorio/index.js | 1 + packages/lib/factorio/patch.js | 6 +- packages/lib/factorio/server.js | 8 +- packages/lib/hash.js | 6 +- packages/lib/is_deep_strict_equal.js | 1 + packages/lib/link/index.js | 1 + packages/lib/link/link.js | 3 +- packages/lib/link/messages.js | 13 +- packages/lib/logging_utils.js | 8 +- packages/lib/package.json | 1 + packages/lib/plugin.js | 4 +- packages/lib/plugin_loader.js | 46 +++ packages/lib/schema.js | 3 +- packages/lib/shared_commands.js | 1 + packages/master/master.js | 354 ++++++++++-------- packages/slave/slave.js | 154 ++++---- packages/web_ui/src/components/data-table.jsx | 136 ++++--- packages/web_ui/webpack.common.js | 2 +- plugins/research_sync/master.js | 48 ++- plugins/statistics_exporter/instance.js | 12 +- plugins/subspace_storage/master.js | 56 ++- plugins/subspace_storage/routes.js | 6 +- test/integration/clusterio.js | 2 +- test/integration/server.js | 4 +- test/lib/config/classes.js | 7 +- 33 files changed, 718 insertions(+), 609 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 30f2d3bd..49d9ea77 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -45,7 +45,7 @@ module.exports = { "arrow-spacing": ["error", { "after": true, "before": true }], "block-scoped-var": "error", "block-spacing": "error", - "brace-style": ["warn", "1tbs", { "allowSingleLine": true }], + "brace-style": ["error", "1tbs", { "allowSingleLine": true }], "callback-return": "off", "camelcase": "off", "capitalized-comments": "off", @@ -60,18 +60,18 @@ module.exports = { "functions": "only-multiline", }, ], - "comma-spacing": "warn", + "comma-spacing": "error", "comma-style": ["error", "last"], - "complexity": "warn", + "complexity": "error", "computed-property-spacing": ["error", "never"], "consistent-return": "error", "consistent-this": "error", - "curly": "warn", + "curly": "error", "default-case": "error", "default-case-last": "error", "default-param-last": "error", "dot-location": ["error", "property"], - "dot-notation": ["warn", { "allowPattern": "^[a-z]+(_[a-z]+)*$" }], + "dot-notation": ["error", { "allowPattern": "^[a-z]+(_[a-z]+)*$" }], "eol-last": "error", "eqeqeq": "error", "func-call-spacing": "error", @@ -81,7 +81,7 @@ module.exports = { "function-call-argument-newline": "off", "function-paren-newline": "off", "generator-star-spacing": ["error", { "before": false, "after": true }], - "global-require": "warn", + "global-require": "error", "grouped-accessor-pairs": "error", "guard-for-in": "error", "handle-callback-err": "error", @@ -94,22 +94,22 @@ module.exports = { "init-declarations": "off", "jsx-quotes": "error", "key-spacing": "error", - "keyword-spacing": "warn", + "keyword-spacing": "error", "line-comment-position": "off", "linebreak-style": ["error", "unix"], "lines-around-comment": "off", "lines-around-directive": "off", "lines-between-class-members": ["error", "always"], "max-classes-per-file": "off", - "max-depth": "warn", - "max-len": ["warn", { "code": 120 }], + "max-depth": "error", + "max-len": ["error", { "code": 120 }], "max-lines": "off", "max-lines-per-function": "off", "max-nested-callbacks": "error", "max-params": "off", "max-statements": "off", "max-statements-per-line": "off", - "multiline-comment-style": ["warn", "separate-lines"], + "multiline-comment-style": ["error", "separate-lines"], "multiline-ternary": "off", "new-cap": "error", "new-parens": "error", @@ -130,7 +130,7 @@ module.exports = { "no-continue": "off", "no-div-regex": "error", "no-duplicate-imports": "error", - "no-else-return": "warn", + "no-else-return": "error", "no-empty-function": "off", "no-eq-null": "error", "no-eval": "error", @@ -143,7 +143,7 @@ module.exports = { "no-implicit-globals": "error", "no-implied-eval": "error", "no-inline-comments": "off", - "no-invalid-this": "warn", + "no-invalid-this": "error", "no-iterator": "error", "no-label-var": "error", "no-lone-blocks": "error", @@ -179,7 +179,7 @@ module.exports = { "no-path-concat": "error", "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }], "no-process-env": "error", - "no-process-exit": "warn", + "no-process-exit": "error", "no-proto": "error", "no-restricted-exports": "error", "no-restricted-globals": "error", @@ -205,7 +205,7 @@ module.exports = { "no-unmodified-loop-condition": "error", "no-unneeded-ternary": "error", "no-unused-expressions": "error", - "no-use-before-define": "warn", + "no-use-before-define": "error", "no-useless-backreference": "error", "no-useless-call": "error", "no-useless-computed-key": "error", @@ -213,7 +213,7 @@ module.exports = { "no-useless-constructor": "error", "no-useless-rename": "error", "no-useless-return": "error", - "no-var": "warn", + "no-var": "error", "no-void": ["error", { "allowAsStatement": true }], "no-warning-comments": "off", "no-whitespace-before-property": "error", @@ -234,7 +234,7 @@ module.exports = { }], "padded-blocks": "off", "padding-line-between-statements": "error", - "prefer-arrow-callback": "warn", + "prefer-arrow-callback": "error", "prefer-const": "off", "prefer-destructuring": "off", "prefer-exponentiation-operator": "error", @@ -246,7 +246,7 @@ module.exports = { "prefer-regex-literals": "error", "prefer-rest-params": "error", "prefer-spread": "error", - "prefer-template": "warn", + "prefer-template": "error", "quote-props": "off", "quotes": ["error", "double", { "avoidEscape": true }], "radix": ["error", "always"], @@ -261,12 +261,12 @@ module.exports = { "sort-imports": "off", "sort-keys": "off", "sort-vars": "error", - "space-before-blocks": "warn", + "space-before-blocks": "error", "space-before-function-paren": "off", "space-in-parens": ["error", "never"], "space-infix-ops": "off", "space-unary-ops": "error", - "spaced-comment": ["warn", "always"], + "spaced-comment": ["error", "always"], "strict": "error", "switch-colon-spacing": "error", "symbol-description": "error", @@ -274,7 +274,7 @@ module.exports = { "template-tag-spacing": "error", "unicode-bom": ["error", "never"], "valid-jsdoc": ["error", { "requireReturn": false }], - "vars-on-top": "warn", + "vars-on-top": "error", "wrap-iife": "error", "wrap-regex": "off", "yield-star-spacing": "error", diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.js index 6b982e94..cf3f28d8 100755 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.js @@ -12,6 +12,7 @@ const asTable = require("as-table").configure({ delimiter: " | " }); const events = require("events"); const path = require("path"); const winston = require("winston"); +const setBlocking = require("set-blocking"); const libLink = require("@clusterio/lib/link"); const libErrors = require("@clusterio/lib/errors"); @@ -627,6 +628,47 @@ class Control extends libLink.Link { } } +async function loadPlugins(pluginList) { + let pluginInfos = await libPluginLoader.loadPluginInfos(pluginList); + libConfig.registerPluginConfigGroups(pluginInfos); + libConfig.finalizeConfigs(); + + let controlPlugins = new Map(); + for (let pluginInfo of pluginInfos) { + if (!pluginInfo.controlEntrypoint) { + continue; + } + + let ControlPluginClass = await libPluginLoader.loadControlPluginClass(pluginInfo); + let controlPlugin = new ControlPluginClass(pluginInfo, logger); + controlPlugins.set(pluginInfo.name, controlPlugin); + await controlPlugin.init(); + } + return controlPlugins; +} + +async function registerCommands(controlPlugins, yargs) { + const rootCommands = new libCommand.CommandTree({ name: "clusterioctl", description: "Manage cluster" }); + rootCommands.add(slaveCommands); + rootCommands.add(instanceCommands); + rootCommands.add(permissionCommands); + rootCommands.add(roleCommands); + rootCommands.add(userCommands); + rootCommands.add(logCommands); + rootCommands.add(debugCommands); + + for (let controlPlugin of controlPlugins.values()) { + await controlPlugin.addCommands(rootCommands); + } + + for (let [name, command] of rootCommands.subCommands) { + if (name === command.name) { + command.register(yargs); + } + } + + return rootCommands; +} async function startControl() { yargs @@ -684,39 +726,11 @@ async function startControl() { return; } + logger.verbose("Loading Plugins"); + let controlPlugins = await loadPlugins(pluginList); + // Add all cluster management commands including ones from plugins - const rootCommands = new libCommand.CommandTree({ name: "clusterioctl", description: "Manage cluster" }); - rootCommands.add(slaveCommands); - rootCommands.add(instanceCommands); - rootCommands.add(permissionCommands); - rootCommands.add(roleCommands); - rootCommands.add(userCommands); - rootCommands.add(logCommands); - rootCommands.add(debugCommands); - - logger.verbose("Loading Plugin info"); - let pluginInfos = await libPluginLoader.loadPluginInfos(pluginList); - libConfig.registerPluginConfigGroups(pluginInfos); - libConfig.finalizeConfigs(); - - let controlPlugins = new Map(); - for (let pluginInfo of pluginInfos) { - if (!pluginInfo.controlEntrypoint) { - continue; - } - - let { ControlPlugin } = require(path.posix.join(pluginInfo.requirePath, pluginInfo.controlEntrypoint)); - let controlPlugin = new ControlPlugin(pluginInfo, logger); - controlPlugins.set(pluginInfo.name, controlPlugin); - await controlPlugin.init(); - await controlPlugin.addCommands(rootCommands); - } - - for (let [name, command] of rootCommands.subCommands) { - if (name === command.name) { - command.register(yargs); - } - } + let rootCommands = await registerCommands(controlPlugins, yargs); // Reparse after commands have been added with help and strict checking. args = yargs @@ -780,7 +794,7 @@ async function startControl() { let control = new Control(controlConnector, controlPlugins); try { await controlConnector.connect(); - } catch(err) { + } catch (err) { if (err instanceof libErrors.AuthenticationFailed) { throw new libErrors.StartupError(err.message); } @@ -790,7 +804,9 @@ async function startControl() { process.on("SIGINT", () => { logger.info("Caught interrupt signal, closing connection"); control.shutdown().catch(err => { + setBlocking(true); logger.error(err.stack); + // eslint-disable-next-line no-process-exit process.exit(1); }); }); diff --git a/packages/lib/RateLimiter.js b/packages/lib/RateLimiter.js index 69adf335..9d19e770 100644 --- a/packages/lib/RateLimiter.js +++ b/packages/lib/RateLimiter.js @@ -34,7 +34,7 @@ class RateLimiter { * Callback invoked at maste `maxRate` per second on activation. */ constructor(options = {}) { - /* + /** * Maximum rate of activations per second. * @type {number} */ diff --git a/packages/lib/build_mod.js b/packages/lib/build_mod.js index 4ea89c51..13211900 100644 --- a/packages/lib/build_mod.js +++ b/packages/lib/build_mod.js @@ -8,64 +8,6 @@ const path = require("path"); const yargs = require("yargs"); -async function main() { - const args = yargs - .scriptName("build") - .options({ - "clean": { describe: "Remove previous builds", type: "boolean", default: false }, - "build": { describe: "Build mod(s)", type: "boolean", default: true }, - "pack": { describe: "Pack into zip file", type: "boolean", default: true }, - "source-dir": { describe: "Path to mod source files", nargs: 1, type: "string", demandOption: true }, - "output-dir": { describe: "Path to output built mod(s)", nargs: 1, type: "string", default: "dist" }, - "factorio-version": { describe: "Override factorio_version", type: "string" }, - }) - .strict() - .argv - ; - - await build(args); -} - -async function build(args) { - let info = JSON.parse(await fs.readFile(path.join(args.sourceDir, "info.json"))); - - if (args.factorioVersion) { - info.factorio_version = args.factorioVersion; - } - - if (args.dependencies) { - info.dependencies = args.dependencies; - } - - if (args.clean) { - let splitter = /^(.*)_(\d+\.\d+\.\d+)(\.zip)?$/; - for (let entry of await fs.readdir(args.outputDir)) { - let match = splitter.exec(entry); - if (match) { - let [, name, version] = match; - if (name === info.name) { - let modPath = path.join(args.outputDir, entry); - console.log(`Removing ${modPath}`); - await fs.remove(modPath); - } - } - } - } - - if (info.variants) { - for (let [variant, variantOverrides] of Object.entries(info.variants)) { - let variantInfo = { - ...info, - ...variantOverrides, - }; - delete variantInfo.variants; - await buildMod(args, variantInfo); - } - } else { - await buildMod(args, info); - } -} - async function buildMod(args, info) { if (args.build) { await fs.ensureDir(args.outputDir); @@ -121,6 +63,65 @@ async function buildMod(args, info) { } } +async function build(args) { + let info = JSON.parse(await fs.readFile(path.join(args.sourceDir, "info.json"))); + + if (args.factorioVersion) { + info.factorio_version = args.factorioVersion; + } + + if (args.dependencies) { + info.dependencies = args.dependencies; + } + + if (args.clean) { + let splitter = /^(.*)_(\d+\.\d+\.\d+)(\.zip)?$/; + for (let entry of await fs.readdir(args.outputDir)) { + let match = splitter.exec(entry); + if (match) { + let [, name, version] = match; + if (name === info.name) { + let modPath = path.join(args.outputDir, entry); + console.log(`Removing ${modPath}`); + await fs.remove(modPath); + } + } + } + } + + if (info.variants) { + for (let [variant, variantOverrides] of Object.entries(info.variants)) { + let variantInfo = { + ...info, + ...variantOverrides, + }; + delete variantInfo.variants; + await buildMod(args, variantInfo); + } + } else { + await buildMod(args, info); + } +} + + +async function main() { + const args = yargs + .scriptName("build") + .options({ + "clean": { describe: "Remove previous builds", type: "boolean", default: false }, + "build": { describe: "Build mod(s)", type: "boolean", default: true }, + "pack": { describe: "Pack into zip file", type: "boolean", default: true }, + "source-dir": { describe: "Path to mod source files", nargs: 1, type: "string", demandOption: true }, + "output-dir": { describe: "Path to output built mod(s)", nargs: 1, type: "string", default: "dist" }, + "factorio-version": { describe: "Override factorio_version", type: "string" }, + }) + .strict() + .argv + ; + + await build(args); +} + if (module === require.main) { main().catch(err => { console.log(err); }); } diff --git a/packages/lib/config/classes.js b/packages/lib/config/classes.js index b9c56d98..38a30e19 100644 --- a/packages/lib/config/classes.js +++ b/packages/lib/config/classes.js @@ -386,8 +386,11 @@ class ConfigGroup { if (basicType(value) === "string") { if (def.type === "boolean") { - if (value === "true") { value = true; } - else if (value === "false") { value = false; } + if (value === "true") { + value = true; + } else if (value === "false") { + value = false; + } } else if (def.type === "number") { let numberRegExp = /^[+-]?(Infinity|\d+\.(\d+)?([eE][+-]?\d+)?|\.?\d+([eE][+-]?\d+)?)$/; diff --git a/packages/lib/config/index.js b/packages/lib/config/index.js index fa9b93b2..f95014ff 100644 --- a/packages/lib/config/index.js +++ b/packages/lib/config/index.js @@ -1,3 +1,4 @@ +/* eslint-disable global-require */ /** * Configuration System * @author Hornwitser diff --git a/packages/lib/database.js b/packages/lib/database.js index b6bd5646..7a727bd2 100644 --- a/packages/lib/database.js +++ b/packages/lib/database.js @@ -59,9 +59,8 @@ async function loadJsonAsMap(filePath) { // File does not exist, return empty map return new Map(); - } else { - throw err; } + throw err; } } @@ -121,9 +120,8 @@ async function loadJsonArrayAsMap(filePath) { // File does not exist, return empty map return new Map(); - } else { - throw err; } + throw err; } } diff --git a/packages/lib/factorio/export.js b/packages/lib/factorio/export.js index cd101a2e..165f011a 100644 --- a/packages/lib/factorio/export.js +++ b/packages/lib/factorio/export.js @@ -99,6 +99,105 @@ async function loadFile(server, modVersions, modPath) { return await file.async("nodebuffer"); } +async function loadIcon(server, modVersions, iconPath, iconSize, iconMipmaps, iconCache) { + let icon = iconCache.get(iconPath); + if (icon === undefined) { + let fileContent = await loadFile(server, modVersions, iconPath); + if (fileContent) { + icon = await Jimp.read(fileContent); + icon.crop(0, 0, iconSize, iconSize); + iconCache.set(iconPath, icon); + } else { + icon = null; + server._logger.warn(`${iconPath} not found`); + } + iconCache.set(iconPath, icon); + } + return icon; +} + +async function loadSimpleIcon(server, modVersions, item, size, iconCache) { + let icon = await loadIcon(server, modVersions, item.icon, item.icon_size, 0, iconCache); + if (icon) { + let iconScale = size / item.icon_size; + if (iconScale !== 1) { + icon = icon.clone(); + icon.scale(iconScale); + } + } + return icon; +} + +async function loadLayeredIcon(server, modVersions, item, size, iconCache) { + let baseLayerSize = item.icons[0].icon_size || item.icon_size; + let icon = await Jimp.create(size, size); + + // The scaling factor of the base layer + let baseLayerScale = item.icons[0].scale || 32 / baseLayerSize; + + // The size in pixels of one unit + let baseUnit = size / (baseLayerSize * baseLayerScale); + + for (let layer of item.icons) { + let layerSize = layer.icon_size || item.icon_size; + let iconLayer = await loadIcon(server, modVersions, layer.icon, layerSize, layer.icon_mipmaps || 0, iconCache); + + if (!iconLayer) { + continue; + } + + iconLayer = iconLayer.clone(); + + let tint; + if (layer.tint) { + let divisor = (layer.tint.r > 1 || layer.tint.g > 1 || layer.tint.b > 1) ? 255 : 1; + tint = { + r: (layer.tint.r || 0) / divisor, + g: (layer.tint.g || 0) / divisor, + b: (layer.tint.b || 0) / divisor, + a: layer.tint.a || 1, + }; + } else { + tint = { r: 1, b: 1, g: 1, a: 1 }; + } + + let layerScale = layer.scale || 32 / layerSize; + let [xs, ys] = layer.shift || [0, 0]; + let realScale = layerScale * baseUnit; + let sizeShift = (size - layerSize * realScale) / 2; + + xs = xs * baseUnit + sizeShift; + ys = ys * baseUnit + sizeShift; + + if (realScale !== 1) { + iconLayer.scale(realScale); + } + + // eslint-disable-next-line no-loop-func + iconLayer.scan(0, 0, iconLayer.bitmap.width, iconLayer.bitmap.height, (x, y, sidx) => { + x += xs; + y += ys; + if (x < 0 || x >= size || y < 0 || y >= size) { + return; + } + let sdata = iconLayer.bitmap.data; + let ddata = icon.bitmap.data; + let didx = icon.getPixelIndex(x, y); + + let sa = sdata[sidx + 3] / 255; + let da = ddata[didx + 3] / 255; + let db = da * (1 - sa * tint.a); + let cb = sa + da * (1 - sa); + ddata[didx + 0] = Math.min(255, (sdata[sidx + 0] * sa * tint.r + ddata[didx + 0] * db) / cb); + ddata[didx + 1] = Math.min(255, (sdata[sidx + 1] * sa * tint.g + ddata[didx + 1] * db) / cb); + ddata[didx + 2] = Math.min(255, (sdata[sidx + 2] * sa * tint.b + ddata[didx + 2] * db) / cb); + ddata[didx + 3] = Math.min(255, sdata[sidx + 3] + ddata[didx + 3] * (1 - sa)); + }); + } + + return icon; +} + /** * Export item icons and data * @@ -130,107 +229,21 @@ async function exportItems(server, modVersions, items) { let pos = 0; let iconCache = new Map(); - async function loadIcon(iconPath, iconSize, iconMipmaps) { - let icon = iconCache.get(iconPath); - if (icon === undefined) { - let fileContent = await loadFile(server, modVersions, iconPath); - if (fileContent) { - icon = await Jimp.read(fileContent); - icon.crop(0, 0, iconSize, iconSize); - iconCache.set(iconPath, icon); - } else { - icon = null; - server._logger.warn(`${iconPath} not found`); - } - iconCache.set(iconPath, icon); - } - return icon; - } - let simpleIcons = new Map(); for (let item of items) { let icon; let iconPos; if (item.icons) { - let baseLayerSize = item.icons[0].icon_size || item.icon_size; - icon = await Jimp.create(size, size); + icon = await loadLayeredIcon(server, modVersions, item, size, iconCache); iconPos = pos; - // The scaling factor of the base layer - let baseLayerScale = item.icons[0].scale || 32 / baseLayerSize; - - // The size in pixels of one unit - let baseUnit = size / (baseLayerSize * baseLayerScale); - - for (let layer of item.icons) { - let layerSize = layer.icon_size || item.icon_size; - let iconLayer = await loadIcon(layer.icon, layerSize, layer.icon_mipmaps || 0); - - if (!iconLayer) { - continue; - } - - iconLayer = iconLayer.clone(); - - let tint; - if (layer.tint) { - let divisor = (layer.tint.r > 1 || layer.tint.g > 1 || layer.tint.b > 1) ? 255 : 1; - tint = { - r: (layer.tint.r || 0) / divisor, - g: (layer.tint.g || 0) / divisor, - b: (layer.tint.b || 0) / divisor, - a: layer.tint.a || 1, - }; - } else { - tint = { r: 1, b: 1, g: 1, a: 1 }; - } - - let layerScale = layer.scale || 32 / layerSize; - let [xs, ys] = layer.shift || [0, 0]; - let realScale = layerScale * baseUnit; - let sizeShift = (size - layerSize * realScale) / 2; - - xs = xs * baseUnit + sizeShift; - ys = ys * baseUnit + sizeShift; - - if (realScale !== 1) { - iconLayer.scale(realScale); - } - - // eslint-disable-next-line no-loop-func - iconLayer.scan(0, 0, iconLayer.bitmap.width, iconLayer.bitmap.height, function(x, y, sidx) { - x += xs; - y += ys; - if (x < 0 || x >= size || y < 0 || y >= size) { - return; - } - let sdata = this.bitmap.data; - let ddata = icon.bitmap.data; - let didx = icon.getPixelIndex(x, y); - - let sa = sdata[sidx + 3] / 255; - let da = ddata[didx + 3] / 255; - let db = da * (1 - sa * tint.a); - let cb = sa + da * (1 - sa); - ddata[didx + 0] = Math.min(255, (sdata[sidx + 0] * sa * tint.r + ddata[didx + 0] * db) / cb); - ddata[didx + 1] = Math.min(255, (sdata[sidx + 1] * sa * tint.g + ddata[didx + 1] * db) / cb); - ddata[didx + 2] = Math.min(255, (sdata[sidx + 2] * sa * tint.b + ddata[didx + 2] * db) / cb); - ddata[didx + 3] = Math.min(255, sdata[sidx + 3] + ddata[didx + 3] * (1 - sa)); - }); - } - } else { iconPos = simpleIcons.get(item.icon); if (iconPos === undefined) { - icon = await loadIcon(item.icon, item.icon_size, 0); + icon = await loadSimpleIcon(server, modVersions, item, size, iconCache); if (icon) { iconPos = pos; simpleIcons.set(item.icon, pos); - let iconScale = size / item.icon_size; - if (iconScale !== 1) { - icon = icon.clone(); - icon.scale(iconScale); - } } } } diff --git a/packages/lib/factorio/index.js b/packages/lib/factorio/index.js index 8624b8c4..8fad79b5 100644 --- a/packages/lib/factorio/index.js +++ b/packages/lib/factorio/index.js @@ -1,3 +1,4 @@ +/* eslint-disable global-require */ /** * Library for interfacing with Factorio Servers and saves. * @module lib/factorio diff --git a/packages/lib/factorio/patch.js b/packages/lib/factorio/patch.js index 6b70b1d2..e0322bfe 100644 --- a/packages/lib/factorio/patch.js +++ b/packages/lib/factorio/patch.js @@ -78,7 +78,7 @@ function generateLoader(patchInfo) { ]; for (let moduleName of patchInfo["scenario"]["modules"]) { - lines.push('event_handler.add_lib(require("'+moduleName+'"))'); + lines.push(`event_handler.add_lib(require("${moduleName}"))`); } lines.push(...[ @@ -90,10 +90,10 @@ function generateLoader(patchInfo) { for (let file of module["files"]) { let requirePath = `modules/${module.name}/${file["path"].slice(0, -4)}`; if (file["load"]) { - lines.push('event_handler.add_lib(require("'+requirePath+'"))'); + lines.push(`event_handler.add_lib(require("${requirePath}"))`); } if (file["require"]) { - lines.push('require("'+requirePath+'")'); + lines.push(`require("${requirePath}")`); } } } diff --git a/packages/lib/factorio/server.js b/packages/lib/factorio/server.js index d9de5ca8..d2c764a6 100644 --- a/packages/lib/factorio/server.js +++ b/packages/lib/factorio/server.js @@ -578,12 +578,12 @@ class FactorioServer extends events.EventEmitter { // Track autosaving this._runningAutosave = null; - this.on("_autosave", function(name) { + this.on("_autosave", name => { this.emit("autosave-start", name); this._runningAutosave = name; }); - this.on("_saved", function() { + this.on("_saved", () => { if (this._runningAutosave) { this.emit("autosave-finished", this._runningAutosave); this._runningAutosave = null; @@ -798,8 +798,8 @@ class FactorioServer extends events.EventEmitter { } else { this.emit("error", new Error( - "Factorio server unexpectedly shut down. Possible causes:\n- "+ - this._unexpected.join("\n- ") + "Factorio server unexpectedly shut down. Possible causes:"+ + `\n- ${this._unexpected.join("\n- ")}` )); } } diff --git a/packages/lib/hash.js b/packages/lib/hash.js index 5aa9d1ab..df26114e 100644 --- a/packages/lib/hash.js +++ b/packages/lib/hash.js @@ -14,14 +14,14 @@ const fs = require("fs"); * @returns {Promise} hash of the stream. */ function hashStream(stream) { - return new Promise(function(resolve, reject) { + return new Promise((resolve, reject) => { let hasher = crypto.createHash("sha1"); hasher.setEncoding("hex"); - hasher.on("finish", function() { + hasher.on("finish", () => { resolve(hasher.read()); }); - stream.on("error", function(error) { + stream.on("error", (error) => { // the docs doesn't say anything about what's passed on error reject(error); }); diff --git a/packages/lib/is_deep_strict_equal.js b/packages/lib/is_deep_strict_equal.js index 2a66f443..3dae54dc 100644 --- a/packages/lib/is_deep_strict_equal.js +++ b/packages/lib/is_deep_strict_equal.js @@ -1,3 +1,4 @@ +/* eslint-disable global-require */ "use strict"; // eslint-disable-next-line no-process-env if (process.env.APP_ENV === "browser") { diff --git a/packages/lib/link/index.js b/packages/lib/link/index.js index 28f7aa3a..7a49b181 100644 --- a/packages/lib/link/index.js +++ b/packages/lib/link/index.js @@ -1,3 +1,4 @@ +/* eslint-disable global-require */ /** * Library for message based communication between nodes * @module lib/link diff --git a/packages/lib/link/link.js b/packages/lib/link/link.js index c2f37953..a13f22c8 100644 --- a/packages/lib/link/link.js +++ b/packages/lib/link/link.js @@ -124,9 +124,8 @@ class Link { if (handler) { handler(message); return true; - } else { - return false; } + return false; } /** diff --git a/packages/lib/link/messages.js b/packages/lib/link/messages.js index 4e558ab6..283a395f 100644 --- a/packages/lib/link/messages.js +++ b/packages/lib/link/messages.js @@ -88,8 +88,8 @@ class Request extends Message { this.forwardTo = forwardTo; this.handlerSuffix = "RequestHandler"; - this.requestType = type + "_request"; - this.responseType = type + "_response"; + this.requestType = `${type}_request`; + this.responseType = `${type}_response`; if (permission === undefined && links.includes("control-master")) { throw new Error(`permission is required for ${this.type} request over control-master links`); @@ -195,8 +195,11 @@ class Request extends Message { if (this.permission !== null && `${link.source}-${link.target}` === "master-control") { let origHandler = handler; handler = async function(message, request) { - this.user.checkPermission(request.permission); - return await origHandler.call(this, message, request); + // Abuse this binding to get a hold of the ControlConnection instance + // eslint-disable-next-line no-invalid-this, consistent-this + let controlConnection = this; + controlConnection.user.checkPermission(request.permission); + return await origHandler.call(controlConnection, message, request); }; } @@ -783,7 +786,7 @@ class Event extends Message { throw new Error(`Invalid broadcastTo value ${broadcastTo}`); } - this.eventType = type + "_event"; + this.eventType = `${type}_event`; this._eventValidator = libSchema.compile({ $schema: "http://json-schema.org/draft-07/schema#", properties: { diff --git a/packages/lib/logging_utils.js b/packages/lib/logging_utils.js index 990b2597..9c0284a3 100644 --- a/packages/lib/logging_utils.js +++ b/packages/lib/logging_utils.js @@ -21,9 +21,9 @@ const libErrors = require("@clusterio/lib/errors"); function formatServerOutput(parsed) { let time = ""; if (parsed.format === "seconds") { - time = chalk.yellow(parsed.time.padStart(8)) + " "; + time = `${chalk.yellow(parsed.time.padStart(8))} `; } else if (parsed.format === "date") { - time = chalk.yellow(parsed.time) + " "; + time = `${chalk.yellow(parsed.time)} `; } let info = ""; @@ -41,10 +41,10 @@ function formatServerOutput(parsed) { level = chalk.bold.redBright(level); } - info = level + " " + chalk.gray(parsed.file) + ": "; + info = `${level} ${chalk.gray(parsed.file)}: `; } else if (parsed.type === "action") { - info = "[" + chalk.yellow(parsed.action) + "] "; + info = `[${chalk.yellow(parsed.action)}] `; } return time + info + parsed.message; diff --git a/packages/lib/package.json b/packages/lib/package.json index 04d21721..516e37ed 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -22,6 +22,7 @@ "node-forge": "^0.10.0", "rcon-client": "^4.2.0", "semver": "^7.1.1", + "set-blocking": "^2.0.0", "triple-beam": "^1.3.0", "winston": "^3.3.3", "winston-transport": "^4.4.0", diff --git a/packages/lib/plugin.js b/packages/lib/plugin.js index 333e2379..08795c84 100644 --- a/packages/lib/plugin.js +++ b/packages/lib/plugin.js @@ -439,9 +439,7 @@ function attachPluginMessages(link, pluginInfo, plugin) { messageFormat.attach(link); } else { - messageFormat.attach(link, async function(message, format) { - return await plugin[handler](message, format, this); - }); + messageFormat.attach(link, async (message, format) => await plugin[handler](message, format)); } } catch (err) { diff --git a/packages/lib/plugin_loader.js b/packages/lib/plugin_loader.js index 8307eef4..58cacda7 100644 --- a/packages/lib/plugin_loader.js +++ b/packages/lib/plugin_loader.js @@ -1,3 +1,4 @@ +/* eslint-disable global-require */ /** * Helpers for loading plugins in Node.js * @module lib/plugin_loader @@ -47,6 +48,51 @@ async function loadPluginInfos(pluginList) { return plugins; } +/** + * Load master plugin class of a plugin + * + * @param {Object} pluginInfo - + * Plugin info object returned from {@link + * module:lib/plugin_loader.loadPluginInfos} to load class from. + * @returns {function} plugin class + * @static + */ +async function loadMasterPluginClass(pluginInfo) { + let entrypoint = require(path.posix.join(pluginInfo.requirePath, pluginInfo.masterEntrypoint)); + return entrypoint.MasterPlugin; +} + +/** + * Load instance plugin class of a plugin + * + * @param {Object} pluginInfo - + * Plugin info object returned from {@link + * module:lib/plugin_loader.loadPluginInfos} to load class from. + * @returns {function} plugin class + * @static + */ +async function loadInstancePluginClass(pluginInfo) { + let entrypoint = require(path.posix.join(pluginInfo.requirePath, pluginInfo.instanceEntrypoint)); + return entrypoint.InstancePlugin; +} + +/** + * Load control plugin class of a plugin + * + * @param {Object} pluginInfo - + * Plugin info object returned from {@link + * module:lib/plugin_loader.loadPluginInfos} to load class from. + * @returns {function} plugin class + * @static + */ +async function loadControlPluginClass(pluginInfo) { + let entrypoint = require(path.posix.join(pluginInfo.requirePath, pluginInfo.controlEntrypoint)); + return entrypoint.ControlPlugin; +} + module.exports = { loadPluginInfos, + loadMasterPluginClass, + loadInstancePluginClass, + loadControlPluginClass, }; diff --git a/packages/lib/schema.js b/packages/lib/schema.js index 4e781812..7245a7fd 100644 --- a/packages/lib/schema.js +++ b/packages/lib/schema.js @@ -3,7 +3,8 @@ * @module lib/schema */ "use strict"; -const ajv = new (require("ajv"))({ +const Ajv = require("ajv"); +const ajv = new Ajv({ verbose: true, format: "full", extendRefs: "fail", diff --git a/packages/lib/shared_commands.js b/packages/lib/shared_commands.js index 68df288d..5b4f39a3 100644 --- a/packages/lib/shared_commands.js +++ b/packages/lib/shared_commands.js @@ -54,6 +54,7 @@ async function handlePluginCommand(args, pluginList, pluginListPath) { let pluginInfo; try { + // eslint-disable-next-line global-require pluginInfo = require(path.posix.join(pluginPath, "info")); } catch (err) { logger.error(`Unable to import plugin info from ${args.path}:\n${err.stack}`); diff --git a/packages/master/master.js b/packages/master/master.js index 611a6231..eff99e7c 100755 --- a/packages/master/master.js +++ b/packages/master/master.js @@ -31,6 +31,8 @@ const JSZip = require("jszip"); const version = require("./package").version; const util = require("util"); const winston = require("winston"); +const http = require("http"); +const https = require("https"); // ugly globals let masterConfig; @@ -46,6 +48,10 @@ let db = { instances: new Map(), slaves: new Map(), }; +let slaveConnections = new Map(); +let controlConnections = []; +let activeConnectors = new Map(); +let pendingSockets = new Set(); // homebrew modules const HttpCloser = require("./src/HttpCloser"); @@ -179,6 +185,22 @@ const wsActiveSlavesGauge = new libPrometheus.Gauge( "How many slaves are currently connected to the master" ); +// Merges samples from sourceResult to destinationResult +function mergeSamples(destinationResult, sourceResult) { + // Merge metrics received by multiple slaves + let receivedSamples = new Map(sourceResult.samples); + for (let [suffix, suffixSamples] of destinationResult.samples) { + if (receivedSamples.has(suffix)) { + suffixSamples.push(...receivedSamples.get(suffix)); + receivedSamples.delete(suffix); + } + } + + for (let entry of receivedSamples) { + sourceResult.samples.push(entry); + } +} + // Prometheus polling endpoint async function getMetrics(req, res, next) { endpointHitCounter.labels(req.route.path).inc(); @@ -214,17 +236,7 @@ async function getMetrics(req, res, next) { } else { // Merge metrics received by multiple slaves - let receivedSamples = new Map(result.samples); - for (let [suffix, suffixSamples] of resultMap.get(result.metric.name).samples) { - if (receivedSamples.has(suffix)) { - suffixSamples.push(...receivedSamples.get(suffix)); - receivedSamples.delete(suffix); - } - } - - for (let entry of receivedSamples) { - result.samples.push(entry); - } + mergeSamples(resultMap.get(result.metric.name), result); } } } @@ -322,6 +334,23 @@ const masterConnectedClientsCount = new libPrometheus.Gauge( }, ); +/** + * Returns the URL needed to connect to the master server. + * + * @returns {string} master URL. + */ +function getMasterUrl() { + let url = masterConfig.get("master.external_address"); + if (!url) { + if (masterConfig.get("master.https_port")) { + url = `https://localhost:${masterConfig.get("master.https_port")}/`; + } else { + url = `http://localhost:${masterConfig.get("master.http_port")}/`; + } + } + return url; +} + /** * Load Map from JSON file in the database directory. * @@ -504,7 +533,7 @@ async function shutdown() { logger.info(`Clusterio cleanly exited in ${Date.now() - exitStartTime}ms`); - } catch(err) { + } catch (err) { setBlocking(true); logger.fatal(` +--------------------------------------------------------------------+ @@ -513,6 +542,7 @@ async function shutdown() { +--------------------------------------------------------------------+ ${err.stack}` ); + // eslint-disable-next-line no-process-exit process.exit(1); } } @@ -606,7 +636,6 @@ const lastQueryLogTime = new libPrometheus.Gauge( "Time in seconds the last log query took to execute." ); -let controlConnections = []; class ControlConnection extends BaseConnection { constructor(registerData, connector, user) { super("control", connector); @@ -1078,7 +1107,6 @@ class ControlConnection extends BaseConnection { } } -let slaveConnections = new Map(); /** * Represents the connection to a slave * @@ -1446,67 +1474,6 @@ class WebSocketServerConnector extends libLink.WebSocketBaseConnector { } } -let pendingSockets = new Set(); -let activeConnectors = new Map(); - -wss.on("connection", function (socket, req) { - logger.verbose(`SOCKET | new connection from ${req.socket.remoteAddress}`); - - // Track statistics - wsConnectionsCounter.inc(); - socket.on("message", (message) => { - wsMessageCounter.labels("in").inc(); - if (!socket.clusterio_ignore_dump) { - debugEvents.emit("message", { direction: "in", content: message }); - } - }); - let originalSend = socket.send; - socket.send = (...args) => { - wsMessageCounter.labels("out").inc(); - if (typeof args[0] === "string" && !socket.clusterio_ignore_dump) { - debugEvents.emit("message", { direction: "out", content: args[0] }); - } - return originalSend.call(socket, ...args); - }; - - // Start connection handshake. - socket.send(JSON.stringify({ seq: null, type: "hello", data: { - version, - plugins: loadedPlugins, - }})); - - function attachHandler() { - pendingSockets.add(socket); - - let timeoutId = setTimeout(() => { - logger.verbose(`SOCKET | closing ${req.socket.remoteAddress} after timing out on handshake`); - wsRejectedConnectionsCounter.inc(); - socket.close(1008, "Handshake timeout"); - pendingSockets.delete(socket); - }, 30*1000); - - socket.once("message", (message) => { - clearTimeout(timeoutId); - pendingSockets.delete(socket); - handleHandshake( - message, socket, req, attachHandler - ).catch(err => { - logger.error(` -+----------------------------------------------------------------+ -| Unexpected error occured in WebSocket handshake, please report | -| it to https://github.com/clusterio/factorioClusterio/issues | -+----------------------------------------------------------------+ -${err.stack}` - ); - wsRejectedConnectionsCounter.inc(); - socket.close(1011, "Unexpected error"); - }); - }); - } - - attachHandler(); -}); - let nextSessionId = 1; // Unique string for the session token audience @@ -1546,7 +1513,7 @@ async function handleHandshake(message, socket, req, attachHandler) { throw new Error(); } - } catch(err) { + } catch (err) { socket.send(JSON.stringify({ seq: null, type: "invalidate", data: {}})); attachHandler(); return; @@ -1626,12 +1593,63 @@ async function handleHandshake(message, socket, req, attachHandler) { connector.ready(sessionToken); } -// handle plugins on the master -async function pluginManagement() { - let startPluginLoad = Date.now(); - masterPlugins = await loadPlugins(); - logger.info(`All plugins loaded in ${Date.now() - startPluginLoad}ms`); -} +wss.on("connection", (socket, req) => { + logger.verbose(`SOCKET | new connection from ${req.socket.remoteAddress}`); + + // Track statistics + wsConnectionsCounter.inc(); + socket.on("message", (message) => { + wsMessageCounter.labels("in").inc(); + if (!socket.clusterio_ignore_dump) { + debugEvents.emit("message", { direction: "in", content: message }); + } + }); + let originalSend = socket.send; + socket.send = (...args) => { + wsMessageCounter.labels("out").inc(); + if (typeof args[0] === "string" && !socket.clusterio_ignore_dump) { + debugEvents.emit("message", { direction: "out", content: args[0] }); + } + return originalSend.call(socket, ...args); + }; + + // Start connection handshake. + socket.send(JSON.stringify({ seq: null, type: "hello", data: { + version, + plugins: loadedPlugins, + }})); + + function attachHandler() { + pendingSockets.add(socket); + + let timeoutId = setTimeout(() => { + logger.verbose(`SOCKET | closing ${req.socket.remoteAddress} after timing out on handshake`); + wsRejectedConnectionsCounter.inc(); + socket.close(1008, "Handshake timeout"); + pendingSockets.delete(socket); + }, 30*1000); + + socket.once("message", (message) => { + clearTimeout(timeoutId); + pendingSockets.delete(socket); + handleHandshake( + message, socket, req, attachHandler + ).catch(err => { + logger.error(` ++----------------------------------------------------------------+ +| Unexpected error occured in WebSocket handshake, please report | +| it to https://github.com/clusterio/factorioClusterio/issues | ++----------------------------------------------------------------+ +${err.stack}` + ); + wsRejectedConnectionsCounter.inc(); + socket.close(1011, "Unexpected error"); + }); + }); + } + + attachHandler(); +}); async function loadPlugins() { let plugins = new Map(); @@ -1643,13 +1661,13 @@ async function loadPlugins() { loadedPlugins[pluginInfo.name] = pluginInfo.version; let pluginLoadStarted = Date.now(); - let MasterPlugin = libPlugin.BaseMasterPlugin; + let MasterPluginClass = libPlugin.BaseMasterPlugin; try { if (pluginInfo.masterEntrypoint) { - ({ MasterPlugin } = require(path.posix.join(pluginInfo.requirePath, pluginInfo.masterEntrypoint))); + MasterPluginClass = await libPluginLoader.loadMasterPluginClass(pluginInfo); } - let masterPlugin = new MasterPlugin( + let masterPlugin = new MasterPluginClass( pluginInfo, { app, config: masterConfig, db, slaveConnections }, { endpointHitCounter }, logger ); await masterPlugin.init(); @@ -1664,6 +1682,13 @@ async function loadPlugins() { return plugins; } +// handle plugins on the master +async function pluginManagement() { + let startPluginLoad = Date.now(); + masterPlugins = await loadPlugins(); + logger.info(`All plugins loaded in ${Date.now() - startPluginLoad}ms`); +} + /** * Calls listen on server capturing any errors that occurs * binding to the port. Also adds handler for WebSocket @@ -1703,28 +1728,61 @@ function _setConfig(config) { masterConfig = config; } -/** - * Returns the URL needed to connect to the master server. - * - * @returns {string} master URL. - */ -function getMasterUrl() { - let url = masterConfig.get("master.external_address"); - if (!url) { - if (masterConfig.get("master.https_port")) { - url = `https://localhost:${masterConfig.get("master.https_port")}/`; +async function handleBootstrapCommand(args) { + let subCommand = args._[1]; + await loadUsers(masterConfig.get("master.database_directory"), "users.json"); + if (subCommand === "create-admin") { + if (!args.name) { + logger.error("name cannot be blank"); + process.exitCode = 1; + return; + } + + let admin = db.users.get(args.name); + if (!admin) { + admin = createUser(args.name); + } + + let adminRole = libUsers.ensureDefaultAdminRole(db.roles); + admin.roles.add(adminRole); + admin.isAdmin = true; + await saveUsers(masterConfig.get("master.database_directory"), "users.json"); + + } else if (subCommand === "generate-user-token") { + let user = db.users.get(args.name); + if (!user) { + logger.error(`No user named '${args.name}'`); + process.exitCode = 1; + return; + } + // eslint-disable-next-line no-console + console.log(user.createToken(masterConfig.get("master.auth_secret"))); + + } else if (subCommand === "create-ctl-config") { + let admin = db.users.get(args.name); + if (!admin) { + logger.error(`No user named '${args.name}'`); + process.exitCode = 1; + return; + } + let controlConfig = new libConfig.ControlConfig(); + await controlConfig.init(); + + controlConfig.set("control.master_url", getMasterUrl()); + controlConfig.set("control.master_token", admin.createToken(masterConfig.get("master.auth_secret"))); + + let content = JSON.stringify(controlConfig.serialize(), null, 4); + if (args.output === "-") { + // eslint-disable-next-line no-console + console.log(content); } else { - url = `http://localhost:${masterConfig.get("master.http_port")}/`; + logger.info(`Writing ${args.output}`); + await fs.outputFile(args.output, content); } } - return url; } -async function startServer() { - // 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 = "clusterioMaster"; - +async function initialize() { // argument parsing let args = yargs .scriptName("master") @@ -1795,9 +1853,6 @@ async function startServer() { })); } - // add better stack traces on promise rejection - process.on("unhandledRejection", err => logger.error(`Unhandled rejection:\n${err.stack}`)); - logger.info(`Loading available plugins from ${args.pluginList}`); let pluginList = new Map(); try { @@ -1812,7 +1867,7 @@ async function startServer() { let command = args._[0]; if (command === "plugin") { await libSharedCommands.handlePluginCommand(args, pluginList, args.pluginList); - return; + return [false, args]; } logger.info("Loading Plugin info"); @@ -1846,72 +1901,26 @@ async function startServer() { if (command === "config") { await libSharedCommands.handleConfigCommand(args, masterConfig, masterConfigPath); - return; + return [false, args]; } else if (command === "bootstrap") { - let subCommand = args._[1]; - await loadUsers(masterConfig.get("master.database_directory"), "users.json"); - if (subCommand === "create-admin") { - if (!args.name) { - logger.error("name cannot be blank"); - process.exitCode = 1; - return; - } - - let admin = db.users.get(args.name); - if (!admin) { - admin = createUser(args.name); - } - - let adminRole = libUsers.ensureDefaultAdminRole(db.roles); - admin.roles.add(adminRole); - admin.isAdmin = true; - await saveUsers(masterConfig.get("master.database_directory"), "users.json"); - - } else if (subCommand === "generate-user-token") { - let user = db.users.get(args.name); - if (!user) { - logger.error(`No user named '${args.name}'`); - process.exitCode = 1; - return; - } - // eslint-disable-next-line no-console - console.log(user.createToken(masterConfig.get("master.auth_secret"))); - - } else if (subCommand === "create-ctl-config") { - let admin = db.users.get(args.name); - if (!admin) { - logger.error(`No user named '${args.name}'`); - process.exitCode = 1; - return; - } - let controlConfig = new libConfig.ControlConfig(); - await controlConfig.init(); - - controlConfig.set("control.master_url", getMasterUrl()); - controlConfig.set("control.master_token", admin.createToken(masterConfig.get("master.auth_secret"))); - - let content = JSON.stringify(controlConfig.serialize(), null, 4); - if (args.output === "-") { - // eslint-disable-next-line no-console - console.log(content); - } else { - logger.info(`Writing ${args.output}`); - await fs.outputFile(args.output, content); - } - } - - return; + await handleBootstrapCommand(args); + return [false, args]; } // If we get here the command was run + return [true, args]; +} +async function startServer(args) { // Start webpack development server if enabled if (args.dev) { logger.warn("Webpack development mode enabled"); + /* eslint-disable global-require */ const webpack = require("webpack"); const webpackDevMiddleware = require("webpack-dev-middleware"); const webpackConfig = require("./webpack.config"); + /* eslint-enable global-require */ const compiler = webpack(webpackConfig({})); devMiddleware = webpackDevMiddleware(compiler, {}); @@ -1922,6 +1931,7 @@ async function startServer() { process.on("SIGINT", () => { if (secondSigint) { logger.fatal("Caught second interrupt, terminating immediately"); + // eslint-disable-next-line no-process-exit process.exit(1); } @@ -1934,6 +1944,7 @@ async function startServer() { process.on("SIGHUP", () => { // No graceful cleanup, no warning out (stdout is likely closed.) // Don't close the terminal with the clusterio master in it. + // eslint-disable-next-line no-process-exit process.exit(1); }); @@ -1949,7 +1960,8 @@ async function startServer() { let bindAddress = masterConfig.get("master.bind_address") || ""; if (!httpPort && !httpsPort) { logger.fatal("Error: at least one of http_port and https_port must be configured"); - process.exit(1); + process.exitCode = 1; + return; } let tls_cert = masterConfig.get("master.tls_certificate"); @@ -1980,7 +1992,7 @@ async function startServer() { // Only start listening for connections after all plugins have loaded if (httpPort) { - httpServer = new (require("http").Server)(app); + httpServer = http.createServer(app); httpServerCloser = new HttpCloser(httpServer); await listen(httpServer, httpPort, bindAddress); logger.info(`Listening for HTTP on port ${httpServer.address().port}`); @@ -1998,7 +2010,7 @@ async function startServer() { ); } - httpsServer = require("https").createServer({ + httpsServer = https.createServer({ key: privateKey, cert: certificate, }, app); @@ -2008,6 +2020,20 @@ async function startServer() { } } +async function startup() { + // 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 = "clusterioMaster"; + + // add better stack traces on promise rejection + process.on("unhandledRejection", err => logger.error(`Unhandled rejection:\n${err.stack}`)); + + let [shouldRun, args] = await initialize(); + if (shouldRun) { + await startServer(args); + } +} + module.exports = { app, @@ -2030,7 +2056,7 @@ I version of clusterio. Expect things to break. I +==========================================================+ ` ); - startServer().catch(err => { + startup().catch(err => { if (err instanceof libErrors.StartupError) { logger.fatal(` +----------------------------------+ diff --git a/packages/slave/slave.js b/packages/slave/slave.js index bc4738a2..3dbef351 100755 --- a/packages/slave/slave.js +++ b/packages/slave/slave.js @@ -118,7 +118,7 @@ const serverSettingsActions = { /** * Keeps track of the runtime parameters of an instance */ -class Instance extends libLink.Link{ +class Instance extends libLink.Link { constructor(slave, connector, dir, factorioDir, instanceConfig) { super("instance", "slave", connector); libLink.attachAllMessages(this); @@ -250,8 +250,8 @@ class Instance extends libLink.Link{ async _loadPlugin(pluginInfo, slave) { let pluginLoadStarted = Date.now(); - let { InstancePlugin } = require(path.posix.join(pluginInfo.requirePath, pluginInfo.instanceEntrypoint)); - let instancePlugin = new InstancePlugin(pluginInfo, this, slave); + let InstancePluginClass = await libPluginLoader.loadInstancePluginClass(pluginInfo); + let instancePlugin = new InstancePluginClass(pluginInfo, this, slave); this.plugins.set(pluginInfo.name, instancePlugin); await instancePlugin.init(); libPlugin.attachPluginMessages(this, pluginInfo, instancePlugin); @@ -380,16 +380,19 @@ class Instance extends libLink.Link{ this.logger.verbose("Rotating old logs..."); // clean old log file to avoid crash - try{ + try { let logPath = this.path("factorio-current.log"); let stat = await fs.stat(logPath); - if(stat.isFile()){ + if (stat.isFile()) { let logFilename = `factorio-${Math.floor(Date.parse(stat.mtime)/1000)}.log`; await fs.rename(logPath, this.path(logFilename)); this.logger.verbose(`Log rotated as ${logFilename}`); } - }catch(e){} + } catch (err) { + this.logger.error(`Error rotating logs:\n${err.stack}`); + } + // eslint-disable-next-line no-use-before-define await symlinkMods(this, "sharedMods"); } @@ -698,6 +701,7 @@ class Instance extends libLink.Link{ await this.writeServerSettings(); this.logger.verbose("Creating save ....."); + // eslint-disable-next-line no-use-before-define await symlinkMods(this, "sharedMods"); } catch (err) { @@ -717,6 +721,7 @@ class Instance extends libLink.Link{ await this.writeServerSettings(); this.logger.info("Exporting data ....."); + // eslint-disable-next-line no-use-before-define await symlinkMods(this, "sharedMods"); let zip = await libFactorio.exportData(this.server); @@ -918,6 +923,46 @@ class SlaveConnector extends libLink.WebSocketClientConnector { } } +function checkFilename(name) { + // All of these are bad in Windows only, except for /, . and .. + // See: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + const badChars = /[<>:"\/\\|?*\x00-\x1f]/g; + const badEnd = /[. ]$/; + + const oneToNine = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + const badNames = [ + // Relative path components + ".", "..", + + // Reserved filenames in Windows + "CON", "PRN", "AUX", "NUL", + ...oneToNine.map(n => `COM${n}`), + ...oneToNine.map(n => `LPT${n}`), + ]; + + if (typeof name !== "string") { + throw new Error("must be a string"); + } + + if (name === "") { + throw new Error("cannot be empty"); + } + + if (badChars.test(name)) { + throw new Error('cannot contain <>:"\\/|=* or control characters'); + } + + if (badNames.includes(name.toUpperCase())) { + throw new Error( + "cannot be named any of . .. CON PRN AUX NUL COM1-9 and LPT1-9" + ); + } + + if (badEnd.test(name)) { + throw new Error("cannot end with . or space"); + } +} + /** * Handles running the slave * @@ -962,11 +1007,6 @@ class Slave extends libLink.Link { this.updateInstances().catch((err) => { logger.fatal(`Unexpected error updating instances:\n${err.stack}`); return this.shutdown(); - - }).catch((err) => { - setBlocking(true); - logger.fatal(`Unexpected error during shutdown:\n${err.stack}`); - process.exit(1); }); }); @@ -980,20 +1020,11 @@ class Slave extends libLink.Link { this.connector.connect().catch((err) => { logger.fatal(`Unexpected error reconnecting to master:\n${err.stack}`); return this.shutdown(); - - }).catch((err) => { - setBlocking(true); - logger.fatal(`Unexpected error during shutdown:\n${err.stack}`); - process.exit(1); }); } else { logger.fatal("Master connection was unexpectedly closed"); - this.shutdown().catch((err) => { - setBlocking(true); - logger.fatal(`Unexpected error during shutdown:\n${err.stack}`); - process.exit(1); - }); + this.shutdown(); } }); @@ -1364,53 +1395,25 @@ class Slave extends libLink.Link { } } - for (let instanceId of this.instanceConnections.keys()) { - await this.stopInstance(instanceId); + try { + for (let instanceId of this.instanceConnections.keys()) { + await this.stopInstance(instanceId); + } + await this.connector.close(1001, "Slave Shutdown"); + + // Clear silly interval in pidfile library. + pidusage.clear(); + } catch (err) { + logger.error(` ++--------------------------------------------------------------------+ +| Unexpected error occured while shutting down slave, please report | +| it to https://github.com/clusterio/factorioClusterio/issues | ++--------------------------------------------------------------------+ +${err.stack}` + ); + // eslint-disable-next-line no-process-exit + process.exit(1); } - await this.connector.close(1001, "Slave Shutdown"); - - // Clear silly interval in pidfile library. - pidusage.clear(); - } -} - -function checkFilename(name) { - // All of these are bad in Windows only, except for /, . and .. - // See: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file - const badChars = /[<>:"\/\\|?*\x00-\x1f]/g; - const badEnd = /[. ]$/; - - const oneToNine = [1, 2, 3, 4, 5, 6, 7, 8, 9]; - const badNames = [ - // Relative path components - ".", "..", - - // Reserved filenames in Windows - "CON", "PRN", "AUX", "NUL", - ...oneToNine.map(n => `COM${n}`), - ...oneToNine.map(n => `LPT${n}`), - ]; - - if (typeof name !== "string") { - throw new Error("must be a string"); - } - - if (name === "") { - throw new Error("cannot be empty"); - } - - if (badChars.test(name)) { - throw new Error('cannot contain <>:"\\/|=* or control characters'); - } - - if (badNames.includes(name.toUpperCase())) { - throw new Error( - "cannot be named any of . .. CON PRN AUX NUL COM1-9 and LPT1-9" - ); - } - - if (badEnd.test(name)) { - throw new Error("cannot end with . or space"); } } @@ -1453,6 +1456,7 @@ async function symlinkMods(instance, sharedMods) { let target = path.join(sharedMods, entry.name); let link = instance.path("mods", entry.name); + /* eslint-disable max-depth */ if (process.platform !== "win32") { await fs.symlink(path.relative(path.dirname(link), target), link); @@ -1464,6 +1468,7 @@ async function symlinkMods(instance, sharedMods) { } else { await fs.link(target, link); } + /* eslint-enable max-depth */ } } else { @@ -1610,22 +1615,15 @@ async function startSlave() { let secondSigint = false; process.on("SIGINT", () => { if (secondSigint) { + setBlocking(true); logger.fatal("Caught second interrupt, terminating immediately"); + // eslint-disable-next-line no-process-exit process.exit(1); } secondSigint = true; logger.info("Caught interrupt signal, shutting down"); - slave.shutdown().catch(err => { - logger.error(` -+---------------------------------------------------------------+ -| Unexpected error occured while stopping slave, please report | -| it to https://github.com/clusterio/factorioClusterio/issues | -+---------------------------------------------------------------+ -${err.stack}` - ); - process.exit(1); - }); + slave.shutdown(); }); slaveConnector.once("connect", () => { @@ -1675,6 +1673,6 @@ ${err.stack}` ); } - process.exit(1); + process.exitCode = 1; }); } diff --git a/packages/web_ui/src/components/data-table.jsx b/packages/web_ui/src/components/data-table.jsx index e9073d55..d3973ab3 100644 --- a/packages/web_ui/src/components/data-table.jsx +++ b/packages/web_ui/src/components/data-table.jsx @@ -16,12 +16,16 @@ const { Panel } = Collapse; * Editable - Boolean - Allow editing * AddRecord - Boolean - Allow adding new records * CustomDataFilterFunction - function(tableData) - Filter tabledata and return results - * RecordIdField - String - By default we assume the column "Id" is a unique identifier for a record. If that isn't the case, supply the column name here + * RecordIdField - String - + * By default we assume the column "Id" is a unique identifier for a record. If + * that isn't the case, supply the column name here */ class DataTable extends Component { constructor(props) { super(props); - if (!this.props.DataFunction) throw new Error("DataFunction not defined"); + if (!this.props.DataFunction) { + throw new Error("DataFunction not defined"); + } this.state = { pagination: { pageSize: 100, @@ -40,32 +44,21 @@ class DataTable extends Component { } async getData(pagination, filters, sorter) { - /* - pagination = { - pageSize: 10, // items per page - current: 1, // page number - total: 123, // Total amount of items - } - filters = { - ReportId: 123 - } - sorter = {} - */ + // pagination = { + // pageSize: 10, // items per page + // current: 1, // page number + // total: 123, // Total amount of items + // } + // filters = { + // ReportId: 123 + // } + // sorter = {} pagination = pagination || this.state.pagination; filters = filters || this.state.filters; sorter = sorter || this.state.sorter; // eslint-disable-next-line new-cap let data = await this.props.DataFunction(); - // await fetchApi(this.state.dataUrl, "post", { - // database: this.props.Database, - // table: this.props.Table, - // dataSource: this.props.DataSource, - // pagination: this.props.RecordCount? {...pagination, pageSize: this.props.RecordCount, current: 1}:pagination, - // filters, - // sorter, - // recordIdField: this.props.RecordIdField || "Id", // Sort by this - // }) data = data || {}; this.setState({ // eslint-disable-next-line new-cap @@ -110,20 +103,16 @@ class DataTable extends Component { async deleteRecord(record) { delete record.key; - // Delete "null" values as they seem to cause deletes to fail silently when the value is null and field type is float + // Delete "null" values as they seem to cause deletes to fail silently when + // the value is null and field type is float for (let key in record) { - if (record[key] === null) delete record[key]; + if (record[key] === null) { delete record[key]; } } - var status = {}; - // await fetchApi(`/api/${this.props.Server}/tableData/record`, "delete", { - // database: this.props.Database, - // table: this.props.Table, - // dataSource: this.props.DataSource, - // record, - // }, undefined, true) + let status = {}; + // TODO: Delete data here - if (status.ok) notify("success", status.msg, "success"); - if (!status.ok) notify("error", status.msg, "error"); + if (status.ok) { notify("success", status.msg, "success"); } + if (!status.ok) { notify("error", status.msg, "error"); } // Force refresh of data this.getData(); @@ -133,60 +122,66 @@ class DataTable extends Component { let tableProps = { ...this.props.TableProps || {}, onRow: (record, rowIndex) => { - /* - To allow for extending functionality, we take a TableProps argument from props and pass it through to the Table component. - Since we are also using onRow internaly here, we handle the externally provided events ourselves by passing along the event - object. This way the end user isn't affected by our event handlers. - */ + // To allow for extending functionality, we take a TableProps argument + // from props and pass it through to the Table component. Since we + // are also using onRow internaly here, we handle the externally + // provided events ourselves by passing along the event object. This + // way the end user isn't affected by our event handlers. let externalOnRow = { - ...(this.props.TableProps && this.props.TableProps.onRow && this.props.TableProps.onRow(record, rowIndex)), + ...( + this.props.TableProps + && this.props.TableProps.onRow + && this.props.TableProps.onRow(record, rowIndex) + ), }; return { onClick: event => { if (event.target.type !== "button") { - if (this.props.Editable) this.showEditModal(record, rowIndex); - if (externalOnRow.onClick) externalOnRow.onClick(event); + if (this.props.Editable) { this.showEditModal(record, rowIndex); } + if (externalOnRow.onClick) { externalOnRow.onClick(event); } } }, onDoubleClick: event => { - if (externalOnRow.onDoubleClick) externalOnRow.onDoubleClick(event); + if (externalOnRow.onDoubleClick) { externalOnRow.onDoubleClick(event); } }, onContextMenu: event => { - if (externalOnRow.onContextMenu) externalOnRow.onContextMenu(event); + if (externalOnRow.onContextMenu) { externalOnRow.onContextMenu(event); } }, onMouseEnter: event => { - if (externalOnRow.onMouseEnter) externalOnRow.onMouseEnter(event); + if (externalOnRow.onMouseEnter) { externalOnRow.onMouseEnter(event); } }, onMouseLeave: event => { - if (externalOnRow.onMouseLeave) externalOnRow.onMouseLeave(event); + if (externalOnRow.onMouseLeave) { externalOnRow.onMouseLeave(event); } }, }; }, }; let columns = this.props.columns || this.inferColumns(this.state.tableData); - if (this.props.DeleteRecord && !columns.find(x => x.dataIndex === "notInUse")) columns.push({ - // Add delete button on the right - dataIndex: "notInUse", - title: "", - render: (text, record) => { - this.deleteRecord(record); - }} - > - Slett permanent + if (this.props.DeleteRecord && !columns.find(x => x.dataIndex === "notInUse")) { + columns.push({ + // Add delete button on the right + dataIndex: "notInUse", + title: "", + render: (text, record) => { + this.deleteRecord(record); + }} + > + Slett permanent + + } + trigger="click" + > + - } - trigger="click" - > - - , - }); + , + }); + } return <> @@ -203,7 +198,9 @@ class DataTable extends Component { {...tableProps} columns={columns.map(x => ({ ...x, // Add a 4th parameter to the stock render function with all of our table data - render: x.render ? (...originalParams) => x.render(...originalParams, this.state.tableData) : undefined, + render: x.render + ? (...originalParams) => x.render(...originalParams, this.state.tableData) + : undefined, }))} dataSource={this.state.tableData} pagination={this.state.pagination.total > 10 ? this.state.pagination : false} @@ -262,7 +259,8 @@ class DataTable extends Component { } inferColumns(data) { - // Look at the data and find columns and how to display them. Can be overridden by props.Columns if more customization is needed + // Look at the data and find columns and how to display them. Can be + // overridden by props.Columns if more customization is needed let columns = []; data.forEach(row => { Object.keys(row).filter(x => x !== "key").forEach(key => { diff --git a/packages/web_ui/webpack.common.js b/packages/web_ui/webpack.common.js index 3b10a606..00dd319c 100644 --- a/packages/web_ui/webpack.common.js +++ b/packages/web_ui/webpack.common.js @@ -25,7 +25,7 @@ module.exports = (env = {}) => ({ // Make sure ant-design icons use the ES variant new webpack.NormalModuleReplacementPlugin( /@ant-design\/icons\/[A-Z]/, - function(resource) { + resource => { resource.request = resource.request.replace(/@ant-design\/icons/, "$&/es/icons"); } ), diff --git a/plugins/research_sync/master.js b/plugins/research_sync/master.js index 9d735ceb..cec2295e 100644 --- a/plugins/research_sync/master.js +++ b/plugins/research_sync/master.js @@ -6,6 +6,28 @@ const libPlugin = require("@clusterio/lib/plugin"); const RateLimiter = require("@clusterio/lib/RateLimiter"); +async function loadTechnologies(masterConfig, logger) { + let filePath = path.join(masterConfig.get("master.database_directory"), "technologies.json"); + logger.verbose(`Loading ${filePath}`); + try { + return new Map(JSON.parse(await fs.readFile(filePath))); + + } catch (err) { + if (err.code === "ENOENT") { + logger.verbose("Creating new technologies database"); + return new Map(); + + } + throw err; + } +} + +async function saveTechnologies(masterConfig, technologies, logger) { + let filePath = path.join(masterConfig.get("master.database_directory"), "technologies.json"); + logger.verbose(`writing ${filePath}`); + await fs.outputFile(filePath, JSON.stringify([...technologies.entries()], null, 4)); +} + class MasterPlugin extends libPlugin.BaseMasterPlugin { async init() { this.technologies = await loadTechnologies(this.master.config, this.logger); @@ -92,9 +114,8 @@ class MasterPlugin extends libPlugin.BaseMasterPlugin { let match = /-(\d+)$/.exec(name); if (!match) { return 1; - } else { - return Number.parseInt(match[1], 10); } + return Number.parseInt(match[1], 10); } for (let instanceTech of message.data.technologies) { @@ -145,29 +166,6 @@ class MasterPlugin extends libPlugin.BaseMasterPlugin { } } -async function loadTechnologies(masterConfig, logger) { - let filePath = path.join(masterConfig.get("master.database_directory"), "technologies.json"); - logger.verbose(`Loading ${filePath}`); - try { - return new Map(JSON.parse(await fs.readFile(filePath))); - - } catch (err) { - if (err.code === "ENOENT") { - logger.verbose("Creating new technologies database"); - return new Map(); - - } else { - throw err; - } - } -} - -async function saveTechnologies(masterConfig, technologies, logger) { - let filePath = path.join(masterConfig.get("master.database_directory"), "technologies.json"); - logger.verbose(`writing ${filePath}`); - await fs.outputFile(filePath, JSON.stringify([...technologies.entries()], null, 4)); -} - module.exports = { MasterPlugin, diff --git a/plugins/statistics_exporter/instance.js b/plugins/statistics_exporter/instance.js index 0784f816..e7204d7a 100644 --- a/plugins/statistics_exporter/instance.js +++ b/plugins/statistics_exporter/instance.js @@ -5,19 +5,23 @@ const libHelpers = require("@clusterio/lib/helpers"); const instancePlayerCount = new Gauge( - "clusterio_statistics_exporter_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_statistics_exporter_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_statistics_exporter_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_statistics_exporter_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/master.js b/plugins/subspace_storage/master.js index 5a8111b7..6cf6e538 100644 --- a/plugins/subspace_storage/master.js +++ b/plugins/subspace_storage/master.js @@ -27,6 +27,33 @@ const masterInventoryGauge = new Gauge( ); +async function loadDatabase(config, logger) { + let itemsPath = path.resolve(config.get("master.database_directory"), "items.json"); + logger.verbose(`Loading ${itemsPath}`); + try { + let content = await fs.readFile(itemsPath); + return new libDatabase.ItemDatabase(JSON.parse(content)); + + } catch (err) { + if (err.code === "ENOENT") { + logger.verbose("Creating new item database"); + return new libDatabase.ItemDatabase(); + } + throw err; + } +} + +async function saveDatabase(masterConfig, items, logger) { + if (items && items.size < 50000) { + let file = path.resolve(masterConfig.get("master.database_directory"), "items.json"); + logger.verbose(`writing ${file}`); + let content = JSON.stringify(items.serialize()); + await fs.outputFile(file, content); + } else if (items) { + logger.error(`Item database too large, not saving (${items.size})`); + } +} + class MasterPlugin extends libPlugin.BaseMasterPlugin { async init() { @@ -169,35 +196,6 @@ class MasterPlugin extends libPlugin.BaseMasterPlugin { } } -async function loadDatabase(config, logger) { - let itemsPath = path.resolve(config.get("master.database_directory"), "items.json"); - logger.verbose(`Loading ${itemsPath}`); - try { - let content = await fs.readFile(itemsPath); - return new libDatabase.ItemDatabase(JSON.parse(content)); - - } catch (err) { - if (err.code === "ENOENT") { - logger.verbose("Creating new item database"); - return new libDatabase.ItemDatabase(); - - } else { - throw err; - } - } -} - -async function saveDatabase(masterConfig, items, logger) { - if (items && items.size < 50000) { - let file = path.resolve(masterConfig.get("master.database_directory"), "items.json"); - logger.verbose(`writing ${file}`); - let content = JSON.stringify(items.serialize()); - await fs.outputFile(file, content); - } else if (items) { - logger.error(`Item database too large, not saving (${items.size})`); - } -} - module.exports = { MasterPlugin, }; diff --git a/plugins/subspace_storage/routes.js b/plugins/subspace_storage/routes.js index ef5504c5..e3b0d3e9 100644 --- a/plugins/subspace_storage/routes.js +++ b/plugins/subspace_storage/routes.js @@ -12,12 +12,12 @@ function addApiRoutes(app, items, endpointHitCounter) { * @alias api/inventory * @returns {object[]} JSON [{name:"iron-plate", count:100},{name:"copper-plate",count:5}] */ - app.get("/api/inventory", function(req, res) { + app.get("/api/inventory", (req, res) => { endpointHitCounter.labels(req.route.path).inc(); res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); // Check it and send it - var inventory = []; + let inventory = []; for (let [name, count] of items._items) { inventory.push({ name, count }); } @@ -33,7 +33,7 @@ function addApiRoutes(app, items, endpointHitCounter) { * @alias api/inventoryAsObject * @returns {object} JSON {"iron-plate":100, "copper-plate":5} */ - app.get("/api/inventoryAsObject", function(req, res) { + app.get("/api/inventoryAsObject", (req, res) => { endpointHitCounter.labels(req.route.path).inc(); res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index a1528e7c..1897b42f 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -173,7 +173,7 @@ describe("Integration of Clusterio", function() { ], ]; - for (let [prop, value, ,] of testConfigs) { + for (let [prop, value] of testConfigs) { value = `"'${JSON.stringify(value).replace(/"/g, process.platform === "win32" ? '""' : '\\"')}'"`; let args = `test factorio.settings ${prop} ${value}`; await execCtl(`instance config set-prop ${args}`); diff --git a/test/integration/server.js b/test/integration/server.js index 480fb11e..e2796ea8 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -34,7 +34,7 @@ describe("Integration of lib/factorio/server", function() { await fs.ensureDir(writePath); logFile = fs.createWriteStream(path.join(writePath, "log.txt"), "utf8"); server.on("output", function(output) { - logFile.write(JSON.stringify(output) + "\n"); + logFile.write(`${JSON.stringify(output)}\n`); }); // Initialize sever. @@ -71,7 +71,7 @@ describe("Integration of lib/factorio/server", function() { }); function log(message) { - logFile.write("=================== " + message + "\n"); + logFile.write(`=================== ${message}\n`); } describe(".create()", function() { diff --git a/test/lib/config/classes.js b/test/lib/config/classes.js index ac3edbb0..48db16ec 100644 --- a/test/lib/config/classes.js +++ b/test/lib/config/classes.js @@ -351,8 +351,11 @@ describe("lib/config/classes", function() { assert.deepEqual(testInstance.get("json"), { json: true }); let errMsg; - try { JSON.parse("blah"); } - catch (err) { errMsg = err.message; } + try { + JSON.parse("blah"); + } catch (err) { + errMsg = err.message; + } assert.throws( () => testInstance.set("json", "blah"), new classes.InvalidValue(`Error parsing value for json: ${errMsg}`)