mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 15:04:52 +09:00
Reverse the direction of the API for sending messages from
messageDefinition.send(link, { content });
to
link.send(new MessageClass(content));
and convert all message definitions to use the JSON serialisable class
pattern and explicit registration of message handlers, along with a
end-to-end based routing of messages instead of link to link.
The experience I gained from the existing way messages were defined and
sent over links was that it was difficult to understand for people
unfamiliar with the system. The automatic registration of message
handlers ment that code was being called as if by magic from nowhere
with no easy way to discover it. The manual forwarding and rules for
what can be sent where and how obscured its working.
The use of pure JSON as both input and output from message handlers made
it tedious to work with. Data had to be manually converted between the
program representation and the over the wire format, which made handler
code unplesant to write. Particularly because the over the wire format
used snake_case while the program code uses camelCase for variables.
The idea behind the decision to do it this way was to make it more clear
which data came from the wire and which from the program, and while this
worked it didn't make message handlers any nicer to write.
The direction of the API and magic registration of message handlers also
meant that a future migration to TypeScript would be troublesome. While
it may be possible to implement, the degree of TypeScript magic required
would go starkly against the principle of keep it simple stupid.
By refactoring to using the JSON serialisable class pattern most of
these issues go away. There is no longer a need to distinguish the on
wire format of messages with the program representation because the only
place that deals with the wire format of messages are the toJSON and
fromJSON methods on the classes themselves. Message handlers will work
with proper instances of classes instead of raw data, something that
makes them easier to write, and trivial to type with TypeScript.
End-to-end routing also simplifies sending messages that need to do
mulitple hops to reach the destination. And removes the need for having
all messages be specified in plugin's info module, as they can be routed
without having to be parsed completely.
The explicit registration of message handlers is something that I
thought would be tedious boilerplate code that could be automated away.
But experience now shows it to be the opposite. Allthough dull
repetition, the purpose it serves is to link together the code in a way
that humans can easily find. With explicit registration searching the
name of the message handler in the editor will show the registration that
ties it together with the type of message it handles and the kind of
registration. With implicit registration searching the name of the
message handler turns up only the handler with no clue as to what calls
it.
284 lines
7.0 KiB
JavaScript
284 lines
7.0 KiB
JavaScript
/**
|
|
* Command library
|
|
*
|
|
* Data types and utilites for commands to clusterioctl's CLI.
|
|
*
|
|
* @author Hornwitser
|
|
* @module lib/command
|
|
*/
|
|
"use strict";
|
|
const libData = require("./data");
|
|
const libLink = require("./link");
|
|
const libErrors = require("./errors");
|
|
|
|
/**
|
|
* Represents a command that can be runned by clusterioctl
|
|
* @static
|
|
*/
|
|
class Command {
|
|
/**
|
|
* Define an executable command.
|
|
*
|
|
* @param {Object} cmd - Command definiton.
|
|
* @param {Array} cmd.definiton -
|
|
* Arguments to pass to yargs .command method to define this
|
|
* command.
|
|
* @param {function(args, control)} handler -
|
|
* Async function invoked when the command is executed. Is given
|
|
* the parsed args and a reference to the {@link
|
|
* module:ctl/ctl.Control} instance.
|
|
*/
|
|
constructor({ definition, handler }) {
|
|
if (definition[0] instanceof Array) {
|
|
this.name = definition[0][0].split(" ")[0];
|
|
this.alias = definition[0].slice(1);
|
|
} else {
|
|
this.name = definition[0].split(" ")[0];
|
|
this.alias = [];
|
|
}
|
|
this._definition = definition;
|
|
this._handler = handler;
|
|
}
|
|
|
|
register(yargs) {
|
|
yargs.command(...this._definition);
|
|
}
|
|
|
|
async run(args, control) {
|
|
await this._handler(args, control);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A node in the command tree that can hold commands and sub trees
|
|
*
|
|
* Container which can contain {@link module:lib/command.Command}s and other
|
|
* CommandTrees. This is used by clusterioctl to hold the full tree of
|
|
* available commands, you may extend this tree by using a control plugin,
|
|
* see {@link module:lib/plugin.BaseControlPlugin#addCommands}
|
|
*
|
|
* @static
|
|
*/
|
|
class CommandTree {
|
|
/**
|
|
* Define a command containing sub commands.
|
|
*
|
|
* @param {Object} cmd - Command definition
|
|
* @param {string} cmd.name - Name of the command.
|
|
* @param {Array<string>} cmd.alias - Aliases for this tree.
|
|
* @param {description} cmd.description -
|
|
* Descripton to provide for this command tree node.
|
|
*/
|
|
constructor({ name, alias, description }) {
|
|
if (typeof name !== "string") {
|
|
throw new Error("name must be a string");
|
|
}
|
|
|
|
this.name = name;
|
|
this.alias = alias || [];
|
|
this.description = description;
|
|
this.subCommands = new Map();
|
|
}
|
|
|
|
/**
|
|
* Add a command or command tree to this tree
|
|
*
|
|
* @param {module:lib/command.Command|module:lib/command.CommandTree} command -
|
|
* The command to add to this command tree.
|
|
*/
|
|
add(command) {
|
|
if (this.subCommands.has(command.name)) {
|
|
throw new Error(`Command ${command.name} already exists`);
|
|
}
|
|
this.subCommands.set(command.name, command);
|
|
for (let alias of command.alias) {
|
|
if (this.subCommands.has(alias)) {
|
|
throw new Error(`Alias ${alias} for command ${command.name} already exists`);
|
|
}
|
|
this.subCommands.set(alias, command);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a command or command tree from this tree
|
|
*
|
|
* @param {string} name -
|
|
* The name of the command or command tree to retrieve.
|
|
* @return {?module:lib/command.Command|module:lib/command.CommandTree}
|
|
* The command to add to this command tree.
|
|
*/
|
|
get(name) {
|
|
return this.subCommands.get(name) || null;
|
|
}
|
|
|
|
/**
|
|
* Insert the registered commands into the given yargs parser
|
|
*
|
|
* Traverses the the tree of sub-commands recursively and adds all of
|
|
* them to the given yargs parser.
|
|
*
|
|
* @param {Object} yargs - yargs parser to register with.
|
|
*/
|
|
register(yargs) {
|
|
yargs.command([this.name].concat(this.alias), this.description, (yargs) => {
|
|
for (let [name, command] of this.subCommands) {
|
|
// Check if the entry is not an alias.
|
|
if (name === command.name) {
|
|
command.register(yargs);
|
|
}
|
|
}
|
|
}, () => {
|
|
yargs.showHelp();
|
|
yargs.exit();
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Resolve a string into a host ID
|
|
*
|
|
* Resolves a string with either an host name or an id into an integer with
|
|
* the host ID.
|
|
*
|
|
* @param {module:lib/link.Link} client - link to controller to query host on.
|
|
* @param {string} hostName - string with name or id of host.
|
|
* @returns {Promise<number>} host ID.
|
|
* @static
|
|
*/
|
|
async function resolveHost(client, hostName) {
|
|
let hostId;
|
|
if (/^-?\d+$/.test(hostName)) {
|
|
hostId = parseInt(hostName, 10);
|
|
} else {
|
|
let hosts = await client.sendTo(new libData.HostListRequest(), "controller");
|
|
for (let host of hosts) {
|
|
if (host.name === hostName) {
|
|
hostId = host.id;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hostId === undefined) {
|
|
throw new libErrors.CommandError(`No host named ${hostName}`);
|
|
}
|
|
}
|
|
|
|
return hostId;
|
|
}
|
|
|
|
/**
|
|
* Resolve a string to an instance ID
|
|
*
|
|
* Resolves a string with either an instance name or an id into an integer
|
|
* with the instance ID.
|
|
*
|
|
* @param {module:lib/link.Link} client - link to controller to query instance on.
|
|
* @param {string} instanceName - string with name or id of instance.
|
|
* @returns {Promise<number>} instance ID.
|
|
* @static
|
|
*/
|
|
async function resolveInstance(client, instanceName) {
|
|
let instanceId;
|
|
if (/^-?\d+$/.test(instanceName)) {
|
|
instanceId = parseInt(instanceName, 10);
|
|
} else {
|
|
let instances = await client.sendTo(new libData.InstanceDetailsListRequest(), "controller");
|
|
for (let instance of instances) {
|
|
if (instance.name === instanceName) {
|
|
instanceId = instance.id;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (instanceId === undefined) {
|
|
throw new libErrors.CommandError(`No instance named ${instanceName}`);
|
|
}
|
|
}
|
|
|
|
return instanceId;
|
|
}
|
|
|
|
/**
|
|
* Resolve a string to a mod pack ID
|
|
*
|
|
* Resolevs a string with either a mod pack name or an id into an integer
|
|
* with the mod pack ID.
|
|
*
|
|
* @param {module:lib/link.Link} client -
|
|
* link to controller to query mod pack on.
|
|
* @param {string} modPackName - string with name or id of mod pack.
|
|
* @returns {Promise<number>} mod pack ID.
|
|
* @static
|
|
*/
|
|
async function resolveModPack(client, modPackName) {
|
|
let modPackId;
|
|
if (/^-?\d+/.test(modPackName)) {
|
|
modPackId = parseInt(modPackName, 10);
|
|
} else {
|
|
let modPacks = await client.sendTo(new libData.ModPackListRequest(), "controller");
|
|
for (let modPack of modPacks) {
|
|
if (modPack.name === modPackName) {
|
|
modPackId = modPack.id;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (modPackId === undefined) {
|
|
throw new libErrors.CommandError(`No mod pack named ${modPackName}`);
|
|
}
|
|
}
|
|
|
|
return modPackId;
|
|
}
|
|
|
|
/**
|
|
* Retrieve role object from string
|
|
*
|
|
* Resolves a string with either a role name or an id into an object
|
|
* representing the role.
|
|
*
|
|
* @param {module:lib/link.Link} client - link to controller to query role on.
|
|
* @param {string} roleName - string with name or id of role.
|
|
* @returns {Promise<object>} Role info.
|
|
* @static
|
|
*/
|
|
async function retrieveRole(client, roleName) {
|
|
let roles = await client.sendTo(new libData.RoleListRequest(), "controller");
|
|
|
|
let resolvedRole;
|
|
if (/^-?\d+$/.test(roleName)) {
|
|
let roleId = parseInt(roleName, 10);
|
|
for (let role of roles) {
|
|
if (role.id === roleId) {
|
|
resolvedRole = role;
|
|
break;
|
|
}
|
|
}
|
|
|
|
} else {
|
|
for (let role of roles) {
|
|
if (role.name === roleName) {
|
|
resolvedRole = role;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!resolvedRole) {
|
|
throw new libErrors.CommandError(`No role named ${roleName}`);
|
|
}
|
|
|
|
return resolvedRole;
|
|
}
|
|
|
|
module.exports = {
|
|
Command,
|
|
CommandTree,
|
|
|
|
resolveHost,
|
|
resolveInstance,
|
|
resolveModPack,
|
|
retrieveRole,
|
|
};
|