Fix the remaning linter warnings (#353)

Refactor the code base to remove all of the remaining warnings from
ESLint and convert them into errors.
This commit is contained in:
Hornwitser authored and GitHub committed 2021-01-05 22:28:08 +01:00
1 parent 9243731646
commit 1b901f5d1c
33 files changed
+718 -609

No files matched your search

+20 -20
View File
@@ -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",
+49 -33
View File
@@ -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);
});
});
+1 -1
View File
@@ -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}
*/
+59 -58
View File
@@ -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); });
}
+5 -2
View File
@@ -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+)?)$/;
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable global-require */
/**
* Configuration System
* @author Hornwitser
+2 -4
View File
@@ -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;
}
}
+101 -88
View File
@@ -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);
}
}
}
}
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable global-require */
/**
* Library for interfacing with Factorio Servers and saves.
* @module lib/factorio
+3 -3
View File
@@ -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}")`);
}
}
}
+4 -4
View File
@@ -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- ")}`
));
}
}
+3 -3
View File
@@ -14,14 +14,14 @@ const fs = require("fs");
* @returns {Promise<string>} 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);
});
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable global-require */
"use strict";
// eslint-disable-next-line no-process-env
if (process.env.APP_ENV === "browser") {
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable global-require */
/**
* Library for message based communication between nodes
* @module lib/link
+1 -2
View File
@@ -124,9 +124,8 @@ class Link {
if (handler) {
handler(message);
return true;
} else {
return false;
}
return false;
}
/**
+8 -5
View File
@@ -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: {
+4 -4
View File
@@ -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;
+1
View File
@@ -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",
+1 -3
View File
@@ -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) {
+46
View File
@@ -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,
};
+2 -1
View File
@@ -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",
+1
View File
@@ -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}`);
+190 -164
View File
@@ -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(`
+----------------------------------+
+76 -78
View File
@@ -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;
});
}
+67 -69
View File
@@ -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) => <Popover
content={
<Button
type="primary"
danger
onClick={() => {
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) => <Popover
content={
<Button
type="primary"
danger
onClick={() => {
this.deleteRecord(record);
}}
>
Slett permanent
</Button>
}
trigger="click"
>
<Button danger style={{ float: "right", fontSize: "16px" }}>
<DeleteOutlined />
</Button>
}
trigger="click"
>
<Button danger style={{ float: "right", fontSize: "16px" }}>
<DeleteOutlined />
</Button>
</Popover>,
});
</Popover>,
});
}
return <>
<Row>
@@ -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 => {
+1 -1
View File
@@ -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");
}
),
+23 -25
View File
@@ -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,
+8 -4
View File
@@ -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"] },
);
+27 -29
View File
@@ -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,
};
+3 -3
View File
@@ -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");
+1 -1
View File
@@ -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}`);
+2 -2
View File
@@ -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() {
+5 -2
View File
@@ -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}`)