Validate plugin entrypoint

Check that Master/Instance/Control plugins export the correct class and
that this class is a subclass of the correct base class.  This gives a
better user experience when plugins are incorrectly set up and loads a
duplicated clusterio/lib, and when accidentally using the wrong name
when developing plugins.
This commit is contained in:
Hornwitser committed 2023-02-15 20:31:02 +01:00
1 parent 6d7a2d46d6
commit 033de0e20a
2 files changed
+61 -6

No files matched your search

+20 -6
View File
@@ -6,6 +6,7 @@
"use strict";
const libErrors = require("./errors");
const libPlugin = require("./plugin");
const path = require("path");
@@ -48,6 +49,22 @@ async function loadPluginInfos(pluginList) {
return plugins;
}
function loadPluginClass(entrypointName, className, pluginClass, pluginInfo) {
let resolvedPath = path.posix.join(pluginInfo.requirePath, pluginInfo[entrypointName]);
let entrypoint = require(resolvedPath);
if (!entrypoint[className]) {
throw new libErrors.PluginError(pluginInfo.name,
new Error(`Expected ${resolvedPath} to export a class named ${className}`)
);
}
if (!(entrypoint[className].prototype instanceof pluginClass)) {
throw new libErrors.PluginError(pluginInfo.name,
new Error(`Expected ${className} exported from ${resolvedPath} to be a subclass of ${pluginClass.name}`)
);
}
return entrypoint[className];
}
/**
* Load master plugin class of a plugin
*
@@ -58,8 +75,7 @@ async function loadPluginInfos(pluginList) {
* @static
*/
async function loadMasterPluginClass(pluginInfo) {
let entrypoint = require(path.posix.join(pluginInfo.requirePath, pluginInfo.masterEntrypoint));
return entrypoint.MasterPlugin;
return loadPluginClass("masterEntrypoint", "MasterPlugin", libPlugin.BaseMasterPlugin, pluginInfo);
}
/**
@@ -72,8 +88,7 @@ async function loadMasterPluginClass(pluginInfo) {
* @static
*/
async function loadInstancePluginClass(pluginInfo) {
let entrypoint = require(path.posix.join(pluginInfo.requirePath, pluginInfo.instanceEntrypoint));
return entrypoint.InstancePlugin;
return loadPluginClass("instanceEntrypoint", "InstancePlugin", libPlugin.BaseInstancePlugin, pluginInfo);
}
/**
@@ -86,8 +101,7 @@ async function loadInstancePluginClass(pluginInfo) {
* @static
*/
async function loadControlPluginClass(pluginInfo) {
let entrypoint = require(path.posix.join(pluginInfo.requirePath, pluginInfo.controlEntrypoint));
return entrypoint.ControlPlugin;
return loadPluginClass("controlEntrypoint", "ControlPlugin", libPlugin.BaseControlPlugin, pluginInfo);
}
module.exports = {
+41
View File
@@ -64,4 +64,45 @@ describe("lib/plugin_loader", function() {
);
});
});
describe("loadMasterPluginClass()", function() {
let baseDir = path.join("temp", "test", "plugin");
let missingClass = path.join(baseDir, "missing_class_plugin");
let wrongParentClass = path.join(baseDir, "wrong_parent_class_plugin");
before(async function() {
async function writeEntrypoint(pluginPath, content) {
await fs.outputFile(path.join(pluginPath, "master.js"), content);
}
await writeEntrypoint(missingClass, "");
await writeEntrypoint(wrongParentClass, "class MasterPlugin {}\n module.exports = { MasterPlugin };\n");
});
it("should throw if class is missing from entrypoint", async function() {
await assert.rejects(
libPluginLoader.loadMasterPluginClass({
name: "test",
masterEntrypoint: "master",
requirePath: path.resolve(missingClass),
}),
{
message:
`PluginError: Expected ${path.resolve(missingClass, "master")} ` +
"to export a class named MasterPlugin",
}
);
});
it("should throw if class is not a subclass of BaseMasterPlugin", async function() {
await assert.rejects(
libPluginLoader.loadMasterPluginClass({
name: "test",
masterEntrypoint: "master",
requirePath: path.resolve(wrongParentClass),
}),
{
message:
"PluginError: Expected MasterPlugin exported from " +
`${path.resolve(wrongParentClass, "master")} to be a subclass of BaseMasterPlugin`,
}
);
});
});
});