Merge pull request #300 from Hornwitser/lint

Lint codebase
This commit is contained in:
Hornwitser authored and GitHub committed 2020-06-17 22:12:27 +02:00
commit 0969e07d79
84 files changed
+1758 -1348

No files matched your search

+253
View File
@@ -0,0 +1,253 @@
"use strict";
module.exports = {
"env": {
"node": true,
"browser": true,
"commonjs": true,
"es6": true,
},
"parserOptions": {
"ecmaVersion": 11,
},
"overrides": [
{
"files": ["test/**/*.js", "plugins/*/test/**/*.js"],
"env": {
"mocha": "true",
},
"rules": {
"prefer-arrow-callback": "off",
},
},
],
"rules": {
"accessor-pairs": "error",
"array-bracket-newline": "off",
"array-bracket-spacing": ["error", "never"],
"array-callback-return": "error",
"array-element-newline": "off",
"arrow-body-style": "error",
"arrow-parens": "off",
"arrow-spacing": ["error", { "after": true, "before": true }],
"block-scoped-var": "error",
"block-spacing": "error",
"brace-style": ["warn", "1tbs", { "allowSingleLine": true }],
"callback-return": "off",
"camelcase": "off",
"capitalized-comments": "off",
"class-methods-use-this": "off",
"comma-dangle": [
"error",
{
"arrays": "always-multiline",
"objects": "always-multiline",
"functions": "only-multiline",
},
],
"comma-spacing": "warn",
"comma-style": ["error", "last"],
"complexity": "warn",
"computed-property-spacing": ["error", "never"],
"consistent-return": "error",
"consistent-this": "error",
"curly": "warn",
"default-case": "error",
"default-case-last": "error",
"default-param-last": "error",
"dot-location": ["error", "property"],
"dot-notation": ["warn", { "allowKeywords": true }],
"eol-last": "error",
"eqeqeq": "warn",
"func-call-spacing": "error",
"func-name-matching": "error",
"func-names": "off",
"func-style": ["error", "declaration"],
"function-call-argument-newline": "off",
"function-paren-newline": "off",
"generator-star-spacing": ["error", { "before": false, "after": true }],
"global-require": "warn",
"grouped-accessor-pairs": "error",
"guard-for-in": "error",
"handle-callback-err": "error",
"id-blacklist": "error",
"id-length": "off",
"id-match": "error",
"implicit-arrow-linebreak": "error",
"indent": ["error", "tab", { "SwitchCase": 1 }],
"indent-legacy": "off",
"init-declarations": "off",
"jsx-quotes": "error",
"key-spacing": "error",
"keyword-spacing": "warn",
"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-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-ternary": "warn",
"new-cap": "warn",
"new-parens": "error",
"newline-after-var": "off",
"newline-before-return": "off",
"newline-per-chained-call": "off",
"no-alert": "error",
"no-array-constructor": "error",
"no-await-in-loop": "off",
"no-bitwise": "off",
"no-buffer-constructor": "error",
"no-caller": "error",
"no-catch-shadow": "error",
"no-confusing-arrow": "error",
"no-console": "off",
"no-constant-condition": ["error", { "checkLoops": false }],
"no-constructor-return": "error",
"no-continue": "off",
"no-div-regex": "error",
"no-duplicate-imports": "error",
"no-else-return": "warn",
"no-empty-function": "off",
"no-eq-null": "error",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-label": "error",
"no-extra-parens": "off",
"no-floating-decimal": "error",
"no-implicit-coercion": "error",
"no-implicit-globals": "error",
"no-implied-eval": "error",
"no-inline-comments": "off",
"no-invalid-this": "warn",
"no-iterator": "error",
"no-label-var": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-loop-func": "error",
"no-magic-numbers": "off",
"no-mixed-operators": "warn",
"no-mixed-requires": "error",
"no-multi-assign": "error",
"no-multi-spaces": "error",
"no-multi-str": "error",
"no-multiple-empty-lines": "error",
"no-native-reassign": "error",
"no-negated-condition": "off",
"no-negated-in-lhs": "error",
"no-nested-ternary": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-object": "error",
"no-new-require": "error",
"no-new-wrappers": "error",
"no-octal-escape": "error",
"no-param-reassign": "off",
"no-path-concat": "error",
"no-plusplus": ["warn", { "allowForLoopAfterthoughts": true }],
"no-process-env": "error",
"no-process-exit": "warn",
"no-proto": "error",
"no-restricted-exports": "error",
"no-restricted-globals": "error",
"no-restricted-imports": "error",
"no-restricted-modules": "error",
"no-restricted-properties": "error",
"no-restricted-syntax": "error",
"no-return-assign": "error",
"no-return-await": "off",
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow": "warn",
"no-spaced-func": "error",
"no-sync": "error",
"no-tabs": ["error", { "allowIndentationTabs": true }],
"no-template-curly-in-string": "error",
"no-ternary": "off",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef-init": "error",
"no-undefined": "off",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": "error",
"no-unused-expressions": "error",
"no-use-before-define": "warn",
"no-useless-backreference": "error",
"no-useless-call": "error",
"no-useless-computed-key": "error",
"no-useless-concat": "error",
"no-useless-constructor": "error",
"no-useless-rename": "error",
"no-useless-return": "error",
"no-var": "warn",
"no-void": "error",
"no-warning-comments": "off",
"no-whitespace-before-property": "error",
"nonblock-statement-body-position": "error",
"object-curly-newline": "error",
"object-curly-spacing": "off",
"object-shorthand": "off",
"one-var": "off",
"one-var-declaration-per-line": "error",
"operator-assignment": "off",
"operator-linebreak": "warn",
"padded-blocks": "off",
"padding-line-between-statements": "error",
"prefer-arrow-callback": "warn",
"prefer-const": "off",
"prefer-destructuring": "off",
"prefer-exponentiation-operator": "error",
"prefer-named-capture-group": "off",
"prefer-numeric-literals": "error",
"prefer-object-spread": "error",
"prefer-promise-reject-errors": "error",
"prefer-reflect": "off",
"prefer-regex-literals": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "warn",
"quote-props": "off",
"quotes": ["error", "double", { "avoidEscape": true }],
"radix": ["error", "always"],
"require-atomic-updates": "off", // XXX Not sure why this triggers on process.title = ...
"require-await": "off",
"require-jsdoc": "off",
"require-unicode-regexp": "off",
"rest-spread-spacing": ["error", "never"],
"semi": "error",
"semi-spacing": "error",
"semi-style": "off",
"sort-imports": "error",
"sort-keys": "off",
"sort-vars": "error",
"space-before-blocks": "warn",
"space-before-function-paren": "off",
"space-in-parens": ["error", "never"],
"space-infix-ops": "off",
"space-unary-ops": "error",
"spaced-comment": ["warn", "always"],
"strict": "error",
"switch-colon-spacing": "error",
"symbol-description": "error",
"template-curly-spacing": ["error", "never"],
"template-tag-spacing": "error",
"unicode-bom": ["error", "never"],
"valid-jsdoc": ["error", { "requireReturn": false }],
"vars-on-top": "warn",
"wrap-iife": "error",
"wrap-regex": "off",
"yield-star-spacing": "error",
"yoda": ["error", "never", { "exceptRange": true }],
},
};
+1
View File
@@ -11,3 +11,4 @@ script:
- npm run-script ci-cover
- cat temp/test/integration/log.txt
- codecov -f coverage/lcov.info
- npm run-script lint
+9 -9
View File
@@ -12,11 +12,11 @@ async function main() {
const args = yargs
.scriptName("build")
.options({
'clean': { describe: "Remove previous builds", type: 'boolean', default: false },
'build': { describe: "Build mod", type: 'boolean', default: true },
'pack': { describe: "Pack into zip file", type: 'boolean', default: true },
"clean": { describe: "Remove previous builds", type: "boolean", default: false },
"build": { describe: "Build mod", 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", default: libPath },
'output-dir': { describe: "Path to output built mod", nargs: 1, type: 'string', default: "dist" },
"output-dir": { describe: "Path to output built mod", nargs: 1, type: "string", default: "dist" },
"bump-patch": { describe: "Increment patch number of build", type: "boolean", default: false },
"factorio-version": { describe: "Override factorio_version", type: "string" },
})
@@ -45,7 +45,7 @@ async function build(args) {
}
if (args.clean) {
let splitter = /^(.*)_(\d+\.\d+\.\d+)(\.zip)?$/
let splitter = /^(.*)_(\d+\.\d+\.\d+)(\.zip)?$/;
for (let entry of await fs.readdir(args.outputDir)) {
let match = splitter.exec(entry);
if (match) {
@@ -66,7 +66,7 @@ async function build(args) {
if (args.pack) {
let zip = new JSZip();
let walker = klaw(args.sourceDir)
.on('data', item => {
.on("data", item => {
if (item.stats.isFile()) {
// On Windows the path created uses backslashes as the directory sepparator
// but the zip file needs to use forward slashes. We can't use the posix
@@ -76,7 +76,7 @@ async function build(args) {
}
})
;
await events.once(walker, 'end');
await events.once(walker, "end");
for (let pathParts of info.additional_files || []) {
let filePath = path.join(...pathParts);
@@ -89,7 +89,7 @@ async function build(args) {
let modPath = path.join(args.outputDir, `${modName}.zip`);
console.log(`Writing ${modPath}`);
let writeStream = zip.generateNodeStream().pipe(fs.createWriteStream(modPath));
await events.once(writeStream, 'finish');
await events.once(writeStream, "finish");
} else {
let modDir = path.join(args.outputDir, modName);
@@ -111,7 +111,7 @@ async function build(args) {
}
if (module === require.main) {
main().catch(err => { console.log(err) });
main().catch(err => { console.log(err); });
}
module.exports = {
+56 -53
View File
@@ -2,11 +2,12 @@
* Command line interface for controlling a Clusterio cluster
* @module
*/
"use strict";
const jwt = require("jsonwebtoken");
const fs = require("fs-extra");
const yargs = require("yargs");
const version = require("./package").version;
const asTable = require("as-table").configure({delimiter: ' | '});
const asTable = require("as-table").configure({ delimiter: " | " });
const chalk = require("chalk");
const events = require("events");
@@ -21,18 +22,20 @@ const config = require("lib/config");
* Formats a parsed Factorio output from lib/factorio into a readable
* colorized output using terminal escape codes that can be printed.
*
* @param {Object} output - Factorio server output.
* @returns {string} terminal colorized message.
* @private
*/
function formatOutputColored(output) {
let time = "";
if (output.format === 'seconds') {
time = chalk.yellow(output.time.padStart(8)) + ' ';
} else if (output.format === 'date') {
time = chalk.yellow(output.time) + ' ';
if (output.format === "seconds") {
time = chalk.yellow(output.time.padStart(8)) + " ";
} else if (output.format === "date") {
time = chalk.yellow(output.time) + " ";
}
let info = "";
if (output.type === 'log') {
if (output.type === "log") {
let level = output.level;
if (level === "Info") {
level = chalk.bold.blueBright(level);
@@ -42,10 +45,10 @@ function formatOutputColored(output) {
level = chalk.bold.redBright(level);
}
info = level + ' ' + chalk.gray(output.file) + ': ';
info = level + " " + chalk.gray(output.file) + ": ";
} else if (output.type === 'action') {
info = '[' + chalk.yellow(output.action) + '] ';
} else if (output.type === "action") {
info = "[" + chalk.yellow(output.action) + "] ";
}
return time + info + output.message;
@@ -66,7 +69,7 @@ class Command {
}
async run(args, control) {
await this._handler.call(this, args, control);
await this._handler(args, control);
}
}
@@ -76,8 +79,8 @@ class Command {
* Resolves a string with either an instance name or an id into an integer
* with the instance ID.
*
* @param client - link to master server to query instance on.
* @param instanceName - string with name or id of instance.
* @param {module:lib/link.Link} client - link to master server to query instance on.
* @param {string} instanceName - string with name or id of instance.
* @returns {number} instance ID.
* @private
*/
@@ -108,8 +111,8 @@ async function resolveInstance(client, instanceName) {
* Resolves a string with either an slave name or an id into an integer with
* the slave ID.
*
* @param client - link to master server to query slave on.
* @param slaveName - string with name or id of slave.
* @param {module:lib/link.Link} client - link to master server to query slave on.
* @param {string} slaveName - string with name or id of slave.
* @returns {number} slave ID.
* @private
*/
@@ -140,8 +143,8 @@ async function resolveSlave(client, slaveName) {
* Resolves a string with either a role name or an id into an object
* representing the role.
*
* @param client - link to master server to query role on.
* @param roleName - string with name or id of role.
* @param {module:lib/link.Link} client - link to master server to query role on.
* @param {string} roleName - string with name or id of role.
* @returns {Object} Role info.
* @private
*/
@@ -199,10 +202,10 @@ commands.push(new Command({
yargs.option("id", { type: "number", nargs: 1, describe: "Slave id", default: null });
yargs.option("name", { type: "string", nargs: 1, describe: "Slave name", default: null });
yargs.option("generate-token", {
type: "boolean", nargs: 0, describe: "Generate authentication token", default: false
type: "boolean", nargs: 0, describe: "Generate authentication token", default: false,
});
yargs.option("output", {
type: "string", nargs: 1, describe: "Path to output config (- for stdout)", default: "config-slave.json"
type: "string", nargs: 1, describe: "Path to output config (- for stdout)", default: "config-slave.json",
});
}],
handler: async function(args, control) {
@@ -233,29 +236,29 @@ commands.push(new Command({
handler: async function(args, control) {
let response = await link.messages.listInstances.send(control);
console.log(asTable(response.list));
}
},
}));
commands.push(new Command({
definition: ['create-instance', "Create an instance", (yargs) => {
definition: ["create-instance", "Create an instance", (yargs) => {
// XXX TODO: set any specific options?
yargs.option('name', {
yargs.option("name", {
type: "string",
nargs: 1,
describe: "Instance name",
group: "Instance Config:",
});
yargs.option('id', {
yargs.option("id", {
type: "number",
nargs: 1,
describe: "Instance ID",
group: "Instance Config:",
});
yargs.option('base', {
yargs.option("base", {
type: "boolean",
nargs: 0,
describe: "Specify this is a base config",
group: "Instance Config:"
group: "Instance Config:",
});
}],
handler: async function(args, control) {
@@ -329,10 +332,10 @@ commands.push(new Command({
}));
commands.push(new Command({
definition: ['assign-instance', "Assign instance to a slave", (yargs) => {
definition: ["assign-instance", "Assign instance to a slave", (yargs) => {
yargs.options({
'instance': { describe: "Instance to assign", nargs: 1, type: 'string', demandOption: true },
'slave': { describe: "Slave to assign to", nargs: 1, type: 'string', demandOption: true },
"instance": { describe: "Instance to assign", nargs: 1, type: "string", demandOption: true },
"slave": { describe: "Slave to assign to", nargs: 1, type: "string", demandOption: true },
});
}],
handler: async function(args, control) {
@@ -346,9 +349,9 @@ commands.push(new Command({
}));
commands.push(new Command({
definition: ['create-save', "Create a new save on an instance", (yargs) => {
definition: ["create-save", "Create a new save on an instance", (yargs) => {
yargs.options({
'instance': { describe: "Instance to create on", nargs: 1, type: 'string', demandOption: true },
"instance": { describe: "Instance to create on", nargs: 1, type: "string", demandOption: true },
});
}],
handler: async function(args, control) {
@@ -361,9 +364,9 @@ commands.push(new Command({
}));
commands.push(new Command({
definition: ['export-data', "Export item icons and locale from instance", (yargs) => {
definition: ["export-data", "Export item icons and locale from instance", (yargs) => {
yargs.options({
'instance': { describe: "Instance to export from", nargs: 1, type: 'string', demandOption: true },
"instance": { describe: "Instance to export from", nargs: 1, type: "string", demandOption: true },
});
}],
handler: async function(args, control) {
@@ -376,10 +379,10 @@ commands.push(new Command({
}));
commands.push(new Command({
definition: ['start-instance', "Start instance", (yargs) => {
definition: ["start-instance", "Start instance", (yargs) => {
yargs.options({
'instance': { describe: "Instance to start", nargs: 1, type: 'string', demandOption: true },
'save': { describe: "Save load, defaults to latest", nargs: 1, type: 'string' },
"instance": { describe: "Instance to start", nargs: 1, type: "string", demandOption: true },
"save": { describe: "Save load, defaults to latest", nargs: 1, type: "string" },
});
}],
handler: async function(args, control) {
@@ -393,9 +396,9 @@ commands.push(new Command({
}));
commands.push(new Command({
definition: ['stop-instance', "Stop instance", (yargs) => {
definition: ["stop-instance", "Stop instance", (yargs) => {
yargs.options({
'instance': { describe: "Instance to stop", nargs: 1, type: 'string', demandOption: true },
"instance": { describe: "Instance to stop", nargs: 1, type: "string", demandOption: true },
});
}],
handler: async function(args, control) {
@@ -408,9 +411,9 @@ commands.push(new Command({
}));
commands.push(new Command({
definition: ['delete-instance', "Delete instance", (yargs) => {
definition: ["delete-instance", "Delete instance", (yargs) => {
yargs.options({
'instance': { describe: "Instance to delete", nargs: 1, type: 'string', demandOption: true },
"instance": { describe: "Instance to delete", nargs: 1, type: "string", demandOption: true },
});
}],
handler: async function(args, control) {
@@ -421,17 +424,17 @@ commands.push(new Command({
}));
commands.push(new Command({
definition: ['send-rcon', "Send RCON command", (yargs) => {
definition: ["send-rcon", "Send RCON command", (yargs) => {
yargs.options({
'instance': { describe: "Instance to sent to", nargs: 1, type: 'string', demandOption: true },
'command': { describe: "command to send", nargs: 1, type: 'string', demandOption: true },
"instance": { describe: "Instance to sent to", nargs: 1, type: "string", demandOption: true },
"command": { describe: "command to send", nargs: 1, type: "string", demandOption: true },
});
}],
handler: async function(args, control) {
let response = await link.messages.sendRcon.send(control, {
instance_id: await resolveInstance(control, args.instance),
command: args.command
})
command: args.command,
});
// Factorio includes a newline in it's response output.
process.stdout.write(response.result);
@@ -466,7 +469,7 @@ commands.push(new Command({
let response = await link.messages.createRole.send(control, {
name: args.name,
description: args.description,
permissions: args.permissions
permissions: args.permissions,
});
console.log(`Created role ID ${response.id}`);
},
@@ -509,7 +512,7 @@ commands.push(new Command({
});
}],
handler: async function(args, control) {
let role = await resolveRole(control, args.role)
let role = await resolveRole(control, args.role);
await link.messages.deleteRole.send(control, { id: role.id });
},
}));
@@ -623,7 +626,7 @@ class Control extends link.Link {
// I don't like God classes, but the alternative of putting all this state
// into global variables is not much better.
constructor(connector) {
super('control', 'master', connector);
super("control", "master", connector);
link.attachAllMessages(this);
}
@@ -647,7 +650,7 @@ class Control extends link.Link {
}
}
await this.connector.close(1001, "Control Quit")
await this.connector.close(1001, "Control Quit");
}
}
@@ -656,12 +659,12 @@ async function startControl() {
yargs
.scriptName("clusterctl")
.usage("$0 <command> [options]")
.option('config', {
.option("config", {
nargs: 1,
describe: "config file to get credentails from",
default: "config-control.json",
defaultDescription: "auto",
type: 'string',
type: "string",
})
.command("control-config", "Manage Control config", config.configCommand)
.wrap(yargs.terminalWidth())
@@ -684,7 +687,7 @@ async function startControl() {
await controlConfig.load(JSON.parse(await fs.readFile(args.config)));
} catch (err) {
if (err.code === 'ENOENT') {
if (err.code === "ENOENT") {
console.log("Config not found, initializing new config");
await controlConfig.init();
@@ -730,10 +733,10 @@ async function startControl() {
});
if (commands.has(commandName)) {
command = commands.get(commandName);
let command = commands.get(commandName);
try {
await command.run(args, control)
await command.run(args, control);
} catch (err) {
if (err instanceof errors.CommandError) {
@@ -761,7 +764,7 @@ module.exports = {
_Control: Control,
_commands: commands,
}
};
if (module === require.main) {
+15 -10
View File
@@ -11,6 +11,7 @@ Contents
- [Workflow](#workflow)
- [Starting a Feature Branch](#starting-a-feature-branch)
- [Checking your code for errors](#checking-your-code-for-errors)
- [Submitting a Pull Request](#submitting-a-pull-request)
- [Editing your Feature Branch](#editing-your-feature-branch)
- [Resolving Merge Conflicts](#resolving-merge-conflicts)
@@ -76,6 +77,18 @@ you've used before. But it's nice to have its name be somewhat
descriptive of what it implements.
### Checking your code for errors
There's automated tests and linting setup for the project. Please do
add tests for any code you add and run both:
npm run test
npm run lint
To check that your changes pass the integration tests as well as the
ESLint rules set up for the project.
### Submitting a Pull Request
Once you've commited changes to your feature branch that you want to
@@ -183,21 +196,13 @@ are at least a few things that have been agreed upon.
JavaScript and Lua code should use double quoted strings (`"string"`)
for all strings with the only exception that strings that contain double
quotes may use single quotes (i.e., `'String with "quotes"'`.) For
quotes may use single quotes (i.e., `'String with "quotes"'`).
JavaScript template literals may also be used where appropriate.
There are places in the codebase that are using `'single'` quoted
strings. Should you be modifying such code use double quotes on new and
modified lines and leave the rest untouched.
### Indenting
JavaScript code should be indented with tabs. There are places in the
codebase that are mixing tabs and spaces or uses only spaces for
indents. Should you be modifiying such code that is mixing spaces and
tabs as indending you should update the indenting to use tabs _on the
code you change_ while leaving the rest of the code untouched.
JavaScript code should be indented with tabs.
Lua code should be indented with 4 spaces.
-1
View File
@@ -16,7 +16,6 @@
"clusterctl.js",
"lib/",
"plugins/",
"routes/",
"routes.js"
],
"excludePattern": "node_modules"
+76 -38
View File
@@ -47,7 +47,7 @@ class InvalidField extends Error { };
function splitOn(separator, string) {
let index = string.indexOf(separator);
if (index === -1) {
return [string, ''];
return [string, ""];
}
return [string.slice(0, index), string.slice(index + separator.length)];
}
@@ -73,7 +73,7 @@ class Config extends events.EventEmitter {
super();
if (!this.constructor._finalized) {
throw new Error(`Cannot instantiate incomplete Config class ${this.constructor.name}`)
throw new Error(`Cannot instantiate incomplete Config class ${this.constructor.name}`);
}
this._groups = new Map();
@@ -120,6 +120,8 @@ class Config extends events.EventEmitter {
/**
* Serialize the config to a plain JavaScript object
*
* @returns {Object} JSON serializable representation of the config.
*/
serialize() {
let groups = [...this._unknownGroups.values()];
@@ -135,6 +137,9 @@ class Config extends events.EventEmitter {
*
* Updates all groups in the config with groups in the serialized
* config passed.
*
* @param {Object} serializedConfig - Output from .serialize().
* @param {boolean} notify - Invoke fieldChanged events if true.
*/
update(serializedConfig, notify) {
this._validate(serializedConfig);
@@ -150,34 +155,47 @@ class Config extends events.EventEmitter {
/**
* Get the value for a config field
*
* @param {string} name - Name of field to get.
* @returns {*} the value for the field.
*/
get(name) {
let [group, field] = splitOn('.', name);
let [group, field] = splitOn(".", name);
return this.group(group).get(field);
}
/**
* Set the value for a config field
*
* @param {string} name - Name of field to set.
* @param {*} value - the value to set on field
*/
set(name, value) {
let [group, field] = splitOn('.', name);
return this.group(group).set(field, value);
let [group, field] = splitOn(".", name);
this.group(group).set(field, value);
}
/**
* Set property of an object value for a config field
*
* @param {string} name - Name of field to set property on.
* @param {string} prop - Name of property to set on field.
* @param {*} value - the value to set the property to.
*/
setProp(name, prop, value) {
let [group, field] = splitOn('.', name);
return this.group(group).setProp(field, prop, value);
let [group, field] = splitOn(".", name);
this.group(group).setProp(field, prop, value);
}
/**
* Get the config group instance with the given name
*
* @param {string} name - Name of group to get.
* @returns {module:lib/config.ConfigGroup} config group.
*/
group(name) {
if (!this._initialized) {
throw new Error(`${this.constructor.name} instance is uninitialized`)
throw new Error(`${this.constructor.name} instance is uninitialized`);
}
let group = this._groups.get(name);
if (!group) {
@@ -223,6 +241,8 @@ class Config extends events.EventEmitter {
*
* Adds the ConfigGroup to this config as a group that will get
* initialized, loaded and serialized with this config class.
*
* @param {module:lib/config.ConfigGroup} group - class to register.
*/
static registerGroup(group) {
this._initSubclass();
@@ -261,9 +281,9 @@ class ConfigGroup {
* After the creation of the new config group you have to call
* either .init() or .load() on it in order to fully initialize it.
*/
constructor(serializedGroup) {
constructor() {
if (!this.constructor._finalized) {
throw new Error(`Cannot instantiate incomplete ConfigGroup class ${this.constructor.name}`)
throw new Error(`Cannot instantiate incomplete ConfigGroup class ${this.constructor.name}`);
}
this._fields = new Map();
@@ -276,6 +296,8 @@ class ConfigGroup {
*
* Computes and assigns the initial values for all fields of the
* config group.
*
* @param {module:lib/config.Config} config - Config group belongs to.
*/
async init(config) {
this._config = config;
@@ -287,7 +309,7 @@ class ConfigGroup {
let value = null;
if (typeof def.initial_value === "function") {
value = await def.initial_value()
value = await def.initial_value();
} else if (def.initial_value !== undefined) {
value = def.initial_value;
@@ -303,6 +325,10 @@ class ConfigGroup {
* Loads all the values from the passed serialized version of the
* group and then initializes any possible missing fields from it to
* their default values.
*
* @param {module:lib/config.Config} config - Config group belongs to.
* @param {Object} serializedGroup -
* Result from a previous call to .serialize().
*/
async load(config, serializedGroup) {
this.update(serializedGroup, false);
@@ -320,6 +346,9 @@ class ConfigGroup {
/**
* Get value for field
*
* @param {string} name - Name of field to get.
* @returns {*} Value stored for the field.
*/
get(name) {
if (!this.constructor._definitions.has(name)) {
@@ -335,6 +364,8 @@ class ConfigGroup {
* The field must be defined for the config group and the value is
* type checked against the field definition.
*
* @param {string} name - Name of field to set.
* @param {*} value - Value to set for field.
* @throws {module:lib/config.InvalidField} if field is not defined.
* @throws {module:lib/config.InvalidValue} if value is not allowed for the field.
*/
@@ -350,7 +381,7 @@ class ConfigGroup {
}
if (def.enum && !def.enum.includes(value)) {
throw new InvalidValue(`Expected one of [${def.enum.join(', ')}], not ${value}`);
throw new InvalidValue(`Expected one of [${def.enum.join(", ")}], not ${value}`);
}
if (basicType(value) === "string") {
@@ -364,24 +395,22 @@ class ConfigGroup {
value = Number.parseFloat(value);
}
} else if (def.type === "object" ) {
} else if (def.type === "object") {
try {
value = JSON.parse(value);
} catch (err) {
throw new InvalidValue(`Error parsing value for ${name}: ${err.message}`)
throw new InvalidValue(`Error parsing value for ${name}: ${err.message}`);
}
}
}
if (value === null) {
if (!def.optional) {
throw new InvalidValue(`Field ${name} cannot be null`)
throw new InvalidValue(`Field ${name} cannot be null`);
}
} else {
if (basicType(value) !== def.type) {
throw new InvalidValue(`Expected type of ${name} to be ${def.type}, not ${basicType(value)}`);
}
} else if (basicType(value) !== def.type) {
throw new InvalidValue(`Expected type of ${name} to be ${def.type}, not ${basicType(value)}`);
}
let prev = this._fields.get(name);
@@ -397,6 +426,9 @@ class ConfigGroup {
* Update value of stored object field by setting the specified property
* on it. The field must be defined as an object for the config group.
*
* @param {string} name - Name of field to set property on.
* @param {string} prop - Name of property to set on field.
* @param {*} value - the value to set the property to.
* @throws {module:lib/config.InvalidField} if field is not defined.
* @throws {module:lib/config.InvalidValue} if field is not an object.
*/
@@ -430,6 +462,10 @@ class ConfigGroup {
*
* Overwrites the values of all the fields in this config group that
* has a valid value in the passed serialized group with that value.
*
* @param {Object} serializedGroup -
* Result from a previous call to .serialize().
* @param {boolean} notify - Invoke fieldChanged events if true.
*/
update(serializedGroup, notify) {
if (basicType(serializedGroup) !== "object") {
@@ -455,10 +491,9 @@ class ConfigGroup {
if (value !== null && basicType(value) !== def.type) {
continue;
}
} else {
if (basicType(value) !== def.type) {
continue;
}
} else if (basicType(value) !== def.type) {
continue;
}
if (def.enum && !def.enum.includes(value)) {
@@ -475,6 +510,8 @@ class ConfigGroup {
/**
* Serialize the config to a plain JavaScript object
*
* @returns {Object} JSON serializable representation of the group.
*/
serialize() {
let fields = {};
@@ -517,34 +554,34 @@ class ConfigGroup {
/**
* Define a new configuration field
*
* The following properties are supported:
* - type:
* @param {Object} item - Field definition.
* @param {string} item.type -
* string declaring the type of the config value, supports
* boolean, string, number and object.
* - name:
* @param {string} item.name -
* Name of the configuration field. Should follow the
* lower_case_underscore style.
* - title:
* @param {string} item.title -
* Text used to identify the config field in user interfaces.
* - description:
* @param {string} item.description -
* Text used to describe the config field in user interfaces.
* - enum (optional):
* @param {Array=} item.enum -
* Array of all values that are acceptible. Useful for making
* enumerated values.
* - optional (optional):
* @param {boolean=} item.optional -
* True if this field can be set to null. Defaults to false.
* - initial_value (optional):
* @param {*=} item.initial_value -
* Value this config field should take in a newly initialized
* config. This can also be an async function returning the
* value to use.
*/
static define(item) {
if (this._finalized) {
throw new Error(`Cannot define field for ConfigGroup class ${this.name} after it has been finalized`)
throw new Error(`Cannot define field for ConfigGroup class ${this.name} after it has been finalized`);
}
if (basicType(this.groupName) !== "string") {
throw new Error(`Expected ConfigGroup class ${this.name} to have the groupName property set to a string`)
throw new Error(`Expected ConfigGroup class ${this.name} to have the groupName property set to a string`);
}
this._initSubclass();
@@ -556,7 +593,7 @@ class ConfigGroup {
if (!itemKeys.includes(prop)) {
throw new Error(`${prop} is required when defining an field`);
}
})
});
// Validate properties
let validTypes = ["boolean", "string", "number", "object"];
@@ -615,10 +652,11 @@ class ConfigGroup {
throw new Error(`Config field ${item.name} has already been defined`);
}
let computed = Object.assign({
let computed = {
optional: false,
fullName: `${this.groupName}.${item.name}`,
}, item)
...item,
};
if (!computed.optional && computed.initial_value === undefined) {
throw new Error(`Non-optional field ${item.name} needs an initial_value`);
@@ -632,7 +670,7 @@ class ConfigGroup {
*/
static finalize() {
if (basicType(this.groupName) !== "string") {
throw new Error(`Expected ConfigGroup class ${this.name} to have the groupName property set to a string`)
throw new Error(`Expected ConfigGroup class ${this.name} to have the groupName property set to a string`);
}
this._initSubclass();
@@ -672,4 +710,4 @@ module.exports = {
Config,
ConfigGroup,
PluginConfigGroup,
}
};
+10 -5
View File
@@ -15,7 +15,7 @@ const classes = require("./classes");
* @memberof module:lib/config
*/
class MasterGroup extends classes.ConfigGroup { }
MasterGroup.groupName = "master"
MasterGroup.groupName = "master";
MasterGroup.define({
name: "database_directory",
title: "Database directory",
@@ -28,7 +28,7 @@ MasterGroup.define({
title: "HTTP Port",
description: "Port to listen for HTTP connections on, set to null to not listen for HTTP connections.",
type: "number",
optional: true
optional: true,
});
MasterGroup.define({
name: "https_port",
@@ -139,7 +139,7 @@ SlaveGroup.define({
name: "factorio_directory",
description: "Path to directory to look for factorio installs",
type: "string",
initial_value: "factorio"
initial_value: "factorio",
});
SlaveGroup.define({
name: "instances_directory",
@@ -189,7 +189,7 @@ SlaveConfig.registerGroup(SlaveGroup);
* @memberof module:lib/config
*/
class InstanceGroup extends classes.ConfigGroup { }
InstanceGroup.groupName = "instance"
InstanceGroup.groupName = "instance";
InstanceGroup.define({
name: "name",
type: "string",
@@ -314,6 +314,7 @@ function validateGroup(pluginInfo, groupName) {
/**
* Registers the config groups for the provided plugin infos
*
* @param {Array<Object>} pluginInfos - Array of plugin info objects.
* @memberof module:lib/config
*/
function registerPluginConfigGroups(pluginInfos) {
@@ -362,6 +363,7 @@ function finalizeConfigs() {
* Can be passed to yargs.command to implement a config command. Use
* handleConfigCommand to do the requested action.
*
* @param {Object} yargs - yargs command builder.
* @memberof module:lib/config
*/
function configCommand(yargs) {
@@ -379,6 +381,9 @@ function configCommand(yargs) {
*
* Handle the actions that are made available by configCommand.
*
* @param {Object} args - yargs args object.
* @param {module:lib/config.Config} instance - Config instance.
* @param {string} configPath - Path to configuration file.
* @memberof module:lib/config
*/
async function handleConfigCommand(args, instance, configPath) {
@@ -438,4 +443,4 @@ module.exports = {
finalizeConfigs,
configCommand,
handleConfigCommand,
}
};
+2 -1
View File
@@ -3,7 +3,8 @@
* @author Hornwitser
* @module lib/config
*/
"use strict";
module.exports = {
...require("./classes"),
...require("./definitions"),
}
};
+5 -6
View File
@@ -21,7 +21,7 @@ const { basicType } = require("lib/helpers");
function mapToObject(map) {
let obj = {};
for (let [key, value] of map) {
if (typeof key !== 'string') {
if (typeof key !== "string") {
throw new Error(
`Expected all keys to be string but got ${typeof key}`
);
@@ -105,11 +105,11 @@ async function loadJsonArrayAsMap(filePath) {
let map = new Map();
for (let element of parsed) {
if (basicType(element) !== "object") {
throw new Error(`Expected all elements to be objects`);
throw new Error("Expected all elements to be objects");
}
if (element.id === undefined) {
throw new Error(`Expected all elements to have an id property`);
throw new Error("Expected all elements to have an id property");
}
map.set(element.id, element);
@@ -144,7 +144,6 @@ async function saveMapAsJsonArray(filePath, map) {
}
function checkName(name) {
if (typeof name !== "string") {
throw new Error("name must be a string");
@@ -232,7 +231,7 @@ class ItemDatabase {
checkName(name);
if (!this._items.has(name)) {
return 0
return 0;
}
return this._items.get(name);
@@ -287,4 +286,4 @@ module.exports = {
loadJsonArrayAsMap,
saveMapAsJsonArray,
ItemDatabase,
}
};
+2 -1
View File
@@ -2,6 +2,7 @@
* Errors thrown in Clusterio
* @module
*/
"use strict";
/**
* Thrown by commands when they fail
@@ -71,4 +72,4 @@ module.exports = {
StartupError,
EnvironmentError,
PluginError,
}
};
+11 -6
View File
@@ -21,7 +21,7 @@ const buildMod = require("../../build-mod");
*/
async function generateExportMod(server) {
let dependencies = [];
let splitter = /^(.*)_(\d+\.\d+\.\d+)(\.zip)?$/
let splitter = /^(.*)_(\d+\.\d+\.\d+)(\.zip)?$/;
for (let entry of await fs.readdir(server.writePath("mods"))) {
let match = splitter.exec(entry);
if (match && match[1] !== "export") {
@@ -106,8 +106,11 @@ async function loadFile(server, modVersions, modPath) {
*
* @param {module:lib/factorio.FactorioServer} server -
* The server to generate the export mod for.
* @param {Map<string, string>} modVersions -
* Mapping of mod name to versions to get icons from.
* @param {Array<Object>} items - Array of item prototypes.
* @return {itemSheet: Jimp, itemData: Map<string, Object>} Item spritesheet and metadata.
* @return {{itemSheet: Jimp, itemData: Map<string,Object>}}
* Item spritesheet and metadata.
* @memberof module:lib/factorio
* @private
* @inner
@@ -193,6 +196,7 @@ async function exportItems(server, modVersions, items) {
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;
@@ -248,7 +252,7 @@ async function exportItems(server, modVersions, items) {
iconSheet.crop(0, 0, width, Math.ceil(pos / (width / size)) * size);
return {iconSheet, itemData};
return { iconSheet, itemData };
}
/**
@@ -259,9 +263,10 @@ async function exportItems(server, modVersions, items) {
*
* @param {module:lib/factorio.FactorioServer} server -
* The server to export the locale from.
* @param modVersions {Map<string, string>} modVersions -
* @param {Map<string, string>} modVersions -
* Mapping of mod name to version to export locale from.
* @param modOrder {Array<string>} modOrder - Load order of the mods.
* @param {Array<string>} modOrder - Load order of the mods.
* @param {string} languageCode - Language to export locale for.
* @returns {Map<string, string>} merged locale information
* @memberof module:lib/factorio
* @private
@@ -338,7 +343,7 @@ async function exportData(server) {
throw new Error("No items got exported");
}
let {iconSheet, itemData} = await exportItems(server, modVersions, items);
let { iconSheet, itemData } = await exportItems(server, modVersions, items);
let locale = await exportLocale(server, modVersions, modOrder, "en");
// Free up the memory used by zip files loaded during the export.
+3 -2
View File
@@ -3,8 +3,9 @@
* @module lib/factorio
* @author Hornwitser
*/
"use strict";
module.exports = {
...require("lib/factorio/export"),
...require('lib/factorio/patch'),
...require('lib/factorio/server'),
...require("lib/factorio/patch"),
...require("lib/factorio/server"),
};
+18 -16
View File
@@ -27,6 +27,7 @@ const knownScenarios = {
* Returns the name of the folder that all files in the zip file is
* contained in. Throws an error if there are multiple such folders.
*
* @param {module:jszip.JSZip} zip - Zip to search through.
* @returns {string} name of the root folder.
* @memberof module:lib/factorio
* @private
@@ -44,12 +45,12 @@ function findRoot(zip) {
if (root === null) {
root = pathRoot;
} else if (root !== pathRoot) {
throw new Error(`Zip contains multiple root folders`);
throw new Error("Zip contains multiple root folders");
}
});
if (root === null) {
throw new Error(`Empty zip file`);
throw new Error("Empty zip file");
}
return root;
@@ -60,19 +61,20 @@ function findRoot(zip) {
* Generates control.lua code for loading the Clusterio modules
*
* @param {Object} patchInfo - The patch info files's json content
* @returns {string} Generated control.lua code.
* @memberof module:lib/factorio
* @private
* @inner
*/
function generateLoader(patchInfo) {
let lines = [
'-- Auto generated scenario module loader created by Clusterio',
'-- Modifications to this file will be lost when loaded in Clusterio',
"-- Auto generated scenario module loader created by Clusterio",
"-- Modifications to this file will be lost when loaded in Clusterio",
`clusterio_patch_number = ${patchInfo.patch_number}`,
'',
"",
'local event_handler = require("event_handler")',
'',
'-- Scenario modules',
"",
"-- Scenario modules",
];
for (let moduleName of patchInfo["scenario"]["modules"]) {
@@ -80,8 +82,8 @@ function generateLoader(patchInfo) {
}
lines.push(...[
'',
'-- Clusterio modules',
"",
"-- Clusterio modules",
]);
for (let module of patchInfo["modules"]) {
@@ -97,9 +99,9 @@ function generateLoader(patchInfo) {
}
// End last line with a newline
lines.push('');
lines.push("");
return lines.join('\n');
return lines.join("\n");
}
/**
@@ -188,7 +190,7 @@ function reorderDependencies(modules) {
if (cycle.includes(module.name)) {
cycle.push(module.name);
cycle.splice(0, cycle.indexOf(module.name))
cycle.splice(0, cycle.indexOf(module.name));
throw new Error(`Module dependency loop detected: ${cycle.join(" -> ")}`);
}
@@ -251,9 +253,9 @@ async function patch(savePath, modules) {
let moduleInfo = {
"name": module.name,
"files": [],
}
};
let dirs = [[module.path, '']];
let dirs = [[module.path, ""]];
while (dirs.length) {
let [dir, relativeDir] = dirs.pop();
for (let entry of await fs.readdir(dir, { withFileTypes: true })) {
@@ -282,7 +284,7 @@ async function patch(savePath, modules) {
root.file("clusterio.json", JSON.stringify(patchInfo, null, 4));
// Write back the save
let stream = zip.generateNodeStream({compression: "DEFLATE"});
let stream = zip.generateNodeStream({ compression: "DEFLATE" });
let pipe = stream.pipe(fs.createWriteStream(savePath));
await events.once(pipe, "finish");
}
@@ -294,4 +296,4 @@ module.exports = {
_findRoot: findRoot,
_generateLoader: generateLoader,
_reorderDependencies: reorderDependencies,
}
};
+112 -99
View File
@@ -16,6 +16,7 @@ const errors = require("lib/errors");
* Determines the version of Factorio the datadir is pointing to by
* reading the changelog.txt in it.
*
* @param {string} changelogPath - Path to changelog.txt.
* @memberof module:lib/factorio
* @private
* @inner
@@ -47,6 +48,9 @@ async function getVersion(changelogPath) {
/**
* Comparison function for sorting Factorio version strings
*
* @param {string} a - Version to compare.
* @param {string} b - Version to compare.
* @returns {number} 1 if a < b, 0 if a = b and -1 if a > b.
* @memberof module:lib/factorio
* @private
* @inner
@@ -74,6 +78,11 @@ function versionOrder(a, b) {
* Searches the given factorio dir for an installation of Factorio of
* the target version.
*
* @param {string} factorioDir - Path to Factorio installation dir(s).
* @param {string} targetVersion -
* Version to look for, supports the special value "latest" for the
* latest version available.
* @returns {Array} Array with path to data dir and version found.
* @memberof module:lib/factorio
* @private
* @inner
@@ -140,7 +149,7 @@ function randomDynamicPort() {
const start = 49152;
const end = 65535 + 1;
return Math.floor(Math.random() * (end - start) + start)
return Math.floor(Math.random() * (end - start) + start);
}
/**
@@ -157,10 +166,10 @@ function randomDynamicPort() {
*/
async function generatePassword(length) {
function validChar(byte) {
const ranges = ['az', 'AZ', '09'];
return ranges.some(range =>
const ranges = ["az", "AZ", "09"];
return ranges.some(range => (
range.codePointAt(0) <= byte && byte <= range.codePointAt(1)
);
));
}
let randomBytesAsync = util.promisify(crypto.randomBytes);
@@ -206,7 +215,7 @@ class LineSplitter {
*/
constructor(callback) {
this._callback = callback;
this._partial = null
this._partial = null;
}
/**
@@ -278,12 +287,12 @@ function parseOutput(line, source) {
let output = {
source,
received: Date.now(),
}
};
// There are three broad categories of output from Factorio, the first kind
// starts with the seconds since the server started and has a format of
// " 0.704 message"
const secRegex = /^ {0,3}(\d+\.\d+) (.*)$/
const secRegex = /^ {0,3}(\d+\.\d+) (.*)$/;
let secMatch = secRegex.exec(line);
if (secMatch) {
output.format = "seconds";
@@ -293,7 +302,7 @@ function parseOutput(line, source) {
// A seconds output has two different kinds of formats: The first is a
// log level and source location and has a format of
// "Level File.cpp:123: message"
const secLogRegex = /^(\w+) (\w+\.cpp:\d+): (.*)$/
const secLogRegex = /^(\w+) (\w+\.cpp:\d+): (.*)$/;
let secLogMatch = secLogRegex.exec(secContent);
if (secLogMatch) {
output.type = "log";
@@ -310,7 +319,7 @@ function parseOutput(line, source) {
// The second category of output starts with a date stamp of the format
// "yyyy-mm-dd hh:mm:ss message"
} else {
const dateRegex = /^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d) (.*)$/
const dateRegex = /^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d) (.*)$/;
let dateMatch = dateRegex.exec(line);
if (dateMatch) {
output.format = "date";
@@ -319,7 +328,7 @@ function parseOutput(line, source) {
// A date output has two general formats. The first is an action
// followed by a message and has a format of "[ACTION] message"
const dateActionRegex = /^\[(\w+)\] (.*)$/
const dateActionRegex = /^\[(\w+)\] (.*)$/;
let dateActionMatch = dateActionRegex.exec(dateContent);
if (dateActionMatch) {
output.type = "action";
@@ -355,29 +364,29 @@ function parseOutput(line, source) {
const outputHeuristics = [
// Message indicating the RCON interface has started
{
filter: { type: 'log', message: /^Starting RCON interface/ },
filter: { type: "log", message: /^Starting RCON interface/ },
action: function(output) {
this._startRcon().catch((err) => { this.emit('error', err); });
this._startRcon().catch((err) => { this.emit("error", err); });
},
},
// Message indicating the server is done starting up
{
filter: { type: 'log', message: /^updateTick\(\d+\) changing state from\(CreatingGame\) to\(InGame\)$/ },
filter: { type: "log", message: /^updateTick\(\d+\) changing state from\(CreatingGame\) to\(InGame\)$/ },
action: function(output) {
this._notifyGameReady().catch((err) => { this.emit('error', err); });
}
this._notifyGameReady().catch((err) => { this.emit("error", err); });
},
},
// Message indicating the server has started an autosave
{
filter: {
type: 'log',
type: "log",
message: /^Saving to _autosave\d+ \((non-)?blocking\)\.$/,
},
action: function(output) {
let name = /^Saving to (_autosave\d+) /.exec(output.message)[1];
this.emit('_autosave', name);
this.emit("_autosave", name);
},
},
@@ -395,7 +404,7 @@ const outputHeuristics = [
// Message indicating the server has finished saving
{
filter: {
type: 'log',
type: "log",
message: [
"Saving finished",
@@ -404,18 +413,18 @@ const outputHeuristics = [
],
},
action: function(output) {
this.emit('_saved');
}
this.emit("_saved");
},
},
// Message indicating the server is shutting down
{
filter: {
type: 'generic',
message: /^Quitting: /
type: "generic",
message: /^Quitting: /,
},
action: function(output) {
this.emit('_quitting');
this.emit("_quitting");
// On windows the Factorio server will wait for input on the console input buffer
// when an error occurs. Since we can't send input there we terminate the server
@@ -423,14 +432,14 @@ const outputHeuristics = [
if (process.platform === "win32" && output.message === "Quitting: multiplayer error.") {
this._server.kill();
}
}
},
},
// Messages that might be tha cause of an unexpected shutdown.
{
filter: {
type: 'log',
level: 'Error',
type: "log",
level: "Error",
message: [
"MultiplayerManager failed: \"Binding IPv4 socket failed: Permission denied\"",
"MultiplayerManager failed: Host address is already in use.",
@@ -442,8 +451,8 @@ const outputHeuristics = [
},
{
filter: {
type: 'log',
level: 'Error',
type: "log",
level: "Error",
message: [
"Can't bind socket: Address already in use",
"Can't bind socket: Permission denied",
@@ -515,22 +524,22 @@ class FactorioServer extends events.EventEmitter {
// Array of possible causes for an unexpected shutdown
this._unexpected = [];
this._stdout = new LineSplitter((line) => { this._handleOutput(line, 'stdout'); });
this._stderr = new LineSplitter((line) => { this._handleOutput(line, 'stderr'); });
this._stdout = new LineSplitter((line) => { this._handleOutput(line, "stdout"); });
this._stderr = new LineSplitter((line) => { this._handleOutput(line, "stderr"); });
// Track autosaving
this._runningAutosave = null;
this.on("_autosave", function(name) {
this.emit("autosave-start", name);
this._runningAutosave = name;
})
});
this.on("_saved", function() {
if (this._runningAutosave) {
this.emit("autosave-finished", this._runningAutosave);
this._runningAutosave = null;
}
})
});
}
_check(expectedStates) {
@@ -542,40 +551,40 @@ class FactorioServer extends events.EventEmitter {
}
async _handleIpc(line) {
let channelEnd = line.indexOf('?');
let channelEnd = line.indexOf("?");
if (channelEnd === -1) {
throw new Error(`Malformed IPC line "${line.toString()}"`);
}
let channel = line
.slice(6, channelEnd)
.toString('utf-8')
.replace(/\\x([0-9a-f]{2})/g, (match, p1) =>
.toString("utf-8")
.replace(/\\x([0-9a-f]{2})/g, (match, p1) => (
String.fromCharCode(parseInt(p1, 16))
)
))
;
let type = line.slice(channelEnd + 1, channelEnd + 2).toString('utf-8');
let type = line.slice(channelEnd + 1, channelEnd + 2).toString("utf-8");
let content;
if (type === 'j') {
content = JSON.parse(line.slice(channelEnd + 2).toString('utf-8'));
if (type === "j") {
content = JSON.parse(line.slice(channelEnd + 2).toString("utf-8"));
} else if (type === 'f') {
let fileName = line.slice(channelEnd + 2).toString('utf-8');
let filePath = this.writePath('script-output', fileName);
} else if (type === "f") {
let fileName = line.slice(channelEnd + 2).toString("utf-8");
let filePath = this.writePath("script-output", fileName);
// Prevent malicious names
if (/[\\/\0]/.test(fileName)) {
throw new Error(`Invalid IPC file name '${fileName}'`)
throw new Error(`Invalid IPC file name '${fileName}'`);
}
if (fileName.endsWith('.json')) {
content = JSON.parse(await fs.readFile(filePath, 'utf-8'));
if (fileName.endsWith(".json")) {
content = JSON.parse(await fs.readFile(filePath, "utf-8"));
await fs.unlink(filePath);
} else {
let format = fileName.slice(fileName.lastIndexOf('.') + 1);
throw new Error(`Unknown IPC file format '${format}'`)
let format = fileName.slice(fileName.lastIndexOf(".") + 1);
throw new Error(`Unknown IPC file format '${format}'`);
}
} else {
@@ -589,13 +598,13 @@ class FactorioServer extends events.EventEmitter {
_handleOutput(line, source) {
if (line.slice(0, 6).equals(Buffer.from("\f$ipc:"))) {
this._handleIpc(line).catch(err => this.emit('error', err));
this._handleIpc(line).catch(err => this.emit("error", err));
return;
}
this.emit(source, line);
let output = parseOutput(line.toString('utf-8'), source);
let output = parseOutput(line.toString("utf-8"), source);
heuristicLoop: for (let heuristic of outputHeuristics) {
for (let [name, expected] of Object.entries(heuristic.filter)) {
if (!Object.hasOwnProperty.call(output, name)) {
@@ -610,10 +619,8 @@ class FactorioServer extends events.EventEmitter {
if (!expected.includes(output[name])) {
continue heuristicLoop;
}
} else {
if (expected !== output[name]) {
continue heuristicLoop;
}
} else if (expected !== output[name]) {
continue heuristicLoop;
}
}
@@ -621,7 +628,7 @@ class FactorioServer extends events.EventEmitter {
heuristic.action.call(this, output);
}
this.emit('output', output);
this.emit("output", output);
}
async _startRcon() {
@@ -642,8 +649,8 @@ class FactorioServer extends events.EventEmitter {
};
this._rconClient = new rconClient.Rcon(config);
this._rconClient.on("error", err => { /* Ignore */ });
this._rconClient.on('authenticated', () => { this._rconReady = true; this.emit('rcon-ready'); });
this._rconClient.on("error", () => { /* Ignore */ });
this._rconClient.on("authenticated", () => { this._rconReady = true; this.emit("rcon-ready"); });
this._rconClient.on("end", () => {
this._rconClient = null;
this._rconReady = false;
@@ -685,37 +692,37 @@ class FactorioServer extends events.EventEmitter {
}
_attachStdio() {
this._server.stdout.on('data', chunk => { this._stdout.data(chunk); });
this._server.stdout.on('close', () => { this._stdout.end(); });
this._server.stderr.on('data', chunk => { this._stderr.data(chunk); });
this._server.stderr.on('close', () => { this._stderr.end(); });
this._server.stdout.on("data", chunk => { this._stdout.data(chunk); });
this._server.stdout.on("close", () => { this._stdout.end(); });
this._server.stderr.on("data", chunk => { this._stderr.data(chunk); });
this._server.stderr.on("close", () => { this._stderr.end(); });
}
_watchExit() {
this._server.on('exit', (code, signal) => {
this._server.on("exit", (code, signal) => {
if (this._state !== "stopping") {
if (this._unexpected.length === 0) {
if (signal === 'SIGKILL') {
this.emit('error', new errors.EnvironmentError(
if (signal === "SIGKILL") {
this.emit("error", new errors.EnvironmentError(
"Factorio server was unexpectedly killed, is the system low on memory?"
));
} else {
let msg;
if (code !== null) {
msg = `Factorio server unexpectedly shut down with code ${code}`
msg = `Factorio server unexpectedly shut down with code ${code}`;
} else {
msg = `Factorio server was unexpectedly shut by down by signal ${signal}`;
}
this.emit('error', new Error(msg));
this.emit("error", new Error(msg));
}
} else if (this._unexpected.length === 1) {
this.emit('error', new Error(this._unexpected[0]));
this.emit("error", new Error(this._unexpected[0]));
} else {
this.emit('error', new Error(
this.emit("error", new Error(
"Factorio server unexpectedly shut down. Possible causes:\n- "+
this._unexpected.join("\n- ")
));
@@ -737,12 +744,12 @@ class FactorioServer extends events.EventEmitter {
async _notifyGameReady() {
this._gameReady = true;
this.emit('game-ready');
this.emit("game-ready");
}
async _waitForReady() {
if (!this._gameReady) {
await events.once(this, 'game-ready');
await events.once(this, "game-ready");
}
}
@@ -761,14 +768,14 @@ class FactorioServer extends events.EventEmitter {
await this._writeConfigIni();
this._server = child_process.spawn(
this.dataPath('..', 'bin', 'x64', 'factorio'),
this.dataPath("..", "bin", "x64", "factorio"),
[
'--config', this.writePath('config.ini'),
'--create', this.writePath('saves', name),
"--config", this.writePath("config.ini"),
"--create", this.writePath("saves", name),
],
{
detached: true,
stdio: 'pipe',
stdio: "pipe",
}
);
@@ -776,7 +783,7 @@ class FactorioServer extends events.EventEmitter {
this._server.once("exit", () => this.emit("exit"));
try {
let [code, signal] = await events.once(this._server, 'exit');
let [code, signal] = await events.once(this._server, "exit");
if (code !== 0) {
throw new Error(`Factorio exited with status ${code}`);
}
@@ -799,17 +806,17 @@ class FactorioServer extends events.EventEmitter {
await this._writeConfigIni();
this._server = child_process.spawn(
this.dataPath('..', 'bin', 'x64', 'factorio'),
this.dataPath("..", "bin", "x64", "factorio"),
[
'--config', this.writePath('config.ini'),
'--start-server', this.writePath('saves', save),
'--port', this.gamePort,
'--rcon-port', this.rconPort,
'--rcon-password', this.rconPassword,
"--config", this.writePath("config.ini"),
"--start-server", this.writePath("saves", save),
"--port", this.gamePort,
"--rcon-port", this.rconPort,
"--rcon-password", this.rconPassword,
],
{
detached: true,
stdio: 'pipe',
stdio: "pipe",
}
);
@@ -832,17 +839,17 @@ class FactorioServer extends events.EventEmitter {
await this._writeConfigIni();
this._server = child_process.spawn(
this.dataPath('..', 'bin', 'x64', 'factorio'),
this.dataPath("..", "bin", "x64", "factorio"),
[
'--config', this.writePath('config.ini'),
'--start-server-load-scenario', scenario,
'--port', this.gamePort,
'--rcon-port', this.rconPort,
'--rcon-password', this.rconPassword,
"--config", this.writePath("config.ini"),
"--start-server-load-scenario", scenario,
"--port", this.gamePort,
"--rcon-port", this.rconPort,
"--rcon-password", this.rconPassword,
],
{
detached: true,
stdio: 'pipe',
stdio: "pipe",
}
);
@@ -865,13 +872,13 @@ class FactorioServer extends events.EventEmitter {
* @returns {string} response from server.
*/
async sendRcon(message, expectEmpty) {
this._check(["running", "stopping"])
this._check(["running", "stopping"]);
if (!this._rconReady) {
await events.once(this, 'rcon-ready');
await events.once(this, "rcon-ready");
}
let response = await this._rconClient.send(message);
if (expectEmpty && response !== '') {
if (expectEmpty && response !== "") {
throw new Error(`Expected empty response but got "${response}"`);
}
return response;
@@ -923,7 +930,7 @@ class FactorioServer extends events.EventEmitter {
// If RCON is not yet fully connected that operation needs to
// complete before the RCON connection can be closed cleanly.
if (!this._rconReady) {
await events.once(this, 'rcon-ready');
await events.once(this, "rcon-ready");
}
await this._rconClient.end();
@@ -942,16 +949,16 @@ class FactorioServer extends events.EventEmitter {
// stop if it's stuck with an infinite lua code loop. In either
// case there's no recovering from it.
if (process.platform !== "win32") {
this.on('_quitting', setAlive);
await events.once(this._server, 'exit');
this.on("_quitting", setAlive);
await events.once(this._server, "exit");
clearTimeout(timeoutId);
this.off('_quitting', setAlive);
this.off("_quitting", setAlive);
// On windows the process is terminated immediately, but to keep
// ordering wait until after the exit event here.
} else {
await events.once(this._server, 'exit');
await events.once(this._server, "exit");
}
}
@@ -981,14 +988,14 @@ class FactorioServer extends events.EventEmitter {
this._check(["running"]);
let check = await this.sendRcon("/sc rcon.print('disabled')");
if (check === 'disabled\n') {
if (check === "disabled\n") {
return false;
}
// Factorio will print a warning to the console and ask for the command
// to be runned again before disabling achievements and allowing commands.
check = await this.sendRcon("/sc rcon.print('disabled')");
if (check === 'disabled\n') {
if (check === "disabled\n") {
return true;
}
@@ -1001,6 +1008,9 @@ class FactorioServer extends events.EventEmitter {
* Creates a path using path.join with the given parts that's relative to
* the data directory of the Factorio server. Not valid before init has
* been called.
*
* @param {...string} parts - Extra parts to add to the data path.
* @returns {string} Data directory path.
*/
dataPath(...parts) {
return path.join(this._dataDir, ...parts);
@@ -1011,6 +1021,9 @@ class FactorioServer extends events.EventEmitter {
*
* Creates a path using path.join with the given parts that's relative to
* the write directory of the Factorio server.
*
* @param {...string} parts - Extra parts to add to the write path.
* @returns {string} Write directory path.
*/
writePath(...parts) {
return path.join(this._writeDir, ...parts);
@@ -1032,7 +1045,7 @@ class FactorioServer extends events.EventEmitter {
path: {
"read-data": this.dataPath(),
"write-data": this.writePath(),
}
},
});
await fs.outputFile(this.writePath("config.ini"), content);
}
+2 -1
View File
@@ -1,6 +1,7 @@
/**
* @module lib/fileOps
*/
"use strict";
const fs = require("fs-extra");
const path = require("path");
@@ -33,4 +34,4 @@ async function getNewestFile(directory, filter = (name) => true) {
module.exports = {
getNewestFile,
}
};
+2 -1
View File
@@ -1,2 +1,3 @@
"use strict";
const fileOps = require("./fileOps");
module.exports = fileOps;
module.exports = fileOps;
+3 -2
View File
@@ -1,3 +1,4 @@
"use strict";
const forge = require("node-forge");
const fs = require("fs-extra");
const util = require("util");
@@ -17,7 +18,7 @@ module.exports = async function({
log("Generating SSL certificate with "+bits+" bits at "+sslCertPath);
let startNs = hrtime();
let keypair = await util.promisify(forge.pki.rsa.generateKeyPair)({bits: bits, e: 0x10001});
let keypair = await util.promisify(forge.pki.rsa.generateKeyPair)({ bits: bits, e: 0x10001 });
log("Generated 2048bit keypair in: "+(Number(hrtime()-startNs) / 1e6)+" ms");
startNs = hrtime();
@@ -41,4 +42,4 @@ module.exports = async function({
await fs.outputFile(sslCertPath, forge.pki.certificateToPem(cert));
log("Generated certificate in: "+(Number(hrtime()-startNs) / 1e6)+" ms");
}
};
+9 -8
View File
@@ -2,25 +2,26 @@
* Hashing functions
* @module
*/
const crypto = require('crypto');
const fs = require('fs');
"use strict";
const crypto = require("crypto");
const fs = require("fs");
/**
* Returns a promise that resolves to the SHA1 hash of the stream given
*
* @param stream - Node stream of the content to hash.
* @param {Stream} stream - Node stream of the content to hash.
* @returns {Promise<string>} hash of the stream.
*/
function hashStream(stream) {
return new Promise(function(resolve, reject) {
let hasher = crypto.createHash('sha1')
hasher.setEncoding('hex')
hasher.on('finish', function() {
let hasher = crypto.createHash("sha1");
hasher.setEncoding("hex");
hasher.on("finish", function() {
resolve(hasher.read());
});
stream.on('error', function(error) {
stream.on("error", function(error) {
// the docs doesn't say anything about what's passed on error
reject(error);
});
@@ -42,4 +43,4 @@ function hashFile(path) {
module.exports = {
hashStream,
hashFile,
}
};
+3 -2
View File
@@ -2,6 +2,7 @@
* Collection of small utilites that are useful in multiple places.
* @module
*/
"use strict";
/**
* Return a string describing the type of the value passed
@@ -9,7 +10,7 @@
* Works the same as typeof, excpet that null and array types get their
* own string.
*
* @param value - value to return the type of.
* @param {*} value - value to return the type of.
* @returns {string} basic type of the value passed.
*/
function basicType(value) {
@@ -20,4 +21,4 @@ function basicType(value) {
module.exports = {
basicType,
}
};
+17 -19
View File
@@ -94,7 +94,9 @@ class WebSocketBaseConnector extends events.EventEmitter {
/**
* Send a message over the socket
*
* @returns the sequence number of the message sent
* @param {string} type - Message type to send.
* @param {Object} data - Data to send with message.
* @returns {number} the sequence number of the message sent
*/
send(type, data) {
if (!["handshake", "connected", "closing"].includes(this._state)) {
@@ -181,6 +183,9 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
*
* Used in the register function in order to send the register handshake
* message over the WebSocket.
*
* @param {string} type - Message type to send.
* @param {Object} data - Data to send with message.
*/
sendHandshake(type, data) {
this._check(["handshake"]);
@@ -191,6 +196,9 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
* Close the connection with the given reason.
*
* Sends a close frame and disconnects the connector.
*
* @param {number} code - WebSocket close code.
* @param {string} reason - WebSocket close reason.
*/
async close(code, reason) {
if (this._state === "new") {
@@ -226,7 +234,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
url.pathname += "api/socket";
// Open WebSocket to master
console.log(`SOCKET | connecting to ${url}`)
console.log(`SOCKET | connecting to ${url}`);
this._socket = new WebSocket(url, {
// For now we do not verify TLS certificates since the default setup is
@@ -261,7 +269,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
}
let delay = Math.random() * this._reconnectDelay;
console.log(`SOCKET | waiting ${delay.toLocaleString("en", { maximumFractionDigits: 2 })} seconds for reconnect`)
console.log(`SOCKET | waiting ${delay.toLocaleString("en", { maximumFractionDigits: 2 })} seconds for reconnect`);
this._reconnectId = setTimeout(() => {
this._reconnectId = null;
this._doConnect();
@@ -302,18 +310,6 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
console.error("SOCKET | Error:", err);
});
/*
this._socket.on("unexpected-response", (req, res) => {
let data = "";
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
console.error(`SOCKET | Unexpeced Response ${res.statusCode}`);
console.error(data);
this._socket.emit("error", new Error(`Unexpected Response ${res.statusCode}`));
});
});
*/
this._socket.on("open", () => {
console.log("SOCKET | Open");
});
@@ -356,7 +352,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
}
let { seq, type, data } = message;
if (type == 'hello') {
if (type == "hello") {
console.log(`SOCKET | received hello from master version ${data.version}`);
this.emit("hello", data);
if (this._sessionToken) {
@@ -369,7 +365,7 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
this.register();
}
} else if (type === 'ready') {
} else if (type === "ready") {
console.log("SOCKET | received ready from master");
this._state = "connected";
this._sessionToken = data.session_token;
@@ -449,10 +445,12 @@ class VirtualConnector extends events.EventEmitter {
/**
* Send a message to the other end of the connector
*
* @returns the sequence number of the message sent
* @param {string} type - Message type to send.
* @param {Object} data - Data to send with message.
* @returns {number} the sequence number of the message sent
*/
send(type, data) {
this.other.emit('message', { seq: this._seq, type, data });
this.other.emit("message", { seq: this._seq, type, data });
return this._seq++;
}
}
+4 -3
View File
@@ -3,8 +3,9 @@
* @module lib/link
* @author Hornwitser
*/
"use strict";
module.exports = {
...require('lib/link/link'),
...require('lib/link/messages'),
...require('lib/link/connectors'),
...require("lib/link/link"),
...require("lib/link/messages"),
...require("lib/link/connectors"),
};
+10 -4
View File
@@ -30,17 +30,17 @@ class Link {
this._validators = new Map();
// Process messages received by the connector
connector.on('message', payload => {
connector.on("message", payload => {
try {
this.processMessage(payload);
} catch (err) {
if (err instanceof errors.InvalidMessage) {
console.error(`Invalid message on ${this.source}-${this.target} link: ${err.message}`)
console.error(`Invalid message on ${this.source}-${this.target} link: ${err.message}`);
if (err.errors) {
console.error(err.errors);
}
} else {
this.connector.emit('error', err);
this.connector.emit("error", err);
}
}
});
@@ -72,6 +72,7 @@ class Link {
* Validates and invokes the handler and/or waiters for a message that has
* been received. An unhandled message is considered to be an error.
*
* @param {Object} message - Message to process.
* @throws {module:lib/errors.InvalidMessage}
* if validation failed, is missing, the message is invalid, or no
* handlers/waiters were invoked to handle the message.
@@ -102,6 +103,7 @@ class Link {
/**
* Invoke the handler for this message if it exists
*
* @param {Object} message - Message to invoke handlers on.
* @returns {Boolean} true if a handler was invoked.
*/
_processHandler(message) {
@@ -117,6 +119,7 @@ class Link {
/**
* Invokes the waiters for this message if any
*
* @param {Object} message - Message to invoke waiters on.
* @returns {Boolean} true if a waiter was triggered.
*/
_processWaiters(message) {
@@ -196,6 +199,9 @@ class Link {
* as any properties specified in data. For example specifying { foo:
* 21 } as the data will cause it to wait until a message with the right
* type and a foo property value of 21 in the data payload.
*
* @param {string} type - Message type to wait for.
* @param {Object} data - Properties to match in the message data.
*/
async waitFor(type, data) {
if (!this._validators.has(type)) {
@@ -243,4 +249,4 @@ class Link {
module.exports = {
Link,
}
};
+76 -68
View File
@@ -11,8 +11,8 @@ const errors = require("lib/errors");
class Message {
/**
* Name of the plugin this message belongs to, or null.
* @returns {?string}
**/
* @returns {?string} - plugin name or null if not from a plugin.
*/
get plugin() {
if (this._plugin === undefined) {
let index = this.type.indexOf(":");
@@ -67,8 +67,8 @@ class Request extends Message {
this.permission = permission || null;
this.forwardTo = forwardTo;
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`);
@@ -77,10 +77,11 @@ class Request extends Message {
throw new Error(`permission is not allowed on ${this.type} request as it is not over control-master link`);
}
if (forwardTo === 'instance') {
requestProperties = Object.assign(
{"instance_id": { type: "integer" }}, requestProperties
);
if (forwardTo === "instance") {
requestProperties = {
"instance_id": { type: "integer" },
...requestProperties,
};
} else if (forwardTo !== "master" && forwardTo !== null) {
throw new Error(`Invalid forwardTo value ${forwardTo}`);
@@ -122,7 +123,7 @@ class Request extends Message {
},
],
},
}
},
});
}
@@ -158,7 +159,7 @@ class Request extends Message {
// Use forwarder if handler is not present.
if (!handler) {
if (this.forwardTo === 'instance') {
if (this.forwardTo === "instance") {
handler = link.forwardRequestToInstance;
} else if (this.forwardTo === "master") {
handler = link.forwardRequestToMaster;
@@ -185,7 +186,7 @@ class Request extends Message {
response = {};
}
if (!this._responseValidator({ seq: 0, type: this.responseType, response})) {
if (!this._responseValidator({ seq: 0, type: this.responseType, response })) {
console.error(this._responseValidator.errors);
throw new Error(`Validation failed responding to ${this.requestType}`);
}
@@ -208,11 +209,13 @@ class Request extends Message {
* Sends the given data over the link as the request payload and waits
* for the response.
*
* @param {module:lib/link.Link} link - Link to send request over.
* @param {Object} data - Data to send with the request.
* @returns {object} response data
*/
async send(link, data = {}) {
// XXX validate link target/source?
if (!this._requestValidator({ seq: 0, type: this.requestType, data})) {
if (!this._requestValidator({ seq: 0, type: this.requestType, data })) {
console.error(this._requestValidator.errors);
throw new Error(`Validation failed sending ${this.requestType}`);
}
@@ -226,7 +229,7 @@ class Request extends Message {
}
let messages = {}
let messages = {};
// Connection requests
let wsLinks = ["master-control", "control-master", "master-slave", "slave-master"];
@@ -256,8 +259,8 @@ messages.debugDumpWs = new Request({
// Management requests
messages.listSlaves = new Request({
type: 'list_slaves',
links: ['control-master'],
type: "list_slaves",
links: ["control-master"],
permission: "core.slave.list",
responseProperties: {
"list": {
@@ -278,8 +281,8 @@ messages.listSlaves = new Request({
});
messages.listInstances = new Request({
type: 'list_instances',
links: ['control-master'],
type: "list_instances",
links: ["control-master"],
permission: "core.instance.list",
responseProperties: {
"list": {
@@ -324,8 +327,8 @@ messages.createSlaveConfig = new Request({
});
messages.setInstanceOutputSubscriptions = new Request({
type: 'set_instance_output_subscriptions',
links: ['control-master'],
type: "set_instance_output_subscriptions",
links: ["control-master"],
permission: "core.instance.follow_log",
requestProperties: {
"instance_ids": {
@@ -336,8 +339,8 @@ messages.setInstanceOutputSubscriptions = new Request({
});
messages.createInstance = new Request({
type: 'create_instance',
links: ['control-master'],
type: "create_instance",
links: ["control-master"],
permission: "core.instance.create",
requestProperties: {
"serialized_config": { type: "object" },
@@ -380,8 +383,8 @@ messages.setInstanceConfigProp = new Request({
});
messages.assignInstanceCommand = new Request({
type: 'assign_instance_command',
links: ['control-master'],
type: "assign_instance_command",
links: ["control-master"],
permission: "core.instance.assign",
requestProperties: {
"instance_id": { type: "integer" },
@@ -390,29 +393,29 @@ messages.assignInstanceCommand = new Request({
});
messages.assignInstance = new Request({
type: 'assign_instance',
links: ['master-slave'],
type: "assign_instance",
links: ["master-slave"],
requestProperties: {
"serialized_config": { type: "object" },
},
forwardTo: 'instance',
})
forwardTo: "instance",
});
messages.startInstance = new Request({
type: 'start_instance',
links: ['control-master', 'master-slave', 'slave-instance'],
type: "start_instance",
links: ["control-master", "master-slave", "slave-instance"],
permission: "core.instance.start",
requestProperties: {
"save": { type: ["string", "null"] },
},
forwardTo: 'instance',
forwardTo: "instance",
});
messages.createSave = new Request({
type: 'create_save',
links: ['control-master', 'master-slave', 'slave-instance'],
type: "create_save",
links: ["control-master", "master-slave", "slave-instance"],
permission: "core.instance.create_save",
forwardTo: 'instance',
forwardTo: "instance",
});
messages.exportData = new Request({
@@ -423,30 +426,30 @@ messages.exportData = new Request({
});
messages.stopInstance = new Request({
type: 'stop_instance',
links: ['control-master', 'master-slave', 'slave-instance'],
type: "stop_instance",
links: ["control-master", "master-slave", "slave-instance"],
permission: "core.instance.stop",
forwardTo: 'instance',
forwardTo: "instance",
});
messages.deleteInstance = new Request({
type: 'delete_instance',
links: ['control-master', 'master-slave'],
type: "delete_instance",
links: ["control-master", "master-slave"],
permission: "core.instance.delete",
forwardTo: 'instance',
forwardTo: "instance",
});
messages.sendRcon = new Request({
type: 'send_rcon',
links: ['control-master', 'master-slave', 'slave-instance'],
type: "send_rcon",
links: ["control-master", "master-slave", "slave-instance"],
permission: "core.instance.send_rcon",
forwardTo: 'instance',
forwardTo: "instance",
requestProperties: {
"command": { type: "string" },
},
responseProperties: {
"result": { type: "string" },
}
},
});
messages.listPermissions = new Request({
@@ -501,7 +504,7 @@ messages.createRole = new Request({
},
responseProperties: {
"id": { type: "integer" },
}
},
});
messages.updateRole = new Request({
@@ -583,8 +586,8 @@ messages.deleteUser = new Request({
});
messages.getMetrics = new Request({
type: 'statistics_exporter:get_metrics',
links: ['master-slave', 'slave-instance'],
type: "statistics_exporter:get_metrics",
links: ["master-slave", "slave-instance"],
responseProperties: {
"results": { type: "array" },
},
@@ -632,20 +635,21 @@ class Event extends Message {
this.forwardTo = forwardTo;
this.broadcastTo = broadcastTo;
if (forwardTo === 'instance') {
eventProperties = Object.assign(
{"instance_id": { type: "integer" }}, eventProperties
);
if (forwardTo === "instance") {
eventProperties = {
"instance_id": { type: "integer" },
...eventProperties,
};
} else if (forwardTo !== 'master' && forwardTo !== null) {
} else if (forwardTo !== "master" && forwardTo !== null) {
throw new Error(`Invalid forwardTo value ${forwardTo}`);
}
if (broadcastTo !== 'instance' && broadcastTo !== null) {
if (broadcastTo !== "instance" && broadcastTo !== null) {
throw new Error(`Invalid broadcastTo value ${broadcastTo}`);
}
this.eventType = type + '_event';
this.eventType = type + "_event";
this._eventValidator = schema.compile({
$schema: "http://json-schema.org/draft-07/schema#",
properties: {
@@ -685,17 +689,17 @@ class Event extends Message {
if (this.links.includes(`${link.target}-${link.source}`)) {
// Use forwarder if handler is not present.
if (!handler) {
if (this.forwardTo === 'instance') {
if (this.forwardTo === "instance") {
handler = link.forwardEventToInstance;
} else if (this.forwardTo === 'master') {
} else if (this.forwardTo === "master") {
handler = link.forwardEventToMaster;
}
}
if (
this.broadcastTo === 'instance'
this.broadcastTo === "instance"
&& [
'instance-slave', 'slave-master', 'control-master', 'master-slave'
"instance-slave", "slave-master", "control-master", "master-slave",
].includes(`${link.target}-${link.source}`)
) {
if (!handler) {
@@ -705,7 +709,7 @@ class Event extends Message {
handler = async (message, event) => {
await link.broadcastEventToInstance(message, event);
await originalHandler.call(link, message, event);
}
};
}
}
@@ -726,9 +730,12 @@ class Event extends Message {
* Send event over the given link
*
* Sends the given event data over the link.
*
* @param {module:lib/link.Link} link - Link to send event over.
* @param {Object} data - Data to send with the event.
*/
send(link, data = {}) {
if (!this._eventValidator({ seq: 0, type: this.eventType, data})) {
if (!this._eventValidator({ seq: 0, type: this.eventType, data })) {
console.error(this._eventValidator.errors);
throw new Error(`Validation failed sending ${this.eventType}`);
}
@@ -754,7 +761,7 @@ messages.instanceInitialized = new Event({
"plugins": {
type: "object",
additionalProperties: { type: "string" },
}
},
},
});
messages.instanceStarted = new Event({
@@ -765,7 +772,7 @@ messages.instanceStarted = new Event({
},
});
messages.instanceStopped = new Event({
type: 'instance_stopped',
type: "instance_stopped",
links: ["instance-slave", "slave-master"],
eventProperties: {
"instance_id": { type: "integer" },
@@ -773,8 +780,8 @@ messages.instanceStopped = new Event({
});
messages.updateInstances = new Event({
type: 'update_instances',
links: ['slave-master'],
type: "update_instances",
links: ["slave-master"],
eventProperties: {
"instances": {
type: "array",
@@ -792,9 +799,9 @@ messages.updateInstances = new Event({
});
messages.instanceOutput = new Event({
type: 'instance_output',
links: ['instance-slave', 'slave-master', 'master-control'],
forwardTo: 'master',
type: "instance_output",
links: ["instance-slave", "slave-master", "master-control"],
forwardTo: "master",
eventProperties: {
"instance_id": { type: "integer" },
"output": {
@@ -843,11 +850,12 @@ messages.playerEvent = new Event({
* link instance as the concatenation of the name of the message, the
* name of message class, and 'Handler'.
*
* @param {module:lib/link.Link} link - Link to attach messages to.
* @memberof module:lib/link
*/
function attachAllMessages(link) {
for (let [name, message] of Object.entries(messages)) {
message.attach(link, link[name + message.constructor.name + 'Handler']);
message.attach(link, link[name + message.constructor.name + "Handler"]);
}
}
@@ -860,4 +868,4 @@ module.exports = {
attachAllMessages,
messages,
}
};
+5 -1
View File
@@ -2,9 +2,13 @@
* Utilities for dealing with Lua
* @module
*/
"use strict";
/**
* Escapes a string for inclusion into a lua string
*
* @param {string} content - String to escape.
* @returns {string} Escaped string.
*/
function escapeString(content) {
return content
@@ -19,4 +23,4 @@ function escapeString(content) {
module.exports = {
escapeString,
}
};
+31 -26
View File
@@ -1,41 +1,45 @@
// This file will be rewritten from scratch
/* eslint-disable */
/**
Provides tools to manage mods in a clusterio install
@module
*/
const asTable = require("as-table").configure({delimiter: ' | '});
"use strict";
const asTable = require("as-table").configure({delimiter: " | "});
const moment = require("moment");
const syncRequest = require("sync-request");
const fs = require("fs-extra");
const https = require('follow-redirects').https;
const https = require("follow-redirects").https;
const factorioAPI = require("node-factorio-api");
const sLog = require("single-line-log").stdout;
const query = require('cli-interact').getYesNo;
const query = require("cli-interact").getYesNo;
const path = require("path");
const fileOps = require("lib/fileOps");
function ellipse(str, max){
return str.length > (max - 3) ? str.substring(0,max-3) + '...' : str;
return str.length > (max - 3) ? str.substring(0,max-3) + "..." : str;
}
// https://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
function formatBytes(size, decimals) {
if (bytes === 0) return '0 Bytes';
if (bytes === 0) return "0 Bytes";
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
}
/**
Lists mods for an instance as a table. Warning, handles printing internally.
@param {Object} config - Obsolete config
@param {Instance} instance - Instance object, see slave.js
@example
@@ -52,7 +56,7 @@ async function listMods(config, instance) {
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
require_ownership: false,
}).then(token => {
console.log("* Currently installed:");
factorioAPI.readModZips().then(list => {
@@ -63,14 +67,14 @@ async function listMods(config, instance) {
date:mod.date,
factorio_ver:mod.factorio_version,
author:mod.author,
}
};
})));
});
// Had issues earlier due to bug in node-factorio-api, see https://github.com/Danacus/node-factorio-api/issues/2
// bug is resolved, thanks to @Danacus and @brotherpot
factorioAPI.getModsFromSaves().then(list => {
console.log("* Mods used in save:");
console.log(asTable(list[0].mods))
console.log(asTable(list[0].mods));
});
}).catch(reason => {
console.log("Failed to authenticate to factorio.com! Please check your connection and the username+token used for authentication in config.json");
@@ -78,14 +82,14 @@ async function listMods(config, instance) {
});
} else {
// list shared mods in /sharedMods/
factorioAPI.init(false, 'sharedMods/', '', instance.server.version);
factorioAPI.init(false, "sharedMods/", "", instance.server.version);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
require_ownership: false,
}).then(() => {
console.log("\r\n")
console.log("* Mods shared by all instances:")
console.log("\r\n");
console.log("* Mods shared by all instances:");
factorioAPI.getInstalledMods().then(mods => {
console.log(asTable(mods));
resolve();
@@ -108,6 +112,7 @@ async function listMods(config, instance) {
Searches mods.factorio.com for mods matching a search term and prints
the results to console as a neat looking table
@param {Object} config - Obsolete config
@param {string} searchTerm What to search for
*/
async function findMods(config, searchTerm) {
@@ -116,7 +121,7 @@ async function findMods(config, searchTerm) {
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
require_ownership: false,
}).then(token => {
factorioAPI.searchMods(searchTerm).then(mods => {
let result = asTable(mods.reverse().map(mod => {
@@ -128,7 +133,7 @@ async function findMods(config, searchTerm) {
latest_version: mod.latest_release.version,
owner:mod.owner,
//license: mod.license_name,
}
};
}));
resolve(result);
// console.log(result);
@@ -154,7 +159,7 @@ async function addMod(config, term, instance) {
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
require_ownership: false,
}).then(() => {
factorioAPI.searchMods(term.split("/")[term.split("/").length-1]).then(body => {
console.log("Im gonna download: "+body[0].name + " in version: " + body[0].latest_release.version);
@@ -168,7 +173,7 @@ async function addMod(config, term, instance) {
// treat it as a github releases download
let apiURL = "https://api.github.com/repos/" + term.split("/")[3] + "/" + term.split("/")[4] + "/releases"; // https://api.github.com/repos/Danielv123/factorioClusterioMod/releases
let res = syncRequest('GET', apiURL, {"headers":{"User-Agent":"factorioClusterio"}});
let res = syncRequest("GET", apiURL, {"headers":{"User-Agent":"factorioClusterio"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
if (url) {
@@ -177,7 +182,7 @@ async function addMod(config, term, instance) {
let bytesDownloaded = 0;
let downloadStartTime = Date.now();
https.get(url, function(response) {
let bytesToDownload = response.headers['content-length'];
let bytesToDownload = response.headers["content-length"];
response.pipe(file);
response.on("data", chunk => {
bytesDownloaded += chunk.length;
@@ -188,7 +193,7 @@ async function addMod(config, term, instance) {
"about "+timeToComplete+" seconds remaining ("+formatBytes(bytesPerSecond)+"/s)");
}).on("end", x => {
sLog.clear();
console.log(" Downloaded in "+moment.utc(moment.duration(Date.now()-downloadStartTime).as('milliseconds')).format('mm:ss')); // because sLog.clear() doesn't seem to do the job of ending the line properly
console.log(" Downloaded in "+moment.utc(moment.duration(Date.now()-downloadStartTime).as("milliseconds")).format("mm:ss")); // because sLog.clear() doesn't seem to do the job of ending the line properly
resolve();
});
});
@@ -199,10 +204,10 @@ async function addMod(config, term, instance) {
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
require_ownership: false,
}).then(() => {
factorioAPI.searchMods(term).then(body => {
console.log(body)
console.log(body);
console.log("Im gonna download: "+body[0].name + " in version: " + body[0].latest_release.version);
factorioAPI.downloadMod( {name: body[0].name, version: body[0].latest_release.version}).then(()=>{
console.log("Mod downloaded");
@@ -242,7 +247,7 @@ async function removeMod(config, term, instance, force = false) {
fs.unlinkSync(path.join(modDirectory, modsMatchingFilter[i]));
console.log("Deleted "+modsMatchingFilter[i]);
}
} else console.log("Did not delete mods.")
} else console.log("Did not delete mods.");
}
resolve();
} else {
@@ -261,10 +266,10 @@ the latest release, saving it to /sharedMods
function downloadFromGitURL(url) {
console.log("Downloading mod from "+url);
// get JSON data about releases
let res = syncRequest('GET', url, {"headers":{"User-Agent":"factorioClusterio"}});
let res = syncRequest("GET", url, {"headers":{"User-Agent":"factorioClusterio"}});
let downloadUrl = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
console.log(JSON.parse(res.getBody()))
console.log(JSON.parse(res.getBody()));
if (downloadUrl) {
console.log(downloadUrl);
let file = fs.createWriteStream("sharedMods/"+name);
@@ -291,4 +296,4 @@ module.exports = {
addMod,
removeMod,
downloadFromGitURL,
}
};
+8 -7
View File
@@ -1,6 +1,7 @@
var http = require('http');
var https = require('https');
var fs = require('fs-extra');
"use strict";
var http = require("http");
var https = require("https");
var fs = require("fs-extra");
function makeDirectory(path){
@@ -25,7 +26,7 @@ function download(url, file, callback){
// this might be an invalid path!
fs.stat(pathArray.join("/"), (err, stat) => {
if(err){
makeDirectory(pathArray.join("/")).then(()=>{
makeDirectory(pathArray.join("/")).then(() => {
doDownload(url, file, callback);
});
} else {
@@ -76,12 +77,12 @@ let downloadList = [
url: "https://fonts.googleapis.com/css?family=Roboto",
file: "static/external/css/Roboto.css",
},
]
];
// initiate folder creations and downloads
function processMkdirRecursive(callback){
let item = directories.shift();
makeDirectory(item).then(()=>{
makeDirectory(item).then(() => {
if(directories.length == 0){
if(callback) callback();
} else {
@@ -91,7 +92,7 @@ function processMkdirRecursive(callback){
}
function processDownloadRecursive(callback){
let item = downloadList.shift();
download(item.url, item.file, item.callback).then(()=>{
download(item.url, item.file, item.callback).then(() => {
if(downloadList.length == 0){
if(callback) callback();
} else {
+29 -27
View File
@@ -1,37 +1,39 @@
/**
Helper functions related to objects
@module
*/
* Helper functions related to objects
* @module
*/
"use strict";
/**
Deep clones an object, is actually just JSON.parse(JSON.stringify()) but looks cleaner.
@param {object} obj to clone
@returns {object}
@example
a = {"thingy":"with a value"};
b = a;
a == b; // true
c = objectOps.deepclone(a);
a == c; // false
*/
* Deep clones an object, is actually just JSON.parse(JSON.stringify()) but looks cleaner.
* @param {object} obj to clone
* @returns {object} The cloned object.
*
* @example
* a = {"thingy":"with a value"};
* b = a;
* a == b; // true
* c = objectOps.deepclone(a);
* a == c; // false
*/
module.exports.deepclone = function(obj){
return JSON.parse(JSON.stringify(obj));
}
/**
Checks if a string is valid JSON using try/catch. If passed an object it will always return true.
@param {string} string to verify
@returns {boolean}
};
@example
objectOps.isJSON("99 bottles of beer"); // false
objectOps.isJSON("99"); // true
objectOps.isJSON({"thingy":"that is an object"}); // true
objectOps.isJSON(JSON.stringify({"thingy":"that is an object"})); // true
*/
/**
* Checks if a string is valid JSON using try/catch. If passed an object it will always return true.
* @param {string} string to verify
* @returns {boolean} true if valid JSON.
*
* @example
* objectOps.isJSON("99 bottles of beer"); // false
* objectOps.isJSON("99"); // true
* objectOps.isJSON({"thingy":"that is an object"}); // true
* objectOps.isJSON(JSON.stringify({"thingy":"that is an object"})); // true
*/
module.exports.isJSON = function(string){
let stringIsJson = false;
let x
let x;
try {
x = JSON.parse(string);
} catch (e){
@@ -41,4 +43,4 @@ module.exports.isJSON = function(string){
stringIsJson = true;
}
return stringIsJson;
}
};
+32 -18
View File
@@ -9,6 +9,11 @@ const path = require("path");
const errors = require("lib/errors");
/**
* Conceptual base for master and instance plugins.
* @typedef {module:lib/plugin.BaseMasterPlugin|module:lib/plugin.BaseInstancePlugin} module:lib/plugin.BasePlugin
*/
/**
* Base class for instance plugins
* @static
@@ -51,7 +56,7 @@ class BaseInstancePlugin {
* @param {module:lib/config.ConfigGroup} group -
* The group who's field got changed on.
* @param {string} field - Name of the field that changed.
* @param prev - The previous value of the field.
* @param {*} prev - The previous value of the field.
*/
async onInstanceConfigFieldChanged(group, field, prev) { }
@@ -68,7 +73,7 @@ class BaseInstancePlugin {
* the Gauges/Counters/etc in onMetric and let Clusterio deal will
* collecting the values.
*
* @returns an async iterator of prometheus metric results or undefined.
* @returns {*} an async iterator of prometheus metric results or undefined.
*/
async onMetrics() { }
@@ -99,16 +104,23 @@ class BaseInstancePlugin {
*
* Called when the Factorio server outputs a line on either stdout or
* stderr. The output is a parsed form of the line and contains the
* following properties (those marked with ? are optional):
* following properties:
*
* - source: Where the message came from, one of "stdout" and "stderr".
* - format: Timestamp format, one of "date", "seconds" and "none".
* - time?: Timestamp of the message. Not present if format is "none".
* - type: Type of message, one of "log", "action" and "generic".
* - level?: Log level for "log" type. i.e "Info" normally.
* - file?: File reported for "log" type.
* - action?: Kind of action for "action" type. i.e "CHAT" for chat.
* - message: Main content of the line.
* @param {Object} output - parsed server output.
* @param {string} output.source -
* Where the message came from, one of "stdout" and "stderr".
* @param {string} output.format -
* Timestamp format, one of "date", "seconds" and "none".
* @param {string=} output.time -
* Timestamp of the message. Not present if format is "none".
* @param {string} output.type -
* Type of message, one of "log", "action" and "generic".
* @param {string=} output.level -
* Log level for "log" type. i.e "Info" normally.
* @param {string=} output.file - File reported for "log" type.
* @param {string=} output.action -
* Kind of action for "action" type. i.e "CHAT" for chat.
* @param {string} output.message - Main content of the line.
*/
async onOutput(output) { }
@@ -215,7 +227,7 @@ class BaseMasterPlugin {
* the Gauges/Counters/etc in onMetric and let Clusterio deal will
* collecting the values.
*
* @returns an async iterator of prometheus metric results or undefined.
* @returns {*} an async iterator of prometheus metric results or undefined.
*/
async onMetrics() { }
@@ -249,9 +261,9 @@ class BaseMasterPlugin {
*
* Invoked when the connection to the slave has been closed.
*
* @param {string} event - one of connect, drop, and close
* @param {module:master~SlaveConnection} connection -
* The connection the event occured on.
* @param {string} event - one of connect, drop, and close
*/
onSlaveConnectionEvent(connection, event) { }
@@ -311,6 +323,7 @@ class BaseMasterPlugin {
* module. Once loaded the info modules will not be reloaded should this
* function be called again.
*
* @param {string} baseDir - posix relative path to plugin directory.
* @returns {Map<string, Object>} mapping of plugin name to info module.
* @static
*/
@@ -321,10 +334,10 @@ async function loadPluginInfos(baseDir) {
try {
// Note: Require path is relative to this module
pluginInfo = require(path.posix.join('..', baseDir, pluginDir, 'info'));
pluginInfo = require(path.posix.join("..", baseDir, pluginDir, "info"));
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
if (err.code === "MODULE_NOT_FOUND") {
continue;
} else {
throw new errors.PluginError(pluginDir, err);
@@ -350,7 +363,8 @@ async function loadPluginInfos(baseDir) {
*
* @param {module:lib/link.Link} link - Link to attach handlers to.
* @param {object} pluginInfo - Plugin info object.
* @param plugin - The instance of the plugin to use handlers from.
* @param {module:lib/plugin.BasePlugin} plugin -
* The instance of the plugin to use handlers from.
*/
function attachPluginMessages(link, pluginInfo, plugin) {
let messageDefinitions = pluginInfo.messages || [];
@@ -359,7 +373,7 @@ function attachPluginMessages(link, pluginInfo, plugin) {
throw new Error(`Type of ${name} message must start with "${pluginInfo.name}:"`);
}
let handler = name + messageFormat.constructor.name + 'Handler';
let handler = name + messageFormat.constructor.name + "Handler";
if (plugin === null || !plugin[handler]) {
messageFormat.attach(link);
@@ -374,7 +388,7 @@ function attachPluginMessages(link, pluginInfo, plugin) {
/**
* Invokes the given hook on all plugins
*
* @param {Map<string, Object} plugins -
* @param {Map<string, Object>} plugins -
* Mapping of plugin names to plugins to invoke the hook on.
* @param {string} hook - Name of hook to invoke.
* @param {...*} args - Arguments to pass on to the hook.
+36 -35
View File
@@ -1,10 +1,11 @@
"use strict";
class Metric {
constructor(type, name, help, labels = []) {
if (typeof name !== 'string') {
if (typeof name !== "string") {
throw new Error("Expected name to be a string");
}
if (typeof help !== 'string') {
if (typeof help !== "string") {
throw new Error("Expected help to be a string");
}
@@ -46,7 +47,7 @@ function escapeLabelValue(value) {
}
function labelsToKey(labels, metricLabels) {
let items = []
let items = [];
if (labels.length === 1 && typeof labels[0] === "object") {
labels = labels[0];
for (let name of metricLabels) {
@@ -73,7 +74,7 @@ function labelsToKey(labels, metricLabels) {
}
if (labels.length > metricLabels.length) {
throw new Error(`Extra positional label`);
throw new Error("Extra positional label");
}
for (let i=0; i < metricLabels.length; ++i) {
@@ -86,14 +87,14 @@ function labelsToKey(labels, metricLabels) {
}
}
return items.join(',');
return items.join(",");
}
function keyToLabels(key) {
let labels = new Map();
if (key !== '') for (let pair of key.split(',')) {
let [name, value] = pair.split('=', 2);
if (key !== "") for (let pair of key.split(",")) {
let [name, value] = pair.split("=", 2);
labels.set(name, value
.slice(1, -1)
.replace(/\\"/g, "\"")
@@ -111,9 +112,9 @@ class ValueCollector extends Collector {
let register = true;
let callback = null;
for (let [name, value] of Object.entries(options)) {
if (name === 'labels') { labels = value; }
else if (name === 'register') { register = value; }
else if (name === 'callback') { callback = value; }
if (name === "labels") { labels = value; }
else if (name === "register") { register = value; }
else if (name === "callback") { callback = value; }
else { throw new Error(`Unrecognized option '${name}'`); }
}
@@ -151,7 +152,7 @@ class ValueCollector extends Collector {
remove(...labels) {
let key = labelsToKey(labels, this.metric.labels);
if (key === '') {
if (key === "") {
throw new Error("labels cannot be empty");
}
this._children.delete(key);
@@ -166,11 +167,11 @@ class ValueCollector extends Collector {
function removeMatchingLabels(mapping, labels) {
for (let key of mapping.keys()) {
let candidate = keyToLabels(key);
let hasLabels = Object.entries(labels).every(([name, value]) => {
return candidate.get(name) === value;
});
let hasLabels = Object.entries(labels).every(([name, value]) => (
candidate.get(name) === value
));
if (hasLabels) {
mapping.delete(key)
mapping.delete(key);
}
}
}
@@ -208,7 +209,7 @@ class CounterChild extends ValueCollectorChild {
throw new Error("Expected value to be a positive number");
}
this._values.set(this._key, this._values.get(this._key) + value)
this._values.set(this._key, this._values.get(this._key) + value);
}
}
@@ -224,19 +225,19 @@ class Counter extends ValueCollector {
class GaugeChild extends ValueCollectorChild {
inc(value = 1) {
this._values.set(this._key, this._values.get(this._key) + value)
this._values.set(this._key, this._values.get(this._key) + value);
}
dec(value = 1) {
this._values.set(this._key, this._values.get(this._key) - value)
this._values.set(this._key, this._values.get(this._key) - value);
}
set(value) {
this._values.set(this._key, value)
this._values.set(this._key, value);
}
setToCurrentTime() {
this._values.set(this._key, Date.now() / 1000)
this._values.set(this._key, Date.now() / 1000);
}
}
@@ -309,9 +310,9 @@ function escapeHelp(help) {
function formatValue(value) {
if (value === Infinity) {
return '+Inf';
return "+Inf";
} else if (value === -Infinity) {
return '-Inf';
return "-Inf";
} else {
return value.toString();
}
@@ -323,14 +324,14 @@ async function* expositionLines(resultsIterator) {
if (first) {
first = false;
} else {
yield `\n`
yield "\n";
}
yield `# HELP ${result.metric.name} ${escapeHelp(result.metric.help)}\n`;
yield `# TYPE ${result.metric.name} ${result.metric.type}\n`;
for (let [key, value] of result.samples) {
if (key === '') {
if (key === "") {
yield `${result.metric.name} ${formatValue(value)}\n`;
} else {
yield `${result.metric.name}{${key}} ${formatValue(value)}\n`;
@@ -340,7 +341,7 @@ async function* expositionLines(resultsIterator) {
}
async function exposition(resultsIterator = defaultRegistry.collect()) {
let exposition = '';
let exposition = "";
for await (let line of expositionLines(resultsIterator)) {
exposition += line;
}
@@ -348,16 +349,16 @@ async function exposition(resultsIterator = defaultRegistry.collect()) {
}
// HTTP Content-Type for the exposition format that's implemented
exposition.contentType = 'text/plain; version=0.0.4';
exposition.contentType = "text/plain; version=0.0.4";
function serializeResult(result, options = {}) {
let addLabels = null;
let metricName = result.metric.name;
let metricHelp = result.metric.help;
for (let [name, value] of Object.entries(options)) {
if (name === 'addLabels') { addLabels = value; }
else if (name === 'metricName') { metricName = value; }
else if (name === 'metricHelp') { metricHelp = value; }
if (name === "addLabels") { addLabels = value; }
else if (name === "metricName") { metricName = value; }
else if (name === "metricHelp") { metricHelp = value; }
else { throw new Error(`Unrecognized option '${name}'`); }
}
@@ -370,7 +371,7 @@ function serializeResult(result, options = {}) {
samples = new Map();
let key = labelsToKey([addLabels], [...Object.keys(addLabels)]);
for (let [labels, value] of result.samples.entries()) {
if (labels === '') { samples.set(key, value); }
if (labels === "") { samples.set(key, value); }
else { samples.set(`${labels},${key}`, value); }
}
}
@@ -395,12 +396,12 @@ function deserializeResult(serializedResult) {
serializedResult.metric.labels,
),
samples: new Map(serializedResult.samples),
}
};
}
let defaultCollectors = {};
defaultCollectors.processCpuSecondsTotal = new Gauge(
'process_cpu_seconds_total',
"process_cpu_seconds_total",
"Total user and system CPU time spent in seconds.",
{
callback: async function() {
@@ -411,7 +412,7 @@ defaultCollectors.processCpuSecondsTotal = new Gauge(
);
defaultCollectors.processResidentMemoryBytes = new Gauge(
'process_resident_memory_bytes',
"process_resident_memory_bytes",
"Resident memory size in bytes.",
{
callback: function() {
@@ -421,7 +422,7 @@ defaultCollectors.processResidentMemoryBytes = new Gauge(
);
defaultCollectors.processHeapBytes = new Gauge(
'process_heap_bytes',
"process_heap_bytes",
"Process heap size in bytes.",
{
callback: function() {
@@ -431,7 +432,7 @@ defaultCollectors.processHeapBytes = new Gauge(
);
defaultCollectors.processStartTimeSeconds = new Gauge(
'process_start_time_seconds',
"process_start_time_seconds",
"Start time of the process since unix epoch in seconds."
);
defaultCollectors.processStartTimeSeconds.setToCurrentTime();
+6 -5
View File
@@ -2,7 +2,8 @@
* JSON schemas used for validating link messages
* @module
*/
const ajv = new (require('ajv'))({
"use strict";
const ajv = new (require("ajv"))({
verbose: true,
format: "full",
extendRefs: "fail",
@@ -73,7 +74,7 @@ const serverHandshake = ajv.compile({
$schema: "http://json-schema.org/draft-07/schema#",
title: "Server Handshake",
description: "Message sent from the server to a client during the handshake",
allOf: [ messageSchema ],
allOf: [messageSchema],
anyOf: [
{
properties: {
@@ -87,7 +88,7 @@ const serverHandshake = ajv.compile({
type: "object",
additionalProperties: { type: "string" },
},
}
},
},
},
},
@@ -136,7 +137,7 @@ const clientHandshake = ajv.compile({
$schema: "http://json-schema.org/draft-07/schema#",
title: "Client Message",
description: "Message sent from a client to the server",
allOf: [ messageSchema ],
allOf: [messageSchema],
anyOf: [
{
properties: {
@@ -168,7 +169,7 @@ const clientHandshake = ajv.compile({
"token": { type: "string" },
"agent": { type: "string" },
"version": { type: "string" },
}
},
},
},
},
+7 -4
View File
@@ -53,7 +53,7 @@ function definePermission({ name, title, description, grantByDefault=false }) {
}
if (permissions.has(name)) {
throw new Error(`Permission '${name}' is already defined`)
throw new Error(`Permission '${name}' is already defined`);
}
permissions.set(name, new Permission(name, title, description, grantByDefault));
@@ -95,6 +95,7 @@ class Role {
*
* Ensures the role with id 0 exisits and has the core.admin permission.
*
* @param {Map<number, module:lib/user.Role>} roles - role storage to modify.
* @returns {module:lib/permission.Role} the default admin role
* @static
*/
@@ -115,7 +116,7 @@ function ensureDefaultAdminRole(roles) {
* assigned by default. Note that this may not be the same role as the one
* that is assigned to new users by default.
*
* @returns {module:lib/permission.Role} the default player role
* @param {Map<number, module:lib/user.Role>} roles - role storage to modify.
* @static
*/
function ensureDefaultPlayerRole(roles) {
@@ -156,7 +157,7 @@ class User {
/**
* Roles this user has
* @type {Set<module:lib/permissions.Role}
* @type {Set<module:lib/permissions.Role>}
*/
this.roles = new Set();
if (roles) {
@@ -186,6 +187,7 @@ class User {
* Generate access token for this user
*
* @param {string} secret - Secret to sign token with.
* @returns {string} JWT access token for the user.
*/
createToken(secret) {
return jwt.sign({ aud: "user", user: this.name }, secret);
@@ -197,7 +199,7 @@ class User {
* Sets the tokenValidAfter property to the current time, which causes
* all currently issued tokens for the user to become invalid.
*/
invalidateToken(secret) {
invalidateToken() {
this.tokenValidAfter = Math.floor(Date.now() / 1000);
}
@@ -208,6 +210,7 @@ class User {
* permission. If the permission is not granted for the user a
* "Permission denied" error is thrown.
*
* @param {string} permission - The permission to check for.
* @throws {Error} if the user does noh have the given permission.
*/
checkPermission(permission) {
+103 -78
View File
@@ -16,11 +16,12 @@
// const updater = require("./updater");
// updater.update().then(console.log);
"use strict";
const deepmerge = require("deepmerge");
const path = require("path");
const fs = require("fs-extra");
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const moment = require("moment");
const request = require("request");
const setBlocking = require("set-blocking");
@@ -49,30 +50,30 @@ const config = require("lib/config");
const users = require("lib/users");
const express = require("express");
const compression = require('compression');
const cookieParser = require('cookie-parser');
const compression = require("compression");
const cookieParser = require("cookie-parser");
// Required for express post requests
const bodyParser = require("body-parser");
const fileUpload = require('express-fileupload');
const fileUpload = require("express-fileupload");
var app = express();
var httpServer;
var httpsServer;
app.use(cookieParser());
app.use(bodyParser.json({
limit: '10mb',
limit: "10mb",
}));
app.use(bodyParser.urlencoded({
parameterLimit: 100000,
limit: '10mb',
extended: true
limit: "10mb",
extended: true,
}));
app.use(fileUpload());
app.use(compression());
// dynamic HTML generations with EJS
app.set('view engine', 'ejs');
app.set('views', ['views', 'plugins']);
app.set("view engine", "ejs");
app.set("views", ["views", "plugins"]);
// give ejs access to some interesting information
app.use(function(req, res, next){
@@ -88,12 +89,12 @@ app.use(function(req, res, next){
require("./routes")(app);
// Set folder to serve static content from (the website)
app.use(express.static('static'));
app.use(express.static("static"));
const endpointHitCounter = new prometheus.Counter(
"clusterio_master_http_endpoint_hits_total",
"How many requests a particular HTTP endpoint has gotten",
{ labels: ['route'] }
{ labels: ["route"] }
);
const wsMessageCounter = new prometheus.Counter(
@@ -126,11 +127,11 @@ const wsActiveSlavesGauge = new prometheus.Gauge(
async function getMetrics(req, res, next) {
endpointHitCounter.labels(req.route.path).inc();
let results = []
let results = [];
let pluginResults = await plugin.invokeHook(masterPlugins, "onMetrics");
for (let metricIterator of pluginResults) {
for await (let metric of metricIterator) {
results.push(metric)
results.push(metric);
}
}
@@ -167,10 +168,10 @@ async function getMetrics(req, res, next) {
let text = await prometheus.exposition(results);
res.set('Content-Type', prometheus.exposition.contentType);
res.set("Content-Type", prometheus.exposition.contentType);
res.send(text);
}
app.get('/metrics', (req, res, next) => getMetrics(req, res, next).catch(next));
app.get("/metrics", (req, res, next) => getMetrics(req, res, next).catch(next));
function validateSlaveToken(req, res, next) {
@@ -232,9 +233,9 @@ app.put("/api/upload-export",
);
const masterConnectedClientsCount = new prometheus.Gauge(
'clusterio_master_connected_clients_count', "How many clients are currently connected to this master server",
"clusterio_master_connected_clients_count", "How many clients are currently connected to this master server",
{
labels: ['type'], callback: async function(gauge) {
labels: ["type"], callback: async function(gauge) {
gauge.labels("slave").set(slaveConnections.size);
gauge.labels("control").set(controlConnections.length);
},
@@ -246,6 +247,10 @@ const db = {};
/**
* Load Map from JSON file in the database directory.
*
* @param {string} databaseDirectory - Path to master database directory.
* @param {string} file - Name of file to load.
* @returns {Map} file loaded as a map.
*/
async function loadMap(databaseDirectory, file) {
let databasePath = path.resolve(databaseDirectory, file);
@@ -267,7 +272,7 @@ async function loadInstances(databaseDirectory, file) {
}
} catch (err) {
if (err.code !== 'ENOENT') {
if (err.code !== "ENOENT") {
throw err;
}
}
@@ -314,17 +319,21 @@ async function loadUsers(databaseDirectory, file) {
*/
function createUser(name) {
if (db.users.has(name)) {
throw new Error(`User '${name}' already exists`)
throw new Error(`User '${name}' already exists`);
}
let defaultRoleId = masterConfig.get("master.default_role_id");
user = new users.User({ name, roles: [defaultRoleId] }, db.roles);
let user = new users.User({ name, roles: [defaultRoleId] }, db.roles);
db.users.set(name, user);
return user;
}
/**
* Save Map to JSON file in the database directory.
*
* @param {string} databaseDirectory - Path to master database directory.
* @param {string} file - Name of file to save.
* @param {Map} map - Mapping to save into file.
*/
async function saveMap(databaseDirectory, file, map) {
let databasePath = path.resolve(databaseDirectory, file);
@@ -365,14 +374,14 @@ async function saveUsers(databaseDirectory, file) {
* Innitiate shutdown of master server
*/
async function shutdown() {
console.log('Shutting down');
console.log("Shutting down");
let exitStartTime = Date.now();
try {
console.log("Saving configs");
await fs.outputFile(masterConfigPath, JSON.stringify(masterConfig.serialize(), null, 4));
await saveMap(masterConfig.get('master.database_directory'), "slaves.json", db.slaves);
await saveInstances(masterConfig.get('master.database_directory'), "instances.json", db.instances);
await saveMap(masterConfig.get("master.database_directory"), "slaves.json", db.slaves);
await saveInstances(masterConfig.get("master.database_directory"), "instances.json", db.instances);
await saveUsers(masterConfig.get("master.database_directory"), "users.json");
await plugin.invokeHook(masterPlugins, "onShutdown");
@@ -439,7 +448,7 @@ async function downloadPage(url) {
app.get("/api/modmeta", async function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
res.header("Access-Control-Allow-Origin", "*");
res.setHeader('Content-Type', 'application/json');
res.setHeader("Content-Type", "application/json");
let modData = await downloadPage("https://mods.factorio.com/api/mods/" + req.query.modname);
res.send(modData);
});
@@ -452,7 +461,7 @@ app.get("/api/modmeta", async function(req, res) {
*/
class BaseConnection extends link.Link {
constructor(target, connector) {
super('master', target, connector);
super("master", target, connector);
link.attachAllMessages(this);
for (let masterPlugin of masterPlugins.values()) {
plugin.attachPluginMessages(this, masterPlugin.info, masterPlugin);
@@ -465,7 +474,7 @@ class BaseConnection extends link.Link {
throw new errors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`);
}
let slaveId = instance.config.get('instance.assigned_slave');
let slaveId = instance.config.get("instance.assigned_slave");
if (slaveId === null) {
throw new errors.RequestError("Instance is not assigned to a slave");
}
@@ -475,7 +484,7 @@ class BaseConnection extends link.Link {
throw new errors.RequestError("Slave containing instance is not connected");
}
if (request.plugin && !connection.plugins.has(request.plugin)) {
throw new errors.RequestError(`Slave containing instance does not have ${request.plugin} plugin`)
throw new errors.RequestError(`Slave containing instance does not have ${request.plugin} plugin`);
}
return await request.send(connection, message.data);
@@ -485,7 +494,7 @@ class BaseConnection extends link.Link {
let instance = db.instances.get(message.data.instance_id);
if (!instance) { return; }
let slaveId = instance.config.get('instance.assigned_slave');
let slaveId = instance.config.get("instance.assigned_slave");
if (slaveId === null) { return; }
let connection = slaveConnections.get(slaveId);
@@ -526,10 +535,10 @@ class BaseConnection extends link.Link {
}
}
let controlConnections = new Array();
let controlConnections = [];
class ControlConnection extends BaseConnection {
constructor(registerData, connector, user) {
super('control', connector)
super("control", connector);
this._agent = registerData.agent;
this._version = registerData.version;
@@ -551,7 +560,7 @@ class ControlConnection extends BaseConnection {
this.ws_dumper = null;
this.connector.on("connect", () => {
this.connector._socket.clusterio_ignore_dump = !!this.ws_dumper;
this.connector._socket.clusterio_ignore_dump = Boolean(this.ws_dumper);
});
this.connector.on("close", () => {
if (this.ws_dumper) {
@@ -617,7 +626,7 @@ class ControlConnection extends BaseConnection {
let instanceConfig = new config.InstanceConfig();
await instanceConfig.load(message.data.serialized_config);
let instanceId = instanceConfig.get("instance.id")
let instanceId = instanceConfig.get("instance.id");
if (db.instances.has(instanceId)) {
throw new errors.RequestError(`Instance with ID ${instanceId} already exists`);
}
@@ -630,7 +639,7 @@ class ControlConnection extends BaseConnection {
throw new errors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`);
}
if (instance.config.get('instance.assigned_slave') !== null) {
if (instance.config.get("instance.assigned_slave") !== null) {
await this.forwardRequestToInstance(message, request);
}
db.instances.delete(message.data.instance_id);
@@ -644,7 +653,7 @@ class ControlConnection extends BaseConnection {
return {
serialized_config: instance.config.serialize(),
}
};
}
async updateInstanceConfig(instance) {
@@ -685,7 +694,7 @@ class ControlConnection extends BaseConnection {
throw new errors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`);
}
let {field, prop, value} = message.data;
let { field, prop, value } = message.data;
instance.config.setProp(field, prop, value);
await this.updateInstanceConfig(instance);
}
@@ -708,7 +717,7 @@ class ControlConnection extends BaseConnection {
throw new errors.RequestError("Target slave is not connected to the master server");
}
instance.config.set("instance.assigned_slave", message.data.slave_id)
instance.config.set("instance.assigned_slave", message.data.slave_id);
return await link.messages.assignInstance.send(connection, {
instance_id: instance.config.get("instance.id"),
@@ -750,7 +759,7 @@ class ControlConnection extends BaseConnection {
// Start at 5 to leave space for future default roles
let id = Math.max(5, lastId+1);
db.roles.set(id, new users.Role({ id, ...message.data}));
db.roles.set(id, new users.Role({ id, ...message.data }));
return { id };
}
@@ -836,7 +845,7 @@ class ControlConnection extends BaseConnection {
}
};
this.connector._socket.clusterio_ignore_dump = true;
debugEvents.on("message", this.ws_dumper)
debugEvents.on("message", this.ws_dumper);
}
}
@@ -848,7 +857,7 @@ var slaveConnections = new Map();
*/
class SlaveConnection extends BaseConnection {
constructor(registerData, connector) {
super('slave', connector);
super("slave", connector);
this._agent = registerData.agent;
this._id = registerData.id;
@@ -871,6 +880,7 @@ class SlaveConnection extends BaseConnection {
});
for (let event of ["connect", "drop", "close"]) {
// eslint-disable-next-line no-loop-func
this.connector.on(event, () => {
for (let masterPlugin of masterPlugins.values()) {
masterPlugin.onSlaveConnectionEvent(this, event);
@@ -883,7 +893,7 @@ class SlaveConnection extends BaseConnection {
let instance = db.instances.get(message.data.instance_id);
let prev = instance.status;
instance.status = "initialized";
console.log(`Clusterio | Instance ${instance.config.get("instance.name")} Initialized`)
console.log(`Clusterio | Instance ${instance.config.get("instance.name")} Initialized`);
await plugin.invokeHook(masterPlugins, "onInstanceStatusChanged", instance, prev);
}
@@ -891,7 +901,7 @@ class SlaveConnection extends BaseConnection {
let instance = db.instances.get(message.data.instance_id);
let prev = instance.status;
instance.status = "running";
console.log(`Clusterio | Instance ${instance.config.get("instance.name")} Started`)
console.log(`Clusterio | Instance ${instance.config.get("instance.name")} Started`);
await plugin.invokeHook(masterPlugins, "onInstanceStatusChanged", instance, prev);
}
@@ -899,7 +909,7 @@ class SlaveConnection extends BaseConnection {
let instance = db.instances.get(message.data.instance_id);
let prev = instance.status;
instance.status = "stopped";
console.log(`Clusterio | Instance ${instance.config.get("instance.name")} Stopped`)
console.log(`Clusterio | Instance ${instance.config.get("instance.name")} Stopped`);
await plugin.invokeHook(masterPlugins, "onInstanceStatusChanged", instance, prev);
}
@@ -919,7 +929,7 @@ class SlaveConnection extends BaseConnection {
let instanceConfig = new config.InstanceConfig();
await instanceConfig.load(instance.serialized_config);
let masterInstance = db.instances.get(instanceConfig.get("instance.id"))
let masterInstance = db.instances.get(instanceConfig.get("instance.id"));
if (masterInstance) {
// Already have this instance, update state instead
if (masterInstance.status !== instance.status) {
@@ -951,7 +961,7 @@ class SlaveConnection extends BaseConnection {
}
async playerEventEventHandler(message) {
let {instance_id, name, type} = message.data;
let { instance_id, name, type } = message.data;
let user = db.users.get(name);
if (!user) {
user = createUser(name);
@@ -976,13 +986,13 @@ const wss = new WebSocket.Server({
/**
* Returns true if value is a signed 32-bit integer
*
* @param value - value to test.
* @param {number} value - value to test.
* @returns {Boolean}
* true if value is an integer between -2<sup>31</sup> and
* 2<sup>31</sup>-1.
*/
function isInteger(value) {
return value | 0 === value;
return (value | 0) === value;
}
@@ -1020,9 +1030,12 @@ class WebSocketServerConnector extends link.WebSocketBaseConnector {
* Send ready over the socket
*
* Sends the ready message over the socket to initiate the session.
*
* @param {string} sessionToken -
* the session token to send to the client.
*/
ready(sessionToken) {
this._heartbeatInterval = masterConfig.get("master.heartbeat_interval")
this._heartbeatInterval = masterConfig.get("master.heartbeat_interval");
this._socket.send(JSON.stringify({
seq: null,
type: "ready",
@@ -1043,6 +1056,9 @@ class WebSocketServerConnector extends link.WebSocketBaseConnector {
*
* Terminates the current socket and contiunes the session over the
* socket given from the message sequence given.
*
* @param {module:net.Socket} socket - New socket to continue on.
* @param {number} lastSeq - The last message the client received.
*/
continue(socket, lastSeq) {
this._socket.terminate();
@@ -1053,7 +1069,7 @@ class WebSocketServerConnector extends link.WebSocketBaseConnector {
this._timeoutId = null;
}
this._heartbeatInterval = masterConfig.get("master.heartbeat_interval")
this._heartbeatInterval = masterConfig.get("master.heartbeat_interval");
this._socket.send(JSON.stringify({
seq: null,
type: "continue",
@@ -1078,7 +1094,7 @@ class WebSocketServerConnector extends link.WebSocketBaseConnector {
clearTimeout(this._timeoutId);
}
this._timeoutId = setTimeout(() => { this._timedOut() }, timeout * 1000);
this._timeoutId = setTimeout(() => { this._timedOut(); }, timeout * 1000);
this._timeout = timeout;
}
@@ -1117,7 +1133,7 @@ class WebSocketServerConnector extends link.WebSocketBaseConnector {
} else {
this._state = "handshake";
this.emit("drop");
this._timeoutId = setTimeout(() => { this._timedOut() }, this._timeout * 1000);
this._timeoutId = setTimeout(() => { this._timedOut(); }, this._timeout * 1000);
}
this.stopHeartbeat();
@@ -1163,6 +1179,9 @@ class WebSocketServerConnector extends link.WebSocketBaseConnector {
* Close the connection with the given reason.
*
* Sends a close frame and disconnects the connector.
*
* @param {number} code - WebSocket close code.
* @param {string} reason - WebSocket close reason.
*/
async close(code, reason) {
if (this._state === "closed") {
@@ -1172,7 +1191,7 @@ class WebSocketServerConnector extends link.WebSocketBaseConnector {
this.stopHeartbeat();
this._state = "closing";
this._socket.close(code, reason);
await events.once(this, "close")
await events.once(this, "close");
}
}
@@ -1202,7 +1221,7 @@ wss.on("connection", function (socket, req) {
// Start connection handshake.
socket.send(JSON.stringify({ seq: null, type: "hello", data: {
version,
plugins: pluginList
plugins: pluginList,
}}));
function attachHandler() {
@@ -1283,7 +1302,7 @@ async function handleHandshake(message, socket, req, attachHandler) {
}
connector.continue(socket, data.last_seq);
return
return;
}
if (stopAcceptingNewSessions) {
@@ -1332,7 +1351,7 @@ async function handleHandshake(message, socket, req, attachHandler) {
}
let sessionId = nextSessionId++;
let sessionToken = jwt.sign({ aud: masterSession, sid: sessionId, }, masterConfig.get("master.auth_secret"));
let sessionToken = jwt.sign({ aud: masterSession, sid: sessionId }, masterConfig.get("master.auth_secret"));
let connector = new WebSocketServerConnector(socket, sessionId);
activeConnectors.set(sessionId, connector);
@@ -1365,7 +1384,7 @@ async function pluginManagement(pluginInfos) {
async function loadPlugins(pluginInfos) {
let plugins = new Map();
for (pluginInfo of pluginInfos) {
for (let pluginInfo of pluginInfos) {
if (!masterConfig.group(pluginInfo.name).get("enabled")) {
continue;
}
@@ -1386,7 +1405,7 @@ async function loadPlugins(pluginInfos) {
plugins.set(pluginInfo.name, masterPlugin);
} catch (err) {
throw new errors.PluginError(pluginInfo.name, err)
throw new errors.PluginError(pluginInfo.name, err);
}
console.log(`Clusterio | Loaded plugin ${pluginInfo.name} in ${Date.now() - pluginLoadStarted}ms`);
@@ -1398,6 +1417,10 @@ async function loadPlugins(pluginInfos) {
* Calls listen on server capturing any errors that occurs
* binding to the port. Also adds handler for WebSocket
* upgrade event.
*
* @param {module:net.Server} server - Server to start the listening on.
* @param {*} args - Arguments to the .listen() call on the server.
* @returns {Promise} promise that resolves the server is listening.
*/
function listen(server, ...args) {
return new Promise((resolve, reject) => {
@@ -1407,7 +1430,7 @@ function listen(server, ...args) {
// For reasons that defy common sense, the connection event has
// to be emitted explictly when using noServer.
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req);
wss.emit("connection", ws, req);
});
});
@@ -1417,9 +1440,9 @@ function listen(server, ...args) {
));
}
server.once('error', wrapError);
server.once("error", wrapError);
server.listen(...args, () => {
server.off('error', wrapError);
server.off("error", wrapError);
resolve();
});
});
@@ -1431,9 +1454,11 @@ function _setConfig(config) {
/**
* Returns the URL needed to connect to the master server.
*
* @returns {string} master URL.
*/
function getMasterUrl() {
let url = masterConfig.get("master.external_address")
let url = masterConfig.get("master.external_address");
if (!url) {
if (masterConfig.get("master.https_port")) {
url = `https://localhost:${masterConfig.get("master.https_port")}/`;
@@ -1450,7 +1475,7 @@ async function startServer() {
process.title = "clusterioMaster";
// add better stack traces on promise rejection
process.on('unhandledRejection', r => console.log(r));
process.on("unhandledRejection", r => console.log(r));
// argument parsing
let args = yargs
@@ -1472,7 +1497,7 @@ async function startServer() {
nargs: 1, default: "config-control.json",
});
})
.demandCommand(1, "You need to specify a command to run")
.demandCommand(1, "You need to specify a command to run");
})
.command("run", "Run master server")
.demandCommand(1, "You need to specify a command to run")
@@ -1481,7 +1506,7 @@ async function startServer() {
;
console.log("Loading Plugin info");
let pluginInfos = await plugin.loadPluginInfos("plugins")
let pluginInfos = await plugin.loadPluginInfos("plugins");
config.registerPluginConfigGroups(pluginInfos);
config.finalizeConfigs();
@@ -1492,7 +1517,7 @@ async function startServer() {
await masterConfig.load(JSON.parse(await fs.readFile(masterConfigPath)));
} catch (err) {
if (err.code === 'ENOENT') {
if (err.code === "ENOENT") {
console.log("Config not found, initializing new config");
await masterConfig.init();
@@ -1554,8 +1579,8 @@ async function startServer() {
// If we get here the command was run
let secondSigint = false
process.on('SIGINT', () => {
let secondSigint = false;
process.on("SIGINT", () => {
if (secondSigint) {
console.log("Caught second interrupt, terminating immediately");
process.exit(1);
@@ -1567,28 +1592,28 @@ async function startServer() {
});
// terminal closed
process.on('SIGHUP', () => {
process.on("SIGHUP", () => {
// No graceful cleanup, no warning out (stdout is likely closed.)
// Don't close the terminal with the clusterio master in it.
process.exit(1);
});
await fs.ensureDir(masterConfig.get('master.database_directory'));
await fs.ensureDir(masterConfig.get("master.database_directory"));
db.slaves = await loadMap(masterConfig.get('master.database_directory'), "slaves.json");
db.instances = await loadInstances(masterConfig.get('master.database_directory'), "instances.json");
db.slaves = await loadMap(masterConfig.get("master.database_directory"), "slaves.json");
db.instances = await loadInstances(masterConfig.get("master.database_directory"), "instances.json");
await loadUsers(masterConfig.get("master.database_directory"), "users.json");
// Make sure we're actually going to listen on a port
let httpPort = masterConfig.get('master.http_port');
let httpsPort = masterConfig.get('master.https_port');
let httpPort = masterConfig.get("master.http_port");
let httpsPort = masterConfig.get("master.https_port");
if (!httpPort && !httpsPort) {
console.error("Error: at least one of http_port and https_port must be configured");
process.exit(1);
}
let tls_cert = masterConfig.get('master.tls_certificate');
let tls_key = masterConfig.get('master.tls_private_key');
let tls_cert = masterConfig.get("master.tls_certificate");
let tls_key = masterConfig.get("master.tls_private_key");
// Create a self signed certificate if the certificate files doesn't exist
if (httpsPort && !await fs.exists(tls_cert) && !await fs.exists(tls_key)) {
await generateSSLcert({
@@ -1623,8 +1648,8 @@ async function startServer() {
httpsServer = require("https").createServer({
key: privateKey,
cert: certificate
}, app)
cert: certificate,
}, app);
await listen(httpsServer, httpsPort);
console.log("Listening for HTTPS on port %s...", httpsServer.address().port);
}
@@ -1641,7 +1666,7 @@ module.exports = {
_ControlConnection: ControlConnection,
_slaveConnections: slaveConnections,
_SlaveConnection: SlaveConnection,
}
};
if (module === require.main) {
console.warn(`
+2
View File
@@ -10,6 +10,7 @@
"cover": "nyc npm test",
"fast-cover": "FAST_TEST=y nyc npm test",
"ci-cover": "nyc --reporter=lcovonly npm run-script test",
"lint": "eslint \"*.js\" lib plugins test",
"docs": "jsdoc --configure docs/jsdoc.json",
"install": "node lib/npmPostinstall",
"bundle-dependencies": "bundle-dependencies"
@@ -70,6 +71,7 @@
"yargs": "^14.2.0"
},
"devDependencies": {
"eslint": "^7.2.0",
"html5-validator": "^1.2.1",
"jsdoc": "^3.6.3",
"klaw": "^3.0.0",
+6 -5
View File
@@ -1,3 +1,4 @@
"use strict";
let link = require("lib/link");
let plugin = require("lib/plugin");
@@ -10,14 +11,14 @@ module.exports = {
messages: {
chat: new link.Event({
type: 'global_chat:chat',
links: ['instance-slave', 'slave-master', 'master-slave', 'slave-instance'],
forwardTo: 'master',
broadcastTo: 'instance',
type: "global_chat:chat",
links: ["instance-slave", "slave-master", "master-slave", "slave-instance"],
forwardTo: "master",
broadcastTo: "instance",
eventProperties: {
"instance_name": { type: "string" },
"content": { type: "string" },
},
}),
},
}
};
+5 -1
View File
@@ -1,12 +1,16 @@
/**
* @module
*/
"use strict";
const plugin = require("lib/plugin");
const luaTools = require("lib/luaTools");
/**
* Removes gps and train tags from messags
*
* @param {string} content - string to strip tags from.
* @returns {string} stripped string.
*/
function removeTags(content) {
return content.replace(/(\[gps=-?\d+,-?\d+\]|\[train=\d+\])/g, "");
@@ -57,4 +61,4 @@ module.exports = {
// For testing only
_removeTags: removeTags,
}
};
+7 -6
View File
@@ -1,9 +1,10 @@
const assert = require('assert').strict;
"use strict";
const assert = require("assert").strict;
const mock = require('../../../test/mock');
const lines = require('../../../test/lib/factorio/lines');
const instance = require('../instance');
const info = require('../info');
const mock = require("../../../test/mock");
const lines = require("../../../test/lib/factorio/lines");
const instance = require("../instance");
const info = require("../info");
describe("global_chat plugin", function() {
@@ -62,6 +63,6 @@ describe("global_chat plugin", function() {
}
assert(count > 0, "no lines were tested");
});
})
});
});
});
+3 -2
View File
@@ -1,3 +1,4 @@
"use strict";
const link = require("lib/link");
@@ -13,7 +14,7 @@ const technologies = {
{ type: "integer" },
{ type: ["null", "number"] },
{ type: "boolean" },
]
],
},
};
@@ -77,4 +78,4 @@ module.exports = {
},
}),
},
}
};
+4 -3
View File
@@ -1,3 +1,4 @@
"use strict";
const needle = require("needle");
const plugin = require("lib/plugin");
@@ -5,8 +6,8 @@ const luaTools = require("lib/luaTools");
function unexpectedError(err) {
console.log("Unexpected error in research_sync")
console.log("---------------------------------")
console.log("Unexpected error in research_sync");
console.log("---------------------------------");
console.log(err);
}
@@ -78,4 +79,4 @@ class InstancePlugin extends plugin.BaseInstancePlugin {
module.exports = {
InstancePlugin,
}
};
+9 -8
View File
@@ -1,3 +1,4 @@
"use strict";
const fs = require("fs-extra");
const path = require("path");
@@ -38,7 +39,7 @@ class MasterPlugin extends plugin.BaseMasterPlugin {
}
broadcastProgress() {
let techs = []
let techs = [];
for (let name of this.progressToBroadcast) {
let tech = this.technologies.get(name);
if (tech.progress) {
@@ -58,7 +59,7 @@ class MasterPlugin extends plugin.BaseMasterPlugin {
let tech = this.technologies.get(name);
if (!tech) {
tech = { level, progress: 0, researched: false };
this.technologies.set(name, tech)
this.technologies.set(name, tech);
// Ignore contribution to already researched technologies
} else if (tech.level > level || tech.level === level && tech.researched) {
@@ -96,7 +97,7 @@ class MasterPlugin extends plugin.BaseMasterPlugin {
let tech = this.technologies.get(name);
if (!tech || tech.level <= level) {
this.progressToBroadcast.delete(name);
this.technologies.set(name, { level, progress: null, researched: true })
this.technologies.set(name, { level, progress: null, researched: true });
}
}
@@ -111,7 +112,7 @@ class MasterPlugin extends plugin.BaseMasterPlugin {
}
for (let instanceTech of message.data.technologies) {
let [name, level, progress, researched] = instanceTech;
let [name, level, progress, researched] = instanceTech;
let tech = this.technologies.get(name);
if (!tech) {
this.technologies.set(name, { level, progress, researched });
@@ -151,10 +152,10 @@ class MasterPlugin extends plugin.BaseMasterPlugin {
let technologies = [];
for (let [name, tech] of this.technologies) {
technologies.push([ name, tech.level, tech.progress, tech.researched ])
technologies.push([name, tech.level, tech.progress, tech.researched]);
}
return { technologies }
return { technologies };
}
}
@@ -183,5 +184,5 @@ async function saveTechnologies(masterConfig, technologies) {
module.exports = {
MasterPlugin
}
MasterPlugin,
};
+3 -2
View File
@@ -1,5 +1,6 @@
// These are libraries that does not have any units test, they are
// required here in order to track coverage for them.
require('../instance');
require('../master');
"use strict";
require("../instance");
require("../master");
+2 -1
View File
@@ -1,3 +1,4 @@
"use strict";
let link = require("lib/link");
module.exports = {
@@ -6,4 +7,4 @@ module.exports = {
description: "Exports statistics to Prometheus",
version: "2.0.0-alpha",
instanceEntrypoint: "instance",
}
};
+9 -8
View File
@@ -1,28 +1,29 @@
const plugin = require('lib/plugin');
const { Gauge } = require('lib/prometheus');
"use strict";
const plugin = require("lib/plugin");
const { Gauge } = require("lib/prometheus");
const instancePlayerCount = new Gauge(
"clusterio_statistics_exporter_instance_player_count", "Amount of players connected to this cluster",
{ labels: ['instance_id'] }
{ labels: ["instance_id"] }
);
const instanceGameTicksTotal = new Gauge(
"clusterio_statistics_exporter_instance_game_ticks_total", "Game tick an instance has progressed to",
{ labels: ['instance_id'] }
{ labels: ["instance_id"] }
);
const instanceForceFlowStatistics = new Gauge(
"clusterio_statistics_exporter_instance_force_flow_statistics", "Items/fluids/enemies/buildings produced/built/killed by a force",
{ labels: ['instance_id', 'force', 'statistic', 'direction', 'name'] },
{ labels: ["instance_id", "force", "statistic", "direction", "name"] },
);
const instanceGameFlowStatistics = new Gauge(
"clusterio_statistics_exporter_instance_game_flow_statistics", "Pollution produced/consumed in the game",
{ labels: ['instance_id', 'statistic', 'direction', 'name'] },
{ labels: ["instance_id", "statistic", "direction", "name"] },
);
class InstancePlugin extends plugin.BaseInstancePlugin {
async onMetrics() {
let string = await this.instance.server.sendRcon('/sc statistics_exporter.export()');
let string = await this.instance.server.sendRcon("/sc statistics_exporter.export()");
let stats;
try {
stats = JSON.parse(string);
@@ -64,4 +65,4 @@ module.exports = {
_instanceGameTicksTotal: instanceGameTicksTotal,
_instanceForceFlowStatistics: instanceForceFlowStatistics,
_instanceGameFlowStatistics: instanceGameFlowStatistics,
}
};
+16 -15
View File
@@ -1,8 +1,9 @@
const assert = require('assert').strict;
"use strict";
const assert = require("assert").strict;
const mock = require('../../../test/mock');
const instance = require('../instance');
const info = require('../info');
const mock = require("../../../test/mock");
const instance = require("../instance");
const info = require("../info");
describe("statistics_exporter plugin", function() {
@@ -16,42 +17,42 @@ describe("statistics_exporter plugin", function() {
describe(".onMetrics()", function() {
it("should record statistics", async function() {
instancePlugin.instance.server.rconCommandResults.set(
'/sc statistics_exporter.export()',
"/sc statistics_exporter.export()",
JSON.stringify({
game_tick: 100,
player_count: 3,
force_flow_statistics: {
player: {
item_production_statistics: {
input: { "inserter": 10, },
output: { "iron-plate": 24, }
input: { "inserter": 10 },
output: { "iron-plate": 24 },
},
}
},
},
game_flow_statistics: {
pollution_statistics: {
input: { "boiler": 2000 },
output: { "tree-proxy": 560 },
}
},
},
})
);
await instancePlugin.onMetrics();
assert.equal(instance._instancePlayerCount.labels('7357').get(), 3);
assert.equal(instance._instanceGameTicksTotal.labels('7357').get(), 100);
assert.equal(instance._instancePlayerCount.labels("7357").get(), 3);
assert.equal(instance._instanceGameTicksTotal.labels("7357").get(), 100);
assert.equal(instance._instanceForceFlowStatistics.labels(
'7357', 'player', 'item_production_statistics', 'input', 'inserter').get(),
"7357", "player", "item_production_statistics", "input", "inserter").get(),
10);
assert.equal(instance._instanceGameFlowStatistics.labels(
'7357', 'pollution_statistics', 'input', 'boiler').get(),
"7357", "pollution_statistics", "input", "boiler").get(),
2000);
});
it("should pass on JSON parse errors", async function() {
let string = 'An error occured\n';
let string = "An error occured\n";
instancePlugin.instance.server.rconCommandResults.set(
'/sc statistics_exporter.export()', string
"/sc statistics_exporter.export()", string
);
let errorMessage;
+78 -75
View File
@@ -1,4 +1,5 @@
const doleNN = require("./dole_nn_base")
"use strict";
const doleNN = require("./dole_nn_base");
const prometheus = require("lib/prometheus");
@@ -15,31 +16,32 @@ const prometheusDoleFactorGauge = new prometheus.Gauge(
);
class neuralDole {
getRequestStats(itemname,samples)
{
var sum=0;
samples=Math.min(samples,(this.stats[itemname] || []).length-1);
if (samples<1) return 0.1;
for (var i = 1; i <= samples; i++) {
sum+=this.stats[itemname][i].req;
}
if (sum==0) return 0.1;
return sum/samples;
}
constructor({
items
}){
// Set some storage variables for the dole divider
this.items = items
this.itemsLastTick = new Map(items._items)
this.dole = {}
this.carry = {}
this.lastRequest = {}
this.stats=[]
this.debt= {}
getRequestStats(itemname,samples)
{
var sum=0;
samples=Math.min(samples,(this.stats[itemname] || []).length-1);
if (samples<1) return 0.1;
for (var i = 1; i <= samples; i++) {
sum+=this.stats[itemname][i].req;
}
if (sum==0) return 0.1;
return sum/samples;
}
constructor({
items,
}){
// Set some storage variables for the dole divider
this.items = items;
this.itemsLastTick = new Map(items._items);
this.dole = {};
this.carry = {};
this.lastRequest = {};
this.stats=[];
this.debt= {};
}
doMagic() {
for (let [name, count] of this.items._items) {
let magicData = doleNN.Tick(
@@ -47,84 +49,85 @@ class neuralDole {
this.dole[name],
this.itemsLastTick.get(name),
this.getRequestStats(name,5)
)
this.stats[name]=this.stats[name] || []
this.stats[name].unshift({req:0,given:0});//stats[name][0] is the one we currently collect
);
this.stats[name]=this.stats[name] || [];
this.stats[name].unshift({ req: 0, given: 0 });//stats[name][0] is the one we currently collect
if (this.stats[name].length>10) this.stats[name].pop();//remove if too many samples in stats
this.dole[name] = magicData[0]
this.dole[name] = magicData[0];
// DONE handle magicData[1] for graphing for our users
prometheusNNDoleGauge.labels(name).set(magicData[1] || 0);
}
this.itemsLastTick = new Map(this.items._items)
}
divider(object) {
let magicData = doleNN.Dose(
object.count, // numReq
this.itemsLastTick = new Map(this.items._items);
}
divider(object) {
let magicData = doleNN.Dose(
object.count, // numReq
this.items.getItemCount(object.name),
this.itemsLastTick.get(object.name) || 0,
this.dole[object.name],
this.carry[object.name+" "+object.instanceID] || 0,
this.lastRequest[object.name+"_"+object.instanceID+"_"+object.instanceName] || 0,
this.getRequestStats(object.name,5),
this.dole[object.name],
this.carry[object.name+" "+object.instanceID] || 0,
this.lastRequest[object.name+"_"+object.instanceID+"_"+object.instanceName] || 0,
this.getRequestStats(object.name,5),
this.debt[object.name+" "+object.instanceID] || 0
)
if ((this.stats[object.name] || []).length>0)
{
this.stats[object.name][0].req+=Number(object.count);
this.stats[object.name][0].given+=Number(magicData[0]);
}
this.lastRequest[object.name+"_"+object.instanceID+"_"+object.instanceName] = object.count
//0.Number of items to give in that dose
//1.New dole for item X
//2.New carry for item X slave Y
this.dole[object.name] = magicData[1]
this.carry[object.name+" "+object.instanceID] = magicData[2]
this.debt[object.name+" "+object.instanceID] = magicData[3]
);
if ((this.stats[object.name] || []).length>0)
{
this.stats[object.name][0].req+=Number(object.count);
this.stats[object.name][0].given+=Number(magicData[0]);
}
this.lastRequest[object.name+"_"+object.instanceID+"_"+object.instanceName] = object.count;
//0.Number of items to give in that dose
//1.New dole for item X
//2.New carry for item X slave Y
this.dole[object.name] = magicData[1];
this.carry[object.name+" "+object.instanceID] = magicData[2];
this.debt[object.name+" "+object.instanceID] = magicData[3];
// Remove item from DB and send it
this.items.removeItem(object.name, magicData[0]);
return magicData[0];
}
}
}
// If the server regularly can't fulfill requests, this number grows until it can. Then it slowly shrinks back down.
let _doleDivisionFactor = {};
function doleDivider({
object,
object,
items,
logItemTransfers,
}){
let itemCount = items.getItemCount(object.name);
const doleDivisionRetardation = 10; //lower rates will equal more dramatic swings
const maxDoleDivision = 250; //a higher cap will divide the store more ways, but will take longer to recover as supplies increase
const originalCount = Number(object.count) || 0;
object.count /= ((_doleDivisionFactor[object.name]||0)+doleDivisionRetardation)/doleDivisionRetardation;
object.count = Math.round(object.count);
const doleDivisionRetardation = 10; //lower rates will equal more dramatic swings
const maxDoleDivision = 250; //a higher cap will divide the store more ways, but will take longer to recover as supplies increase
const originalCount = Number(object.count) || 0;
object.count /= ((_doleDivisionFactor[object.name]||0)+doleDivisionRetardation)/doleDivisionRetardation;
object.count = Math.round(object.count);
if (logItemTransfers) {
console.info(`Serving ${object.count}/${originalCount} ${object.name} from ${itemCount} ${object.name} with dole division factor ${(_doleDivisionFactor[object.name]||0)} (real=${((_doleDivisionFactor[object.name]||0)+doleDivisionRetardation)/doleDivisionRetardation}), item is ${itemCount > object.count?'stocked':'short'}.`);
console.info(`Serving ${object.count}/${originalCount} ${object.name} from ${itemCount} ${object.name} with dole division factor ${(_doleDivisionFactor[object.name]||0)} (real=${((_doleDivisionFactor[object.name]||0)+doleDivisionRetardation)/doleDivisionRetardation}), item is ${itemCount > object.count?"stocked":"short"}.`);
}
// Update existing items if item name already exists
if (itemCount > object.count) {
//If successful, increase dole
_doleDivisionFactor[object.name] = Math.max((_doleDivisionFactor[object.name]||0)||1, 1) - 1;
items.removeItem(object.name, object.count)
//If successful, increase dole
_doleDivisionFactor[object.name] = Math.max((_doleDivisionFactor[object.name]||0)||1, 1) - 1;
items.removeItem(object.name, object.count);
prometheusDoleFactorGauge.labels(object.name).set(_doleDivisionFactor[object.name] || 0);
return object.count;
} else {
// if we didn't have enough, attempt giving out a smaller amount next time
_doleDivisionFactor[object.name] = Math.min(maxDoleDivision, Math.max((_doleDivisionFactor[object.name]||0)||1, 1) * 2);
prometheusDoleFactorGauge.labels(object.name).set(_doleDivisionFactor[object.name] || 0);
return 0;
//console.log('failure out of ' + object.name + " | " + object.count + " from " + object.instanceID + " ("+object.instanceName+")");
}
prometheusDoleFactorGauge.labels(object.name).set(_doleDivisionFactor[object.name] || 0);
return object.count;
} else {
// if we didn't have enough, attempt giving out a smaller amount next time
_doleDivisionFactor[object.name] = Math.min(maxDoleDivision, Math.max((_doleDivisionFactor[object.name]||0)||1, 1) * 2);
prometheusDoleFactorGauge.labels(object.name).set(_doleDivisionFactor[object.name] || 0);
return 0;
//console.log('failure out of ' + object.name + " | " + object.count + " from " + object.instanceID + " ("+object.instanceName+")");
}
}
module.exports = {
doleDivider,
neuralDole
}
doleDivider,
neuralDole,
};
+25 -24
View File
@@ -1,5 +1,6 @@
//Sigmoids...
"use strict";
function sigmoid0(x)
{
return x/(1+Math.abs(x));
@@ -7,17 +8,17 @@ function sigmoid0(x)
function sigmoid1(x)
{
if (x>=0.0) {
return 1.0
return 1.0;
} else {
return 0.0
return 0.0;
}
}
function sigmoid2(x)
{
if (x>=0.0){
return x
return x;
} else {
return 0.0
return 0.0;
}
}
function sigmoid3(x)
@@ -46,7 +47,7 @@ function Dose(numreq,instore,store_last_tick,dole,carry,prev_req,numreq_total_ad
instore=Number(instore);
var instore_adj=instore;
if (instore_adj==0) instore_adj=0.1;
var b=Math.trunc(prev_req/instore_adj*numreq);
var inp1=[numreq,instore_adj,store_last_tick/numreq,dole,carry];//0..4
var inp2=[prev_req/numreq,numreq/numreq_total_adj,store_last_tick/numreq_total_adj,prev_req/instore_adj,b,numreq_total_adj,store_last_tick,debt];//5..12
@@ -54,30 +55,30 @@ function Dose(numreq,instore,store_last_tick,dole,carry,prev_req,numreq_total_ad
var internal=new Array(20).fill(0);
var outputs=[0,0,0,0];
//NN: Please don't touch :), code is generated
outputs[0]=inputs[7]*0.8058582544;
outputs[0]=sigmoid2(outputs[0]);
outputs[1]=sigmoid0(outputs[1]);
outputs[2]=sigmoid0(outputs[2]);
outputs[3]=inputs[6]*0.2294657379;
outputs[3]=sigmoid0(outputs[3]);
outputs[0]=inputs[7]*0.8058582544;
outputs[0]=sigmoid2(outputs[0]);
outputs[1]=sigmoid0(outputs[1]);
outputs[2]=sigmoid0(outputs[2]);
outputs[3]=inputs[6]*0.2294657379;
outputs[3]=sigmoid0(outputs[3]);
debt=debt || 0;
if (outputs[0]>0.99) outputs[0]=1.0;
var res1=Number(outputs[0])*numreq+outputs[3]+debt;
var res_capped=Math.round(res1);
if (res_capped>numreq) res_capped=numreq;
if (res_capped>instore) res_capped=instore;
if (res_capped<0) res_capped=0;
res1=res1 || 0;
res_capped=res_capped || 0;//Safeguard against wrong values
debt=res1-res_capped;
debt_cap=Math.round(numreq*1.5)+5;
if (debt>debt_cap) debt=debt_cap;
carry=sigmoid0(outputs[2]*carry+outputs[0]) || 0;
return [res_capped,dole+outputs[1],carry,debt];
}
@@ -101,19 +102,19 @@ function Tick(instore,dole,store_last_tick,numreq_total_adj)
var internal=new Array(10).fill(0);
var outputs=[0,0];
//NN: Please don't touch :), code is generated
outputs[0]=inputs[3]*0.9364774823;
outputs[0]=outputs[0]*1.0*0.4590721726;
outputs[0]=sigmoid2(outputs[0]);
outputs[1]=outputs[0]*1.828422785;
outputs[1]=sigmoid3(outputs[1]);
outputs[0]=inputs[3]*0.9364774823;
outputs[0]=outputs[0]*0.4590721726;
outputs[0]=sigmoid2(outputs[0]);
outputs[1]=outputs[0]*1.828422785;
outputs[1]=sigmoid3(outputs[1]);
avg=outputs[1];
if (numreq_total_adj<0.11) avg=1;//When no item's are requested, result is 0.1 ; In that case there is no deficit, so return 1 = 100% demand fulfilled
if (avg>1) avg=1;
if (avg<0) avg=0;
return [sigmoid0(outputs[0]),avg];
}
module.exports = {Dose,Tick}
module.exports = { Dose,Tick };
+3 -2
View File
@@ -1,3 +1,4 @@
"use strict";
const link = require("lib/link");
const config = require("lib/config");
@@ -50,7 +51,7 @@ const items = {
{ type: "integer" },
],
},
}
};
module.exports = {
name: "subspace_storage",
@@ -103,4 +104,4 @@ module.exports = {
},
}),
},
}
};
+5 -4
View File
@@ -1,11 +1,12 @@
"use strict";
const fs = require("fs-extra");
const plugin = require("lib/plugin");
const luaTools = require("lib/luaTools");
function unexpectedError(err) {
console.log("Unexpected error in subspace_storage")
console.log("------------------------------------")
console.log("Unexpected error in subspace_storage");
console.log("------------------------------------");
console.log(err);
}
@@ -44,7 +45,7 @@ class InstancePlugin extends plugin.BaseInstancePlugin {
});
if (this.instance.config.get("subspace_storage.log_item_transfers")) {
console.log("Exported the following to master:")
console.log("Exported the following to master:");
console.log(items);
}
}
@@ -62,7 +63,7 @@ class InstancePlugin extends plugin.BaseInstancePlugin {
}
if (this.instance.config.get("subspace_storage.log_item_transfers")) {
console.log("Imported following from master:")
console.log("Imported following from master:");
console.log(response.items);
}
+6 -6
View File
@@ -47,8 +47,8 @@ class MasterPlugin extends plugin.BaseMasterPlugin {
this.itemsLastUpdate = new Map(this.items._items.entries());
this.autosaveId = setInterval(() => {
saveDatabase(this.master.config, this.items).catch(err => {
console.log("Unexpected error autosaving items in subspace_storage")
console.log("-----------------------------------------------------")
console.log("Unexpected error autosaving items in subspace_storage");
console.log("-----------------------------------------------------");
console.log(err);
});
}, this.master.config.get("subspace_storage.autosave_interval") * 1000);
@@ -56,7 +56,7 @@ class MasterPlugin extends plugin.BaseMasterPlugin {
this.neuralDole = new dole.neuralDole({ items: this.items });
this.doleMagicId = setInterval(() => {
if (this.master.config.get("subspace_storage.division_method") === "neural_dole") {
this.neuralDole.doMagic()
this.neuralDole.doMagic();
}
}, 1000);
@@ -97,7 +97,7 @@ class MasterPlugin extends plugin.BaseMasterPlugin {
this.updateStorage();
if (this.master.config.get("subspace_storage.log_item_transfers")) {
console.log(`Imported the following from ${message.data.instance_id}:`)
console.log(`Imported the following from ${message.data.instance_id}:`);
console.log(message.data.items);
}
}
@@ -136,7 +136,7 @@ class MasterPlugin extends plugin.BaseMasterPlugin {
} else if (method === "dole") {
for (let item of message.data.items) {
let count = dole.doleDivider({
object: { name: item[0], count: item[1], instanceId, instanceName},
object: { name: item[0], count: item[1], instanceId, instanceName },
items: this.items,
logItemTransfers: this.master.config.get("subspace_storage.log_item_transfers"),
});
@@ -166,7 +166,7 @@ class MasterPlugin extends plugin.BaseMasterPlugin {
return {
items: itemsRemoved,
}
};
}
onMetrics() {
+2 -1
View File
@@ -1,3 +1,4 @@
"use strict";
const express = require("express");
const path = require("path");
@@ -53,4 +54,4 @@ function addApiRoutes(app, items, endpointHitCounter) {
module.exports = {
addWebRoutes,
addApiRoutes,
}
};
+24 -23
View File
@@ -1,35 +1,36 @@
// function to draw data we recieve from ajax requests
"use strict";
_lastData = [];
function drawcontents(data) {
data = data || _lastData; //Cache data so we can drawcontents without waiting for the server, for the search box.
_lastData = data;
// allow searching for multiple criteria separated by space
var searchArgs = document.querySelector("#search").value;
searchArgs = searchArgs.trim();
searchArgs = '(' + searchArgs.replace(/ +/g,")|(") + ')';
const search = new RegExp(searchArgs, 'i');
data = data.filter(function(item) {
// allow searching for multiple criteria separated by space
var searchArgs = document.querySelector("#search").value;
searchArgs = searchArgs.trim();
searchArgs = "(" + searchArgs.replace(/ +/g,")|(") + ")";
const search = new RegExp(searchArgs, "i");
data = data.filter(function(item) {
return search.test(item.name);
})
});
sortByKey(data, "count");
const table = document.querySelector("#contents tbody"); //tables have tbody inserted automatically
const rows = table.children;
//update existing rows or create new ones
data.forEach(function(item, i) {
// format count to have .s in it for large number readability (1,000,000)
item.count = item.count.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
let row = rows[i];
if(!row) {
row = document.createElement('tr');
row = document.createElement("tr");
row.innerHTML = "<td><div></div></td><td class=name></td><td class=count></td>";
table.appendChild(row);
}
const itemIcon = row.querySelector("div");
const iconClass = getItemIconClass(item.name);
if (itemIcon.getAttribute("class") !== iconClass) {
@@ -51,17 +52,17 @@ function drawcontents(data) {
}
}
const name = row.querySelector('.name');
const name = row.querySelector(".name");
if(name.textContent !== localeName) {
name.textContent = localeName;
}
const count = row.querySelector('.count');
if(count.textContent !== ''+item.count) {
const count = row.querySelector(".count");
if(count.textContent !== String(item.count)) {
count.textContent = item.count;
}
});
//remove excess rows, for example, after filtering
while (data.length < rows.length) {
table.removeChild(rows[data.length]);
@@ -76,7 +77,7 @@ function updateInventory() {
let data = JSON.parse(xmlhttp.responseText);
drawcontents(data);
}
}
};
xmlhttp.open("GET", `${root}api/inventory`, true);
xmlhttp.send();
}
@@ -88,11 +89,11 @@ if(JSON.parse(localStorage.settings)["Periodically update storage screen"]) {
// function to sort arrays of objects after a keys value
function sortByKey(array, key) {
array.sort(function(a, b) {
return b[key] - a[key];
});
array.sort(function(a, b) {
return b[key] - a[key];
});
}
document.querySelector("#search").addEventListener('input', function() {
document.querySelector("#search").addEventListener("input", function() {
drawcontents();
});
+5 -4
View File
@@ -1,7 +1,8 @@
// These are libraries that does not have any units test, they are
// required here in order to track coverage for them.
require('../instance');
require('../master');
require('../dole');
require('../dole_nn_base');
"use strict";
require("../instance");
require("../master");
require("../dole");
require("../dole_nn_base");
+2 -1
View File
@@ -1,3 +1,4 @@
"use strict";
const assert = require("assert").strict;
const routes = require("../routes");
@@ -18,7 +19,7 @@ describe("subspace_storage endpoints", function() {
inventory.forEach(itemStack => {
if(itemStack.name == "steel-plate" && itemStack.count >= 20) contains20SteelPlate = true;
});
assert(contains20SteelPlate, "Please ensure there are at least 20 steel plate in the inventory")
assert(contains20SteelPlate, "Please ensure there are at least 20 steel plate in the inventory");
});
});
});
+2 -1
View File
@@ -1,3 +1,4 @@
"use strict";
module.exports = function(app) {
app.get("/", function(req, res) {
res.render("index");
@@ -11,4 +12,4 @@ module.exports = function(app) {
app.get("/nodeDetails", function(req, res) {
res.render("nodeDetails");
});
};
};
+74 -99
View File
@@ -10,8 +10,9 @@
* @example
* node slave run
*/
const fs = require('fs-extra');
const path = require('path');
"use strict";
const fs = require("fs-extra");
const path = require("path");
const yargs = require("yargs");
const events = require("events");
const pidusage = require("pidusage");
@@ -22,14 +23,13 @@ const version = require("./package").version;
// internal libraries
const fileOps = require("lib/fileOps");
const hashFile = require('lib/hash').hashFile;
const factorio = require("lib/factorio");
const link = require("lib/link");
const plugin = require("lib/plugin");
const errors = require("lib/errors");
const prometheus = require('lib/prometheus');
const luaTools = require('lib/luaTools');
const config = require('lib/config');
const prometheus = require("lib/prometheus");
const luaTools = require("lib/luaTools");
const config = require("lib/config");
const instanceRconCommandsCounter = new prometheus.Counter(
@@ -69,7 +69,7 @@ function applyAsConfig(name) {
} catch (err) {
console.error(`Error applying server setting ${name}`, err);
}
}
};
}
const serverSettingsActions = {
@@ -87,7 +87,7 @@ const serverSettingsActions = {
"game_password": applyAsConfig("password"),
"require_user_verification": applyAsConfig("require-user-verification"),
"tags": applyAsConfig("tags"),
"visibility": async function action(server, value) {
"visibility": async (server, value) => {
for (let scope of ["lan", "public", "steam"]) {
try {
let enabled = Boolean(value[scope]);
@@ -104,7 +104,7 @@ const serverSettingsActions = {
*/
class Instance extends link.Link{
constructor(slave, connector, dir, factorioDir, instanceConfig) {
super('instance', 'slave', connector);
super("instance", "slave", connector);
link.attachAllMessages(this);
this._slave = slave;
this._dir = dir;
@@ -125,9 +125,9 @@ class Instance extends link.Link{
let serverOptions = {
version: this.config.get("factorio.version"),
gamePort: this.config.get('factorio.game_port'),
rconPort: this.config.get('factorio.rcon_port'),
rconPassword: this.config.get('factorio.rcon_password'),
gamePort: this.config.get("factorio.game_port"),
rconPort: this.config.get("factorio.rcon_port"),
rconPassword: this.config.get("factorio.rcon_password"),
};
this._running = false;
@@ -139,10 +139,10 @@ class Instance extends link.Link{
this.server.sendRcon = (...args) => {
instanceRconCommandsCounter.labels(String(this.config.get("instance.id"))).inc();
return originalSendRcon.call(this.server, ...args);
}
};
this.server.on('output', (output) => {
link.messages.instanceOutput.send(this, { instance_id: this.config.get("instance.id"), output })
this.server.on("output", (output) => {
link.messages.instanceOutput.send(this, { instance_id: this.config.get("instance.id"), output });
plugin.invokeHook(this.plugins, "onOutput", output);
});
@@ -181,7 +181,7 @@ class Instance extends link.Link{
for (let collector of prometheus.defaultRegistry.collectors) {
if (
collector instanceof prometheus.ValueCollector
&& collector.metric.labels.includes('instance_id')
&& collector.metric.labels.includes("instance_id")
) {
collector.removeAll({ instance_id: String(this.config.get("instance.id")) });
}
@@ -225,7 +225,7 @@ class Instance extends link.Link{
}
}
let plugins = {}
let plugins = {};
for (let [name, plugin] of this.plugins) {
plugins[name] = plugin.info.version;
}
@@ -239,7 +239,8 @@ class Instance extends link.Link{
* entries from the given settings object.
*
* @param {Object} overrides - Server settings to override.
* @returns server example settings with the given settings applied over it.
* @returns {Object}
* server example settings with the given settings applied over it.
*/
async resolveServerSettings(overrides) {
let serverSettings = await this.server.exampleSettings();
@@ -275,13 +276,11 @@ class Instance extends link.Link{
* Creates the neccessary files for starting up a new instance into the
* provided instance directory.
*
* @param {Number} id -
* ID of the new instance. Must be unique to the cluster.
* @param {String} instanceDir -
* Directory to create the new instance into.
* @param {String} factorioDir - Path to factorio installation.
*/
static async create(instanceConfig, instanceDir, factorioDir) {
static async create(instanceDir, factorioDir) {
console.log(`Clusterio | Creating ${instanceDir}`);
await fs.ensureDir(instanceDir);
await fs.ensureDir(path.join(instanceDir, "script-output"));
@@ -320,13 +319,13 @@ class Instance extends link.Link{
// Use latest save if no save was specified
if (saveName === null) {
saveName = await fileOps.getNewestFile(
this.path("saves"), (name) => !name.endsWith('.tmp.zip')
this.path("saves"), (name) => !name.endsWith(".tmp.zip")
);
}
// Create save if no save was found.
if (saveName === null) {
console.log("Clusterio | Creating new save")
console.log("Clusterio | Creating new save");
await this.server.create("world.zip");
saveName = "world.zip";
}
@@ -337,12 +336,12 @@ class Instance extends link.Link{
// Find plugin modules to patch in
let modules = new Map();
for (let [pluginName, plugin] of this.plugins) {
let modulePath = path.join('plugins', pluginName, 'module');
let modulePath = path.join("plugins", pluginName, "module");
if (!await fs.pathExists(modulePath)) {
continue;
}
let moduleJsonPath = path.join(modulePath, 'module.json');
let moduleJsonPath = path.join(modulePath, "module.json");
if (!await fs.pathExists(moduleJsonPath)) {
throw new Error(`Module for plugin ${pluginName} is missing module.json`);
}
@@ -352,25 +351,26 @@ class Instance extends link.Link{
throw new Error(`Expected name of module for plugin ${pluginName} to match the plugin name`);
}
module = Object.assign({
module = {
version: plugin.info.version,
dependencies: { 'clusterio': '*' },
dependencies: { "clusterio": "*" },
path: modulePath,
load: [],
require: [],
}, module);
...module,
};
modules.set(module.name, module);
}
// Find stand alone modules to load
// XXX for now it's assumed all available modules should be loaded.
for (let entry of await fs.readdir('modules', { withFileTypes: true })) {
for (let entry of await fs.readdir("modules", { withFileTypes: true })) {
if (entry.isDirectory()) {
if (modules.has(entry.name)) {
throw new Error(`Module with name ${entry.name} already exists in a plugin`);
}
let moduleJsonPath = path.join('modules', entry.name, 'module.json');
let moduleJsonPath = path.join("modules", entry.name, "module.json");
if (!await fs.pathExists(moduleJsonPath)) {
throw new Error(`Module ${entry.name} is missing module.json`);
}
@@ -380,12 +380,13 @@ class Instance extends link.Link{
throw new Error(`Expected name of module ${entry.name} to match the directory name`);
}
module = Object.assign({
path: path.join('modules', entry.name),
dependencies: { 'clusterio': '*' },
module = {
path: path.join("modules", entry.name),
dependencies: { "clusterio": "*" },
load: [],
require: [],
}, module);
...module,
};
modules.set(module.name, module);
}
}
@@ -402,13 +403,13 @@ class Instance extends link.Link{
* @param {String} saveName - Name of save game to load.
*/
async start(saveName) {
this.server.on('rcon-ready', () => {
this.server.on("rcon-ready", () => {
console.log("Clusterio | RCON connection established");
});
this.server.on("exit", () => this.notifyExit());
await this.server.start(saveName);
await this.server.disableAchievements()
await this.server.disableAchievements();
await this.updateInstanceData();
await plugin.invokeHook(this.plugins, "onStart");
@@ -459,12 +460,12 @@ class Instance extends link.Link{
}
async getMetricsRequestHandler() {
let results = []
let results = [];
if (this._running) {
let pluginResults = await plugin.invokeHook(this.plugins, "onMetrics");
for (let metricIterator of pluginResults) {
for await (let metric of metricIterator) {
results.push(prometheus.serializeResult(metric))
results.push(prometheus.serializeResult(metric));
}
}
}
@@ -482,7 +483,7 @@ class Instance extends link.Link{
async startInstanceRequestHandler(message) {
let saveName = message.data.save;
try {
saveName = await this.prepare(saveName)
saveName = await this.prepare(saveName);
} catch (err) {
this.notifyExit();
throw err;
@@ -536,7 +537,7 @@ class Instance extends link.Link{
},
});
if (response.statusCode !== 200) {
throw Error(`Upload failed: ${response.statusCode} ${response.statusMessage}: ${response.body}`)
throw Error(`Upload failed: ${response.statusCode} ${response.statusMessage}: ${response.body}`);
}
} finally {
@@ -569,6 +570,8 @@ class Instance extends link.Link{
* the directory of the instance. For example instance.path("mods")
* returns a path to the mods directory of the instance. If no parts are
* given it returns a path to the directory of the instance.
*
* @returns {string} path in instance directory.
*/
path(...parts) {
return path.join(this._dir, ...parts);
@@ -585,7 +588,7 @@ class Instance extends link.Link{
* @param {Map<integer, Object>} instanceInfos -
* mapping between instance id and information about this instance.
* @param {string} instancesDir - Directory containing instances
* @param logger - console like logging interface.
* @param {Object} logger - console like logging interface.
*/
async function discoverInstances(instanceInfos, instancesDir, logger) {
for (let entry of await fs.readdir(instancesDir, { withFileTypes: true })) {
@@ -622,7 +625,7 @@ async function discoverInstances(instanceInfos, instancesDir, logger) {
class InstanceConnection extends link.Link {
constructor(connector, slave, instanceId) {
super('slave', 'instance', connector);
super("slave", "instance", connector);
this.slave = slave;
this.instanceId = instanceId;
this.plugins = new Map();
@@ -717,7 +720,7 @@ class Slave extends link.Link {
// I don't like God classes, but the alternative of putting all this state
// into global variables is not much better.
constructor(connector, slaveConfig, pluginInfos) {
super('slave', 'master', connector);
super("slave", "master", connector);
link.attachAllMessages(this);
this.pluginInfos = pluginInfos;
@@ -750,9 +753,9 @@ class Slave extends link.Link {
setBlocking(true);
console.error("ERROR: Unexpected error during shutdown");
console.error(err);
process.exit(1)
process.exit(1);
});
});;
});
});
this.connector.on("close", () => {
@@ -769,7 +772,7 @@ class Slave extends link.Link {
setBlocking(true);
console.error("ERROR: Unexpected error during shutdown");
console.error(err);
process.exit(1)
process.exit(1);
});
});
@@ -779,7 +782,7 @@ class Slave extends link.Link {
setBlocking(true);
console.error("ERROR: Unexpected error during shutdown");
console.error(err);
process.exit(1)
process.exit(1);
});
}
});
@@ -795,7 +798,7 @@ class Slave extends link.Link {
async _findNewInstanceDir(name) {
try {
checkFilename(name)
checkFilename(name);
} catch (err) {
throw new Error(`Instance name ${err.message}`);
}
@@ -803,7 +806,7 @@ class Slave extends link.Link {
// For now add dashes until an unused directory name is found
let dir = path.join(this.config.get("slave.instances_directory"), name);
while (await fs.pathExists(dir)) {
dir += '-';
dir += "-";
}
return dir;
@@ -821,7 +824,7 @@ class Slave extends link.Link {
}
if (request.plugin && !instanceConnection.plugins.has(request.plugin)) {
throw new errors.RequestError(`Instance ID ${instanceId} does not have ${request.plugin} plugin loaded`)
throw new errors.RequestError(`Instance ID ${instanceId} does not have ${request.plugin} plugin loaded`);
}
return await request.send(instanceConnection, message.data);
@@ -861,7 +864,7 @@ class Slave extends link.Link {
// XXX: race condition on multiple simultanious calls
let instanceDir = await this._findNewInstanceDir(instanceConfig.get("instance.name"));
await Instance.create(instanceConfig, instanceDir, this.config.get("slave.factorio_directory"));
await Instance.create(instanceDir, this.config.get("slave.factorio_directory"));
instanceInfo = {
path: instanceDir,
config: instanceConfig,
@@ -872,10 +875,10 @@ class Slave extends link.Link {
// save a copy of the instance config
let warnedOutput = Object.assign(
{ _warning: "Changes to this file will be overwritten by the master server's copy." },
instanceInfo.config.serialize()
);
let warnedOutput = {
_warning: "Changes to this file will be overwritten by the master server's copy.",
...instanceInfo.config.serialize(),
};
await fs.outputFile(
path.join(instanceInfo.path, "instance.json"),
JSON.stringify(warnedOutput, null, 4)
@@ -884,6 +887,9 @@ class Slave extends link.Link {
/**
* Initialize and connect an unloaded instance
*
* @param {number} instanceId - ID of instance to initialize.
* @returns {module:slave~InstanceConnection} connection to instance.
*/
async _connectInstance(instanceId) {
let instanceInfo = this.instanceInfos.get(instanceId);
@@ -919,10 +925,10 @@ class Slave extends link.Link {
}
for await (let result of prometheus.defaultRegistry.collect()) {
if (result.metric.name.startsWith('process_')) {
if (result.metric.name.startsWith("process_")) {
results.push(prometheus.serializeResult(result, {
addLabels: { 'slave_id': String(this.config.get("slave.id")) },
metricName: result.metric.name.replace('process_', 'clusterio_slave_'),
addLabels: { "slave_id": String(this.config.get("slave.id")) },
metricName: result.metric.name.replace("process_", "clusterio_slave_"),
}));
} else {
@@ -1034,10 +1040,10 @@ function checkFilename(name) {
const oneToNine = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const badNames = [
// Relative path components
'.', '..',
".", "..",
// Reserved filenames in Windows
'CON', 'PRN', 'AUX', 'NUL',
"CON", "PRN", "AUX", "NUL",
...oneToNine.map(n => `COM${n}`),
...oneToNine.map(n => `LPT${n}`),
];
@@ -1099,7 +1105,7 @@ async function symlinkMods(instance, sharedMods, logger) {
let instanceModsEntries = new Set(await fs.readdir(instance.path("mods")));
for (let entry of await fs.readdir(sharedMods, { withFileTypes: true })) {
if (entry.isFile()) {
if (['.zip', '.dat'].includes(path.extname(entry.name))) {
if ([".zip", ".dat"].includes(path.extname(entry.name))) {
if (!instanceModsEntries.has(entry.name)) {
logger.log(`linking ${entry.name} from ${sharedMods}`);
let target = path.join(sharedMods, entry.name);
@@ -1130,17 +1136,17 @@ async function symlinkMods(instance, sharedMods, logger) {
async function startSlave() {
// add better stack traces on promise rejection
process.on('unhandledRejection', r => console.log(r));
process.on("unhandledRejection", r => console.log(r));
// argument parsing
const args = yargs
.scriptName("slave")
.usage("$0 <command> [options]")
.option('config', {
.option("config", {
nargs: 1,
describe: "slave config file to use",
default: 'config-slave.json',
type: 'string',
default: "config-slave.json",
type: "string",
})
.command("config", "Manage Slave config", config.configCommand)
.command("run", "Run slave")
@@ -1160,7 +1166,7 @@ async function startSlave() {
await slaveConfig.load(JSON.parse(await fs.readFile(args.config)));
} catch (err) {
if (err.code === 'ENOENT') {
if (err.code === "ENOENT") {
console.log("Config not found, initializing new config");
await slaveConfig.init();
@@ -1210,8 +1216,8 @@ async function startSlave() {
let slave = new Slave(slaveConnector, slaveConfig, pluginInfos);
// Handle interrupts
let secondSigint = false
process.on('SIGINT', () => {
let secondSigint = false;
process.on("SIGINT", () => {
if (secondSigint) {
console.log("Caught second interrupt, terminating immediately");
process.exit(1);
@@ -1232,37 +1238,6 @@ async function startSlave() {
});
await slaveConnector.connect();
/*
} else if (command == "manage"){
await manage(config, instance);
// process.exit(0);
*/
}
// string, function
// returns [{modName:string,hash:string}, ... ]
function hashMods(instance, callback) {
if(!callback) {
throw new Error("ERROR in function hashMods NO CALLBACK");
}
function hashMod(name) {
if (path.extname(name) != ".zip") {
// Can't hash unzipped mods, return null that's filtered out later
return null;
} else {
return hashFile(instance.path("mods", name)).then(hash => (
{modName: name, hash: hash}
));
}
}
let promises = fs.readdirSync(instance.path("mods")).map(hashMod);
Promise.all(promises).then(hashes => {
// Remove null entries from hashMod
callback(hashes.filter(entry => entry !== null));
});
}
module.exports = {
+12 -11
View File
@@ -1,10 +1,11 @@
const assert = require('assert').strict;
const chalk = require('chalk');
"use strict";
const assert = require("assert").strict;
const chalk = require("chalk");
const errors = require('lib/errors');
const { testLines } = require('./lib/factorio/lines');
const clusterctl = require('../clusterctl.js');
const mock = require('./mock');
const errors = require("lib/errors");
const { testLines } = require("./lib/factorio/lines");
const clusterctl = require("../clusterctl.js");
const mock = require("./mock");
describe("clusterctl", function() {
@@ -24,13 +25,13 @@ describe("clusterctl", function() {
});
let mockConnector = new mock.MockConnector();
mockConnector.on('send', function(message) {
if (message.type === 'list_instances_request') {
this.emit('message', {
seq: 1, type: 'list_instances_response',
mockConnector.on("send", function(message) {
if (message.type === "list_instances_request") {
this.emit("message", {
seq: 1, type: "list_instances_response",
data: {
seq: message.seq,
list: [{ id: 57, assigned_slave: 4, name: 'Test Instance' }],
list: [{ id: 57, assigned_slave: 4, name: "Test Instance" }],
},
});
}
+7 -6
View File
@@ -1,8 +1,9 @@
"use strict";
const assert = require("assert").strict;
const fs = require("fs-extra");
const path = require("path");
const validateHTML = require('html5-validator');
const parallel = require('mocha.parallel');
const validateHTML = require("html5-validator");
const parallel = require("mocha.parallel");
const link = require("lib/link");
@@ -113,7 +114,7 @@ describe("Integration of Clusterio", function() {
],
[
"ignore_player_limit_for_returning_players", true,
"ignore-player-limit-for-returning-players", "Ignore player limit for returning players: true."
"ignore-player-limit-for-returning-players", "Ignore player limit for returning players: true.",
],
[
"max_players", 11,
@@ -155,7 +156,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 = `--instance test --field factorio.settings --prop ${prop} --value ${value}`;
await exec(`node clusterctl --config ${controlConfigPath} set-instance-config-prop ${args}`);
@@ -202,7 +203,7 @@ describe("Integration of Clusterio", function() {
await exec(`node clusterctl --config ${controlConfigPath} create-role --name temp --description "A temp role" --permissions core.control.connect`);
let result = await link.messages.listRoles.send(getControl());
let role = result.list.find(role => role.name === "temp");
assert.deepEqual(role, { id: 5, name: "temp", description: "A temp role", permissions: ["core.control.connect"]});
assert.deepEqual(role, { id: 5, name: "temp", description: "A temp role", permissions: ["core.control.connect"] });
});
});
@@ -211,7 +212,7 @@ describe("Integration of Clusterio", function() {
await exec(`node clusterctl --config ${controlConfigPath} edit-role --role temp --name new --description "A new role" --permissions`);
let result = await link.messages.listRoles.send(getControl());
let role = result.list.find(role => role.name === "new");
assert.deepEqual(role, { id: 5, name: "new", description: "A new role", permissions: []});
assert.deepEqual(role, { id: 5, name: "new", description: "A new role", permissions: [] });
});
});
+1
View File
@@ -1,3 +1,4 @@
"use strict";
const assert = require("assert").strict;
const path = require("path");
+6 -3
View File
@@ -1,3 +1,4 @@
"use strict";
const path = require("path");
const fs = require("fs-extra");
const child_process = require("child_process");
@@ -22,6 +23,7 @@ class TestControl extends link.Link {
}
async debugWsMessageEventHandler() { }
async instanceOutputEventHandler() { }
}
@@ -34,6 +36,7 @@ class TestControlConnector extends link.WebSocketClientConnector {
// Mark that this test takes a lot of time, or depeneds on a test
// that takes a lot of time.
function slowTest(test) {
// eslint-disable-next-line no-process-env
if (process.env.FAST_TEST) {
test.skip();
}
@@ -67,7 +70,7 @@ async function exec(...args) {
}
async function sendRcon(instanceId, command) {
let response = await link.messages.sendRcon.send(control, { instance_id: instanceId, command })
let response = await link.messages.sendRcon.send(control, { instance_id: instanceId, command });
return response.result;
}
@@ -86,8 +89,8 @@ function spawn(name, cmd, waitFor) {
resolve(process);
}
console.log(name, line);
})
let stderr = new server._LineSplitter((line) => { console.log(name, line.toString("utf8")); })
});
let stderr = new server._LineSplitter((line) => { console.log(name, line.toString("utf8")); });
process.stdout.on("data", chunk => { stdout.data(chunk); });
process.stdout.on("close", () => { stdout.end(); });
process.stderr.on("data", chunk => { stderr.data(chunk); });
+1
View File
@@ -1,3 +1,4 @@
"use strict";
const assert = require("assert").strict;
const events = require("events");
const jwt = require("jsonwebtoken");
+2 -1
View File
@@ -1,3 +1,4 @@
"use strict";
const assert = require("assert").strict;
const fs = require("fs-extra");
const jszip = require("jszip");
@@ -9,7 +10,7 @@ const { slowTest } = require("./index");
// The server integration test is required to run before this one
require("./server")
require("./server");
describe("Integration of lib/factorio/patch", function() {
describe("patch()", function() {
+9 -8
View File
@@ -1,3 +1,4 @@
"use strict";
const assert = require("assert").strict;
const events = require("events");
const fs = require("fs-extra");
@@ -26,13 +27,13 @@ describe("Integration of lib/factorio/server", function() {
before(async function() {
// Delete result from previous run of these tests
if (fs.existsSync(writePath)) {
if (await fs.pathExists(writePath)) {
await fs.remove(writePath);
}
await fs.ensureDir(writePath);
logFile = fs.createWriteStream(path.join(writePath, "log.txt"), "utf8");
server.on('output', function(output) {
server.on("output", function(output) {
logFile.write(JSON.stringify(output) + "\n");
});
@@ -119,7 +120,7 @@ describe("Integration of lib/factorio/server", function() {
log(".sendRcon()");
let result = await server.sendRcon("/sc rcon.print('success')");
assert.equal(result, 'success\n');
assert.equal(result, "success\n");
});
it("throws on non-empty response when enabled", async function() {
slowTest(this);
@@ -149,20 +150,20 @@ describe("Integration of lib/factorio/server", function() {
slowTest(this);
log(".startScenario()");
let pass = false
let pass = false;
function filter(output) {
if (output.message === "test_scenario init") {
pass = true;
}
}
server.on('output', filter);
server.on("output", filter);
await server.startScenario("test_scenario");
log(".stop()");
await server.stop();
server.off('output', filter);
server.off("output", filter);
assert(pass, "server did not output line from test scenario");
});
});
@@ -191,7 +192,7 @@ describe("Integration of lib/factorio/server", function() {
await server.start("test.zip");
if (!server._rconReady) {
await events.once(server, 'rcon-ready');
await events.once(server, "rcon-ready");
}
server.sendRcon("/c while true do end").catch(() => {});
await new Promise((resolve) => setTimeout(resolve, 300));
@@ -210,7 +211,7 @@ describe("Integration of lib/factorio/server", function() {
log(".start() for kill detection");
let startPromise = server.start("test.zip");
server.once('output', () => server._server.kill('SIGKILL'));
server.once("output", () => server._server.kill("SIGKILL"));
await assert.rejects(
startPromise,
+39 -38
View File
@@ -25,35 +25,35 @@ describe("lib/config/classes", function() {
for (let [def, error] of [
[
{ name: "a", type: "invalid", optional: true },
"invalid is not a valid type"
"invalid is not a valid type",
],
[
{ name: [], type: "string", optional: true },
"name must be a string"
"name must be a string",
],
[
{ name: "a", type: "string", enum: true, optional: true },
"enum must be an array"
"enum must be an array",
],
[
{ name: "a", title: 2, type: "string", optional: true },
"title must be a string"
"title must be a string",
],
[
{ name: "a", description: 2, type: "string", optional: true },
"description must be a string"
"description must be a string",
],
[
{ name: "a", type: "string", optional: "yes" },
"optional must be a boolean"
"optional must be a boolean",
],
[
{ name: "a", type: "string", initial_value: 1 },
"initial_value must match the type or be a function"
"initial_value must match the type or be a function",
],
[
{ name: "a", type: "string" },
"Non-optional field a needs an initial_value"
"Non-optional field a needs an initial_value",
],
]) {
assert.throws(() => TestGroup.define(def), new Error(error));
@@ -97,13 +97,13 @@ describe("lib/config/classes", function() {
enum: ["a", "b", "c"],
optional: true,
initial_value: "b",
}
};
TestGroup.define(field);
// Generated properties
field.fullName = "test_group.enum";
assert.deepEqual(TestGroup._definitions.get("enum"), field);
})
});
});
describe("get definitions", function() {
@@ -191,7 +191,7 @@ describe("lib/config/classes", function() {
func: 22,
bool: null,
json: { valid: true },
}})
}});
assert.equal(testInstance.get("enum"), "c");
assert.equal(testInstance.get("test"), "blah");
@@ -218,7 +218,7 @@ describe("lib/config/classes", function() {
alpha: null,
beta: "decay",
gamma: 99,
}
};
let testInstance = new TestGroup();
await testInstance.load(null, { name: "test_group", fields: testFields });
@@ -253,11 +253,11 @@ describe("lib/config/classes", function() {
new Error("Expected fields to be an object, not undefined")
);
assert.throws(
() => testInstance.update({ name: "test_group", fields: []}, false),
() => testInstance.update({ name: "test_group", fields: [] }, false),
new Error("Expected fields to be an object, not array")
);
assert.throws(
() => testInstance.update({ name: "test_group", fields: null}, false),
() => testInstance.update({ name: "test_group", fields: null }, false),
new Error("Expected fields to be an object, not null")
);
});
@@ -277,43 +277,43 @@ describe("lib/config/classes", function() {
() => testInstance.set("bar", 1),
new classes.InvalidField("No field named 'bar'")
);
})
});
it("should throw if field is not in enum", function() {
assert.throws(
() => testInstance.set("enum", "bar"),
new Error("Expected one of [a, b, c], not bar")
);
})
});
it("should work if field is in enum", function() {
testInstance.set("enum", "c"),
testInstance.set("enum", "c");
assert.equal(testInstance.get("enum"), "c");
})
});
it("should throw if field is not optional", function() {
assert.throws(
() => testInstance.set("func", null),
new Error("Field func cannot be null")
);
})
});
it("should work if field is optional", function() {
testInstance.set("test", "spam"),
testInstance.set("test", "spam");
assert.equal(testInstance.get("test"), "spam");
})
});
it("should throw if field is of wrong type", function() {
assert.throws(
() => testInstance.set("test", 1),
new Error("Expected type of test to be string, not number")
);
})
});
it("should treat empty string as null", function() {
testInstance.set("test", ""),
testInstance.set("test", "");
assert.equal(testInstance.get("test"), null);
})
});
it("should auto convert string to boolean if possible", function() {
testInstance.set("bool", "true");
@@ -374,14 +374,14 @@ describe("lib/config/classes", function() {
() => testInstance.setProp("bar", 1),
new classes.InvalidField("No field named 'bar'")
);
})
});
it("should throw if field is not an object", function() {
assert.throws(
() => testInstance.setProp("enum", "a"),
new classes.InvalidField("Cannot set property on non-object field 'enum'")
);
})
});
it("should work if field is an object", function() {
testInstance.set("json", { prev: 32, test: false });
@@ -451,7 +451,8 @@ describe("lib/config/classes", function() {
});
it("should construct a finalized class", function() {
TestConfig.finalize();
new TestConfig();
let config = new TestConfig();
assert(config);
});
});
@@ -461,7 +462,7 @@ describe("lib/config/classes", function() {
await testInstance.init();
assert.equal(testInstance.get("alpha.foo"), null);
assert.deepEqual(testInstance.get("beta.bar"), {});
})
});
});
describe(".serialize()", function() {
@@ -477,10 +478,10 @@ describe("lib/config/classes", function() {
{
name: "beta",
fields: { bar: {} },
}
]
},
],
});
})
});
});
describe(".load()", function() {
@@ -521,8 +522,8 @@ describe("lib/config/classes", function() {
{
name: "beta",
fields: { bar: { value: 20 }},
}
]
},
];
let testInstance = new TestConfig();
await testInstance.load({ groups: testGroups });
@@ -543,7 +544,7 @@ describe("lib/config/classes", function() {
{
name: "beta",
fields: { bar: { value: 30 }},
}
},
]}, false);
assert.deepEqual(testInstance.serialize(), {
@@ -559,10 +560,10 @@ describe("lib/config/classes", function() {
{
name: "beta",
fields: { bar: { value: 30 }},
}
},
],
});
})
});
});
describe(".group()", function() {
@@ -580,14 +581,14 @@ describe("lib/config/classes", function() {
let testInstance = new TestConfig();
await testInstance.init();
assert.throws(() => testInstance.get("invalid"), new Error("No config group named 'invalid'"))
assert.throws(() => testInstance.get("invalid"), new Error("No config group named 'invalid'"));
});
it("should throw if no field is specified", async function() {
let testInstance = new TestConfig();
await testInstance.init();
assert.throws(() => testInstance.get("alpha"), new Error("No field named ''"))
assert.throws(() => testInstance.get("alpha"), new Error("No field named ''"));
});
});
+43 -42
View File
@@ -1,3 +1,4 @@
"use strict";
const assert = require("assert").strict;
const fs = require("fs-extra");
const path = require("path");
@@ -12,21 +13,21 @@ describe("lib/database", function() {
new Error("Expected all keys to be string but got number")
);
assert.throws(
() => database.mapToObject(new Map([[Symbol(), 1]])),
() => database.mapToObject(new Map([[Symbol("test"), 1]])),
new Error("Expected all keys to be string but got symbol")
);
});
it("should throw convert a map to an object", function() {
assert.deepEqual(
database.mapToObject(new Map([['a', 1], ['b', true]])),
{'a': 1, 'b': true}
database.mapToObject(new Map([["a", 1], ["b", true]])),
{ "a": 1, "b": true }
);
});
});
describe("loadJsonAsMap()", function() {
let badTypes = ['null', 'array', 'number', 'string', 'boolean'];
let badTypes = ["null", "array", "number", "string", "boolean"];
for (let type of badTypes) {
it(`should reject on ${type} JSON`, async function() {
@@ -47,7 +48,7 @@ describe("lib/database", function() {
it("should work on object JSON", async function() {
assert.deepEqual(
await database.loadJsonAsMap(path.join("test", "file", "json", "load_map.json")),
new Map([['a', 1], ['b', true]])
new Map([["a", 1], ["b", true]])
);
});
@@ -67,7 +68,7 @@ describe("lib/database", function() {
await fs.unlink(testFile);
} catch (err) {
/* istanbul ignore if */
if (err.code !== 'ENOENT') {
if (err.code !== "ENOENT") {
throw err;
}
}
@@ -75,18 +76,18 @@ describe("lib/database", function() {
await deleteTestFile();
await database.saveMapAsJson(
testFile, new Map([['c', {}], ['d', "foo"]])
testFile, new Map([["c", {}], ["d", "foo"]])
);
assert.equal(
await fs.readFile(testFile, {encoding: 'utf-8'}),
await fs.readFile(testFile, { encoding: "utf-8" }),
'{\n "c": {},\n "d": "foo"\n}'
);
});
});
describe("loadJsonArrayAsMap()", function() {
let badTypes = ['null', 'object', 'number', 'string', 'boolean'];
let badTypes = ["null", "object", "number", "string", "boolean"];
for (let type of badTypes) {
it(`should reject on ${type} JSON`, async function() {
@@ -121,7 +122,7 @@ describe("lib/database", function() {
it("should work on array JSON", async function() {
assert.deepEqual(
await database.loadJsonArrayAsMap(path.join("test", "file", "json", "load_array_map.json")),
new Map([['a', {id: 'a'}], ['b', {id: 'b'}]])
new Map([["a", { id: "a" }], ["b", { id: "b" }]])
);
});
@@ -141,7 +142,7 @@ describe("lib/database", function() {
await fs.unlink(testFile);
} catch (err) {
/* istanbul ignore if */
if (err.code !== 'ENOENT') {
if (err.code !== "ENOENT") {
throw err;
}
}
@@ -149,11 +150,11 @@ describe("lib/database", function() {
await deleteTestFile();
await database.saveMapAsJsonArray(
testFile, new Map([['c', {id: 'c'}], ['d', {id: 'd'}]])
testFile, new Map([["c", { id: "c" }], ["d", { id: "d" }]])
);
assert.equal(
await fs.readFile(testFile, {encoding: 'utf-8'}),
await fs.readFile(testFile, { encoding: "utf-8" }),
'[\n {\n "id": "c"\n },\n {\n "id": "d"\n }\n]'
);
});
@@ -167,18 +168,18 @@ describe("lib/database", function() {
});
it("should restore the passed serialized database", function() {
let items = new database.ItemDatabase({'a': 1, 'b': 2});
assert.deepEqual(items._items, new Map([['a', 1], ['b', 2]]));
let items = new database.ItemDatabase({ "a": 1, "b": 2 });
assert.deepEqual(items._items, new Map([["a", 1], ["b", 2]]));
});
it("should throw on invalid serialized database", function() {
assert.throws(
() => new database.ItemDatabase({'a': NaN}),
() => new database.ItemDatabase({ "a": NaN }),
new Error("count must be a number")
);
assert.throws(
() => new database.ItemDatabase({'a': 'a'}),
() => new database.ItemDatabase({ "a": "a" }),
new Error("count must be a number")
);
});
@@ -186,32 +187,32 @@ describe("lib/database", function() {
describe(".serialize()", function() {
it("should return a serialized database", function() {
let items = new database.ItemDatabase({'a': 10});
assert.deepEqual(items.serialize(), {'a': 10});
let items = new database.ItemDatabase({ "a": 10 });
assert.deepEqual(items.serialize(), { "a": 10 });
});
it("should remove zero count entries", function() {
let items = new database.ItemDatabase({'a': 0});
let items = new database.ItemDatabase({ "a": 0 });
assert.deepEqual(items.serialize(), {});
});
});
describe(".size", function() {
it("should give an approximate size of the database", function() {
let items = new database.ItemDatabase({'a': 10});
let items = new database.ItemDatabase({ "a": 10 });
assert.equal(items.size, 1);
});
});
describe(".getItemCount()", function() {
it("should return the count of the given item", function() {
let items = new database.ItemDatabase({'a': 10});
assert.equal(items.getItemCount('a'), 10);
let items = new database.ItemDatabase({ "a": 10 });
assert.equal(items.getItemCount("a"), 10);
});
it("should return zero if item does not exist", function() {
let items = new database.ItemDatabase();
assert.equal(items.getItemCount('b'), 0);
assert.equal(items.getItemCount("b"), 0);
});
it("should throw on invalid name", function() {
@@ -226,15 +227,15 @@ describe("lib/database", function() {
describe(".addItem()", function() {
it("should add a new item", function() {
let items = new database.ItemDatabase();
items.addItem('a', 10);
assert.deepEqual(items._items, new Map([['a', 10]]));
})
items.addItem("a", 10);
assert.deepEqual(items._items, new Map([["a", 10]]));
});
it("should add an existing item", function() {
let items = new database.ItemDatabase({'a': 10});
items.addItem('a', 10);
assert.deepEqual(items._items, new Map([['a', 20]]));
})
let items = new database.ItemDatabase({ "a": 10 });
items.addItem("a", 10);
assert.deepEqual(items._items, new Map([["a", 20]]));
});
it("should throw on invalid name", function() {
let items = new database.ItemDatabase();
@@ -247,11 +248,11 @@ describe("lib/database", function() {
it("should throw on invalid count", function() {
let items = new database.ItemDatabase();
assert.throws(
() => items.addItem('a', NaN),
() => items.addItem("a", NaN),
new Error("count must be a number")
);
assert.throws(
() => items.addItem('a', "1"),
() => items.addItem("a", "1"),
new Error("count must be a number")
);
});
@@ -259,16 +260,16 @@ describe("lib/database", function() {
describe(".removeItem()", function() {
it("should remove an existing item", function() {
let items = new database.ItemDatabase({'a': 20});
items.removeItem('a', 10);
assert.deepEqual(items._items, new Map([['a', 10]]));
})
let items = new database.ItemDatabase({ "a": 20 });
items.removeItem("a", 10);
assert.deepEqual(items._items, new Map([["a", 10]]));
});
it("should turn a non-existing item negative", function() {
let items = new database.ItemDatabase();
items.removeItem('a', 10);
assert.deepEqual(items._items, new Map([['a', -10]]));
})
items.removeItem("a", 10);
assert.deepEqual(items._items, new Map([["a", -10]]));
});
it("should throw on invalid name", function() {
let items = new database.ItemDatabase();
@@ -281,11 +282,11 @@ describe("lib/database", function() {
it("should throw on invalid count", function() {
let items = new database.ItemDatabase();
assert.throws(
() => items.removeItem('a', 'b'),
() => items.removeItem("a", "b"),
new Error("count must be a number")
);
assert.throws(
() => items.removeItem('a', "1"),
() => items.removeItem("a", "1"),
new Error("count must be a number")
);
});
+2 -1
View File
@@ -1,3 +1,4 @@
"use strict";
const assert = require("assert").strict;
const path = require("path");
@@ -17,5 +18,5 @@ describe("lib/factorio/export", function() {
let locale = await factorio._exportLocale(testServer, new Map(), ["base"], "en");
assert.deepEqual(locale, new Map([["test.key-a", "1"], ["test.key-b", "2"]]));
});
});;
});
});
+52 -51
View File
@@ -1,113 +1,114 @@
// Lines for testing the factorio output
"use strict";
let testLines = new Map([
[
" 1.306 Info RemoteCommandProcessor.cpp:131: Starting RCON interface at port 4000",
{
format: 'seconds',
time: '1.306',
type: 'log',
level: 'Info',
format: "seconds",
time: "1.306",
type: "log",
level: "Info",
file: "RemoteCommandProcessor.cpp:131",
message: "Starting RCON interface at port 4000",
}
},
],
[
" 0.622 Warning FileUtil.cpp:527: test not found; using test.zip",
{
format: 'seconds',
time: '0.622',
type: 'log',
level: 'Warning',
format: "seconds",
time: "0.622",
type: "log",
level: "Warning",
file: "FileUtil.cpp:527",
message: "test not found; using test.zip",
}
},
],
[
" 0.554 Error Main.cpp:839: require_user_verification must be enabled for public games.",
{
format: 'seconds',
time: '0.554',
type: 'log',
level: 'Error',
format: "seconds",
time: "0.554",
type: "log",
level: "Error",
file: "Main.cpp:839",
message: "require_user_verification must be enabled for public games.",
}
},
],
[
" 640.910 Quitting: remote-quit.",
{
format: 'seconds',
time: '640.910',
type: 'generic',
format: "seconds",
time: "640.910",
type: "generic",
message: "Quitting: remote-quit.",
}
},
],
[
"1641.421 Goodbye",
{
format: 'seconds',
time: '1641.421',
type: 'generic',
format: "seconds",
time: "1641.421",
type: "generic",
message: "Goodbye",
}
},
],
[
"2019-01-20 10:30:04 [JOIN] User joined the game",
{
format: 'date',
time: '2019-01-20 10:30:04',
type: 'action',
format: "date",
time: "2019-01-20 10:30:04",
type: "action",
action: "JOIN",
message: "User joined the game",
}
},
],
[
"2019-01-20 10:30:07 [CHAT] User: chat message",
{
format: 'date',
time: '2019-01-20 10:30:07',
type: 'action',
format: "date",
time: "2019-01-20 10:30:07",
type: "action",
action: "CHAT",
message: "User: chat message",
}
},
],
[
"2019-01-20 10:30:14 [COMMAND] User (command): blah",
{
format: 'date',
time: '2019-01-20 10:30:14',
type: 'action',
format: "date",
time: "2019-01-20 10:30:14",
type: "action",
action: "COMMAND",
message: "User (command): blah",
}
},
],
[
"2019-01-20 10:30:14 Cannot execute command. Error: [string \"blah\"]:1: syntax error near <eof>",
{
format: 'date',
time: '2019-01-20 10:30:14',
type: 'generic',
format: "date",
time: "2019-01-20 10:30:14",
type: "generic",
message: "Cannot execute command. Error: [string \"blah\"]:1: syntax error near <eof>",
}
},
],
[
"2019-01-20 10:30:21 [LEAVE] User left the game",
{
format: 'date',
time: '2019-01-20 10:30:21',
type: 'action',
format: "date",
time: "2019-01-20 10:30:21",
type: "action",
action: "LEAVE",
message: "User left the game",
}
},
],
[
"Error while running event level::on_nth_tick(1)",
{
format: 'none',
type: 'generic',
format: "none",
type: "generic",
message: "Error while running event level::on_nth_tick(1)",
}
},
],
[
"4802.763 Info AppManager.cpp:287: Saving to _autosave4 (blocking).",
@@ -118,7 +119,7 @@ let testLines = new Map([
level: "Info",
file: "AppManager.cpp:287",
message: "Saving to _autosave4 (blocking).",
}
},
],
[
" 10.806 Info AppManager.cpp:394: Saving game as C:\\factorio\\saves\\world.zip",
@@ -129,7 +130,7 @@ let testLines = new Map([
level: "Info",
file: "AppManager.cpp:394",
message: "Saving game as C:\\factorio\\saves\\world.zip",
}
},
],
[
"4202.780 Info AppManagerStates.cpp:1802: Saving finished",
@@ -140,10 +141,10 @@ let testLines = new Map([
level: "Info",
file: "AppManagerStates.cpp:1802",
message: "Saving finished",
}
},
],
])
]);
module.exports = {
testLines,
}
};
+32 -31
View File
@@ -1,3 +1,4 @@
"use strict";
const assert = require("assert").strict;
const fs = require("fs-extra");
const jszip = require("jszip");
@@ -50,20 +51,20 @@ describe("lib/factorio", function() {
describe("generateLoader()", function() {
let reference = [
'-- Auto generated scenario module loader created by Clusterio',
'-- Modifications to this file will be lost when loaded in Clusterio',
'clusterio_patch_number = 1',
'',
"-- Auto generated scenario module loader created by Clusterio",
"-- Modifications to this file will be lost when loaded in Clusterio",
"clusterio_patch_number = 1",
"",
'local event_handler = require("event_handler")',
'',
'-- Scenario modules',
"",
"-- Scenario modules",
'event_handler.add_lib(require("foo"))',
'',
'-- Clusterio modules',
"",
"-- Clusterio modules",
'event_handler.add_lib(require("modules/spam/bar"))',
'require("modules/spam/spam")',
'',
].join('\n');
"",
].join("\n");
let patchInfo = {
"patch_number": 1,
"scenario": { "modules": ["foo"] },
@@ -85,15 +86,15 @@ describe("lib/factorio", function() {
describe("reorderDependencies()", function() {
it("should reorder to satisfy simple dependency", function() {
let modules = [
{ name: 'a', version: '1.0.0', dependencies: {'b': '*'} },
{ name: 'b', version: '1.0.0', dependencies: {} },
{ name: "a", version: "1.0.0", dependencies: {"b": "*"} },
{ name: "b", version: "1.0.0", dependencies: {} },
];
factorio._reorderDependencies(modules);
assert(modules[0].name === 'b', "Dependency was not reorederd");
assert(modules[0].name === "b", "Dependency was not reorederd");
});
it("should throw on invalid version", function() {
let modules = [
{ name: 'a', version: 'foo', dependencies: {} },
{ name: "a", version: "foo", dependencies: {} },
];
assert.throws(
() => factorio._reorderDependencies(modules),
@@ -102,7 +103,7 @@ describe("lib/factorio", function() {
});
it("should throw on invalid version range", function() {
let modules = [
{ name: 'a', version: '1.0.0', dependencies: {'b': 'invalid'} },
{ name: "a", version: "1.0.0", dependencies: {"b": "invalid"} },
];
assert.throws(
() => factorio._reorderDependencies(modules),
@@ -111,16 +112,16 @@ describe("lib/factorio", function() {
});
it("should throw on missing dependency", function() {
let modules = [
{ name: 'a', version: '1.0.0', dependencies: {'b': '*'} },
{ name: "a", version: "1.0.0", dependencies: {"b": "*"} },
];
assert.throws(
() => factorio._reorderDependencies(modules),
new Error("Missing dependency b for module a")
);
modules = [
{ name: 'a', version: '1.0.0', dependencies: {'b': '*'} },
{ name: 'b', version: '1.0.0', dependencies: {'c': '*'} },
{ name: 'c', version: '1.0.0', dependencies: {'d': '*'} },
{ name: "a", version: "1.0.0", dependencies: {"b": "*"} },
{ name: "b", version: "1.0.0", dependencies: {"c": "*"} },
{ name: "c", version: "1.0.0", dependencies: {"d": "*"} },
];
assert.throws(
() => factorio._reorderDependencies(modules),
@@ -129,8 +130,8 @@ describe("lib/factorio", function() {
});
it("should throw on outdated dependency", function() {
let modules = [
{ name: 'a', version: '1.0.0', dependencies: {} },
{ name: 'b', version: '1.0.0', dependencies: {'a': '>=2'} },
{ name: "a", version: "1.0.0", dependencies: {} },
{ name: "b", version: "1.0.0", dependencies: {"a": ">=2"} },
];
assert.throws(
() => factorio._reorderDependencies(modules),
@@ -139,18 +140,18 @@ describe("lib/factorio", function() {
});
it("should throw on dependency loops", function() {
let modules = [
{ name: 'a', version: '1.0.0', dependencies: {'b': '*'} },
{ name: 'b', version: '1.0.0', dependencies: {'a': '*'} },
{ name: "a", version: "1.0.0", dependencies: {"b": "*"} },
{ name: "b", version: "1.0.0", dependencies: {"a": "*"} },
];
assert.throws(
() => factorio._reorderDependencies(modules),
new Error("Module dependency loop detected: a -> b -> a")
);
modules = [
{ name: 'd', version: '1.0.0', dependencies: {'b': '*'} },
{ name: 'a', version: '1.0.0', dependencies: {'b': '*'} },
{ name: 'b', version: '1.0.0', dependencies: {'c': '*'} },
{ name: 'c', version: '1.0.0', dependencies: {'a': '*'} },
{ name: "d", version: "1.0.0", dependencies: {"b": "*"} },
{ name: "a", version: "1.0.0", dependencies: {"b": "*"} },
{ name: "b", version: "1.0.0", dependencies: {"c": "*"} },
{ name: "c", version: "1.0.0", dependencies: {"a": "*"} },
];
assert.throws(
() => factorio._reorderDependencies(modules),
@@ -170,7 +171,7 @@ describe("lib/factorio", function() {
}
}
let names = ['a', 'b', 'c'];
let names = ["a", "b", "c"];
let successCount = 0;
const bitCount = names.length * (names.length - 1);
for (let i=0; i < 2**bitCount; i++) {
@@ -186,10 +187,10 @@ describe("lib/factorio", function() {
let dependencies = {};
for (let k=0; k < names.length - 1; k++) {
if (bits[k + j * (names.length - 1)]) {
dependencies[names[k + (k >= j)]] = '*';
dependencies[names[k + (k >= j)]] = "*";
}
}
modules.push({ name, version: '1.0.0', dependencies });
modules.push({ name, version: "1.0.0", dependencies });
}
let success = false;
try {
@@ -213,7 +214,7 @@ describe("lib/factorio", function() {
let zip = new jszip();
zip.file("world/control.lua", "-- unknown\n");
let zipPath = path.join("temp", "test", "patch.zip");
await fs.outputFile(zipPath, await zip.generateAsync({type: "nodebuffer"}))
await fs.outputFile(zipPath, await zip.generateAsync({ type: "nodebuffer" }));
await assert.rejects(
factorio.patch(zipPath, []),
+19 -18
View File
@@ -1,3 +1,4 @@
"use strict";
const assert = require("assert").strict;
const events = require("events");
const fs = require("fs-extra");
@@ -84,7 +85,7 @@ describe("lib/factorio/server", function() {
describe("randomDynamicPort()", function() {
it("should return a port number", function() {
let port = factorio._randomDynamicPort()
let port = factorio._randomDynamicPort();
assert.equal(typeof port, "number");
assert(Number.isInteger(port));
assert(0 <= port && port < 2**16);
@@ -92,7 +93,7 @@ describe("lib/factorio/server", function() {
it("should return a port number in the dynamic range", function() {
function validate(port) {
return 49152 <= port && port <= 65535;
return (49152 <= port && port <= 65535);
}
for (let i=0; i < 20; i++) {
assert(validate(factorio._randomDynamicPort()));
@@ -157,8 +158,8 @@ describe("lib/factorio/server", function() {
describe("parseOutput()", function() {
it("should parse the test lines", function() {
for (let [line, reference] of testLines) {
reference.source = 'test';
let output = factorio._parseOutput(line, 'test');
reference.source = "test";
let output = factorio._parseOutput(line, "test");
delete output.received;
assert.deepEqual(output, reference);
}
@@ -187,50 +188,50 @@ describe("lib/factorio/server", function() {
describe("._handleIpc()", function() {
it("should emit the correct ipc event", async function() {
let waiter = events.once(server, 'ipc-channel');
let waiter = events.once(server, "ipc-channel");
await server._handleIpc(Buffer.from('\f$ipc:channel?j"value"'));
let result = await waiter;
assert.equal(result[0], "value");
});
it("should handle special characters in channel name", async function() {
let waiter = events.once(server, 'ipc-$ ?\x00\x0a:');
let waiter = events.once(server, "ipc-$ ?\x00\x0a:");
await server._handleIpc(Buffer.from('\f$ipc:$ \\x3f\\x00\\x0a:?j"value"'));
let result = await waiter;
assert.equal(result[0], "value");
});
it("should throw on malformed ipc line", async function() {
await assert.rejects(
server._handleIpc(Buffer.from('\f$ipc:blah')),
server._handleIpc(Buffer.from("\f$ipc:blah")),
new Error('Malformed IPC line "\f$ipc:blah"')
);
})
});
it("should throw on unknown type", async function() {
await assert.rejects(
server._handleIpc(Buffer.from('\f$ipc:channel??')),
server._handleIpc(Buffer.from("\f$ipc:channel??")),
new Error("Unknown IPC type '?'")
);
})
});
it("should throw on unknown file type", async function() {
await assert.rejects(
server._handleIpc(Buffer.from('\f$ipc:channel?ffoo.invalid')),
server._handleIpc(Buffer.from("\f$ipc:channel?ffoo.invalid")),
new Error("Unknown IPC file format 'invalid'")
);
})
});
it("should throw on file name with slash", async function() {
await assert.rejects(
server._handleIpc(Buffer.from('\f$ipc:channel?fa/b')),
server._handleIpc(Buffer.from("\f$ipc:channel?fa/b")),
new Error("Invalid IPC file name 'a/b'")
);
})
});
it("should load and delete json file", async function() {
let filePath = server.writePath('script-output', 'data.json');
let filePath = server.writePath("script-output", "data.json");
await fs.outputFile(filePath, '{"data":"spam"}');
let waiter = events.once(server, 'ipc-channel');
await server._handleIpc(Buffer.from('\f$ipc:channel?fdata.json'));
let waiter = events.once(server, "ipc-channel");
await server._handleIpc(Buffer.from("\f$ipc:channel?fdata.json"));
let result = await waiter;
assert.deepEqual(result[0], { "data": "spam" });
assert(!await fs.pathExists(filePath), "File was not deleted");
})
});
});
});
});
+5 -4
View File
@@ -1,8 +1,9 @@
const assert = require('assert').strict;
const fs = require('fs-extra');
const path = require('path');
"use strict";
const assert = require("assert").strict;
const fs = require("fs-extra");
const path = require("path");
const fileOps = require('lib/fileOps');
const fileOps = require("lib/fileOps");
describe("fileOps.js", function(){
let baseDir = path.join("temp", "test", "fileOps");
+4 -2
View File
@@ -1,10 +1,11 @@
"use strict";
const assert = require("assert");
const fs = require("fs-extra");
const path = require("path");
const generateSSLcert = require("lib/generateSSLcert")
const generateSSLcert = require("lib/generateSSLcert");
describe("generateSSLcert.js(options)", ()=>{
describe("generateSSLcert.js(options)", function() {
let testDir = path.join("temp", "test", "certTest");
before(async function() {
await fs.remove(testDir);
@@ -12,6 +13,7 @@ describe("generateSSLcert.js(options)", ()=>{
it("Creates a folder with a .crt and .key file in it", async function() {
this.timeout(1000);
// eslint-disable-next-line no-process-env
if (process.env.FAST_TEST) {
this.skip();
}
+6 -5
View File
@@ -1,9 +1,10 @@
const assert = require('assert').strict;
const fs = require('fs');
const path = require('path');
const stream = require('stream');
"use strict";
const assert = require("assert").strict;
const fs = require("fs");
const path = require("path");
const stream = require("stream");
const hash = require('lib/hash');
const hash = require("lib/hash");
describe("lib/hash", function() {
+11 -10
View File
@@ -1,17 +1,18 @@
"use strict";
const assert = require("assert").strict;
const helpers = require("lib/helpers");
describe("lib/helpers", function() {
describe("basicType()", function() {
it("should return the expected values", function() {
assert.equal(helpers.basicType("s"), "string");
assert.equal(helpers.basicType(null), "null");
assert.equal(helpers.basicType(undefined), "undefined");
assert.equal(helpers.basicType([1, 2]), "array");
assert.equal(helpers.basicType(7), "number");
assert.equal(helpers.basicType({ a: 1 }), "object");
});
});
describe("basicType()", function() {
it("should return the expected values", function() {
assert.equal(helpers.basicType("s"), "string");
assert.equal(helpers.basicType(null), "null");
assert.equal(helpers.basicType(undefined), "undefined");
assert.equal(helpers.basicType([1, 2]), "array");
assert.equal(helpers.basicType(7), "number");
assert.equal(helpers.basicType({ a: 1 }), "object");
});
});
});
+14 -13
View File
@@ -1,8 +1,9 @@
const assert = require('assert').strict;
const events = require('events');
"use strict";
const assert = require("assert").strict;
const events = require("events");
const mock = require('../../mock');
const link = require('lib/link');
const mock = require("../../mock");
const link = require("lib/link");
describe("lib/link/connectors", function() {
@@ -33,7 +34,7 @@ describe("lib/link/connectors", function() {
});
describe("class WebSocketClientConnector", function() {
let testConnector = new link.WebSocketClientConnector('url', 1);
let testConnector = new link.WebSocketClientConnector("url", 1);
testConnector._socket = new mock.MockSocket();
describe(".register()", function() {
@@ -49,10 +50,10 @@ describe("lib/link/connectors", function() {
it("calls send on the socket", function() {
testConnector._state = "handshake";
testConnector._socket.sentMessages = [];
testConnector.sendHandshake('test', { test: true });
testConnector.sendHandshake("test", { test: true });
assert.deepEqual(
testConnector._socket.sentMessages.map(JSON.parse),
[{ seq: null, type: 'test', data: { test: true }}]
[{ seq: null, type: "test", data: { test: true }}]
);
});
});
@@ -61,10 +62,10 @@ describe("lib/link/connectors", function() {
it("calls send on the socket", function() {
testConnector._state = "connected";
testConnector._socket.sentMessages = [];
let seq = testConnector.send('test', { test: true });
let seq = testConnector.send("test", { test: true });
assert.deepEqual(
testConnector._socket.sentMessages.map(JSON.parse),
[{ seq, type: 'test', data: { test: true }}]
[{ seq, type: "test", data: { test: true }}]
);
});
});
@@ -80,19 +81,19 @@ describe("lib/link/connectors", function() {
describe(".processHandshake()", function() {
it("should close on invalid message", function() {
testConnector._socket.closeCalled = false;
testConnector._processHandshake({ data: "invalid message" }),
testConnector._processHandshake({ data: "invalid message" });
assert(testConnector._socket.closeCalled, "Close was not called on the socket");
});
it("should call register on hello", function() {
let called = false;
testConnector.register = () => { called = true; };
testConnector._processHandshake({ seq: 1, type: 'hello', data: { version: "test", plugins: {} }});
testConnector._processHandshake({ seq: 1, type: "hello", data: { version: "test", plugins: {} }});
assert(called, "register was not called");
});
it("should emit connect on ready", async function() {
let result = events.once(testConnector, "connect");
testConnector._processHandshake(
{ seq: 1, type: 'ready', data: { session_token: "x", heartbeat_interval: 10 }}
{ seq: 1, type: "ready", data: { session_token: "x", heartbeat_interval: 10 }}
);
await result;
clearInterval(testConnector._heartbeatId);
@@ -121,7 +122,7 @@ describe("lib/link/connectors", function() {
it("should emit message on message in connected state", async function() {
testConnector._socket.sentMessages = [];
testConnector._state = "connected";
let result = events.once(testConnector, 'message');
let result = events.once(testConnector, "message");
testConnector._socket.events.get("message")('{"message":true}');
assert.deepEqual(await result, [{ message: true }]);
});
+44 -43
View File
@@ -1,5 +1,6 @@
const assert = require('assert').strict;
const events = require('events');
"use strict";
const assert = require("assert").strict;
const events = require("events");
const link = require("lib/link");
const errors = require("lib/errors");
@@ -14,13 +15,13 @@ describe("lib/link/link", function() {
before(function() {
testConnector = new mock.MockConnector();
testLink = new link.Link('source', 'target', testConnector);
testLink.setValidator('simple', schema.compile({
testLink = new link.Link("source", "target", testConnector);
testLink.setValidator("simple", schema.compile({
properties: {
"data": {
type: 'object',
type: "object",
properties: {
"string": { type: 'string' },
"string": { type: "string" },
},
},
},
@@ -28,20 +29,20 @@ describe("lib/link/link", function() {
});
it("should emitt error processing message to connector", async function() {
testLink.setHandler('throws', () => { throw new Error("Handler error"); }, () => true);
let result = events.once(testConnector, 'message');
testConnector.emit('message', { seq: 1, type: 'throws', data: {}});
testLink.setHandler("throws", () => { throw new Error("Handler error"); }, () => true);
let result = events.once(testConnector, "message");
testConnector.emit("message", { seq: 1, type: "throws", data: {}});
await assert.rejects(result, new Error("Handler error"));
});
describe(".setValidator()", function() {
it("should set the validator", function() {
testLink.setValidator('test', () => true);
assert(testLink._validators.has('test'), "validator was not added");
testLink.setValidator("test", () => true);
assert(testLink._validators.has("test"), "validator was not added");
});
it("should throw on duplicated validator", function() {
assert.throws(
() => testLink.setValidator('test', () => true),
() => testLink.setValidator("test", () => true),
new Error("test already has a validator")
);
});
@@ -50,18 +51,18 @@ describe("lib/link/link", function() {
describe(".setHandler()", function() {
it("should throw if validator is not passed", function() {
assert.throws(
() => testLink.setHandler('no_validator_test', (message) => {}),
() => testLink.setHandler("no_validator_test", (message) => {}),
new Error("validator is required")
);
});
it("should set the handler", function() {
let handler = (message) => {};
testLink.setHandler('handler_test', handler, (message) => true);
assert.equal(testLink._handlers.get('handler_test'), handler);
function handler(message) { }
testLink.setHandler("handler_test", handler, (message) => true);
assert.equal(testLink._handlers.get("handler_test"), handler);
});
it("should throw on duplicated handler", function() {
assert.throws(
() => testLink.setHandler('handler_test', (message) => {}),
() => testLink.setHandler("handler_test", (message) => {}),
new Error("handler_test already has a handler")
);
});
@@ -76,19 +77,19 @@ describe("lib/link/link", function() {
});
it("should throw on message without validator", function() {
assert.throws(
() => testLink.processMessage({ seq: 1, type: 'no_validator', data: {} }),
() => testLink.processMessage({ seq: 1, type: "no_validator", data: {} }),
new errors.InvalidMessage("No validator for no_validator on source-target")
);
});
it("should throw on message failing validation", function() {
assert.throws(
() => testLink.processMessage({ seq: 1, type: 'simple', data: { string: 1 }}),
() => testLink.processMessage({ seq: 1, type: "simple", data: { string: 1 }}),
{ message: "Validation failed for simple" }
);
});
it("should throw on unhandled message", function() {
assert.throws(
() => testLink.processMessage({ seq: 1, type: 'simple', data: { string: "a" }}),
() => testLink.processMessage({ seq: 1, type: "simple", data: { string: "a" }}),
new errors.InvalidMessage("Unhandled message simple")
);
});
@@ -97,16 +98,16 @@ describe("lib/link/link", function() {
describe("._processHandler()", function() {
it("should call the handler", function() {
let handled = [];
testLink.setHandler('handled', (message) => handled.push(message), (message) => true)
testLink._processHandler({ seq: 1, type: 'handled', data: { test: "foo" } });
assert.deepEqual(handled, [{ seq: 1, type: 'handled', data: { test: "foo" } }]);
testLink.setHandler("handled", (message) => handled.push(message), (message) => true);
testLink._processHandler({ seq: 1, type: "handled", data: { test: "foo" } });
assert.deepEqual(handled, [{ seq: 1, type: "handled", data: { test: "foo" } }]);
});
it("should return true if handled", function() {
let result = testLink._processHandler({ seq: 1, type: 'handled', data: {} });
let result = testLink._processHandler({ seq: 1, type: "handled", data: {} });
assert.equal(result, true);
});
it("should return false if not handled", function() {
let result = testLink._processHandler({ seq: 1, type: 'not_handled', data: {} });
let result = testLink._processHandler({ seq: 1, type: "not_handled", data: {} });
assert.equal(result, false);
});
});
@@ -114,62 +115,62 @@ describe("lib/link/link", function() {
describe("._processWaiters()", function() {
it("should call waiters", async function() {
let waiter = new Promise(resolve => {
testLink._waiters.set('test', [{ resolve, data: {} }]);
testLink._waiters.set("test", [{ resolve, data: {} }]);
});
assert(
testLink._processWaiters({ type: 'test', seq: 4, data: {} }),
testLink._processWaiters({ type: "test", seq: 4, data: {} }),
"Waiter was not processed"
);
assert.deepEqual(await waiter, { type: 'test', seq: 4, data: {} });
assert.deepEqual(await waiter, { type: "test", seq: 4, data: {} });
});
it("should filter on data", async function() {
let waiter = new Promise(resolve => {
testLink._waiters.set('test', [{ resolve, data: { a: 1, b: 2 } }]);
testLink._waiters.set("test", [{ resolve, data: { a: 1, b: 2 } }]);
});
assert(
!testLink._processWaiters({ type: 'test', seq: 4, data: {} }),
!testLink._processWaiters({ type: "test", seq: 4, data: {} }),
"Incorrectly matched empty message"
);
assert(
!testLink._processWaiters({ type: 'test', seq: 4, data: { a: 1, b: 0 } }),
!testLink._processWaiters({ type: "test", seq: 4, data: { a: 1, b: 0 } }),
"Incorrectly matched message with wrong data"
);
assert(
testLink._processWaiters({ type: 'test', seq: 4, data: { a: 1, b: 2, c: 3 } }),
testLink._processWaiters({ type: "test", seq: 4, data: { a: 1, b: 2, c: 3 } }),
"Did not match correct message"
);
assert.deepEqual(await waiter, { type: 'test', seq: 4, data: { a: 1, b: 2, c: 3 } });
assert.deepEqual(await waiter, { type: "test", seq: 4, data: { a: 1, b: 2, c: 3 } });
});
});
describe(".waitFor()", function() {
before(function() {
testLink.setValidator('waiter', (message) => true);
testLink.setValidator("waiter", (message) => true);
});
it("should throw on missing validator", async function() {
await assert.rejects(
testLink.waitFor('no_validator', {}),
testLink.waitFor("no_validator", {}),
new Error("No validator for no_validator on source-target")
);
});
it("should wait for a given message", async function() {
let result = testLink.waitFor('waiter', {});
let result = testLink.waitFor("waiter", {});
assert(
testLink._processWaiters({ type: 'waiter', seq: 5, data: {} }),
testLink._processWaiters({ type: "waiter", seq: 5, data: {} }),
"Did not proccess waiter"
);
assert.deepEqual(await result, { type: 'waiter', seq: 5, data: {} });
assert.deepEqual(await result, { type: "waiter", seq: 5, data: {} });
});
it("should handle multiple waiters", async function() {
let result1 = testLink.waitFor('waiter', {});
let result2 = testLink.waitFor('waiter', {});
let result1 = testLink.waitFor("waiter", {});
let result2 = testLink.waitFor("waiter", {});
assert(
testLink._processWaiters({ type: 'waiter', seq: 5, data: {} }),
testLink._processWaiters({ type: "waiter", seq: 5, data: {} }),
"Did not proccess waiter"
);
assert.deepEqual(await result1, { type: 'waiter', seq: 5, data: {} });
assert.deepEqual(await result2, { type: 'waiter', seq: 5, data: {} });
assert.deepEqual(await result1, { type: "waiter", seq: 5, data: {} });
assert.deepEqual(await result2, { type: "waiter", seq: 5, data: {} });
});
});
});
+26 -29
View File
@@ -1,21 +1,22 @@
const assert = require('assert').strict;
"use strict";
const assert = require("assert").strict;
const link = require('lib/link');
const errors = require('lib/errors');
const mock = require('../../mock');
const link = require("lib/link");
const errors = require("lib/errors");
const mock = require("../../mock");
describe("lib/link/messages", function() {
let testSourceLink = new link.Link('source', 'target', new mock.MockConnector());
let testTargetLink = new link.Link('target', 'source', new mock.MockConnector());
let testSourceLink = new link.Link("source", "target", new mock.MockConnector());
let testTargetLink = new link.Link("target", "source", new mock.MockConnector());
let lastTargetSent;
testTargetLink.send = (type, data) => { lastTargetSent = { type, data }; };
describe("class Request", function() {
let testRequest = new link.Request({
type: 'test',
links: ['source-target'],
type: "test",
links: ["source-target"],
requestProperties: {
"test": { type: "string" },
},
@@ -24,7 +25,7 @@ describe("lib/link/messages", function() {
describe("constructor", function() {
it("should throw on invalid forwardTo", function() {
assert.throws(
() => new link.Request({ links: [], forwardTo: 'invalid'}),
() => new link.Request({ links: [], forwardTo: "invalid" }),
new Error("Invalid forwardTo value invalid")
);
});
@@ -33,7 +34,7 @@ describe("lib/link/messages", function() {
describe(".attach()", function() {
it("should attach validator to a source link", function() {
testRequest.attach(testSourceLink);
assert(testSourceLink._validators.has('test_response'), "Validator was not set");
assert(testSourceLink._validators.has("test_response"), "Validator was not set");
});
it("should throw if missing handler on target link", function() {
assert.throws(
@@ -44,17 +45,17 @@ describe("lib/link/messages", function() {
let handlerResult;
it("should attach handler to a target link", function() {
testRequest.attach(testTargetLink, async (message) => handlerResult );
assert(testTargetLink._handlers.has('test_request'), "Handler was not set");
testRequest.attach(testTargetLink, async (message) => handlerResult);
assert(testTargetLink._handlers.has("test_request"), "Handler was not set");
});
it("should send result of calling the handler", async function() {
handlerResult = { test: "handler" };
testTargetLink._handlers.get('test_request')({ seq: 2 });
testTargetLink._handlers.get("test_request")({ seq: 2 });
let result = await new Promise(resolve => {
testTargetLink.connector.send = (type, data) => resolve({ type, data });
});
assert.deepEqual(result, {
type: 'test_response',
type: "test_response",
data: {
test: "handler",
seq: 2,
@@ -63,12 +64,12 @@ describe("lib/link/messages", function() {
});
it("should implicitly send an empty object on empty return", async function() {
handlerResult = undefined;
testTargetLink._handlers.get('test_request')({ seq: 2 });
testTargetLink._handlers.get("test_request")({ seq: 2 });
let result = await new Promise(resolve => {
testTargetLink.connector.send = (type, data) => resolve({ type, data });
});
assert.deepEqual(result, {
type: 'test_response',
type: "test_response",
data: {
seq: 2,
},
@@ -82,20 +83,16 @@ describe("lib/link/messages", function() {
testSourceLink.connector.send = (type, data) => {
request = { type, data };
};
testSourceLink.waitFor = (type, condition) => {
return { data: { type, request }};
};
testSourceLink.waitFor = (type, condition) => ({ data: { type, request }});
assert.deepEqual(
await testRequest.send(testSourceLink, { test: "request" }),
{ type: 'test_response', request: { type: 'test_request', data: {test: "request" }}}
{ type: "test_response", request: { type: "test_request", data: { test: "request" }}}
);
delete testSourceLink.connector.send;
delete testSourceLink.waitFor;
});
it("should throw error response", async function() {
testSourceLink.waitFor = (type, condition) => {
return { data: { error: "test error" }};
};
testSourceLink.waitFor = (type, condition) => ({ data: { error: "test error" }});
await assert.rejects(
testRequest.send(testSourceLink, { test: "blah" }),
new errors.RequestError("test error")
@@ -107,8 +104,8 @@ describe("lib/link/messages", function() {
describe("class Event", function() {
let testEvent = new link.Event({
type: 'test',
links: ['source-target'],
type: "test",
links: ["source-target"],
eventProperties: {
"test": { type: "string" },
},
@@ -117,13 +114,13 @@ describe("lib/link/messages", function() {
describe("constructor", function() {
it("should throw on invalid forwardTo", function() {
assert.throws(
() => new link.Event({ forwardTo: 'invalid'}),
() => new link.Event({ forwardTo: "invalid" }),
new Error("Invalid forwardTo value invalid")
);
});
it("should throw on invalid broadcastTo", function() {
assert.throws(
() => new link.Event({ broadcastTo: 'invalid'}),
() => new link.Event({ broadcastTo: "invalid" }),
new Error("Invalid broadcastTo value invalid")
);
});
@@ -139,7 +136,7 @@ describe("lib/link/messages", function() {
let called = false;
it("should attach handler and validator to a target link", function() {
testEvent.attach(testTargetLink, async (message) => { called = true; });
assert(testTargetLink._validators.has('test_event'), "Validator was not set");
assert(testTargetLink._validators.has("test_event"), "Validator was not set");
});
it("should call the attached event handler", function() {
testTargetLink._handlers.get("test_event")();
@@ -163,6 +160,6 @@ describe("lib/link/messages", function() {
describe("attachAllMessages()", function() {
it("does not throw", function() {
link.attachAllMessages(testSourceLink);
})
});
});
});
+3 -2
View File
@@ -1,6 +1,7 @@
const assert = require('assert').strict;
"use strict";
const assert = require("assert").strict;
const luaTools = require('lib/luaTools');
const luaTools = require("lib/luaTools");
describe("lib/luaTools", function() {
+6 -24
View File
@@ -1,32 +1,14 @@
var assert = require('assert');
var objectOps = require('lib/objectOps');
"use strict";
var assert = require("assert");
var objectOps = require("lib/objectOps");
describe("objectOps.js", function(){
/*describe("objectOps.clone()", function(){
it("clones javascript objects", function(){
var object1 = {hello:"world", cat:{legs:4, name:"Kitty", colors:["brown", "yellow", "purple"]}};
var object2 = {};
object2 = objectOps.clone(object1);
assert.equal(object2.cat.name, object1.cat.name)
assert.equal(object1.hello, object2.hello)
});
it("does not deep clone objects", function(){
var obj1 = {hello:"world", cat:{legs:4, name:"Kitty", colors:["brown", "yellow", "purple"]}};
var obj2 = {};
obj2 = objectOps.clone(obj1);
obj1.cat.colors = "black";
assert.equal(obj1.cat.colors, "black");
assert.notEqual(obj2.cat.colors[1], "yellow");
});
});*/
describe("objectOps.deepclone()", function(){
it("deep clones javascript objects", function(){
var obj1 = {hello:"world", cat:{legs:4, name:"Kitty", colors:["brown", "yellow", "purple"]}};
var obj1 = { hello: "world", cat: { legs: 4, name: "Kitty", colors: ["brown", "yellow", "purple"] }};
var obj2 = {};
obj2 = objectOps.deepclone(obj1);
obj1.cat.colors = "black";
assert.equal(obj1.cat.colors, "black");
assert.equal(obj2.cat.colors[1], "yellow");
@@ -37,4 +19,4 @@ describe("objectOps.js", function(){
});
});
});
});
});
+27 -26
View File
@@ -1,11 +1,12 @@
const assert = require('assert').strict;
const fs = require('fs-extra');
const path = require('path');
"use strict";
const assert = require("assert").strict;
const fs = require("fs-extra");
const path = require("path");
const mock = require('../mock');
const link = require('lib/link');
const plugin = require('lib/plugin');
const errors = require('lib/errors');
const mock = require("../mock");
const link = require("lib/link");
const plugin = require("lib/plugin");
const errors = require("lib/errors");
describe("lib/plugin", function() {
@@ -14,7 +15,7 @@ describe("lib/plugin", function() {
it("should be constructible", async function() {
instancePlugin = new plugin.BaseInstancePlugin();
await instancePlugin.init();
})
});
it("should define defaults for hooks", async function() {
await instancePlugin.onMetrics();
await instancePlugin.onStart();
@@ -23,7 +24,7 @@ describe("lib/plugin", function() {
await instancePlugin.onOutput({});
instancePlugin.onMasterConnectionEvent("connect");
await instancePlugin.onPrepareMasterDisconnect();
})
});
});
describe("class BaseMasterPlugin", function() {
@@ -31,38 +32,38 @@ describe("lib/plugin", function() {
it("should be constructible", async function() {
masterPlugin = new plugin.BaseMasterPlugin();
await masterPlugin.init();
})
});
it("should define defaults for hooks", async function() {
await masterPlugin.onInstanceStatusChanged({}, "running", "initialized");
await masterPlugin.onMetrics();
await masterPlugin.onShutdown();
masterPlugin.onSlaveConnectionEvent({}, "connect");
await masterPlugin.onPrepareSlaveDisconnect({});
})
});
});
describe("loadPluginInfos()", function() {
let baseDir = path.join("temp", "test", "plugin");
let emptyDir = path.join(baseDir, 'emptyDir');
let emptyPlugin = path.join(baseDir, 'emptyPlugin');
let testPlugin = path.join(baseDir, 'testPlugin');
let brokenPlugin = path.join(baseDir, 'brokenPlugin');
let invalidPlugin = path.join(baseDir, 'invalidPlugin');
let emptyDir = path.join(baseDir, "emptyDir");
let emptyPlugin = path.join(baseDir, "emptyPlugin");
let testPlugin = path.join(baseDir, "testPlugin");
let brokenPlugin = path.join(baseDir, "brokenPlugin");
let invalidPlugin = path.join(baseDir, "invalidPlugin");
before(async function() {
await fs.ensureDir(emptyDir);
await fs.ensureDir(path.join(emptyPlugin, 'empty'));
await fs.ensureDir(path.join(emptyPlugin, "empty"));
async function writePlugin(pluginPath, name, infoName = name) {
await fs.outputFile(
path.join(pluginPath, name, 'info.js'),
path.join(pluginPath, name, "info.js"),
`module.exports = { name: "${infoName}" };`
);
}
await writePlugin(testPlugin, 'test');
await writePlugin(brokenPlugin, 'broken');
await fs.outputFile(path.join(brokenPlugin, 'broken', 'info.js'), "Syntax Error");
await writePlugin(invalidPlugin, 'invalid', 'wrong');
await writePlugin(testPlugin, "test");
await writePlugin(brokenPlugin, "broken");
await fs.outputFile(path.join(brokenPlugin, "broken", "info.js"), "Syntax Error");
await writePlugin(invalidPlugin, "invalid", "wrong");
});
it("should return an empty array for an empty directory", async function() {
@@ -74,7 +75,7 @@ describe("lib/plugin", function() {
it("should discover test plugin", async function() {
assert.deepEqual(
await plugin.loadPluginInfos(testPlugin),
[{ name: 'test' }]
[{ name: "test" }]
);
});
it("should reject on broken plugin", async function() {
@@ -91,18 +92,18 @@ describe("lib/plugin", function() {
});
after(async function() {
await fs.unlink(path.join(brokenPlugin, 'broken', 'info.js'));
await fs.unlink(path.join(brokenPlugin, "broken", "info.js"));
});
});
describe("attachPluginMessages()", function() {
let mockLink = new link.Link('source', 'target', new mock.MockConnector());
let mockLink = new link.Link("source", "target", new mock.MockConnector());
let mockEvent = new link.Event({ type: "test:test", links: ["target-source"] });
it("should accept pluginInfo without messages", function() {
plugin.attachPluginMessages(mockLink, {}, null);
});
it("should attach handler for the given message", function() {
let mockEventEventHandler = function() {};
function mockEventEventHandler() { };
plugin.attachPluginMessages(mockLink, { name: "test", messages: { mockEvent }}, { mockEventEventHandler });
assert(mockLink._handlers.get("test:test_event"), "handler was not registered");
});
+108 -107
View File
@@ -1,6 +1,7 @@
const assert = require('assert').strict;
"use strict";
const assert = require("assert").strict;
const prometheus = require('lib/prometheus');
const prometheus = require("lib/prometheus");
describe("lib/prometheus", function() {
@@ -8,7 +9,7 @@ describe("lib/prometheus", function() {
let registry;
beforeEach(function() {
registry = new prometheus.CollectorRegistry();
})
});
describe(".register()", function() {
it("should register a collector", function() {
@@ -105,22 +106,22 @@ describe("lib/prometheus", function() {
describe("class Counter", function() {
let counter;
beforeEach(function() {
counter = new prometheus.Counter('test', "Help", { register: false });
counter = new prometheus.Counter("test", "Help", { register: false });
});
describe("constructor", function() {
it("should be initialized to 0", function() {
assert.equal(counter._values.get(''), 0);
assert.equal(counter._values.get(""), 0);
});
});
describe(".inc()", function() {
it("should increment the counter by 1 by default", function() {
counter.inc();
assert.equal(counter._values.get(''), 1);
assert.equal(counter._values.get(""), 1);
});
it("should increment the counter by specified value", function() {
counter.inc(3.2);
assert.equal(counter._values.get(''), 3.2);
assert.equal(counter._values.get(""), 3.2);
});
it("should throw if value is negative", function() {
assert.throws(
@@ -146,53 +147,53 @@ describe("lib/prometheus", function() {
describe("class Gauge", function() {
let gauge;
beforeEach(function() {
gauge = new prometheus.Gauge('test', "Help", { register: false });
gauge = new prometheus.Gauge("test", "Help", { register: false });
});
describe("constructor", function() {
it("should be initialized to 0", function() {
assert.equal(gauge._values.get(''), 0);
assert.equal(gauge._values.get(""), 0);
});
});
describe(".inc()", function() {
it("should increment the gauge by 1 by default", function() {
gauge.inc();
assert.equal(gauge._values.get(''), 1);
assert.equal(gauge._values.get(""), 1);
});
it("should increment the gauge by specified value", function() {
gauge.inc(3.2);
assert.equal(gauge._values.get(''), 3.2);
assert.equal(gauge._values.get(""), 3.2);
});
it("should decrement on negative value", function() {
gauge.inc(-2);
assert.equal(gauge._values.get(''), -2);
assert.equal(gauge._values.get(""), -2);
});
});
describe(".dec()", function() {
it("should decrement the gauge by 1 by default", function() {
gauge.dec();
assert.equal(gauge._values.get(''), -1);
assert.equal(gauge._values.get(""), -1);
});
it("should decrement the gauge by specified value", function() {
gauge.dec(3.2);
assert.equal(gauge._values.get(''), -3.2);
assert.equal(gauge._values.get(""), -3.2);
});
it("should increment on negative value", function() {
gauge.dec(-2);
assert.equal(gauge._values.get(''), 2);
assert.equal(gauge._values.get(""), 2);
});
});
describe(".set()", function() {
it("should set the gauge to the specified value", function() {
gauge.set(7);
assert.equal(gauge._values.get(''), 7);
assert.equal(gauge._values.get(""), 7);
});
});
describe(".setToCurrentTime()", function() {
it("should set the gauge to current unix time in seconds", function() {
gauge.setToCurrentTime();
assert(
Math.abs(gauge._values.get('') - Date.now() / 1000) < 1,
Math.abs(gauge._values.get("") - Date.now() / 1000) < 1,
"value set is not close to the current unix time"
);
});
@@ -208,17 +209,17 @@ describe("lib/prometheus", function() {
name, "Help", { register: false }
),
new Error(`Invalid name '${name}'`)
)
);
}
});
it("should throw on invalid label names", function() {
for (let name of ["", " ", "0a", "$a", "a:"]) {
assert.throws(
() => new prometheus.Gauge(
'test', "Help", { register: false, labels: [name] }
"test", "Help", { register: false, labels: [name] }
),
new Error(`Invalid label '${name}'`)
)
);
}
});
it("should throw on missing name", function() {
@@ -243,7 +244,7 @@ describe("lib/prometheus", function() {
describe(".get()", function() {
it("should return the current value", function() {
let gauge = new prometheus.Gauge('test', "Help", { register: false });
let gauge = new prometheus.Gauge("test", "Help", { register: false });
gauge.set(11);
assert.equal(gauge.get(), 11);
});
@@ -253,34 +254,34 @@ describe("lib/prometheus", function() {
let gauge;
beforeEach(function() {
gauge = new prometheus.Gauge(
'test', "Help", { register: false, labels: ['a', 'b'] }
"test", "Help", { register: false, labels: ["a", "b"] }
);
});
it("should require all labels set at creation", function() {
assert.throws(
() => gauge.labels({a: "1"}),
() => gauge.labels({ a: "1" }),
new Error("Missing label 'b'")
);
assert.throws(
() => gauge.labels({b: "1"}),
() => gauge.labels({ b: "1" }),
new Error("Missing label 'a'")
);
assert.throws(
() => gauge.labels({a: '1', b: '2', c: '3'}),
() => gauge.labels({ a: "1", b: "2", c: "3" }),
new Error("Extra label 'c'")
);
assert.throws(
() => gauge.labels('1'),
() => gauge.labels("1"),
new Error("Missing label 'b'")
);
assert.throws(
() => gauge.labels('1', '2', '3'),
() => gauge.labels("1", "2", "3"),
new Error("Extra positional label")
);
});
it("should throw if label value is not a string", function() {
assert.throws(
() => gauge.labels({a: "1", b: 2}),
() => gauge.labels({ a: "1", b: 2 }),
new Error("Expected value for label 'b' to be a string")
);
assert.throws(
@@ -289,39 +290,39 @@ describe("lib/prometheus", function() {
);
});
it("should initialize a value for the label set", function() {
gauge.labels({ a: '1', b: '2'});
gauge.labels({ a: "1", b: "2" });
assert(gauge._values.has('a="1",b="2"'), "value was not set");
gauge.labels('4', '5');
gauge.labels("4", "5");
assert(gauge._values.has('a="4",b="5"'), "value was not set");
});
it("should return a child supporting the usual methods", function() {
let child = gauge.labels({ a: '1', b: '2'});
let child = gauge.labels({ a: "1", b: "2" });
child.get();
child.inc();
child.dec();
child.setToCurrentTime();
child = gauge.labels('7', '8');
child = gauge.labels("7", "8");
child.get();
child.inc();
child.dec();
child.setToCurrentTime();
});
it("should return the same child for the same labels", function() {
let child = gauge.labels('1', '2');
assert.equal(gauge.labels('1', '2'), child);
let child = gauge.labels("1", "2");
assert.equal(gauge.labels("1", "2"), child);
});
});
describe(".remove()", function() {
it("should remove a label set created with .labels()", function() {
let gauge = new prometheus.Gauge(
'test', "Help", { register: false, labels: ['a', 'b'] }
"test", "Help", { register: false, labels: ["a", "b"] }
);
gauge.labels({ a: '1', b: '2'});
gauge.remove({ a: '1', b: '2'});
gauge.labels({ a: "1", b: "2" });
gauge.remove({ a: "1", b: "2" });
assert(!gauge._values.has('a="1",b="2"'), "value was not removed");
});
it("should throw when removing the empty label set", function() {
let gauge = new prometheus.Gauge('test', "Help", { register: false });
let gauge = new prometheus.Gauge("test", "Help", { register: false });
assert.throws(
() => gauge.remove({}), new Error("labels cannot be empty")
);
@@ -330,46 +331,46 @@ describe("lib/prometheus", function() {
describe(".removeAll()", function() {
it("should remove a label set created with .labels()", function() {
let gauge = new prometheus.Gauge(
'test', "Help", { register: false, labels: ['a', 'b'] }
"test", "Help", { register: false, labels: ["a", "b"] }
);
gauge.labels('1', '2');
gauge.labels('1', '3');
gauge.labels('4', '6');
gauge.removeAll({a: '1'});
assert(gauge._values.size <= 1, "Matching labels remained")
gauge.labels("1", "2");
gauge.labels("1", "3");
gauge.labels("4", "6");
gauge.removeAll({ a: "1" });
assert(gauge._values.size <= 1, "Matching labels remained");
assert(
gauge._values.has('a="4",b="6"'),
"Non-matching label was removed"
);
});
it("should throw when removing the empty label set", function() {
let gauge = new prometheus.Gauge('test', "Help", { register: false });
let gauge = new prometheus.Gauge("test", "Help", { register: false });
assert.throws(
() => gauge.removeAll({}), new Error("labels cannot be empty")
);
});
it("should handle special characters", function() {
let gauge = new prometheus.Gauge(
'test', "Help", { register: false, labels: ['a', 'b'] }
)
gauge.labels('x\\\\\n\"\"\n\\', '2');
gauge.removeAll({a: 'x\\\\\n\"\"\n\\'});
assert(gauge._values.size === 0, "Matching labels remained")
"test", "Help", { register: false, labels: ["a", "b"] }
);
gauge.labels('x\\\\\n\"\"\n\\', "2");
gauge.removeAll({ a: 'x\\\\\n\"\"\n\\' });
assert(gauge._values.size === 0, "Matching labels remained");
});
});
describe(".clear()", function() {
it("should remove all label sets created with .labels()", function() {
let gauge = new prometheus.Gauge(
'test', "Help", { register: false, labels: ['a', 'b'] }
"test", "Help", { register: false, labels: ["a", "b"] }
);
gauge.labels({ a: '1', b: '2'});
gauge.labels({ a: '3', b: '4'});
gauge.labels({ a: '5', b: '6'});
gauge.labels({ a: "1", b: "2" });
gauge.labels({ a: "3", b: "4" });
gauge.labels({ a: "5", b: "6" });
gauge.clear();
assert(gauge._values.size == 0, "values was not cleared");
});
it("should throw when used on an unlabled metric", function() {
let gauge = new prometheus.Gauge('test', "Help", { register: false });
let gauge = new prometheus.Gauge("test", "Help", { register: false });
assert.throws(
() => gauge.clear(), new Error("Cannot clear unlabeled metric")
);
@@ -381,7 +382,7 @@ describe("lib/prometheus", function() {
describe("processStartTimeSeconds", function() {
it("should give a close point in time", async function() {
this.skip(); // XXX Doesn't work in Clusterio's shared testing env
let results = []
let results = [];
let collector = prometheus.defaultCollectors.processStartTimeSeconds;
for await (let result of collector.collect()) {
results.push(result);
@@ -393,7 +394,7 @@ describe("lib/prometheus", function() {
);
assert(
Math.abs(results[0].samples.get('') - Date.now() / 1000) < 1,
Math.abs(results[0].samples.get("") - Date.now() / 1000) < 1,
"value set is not close to the current unix time"
);
});
@@ -401,7 +402,7 @@ describe("lib/prometheus", function() {
describe("processCpuSecondsTotal", function() {
it("should give a low value", async function() {
this.skip(); // XXX Doesn't work in Clusterio's shared testing env
let results = []
let results = [];
let collector = prometheus.defaultCollectors.processCpuSecondsTotal;
for await (let result of collector.collect()) {
results.push(result);
@@ -412,7 +413,7 @@ describe("lib/prometheus", function() {
"collector did not give exactly one result"
);
let value = results[0].samples.get('')
let value = results[0].samples.get("");
assert(
value < 5 && value > 0,
`process time is not between 0 and 5 seconds (${value})`
@@ -422,7 +423,7 @@ describe("lib/prometheus", function() {
describe("processResidentMemoryBytes", function() {
it("should give a low value", async function() {
this.skip(); // XXX Doesn't work in Clusterio's shared testing env
let results = []
let results = [];
let collector = prometheus.defaultCollectors.processResidentMemoryBytes;
for await (let result of collector.collect()) {
results.push(result);
@@ -433,7 +434,7 @@ describe("lib/prometheus", function() {
"collector did not give exactly one result"
);
let value = results[0].samples.get('')
let value = results[0].samples.get("");
assert(
value < 200e6 && value > 0,
`resident memory is not between 0 and 200 MB (${value})`
@@ -443,7 +444,7 @@ describe("lib/prometheus", function() {
describe("processHeapBytes", function() {
it("should give a low value", async function() {
this.skip(); // XXX Doesn't work in Clusterio's shared testing env
let results = []
let results = [];
let collector = prometheus.defaultCollectors.processHeapBytes;
for await (let result of collector.collect()) {
results.push(result);
@@ -454,7 +455,7 @@ describe("lib/prometheus", function() {
"collector did not give exactly one result"
);
let value = results[0].samples.get('')
let value = results[0].samples.get("");
assert(
value < 200e6 && value > 0,
`heap is not between 0 and 200 MB (${value})`
@@ -466,35 +467,35 @@ describe("lib/prometheus", function() {
describe("exposition()", function() {
it("should format simple metrics", async function() {
let registry = new prometheus.CollectorRegistry();
let counter = new prometheus.Counter('a', "Help", { register: false });
let counter = new prometheus.Counter("a", "Help", { register: false });
registry.register(counter);
let gauge = new prometheus.Gauge('b', "Help", { register: false });
let gauge = new prometheus.Gauge("b", "Help", { register: false });
registry.register(gauge);
let exposition = await prometheus.exposition(registry.collect());
assert.equal(
exposition,
'# HELP a Help\n' +
'# TYPE a counter\n' +
'a 0\n' +
'\n' +
'# HELP b Help\n' +
'# TYPE b gauge\n' +
'b 0\n'
"# HELP a Help\n" +
"# TYPE a counter\n" +
"a 0\n" +
"\n" +
"# HELP b Help\n" +
"# TYPE b gauge\n" +
"b 0\n"
);
});
it("should format labeled metrics", async function() {
let registry = new prometheus.CollectorRegistry();
let gauge = new prometheus.Gauge(
'test', "Help", { register: false, labels: ['a', 'b']}
"test", "Help", { register: false, labels: ["a", "b"] }
);
gauge.labels({a: '1', b: '2'});
gauge.labels({b: 'y', a: 'x'});
gauge.labels({ a: "1", b: "2" });
gauge.labels({ b: "y", a: "x" });
registry.register(gauge);
let exposition = await prometheus.exposition(registry.collect());
assert.equal(
exposition,
'# HELP test Help\n' +
'# TYPE test gauge\n' +
"# HELP test Help\n" +
"# TYPE test gauge\n" +
'test{a="1",b="2"} 0\n' +
'test{a="x",b="y"} 0\n'
);
@@ -502,17 +503,17 @@ describe("lib/prometheus", function() {
it("should format Infinity and NaN", async function() {
let registry = new prometheus.CollectorRegistry();
let gauge = new prometheus.Gauge(
'test', "Help", { register: false, labels: ['n']}
"test", "Help", { register: false, labels: ["n"] }
);
gauge.labels({n: '1'}).set(Infinity);
gauge.labels({n: '2'}).set(-Infinity);
gauge.labels({n: '3'}).set(NaN);
gauge.labels({ n: "1" }).set(Infinity);
gauge.labels({ n: "2" }).set(-Infinity);
gauge.labels({ n: "3" }).set(NaN);
registry.register(gauge);
let exposition = await prometheus.exposition(registry.collect());
assert.equal(
exposition,
'# HELP test Help\n' +
'# TYPE test gauge\n' +
"# HELP test Help\n" +
"# TYPE test gauge\n" +
'test{n="1"} +Inf\n' +
'test{n="2"} -Inf\n' +
'test{n="3"} NaN\n'
@@ -521,11 +522,11 @@ describe("lib/prometheus", function() {
});
let serializedResult = {
metric: { type: 'type', name: 'name', help: "Help", labels: ['a', 'b'] },
samples: [ ['a="1",b="2"', 1], ['a="3",b="4"', 5], ]
metric: { type: "type", name: "name", help: "Help", labels: ["a", "b"] },
samples: [['a="1",b="2"', 1], ['a="3",b="4"', 5]],
};
let deserializedResult = {
metric: new prometheus.Metric('type', 'name', "Help", ['a', 'b']),
metric: new prometheus.Metric("type", "name", "Help", ["a", "b"]),
samples: new Map([['a="1",b="2"', 1], ['a="3",b="4"', 5]]),
};
describe("serializeResult()", function() {
@@ -533,26 +534,26 @@ describe("lib/prometheus", function() {
assert.deepEqual(
prometheus.serializeResult(deserializedResult),
serializedResult
)
);
});
it("should support adding labels", function() {
assert.deepEqual(
prometheus.serializeResult({
metric: new prometheus.Metric('type', 'name', "Help"),
samples: new Map([['', 2]]),
}, { addLabels: { c: '8' }}),
metric: new prometheus.Metric("type", "name", "Help"),
samples: new Map([["", 2]]),
}, { addLabels: { c: "8" }}),
{
metric: {type: 'type', name: 'name', help: "Help", labels: ['c']},
metric: { type: "type", name: "name", help: "Help", labels: ["c"] },
samples: [['c="8"', 2]],
}
);
assert.deepEqual(
prometheus.serializeResult({
metric: new prometheus.Metric('type', 'name', "Help", ['a']),
metric: new prometheus.Metric("type", "name", "Help", ["a"]),
samples: new Map([['a="4"', 3]]),
}, { addLabels: { c: '8' }}),
}, { addLabels: { c: "8" }}),
{
metric: {type: 'type', name: 'name', help: "Help", labels: ['a', 'c']},
metric: { type: "type", name: "name", help: "Help", labels: ["a", "c"] },
samples: [['a="4",c="8"', 3]],
}
);
@@ -560,30 +561,30 @@ describe("lib/prometheus", function() {
it("should support redefining name and help", function() {
assert.deepEqual(
prometheus.serializeResult({
metric: new prometheus.Metric('type', 'name', "Help"),
samples: new Map([['', 2]]),
}, { metricName: 'new', }),
metric: new prometheus.Metric("type", "name", "Help"),
samples: new Map([["", 2]]),
}, { metricName: "new" }),
{
metric: {type: 'type', name: 'new', help: "Help", labels: []},
samples: [['', 2]],
metric: { type: "type", name: "new", help: "Help", labels: [] },
samples: [["", 2]],
}
);
assert.deepEqual(
prometheus.serializeResult({
metric: new prometheus.Metric('type', 'name', "Help"),
samples: new Map([['', 2]]),
}, { metricHelp: "old", }),
metric: new prometheus.Metric("type", "name", "Help"),
samples: new Map([["", 2]]),
}, { metricHelp: "old" }),
{
metric: {type: 'type', name: 'name', help: "old", labels: []},
samples: [['', 2]],
metric: { type: "type", name: "name", help: "old", labels: [] },
samples: [["", 2]],
}
);
});
it("should throw on unrecoginzed option", function() {
assert.throws(
() => new prometheus.serializeResult({
metric: new prometheus.Metric('type', 'name', "Help"),
samples: new Map([['', 3]]),
metric: new prometheus.Metric("type", "name", "Help"),
samples: new Map([["", 3]]),
}, { invalid: true }),
new Error("Unrecognized option 'invalid'")
);
@@ -595,7 +596,7 @@ describe("lib/prometheus", function() {
assert.deepEqual(
prometheus.deserializeResult(serializedResult),
deserializedResult
)
);
});
});
+1
View File
@@ -1,3 +1,4 @@
"use strict";
const assert = require("assert").strict;
const users = require("lib/users");
+5 -4
View File
@@ -1,8 +1,9 @@
const assert = require('assert').strict;
"use strict";
const assert = require("assert").strict;
const mock = require('./mock');
const master = require('../master');
const mock = require("./mock");
const master = require("../master");
describe('Master testing', function() {
describe("Master testing", function() {
});
+3 -2
View File
@@ -1,5 +1,6 @@
// These are libraries that does not have any units test, they are
// required here in order to track coverage for them.
require('lib/manager/modManager');
require('lib/npmPostinstall.js');
"use strict";
require("lib/manager/modManager");
require("lib/npmPostinstall.js");
+8 -7
View File
@@ -1,3 +1,4 @@
"use strict";
const events = require("events");
const link = require("lib/link");
@@ -42,7 +43,7 @@ class MockConnector extends events.EventEmitter {
send(type, data) {
let message = { seq: this._seq, type, data };
this.sentMessages.push(message);
setImmediate(() => this.emit('send', message));
setImmediate(() => this.emit("send", message));
return this._seq++;
}
}
@@ -55,27 +56,27 @@ class MockServer {
async sendRcon(command) {
this.rconCommands.push(command);
return this.rconCommandResults.get(command) || '';
return this.rconCommandResults.get(command) || "";
}
}
class MockInstance extends link.Link {
constructor() {
super('instance', 'slave', new MockConnector());
super("instance", "slave", new MockConnector());
this.server = new MockServer();
this.name = "test";
this.config = {
get: (name) => {
if (name === "instance.id") { return 7357 }
if (name === "instance.id") { return 7357; }
throw Error("Not implemented");
}
}
},
};
}
}
class MockSlave extends link.Link {
constructor() {
super('slave', 'master', new MockConnector());
super("slave", "master", new MockConnector());
}
}
+14 -13
View File
@@ -1,6 +1,7 @@
const assert = require('assert').strict;
const path = require('path');
const fs = require('fs-extra');
"use strict";
const assert = require("assert").strict;
const path = require("path");
const fs = require("fs-extra");
const link = require("lib/link");
const config = require("lib/config");
@@ -23,16 +24,16 @@ describe("Slave testing", function() {
describe(".name", function() {
it("should give the name of the instance", function() {
assert.equal(instance.name, "foo");
})
});
});
describe(".path()", function() {
it("should give the path when called without arguments", function() {
assert.equal(instance.path(), "dir");
})
});
it("should join path with arguments", function() {
assert.equal(instance.path("bar"), path.join("dir", "bar"));
})
});
});
});
@@ -65,19 +66,19 @@ describe("Slave testing", function() {
});
it("should throw on CON, PRN, AUX, NUL, COM1, LPT1", function() {
for (let bad of ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'LPT1']) {
for (let bad of ["CON", "PRN", "AUX", "NUL", "COM1", "LPT1"]) {
check(
bad, 'cannot be named any of . ..'
+' CON PRN AUX NUL COM1-9 and LPT1-9'
bad, "cannot be named any of . .."
+" CON PRN AUX NUL COM1-9 and LPT1-9"
);
}
});
it("should throw on . and ..", function() {
for (let bad of ['.', '..']) {
for (let bad of [".", ".."]) {
check(
bad, 'cannot be named any of . ..'
+' CON PRN AUX NUL COM1-9 and LPT1-9'
bad, "cannot be named any of . .."
+" CON PRN AUX NUL COM1-9 and LPT1-9"
);
}
});
@@ -93,7 +94,7 @@ describe("Slave testing", function() {
let discardingLogger = {
warning: function() { },
log: function() { },
}
};
let instance;
before(async function() {