diff --git a/.eslintrc.js b/.eslintrc.js index a6e7c847..19bddc01 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -177,7 +177,7 @@ module.exports = { "no-octal-escape": "error", "no-param-reassign": "off", "no-path-concat": "error", - "no-plusplus": ["warn", { "allowForLoopAfterthoughts": true }], + "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }], "no-process-env": "error", "no-process-exit": "warn", "no-proto": "error", @@ -224,7 +224,14 @@ module.exports = { "one-var": "off", "one-var-declaration-per-line": "error", "operator-assignment": "off", - "operator-linebreak": "warn", + "operator-linebreak": ["error", "after", { + "overrides": { + "?": "before", + ":": "before", + "||": "before", + "&&": "before", + }, + }], "padded-blocks": "off", "padding-line-between-statements": "error", "prefer-arrow-callback": "warn", diff --git a/.travis.yml b/.travis.yml index 52d21e37..b75fb52d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ script: - npx lerna bootstrap --no-ci - node packages/lib/build_mod --source-dir packages/slave/lua/clusterio_lib --bump-patch --factorio-version 0.18 --output-dir temp/test/sharedMods - curl -o temp/test/sharedMods/subspace_storage_1.99.1.zip -L https://github.com/clusterio/factorioClusterioMod/releases/download/1.99.1/subspace_storage_1.99.1.zip + - npx lerna run build - npm run-script ci-cover - - cat temp/test/integration/log.txt - codecov -f coverage/lcov.info - npm run-script lint diff --git a/README.md b/README.md index 85a86b4d..b7b2d1af 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,11 @@ cloud storage of items via teleporter chests. But this functionality has been pulled out of Clusterio into its own plugin for Clusterio named [Subspace Storage](https://github.com/clusterio/factorioClusterioMod). -By itself Clusterio does change the gameplay in any way, you could even -use Clusterio to manage completely vanilla Factorio servers. Plugins do -the work of modding in the visible changes into the game, see the -[Plugins section](#plugins) for ready made plugins you can install into -a Clusterio cluster. +By itself Clusterio doesn't change the gameplay in any way, you could +even use Clusterio to manage completely vanilla Factorio servers. +Plugins do the work of modding in the visible changes into the game, see +the [Plugins section](#plugins) for ready made plugins you can install +into a Clusterio cluster. ## Features diff --git a/docs/writing-plugins.md b/docs/writing-plugins.md index de648946..e3a00183 100644 --- a/docs/writing-plugins.md +++ b/docs/writing-plugins.md @@ -55,7 +55,7 @@ clusterioctl. Note that it's important that the path starts with ./ or For a plugin the most important file is the `info.js` file. Without it the plugin will not recognized by Clusterio. Here's an example of it: - const libLink = require('@clusterio/lib/link'); // For messages + const libLink = require("@clusterio/lib/link"); // For messages module.exports = { name: "foo_frobber", @@ -136,7 +136,7 @@ The plugin class should derive from its respective base class defined in `lib/plugin`. For example, to define a MasterPlugin class the following code can be used: - const libPlugin = require('@clusterio/lib/plugin'); + const libPlugin = require("@clusterio/lib/plugin"); class MasterPlugin extends libPlugin.BaseMasterPlugin { async init() { @@ -257,7 +257,7 @@ plugin you listen for an event named `ipc-channel_name` in order to get data sent by `send_json`. For example in the plugin code: async init() { - this.instance.server.on('ipc-my_plugin_foo', content => + this.instance.server.on("ipc-my_plugin_foo", content => this.handleFoo(content).catch(err => console.log( "Error handling foo:", foo )) @@ -270,10 +270,10 @@ data sent by `send_json`. For example in the plugin code: And then in the module for the plugin: - local clusterio_api = require('modules/clusterio/api') + local clusterio_api = require("modules/clusterio/api") -- inside some event handler - clusterio_api.send_json('my_plugin_foo', { data = 123 }) + clusterio_api.send_json("my_plugin_foo", { data = 123 }) It's recommended to either use the plugin name as the channel name or to prefix the channel name with the name of the plugin if you need multiple @@ -311,7 +311,7 @@ and requests. Events are simple one-way notifications that invoke a handler on the target it's sent to. Requests are pairs of request and response messages where the request is sent to the target and the response is the reply back from the target. The requests are similar to -HTTP requests, in that both parties of a link may initiate one. +HTTP requests, except both parties of a link may initiate one. ### Defining Events @@ -324,9 +324,9 @@ the event, for example the following could be defined in `info.js`: messages: { startFrobnication: new libLink.Event({ - type: 'foo_frobber:start_frobnication', - links: ['master-slave', 'slave-instance'], - forwardTo: 'instance', + type: "foo_frobber:start_frobnication", + links: ["master-slave", "slave-instance"], + forwardTo: "instance", eventProperties: { "frobnication_type": { type: "string" }, }, @@ -351,22 +351,22 @@ appended to it. #### links An array of strings describing which links this event can be sent over. -Direction matters, `'master-slave'` means the event can be sent from the +Direction matters, `"master-slave"` means the event can be sent from the master to the slave, but can't be sent back the other way, unless -`'slave-master'` is also present in the links array. +`"slave-master"` is also present in the links array. The available endpoints are `master`, `slave`, `instance`, and `control`. Master talks with slave and control, and slave talks to instance. The full chain must be specified as the individual links in order for a message to travers multiple hops, (i.e., for a message to go -from master to instance it must have both `'master-slave'` and -`'slave-instance'` in the links array). See `forwardTo` and +from master to instance it must have both `"master-slave"` and +`"slave-instance"` in the links array). See `forwardTo` and `broadcastTo` for ways to forward an event to the next link in a chain. #### forwardTo -Target to forward an event to. Can either be `'master'`, to indicate a -slave should forward it to the master server, or `'instance'`, to +Target to forward an event to. Can either be `"master"`, to indicate a +slave should forward it to the master server, or `"instance"`, to indicate it should be forwarded to the instances specified by the `instance_id` event property. This works by using a default handler for the event at the links that forward it. @@ -390,7 +390,7 @@ are not allowed. See [this guide][guide] for an introduction to writing JSON schemas. The forwardTo and broadcastTo can be combined such that specifying -`'master'` as the forwardTo value and `'instance'` as the broadcastTo +`"master"` as the forwardTo value and `"instance"` as the broadcastTo value will cause the event to be broadcast to all instances in the cluster. For this to work, you will need to specify `instance-slave`, `slave-master`, `master-slave`, and `slave-instance` as the links. @@ -409,9 +409,9 @@ the event. For example, the following could be defined in `info.js`: messages: { reportFrobnication: new libLink.Request({ - type: 'foo_frobber:report_frobnication', - links: ['master-slave', 'slave-instance'], - forwardTo: 'instance', + type: "foo_frobber:report_frobnication", + links: ["master-slave", "slave-instance"], + forwardTo: "instance", requestProperties: { "verbosity": { type: "integer" }, }, @@ -427,7 +427,7 @@ the event. For example, the following could be defined in `info.js`: This specifies a request that can be sent from the master to a slave, and from a slave to an instance. The request data must contain the property `verbosity` with an integer number as the value, as well as the -`instance_id` property (implied by `forwardTo: 'instance'`), and the +`instance_id` property (implied by `forwardTo: "instance"`), and the response sent must contain a `report` property mapping to an array of strings. When received by a slave, it will also be forwarded to the instance specified by `instance_id`. @@ -445,24 +445,24 @@ response. #### links An array of strings describing which links this request can be sent -over. Direction matters; `'master-slave'` means the request can be sent +over. Direction matters; `"master-slave"` means the request can be sent from the master to the slave and the slave can reply to the master, but -the slave can't send a request to the master unless `'slave-master'` is +the slave can't send a request to the master unless `"slave-master"` is also present in the links array. The available endpoints are `master`, `slave`, `instance`, and `control`. Master talks with slave and control, and slave talks to instance. The full chain must be specified as the individual links in order for a message to travers multiple hops, (i.e., for a message to go -from master to instance it must have both `'master-slave'` and -`'slave-instance'` in the links array). See `forwardTo` for ways to +from master to instance it must have both `"master-slave"` and +`"slave-instance"` in the links array). See `forwardTo` for ways to forward a request to the next link in a chain. #### forwardTo -Target to forward the request to. Can either be `'master'` to indicate +Target to forward the request to. Can either be `"master"` to indicate a slave should forward it to the master server when receiving it from an -instance, or `'instance'` to indicate it should be forwarded to the +instance, or `"instance"` to indicate it should be forwarded to the instances specified by the `instance_id` request property. This works by using a default handler for the request by the links that forward it. @@ -557,10 +557,10 @@ from plugins optionally loaded at runtime on different computers. In its simplest form collecting data from plugins consists of defining the metric and updating it somewhere in the plugin code. For example: - const { Counter } = require('@clusterio/lib/prometheus'); + const { Counter } = require("@clusterio/lib/prometheus"); const fooMetric = new Counter( - 'clusterio_foo_frobber_foo_metric', "Measures the level of foo", + "clusterio_foo_frobber_foo_metric", "Measures the level of foo", ); // Somewhere in the master plugin code @@ -574,11 +574,11 @@ naming scheme. For metrics that are per-instance, you must define an `instance_id` label and set it accordingly, for example: - const { Counter } = require('@clusterio/lib/prometheus'); + const { Counter } = require("@clusterio/lib/prometheus"); const barMetric = new Gauge( - 'clusterio_foo_frobber_bar_metric', "Bar instance level", - { labels: ['instance_id'] } + "clusterio_foo_frobber_bar_metric", "Bar instance level", + { labels: ["instance_id"] } ); // Somewhere in the instance plugin code diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.js index 514cd0eb..3966abb8 100755 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.js @@ -17,6 +17,7 @@ const libLink = require("@clusterio/lib/link"); const libErrors = require("@clusterio/lib/errors"); const libConfig = require("@clusterio/lib/config"); const libPlugin = require("@clusterio/lib/plugin"); +const libPluginLoader = require("@clusterio/lib/plugin_loader"); const libCommand = require("@clusterio/lib/command"); const libSharedCommands = require("@clusterio/lib/shared_commands"); @@ -73,7 +74,7 @@ slaveCommands.add(new libCommand.Command({ yargs.option("id", { type: "number", nargs: 1, describe: "Slave id", demandOption: true }); }], handler: async function(args, control) { - let response = await link.messages.generateSlaveToken.send(control, { slave_id: args.id }); + let response = await libLink.messages.generateSlaveToken.send(control, { slave_id: args.id }); console.log(response.token); }, })); @@ -594,7 +595,7 @@ async function startControl() { type: "string", }) .command("plugin", "Manage available plugins", libSharedCommands.pluginCommand) - .command("control-config", "Manage Control config", libConfig.configCommand) + .command("control-config", "Manage Control config", libSharedCommands.configCommand) .wrap(yargs.terminalWidth()) .help(false) // Disable help to avoid triggering it on the first parse. ; @@ -628,7 +629,7 @@ async function startControl() { rootCommands.add(debugCommands); console.log("Loading Plugin info"); - let pluginInfos = await libPlugin.loadPluginInfos(pluginList); + let pluginInfos = await libPluginLoader.loadPluginInfos(pluginList); libConfig.registerPluginConfigGroups(pluginInfos); libConfig.finalizeConfigs(); @@ -680,7 +681,7 @@ async function startControl() { // Handle the control-config command before trying to connect. if (args._[0] === "control-config") { - await libConfig.handleConfigCommand(args, controlConfig, args.config); + await libSharedCommands.handleConfigCommand(args, controlConfig, args.config); return; } diff --git a/packages/lib/config/definitions.js b/packages/lib/config/definitions.js index 7958db7e..c95c0fb3 100644 --- a/packages/lib/config/definitions.js +++ b/packages/lib/config/definitions.js @@ -1,8 +1,6 @@ // Core definitions for the configuration system "use strict"; -const fs = require("fs-extra"); - const classes = require("./classes"); @@ -69,9 +67,9 @@ MasterGroup.define({ name: "auth_secret", title: "Master Authentication Secret", description: - "Secret used to generate and verify authentication tokens." - +" Should be a long string of random letters and numbers." - +" Do not share this.", + "Secret used to generate and verify authentication tokens. " + + "Should be a long string of random letters and numbers. " + + "Do not share this.", type: "string", optional: true, }); @@ -379,77 +377,6 @@ function finalizeConfigs() { ControlConfig.finalize(); } -/** - * Yargs config command - * - * 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) { - yargs - .command("set [value]", "Set config field") - .command("show ", "Show value of the given config field") - .command("list", "List all configuration fields and their values") - .demandCommand(1, "You need to specify a command to run") - .help() - .strict() - ; -} - -/** - * Handle yargs command - * - * 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) { - let command = args._[1]; - - if (command === "list") { - for (let GroupClass of instance.constructor.groups.values()) { - for (let def of GroupClass.definitions.values()) { - let value = instance.get(def.fullName); - console.log(`${def.fullName} ${JSON.stringify(value)}`); - } - } - - } else if (command === "show") { - try { - console.log(instance.get(args.field)); - } catch (err) { - if (err instanceof classes.InvalidField) { - console.error(err.message); - } else { - throw err; - } - } - - } else if (command === "set") { - if (args.value === undefined) { - args.value = null; - } - - try { - instance.set(args.field, args.value); - await fs.outputFile(configPath, JSON.stringify(instance.serialize(), null, 4)); - } catch (err) { - if (err instanceof classes.InvalidField || err instanceof classes.InvalidValue) { - console.error(err.message); - } else { - throw err; - } - } - } -} - - module.exports = { MasterGroup, SlaveGroup, @@ -464,6 +391,4 @@ module.exports = { registerPluginConfigGroups, finalizeConfigs, - configCommand, - handleConfigCommand, }; diff --git a/packages/lib/factorio/patch.js b/packages/lib/factorio/patch.js index 7e9fbec6..6b70b1d2 100644 --- a/packages/lib/factorio/patch.js +++ b/packages/lib/factorio/patch.js @@ -152,7 +152,7 @@ function reorderDependencies(modules) { // No unmet dependencies, record and continue present.set(module.name, module.version); - index++; + index += 1; if (hold.has(module.name)) { modules.splice(index, 0, ...hold.get(module.name)); diff --git a/packages/lib/link/connectors.js b/packages/lib/link/connectors.js index 37f9c05e..a8d494c9 100644 --- a/packages/lib/link/connectors.js +++ b/packages/lib/link/connectors.js @@ -34,7 +34,7 @@ class WebSocketBaseConnector extends events.EventEmitter { let dropCount = 0; for (let index = 0; index < this._sendBuffer.length; index++) { if (this._sendBuffer[index].seq <= seq) { - dropCount++; + dropCount += 1; } else { break; } @@ -102,12 +102,14 @@ class WebSocketBaseConnector extends events.EventEmitter { if (!["handshake", "connected", "closing"].includes(this._state)) { throw new libErrors.SessionLost("No session"); } - let message = { seq: this._seq, type, data }; + let seq = this._seq; + this._seq += 1; + let message = { seq, type, data }; this._sendBuffer.push(message); if (["connected", "closing"].includes(this._state)) { this._socket.send(JSON.stringify(message)); } - return this._seq++; + return seq; } /** @@ -450,8 +452,10 @@ class VirtualConnector extends events.EventEmitter { * @returns {number} the sequence number of the message sent */ send(type, data) { - this.other.emit("message", { seq: this._seq, type, data }); - return this._seq++; + let seq = this._seq; + this._seq += 1; + this.other.emit("message", { seq, type, data }); + return seq; } } diff --git a/packages/lib/plugin.js b/packages/lib/plugin.js index 3fe5a9f3..5444ba23 100644 --- a/packages/lib/plugin.js +++ b/packages/lib/plugin.js @@ -3,9 +3,6 @@ * @module lib/plugin */ "use strict"; -const path = require("path"); - -const libErrors = require("@clusterio/lib/errors"); /** @@ -386,45 +383,6 @@ class BaseControlPlugin { async addCommands(rootCommand) { } } -/** - * Load plugin information - * - * Loads plugin info modules for the paths to the given plugins. Once - * loaded the info modules will not be reloaded should this function be - * called again. - * - * @param {Map} pluginList - - * Mapping of plugin name to require path for the plugins to load. - * @returns {Array} Array of plugin info modules. - * @static - */ -async function loadPluginInfos(pluginList) { - let plugins = []; - for (let [pluginName, pluginPath] of pluginList) { - let pluginInfo; - let pluginPackage; - - try { - pluginInfo = require(path.posix.join(pluginPath, "info")); - pluginPackage = require(path.posix.join(pluginPath, "package.json")); - - } catch (err) { - throw new libErrors.PluginError(pluginName, err); - } - - if (pluginInfo.name !== pluginName) { - throw new libErrors.EnvironmentError( - `Expected plugin at ${pluginPath} to be named ${pluginName} but got ${pluginInfo.name}` - ); - } - - pluginInfo.requirePath = pluginPath; - pluginInfo.version = pluginPackage.version; - plugins.push(pluginInfo); - } - return plugins; -} - /** * Attach plugin messages * @@ -485,7 +443,6 @@ module.exports = { BaseMasterPlugin, BaseControlPlugin, - loadPluginInfos, attachPluginMessages, invokeHook, }; diff --git a/packages/lib/plugin_loader.js b/packages/lib/plugin_loader.js new file mode 100644 index 00000000..8307eef4 --- /dev/null +++ b/packages/lib/plugin_loader.js @@ -0,0 +1,52 @@ +/** + * Helpers for loading plugins in Node.js + * @module lib/plugin_loader + */ +"use strict"; + +const libErrors = require("@clusterio/lib/errors"); +const path = require("path"); + + +/** + * Load plugin information + * + * Loads plugin info modules for the paths to the given plugins. Once + * loaded the info modules will not be reloaded should this function be + * called again. + * + * @param {Map} pluginList - + * Mapping of plugin name to require path for the plugins to load. + * @returns {Array} Array of plugin info modules. + * @static + */ +async function loadPluginInfos(pluginList) { + let plugins = []; + for (let [pluginName, pluginPath] of pluginList) { + let pluginInfo; + let pluginPackage; + + try { + pluginInfo = require(path.posix.join(pluginPath, "info")); + pluginPackage = require(path.posix.join(pluginPath, "package.json")); + + } catch (err) { + throw new libErrors.PluginError(pluginName, err); + } + + if (pluginInfo.name !== pluginName) { + throw new libErrors.EnvironmentError( + `Expected plugin at ${pluginPath} to be named ${pluginName} but got ${pluginInfo.name}` + ); + } + + pluginInfo.requirePath = pluginPath; + pluginInfo.version = pluginPackage.version; + plugins.push(pluginInfo); + } + return plugins; +} + +module.exports = { + loadPluginInfos, +}; diff --git a/packages/lib/shared_commands.js b/packages/lib/shared_commands.js index 29550cb6..0bd370ce 100644 --- a/packages/lib/shared_commands.js +++ b/packages/lib/shared_commands.js @@ -6,6 +6,9 @@ const fs = require("fs-extra"); const path = require("path"); +const libConfig = require("./config"); + + /** * Yargs plugin command * @@ -80,7 +83,80 @@ async function handlePluginCommand(args, pluginList, pluginListPath) { } } + +/** + * Yargs config command + * + * Can be passed to yargs.command to implement a config command. Use + * handleConfigCommand to do the requested action. + * + * @param {Object} yargs - yargs command builder. + */ +function configCommand(yargs) { + yargs + .command("set [value]", "Set config field") + .command("show ", "Show value of the given config field") + .command("list", "List all configuration fields and their values") + .demandCommand(1, "You need to specify a command to run") + .help() + .strict() + ; +} + +/** + * Handle yargs command + * + * 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. + */ +async function handleConfigCommand(args, instance, configPath) { + let command = args._[1]; + + if (command === "list") { + for (let GroupClass of instance.constructor.groups.values()) { + for (let def of GroupClass.definitions.values()) { + let value = instance.get(def.fullName); + console.log(`${def.fullName} ${JSON.stringify(value)}`); + } + } + + } else if (command === "show") { + try { + console.log(instance.get(args.field)); + } catch (err) { + if (err instanceof libConfig.InvalidField) { + console.error(err.message); + } else { + throw err; + } + } + + } else if (command === "set") { + if (args.value === undefined) { + args.value = null; + } + + try { + instance.set(args.field, args.value); + await fs.outputFile(configPath, JSON.stringify(instance.serialize(), null, 4)); + } catch (err) { + if (err instanceof libConfig.InvalidField || err instanceof libConfig.InvalidValue) { + console.error(err.message); + } else { + throw err; + } + } + } +} + + module.exports = { pluginCommand, handlePluginCommand, + + configCommand, + handleConfigCommand, }; diff --git a/packages/master/README.md b/packages/master/README.md index 99e76433..8e22c08c 100644 --- a/packages/master/README.md +++ b/packages/master/README.md @@ -110,6 +110,13 @@ exist in the cluster and grant it full cluster admin permission. You should be using your own Factorio multiplayer username here. +#### `bootstrap generate-user-token ` + +Generate an access token for logging in and possibly managing the +cluster from the given user account. This can be used both in the web +interface and as the `master_token` to connect with clusterioctl. + + #### `bootstrap create-ctl-config ` Creates a clusterioctl config for the given user with url and token set diff --git a/packages/master/master.js b/packages/master/master.js index b8adf2fd..37efa82c 100755 --- a/packages/master/master.js +++ b/packages/master/master.js @@ -49,6 +49,7 @@ const libSchema = require("@clusterio/lib/schema"); const libLink = require("@clusterio/lib/link"); const libErrors = require("@clusterio/lib/errors"); const libPlugin = require("@clusterio/lib/plugin"); +const libPluginLoader = require("@clusterio/lib/plugin_loader"); const libPrometheus = require("@clusterio/lib/prometheus"); const libConfig = require("@clusterio/lib/config"); const libUsers = require("@clusterio/lib/users"); @@ -598,7 +599,7 @@ class ControlConnection extends BaseConnection { } async generateSlaveTokenRequestHandler(message) { - return { token: generateSlaveToken(message.data.slave_id) }; + return { token: this.generateSlaveToken(message.data.slave_id) }; } async createSlaveConfigRequestHandler(message) { @@ -1429,7 +1430,8 @@ async function handleHandshake(message, socket, req, attachHandler) { return; } - let sessionId = nextSessionId++; + let sessionId = nextSessionId; + nextSessionId += 1; let sessionToken = jwt.sign({ aud: masterSession, sid: sessionId }, masterConfig.get("master.auth_secret")); let connector = new WebSocketServerConnector(socket, sessionId); activeConnectors.set(sessionId, connector); @@ -1572,10 +1574,11 @@ async function startServer() { type: "string", }) .command("plugin", "Manage available plugins", libSharedCommands.pluginCommand) - .command("config", "Manage Master config", libConfig.configCommand) + .command("config", "Manage Master config", libSharedCommands.configCommand) .command("bootstrap", "Bootstrap access to cluster", yargs => { yargs .command("create-admin ", "Create a cluster admin") + .command("generate-user-token ", "Generate authentication token for the given user") .command("create-ctl-config ", "Create clusterioctl config for the given user", yargs => { yargs.option("output", { describe: "Path to output config (- for stdout)", type: "string", @@ -1610,7 +1613,7 @@ async function startServer() { } console.log("Loading Plugin info"); - pluginInfos = await libPlugin.loadPluginInfos(pluginList); + pluginInfos = await libPluginLoader.loadPluginInfos(pluginList); libConfig.registerPluginConfigGroups(pluginInfos); libConfig.finalizeConfigs(); @@ -1639,7 +1642,7 @@ async function startServer() { } if (command === "config") { - await libConfig.handleConfigCommand(args, masterConfig, masterConfigPath); + await libSharedCommands.handleConfigCommand(args, masterConfig, masterConfigPath); return; } else if (command === "bootstrap") { @@ -1662,6 +1665,15 @@ async function startServer() { admin.isAdmin = true; await saveUsers(masterConfig.get("master.database_directory"), "users.json"); + } else if (subCommand === "generate-user-token") { + let user = db.users.get(args.name); + if (!user) { + console.error(`No user named '${args.name}'`); + process.exitCode = 1; + return; + } + console.log(user.createToken(masterConfig.get("master.auth_secret"))); + } else if (subCommand === "create-ctl-config") { let admin = db.users.get(args.name); if (!admin) { diff --git a/packages/slave/slave.js b/packages/slave/slave.js index 71974e5f..37c13d7b 100755 --- a/packages/slave/slave.js +++ b/packages/slave/slave.js @@ -28,6 +28,7 @@ const libFileOps = require("@clusterio/lib/file_ops"); const libFactorio = require("@clusterio/lib/factorio"); const libLink = require("@clusterio/lib/link"); const libPlugin = require("@clusterio/lib/plugin"); +const libPluginLoader = require("@clusterio/lib/plugin_loader"); const libErrors = require("@clusterio/lib/errors"); const libPrometheus = require("@clusterio/lib/prometheus"); const libLuaTools = require("@clusterio/lib/lua_tools"); @@ -1394,7 +1395,7 @@ async function startSlave() { type: "string", }) .command("plugin", "Manage available plugins", libSharedCommands.pluginCommand) - .command("config", "Manage Slave config", libConfig.configCommand) + .command("config", "Manage Slave config", libSharedCommands.configCommand) .command("run", "Run slave") .demandCommand(1, "You need to specify a command to run") .strict() @@ -1419,7 +1420,7 @@ async function startSlave() { } console.log("Loading Plugin info"); - let pluginInfos = await libPlugin.loadPluginInfos(pluginList); + let pluginInfos = await libPluginLoader.loadPluginInfos(pluginList); libConfig.registerPluginConfigGroups(pluginInfos); libConfig.finalizeConfigs(); @@ -1439,7 +1440,7 @@ async function startSlave() { } if (command === "config") { - await libConfig.handleConfigCommand(args, slaveConfig, args.config); + await libSharedCommands.handleConfigCommand(args, slaveConfig, args.config); return; } diff --git a/packages/web_ui/src/components/PageLayout.jsx b/packages/web_ui/src/components/PageLayout.jsx index dda21b32..f4373d32 100644 --- a/packages/web_ui/src/components/PageLayout.jsx +++ b/packages/web_ui/src/components/PageLayout.jsx @@ -6,7 +6,7 @@ const { Content } = Layout; export default function PageLayout(props) { return <> - + {props.nav.map((part, index) => {part.path ? {part.name} : part.name} )} diff --git a/packages/web_ui/src/components/SiteLayout.jsx b/packages/web_ui/src/components/SiteLayout.jsx index 88756b87..d82ef9e5 100644 --- a/packages/web_ui/src/components/SiteLayout.jsx +++ b/packages/web_ui/src/components/SiteLayout.jsx @@ -48,10 +48,11 @@ export default function SiteLayout(props) { Dashboard - + {name})} - + {sidebar.map(({path, content}) => diff --git a/packages/web_ui/src/index.css b/packages/web_ui/src/index.css index 692bef64..246613cb 100644 --- a/packages/web_ui/src/index.css +++ b/packages/web_ui/src/index.css @@ -26,6 +26,38 @@ body { flex-grow: 0; } +.site-layout { + overflow-x: hidden; +} + +.site-layout-content-container { + min-width: 100%; +} + +.site-breadcrumb { + margin: 16px 0 16px 52px; +} + +@media (min-width: 576px) { + .site-layout-content-container { + padding: 0 24px 24px; + } + + .site-breadcrumb { + margin-left: 28px; + } +} + +@media (min-width: 768px) { + .site-layout { + overflow-x: initial; + } + + .site-layout-content-container { + min-width: initial; + } +} + .site-layout-sider { z-index: 0; } diff --git a/packages/web_ui/src/util/wslink.jsx b/packages/web_ui/src/util/wslink.jsx deleted file mode 100644 index 07d4775a..00000000 --- a/packages/web_ui/src/util/wslink.jsx +++ /dev/null @@ -1,116 +0,0 @@ -import * as errors from "@clusterio/lib/errors"; -import * as link from "@clusterio/lib/link"; - -// Note: control is created as in bootstrap.jsx as a global. - -let listSlaves = createListFunction("listSlaves"); -let listInstances = createListFunction("listInstances"); -let listUsers = createListFunction("listUsers", "name"); -let listRoles = createListFunction("listRoles"); -let listPermissions = createListFunction("listPermissions", "name"); - -async function startInstance({ instance_id, save = null }) { - return await link.messages.startInstance.send(control, { instance_id, save }); -}; -async function stopInstance({ instance_id }) { - return await link.messages.stopInstance.send(control, { instance_id }); -}; -async function createInstance(serialized_config) { - return await link.messages.createInstance.send(control, { serialized_config }); -}; -async function deleteInstance({ instance_id }) { - return await link.messages.deleteInstance.send(control, { instance_id }); -}; -async function assignInstance({ instance_id, slave_id }) { - return await link.messages.assignInstanceCommand.send(control, { - instance_id, - slave_id, - }); -}; -async function createSave({ instance_id }) { - return await link.messages.createSave.send(control, { instance_id }); -}; -async function getInstanceConfig({ instance_id }) { - return await link.messages.getInstanceConfig.send(control, { instance_id }); -}; -async function setInstanceConfigField({ instance_id, field, value }) { - return await link.messages.setInstanceConfigField.send(control, { - instance_id, - field, - value: typeof value === "object" ? JSON.stringify(value) : value, - }); -}; -async function setInstanceConfigProp({ instance_id, field, prop, value }) { - return await link.messages.setInstanceConfigProp.send(control, { - instance_id, - field, - prop, - value, // JSON Object - }); -}; -async function sendRcon({ instance_id, command }) { - return await link.messages.sendRcon.send(control, { instance_id, command }); -}; -async function setInstanceOutputSubscriptions({ instance_id }) { - return await link.messages.setInstanceOutputSubscriptions.send(control, { instance_ids: [instance_id] }); -}; - -async function createUser({ name }) { - return await link.messages.createUser.send(control, { name }); -}; -async function deleteUser({ name }) { - return await link.messages.deleteUser.send(control, { name }); -}; -async function setRoles({ name, roles }) { - return await link.messages.updateUserRoles.send(control, { - name, - roles, - }); -}; - -async function createRole({ name, description, permissions = [] }) { - return await link.messages.createRole.send(control, { name, description, permissions }); -}; -async function updateRole({ id, name, description, permissions }) { - return await link.messages.updateRole.send(control, { - id, name, description, permissions, - }); -}; - -export { - listSlaves, - listInstances, - listUsers, - createUser, - deleteUser, - listRoles, - setRoles, - createRole, - updateRole, - listPermissions, - startInstance, - stopInstance, - createInstance, - deleteInstance, - assignInstance, - createSave, - getInstanceConfig, - setInstanceConfigField, - setInstanceConfigProp, - sendRcon, - setInstanceOutputSubscriptions, -}; - -function createListFunction(name, key) { - return async () => ( - (await link.messages[name].send(control)).list.map(addKey(key || "id")) - ); -} -function addKey(key) { - return function (el) { - return { - ...el, - key: el[key], - }; - }; -} diff --git a/plugins/global_chat/test/plugin.js b/plugins/global_chat/test/plugin.js index 7576a9b6..f371d360 100644 --- a/plugins/global_chat/test/plugin.js +++ b/plugins/global_chat/test/plugin.js @@ -46,7 +46,7 @@ describe("global_chat plugin", function() { instancePlugin.instance.connector.sentMessages = []; await instancePlugin.onOutput(output); assert(instancePlugin.instance.connector.sentMessages.length, "message was not sent"); - count++; + count += 1; } } assert(count > 0, "no lines were tested"); @@ -58,7 +58,7 @@ describe("global_chat plugin", function() { instancePlugin.instance.connector.sentMessages = []; await instancePlugin.onOutput(output); assert(!instancePlugin.instance.connector.sentMessages.length, "message was sent"); - count++; + count += 1; } } assert(count > 0, "no lines were tested"); diff --git a/plugins/subspace_storage/info.js b/plugins/subspace_storage/info.js index bee1faff..b60fe71d 100644 --- a/plugins/subspace_storage/info.js +++ b/plugins/subspace_storage/info.js @@ -21,7 +21,7 @@ MasterConfigGroup.define({ }); MasterConfigGroup.define({ name: "log_item_transfers", - title: "Log Item Trassfers", + title: "Log Item Transfers", description: "Spam master console with item transfers done.", type: "boolean", initial_value: false, @@ -32,7 +32,7 @@ class InstanceConfigGroup extends libConfig.PluginConfigGroup {} InstanceConfigGroup.groupName = "subspace_storage"; InstanceConfigGroup.define({ name: "log_item_transfers", - title: "Log Item Trassfers", + title: "Log Item Transfers", description: "Spam slave console with item transfers done.", type: "boolean", initial_value: false, diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index 96b90146..e9d80333 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -5,7 +5,7 @@ const path = require("path"); const libLink = require("@clusterio/lib/link"); -const { slowTest, get, execCtl, sendRcon, getControl, instancesDir } = require("./index"); +const { slowTest, get, exec, execCtl, sendRcon, getControl, instancesDir } = require("./index"); async function getInstances() { @@ -19,12 +19,31 @@ async function checkInstanceStatus(id, status) { } describe("Integration of Clusterio", function() { + describe("clusteriomaster", function() { + describe("bootstrap generate-user-token", function() { + it("work for existing user", async function() { + await exec("node ../../packages/master bootstrap generate-user-token test"); + }); + + it("fails if user does not exist", async function() { + await assert.rejects( + exec("node ../../packages/master bootstrap generate-user-token invalid") + ); + }); + }); + }); + describe("clusterioctl", function() { describe("slave list", function() { it("runs", async function() { await execCtl("slave list"); }); }); + describe("slave generate-token", function() { + it("runs", async function() { + await execCtl("slave generate-token --id 42"); + }); + }); describe("instance list", function() { it("runs", async function() { await execCtl("instance list"); diff --git a/test/integration/export.js b/test/integration/export.js index c7dc20f8..dccfbb10 100644 --- a/test/integration/export.js +++ b/test/integration/export.js @@ -24,7 +24,7 @@ describe("Integration of lib/factorio/export", function() { // Value is always a string, nearly always truthy if (!value) { - empty_keys++; + empty_keys += 1; } assert.equal(typeof value, "string"); } diff --git a/test/lib/factorio/patch.js b/test/lib/factorio/patch.js index d5d1bdfb..00850590 100644 --- a/test/lib/factorio/patch.js +++ b/test/lib/factorio/patch.js @@ -199,7 +199,7 @@ describe("lib/factorio", function() { } catch (err) { } if (success) { validate(modules); - successCount++; + successCount += 1; } } diff --git a/test/lib/plugin.js b/test/lib/plugin.js index 5ef3b34f..2ca64415 100644 --- a/test/lib/plugin.js +++ b/test/lib/plugin.js @@ -1,7 +1,5 @@ "use strict"; const assert = require("assert").strict; -const fs = require("fs-extra"); -const path = require("path"); const mock = require("../mock"); const libLink = require("@clusterio/lib/link"); @@ -41,56 +39,6 @@ describe("lib/plugin", function() { }); }); - describe("loadPluginInfos()", function() { - let baseDir = path.join("temp", "test", "plugin"); - let missingPlugin = path.join(baseDir, "missing_plugin"); - let testPlugin = path.join(baseDir, "test_plugin"); - let brokenPlugin = path.join(baseDir, "broken_plugin"); - let invalidPlugin = path.join(baseDir, "invalid_plugin"); - before(async function() { - async function writePlugin(pluginPath, infoName) { - await fs.outputFile( - path.join(pluginPath, "info.js"), - `module.exports = { name: "${infoName}" };` - ); - await fs.outputFile( - path.join(pluginPath, "package.json"), - '{ "version": "0.0.1" }' - ); - } - - await writePlugin(testPlugin, "test"); - await writePlugin(brokenPlugin, "broken"); - await fs.outputFile(path.join(brokenPlugin, "info.js"), "Syntax Error"); - await writePlugin(invalidPlugin, "wrong"); - }); - - it("should throw on missing plugin", async function() { - await assert.rejects( - libPlugin.loadPluginInfos(new Map([["missing", missingPlugin]]), []), - new RegExp(`^Error: PluginError: Cannot find module '${missingPlugin}/info'`) - ); - }); - it("should load test plugin", async function() { - assert.deepEqual( - await libPlugin.loadPluginInfos(new Map([["test", path.resolve(testPlugin)]])), - [{ name: "test", version: "0.0.1", requirePath: path.resolve(testPlugin) }] - ); - }); - it("should reject on broken plugin", async function() { - await assert.rejects( - libPlugin.loadPluginInfos(new Map([["broken", path.resolve(brokenPlugin)]])), - { message: "PluginError: Unexpected identifier" } - ); - }); - it("should reject on invalid plugin", async function() { - await assert.rejects( - libPlugin.loadPluginInfos(new Map([["invalid", path.resolve(invalidPlugin)]])), - { message: `Expected plugin at ${path.resolve(invalidPlugin)} to be named invalid but got wrong` } - ); - }); - }); - describe("attachPluginMessages()", function() { let mockLink = new libLink.Link("source", "target", new mock.MockConnector()); let mockEvent = new libLink.Event({ type: "test:test", links: ["target-source"] }); diff --git a/test/lib/plugin_loader.js b/test/lib/plugin_loader.js new file mode 100644 index 00000000..c5ce7332 --- /dev/null +++ b/test/lib/plugin_loader.js @@ -0,0 +1,59 @@ +"use strict"; +const assert = require("assert").strict; +const fs = require("fs-extra"); +const path = require("path"); + +const libPluginLoader = require("@clusterio/lib/plugin_loader"); + + +describe("lib/plugin_loader", function() { + describe("loadPluginInfos()", function() { + let baseDir = path.join("temp", "test", "plugin"); + let missingPlugin = path.join(baseDir, "missing_plugin"); + let testPlugin = path.join(baseDir, "test_plugin"); + let brokenPlugin = path.join(baseDir, "broken_plugin"); + let invalidPlugin = path.join(baseDir, "invalid_plugin"); + before(async function() { + async function writePlugin(pluginPath, infoName) { + await fs.outputFile( + path.join(pluginPath, "info.js"), + `module.exports = { name: "${infoName}" };` + ); + await fs.outputFile( + path.join(pluginPath, "package.json"), + '{ "version": "0.0.1" }' + ); + } + + await writePlugin(testPlugin, "test"); + await writePlugin(brokenPlugin, "broken"); + await fs.outputFile(path.join(brokenPlugin, "info.js"), "Syntax Error"); + await writePlugin(invalidPlugin, "wrong"); + }); + + it("should throw on missing plugin", async function() { + await assert.rejects( + libPluginLoader.loadPluginInfos(new Map([["missing", missingPlugin]]), []), + new RegExp(`^Error: PluginError: Cannot find module '${missingPlugin}/info'`) + ); + }); + it("should load test plugin", async function() { + assert.deepEqual( + await libPluginLoader.loadPluginInfos(new Map([["test", path.resolve(testPlugin)]])), + [{ name: "test", version: "0.0.1", requirePath: path.resolve(testPlugin) }] + ); + }); + it("should reject on broken plugin", async function() { + await assert.rejects( + libPluginLoader.loadPluginInfos(new Map([["broken", path.resolve(brokenPlugin)]])), + { message: "PluginError: Unexpected identifier" } + ); + }); + it("should reject on invalid plugin", async function() { + await assert.rejects( + libPluginLoader.loadPluginInfos(new Map([["invalid", path.resolve(invalidPlugin)]])), + { message: `Expected plugin at ${path.resolve(invalidPlugin)} to be named invalid but got wrong` } + ); + }); + }); +}); diff --git a/test/mock.js b/test/mock.js index 9b9f6cdb..c8cddfbb 100644 --- a/test/mock.js +++ b/test/mock.js @@ -58,10 +58,12 @@ class MockConnector extends events.EventEmitter { } send(type, data) { - let message = { seq: this._seq, type, data }; + let seq = this._seq; + this._seq += 1; + let message = { seq, type, data }; this.sentMessages.push(message); setImmediate(() => this.emit("send", message)); - return this._seq++; + return seq; } } diff --git a/test/slave.js b/test/slave.js index b7b12ee9..a14c40f1 100644 --- a/test/slave.js +++ b/test/slave.js @@ -67,19 +67,13 @@ 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"]) { - check( - bad, "cannot be named any of . .." - +" CON PRN AUX NUL COM1-9 and LPT1-9" - ); + check(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 [".", ".."]) { - check( - bad, "cannot be named any of . .." - +" CON PRN AUX NUL COM1-9 and LPT1-9" - ); + check(bad, "cannot be named any of . .. CON PRN AUX NUL COM1-9 and LPT1-9"); } });