Reimplement plugin system

Implement a new plugin system based subclassing pre-defined base clases
and communicating with pre-defined link messages.
This commit is contained in:
Hornwitser committed 2019-12-14 17:48:56 +01:00
1 parent 513651393b
commit bcceae687f
10 files changed
+688 -34

No files matched your search

+8
View File
@@ -19,6 +19,9 @@ Version 2.0.0
- Rewritten the communication between slaves and the master from scratch. The
new system is based on a WebSocket connection between the slaves and the
master server and provides efficient validated bi-directonal communication.
- Rewritten the plugin system from scratch. Plugins now inherit from
a base class and use the same WebSocket connection Clusterio uses to
communicate.
### Changes
@@ -100,6 +103,11 @@ Version 2.0.0
- Changed the format of database/slaves.json.
- Removed the output file subscription system. Breaks inventoryImports,
playerManager, trainTeleports, serverSelect and researchSync.
- Removed the factorioOutput hook from instance plugins. The onOutput
hook provides parsed output instead.
- Removed the onLoadFinish hook from master plugins.
- Moved plugins from the sharedPlugins directory to plugins directory.
- Implemented a new plugin system that replaces the old. Breaks all plugins.
Version 1.2.4
+51 -7
View File
@@ -8,6 +8,7 @@ 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");
@@ -20,6 +21,8 @@ class Instance extends link.Link{
link.attachAllMessages(this);
this._dir = dir;
this.plugins = new Map();
// This is expected to change with the config system rewrite
this.config = {
id: instanceConfig.id,
@@ -41,11 +44,34 @@ class Instance extends link.Link{
this.server.on('output', (output) => {
link.messages.instanceOutput.send(this, { instance_id: this.config.id, output })
for (let [name, plugin] of this.plugins) {
plugin.onOutput(output).catch(err => {
console.error(`Plugin ${name} raised error in onOutput:`, err);
});
}
});
}
async init() {
async init(pluginInfos) {
await this.server.init();
// load plugins
for (let pluginInfo of pluginInfos) {
if (!pluginInfo.enabled || !pluginInfo.instanceEntrypoint) {
continue;
}
// require plugin class and initialize it
let pluginLoadStarted = Date.now();
let { InstancePlugin } = require(`./plugins/${pluginInfo.name}/${pluginInfo.instanceEntrypoint}`);
let instancePlugin = new InstancePlugin(pluginInfo, this);
await instancePlugin.init();
this.plugins.set(pluginInfo.name, instancePlugin);
plugin.attachPluginMessages(this, pluginInfo, instancePlugin);
console.log(`Clusterio | Loaded plugin ${pluginInfo.name} in ${Date.now() - pluginLoadStarted}ms`);
}
}
/**
@@ -133,8 +159,8 @@ class Instance extends link.Link{
// plugins that are not disabled. This will most likely change in the
// future when the plugin refactor is done.
let modules = [];
for (let pluginName of await fs.readdir("sharedPlugins")) {
let pluginDir = path.join("sharedPlugins", pluginName);
for (let pluginName of await fs.readdir("plugins")) {
let pluginDir = path.join("plugins", pluginName);
if (await fs.pathExists(path.join(pluginDir, "DISABLED"))) {
continue;
}
@@ -177,6 +203,10 @@ class Instance extends link.Link{
});
await this.server.start(saveName);
for (let pluginInstance of this.plugins.values()) {
await pluginInstance.onStart();
}
}
/**
@@ -185,6 +215,10 @@ class Instance extends link.Link{
async stop() {
// XXX this needs more thought to it
if (this.server._state === "running") {
for (let pluginInstance of this.plugins.values()) {
await pluginInstance.onStop();
}
await this.server.stop();
}
}
@@ -295,6 +329,10 @@ class InstanceConnection extends link.Link {
super('slave', 'instance', connector);
this.slave = slave;
link.attachAllMessages(this);
for (let pluginInfo of slave.pluginInfos) {
plugin.attachPluginMessages(this, pluginInfo, null);
}
}
async forwardEventToInstance(message, event) {
@@ -355,9 +393,15 @@ class SlaveConnector extends link.SocketIOClientConnector {
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) {
constructor(connector, slaveConfig, pluginInfos) {
super('slave', 'master', connector);
link.attachAllMessages(this);
this.pluginInfos = pluginInfos;
for (let pluginInfo of pluginInfos) {
plugin.attachPluginMessages(this, pluginInfo, null);
}
this.config = {
id: slaveConfig.id,
name: slaveConfig.name,
@@ -449,7 +493,7 @@ class Slave extends link.Link {
let instance = new Instance(
connectionClient, instanceInfo.path, this.config.factorioDir, instanceInfo.config
);
await instance.init();
await instance.init(this.pluginInfos);
// XXX: race condition on multiple simultanious calls
this.instanceConnections.set(instanceId, instanceConnection);
@@ -709,7 +753,6 @@ async function startClient() {
const slaveConfig = JSON.parse(await fs.readFile(args.config));
await fs.ensureDir(slaveConfig.instanceDirectory);
await fs.ensureDir("sharedPlugins");
await fs.ensureDir("sharedMods");
// Set the process title, shows up as the title of the CMD window on windows
@@ -737,8 +780,9 @@ async function startClient() {
return;
}
let pluginInfos = await plugin.getPluginInfos("plugins");
let slaveConnector = new SlaveConnector(slaveConfig);
let slave = new Slave(slaveConnector, slaveConfig);
let slave = new Slave(slaveConnector, slaveConfig, pluginInfos);
// Handle interrupts
let secondSigint = false
+303
View File
@@ -0,0 +1,303 @@
Writing Plugins
===============
Plugins for Clusterio are classes written in JavaScript that run under
Node.js. The plugin classes have pre-defined hooks that are called
during various stages and operations of Clusterio.
Plugin Structure
----------------
The basic file structure of a plugin is the following.
plugin_name/
+- info.js
+- package.json
+- master.js
+- instance.js
+- test/
| +- plugin.js
+- lua/
+- plugin.lua
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 link = require('lib/link'); // For messages
module.exports = {
name: "foo_frobber",
title: "Foo Frobber",
description: "Does advance frobnication",
version: "0.8.1",
instanceEntrypoint: "instance",
masterEntrypoint: "master",
messages: {
/* See below */
},
};
The following properties are recognized:
**name**:
Internal name of the plugin. Must be the same as the plugin's
directory name
**title**:
Name of the plugin as shown to users. Currently not used.
**description**:
Brief description of what the plugin does. Currently not used.
**version**:
Semver compatible version string of the plugin. Currently not used.
**instanceEntrypoint**:
Path to a Node.js module relative to the plugin directory which
contains the InstancePlugin class definition for this plugin. This
is an optional paramater. A plugin can be for instances only but
for it to be able to send any messages to other instances it will
still have to be loaded on the master server.
**masterEntrypoint**:
Path to a Node.js module relative to the plugin directory which
contains the MasterPlugin class definiton for this plugin. This is
an optional parameter. A plugin can be made that only runs on the
master server.
**messages**:
Object with link messages definitions for this plugin. See guide
for [defining link messages](#defining-link-messages) below.
While there is no standard for how to organize a plugin it's recommended
to put the MasterPlugin class definition into master.js and the
InstancePlugin class definition into instance.js. You can put them into
whatever file you want (even the same one for both)
For both instanceEntrypoint and masterEntrypoint the path should not end
with .js and it should use forward slashes for directory sepparators if
any.
Defining the plugin class
-------------------------
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 plugin = require('lib/plugin');
class MasterPlugin extends plugin.BaseMasterPlugin {
async init() {
this.foo = 42;
await this.startFrobnication();
}
// ...
}
module.exports = {
MasterPlugin,
}
For the instance plugin it's exactly the same except "Master" is
replaced with "Instance". The available hooks that you can override is
documented on the base class [in lib/plugin.js](../lib/plugin.js).
It's best to avoid defining a constructor, but if you insist on defining
one forward all arguments to be the base class. E.g.:
constructor(...args) {
super(...args);
// Code here
}
The arguments passed may change, and attempting to modify them will
result in unpredicatable behaviour. The async init method always called
immediatly after the constructor so there's little reason to do this.
Defining Link Messages
----------------------
You will most likely have to communicate with the master or other
instances in your plugin for it to do anything useful. For this there's
a WebSocket communication channel established between the slaves and the
master server that plugins can define their own messages to send over
it. This channel is bi-directional and all messages send over it are
validated with a JSON schema (see [this guide][guide] for an
introduction to writing JSON schema.)
[guide]: https://json-schema.org/learn/getting-started-step-by-step.html
There are currently two kinds of messages that can be defined. Events
and requests. Events are simple one way notifications that invoke a
handler on 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 only that both parties of a link may innitiate one.
### Defining Events
Events are defined as properties of the messages object exported by
`info.js` that map to instances of the `Event` class from `lib/link`.
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 link.Event({
type: 'foo_frobber:start_frobnication',
links: ['master-slave', 'slave-instance'],
forwardTo: 'instance',
eventProperties: {
"frobnication_type": { type: "string" },
},
}),
},
This specifies an event that can be sent from the master to a slave,
and from a slave to an instance. And that the event must contain the
property `frobnication_type` with a string value in the data payload.
It will also be forwarded by slaves to a specific instance.
The following properties are recognized by the Event constructor:
**type**:
The message type sent over the wire. This can be any string but
it must be unique across all plugins and it's recommended that it is
of format `plugin_name:message_name`. The suffix `_event` will be
appened to the type.
**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 master to slave, but can't be sent back the other way
unless `'slave-master'` is also present in the links array.
**forwardTo**:
Target to forward event to. Can be either `'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.
**broadcastTo**:
Target to broadcast this message towards. Currently only
`'instance'` is supported and means the event will be broadcast to
all instances downstream of the target it's sent to, but not back
from where it came from. This means that sending an event to a
slave from an instance will cause it to be broadcast to all
instances of that slave except for the instance it came from.
**eventProperties**:
Object with properties mapping to a JSON schema of that property
that specifies what's valid to send in the event. This is
equivalent to using the `properties` keyword in JSON schema except
that the properties specified are implicitly required and additional
properties 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
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 and master-slave, slave-instance as the links.
Keep in mind when forwarding events that if the target an event is being
forwarded to is not online the event will be dropped. If you need a
confirmation that the message was received use a request.
### Definining Requests
Requests are defined as properties of the messages object exported by
`info.js` that map to instances of the `Request` class from `lib/link`.
The name of the property correspond 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 link.Request({
type: 'foo_frobber:report_frobnication',
links: ['master-slave', 'slave-instance'],
forwardTo: 'instance',
requestProperties: {
"verbosity": { type: "integer" },
},
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 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`.
The following properties are recognized by the Request constructor:
**type**:
The message type sent over the wire. This can be any string but
it must be unique across all plugins and it's recommended that it is
of format `plugin_name:message_name`. The suffix `_request` will be
appened to the type for the request message sent, and the suffix
`_response` will be appended to the type for the 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 from the master to slave and the slave can reply to the master,
but the slave can't sent a request to the master unless
`'slave-master'` is also present in the links array.
**forwardTo**:
Target to forward request to. Can be either `'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 instances specified by the `instance_id` event property.
This works by using a default handler for the event at the links
that forward it.
**requestProperties**:
Object with properties mapping to a JSON schema of that property
that specifies what's valid to send in the request. This is
equivalent to using the `properties` keyword in JSON schema except
that the properties specified are implicitly required and additional
properties are not allowed. See [this guide][guide] for an
introduction to writing JSON schemas
**responseProperties**:
Same as the requestProperties only for the response sent back by the
target.
Sending Link Messages
---------------------
Link messages are sent by calling the `.send()` method on the
Event/Request instance with the link you want to send it over and the
data to send. For instance plugins the link is the instance itself
which is accessible through the `.instance` property on the plugin. 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" });
}
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.
+10
View File
@@ -15,10 +15,20 @@ class StartupError extends Error { }
// Errors outside of our control.
class EnvironmentError extends Error { }
// Errors caused by plugins
class PluginError extends Error {
constructor(pluginName, original) {
super(`PluginError: ${original.message}`);
this.pluginName = pluginName;
this.original = original;
}
}
module.exports = {
CommandError,
RequestError,
InvalidMessage,
StartupError,
EnvironmentError,
PluginError,
}
+126
View File
@@ -0,0 +1,126 @@
"use strict";
const fs = require("fs-extra");
const path = require("path");
const errors = require("lib/errors");
/**
* Base class for instance plugins
*/
class BaseInstancePlugin {
constructor(info, instance) {
this.info = info;
this.instance = instance;
}
/**
* Called immediately after the class is instantiated
*/
async init() { }
/**
* Called after the Factorio server is started
*/
async onStart() { }
/**
* Called before the Factorio server is stopped
*
* This will not be called if for example the Factorio server crashes or
* is killed.
*/
async onStop() { }
/**
* Called when the instance is shutting down
*/
async onExit() { }
/**
* Called when the Factorio outputs a line
*
* 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):
*
* - 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.
*/
async onOutput(output) { }
}
class BaseMasterPlugin {
constructor(info) {
this.info = info;
}
/**
* Called immediately after the class is instantiated
*/
async init() { }
/**
* Called when the master server is shutting down
*/
async onExit() { }
}
async function getPluginInfos(baseDir) {
let plugins = [];
for (let pluginDir of await fs.readdir(baseDir)) {
let pluginInfo;
try {
// Note: Require path is relative to this module
pluginInfo = require(path.posix.join('..', baseDir, pluginDir, 'info'));
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
continue;
} else {
throw new errors.PluginError(pluginDir, err);
}
}
if (pluginInfo.name !== pluginDir) {
throw new errors.EnvironmentError(
`Plugin dir ${baseDir}/${pluginDir} does not match the name of the plugin (${pluginInfo.name})`
);
}
pluginInfo.enabled = !await fs.exists(path.join(baseDir, pluginDir, "DISABLED"));
plugins.push(pluginInfo);
}
return plugins;
}
function attachPluginMessages(link, pluginInfo, plugin) {
let messageDefinitions = pluginInfo.messages || [];
for (let [name, messageFormat] of Object.entries(messageDefinitions)) {
let handler = name + messageFormat.constructor.name + 'Handler';
if (plugin === null || !plugin[handler]) {
messageFormat.attach(link);
} else {
messageFormat.attach(link, async function(message, format) {
return await plugin[handler](message, format, this);
});
}
}
}
module.exports = {
BaseInstancePlugin,
BaseMasterPlugin,
getPluginInfos,
attachPluginMessages,
};
+49 -14
View File
@@ -37,6 +37,7 @@ const factorio = require("lib/factorio");
const schema = require("lib/schema");
const link = require("lib/link");
const errors = require("lib/errors");
const plugin = require("lib/plugin");
// homemade express middleware for token auth
const authenticate = require("lib/authenticate");
@@ -69,7 +70,6 @@ app.set('views', ['views', 'sharedPlugins']);
app.use(function(req, res, next){
res.locals.res = res;
res.locals.req = req;
res.locals.masterPlugins = masterPlugins;
res.locals.slaves = db.slaves;
res.locals.moment = moment;
next();
@@ -142,10 +142,8 @@ function registerMoreMetrics(){
}
// plugins
for (let plugin of masterPlugins) {
if (plugin.main && plugin.main.onMetrics && typeof plugin.main.onMetrics == "function") {
plugin.main.onMetrics();
}
for (let masterPlugin of masterPlugins.values()) {
masterPlugin.onMetrics();
}
// Slave count
@@ -186,13 +184,10 @@ async function shutdown() {
await saveMap(config.databaseDirectory, "slaves.json", db.slaves);
await saveMap(config.databaseDirectory, "instances.json", db.instances);
for(let i in masterPlugins){
let plugin = masterPlugins[i];
if(plugin.main && plugin.main.onExit && typeof plugin.main.onExit == "function"){
let startTime = Date.now();
await plugin.main.onExit();
console.log("Plugin "+plugin.pluginConfig.name+" exited in "+(Date.now()-startTime)+"ms");
}
for (let [pluginName, masterPlugin] of masterPlugins) {
let startTime = Date.now();
await masterPlugin.onExit();
console.log(`Plugin ${pluginName} exited in ${Date.now() - startTime}ms`);
}
console.log("Clusterio cleanly exited in "+(Date.now()-exitStartTime)+"ms");
process.exit(0);
@@ -252,6 +247,9 @@ class BaseConnection extends link.Link {
constructor(target, connector) {
super('master', target, connector);
link.attachAllMessages(this);
for (let masterPlugin of masterPlugins.values()) {
plugin.attachPluginMessages(this, masterPlugin.info, masterPlugin);
}
}
async forwardRequestToInstance(message, request) {
@@ -518,13 +516,43 @@ io.on('connection', function (socket) {
});
// handle plugins on the master
var masterPlugins = [];
var masterPlugins = new Map();
async function pluginManagement(){
let startPluginLoad = Date.now();
// masterPlugins = await getPlugins();
masterPlugins = await loadPlugins();
console.log("All plugins loaded in "+(Date.now() - startPluginLoad)+"ms");
}
async function loadPlugins() {
let plugins = new Map();
for (pluginInfo of await plugin.getPluginInfos("plugins")) {
if (!pluginInfo.enabled) {
continue;
}
let pluginLoadStarted = Date.now();
let MasterPlugin = plugin.BaseMasterPlugin;
if (pluginInfo.masterEntrypoint) {
({ MasterPlugin } = require(`./plugins/${pluginInfo.name}/${pluginInfo.masterEntrypoint}`));
}
let masterPlugin = new MasterPlugin(pluginInfo);
await masterPlugin.init();
plugins.set(pluginInfo.name, masterPlugin);
console.log(`Clusterio | Loaded plugin ${pluginInfo.name} in ${Date.now() - pluginLoadStarted}ms`);
/*
main:new masterPlugin({
TODO?
config, socketio: io, express: app,
db, Prometheus, prometheusPrefix, endpointHitCounter,
}),
pluginConfig,
});*/
}
return plugins;
}
/**
* Calls listen on server capturing any errors that occurs
* binding to the port.
@@ -666,6 +694,13 @@ I version of clusterio. Expect things to break. I
| Unable to to start master server |
+----------------------------------+`
);
} else if (err instanceof errors.PluginError) {
console.error(`
Error: ${err.pluginName} plugin threw an unexpected error
during startup, please report it to the plugin author.
--------------------------------------------------------------`
);
err = err.original;
} else {
console.error(`
+---------------------------------------------------------------+
+2 -2
View File
@@ -5,7 +5,7 @@
"repository": "https://github.com/clusterio/factorioClusterio",
"main": "master.js",
"scripts": {
"test": "mocha --exit --check-leaks -R spec --recursive test sharedPlugins/*/test",
"test": "mocha --exit --check-leaks -R spec --recursive test plugins/*/test",
"fast-test": "FAST_TEST=y npm test",
"cover": "nyc npm test",
"fast-cover": "FAST_TEST=y nyc npm test",
@@ -17,7 +17,7 @@
"nyc": {
"exclude": [
"test/**",
"sharedPlugins/*/test/**"
"plugins/*/test/**"
]
},
"keywords": [
+13 -10
View File
@@ -8,17 +8,20 @@ const client = require("../client");
describe("Client testing", function() {
describe("class Instance", function() {
let instance = new client._Instance(new link.VirtualConnector(), "dir", "factorioDir", { name:"foo" });
it("should give the name on .name", function() {
assert.equal(instance.name, "foo");
})
describe(".name", function() {
it("should give the name of the instance", function() {
assert.equal(instance.name, "foo");
})
});
it("should give the path to it on .path()", function() {
assert.equal(instance.path(), "dir");
})
it("should join path on .path(...parts)", function() {
assert.equal(instance.path("bar"), path.join("dir", "bar"));
})
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"));
})
});
});
describe("checkFilename()", function() {
+2 -1
View File
@@ -3,6 +3,7 @@ const fs = require("fs-extra");
const path = require("path");
const link = require("lib/link");
const plugin = require("lib/plugin");
const master = require("../../master");
const client = require("../../client");
const clusterctl = require("../../clusterctl");
@@ -62,7 +63,7 @@ describe("Integration of Clusterio", function() {
masterURL: "http://invalid",
masterAuthToken: "invalid",
publicIP: "invalid",
});
}, await plugin.getPluginInfos("plugins"));
testSlaveConnection = new master._SlaveConnection({ agent: "test", version, name: "slave", id: 4}, slaveServer);
master._slaveConnections.set(4, testSlaveConnection);
});
+124
View File
@@ -0,0 +1,124 @@
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');
describe("lib/plugin", function() {
describe("class BaseInstancePlugin", function() {
let instancePlugin;
it("should be constructible", async function() {
instancePlugin = new plugin.BaseInstancePlugin();
await instancePlugin.init();
})
it("should define defaults for hooks", async function() {
await instancePlugin.onStart();
await instancePlugin.onStop();
await instancePlugin.onExit();
await instancePlugin.onOutput({});
})
});
describe("class BaseMasterPlugin", function() {
let masterPlugin;
it("should be constructible", async function() {
masterPlugin = new plugin.BaseMasterPlugin();
await masterPlugin.init();
})
it("should define defaults for hooks", async function() {
await masterPlugin.onExit();
})
});
describe("getPluginInfos()", function() {
let baseDir = path.join('test', 'temp', 'plugin');
let emptyDir = path.join(baseDir, 'emptyDir');
let emptyPlugin = path.join(baseDir, 'emptyPlugin');
let testPlugin = path.join(baseDir, 'testPlugin');
let disabledPlugin = path.join(baseDir, 'disabledPlugin');
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'));
async function writePlugin(pluginPath, name, infoName = name) {
await fs.outputFile(
path.join(pluginPath, name, 'info.js'),
`module.exports = { name: "${infoName}" };`
);
}
await writePlugin(testPlugin, 'test');
await writePlugin(disabledPlugin, 'disabled');
await fs.outputFile(path.join(disabledPlugin, 'disabled', 'DISABLED'), "");
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() {
assert.deepEqual(await plugin.getPluginInfos(emptyDir), []);
});
it("should ignore plugin dirs without info module", async function() {
assert.deepEqual(await plugin.getPluginInfos(emptyPlugin), []);
});
it("should discover test plugin", async function() {
assert.deepEqual(
await plugin.getPluginInfos(testPlugin),
[{ name: 'test', enabled: true }]
);
});
it("should discover disabled plugin", async function() {
assert.deepEqual(
await plugin.getPluginInfos(disabledPlugin),
[{ name: 'disabled', enabled: false }]
);
});
it("should discover disabled plugin", async function() {
assert.deepEqual(
await plugin.getPluginInfos(disabledPlugin),
[{ name: 'disabled', enabled: false }]
);
});
it("should reject on broken plugin", async function() {
await assert.rejects(
plugin.getPluginInfos(brokenPlugin),
{ message: "PluginError: Unexpected identifier" }
);
});
it("should reject on invalid plugin", async function() {
await assert.rejects(
plugin.getPluginInfos(invalidPlugin),
{ message: `Plugin dir ${invalidPlugin}/invalid does not match the name of the plugin (wrong)` }
);
});
after(async function() {
await fs.unlink(path.join(brokenPlugin, 'broken', 'info.js'));
});
});
describe("attachPluginMessages()", function() {
let mockLink = new link.Link('source', 'target', new mock.MockConnector());
let mockEvent = new link.Event({ type: '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() {};
plugin.attachPluginMessages(mockLink, { messages: { mockEvent }}, { mockEventEventHandler });
assert(mockLink._handlers.get('test_event'), "handler was not registered");
});
it("should throw if missing handler for the given message", function() {
assert.throws(
() => plugin.attachPluginMessages(mockLink, { messages: { mockEvent }}, {}),
new Error("Missing handler for test_event on source-target link")
);
});
});
});