diff --git a/docs/writing-plugins.md b/docs/writing-plugins.md index e948238e..b66dfe64 100644 --- a/docs/writing-plugins.md +++ b/docs/writing-plugins.md @@ -48,18 +48,20 @@ 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 +```js +const libLink = require("@clusterio/lib/link"); // For messages - module.exports = { - name: "foo_frobber", - title: "Foo Frobber", - description: "Does advanced frobnication", - instanceEntrypoint: "instance", - masterEntrypoint: "master", - messages: { - /* See below */ - }, - }; +module.exports = { + name: "foo_frobber", + title: "Foo Frobber", + description: "Does advanced frobnication", + instanceEntrypoint: "instance", + masterEntrypoint: "master", + messages: { + /* See below */ + }, +}; +``` The following properties are recognized: @@ -117,20 +119,22 @@ For both instanceEntrypoint and masterEntrypoint the path should not end with .j 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"); +```js +const libPlugin = require("@clusterio/lib/plugin"); - class MasterPlugin extends libPlugin.BaseMasterPlugin { - async init() { - this.foo = 42; - await this.startFrobnication(); - } - - // ... +class MasterPlugin extends libPlugin.BaseMasterPlugin { + async init() { + this.foo = 42; + await this.startFrobnication(); } - module.exports = { - MasterPlugin, - } + // ... +} + +module.exports = { + MasterPlugin, +} +``` For the instance plugin it's exactly the same except "Master" is replaced with "Instance", and for the clusterioctl plugin "Control" is used. The available hooks that you can override are documented in the base class [in lib/plugin.js](/packages/lib/plugin.js). @@ -138,11 +142,13 @@ The available hooks that you can override are documented in the base class [in l It's best to avoid defining a constructor, but if you insist on defining one, forward all arguments to the base class. E.g.: - constructor(...args) { - super(...args); +```js +constructor(...args) { + super(...args); - // Code here - } + // Code here +} +``` The arguments passed may change, and attempting to modify them will result in unpredicatable behaviour. The async init method is always called immediatly after the constructor, so there's little reason to do this. @@ -154,9 +160,11 @@ The base plugin classes provide a winston logger for logging messages to the sha For instances a copy of the log is also stored on the slave the instance is on. To use it, pass a string to one of the log levels functions, for example: - async init() { - this.logger.info("Initializing frobbing"); - } +```js +async init() { + this.logger.info("Initializing frobbing"); +} +``` Metadata covering which plugin and instance (for instance plugin classes) the log event happened on will automatically be attached to the log message. @@ -180,11 +188,13 @@ These levels have roughly the following meanings: For guarding unexpected errors the best option is to log a short description along with the`stack` property of the Error: - try { - // Operation that should not throw but may end up throwing - } catch (err) { - this.logger.error(`Operation failed:\n${err.stack}`); - ) +```js +try { + // Operation that should not throw but may end up throwing +} catch (err) { + this.logger.error(`Operation failed:\n${err.stack}`); +) +``` For plugin hooks expections thrown are automatically catched and logged, but for event handlers registered on EventEmitters it's critical that exceptions are catched and handled appropriately. @@ -195,30 +205,34 @@ Clusterio provides a configuration system that handles storing, distributing, ed You can take advantage of it by subclassing `PluginConfigGroup`, setting `defaultAccess` to where config entries can be accessed from, setting the `groupName` to your plugin name, defining fields on it, finalizing it, and passing it as either `MasterConfigGroup` or `InstanceConfigGroup` in the `info.js` export. For example in info.js: - const libConfig = require("@clusterio/lib/config"); +```js +const libConfig = require("@clusterio/lib/config"); - class MasterConfigGroup extends libConfig.PluginConfigGroup { } - MasterConfigGroup.defaultAccess = ["master", "slave", "control"]; - MasterConfigGroup.groupName = "foo_frobber"; - MasterConfigGroup.define({ - name: "level", - description: "Level of frobnication done", - type: "number", - initial_value: 2, - }); - MasterConfigGroup.finalize(); +class MasterConfigGroup extends libConfig.PluginConfigGroup { } +MasterConfigGroup.defaultAccess = ["master", "slave", "control"]; +MasterConfigGroup.groupName = "foo_frobber"; +MasterConfigGroup.define({ + name: "level", + description: "Level of frobnication done", + type: "number", + initial_value: 2, +}); +MasterConfigGroup.finalize(); - module.exports = { - ... - MasterConfigGroup: MasterConfigGroup, - }; +module.exports = { + ... + MasterConfigGroup: MasterConfigGroup, +}; +``` Code inside the `MasterPlugin` class will then be able to access the level config field through the `Config` object at `this.master.config`, for example in the MasterPluginClass: - async init() { - let level = this.master.config.get("foo_frobber.level"); - this.logger.info(`I got a frobnication level of ${level}`); - } +```js +async init() { + let level = this.master.config.get("foo_frobber.level"); + this.logger.info(`I got a frobnication level of ${level}`); +} +``` The same applies for instance configs, replace "master" with "instance" where appropriate. See [Configuration System](config-system.md) for more details on how this system works. @@ -230,11 +244,13 @@ If the plugin requires a certain feature to be enabled to function it should thr The most common such feature is the save patching, which can be disabled to run vanilla or scenarios not compatible with Clusterio. For example: - async init() { - if (!this.instance.config.get("factorio.enable_save_patching")) { - throw new Error("foo_frobber plugin requires save patching."); - } +```js +async init() { + if (!this.instance.config.get("factorio.enable_save_patching")) { + throw new Error("foo_frobber plugin requires save patching."); } +} +``` ## Communicating with Factorio @@ -243,14 +259,15 @@ For pushing data into Factorio there's RCON, which lets you send arbitrary Lua c This is done by calling the `sendRcon` method on the plugin object. For example: - async onStart() { - let response = await this.sendRcon( - "/sc rcon.print('data')" - ); - - // Do stuff with response. - } +```js +async onStart() { + let response = await this.sendRcon( + "/sc rcon.print('data')" + ); + // Do stuff with response. +} +``` Because data into Factorio is streamed at a rate of 3-6 kB/s by default, it is recommended to avoid sending large commands as much as possible, and to strip down the data on the ones you send to only what's strictly necessary. You can have lua code injected into the game via the module system and call that from RCON to avoid having to send code through the commands. @@ -261,24 +278,28 @@ The `send_json` API allows sending JSON payloads on channels that plugins can li From a 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.handleFoo(content).catch(err => this.logger.error( - `Error handling foo:\n${err.stack}` - )) - ); - } +```js +async init() { + this.instance.server.on("ipc-my_plugin_foo", content => + this.handleFoo(content).catch(err => this.logger.error( + `Error handling foo:\n${err.stack}` + )) + ); +} - async handleFoo(content) { - // Do stuff with content - } +async handleFoo(content) { + // Do stuff with content +} +``` And then in the module for the plugin: - local clusterio_api = require("modules/clusterio/api") +```lua +local clusterio_api = require("modules/clusterio/api") - -- inside some event handler - clusterio_api.send_json("my_plugin_foo", { data = 123 }) +-- inside some event handler +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 channels. It's also important to catch any errors that might occur as they will otherwise be propogated to the instance code and kill the server. @@ -310,18 +331,20 @@ Events are defined as properties of the messages object exported by `info.js` th The name of the property correspond to the handler invoked on the plugin class. The Event constructor takes an object of properties that define 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", - eventRequired: ["frobnication_type"], - eventProperties: { - "frobnication_type": { type: "string" }, - "urgent": { type: "boolean" }, - }, - }), - }, +```js +messages: { + startFrobnication: new libLink.Event({ + type: "foo_frobber:start_frobnication", + links: ["master-slave", "slave-instance"], + forwardTo: "instance", + eventRequired: ["frobnication_type"], + eventProperties: { + "frobnication_type": { type: "string" }, + "urgent": { type: "boolean" }, + }, + }), +}, +``` This specifies an event that can be sent from the master to a slave, and from a slave to an instance. It also specifies that the event must contain the property `frobnication_type`, with a string value in the data payload and that it may optionally contain a boolean `urgent` property. @@ -383,24 +406,26 @@ The name of the property corresponds to the handler invoked on the plugin class. The Request constructor takes an object of properties that define 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", - requestRequired: ["verbosity"], - requestProperties: { - "verbosity": { type: "integer" }, - "special": { type: "boolean" }, +```js +messages: { + reportFrobnication: new libLink.Request({ + type: "foo_frobber:report_frobnication", + links: ["master-slave", "slave-instance"], + forwardTo: "instance", + requestRequired: ["verbosity"], + requestProperties: { + "verbosity": { type: "integer" }, + "special": { type: "boolean" }, + }, + responseProperties: { + "report": { + type: "array", + items: { type: "string" }, }, - responseProperties: { - "report": { - type: "array", - items: { type: "string" }, - }, - }, - }), - }, + }, + }), +}, +``` 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 it may also contain a boolean `special` property. @@ -459,10 +484,12 @@ For `InstancePlugin` code the link to the slave is the `instance` itself, which The `.info` property of the plugin class exposes the data exported from the plugin's `info.js` module. In other words: - // In an InstancePlugin class - async frobnicate() { - this.info.messages.exampleEvent.send(this.instance, { foo: "bar" }); - } +```js +// In an InstancePlugin class +async frobnicate() { + this.info.messages.exampleEvent.send(this.instance, { foo: "bar" }); +} +``` For the Request class the send method is async and returns the response data received from the target it was sent to, or throws an error if the request failed. @@ -478,9 +505,11 @@ Plugins must respond to the prepare disconnect by stopping any processess it doe This can be accomplished either through listening for the prepare disconnect hook, or by checking the `connected` or `closing` property of the connector for the master/slave connection. For example the sending of an event from an `InstancePlugin` class can be stopped while the connection is closing by using the following code: - if (!this.slave.connector.closing) { - this.info.messages.frobnicate.send(this.instance, { foo: "bar" }); - } +```js +if (!this.slave.connector.closing) { + this.info.messages.frobnicate.send(this.instance, { foo: "bar" }); +} +``` If the event or request needs to be sent to the master it can be put into a queue stored on the plugin instance and sent out when the connection is established again. The re-establishement of the connection is notified to plugins via the `connect` event to the `onMasterConnectionEvent` and `onSlaveConnectionEvent` hooks. @@ -500,29 +529,33 @@ Clusterio comes with its own Prometheus client implementation, one part due to N 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"); +```js +const { Counter } = require("@clusterio/lib/prometheus"); - const fooMetric = new Counter( - "clusterio_foo_frobber_foo_metric", "Measures the level of foo", - ); +const fooMetric = new Counter( + "clusterio_foo_frobber_foo_metric", "Measures the level of foo", +); - // Somewhere in the master plugin code - fooMetric.inc(); +// Somewhere in the master plugin code +fooMetric.inc(); +``` This works for master plugins, and the metric will be automatically available through the /metric HTTP endpoint. It's recommended that plugin metrics follow `clusterio__` as the 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"); +```js +const { Counter } = require("@clusterio/lib/prometheus"); - const barMetric = new Gauge( - "clusterio_foo_frobber_bar_metric", "Bar instance level", - { labels: ["instance_id"] } - ); +const barMetric = new Gauge( + "clusterio_foo_frobber_bar_metric", "Bar instance level", + { labels: ["instance_id"] } +); - // Somewhere in the instance plugin code - barMetric.labels(String(this.instance.id)).set(someValue); +// Somewhere in the instance plugin code +barMetric.labels(String(this.instance.id)).set(someValue); +``` Metrics are automatically registered to the default registry, and this default registry is automatically polled by the master server on slaves. This means that it's important that you place the definition of the metric at module level so that it's not created more than once over the lifetime of a slave. @@ -536,28 +569,32 @@ For statistics you need to update on collection there's an `onMetrics` hook on b The control entrypoint for plugins allows you to extend clustectl with your own commands. The creation of custom commands typically starts with defining a command tree for the plugin: - const { Command, CommandTree } = require("@clusterio/lib/command"); - const fooFrobberCommands = new CommandTree({ - name: "foo-frobber", description: "Foo Frobber Plugin commands" - }); +```js +const { Command, CommandTree } = require("@clusterio/lib/command"); +const fooFrobberCommands = new CommandTree({ + name: "foo-frobber", description: "Foo Frobber Plugin commands" +}); +``` Then commands are added to the the plugin's command tree: - const info = require("./info"); +```js +const info = require("./info"); - fooFrobberCommands.add(new Command({ - definition: ["frobnicate ", "Do frobnications", (yargs) => { - yargs.positional("level", { - describe: "type of frobnication", type: "string" - }); - }], - handler: async function(args, control) { - await info.messages.frobnicate.send(control, { - instance_name: "Console", - content: args.message, - }); - }, - })); +fooFrobberCommands.add(new Command({ + definition: ["frobnicate ", "Do frobnications", (yargs) => { + yargs.positional("level", { + describe: "type of frobnication", type: "string" + }); + }], + handler: async function(args, control) { + await info.messages.frobnicate.send(control, { + instance_name: "Console", + content: args.message, + }); + }, +})); +``` For a command the `definition` is the arguments to pass to [yargs.command](http://yargs.js.org/docs/#api-reference-commandcmd-desc-builder-handler) (see also [yargs.positional](http://yargs.js.org/docs/#api-reference-positionalkey-opt) and [yargs.options](http://yargs.js.org/docs/#api-reference-optionskey-opt) for setting up positional and optional arguments to commands). The `handler` is an async function that's invoked when the command is executed and it's passed the parsed command line arguments and a reference to the `Control` class of clusterioctl. @@ -567,14 +604,16 @@ Note that messages sent from clusterioctl needs to have `"control-master"` as a To have the command tree become part of clusterioctl it needs to be added to the rootCommand tree in `addCommands` callback of the Control plugin: - const libPlugin = require("@clusterio/lib/plugin"); +```js +const libPlugin = require("@clusterio/lib/plugin"); - class ControlPlugin extends libPlugin.BaseControlPlugin { - async addCommands(rootCommand) { - rootCommand.add(fooFrobberCommands); - } +class ControlPlugin extends libPlugin.BaseControlPlugin { + async addCommands(rootCommand) { + rootCommand.add(fooFrobberCommands); } +} - module.exports = { - ControlPlugin, - } +module.exports = { + ControlPlugin, +} +``` \ No newline at end of file