mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Move base plugins to their respective packages
Move BaseControllerPlugin to controller/src/BaseControllerPlugin.ts Move BaseInstancePlugin to host/src/BaseInstancePlugin.ts Move BaseControlPlugin to ctl/src/BaseCtlPlugin.ts Move BaseWebPlugin to web_ui/src/BaseWebPlugin.ts This solves the circular package dependency problem of the base plugins needing types that are available only in the dependencies to lib.
This commit is contained in:
1 parent
63dc370ea0
commit
808eda0513
51 files changed
+1073
-1060
No files matched your search
@@ -95,8 +95,8 @@ The following properties are recognized:
|
||||
Subclass of `PluginConfigGroup` for defining the controller configuration fields for this plugin.
|
||||
See [Plugin Configuration](#plugin-configuration)
|
||||
|
||||
**controlEntrypoint**:
|
||||
Path to a Node.js module relative to the plugin directory which contains the ControlPlugin class definition for this plugin.
|
||||
**ctlEntrypoint**:
|
||||
Path to a Node.js module relative to the plugin directory which contains the CtlPlugin class definition for this plugin.
|
||||
This is an optional paramater.
|
||||
A plugin can be made that only runs on the clusterioctl side.
|
||||
|
||||
@@ -120,9 +120,9 @@ The plugin class should derive from its respective base class defined in `lib/pl
|
||||
For example, to define a ControllerPlugin class the following code can be used:
|
||||
|
||||
```js
|
||||
const lib = require("@clusterio/lib");
|
||||
const { BaseControllerPlugin } = require("@clusterio/controller");
|
||||
|
||||
class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
class ControllerPlugin extends BaseControllerPlugin {
|
||||
async init() {
|
||||
this.foo = 42;
|
||||
await this.startFrobnication();
|
||||
@@ -136,7 +136,7 @@ module.exports = {
|
||||
}
|
||||
```
|
||||
|
||||
For the instance plugin it's exactly the same except "Controller" is replaced with "Instance", and for the clusterioctl plugin "Control" is used.
|
||||
For the instance plugin it's exactly the same except "Controller" is replaced with "Instance", and for the clusterioctl plugin "Ctl" is used.
|
||||
The available hooks that you can override are documented in the base class [in lib/plugin.js](/packages/lib/plugin.js).
|
||||
|
||||
It's best to avoid defining a constructor, but if you insist on defining one, forward all arguments to the base class.
|
||||
@@ -602,18 +602,18 @@ It's possible to optain a reference to the plugin class with `control.plugins.ge
|
||||
|
||||
Note that messages sent from clusterioctl needs to have `"control-controller"` as a part of the links array for it to be accepted by the controller, see [Defining Link Messages](#defining-link-messages) for how to define the messages that can be sent to the controller.
|
||||
|
||||
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:
|
||||
To have the command tree become part of clusterioctl it needs to be added to the rootCommand tree in `addCommands` callback of the Ctl plugin:
|
||||
|
||||
```js
|
||||
const lib = require("@clusterio/lib");
|
||||
const { BaseCtlPlugin } = require("@clusterio/ctl");
|
||||
|
||||
class ControlPlugin extends lib.BaseControlPlugin {
|
||||
class CtlPlugin extends BaseCtlPlugin {
|
||||
async addCommands(rootCommand) {
|
||||
rootCommand.add(fooFrobberCommands);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ControlPlugin,
|
||||
CtlPlugin,
|
||||
}
|
||||
```
|
||||
@@ -420,10 +420,7 @@ async function startup() {
|
||||
await controller.start(args);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
};
|
||||
|
||||
if (module === require.main) {
|
||||
export function bootstrap() {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`
|
||||
+==========================================================+
|
||||
@@ -460,3 +457,7 @@ ${err.stack}`
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
|
||||
if (module === require.main) {
|
||||
bootstrap();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { bootstrap } from "./controller";
|
||||
export { default as Controller } from "./src/Controller";
|
||||
export { default as ControlConnection } from "./src/ControlConnection";
|
||||
export { default as InstanceInfo } from "./src/InstanceInfo";
|
||||
export { default as BaseControllerPlugin } from "./src/BaseControllerPlugin";
|
||||
|
||||
if (module === require.main) {
|
||||
bootstrap();
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.13",
|
||||
"main": "dist/controller.js",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build && webpack-cli --env production",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
import type {
|
||||
CollectorResult, ConfigGroup, Event, InstanceStatus, Logger,
|
||||
ModPack, ModInfo, PlayerEvent, PluginNodeEnvInfo,
|
||||
} from "@clusterio/lib";
|
||||
import type Controller from "./Controller";
|
||||
import type InstanceInfo from "./InstanceInfo";
|
||||
import type ControlConnection from "./ControlConnection";
|
||||
import type HostConnection from "./HostConnection";
|
||||
|
||||
/**
|
||||
* Base class for controller plugins
|
||||
*
|
||||
* Controller plugins are subclasses of this class which get instantiated by
|
||||
* the controller on startup when the plugin is enabled in the config.
|
||||
* To be discovered the class must be exported under the name `ControllerPlugin`
|
||||
* in the module specified by the `controllerEntrypoint` in the plugin's info.js
|
||||
* file.
|
||||
*/
|
||||
export default class BaseControllerPlugin {
|
||||
/**
|
||||
* Logger for this plugin
|
||||
*
|
||||
* Instance of winston Logger for sending log messages from this
|
||||
* plugin. Supported methods and their corresponding log levels are
|
||||
* `error`, `warn`, `audit`, `info` and `verbose`.
|
||||
*/
|
||||
logger: Logger;
|
||||
|
||||
constructor(
|
||||
public info: PluginNodeEnvInfo,
|
||||
public controller: Controller,
|
||||
public metrics: any,
|
||||
logger: Logger
|
||||
) {
|
||||
this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called immediately after the class is instantiated
|
||||
*/
|
||||
async init() { }
|
||||
|
||||
/**
|
||||
* Called when the status of an instance changes
|
||||
*
|
||||
* Invoked when the controller has changed the status of an instance
|
||||
* or received notice from a host that the status of an instance has
|
||||
* changed. The possible statuses that can be notified about are:
|
||||
* - `unassigned:`: Instance is no longer asssigned to a host.
|
||||
* - `unknown`: Host assigned to instance is offline.
|
||||
* - `stopped`: Instance is no longer running or was just assigned to a
|
||||
* host.
|
||||
* - `starting`: Instance is in the process of starting up.
|
||||
* - `running`: Instance startup completed and is now running.
|
||||
* - `stopping`: Instance is in the processing stopping.
|
||||
* - `creating_save`: Instance is in the process of creating a save.
|
||||
* - `exporting_data`: Instance is in the process of exporting item
|
||||
* icons and locale data.
|
||||
* - `deleted`: Instance was deleted.
|
||||
*
|
||||
* On controller startup all known instances gets the status `unknown` if
|
||||
* they are assigned to a host, when the host then connects to the
|
||||
* controller and informs of the current status of its instances this hook
|
||||
* is invoked for all those instances. You can detect this situation by
|
||||
* checking if prev equals `unknown`. If the host disconnects the
|
||||
* instances will again get the `unknown` status.
|
||||
*
|
||||
* When instances are created on the controller they will notify of a status
|
||||
* change with prev set to null. While the status of new instances is
|
||||
* in most cases `unassigned` it's possible for the created instance to
|
||||
* start with any state in some host connection corner cases.
|
||||
*
|
||||
* Note that it's possible for status change notification to get lost in
|
||||
* the case of network outages if reconnect fails to re-establish the
|
||||
* session between the controller and the host.
|
||||
*
|
||||
* @param instance -
|
||||
* The instance that changed.
|
||||
* @param prev - the previous status of the instance.
|
||||
*/
|
||||
async onInstanceStatusChanged(instance: InstanceInfo, prev?: InstanceStatus) { }
|
||||
|
||||
/**
|
||||
* Called when the value of a controller config field changed.
|
||||
*
|
||||
* Invoked after the value of the config field given by `field` has
|
||||
* changed on the controller.
|
||||
*
|
||||
* @param group -
|
||||
* The group who's field got changed.
|
||||
* @param field - Name of the field that changed.
|
||||
* @param prev - The previous value of the field.
|
||||
*/
|
||||
async onControllerConfigFieldChanged(group: ConfigGroup, field: string, prev: unknown) { }
|
||||
|
||||
/**
|
||||
* Called when the value of an instance config field changed.
|
||||
*
|
||||
* Invoked after the value of the config field given by `field` has
|
||||
* changed on an instance.
|
||||
*
|
||||
* @param instance -
|
||||
* The instance the config changed on.
|
||||
* @param group -
|
||||
* The group who's field got changed.
|
||||
* @param field - Name of the field that changed.
|
||||
* @param prev - The previous value of the field.
|
||||
*/
|
||||
async onInstanceConfigFieldChanged(instance: InstanceInfo, group: ConfigGroup, field: string, prev: unknown) { }
|
||||
|
||||
/**
|
||||
* Called before collecting Prometheus metrics
|
||||
*
|
||||
* Invoked before the default metrics of prometheus is collected. Note
|
||||
* that since this is done while replying to /metrics this should not
|
||||
* take a long time, otherwise the requst will time out.
|
||||
*
|
||||
* Although you can return the results of collecting your prometheus
|
||||
* collectors directly, all gauges in the default registry is already
|
||||
* automatically collected. It's therefore recommended to only update
|
||||
* the Gauges/Counters/etc in onMetric and let Clusterio deal will
|
||||
* collecting the values.
|
||||
*
|
||||
* @returns an async iterator of prometheus metric results or undefined.
|
||||
*/
|
||||
async onMetrics(): Promise<void | AsyncIterator<CollectorResult>> { }
|
||||
|
||||
/**
|
||||
* Called when the controller is shutting down
|
||||
*/
|
||||
async onShutdown() { }
|
||||
|
||||
/**
|
||||
* Called when an event on a host connection happens
|
||||
*
|
||||
* The event param may be one of connect, drop, resume and close and has
|
||||
* the following meaning:
|
||||
*
|
||||
* ##### connect
|
||||
*
|
||||
* Invoked when a new connection to the host has been established.
|
||||
*
|
||||
* ##### drop
|
||||
*
|
||||
* Invoked when a connection loss is detected between the controller and a
|
||||
* host. Plugins should respond to this event by throtteling messages
|
||||
* it is sending to the given host connection to an absolute minimum.
|
||||
*
|
||||
* Messages sent over a dropped host connection will get queued up in
|
||||
* memory on the controller and sent all in one go when the connection is
|
||||
* re-established again.
|
||||
*
|
||||
* ##### resume
|
||||
*
|
||||
* Invoked when a connection that had previously been dropped is
|
||||
* re-established
|
||||
*
|
||||
* ##### close
|
||||
*
|
||||
* Invoked when the connection to the host has been closed.
|
||||
*
|
||||
* @param connection -
|
||||
* The connection the event occured on.
|
||||
* @param event - one of connect, drop, resume and close
|
||||
*/
|
||||
onHostConnectionEvent(connection: HostConnection, event: "connect" | "drop" | "resume" | "close") { }
|
||||
|
||||
/**
|
||||
* Called when an avent on a control connection happens
|
||||
*
|
||||
* The event param may be one of connect, drop, resume and close and has
|
||||
* the following meaning:
|
||||
*
|
||||
* ##### connect
|
||||
*
|
||||
* Invoked when a new connection to the control has been established.
|
||||
*
|
||||
* ##### drop
|
||||
*
|
||||
* Invoked when a connection loss is detected between the controller and a
|
||||
* control. Plugins should respond to this event by throtteling
|
||||
* messages it is sending to the given control connection to an absolute
|
||||
* minimum.
|
||||
*
|
||||
* Messages sent over a dropped control connection will get queued up in
|
||||
* memory on the controller and sent all in one go when the connection is
|
||||
* re-established again.
|
||||
*
|
||||
* ##### resume
|
||||
*
|
||||
* Invoked when a connection that had previously been dropped is
|
||||
* re-established.
|
||||
*
|
||||
* ##### close
|
||||
*
|
||||
* Invoked when the connection to the control has been closed.
|
||||
*
|
||||
* @param connection -
|
||||
* The connection the event occured on.
|
||||
* @param event - one of connect, drop, resume, and close.
|
||||
*/
|
||||
onControlConnectionEvent(connection: ControlConnection, event: "connect" | "drop" | "resume" | "close") { }
|
||||
|
||||
/**
|
||||
* Called when a host is preparing to disconnect from the controller
|
||||
*
|
||||
* Invoked when a host has requested the disconnection from the controller.
|
||||
* This typically happens when it is in the process of shutting down.
|
||||
*
|
||||
* Plugins must stop sending messages to the host in question after the
|
||||
* prepare disconnect has been handled.
|
||||
*
|
||||
* @param connection -
|
||||
* The connection to the host preparing to disconnect.
|
||||
*/
|
||||
async onPrepareHostDisconnect(connection: HostConnection) { }
|
||||
|
||||
/**
|
||||
* Called when a mod pack is updated
|
||||
*
|
||||
* Invoked when a mod pack has been added, updated or deleted in the
|
||||
* cluster.
|
||||
*
|
||||
* If the mod pack has been deleted its `.isDeleted` property will be
|
||||
* true.
|
||||
*
|
||||
* @param modPack - Mod pack that updated.
|
||||
*/
|
||||
async onModPackUpdated(modPack: ModPack) { }
|
||||
|
||||
/**
|
||||
* Called when a mod stored on the controller is updated
|
||||
*
|
||||
* Invoked when a mod has been added, updated or deleted from the pool
|
||||
* of shared mods stored on the cluster.
|
||||
*
|
||||
* If a mod has been deleted its `.isDeleted` property will be true.
|
||||
*
|
||||
* @param mod - Mod that updated.
|
||||
*/
|
||||
async onModUpdated(mod: ModInfo) { }
|
||||
|
||||
/**
|
||||
* Called when a player joins or leaves an instance
|
||||
*
|
||||
* Invoked when a player either joins or leaves an instance in the
|
||||
* cluster.
|
||||
*
|
||||
* @param instance -
|
||||
* The instance it occured on.
|
||||
* @param event - Information about the event.
|
||||
*/
|
||||
async onPlayerEvent(instance: InstanceInfo, event: PlayerEvent) { }
|
||||
|
||||
/**
|
||||
* Broadcast event to all connected hosts
|
||||
*
|
||||
* Sends the given event to all hosts connected to the controller.
|
||||
* This does not include hosts that are in the process of closing the
|
||||
* connection, which typically happens when they are shutting down.
|
||||
*
|
||||
* @param event - Event to send
|
||||
*/
|
||||
broadcastEventToHosts<T>(event: Event<T>) {
|
||||
for (let hostConnection of this.controller.wsServer.hostConnections.values()) {
|
||||
if (
|
||||
!hostConnection.connector.closing
|
||||
&& (!event.constructor.plugin || hostConnection.plugins.has(event.constructor.plugin))
|
||||
) {
|
||||
hostConnection.send(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import * as routes from "./routes";
|
||||
import UserManager from "./UserManager";
|
||||
import WsServer from "./WsServer";
|
||||
import HostConnection, { type HostInfo } from "./HostConnection";
|
||||
import BaseControllerPlugin from "./BaseControllerPlugin";
|
||||
|
||||
const endpointDurationSummary = new Summary(
|
||||
"clusterio_controller_http_endpoint_duration_seconds",
|
||||
@@ -64,7 +65,7 @@ export default class Controller {
|
||||
httpsServerCloser: HttpCloser | null = null;
|
||||
|
||||
/** Mapping of plugin name to loaded plugin */
|
||||
plugins: Map<string, lib.BaseControllerPlugin> = new Map();
|
||||
plugins: Map<string, BaseControllerPlugin> = new Map();
|
||||
|
||||
/** WebSocket server */
|
||||
wsServer: WsServer;
|
||||
@@ -877,10 +878,15 @@ export default class Controller {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ControllerPluginClass = lib.BaseControllerPlugin;
|
||||
let ControllerPluginClass = BaseControllerPlugin;
|
||||
try {
|
||||
if (pluginInfo.controllerEntrypoint) {
|
||||
ControllerPluginClass = await lib.loadControllerPluginClass(pluginInfo);
|
||||
ControllerPluginClass = await lib.loadPluginClass(
|
||||
pluginInfo.name,
|
||||
path.posix.join(pluginInfo.requirePath, pluginInfo.controllerEntrypoint),
|
||||
"ControllerPlugin",
|
||||
BaseControllerPlugin,
|
||||
);
|
||||
}
|
||||
|
||||
let controllerPlugin = new ControllerPluginClass(pluginInfo, this, metrics as any, logger);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"references": [
|
||||
{ "path": "../lib" },
|
||||
],
|
||||
"include": ["controller.ts", "package.json", "src/**/*.ts"],
|
||||
"include": ["index.ts", "controller.ts", "package.json", "src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
},
|
||||
|
||||
+25
-15
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
import fs from "fs-extra";
|
||||
import yargs from "yargs";
|
||||
import path from "path";
|
||||
import { version } from "./package.json";
|
||||
import setBlocking from "set-blocking";
|
||||
import { strict as assert } from "assert";
|
||||
@@ -16,6 +17,7 @@ import * as lib from "@clusterio/lib";
|
||||
import { ConsoleTransport, levels, logger } from "@clusterio/lib";
|
||||
|
||||
import * as commands from "./src/commands";
|
||||
import BaseCtlPlugin from "./src/BaseCtlPlugin";
|
||||
|
||||
|
||||
/**
|
||||
@@ -56,7 +58,7 @@ export class Control extends lib.Link {
|
||||
/** Certificate authority used to validate TLS connections to the controller. */
|
||||
tlsCa?: string;
|
||||
/** Mapping of plugin names to their instance for loaded plugins. */
|
||||
plugins: Map<string, lib.BaseControlPlugin>;
|
||||
plugins: Map<string, BaseCtlPlugin>;
|
||||
/** Keep the control connection alive after the command completes. */
|
||||
keepOpen = false;
|
||||
|
||||
@@ -64,12 +66,12 @@ export class Control extends lib.Link {
|
||||
connector: ControlConnector,
|
||||
controlConfig: lib.ControlConfig,
|
||||
tlsCa: string | undefined,
|
||||
controlPlugins: Map<string, lib.BaseControlPlugin>
|
||||
ctlPlugins: Map<string, BaseCtlPlugin>
|
||||
) {
|
||||
super(connector);
|
||||
this.config = controlConfig;
|
||||
this.tlsCa = tlsCa;
|
||||
this.plugins = controlPlugins;
|
||||
this.plugins = ctlPlugins;
|
||||
|
||||
this.handle(lib.LogMessageEvent, this.handleLogMessageEvent.bind(this));
|
||||
this.handle(lib.DebugWsMessageEvent, this.handleDebugWsMessageEvent.bind(this));
|
||||
@@ -115,18 +117,23 @@ async function loadPlugins(pluginList: Map<string, string>) {
|
||||
lib.registerPluginConfigGroups(pluginInfos);
|
||||
lib.finalizeConfigs();
|
||||
|
||||
let controlPlugins = new Map<string, lib.BaseControlPlugin>();
|
||||
let ctlPlugins = new Map<string, BaseCtlPlugin>();
|
||||
for (let pluginInfo of pluginInfos) {
|
||||
if (!pluginInfo.controlEntrypoint) {
|
||||
if (!pluginInfo.ctlEntrypoint) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ControlPluginClass = await lib.loadControlPluginClass(pluginInfo);
|
||||
let controlPlugin = new ControlPluginClass(pluginInfo, logger);
|
||||
controlPlugins.set(pluginInfo.name, controlPlugin);
|
||||
await controlPlugin.init();
|
||||
let CtlPlugin = await lib.loadPluginClass(
|
||||
pluginInfo.name,
|
||||
path.posix.join(pluginInfo.requirePath, pluginInfo.ctlEntrypoint),
|
||||
"CtlPlugin",
|
||||
BaseCtlPlugin,
|
||||
);
|
||||
let ctlPlugin = new CtlPlugin(pluginInfo, logger);
|
||||
ctlPlugins.set(pluginInfo.name, ctlPlugin);
|
||||
await ctlPlugin.init();
|
||||
}
|
||||
return controlPlugins;
|
||||
return ctlPlugins;
|
||||
}
|
||||
|
||||
interface CtlArguments {
|
||||
@@ -199,10 +206,10 @@ async function startControl() {
|
||||
}
|
||||
|
||||
logger.verbose("Loading Plugins");
|
||||
let controlPlugins = await loadPlugins(pluginList);
|
||||
let ctlPlugins = await loadPlugins(pluginList);
|
||||
|
||||
// Add all cluster management commands including ones from plugins
|
||||
let rootCommands = await commands.registerCommands(controlPlugins, yargs);
|
||||
let rootCommands = await commands.registerCommands(ctlPlugins, yargs);
|
||||
|
||||
// Reparse after commands have been added with help and strict checking.
|
||||
args = yargs
|
||||
@@ -264,7 +271,7 @@ async function startControl() {
|
||||
tlsCa,
|
||||
controlConfig.get("control.controller_token") as string,
|
||||
);
|
||||
let control = new Control(controlConnector, controlConfig, tlsCa, controlPlugins);
|
||||
let control = new Control(controlConnector, controlConfig, tlsCa, ctlPlugins);
|
||||
try {
|
||||
await controlConnector.connect();
|
||||
} catch (err) {
|
||||
@@ -312,8 +319,7 @@ async function startControl() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (module === require.main) {
|
||||
export function bootstrap() {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`
|
||||
+==========================================================+
|
||||
@@ -346,3 +352,7 @@ ${err.stack}`
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
|
||||
if (module === require.main) {
|
||||
bootstrap();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { bootstrap } from "./ctl";
|
||||
export { default as BaseCtlPlugin } from "./src/BaseCtlPlugin";
|
||||
|
||||
if (module === require.main) {
|
||||
bootstrap();
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.13",
|
||||
"main": "dist/ctl.js",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { CommandTree, Logger, PluginNodeEnvInfo } from "@clusterio/lib";
|
||||
|
||||
/**
|
||||
* Base class for clusterioctl plugins
|
||||
*
|
||||
* Ctl plugins are subclasses of this class which get instantiated by
|
||||
* clusterioctl in order to extend its functionallity. To be discovered the
|
||||
* class must be exported under the name `CtlPlugin` in the module
|
||||
* specified by the `ctlEntrypoint` in the plugin's info.js file.
|
||||
*/
|
||||
export default class BaseCtlPlugin {
|
||||
/**
|
||||
* Logger for this plugin
|
||||
*
|
||||
* Instance of winston Logger for sending log messages from this
|
||||
* plugin. Supported methods and their corresponding log levels are
|
||||
* `error`, `warn`, `audit`, `info` and `verbose`.
|
||||
*/
|
||||
logger: Logger;
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* The plugin's own info module
|
||||
*/
|
||||
public info: PluginNodeEnvInfo,
|
||||
logger: Logger,
|
||||
) {
|
||||
this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called immediately after the class is instantiated
|
||||
*/
|
||||
async init() { }
|
||||
|
||||
/**
|
||||
* Called to add commands to the command line interface.
|
||||
*
|
||||
* Invoked by clusterioctl to let plugins add commands. `rootCommand` is
|
||||
* the top level command node which the plugin should add its own {@link
|
||||
* CommandTree} to.
|
||||
*
|
||||
* @param rootCommand -
|
||||
* Root of the clusterioctl command tree.
|
||||
*/
|
||||
async addCommands(rootCommand: CommandTree) { }
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import type { Argv } from "yargs";
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { ConsoleTransport, levels, logger } from "@clusterio/lib";
|
||||
import type { Control } from "../ctl";
|
||||
import type BaseCtlPlugin from "./BaseCtlPlugin";
|
||||
|
||||
const asTable = asTableModule.configure({ delimiter: " | " });
|
||||
const finished = util.promisify(stream.finished);
|
||||
@@ -1722,7 +1723,7 @@ debugCommands.add(new lib.Command({
|
||||
},
|
||||
}));
|
||||
|
||||
export async function registerCommands(controlPlugins: Map<string, lib.BaseControlPlugin>, yargs: Argv) {
|
||||
export async function registerCommands(ctlPlugins: Map<string, BaseCtlPlugin>, yargs: Argv) {
|
||||
const rootCommands = new lib.CommandTree({ name: "clusterioctl", description: "Manage cluster" });
|
||||
rootCommands.add(controllerCommands);
|
||||
rootCommands.add(hostCommands);
|
||||
@@ -1735,7 +1736,7 @@ export async function registerCommands(controlPlugins: Map<string, lib.BaseContr
|
||||
rootCommands.add(logCommands);
|
||||
rootCommands.add(debugCommands);
|
||||
|
||||
for (let controlPlugin of controlPlugins.values()) {
|
||||
for (let controlPlugin of ctlPlugins.values()) {
|
||||
await controlPlugin.addCommands(rootCommands);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"references": [
|
||||
{ "path": "../lib" },
|
||||
],
|
||||
"include": ["package.json", "ctl.ts", "src/**/*.ts"],
|
||||
"include": ["index.ts", "package.json", "ctl.ts", "src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
},
|
||||
|
||||
@@ -245,7 +245,7 @@ async function startHost() {
|
||||
logger.info("Started host");
|
||||
}
|
||||
|
||||
if (module === require.main) {
|
||||
export function bootstrap() {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`
|
||||
+==========================================================+
|
||||
@@ -287,3 +287,7 @@ ${err.stack}`
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
|
||||
if (module === require.main) {
|
||||
bootstrap();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { bootstrap } from "./host";
|
||||
export { default as BaseInstancePlugin } from "./src/BaseInstancePlugin";
|
||||
|
||||
if (module === require.main) {
|
||||
bootstrap();
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"author": "Hornwitser",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0-alpha.13",
|
||||
"main": "dist/host.js",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"prepare": "tsc --build",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import type {
|
||||
ConfigGroup, CollectorResult, Logger, ParsedFactorioOutput, PlayerEvent, PluginNodeEnvInfo,
|
||||
} from "@clusterio/lib";
|
||||
import type Instance from "./Instance";
|
||||
import type Host from "./Host";
|
||||
|
||||
/**
|
||||
* Base class for instance plugins
|
||||
*
|
||||
* Instance plugins are subclasses of this class which get instantiated by
|
||||
* the host when it brings up an instance with the plugin enabled in the
|
||||
* config. To be discovered the class must be exported under the name
|
||||
* `InstancePlugin` in the module specified by the `instanceEntrypoint` in
|
||||
* the plugin's info.js file.
|
||||
*
|
||||
* Instances may be started and stopped many times, and many instances may
|
||||
* be running at the same time, each of which will have their own instance
|
||||
* of the InstancePlugin class.
|
||||
*/
|
||||
export default class BaseInstancePlugin {
|
||||
/**
|
||||
* Logger for this plugin
|
||||
*
|
||||
* Instance of winston Logger for sending log messages from this
|
||||
* plugin. Supported methods and their corresponding log levels are
|
||||
* `error`, `warn`, `audit`, `info` and `verbose`.
|
||||
*/
|
||||
logger: Logger;
|
||||
|
||||
private _pendingRconMessages: {
|
||||
resolve: (result: string) => void,
|
||||
reject: (err: Error) => void,
|
||||
message: string,
|
||||
expectEmpty: boolean,
|
||||
}[] = [];
|
||||
|
||||
private _sendingRconMessages = false;
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* The plugin's own info module
|
||||
*/
|
||||
public info: PluginNodeEnvInfo,
|
||||
/**
|
||||
* Instance the plugin started for
|
||||
*/
|
||||
public instance: Instance,
|
||||
/**
|
||||
* Host running the instance
|
||||
*
|
||||
* With the exepction of accessing the host's config you should
|
||||
* avoid ineracting with the host object directly.
|
||||
*/
|
||||
public host: Host,
|
||||
) {
|
||||
this.logger = instance.logger.child({ plugin: this.info.name }) as unknown as Logger;
|
||||
|
||||
this._pendingRconMessages = [];
|
||||
this._sendingRconMessages = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called immediately after the class is instantiated
|
||||
*/
|
||||
async init() { }
|
||||
|
||||
/**
|
||||
* Called when the value of a config field changed.
|
||||
*
|
||||
* Invoked after the value of the config field given by `field` has
|
||||
* changed.
|
||||
*
|
||||
* @param group -
|
||||
* The group who's field got changed.
|
||||
* @param field - Name of the field that changed.
|
||||
* @param prev - The previous value of the field.
|
||||
*/
|
||||
async onInstanceConfigFieldChanged(group: ConfigGroup, field: string, prev: unknown) { }
|
||||
|
||||
/**
|
||||
* Called before collecting Prometheus metrics
|
||||
*
|
||||
* Invoked before the default metrics of prometheus is collected. Note
|
||||
* that since this is done while replying to /metrics this should not
|
||||
* take a long time, otherwise the requst will time out.
|
||||
*
|
||||
* Although you can return the results of collecting your prometheus
|
||||
* collectors directly, all gauges in the default registry is already
|
||||
* automatically collected. It's therefore recommended to only update
|
||||
* the Gauges/Counters/etc in onMetric and let Clusterio deal will
|
||||
* collecting the values.
|
||||
*
|
||||
* @returns an async iterator of prometheus metric results or undefined.
|
||||
*/
|
||||
async onMetrics(): Promise<void | AsyncIterator<CollectorResult>> { }
|
||||
|
||||
/**
|
||||
* 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 exits
|
||||
*
|
||||
* Invoked when the instance is shut down. This may occur after init
|
||||
* has been called if an error occurs during startup. Note that if
|
||||
* the plugin's init() throws this method will still be invoked.
|
||||
*/
|
||||
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:
|
||||
*
|
||||
* @param parsed - parsed server output.
|
||||
* @param line - raw line of server output.
|
||||
*/
|
||||
async onOutput(parsed: ParsedFactorioOutput, line: string) { }
|
||||
|
||||
/**
|
||||
* Called when an event on the controller connection happens
|
||||
*
|
||||
* The event param may be one of connect, drop, resume and close and has
|
||||
* the following meaning:
|
||||
*
|
||||
* ##### connect
|
||||
*
|
||||
* Invoked when a new connection to the controller has been established.
|
||||
*
|
||||
* ##### drop
|
||||
*
|
||||
* Invoked when a connection loss is detected between the host and the
|
||||
* controller. Plugins should respond to this event by throtteling messages
|
||||
* it is sending to the controller to an absolute minimum.
|
||||
*
|
||||
* Messages sent over a dropped controller connection will get queued up in
|
||||
* memory on the host and sent all in one go when the connection is
|
||||
* re-established again.
|
||||
*
|
||||
* ##### resume
|
||||
*
|
||||
* Invoked when the connection that had previously dropped is
|
||||
* re-established.
|
||||
*
|
||||
* ##### close
|
||||
*
|
||||
* Invoked when the connection to the controller has been closed. This
|
||||
* typically means the controller has shut down. Plugins should not
|
||||
* send any messages that goes to or via the controller after the
|
||||
* connection has been closed and before a new one is established.
|
||||
*
|
||||
* @param event - one of connect, drop, resume and close
|
||||
*/
|
||||
onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") { }
|
||||
|
||||
/**
|
||||
* Called when the controller is preparing to disconnect from the host
|
||||
*
|
||||
* Invoked when the controller has requested the disconnection from
|
||||
* the host. This typically happens when it is in the process of
|
||||
* shutting down.
|
||||
*
|
||||
* Plugins must stop sending messages to the controller, or are forwarded
|
||||
* via the controller after the prepare disconnect has been handled.
|
||||
*
|
||||
* @param connection -
|
||||
* The connection to the host preparing to disconnect.
|
||||
*/
|
||||
async onPrepareControllerDisconnect(connection: Instance) { }
|
||||
|
||||
/**
|
||||
* Called when a player joins or leaves the game
|
||||
*
|
||||
* Invoked when a player either joins or leaves the instance.
|
||||
*
|
||||
* @param event - Information about the event.
|
||||
*/
|
||||
async onPlayerEvent(event: PlayerEvent) { }
|
||||
|
||||
/**
|
||||
* Send RCON message to instance
|
||||
*
|
||||
* Send a message or command to the server which can potentially be
|
||||
* executed out of order in relation to other RCON commands.
|
||||
*
|
||||
* This should not be called before onStart or after onStop. A simple
|
||||
* way to achieve this is to check that the instance status is running
|
||||
* before sending commands. Note that the instance status is set to
|
||||
* running some time after `onStart` and some time before `onStop` is
|
||||
* invoked.
|
||||
*
|
||||
* @param message - message to send to server over RCON.
|
||||
* @param expectEmpty -
|
||||
* if true throw if the response is not empty. Useful for detecting
|
||||
* errors that might have been sent in response.
|
||||
* @returns response from server.
|
||||
*/
|
||||
async sendRcon(message: string, expectEmpty=false): Promise<string> {
|
||||
return await this.instance.sendRcon(message, expectEmpty, this.info.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send serially ordered RCON message to instance
|
||||
*
|
||||
* Send a message or command to the server which will not be executed
|
||||
* out of order in relation to other RCON commands sent with this
|
||||
* method. The ordering applies per plugin, and two plugins sending
|
||||
* commands with this method may execute out of order in relation to
|
||||
* each other.
|
||||
*
|
||||
* This should not be called before onStart or after onStop. A simple
|
||||
* way to achieve this is to check that the instance status is running
|
||||
* before sending commands. Note that the instance status is set to
|
||||
* running some time after `onStart` and some time before `onStop` is
|
||||
* invoked.
|
||||
*
|
||||
* @param message - message to send to server over RCON.
|
||||
* @param [expectEmpty=false] -
|
||||
* if true throw if the response is not empty. Useful for detecting
|
||||
* errors that might have been sent in response.
|
||||
* @returns response from server.
|
||||
*/
|
||||
async sendOrderedRcon(message: string, expectEmpty=false) {
|
||||
let promise = new Promise<string>((resolve, reject) => {
|
||||
this._pendingRconMessages.push({resolve, reject, message, expectEmpty});
|
||||
});
|
||||
if (!this._sendingRconMessages) {
|
||||
this._sendPendingRconMessages();
|
||||
}
|
||||
return await promise;
|
||||
}
|
||||
|
||||
async _sendPendingRconMessages() {
|
||||
this._sendingRconMessages = true;
|
||||
while (this._pendingRconMessages.length) {
|
||||
let task = this._pendingRconMessages.shift()!;
|
||||
try {
|
||||
let result = await this.sendRcon(task.message, task.expectEmpty);
|
||||
task.resolve(result);
|
||||
} catch (err: any) {
|
||||
task.reject(err);
|
||||
}
|
||||
}
|
||||
this._sendingRconMessages = false;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { FactorioServer } from "./server";
|
||||
import { SaveModule, patch } from "./patch";
|
||||
import { exportData } from "./export";
|
||||
import type Host from "./Host";
|
||||
import BaseInstancePlugin from "./BaseInstancePlugin";
|
||||
|
||||
|
||||
const instanceRconCommandDuration = new lib.Histogram(
|
||||
@@ -100,7 +101,7 @@ export default class Instance extends lib.Link {
|
||||
* ID of this instance, equivalenet to `instance.config.get("instance.id")`.
|
||||
*/
|
||||
readonly id: number;
|
||||
plugins: Map<string, lib.BaseInstancePlugin>;
|
||||
plugins: Map<string, BaseInstancePlugin>;
|
||||
config: lib.InstanceConfig;
|
||||
logger: lib.Logger;
|
||||
server: FactorioServer;
|
||||
@@ -492,7 +493,12 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
|
||||
async _loadPlugin(pluginInfo: lib.PluginNodeEnvInfo, host: Host) {
|
||||
let pluginLoadStarted = Date.now();
|
||||
let InstancePluginClass = await lib.loadInstancePluginClass(pluginInfo);
|
||||
let InstancePluginClass = await lib.loadPluginClass(
|
||||
pluginInfo.name,
|
||||
path.posix.join(pluginInfo.requirePath, pluginInfo.instanceEntrypoint!),
|
||||
"InstancePlugin",
|
||||
BaseInstancePlugin,
|
||||
);
|
||||
let instancePlugin = new InstancePluginClass(pluginInfo, this, host);
|
||||
this.plugins.set(pluginInfo.name, instancePlugin);
|
||||
await instancePlugin.init();
|
||||
|
||||
@@ -9,6 +9,8 @@ import { Type, Static } from "@sinclair/typebox";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
import BaseInstancePlugin from "./BaseInstancePlugin";
|
||||
|
||||
|
||||
/**
|
||||
* Describes a module that can be patched into a save
|
||||
@@ -81,7 +83,7 @@ export class SaveModule {
|
||||
return module;
|
||||
}
|
||||
|
||||
static async fromPlugin(plugin: lib.BasePlugin) {
|
||||
static async fromPlugin(plugin: BaseInstancePlugin) {
|
||||
let pluginPackagePath = require.resolve(path.posix.join(plugin.info.requirePath, "package.json"));
|
||||
let moduleDirectory = path.join(path.dirname(pluginPackagePath), "module");
|
||||
if (!await fs.pathExists(moduleDirectory)) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"references": [
|
||||
{ "path": "../lib" },
|
||||
],
|
||||
"include": ["host.ts", "package.json", "src/**/*.ts", "src/**/*.js"],
|
||||
"include": ["index.ts", "host.ts", "package.json", "src/**/*.ts", "src/**/*.js"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
},
|
||||
|
||||
@@ -3,7 +3,6 @@ import { TSchema, Type, Static } from "@sinclair/typebox";
|
||||
// eslint-disable-next-line node/no-missing-import
|
||||
import messageValidate from "./message_validate"; // generated file
|
||||
import { StringEnum } from "./composites";
|
||||
import { AccountRole } from "../plugin";
|
||||
|
||||
export type AddressType = "controller" | "host" | "instance" | "control" | "broadcast";
|
||||
export type AddressShorthand =
|
||||
@@ -287,6 +286,12 @@ export class MessageRegisterControl extends Message {
|
||||
}
|
||||
};
|
||||
|
||||
export type AccountRole = {
|
||||
name: string;
|
||||
id: number;
|
||||
permissions: string[];
|
||||
};
|
||||
|
||||
export class AccountDetails {
|
||||
constructor(
|
||||
public name: string,
|
||||
@@ -632,3 +637,28 @@ export class PingRequest {
|
||||
static dst = ["controller", "host", "control"] as const;
|
||||
static permission = null;
|
||||
}
|
||||
|
||||
export class AccountUpdateEvent {
|
||||
declare ["constructor"]: typeof AccountUpdateEvent;
|
||||
static type = "event" as const;
|
||||
static src = "controller" as const;
|
||||
static dst = "control" as const;
|
||||
|
||||
constructor(
|
||||
public roles?: AccountRole[],
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
roles: Type.Array(
|
||||
Type.Object({
|
||||
name: Type.String(),
|
||||
id: Type.Integer(),
|
||||
permissions: Type.Array(Type.String()),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(json.roles);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Type, Static } from "@sinclair/typebox";
|
||||
import PlayerStats from "../PlayerStats";
|
||||
import { JsonNumber, jsonArray } from "./composites";
|
||||
import { AccountRole } from "../plugin";
|
||||
|
||||
export class RawPermission { // TODO refactor into lib/user.Permission
|
||||
constructor(
|
||||
@@ -407,32 +406,6 @@ export class UserDeleteRequest {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class AccountUpdateEvent {
|
||||
declare ["constructor"]: typeof AccountUpdateEvent;
|
||||
static type = "event" as const;
|
||||
static src = "controller" as const;
|
||||
static dst = "control" as const;
|
||||
|
||||
constructor(
|
||||
public roles?: AccountRole[],
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
roles: Type.Array(
|
||||
Type.Object({
|
||||
name: Type.String(),
|
||||
id: Type.Integer(),
|
||||
permissions: Type.Array(Type.String()),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(json.roles);
|
||||
}
|
||||
}
|
||||
|
||||
export class UserUpdateEvent {
|
||||
declare ["constructor"]: typeof UserUpdateEvent;
|
||||
static type = "event" as const;
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { RequestClass, EventClass } from "./link";
|
||||
*/
|
||||
export const dataClasses: (RequestClass<unknown, unknown> | EventClass<unknown>)[] = [
|
||||
core.PingRequest,
|
||||
core.AccountUpdateEvent,
|
||||
|
||||
controller.ControllerConfigGetRequest,
|
||||
controller.ControllerConfigSetFieldRequest,
|
||||
@@ -105,6 +106,5 @@ export const dataClasses: (RequestClass<unknown, unknown> | EventClass<unknown>)
|
||||
user.UserSetWhitelistedRequest,
|
||||
user.UserSetBannedRequest,
|
||||
user.UserDeleteRequest,
|
||||
user.AccountUpdateEvent,
|
||||
user.UserUpdateEvent,
|
||||
];
|
||||
+6
-811
@@ -3,22 +3,7 @@
|
||||
* @module lib/plugin
|
||||
*/
|
||||
import * as libHelpers from "./helpers";
|
||||
import type { Logger } from "./logging";
|
||||
import type { ConfigGroup, PluginConfigGroup } from "./config";
|
||||
import type { CollectorResult } from "./prometheus";
|
||||
import type { ParsedFactorioOutput } from "./logging_utils";
|
||||
import type { ModPack, ModInfo } from "./data";
|
||||
import type { Link } from "./link";
|
||||
import type { CommandTree } from "./command";
|
||||
|
||||
/**
|
||||
* Conceptual base for controller and instance plugins.
|
||||
*/
|
||||
export type BasePlugin =
|
||||
| BaseControllerPlugin
|
||||
| BaseInstancePlugin
|
||||
| BaseControlPlugin
|
||||
;
|
||||
import type { PluginConfigGroup } from "./config";
|
||||
|
||||
// TODO Add proper typing for plugins
|
||||
/* Used to define the export of info.ts from plugins */
|
||||
@@ -33,14 +18,14 @@ export type PluginDeclaration = {
|
||||
controllerEntrypoint?: string;
|
||||
ControllerConfigGroup?: typeof PluginConfigGroup;
|
||||
|
||||
controlEntrypoint?: string;
|
||||
ctlEntrypoint?: string;
|
||||
webEntrypoint?: string;
|
||||
ControlConfigGroup?: typeof PluginConfigGroup;
|
||||
|
||||
messages?: any[];
|
||||
|
||||
webEntrypoint?: string;
|
||||
routes?: string[];
|
||||
}
|
||||
|
||||
export type PluginNodeEnvInfo = PluginDeclaration & {
|
||||
requirePath: string;
|
||||
version: string;
|
||||
@@ -53,19 +38,10 @@ export type PluginWebpackEnvInfo = PluginDeclaration & {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
type Controller = any;
|
||||
type Instance = any;
|
||||
type Host = any;
|
||||
type InstanceInfo = any;
|
||||
type HostConnection = any;
|
||||
type ControlConnection = any;
|
||||
type Event = any;
|
||||
type React = any;
|
||||
|
||||
/**
|
||||
* Information about the event.
|
||||
*/
|
||||
interface PlayerEvent {
|
||||
export interface PlayerEvent {
|
||||
type: "join" | "leave" | "import";
|
||||
/** Name of the player that joined/left */
|
||||
name: string,
|
||||
@@ -76,787 +52,6 @@ interface PlayerEvent {
|
||||
*/
|
||||
reason?: string;
|
||||
}
|
||||
type InstanceStatus =
|
||||
| "unassigned"
|
||||
| "unknown"
|
||||
| "stopped"
|
||||
| "starting"
|
||||
| "running"
|
||||
| "stopping"
|
||||
| "creating_save"
|
||||
| "exporting_data"
|
||||
| "deleted"
|
||||
;
|
||||
|
||||
/**
|
||||
* Base class for instance plugins
|
||||
*
|
||||
* Instance plugins are subclasses of this class which get instantiated by
|
||||
* the host when it brings up an instance with the plugin enabled in the
|
||||
* config. To be discovered the class must be exported under the name
|
||||
* `InstancePlugin` in the module specified by the `instanceEntrypoint` in
|
||||
* the plugin's info.js file.
|
||||
*
|
||||
* Instances may be started and stopped many times, and many instances may
|
||||
* be running at the same time, each of which will have their own instance
|
||||
* of the InstancePlugin class.
|
||||
*/
|
||||
export class BaseInstancePlugin {
|
||||
/**
|
||||
* Logger for this plugin
|
||||
*
|
||||
* Instance of winston Logger for sending log messages from this
|
||||
* plugin. Supported methods and their corresponding log levels are
|
||||
* `error`, `warn`, `audit`, `info` and `verbose`.
|
||||
*/
|
||||
logger: Logger;
|
||||
|
||||
private _pendingRconMessages: {
|
||||
resolve: (result: string) => void,
|
||||
reject: (err: Error) => void,
|
||||
message: string,
|
||||
expectEmpty: boolean,
|
||||
}[] = [];
|
||||
|
||||
private _sendingRconMessages = false;
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* The plugin's own info module
|
||||
*/
|
||||
public info: PluginNodeEnvInfo,
|
||||
/**
|
||||
* Instance the plugin started for
|
||||
*/
|
||||
public instance: Instance,
|
||||
/**
|
||||
* Host running the instance
|
||||
*
|
||||
* With the exepction of accessing the host's config you should
|
||||
* avoid ineracting with the host object directly.
|
||||
*/
|
||||
public host: Host,
|
||||
) {
|
||||
this.logger = instance.logger.child({ plugin: this.info.name }) as unknown as Logger;
|
||||
|
||||
this._pendingRconMessages = [];
|
||||
this._sendingRconMessages = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called immediately after the class is instantiated
|
||||
*/
|
||||
async init() { }
|
||||
|
||||
/**
|
||||
* Called when the value of a config field changed.
|
||||
*
|
||||
* Invoked after the value of the config field given by `field` has
|
||||
* changed.
|
||||
*
|
||||
* @param group -
|
||||
* The group who's field got changed.
|
||||
* @param field - Name of the field that changed.
|
||||
* @param prev - The previous value of the field.
|
||||
*/
|
||||
async onInstanceConfigFieldChanged(group: ConfigGroup, field: string, prev: unknown) { }
|
||||
|
||||
/**
|
||||
* Called before collecting Prometheus metrics
|
||||
*
|
||||
* Invoked before the default metrics of prometheus is collected. Note
|
||||
* that since this is done while replying to /metrics this should not
|
||||
* take a long time, otherwise the requst will time out.
|
||||
*
|
||||
* Although you can return the results of collecting your prometheus
|
||||
* collectors directly, all gauges in the default registry is already
|
||||
* automatically collected. It's therefore recommended to only update
|
||||
* the Gauges/Counters/etc in onMetric and let Clusterio deal will
|
||||
* collecting the values.
|
||||
*
|
||||
* @returns an async iterator of prometheus metric results or undefined.
|
||||
*/
|
||||
async onMetrics(): Promise<void | AsyncIterator<CollectorResult>> { }
|
||||
|
||||
/**
|
||||
* 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 exits
|
||||
*
|
||||
* Invoked when the instance is shut down. This may occur after init
|
||||
* has been called if an error occurs during startup. Note that if
|
||||
* the plugin's init() throws this method will still be invoked.
|
||||
*/
|
||||
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:
|
||||
*
|
||||
* @param parsed - parsed server output.
|
||||
* @param line - raw line of server output.
|
||||
*/
|
||||
async onOutput(parsed: ParsedFactorioOutput, line: string) { }
|
||||
|
||||
/**
|
||||
* Called when an event on the controller connection happens
|
||||
*
|
||||
* The event param may be one of connect, drop, resume and close and has
|
||||
* the following meaning:
|
||||
*
|
||||
* ##### connect
|
||||
*
|
||||
* Invoked when a new connection to the controller has been established.
|
||||
*
|
||||
* ##### drop
|
||||
*
|
||||
* Invoked when a connection loss is detected between the host and the
|
||||
* controller. Plugins should respond to this event by throtteling messages
|
||||
* it is sending to the controller to an absolute minimum.
|
||||
*
|
||||
* Messages sent over a dropped controller connection will get queued up in
|
||||
* memory on the host and sent all in one go when the connection is
|
||||
* re-established again.
|
||||
*
|
||||
* ##### resume
|
||||
*
|
||||
* Invoked when the connection that had previously dropped is
|
||||
* re-established.
|
||||
*
|
||||
* ##### close
|
||||
*
|
||||
* Invoked when the connection to the controller has been closed. This
|
||||
* typically means the controller has shut down. Plugins should not
|
||||
* send any messages that goes to or via the controller after the
|
||||
* connection has been closed and before a new one is established.
|
||||
*
|
||||
* @param event - one of connect, drop, resume and close
|
||||
*/
|
||||
onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") { }
|
||||
|
||||
/**
|
||||
* Called when the controller is preparing to disconnect from the host
|
||||
*
|
||||
* Invoked when the controller has requested the disconnection from
|
||||
* the host. This typically happens when it is in the process of
|
||||
* shutting down.
|
||||
*
|
||||
* Plugins must stop sending messages to the controller, or are forwarded
|
||||
* via the controller after the prepare disconnect has been handled.
|
||||
*
|
||||
* @param connection -
|
||||
* The connection to the host preparing to disconnect.
|
||||
*/
|
||||
async onPrepareControllerDisconnect(connection: HostConnection) { }
|
||||
|
||||
/**
|
||||
* Called when a player joins or leaves the game
|
||||
*
|
||||
* Invoked when a player either joins or leaves the instance.
|
||||
*
|
||||
* @param event - Information about the event.
|
||||
*/
|
||||
async onPlayerEvent(event: PlayerEvent) { }
|
||||
|
||||
/**
|
||||
* Send RCON message to instance
|
||||
*
|
||||
* Send a message or command to the server which can potentially be
|
||||
* executed out of order in relation to other RCON commands.
|
||||
*
|
||||
* This should not be called before onStart or after onStop. A simple
|
||||
* way to achieve this is to check that the instance status is running
|
||||
* before sending commands. Note that the instance status is set to
|
||||
* running some time after `onStart` and some time before `onStop` is
|
||||
* invoked.
|
||||
*
|
||||
* @param message - message to send to server over RCON.
|
||||
* @param expectEmpty -
|
||||
* if true throw if the response is not empty. Useful for detecting
|
||||
* errors that might have been sent in response.
|
||||
* @returns response from server.
|
||||
*/
|
||||
async sendRcon(message: string, expectEmpty=false): Promise<string> {
|
||||
return await this.instance.sendRcon(message, expectEmpty, this.info.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send serially ordered RCON message to instance
|
||||
*
|
||||
* Send a message or command to the server which will not be executed
|
||||
* out of order in relation to other RCON commands sent with this
|
||||
* method. The ordering applies per plugin, and two plugins sending
|
||||
* commands with this method may execute out of order in relation to
|
||||
* each other.
|
||||
*
|
||||
* This should not be called before onStart or after onStop. A simple
|
||||
* way to achieve this is to check that the instance status is running
|
||||
* before sending commands. Note that the instance status is set to
|
||||
* running some time after `onStart` and some time before `onStop` is
|
||||
* invoked.
|
||||
*
|
||||
* @param message - message to send to server over RCON.
|
||||
* @param [expectEmpty=false] -
|
||||
* if true throw if the response is not empty. Useful for detecting
|
||||
* errors that might have been sent in response.
|
||||
* @returns response from server.
|
||||
*/
|
||||
async sendOrderedRcon(message: string, expectEmpty=false) {
|
||||
let promise = new Promise<string>((resolve, reject) => {
|
||||
this._pendingRconMessages.push({resolve, reject, message, expectEmpty});
|
||||
});
|
||||
if (!this._sendingRconMessages) {
|
||||
this._sendPendingRconMessages();
|
||||
}
|
||||
return await promise;
|
||||
}
|
||||
|
||||
async _sendPendingRconMessages() {
|
||||
this._sendingRconMessages = true;
|
||||
while (this._pendingRconMessages.length) {
|
||||
let task = this._pendingRconMessages.shift()!;
|
||||
try {
|
||||
let result = await this.sendRcon(task.message, task.expectEmpty);
|
||||
task.resolve(result);
|
||||
} catch (err: any) {
|
||||
task.reject(err);
|
||||
}
|
||||
}
|
||||
this._sendingRconMessages = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for controller plugins
|
||||
*
|
||||
* Controller plugins are subclasses of this class which get instantiated by
|
||||
* the controller on startup when the plugin is enabled in the config.
|
||||
* To be discovered the class must be exported under the name `ControllerPlugin`
|
||||
* in the module specified by the `controllerEntrypoint` in the plugin's info.js
|
||||
* file.
|
||||
*/
|
||||
export class BaseControllerPlugin {
|
||||
/**
|
||||
* Logger for this plugin
|
||||
*
|
||||
* Instance of winston Logger for sending log messages from this
|
||||
* plugin. Supported methods and their corresponding log levels are
|
||||
* `error`, `warn`, `audit`, `info` and `verbose`.
|
||||
*/
|
||||
logger: Logger;
|
||||
|
||||
constructor(
|
||||
public info: PluginNodeEnvInfo,
|
||||
public controller: Controller,
|
||||
public metrics: any,
|
||||
logger: Logger
|
||||
) {
|
||||
this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called immediately after the class is instantiated
|
||||
*/
|
||||
async init() { }
|
||||
|
||||
/**
|
||||
* Called when the status of an instance changes
|
||||
*
|
||||
* Invoked when the controller has changed the status of an instance
|
||||
* or received notice from a host that the status of an instance has
|
||||
* changed. The possible statuses that can be notified about are:
|
||||
* - `unassigned:`: Instance is no longer asssigned to a host.
|
||||
* - `unknown`: Host assigned to instance is offline.
|
||||
* - `stopped`: Instance is no longer running or was just assigned to a
|
||||
* host.
|
||||
* - `starting`: Instance is in the process of starting up.
|
||||
* - `running`: Instance startup completed and is now running.
|
||||
* - `stopping`: Instance is in the processing stopping.
|
||||
* - `creating_save`: Instance is in the process of creating a save.
|
||||
* - `exporting_data`: Instance is in the process of exporting item
|
||||
* icons and locale data.
|
||||
* - `deleted`: Instance was deleted.
|
||||
*
|
||||
* On controller startup all known instances gets the status `unknown` if
|
||||
* they are assigned to a host, when the host then connects to the
|
||||
* controller and informs of the current status of its instances this hook
|
||||
* is invoked for all those instances. You can detect this situation by
|
||||
* checking if prev equals `unknown`. If the host disconnects the
|
||||
* instances will again get the `unknown` status.
|
||||
*
|
||||
* When instances are created on the controller they will notify of a status
|
||||
* change with prev set to null. While the status of new instances is
|
||||
* in most cases `unassigned` it's possible for the created instance to
|
||||
* start with any state in some host connection corner cases.
|
||||
*
|
||||
* Note that it's possible for status change notification to get lost in
|
||||
* the case of network outages if reconnect fails to re-establish the
|
||||
* session between the controller and the host.
|
||||
*
|
||||
* @param instance -
|
||||
* The instance that changed.
|
||||
* @param prev - the previous status of the instance.
|
||||
*/
|
||||
async onInstanceStatusChanged(instance: InstanceInfo, prev?: InstanceStatus) { }
|
||||
|
||||
/**
|
||||
* Called when the value of a controller config field changed.
|
||||
*
|
||||
* Invoked after the value of the config field given by `field` has
|
||||
* changed on the controller.
|
||||
*
|
||||
* @param group -
|
||||
* The group who's field got changed.
|
||||
* @param field - Name of the field that changed.
|
||||
* @param prev - The previous value of the field.
|
||||
*/
|
||||
async onControllerConfigFieldChanged(group: ConfigGroup, field: string, prev: unknown) { }
|
||||
|
||||
/**
|
||||
* Called when the value of an instance config field changed.
|
||||
*
|
||||
* Invoked after the value of the config field given by `field` has
|
||||
* changed on an instance.
|
||||
*
|
||||
* @param instance -
|
||||
* The instance the config changed on.
|
||||
* @param group -
|
||||
* The group who's field got changed.
|
||||
* @param field - Name of the field that changed.
|
||||
* @param prev - The previous value of the field.
|
||||
*/
|
||||
async onInstanceConfigFieldChanged(instance: InstanceInfo, group: ConfigGroup, field: string, prev: unknown) { }
|
||||
|
||||
/**
|
||||
* Called before collecting Prometheus metrics
|
||||
*
|
||||
* Invoked before the default metrics of prometheus is collected. Note
|
||||
* that since this is done while replying to /metrics this should not
|
||||
* take a long time, otherwise the requst will time out.
|
||||
*
|
||||
* Although you can return the results of collecting your prometheus
|
||||
* collectors directly, all gauges in the default registry is already
|
||||
* automatically collected. It's therefore recommended to only update
|
||||
* the Gauges/Counters/etc in onMetric and let Clusterio deal will
|
||||
* collecting the values.
|
||||
*
|
||||
* @returns an async iterator of prometheus metric results or undefined.
|
||||
*/
|
||||
async onMetrics(): Promise<void | AsyncIterator<CollectorResult>> { }
|
||||
|
||||
/**
|
||||
* Called when the controller is shutting down
|
||||
*/
|
||||
async onShutdown() { }
|
||||
|
||||
/**
|
||||
* Called when an event on a host connection happens
|
||||
*
|
||||
* The event param may be one of connect, drop, resume and close and has
|
||||
* the following meaning:
|
||||
*
|
||||
* ##### connect
|
||||
*
|
||||
* Invoked when a new connection to the host has been established.
|
||||
*
|
||||
* ##### drop
|
||||
*
|
||||
* Invoked when a connection loss is detected between the controller and a
|
||||
* host. Plugins should respond to this event by throtteling messages
|
||||
* it is sending to the given host connection to an absolute minimum.
|
||||
*
|
||||
* Messages sent over a dropped host connection will get queued up in
|
||||
* memory on the controller and sent all in one go when the connection is
|
||||
* re-established again.
|
||||
*
|
||||
* ##### resume
|
||||
*
|
||||
* Invoked when a connection that had previously been dropped is
|
||||
* re-established
|
||||
*
|
||||
* ##### close
|
||||
*
|
||||
* Invoked when the connection to the host has been closed.
|
||||
*
|
||||
* @param connection -
|
||||
* The connection the event occured on.
|
||||
* @param event - one of connect, drop, resume and close
|
||||
*/
|
||||
onHostConnectionEvent(connection: HostConnection, event: "connect" | "drop" | "resume" | "close") { }
|
||||
|
||||
/**
|
||||
* Called when an avent on a control connection happens
|
||||
*
|
||||
* The event param may be one of connect, drop, resume and close and has
|
||||
* the following meaning:
|
||||
*
|
||||
* ##### connect
|
||||
*
|
||||
* Invoked when a new connection to the control has been established.
|
||||
*
|
||||
* ##### drop
|
||||
*
|
||||
* Invoked when a connection loss is detected between the controller and a
|
||||
* control. Plugins should respond to this event by throtteling
|
||||
* messages it is sending to the given control connection to an absolute
|
||||
* minimum.
|
||||
*
|
||||
* Messages sent over a dropped control connection will get queued up in
|
||||
* memory on the controller and sent all in one go when the connection is
|
||||
* re-established again.
|
||||
*
|
||||
* ##### resume
|
||||
*
|
||||
* Invoked when a connection that had previously been dropped is
|
||||
* re-established.
|
||||
*
|
||||
* ##### close
|
||||
*
|
||||
* Invoked when the connection to the control has been closed.
|
||||
*
|
||||
* @param connection -
|
||||
* The connection the event occured on.
|
||||
* @param event - one of connect, drop, resume, and close.
|
||||
*/
|
||||
onControlConnectionEvent(connection: ControlConnection, event: "connect" | "drop" | "resume" | "close") { }
|
||||
|
||||
/**
|
||||
* Called when a host is preparing to disconnect from the controller
|
||||
*
|
||||
* Invoked when a host has requested the disconnection from the controller.
|
||||
* This typically happens when it is in the process of shutting down.
|
||||
*
|
||||
* Plugins must stop sending messages to the host in question after the
|
||||
* prepare disconnect has been handled.
|
||||
*
|
||||
* @param connection -
|
||||
* The connection to the host preparing to disconnect.
|
||||
*/
|
||||
async onPrepareHostDisconnect(connection: HostConnection) { }
|
||||
|
||||
/**
|
||||
* Called when a mod pack is updated
|
||||
*
|
||||
* Invoked when a mod pack has been added, updated or deleted in the
|
||||
* cluster.
|
||||
*
|
||||
* If the mod pack has been deleted its `.isDeleted` property will be
|
||||
* true.
|
||||
*
|
||||
* @param modPack - Mod pack that updated.
|
||||
*/
|
||||
async onModPackUpdated(modPack: ModPack) { }
|
||||
|
||||
/**
|
||||
* Called when a mod stored on the controller is updated
|
||||
*
|
||||
* Invoked when a mod has been added, updated or deleted from the pool
|
||||
* of shared mods stored on the cluster.
|
||||
*
|
||||
* If a mod has been deleted its `.isDeleted` property will be true.
|
||||
*
|
||||
* @param mod - Mod that updated.
|
||||
*/
|
||||
async onModUpdated(mod: ModInfo) { }
|
||||
|
||||
/**
|
||||
* Called when a player joins or leaves an instance
|
||||
*
|
||||
* Invoked when a player either joins or leaves an instance in the
|
||||
* cluster.
|
||||
*
|
||||
* @param instance -
|
||||
* The instance it occured on.
|
||||
* @param event - Information about the event.
|
||||
*/
|
||||
async onPlayerEvent(instance: InstanceInfo, event: PlayerEvent) { }
|
||||
|
||||
/**
|
||||
* Broadcast event to all connected hosts
|
||||
*
|
||||
* Sends the given event to all hosts connected to the controller.
|
||||
* This does not include hosts that are in the process of closing the
|
||||
* connection, which typically happens when they are shutting down.
|
||||
*
|
||||
* @param event - Event to send
|
||||
*/
|
||||
broadcastEventToHosts(event: Event) {
|
||||
for (let hostConnection of this.controller.wsServer.hostConnections.values()) {
|
||||
if (
|
||||
!hostConnection.connector.closing
|
||||
&& (!event.constructor.plugin || hostConnection.plugins.has(event.constructor.plugin))
|
||||
) {
|
||||
hostConnection.send(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for clusterioctl plugins
|
||||
*
|
||||
* Control plugins are subclasses of this class which get instantiated by
|
||||
* clusterioctl in order to extend its functionallity. To be discovered the
|
||||
* class must be exported under the name `ControlPlugin` in the module
|
||||
* specified by the `controlEntrypoint` in the plugin's info.js file.
|
||||
*/
|
||||
export class BaseControlPlugin {
|
||||
/**
|
||||
* Logger for this plugin
|
||||
*
|
||||
* Instance of winston Logger for sending log messages from this
|
||||
* plugin. Supported methods and their corresponding log levels are
|
||||
* `error`, `warn`, `audit`, `info` and `verbose`.
|
||||
*/
|
||||
logger: Logger;
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* The plugin's own info module
|
||||
*/
|
||||
public info: PluginNodeEnvInfo,
|
||||
logger: Logger,
|
||||
) {
|
||||
this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called immediately after the class is instantiated
|
||||
*/
|
||||
async init() { }
|
||||
|
||||
/**
|
||||
* Called to add commands to the command line interface.
|
||||
*
|
||||
* Invoked by clusterioctl to let plugins add commands. `rootCommand` is
|
||||
* the top level command node which the plugin should add its own {@link
|
||||
* CommandTree} to.
|
||||
*
|
||||
* @param rootCommand -
|
||||
* Root of the clusterioctl command tree.
|
||||
*/
|
||||
async addCommands(rootCommand: CommandTree) { }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Plugin supplied login form
|
||||
*/
|
||||
export interface PluginLoginForm {
|
||||
/**
|
||||
* Internal name of the login form, this should start with the
|
||||
* plugin name followed by a dot.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/** Name displayed above this form in the login window. */
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* React component that's rendered for this login form. This is
|
||||
* supplied the setToken function via its props which should be called
|
||||
* when an authentication token is aquired via this form.
|
||||
*/
|
||||
Component: React["Component"];
|
||||
};
|
||||
|
||||
export type AccountRole = {
|
||||
name: string;
|
||||
id: number;
|
||||
permissions: string[];
|
||||
};
|
||||
|
||||
export type UserAccount = {
|
||||
/** Name of the currently logged in account. */
|
||||
name: string;
|
||||
/** Roles of the corrently logged in account. */
|
||||
roles: AccountRole[];
|
||||
/** Check if the currently logged in account has the given permission. */
|
||||
hasPermission: (permission: string) => boolean | null;
|
||||
/** Check if the currently logged in account has any of the given permissions. */
|
||||
hasAnyPermission: (...permissions: string[]) => boolean | null;
|
||||
/** Check if the currently logged in account has all of given permissions. */
|
||||
hasAllPermission: (...permissions: string[]) => boolean | null;
|
||||
/** Logs out of the web interface. */
|
||||
logOut: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin supplied pages
|
||||
*/
|
||||
export interface PluginPage {
|
||||
/** URL path to this page. */
|
||||
path: string;
|
||||
/**
|
||||
* If present and this path matches one of the pages in the sidebar it
|
||||
* will cause that sidebar entry to be highlighted as active.
|
||||
*/
|
||||
sidebarPath?: string;
|
||||
/**
|
||||
* If present group this entry under a group of the given name in the
|
||||
* sidebar.
|
||||
*/
|
||||
sidebarGroup?: string;
|
||||
/**
|
||||
* If present creates an entry in the sidebar for this page with the
|
||||
* given text.
|
||||
*/
|
||||
sidebarName?: string;
|
||||
/**
|
||||
* A react node which is rendered when this page is navigated to.
|
||||
* Should render a PageLayout.
|
||||
*/
|
||||
content?: React["ReactNode"];
|
||||
/**
|
||||
* Permission to access page. function are expected to throw an error if access is deny.
|
||||
*/
|
||||
permission?: string | ((account: UserAccount) => (boolean|null));
|
||||
};
|
||||
|
||||
/**
|
||||
* Base class for web interface plugins
|
||||
*/
|
||||
export class BaseWebPlugin {
|
||||
/**
|
||||
* Contents of the plugin's package.json file
|
||||
*/
|
||||
package: any;
|
||||
/**
|
||||
* Control link to the controller, not available until the
|
||||
* connect event in onControllerConnectionEvent is signaled.
|
||||
*/
|
||||
control?: Link;
|
||||
/**
|
||||
* Logger for this plugin
|
||||
*
|
||||
* Instance of winston Logger for sending log messages from this
|
||||
* plugin. Supported methods and their corresponding log levels are
|
||||
* `error`, `warn`, `audit`, `info` and `verbose`.
|
||||
*/
|
||||
logger: Logger;
|
||||
/**
|
||||
* List of login forms provided by this plugin
|
||||
*/
|
||||
loginForms: PluginLoginForm[] = [];
|
||||
/**
|
||||
* List of pages provided by this plugin
|
||||
*/
|
||||
pages: PluginPage[] = [];
|
||||
/**
|
||||
* Extra react component to add to core components
|
||||
*
|
||||
* Interface to augment core components of the web UI. Setting a
|
||||
* component as one of the supported properties of this object will
|
||||
* cause the web UI to render it when displaying that component,
|
||||
* usually at the end. Each component will receive a `plugin` param
|
||||
* which is the instance of the web plugin that contained the
|
||||
* component extra.
|
||||
*/
|
||||
componentExtra: {
|
||||
/** Placed at the end of the controller page. */
|
||||
ControllerPage?: React["ComponentType"],
|
||||
/** Placed at the end of the hosts list page. */
|
||||
HostsPage?: React["ComponentType"],
|
||||
/**
|
||||
* Placed at the end of each host page. Takes a `host` param which
|
||||
* is the host the page is displayed for.
|
||||
*/
|
||||
HostViewPage?: React["ComponentType"],
|
||||
/** Placed at the end of the instance list page. */
|
||||
InstancesPage?: React["ComponentType"],
|
||||
/**
|
||||
* Placed at the end of each instance page. Takes an `instance`
|
||||
* param which is the instance the page is displayed for.
|
||||
*/
|
||||
InstanceViewPage?: React["ComponentType"],
|
||||
/** Placed at the end of the users list page. */
|
||||
UsersPage?: React["ComponentType"],
|
||||
/**
|
||||
* Placed at the end of each user page. Takes a `user` param which
|
||||
* is the user object the page is displayed for.
|
||||
*/
|
||||
UserViewPage?: React["ComponentType"],
|
||||
/** Placed at the end of the roles list page. */
|
||||
RolesPage?: React["ComponentType"],
|
||||
/**
|
||||
* Placed at the end of each role page. Takes a `role` param which
|
||||
* is the role object the page is displayed for.
|
||||
*/
|
||||
RoleViewPage?: React["ComponentType"],
|
||||
} = {};
|
||||
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* Webpack container for this plugin
|
||||
*/
|
||||
public container: any,
|
||||
packageData: any,
|
||||
/**
|
||||
* The plugin's own info module
|
||||
*/
|
||||
public info: PluginWebpackEnvInfo,
|
||||
logger: Logger,
|
||||
) {
|
||||
this.package = packageData;
|
||||
this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called immediately after the class is instantiated.
|
||||
*/
|
||||
async init() { }
|
||||
|
||||
/**
|
||||
* Called when an event on the controller connection happens
|
||||
*
|
||||
* The event param may be one of connect, drop, resume and close and has
|
||||
* the following meaning:
|
||||
*
|
||||
* ##### connect
|
||||
*
|
||||
* Invoked when a new connection to the controller has been established.
|
||||
*
|
||||
* ##### drop
|
||||
*
|
||||
* Invoked when a connection loss is detected between the control link
|
||||
* and the controller. Plugins should respond to this event by throtteling
|
||||
* messages it is sending to the controller to an absolute minimum.
|
||||
*
|
||||
* Messages sent over a dropped controller connection will get queued up in
|
||||
* memory in the browser and sent all in one go when the connection is
|
||||
* re-established again.
|
||||
*
|
||||
* ##### resume
|
||||
*
|
||||
* Invoked when the connection that had previously dropped is
|
||||
* re-established.
|
||||
*
|
||||
* ##### close
|
||||
*
|
||||
* Invoked when the connection to the controller has been closed. This
|
||||
* typically means the controller has shut down. Plugins should not
|
||||
* send any messages that goes to or via the controller after the
|
||||
* connection has been closed and before a new one is established.
|
||||
*
|
||||
* @param event - one of connect, drop, resume and close
|
||||
*/
|
||||
onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the given hook on all plugins
|
||||
@@ -867,7 +62,7 @@ export class BaseWebPlugin {
|
||||
* @param args - Arguments to pass on to the hook.
|
||||
* @returns Non-undefined return values from the hooks.
|
||||
*/
|
||||
export async function invokeHook(plugins: Map<string, BasePlugin>, hook: string, ...args: any[]) {
|
||||
export async function invokeHook(plugins: Map<string, any>, hook: string, ...args: any[]) {
|
||||
let results: unknown[] = [];
|
||||
for (let [name, plugin] of plugins) {
|
||||
try {
|
||||
|
||||
@@ -52,65 +52,22 @@ export async function loadPluginInfos(pluginList: Map<string, string>) {
|
||||
return plugins;
|
||||
}
|
||||
|
||||
function loadPluginClass(
|
||||
entrypointName: "controllerEntrypoint"|"instanceEntrypoint"|"controlEntrypoint",
|
||||
export async function loadPluginClass<Class extends { new (...args: any): any }>(
|
||||
pluginName: string,
|
||||
requirePath: string,
|
||||
className: string,
|
||||
pluginClass: any,
|
||||
pluginInfo: libPlugin.PluginNodeEnvInfo
|
||||
) {
|
||||
let resolvedPath = path.posix.join(pluginInfo.requirePath, pluginInfo[entrypointName]!);
|
||||
let entrypoint = require(resolvedPath);
|
||||
pluginClass: Class,
|
||||
): Promise<Class> {
|
||||
let entrypoint = require(requirePath);
|
||||
if (!entrypoint[className]) {
|
||||
throw new libErrors.PluginError(pluginInfo.name,
|
||||
new Error(`Expected ${resolvedPath} to export a class named ${className}`)
|
||||
throw new libErrors.PluginError(pluginName,
|
||||
new Error(`Expected ${requirePath} 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}`)
|
||||
throw new libErrors.PluginError(pluginName,
|
||||
new Error(`Expected ${className} exported from ${requirePath} to be a subclass of ${pluginClass.name}`)
|
||||
);
|
||||
}
|
||||
return entrypoint[className];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load controller plugin class of a plugin
|
||||
*
|
||||
* @param pluginInfo -
|
||||
* Plugin info object returned from {@link
|
||||
* loadPluginInfos} to load class from.
|
||||
* @returns plugin class
|
||||
*/
|
||||
export async function loadControllerPluginClass(
|
||||
pluginInfo: libPlugin.PluginNodeEnvInfo
|
||||
): Promise<typeof libPlugin.BaseControllerPlugin> {
|
||||
return loadPluginClass("controllerEntrypoint", "ControllerPlugin", libPlugin.BaseControllerPlugin, pluginInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load instance plugin class of a plugin
|
||||
*
|
||||
* @param pluginInfo -
|
||||
* Plugin info object returned from {@link
|
||||
* loadPluginInfos} to load class from.
|
||||
* @returns plugin class
|
||||
*/
|
||||
export async function loadInstancePluginClass(
|
||||
pluginInfo: libPlugin.PluginNodeEnvInfo
|
||||
): Promise<typeof libPlugin.BaseInstancePlugin> {
|
||||
return loadPluginClass("instanceEntrypoint", "InstancePlugin", libPlugin.BaseInstancePlugin, pluginInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load control plugin class of a plugin
|
||||
*
|
||||
* @param pluginInfo -
|
||||
* Plugin info object returned from {@link
|
||||
* loadPluginInfos} to load class from.
|
||||
* @returns plugin class
|
||||
*/
|
||||
export async function loadControlPluginClass(
|
||||
pluginInfo: libPlugin.PluginNodeEnvInfo
|
||||
): Promise<typeof libPlugin.BaseControlPlugin> {
|
||||
return loadPluginClass("controlEntrypoint", "ControlPlugin", libPlugin.BaseControlPlugin, pluginInfo);
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import type React from "react";
|
||||
import type { AccountRole, Link, Logger, PluginWebpackEnvInfo } from "@clusterio/lib";
|
||||
|
||||
/**
|
||||
* Plugin supplied login form
|
||||
*/
|
||||
export interface PluginLoginForm {
|
||||
/**
|
||||
* Internal name of the login form, this should start with the
|
||||
* plugin name followed by a dot.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/** Name displayed above this form in the login window. */
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* React component that's rendered for this login form. This is
|
||||
* supplied the setToken function via its props which should be called
|
||||
* when an authentication token is aquired via this form.
|
||||
*/
|
||||
Component: typeof React.Component;
|
||||
};
|
||||
|
||||
export type UserAccount = {
|
||||
/** Name of the currently logged in account. */
|
||||
name: string;
|
||||
/** Roles of the corrently logged in account. */
|
||||
roles: AccountRole[];
|
||||
/** Check if the currently logged in account has the given permission. */
|
||||
hasPermission: (permission: string) => boolean | null;
|
||||
/** Check if the currently logged in account has any of the given permissions. */
|
||||
hasAnyPermission: (...permissions: string[]) => boolean | null;
|
||||
/** Check if the currently logged in account has all of given permissions. */
|
||||
hasAllPermission: (...permissions: string[]) => boolean | null;
|
||||
/** Logs out of the web interface. */
|
||||
logOut: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin supplied pages
|
||||
*/
|
||||
export interface PluginPage {
|
||||
/** URL path to this page. */
|
||||
path: string;
|
||||
/**
|
||||
* If present and this path matches one of the pages in the sidebar it
|
||||
* will cause that sidebar entry to be highlighted as active.
|
||||
*/
|
||||
sidebarPath?: string;
|
||||
/**
|
||||
* If present group this entry under a group of the given name in the
|
||||
* sidebar.
|
||||
*/
|
||||
sidebarGroup?: string;
|
||||
/**
|
||||
* If present creates an entry in the sidebar for this page with the
|
||||
* given text.
|
||||
*/
|
||||
sidebarName?: string;
|
||||
/**
|
||||
* A react node which is rendered when this page is navigated to.
|
||||
* Should render a PageLayout.
|
||||
*/
|
||||
content?: React.ReactElement;
|
||||
/**
|
||||
* Permission to access page. function are expected to throw an error if access is deny.
|
||||
*/
|
||||
permission?: string | ((account: UserAccount) => (boolean|null));
|
||||
};
|
||||
|
||||
/**
|
||||
* Base class for web interface plugins
|
||||
*/
|
||||
export default class BaseWebPlugin {
|
||||
/**
|
||||
* Contents of the plugin's package.json file
|
||||
*/
|
||||
package: any;
|
||||
/**
|
||||
* Control link to the controller, not available until the
|
||||
* connect event in onControllerConnectionEvent is signaled.
|
||||
*/
|
||||
control?: Link;
|
||||
/**
|
||||
* Logger for this plugin
|
||||
*
|
||||
* Instance of winston Logger for sending log messages from this
|
||||
* plugin. Supported methods and their corresponding log levels are
|
||||
* `error`, `warn`, `audit`, `info` and `verbose`.
|
||||
*/
|
||||
logger: Logger;
|
||||
/**
|
||||
* List of login forms provided by this plugin
|
||||
*/
|
||||
loginForms: PluginLoginForm[] = [];
|
||||
/**
|
||||
* List of pages provided by this plugin
|
||||
*/
|
||||
pages: PluginPage[] = [];
|
||||
/**
|
||||
* Extra react component to add to core components
|
||||
*
|
||||
* Interface to augment core components of the web UI. Setting a
|
||||
* component as one of the supported properties of this object will
|
||||
* cause the web UI to render it when displaying that component,
|
||||
* usually at the end. Each component will receive a `plugin` param
|
||||
* which is the instance of the web plugin that contained the
|
||||
* component extra.
|
||||
*/
|
||||
componentExtra: {
|
||||
/** Placed at the end of the controller page. */
|
||||
ControllerPage?: React.ComponentType,
|
||||
/** Placed at the end of the hosts list page. */
|
||||
HostsPage?: React.ComponentType,
|
||||
/**
|
||||
* Placed at the end of each host page. Takes a `host` param which
|
||||
* is the host the page is displayed for.
|
||||
*/
|
||||
HostViewPage?: React.ComponentType,
|
||||
/** Placed at the end of the instance list page. */
|
||||
InstancesPage?: React.ComponentType,
|
||||
/**
|
||||
* Placed at the end of each instance page. Takes an `instance`
|
||||
* param which is the instance the page is displayed for.
|
||||
*/
|
||||
InstanceViewPage?: React.ComponentType,
|
||||
/** Placed at the end of the users list page. */
|
||||
UsersPage?: React.ComponentType,
|
||||
/**
|
||||
* Placed at the end of each user page. Takes a `user` param which
|
||||
* is the user object the page is displayed for.
|
||||
*/
|
||||
UserViewPage?: React.ComponentType,
|
||||
/** Placed at the end of the roles list page. */
|
||||
RolesPage?: React.ComponentType,
|
||||
/**
|
||||
* Placed at the end of each role page. Takes a `role` param which
|
||||
* is the role object the page is displayed for.
|
||||
*/
|
||||
RoleViewPage?: React.ComponentType,
|
||||
} = {};
|
||||
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* Webpack container for this plugin
|
||||
*/
|
||||
public container: any,
|
||||
packageData: any,
|
||||
/**
|
||||
* The plugin's own info module
|
||||
*/
|
||||
public info: PluginWebpackEnvInfo,
|
||||
logger: Logger,
|
||||
) {
|
||||
this.package = packageData;
|
||||
this.logger = logger.child({ plugin: this.info.name }) as unknown as Logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called immediately after the class is instantiated.
|
||||
*/
|
||||
async init() { }
|
||||
|
||||
/**
|
||||
* Called when an event on the controller connection happens
|
||||
*
|
||||
* The event param may be one of connect, drop, resume and close and has
|
||||
* the following meaning:
|
||||
*
|
||||
* ##### connect
|
||||
*
|
||||
* Invoked when a new connection to the controller has been established.
|
||||
*
|
||||
* ##### drop
|
||||
*
|
||||
* Invoked when a connection loss is detected between the control link
|
||||
* and the controller. Plugins should respond to this event by throtteling
|
||||
* messages it is sending to the controller to an absolute minimum.
|
||||
*
|
||||
* Messages sent over a dropped controller connection will get queued up in
|
||||
* memory in the browser and sent all in one go when the connection is
|
||||
* re-established again.
|
||||
*
|
||||
* ##### resume
|
||||
*
|
||||
* Invoked when the connection that had previously dropped is
|
||||
* re-established.
|
||||
*
|
||||
* ##### close
|
||||
*
|
||||
* Invoked when the connection to the controller has been closed. This
|
||||
* typically means the controller has shut down. Plugins should not
|
||||
* send any messages that goes to or via the controller after the
|
||||
* connection has been closed and before a new one is established.
|
||||
*
|
||||
* @param event - one of connect, drop, resume and close
|
||||
*/
|
||||
onControllerConnectionEvent(event: "connect" | "drop" | "resume" | "close") { }
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import * as lib from "@clusterio/lib";
|
||||
import type { PluginWebApi } from "@clusterio/controller/src/routes";
|
||||
|
||||
import App from "./components/App";
|
||||
import BaseWebPlugin from "./BaseWebPlugin";
|
||||
import { Control, ControlConnector } from "./util/websocket";
|
||||
|
||||
const { ConsoleTransport, WebConsoleFormat, logger } = lib;
|
||||
@@ -74,7 +75,7 @@ async function loadPlugins(pluginInfos: lib.PluginWebpackEnvInfo[]) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
let WebPluginClass = lib.BaseWebPlugin;
|
||||
let WebPluginClass = BaseWebPlugin;
|
||||
if (pluginInfo.webEntrypoint) {
|
||||
let webModule = (await pluginInfo.container.get(pluginInfo.webEntrypoint))();
|
||||
if (!webModule.WebPlugin) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useContext } from "react";
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import BaseWebPlugin from "../BaseWebPlugin";
|
||||
import { RawUserState } from "../model/user";
|
||||
import { HostState } from "../model/host";
|
||||
import { InstanceState } from "../model/instance";
|
||||
@@ -22,7 +23,7 @@ export default function PluginExtra(props: PluginExtraProps) {
|
||||
let components = [];
|
||||
for (let [name, plugin] of control.plugins) {
|
||||
if (Object.prototype.hasOwnProperty.call(plugin.componentExtra, props.component)) {
|
||||
type ComponentExtra = {[key: string]: React.ComponentType<PluginExtraProps & {plugin:lib.BaseWebPlugin}>};
|
||||
type ComponentExtra = {[key: string]: React.ComponentType<PluginExtraProps & {plugin:BaseWebPlugin}>};
|
||||
let ComponentExtra = (plugin.componentExtra as ComponentExtra)[props.component];
|
||||
components.push(<ComponentExtra key={name} plugin={plugin} {...props}/>);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default as bootstrap } from "./bootstrap";
|
||||
export { default as notify, notifyErrorHandler } from "./util/notify";
|
||||
export { default as BaseWebPlugin } from "./BaseWebPlugin";
|
||||
|
||||
export * from "./model/instance";
|
||||
export * from "./model/host";
|
||||
@@ -18,9 +19,8 @@ export { default as LogConsole } from "./components/LogConsole";
|
||||
export { default as AssignInstanceModal } from "./components/AssignInstanceModal";
|
||||
export { default as InstanceList } from "./components/InstanceList";
|
||||
export { default as InstanceRcon } from "./components/InstanceRcon";
|
||||
export { default as InstanceStatusTag } from "./components/InstanceStatusTag";
|
||||
export { default as InstanceStatusTag, statusColors } from "./components/InstanceStatusTag";
|
||||
export { default as StartStopInstanceButton } from "./components/StartStopInstanceButton";
|
||||
export { statusColors } from "./components/InstanceStatusTag";
|
||||
|
||||
export { default as PageHeader } from "./components/PageHeader";
|
||||
export { default as PageLayout } from "./components/PageLayout";
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { UserAccount } from "../BaseWebPlugin";
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
import ControlContext from "../components/ControlContext";
|
||||
|
||||
|
||||
export function useAccount(): lib.UserAccount {
|
||||
export function useAccount(): UserAccount {
|
||||
let control = useContext(ControlContext);
|
||||
let [name, setName] = useState<string>(control.accountName || "");
|
||||
let [roles, setRoles] = useState<any[]>(control.accountRoles || []);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { ReactElement } from "react";
|
||||
|
||||
import type * as lib from "@clusterio/lib";
|
||||
import { PluginPage, UserAccount } from "./BaseWebPlugin";
|
||||
|
||||
import ControllerPage from "./components/ControllerPage";
|
||||
import HostsPage from "./components/HostsPage";
|
||||
@@ -16,7 +16,7 @@ import RoleViewPage from "./components/RoleViewPage";
|
||||
import PluginsPage from "./components/PluginsPage";
|
||||
import PluginViewPage from "./components/PluginViewPage";
|
||||
|
||||
export const pages: lib.PluginPage[] = [
|
||||
export const pages: PluginPage[] = [
|
||||
{
|
||||
path: "/controller",
|
||||
sidebarName: "Controller",
|
||||
@@ -47,7 +47,7 @@ export const pages: lib.PluginPage[] = [
|
||||
{
|
||||
path: "/mods",
|
||||
sidebarName: "Mods",
|
||||
permission: (account:lib.UserAccount) => account.hasAnyPermission("core.mod.list", "core.mod-pack.list"),
|
||||
permission: (account: UserAccount) => account.hasAnyPermission("core.mod.list", "core.mod-pack.list"),
|
||||
content: <ModsPage />,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import packageJson from "../../package.json";
|
||||
import BaseWebPlugin from "../BaseWebPlugin";
|
||||
|
||||
const { logFilter, logger } = lib;
|
||||
|
||||
@@ -84,7 +85,7 @@ export class Control extends lib.Link {
|
||||
|
||||
constructor(
|
||||
connector: ControlConnector,
|
||||
public plugins: Map<string, lib.BaseWebPlugin>,
|
||||
public plugins: Map<string, BaseWebPlugin>,
|
||||
) {
|
||||
super(connector);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BaseControlPlugin, CommandTree, Command } from "@clusterio/lib";
|
||||
import { CommandTree, Command } from "@clusterio/lib";
|
||||
import { BaseCtlPlugin } from "@clusterio/ctl";
|
||||
import { ChatEvent } from "./messages";
|
||||
|
||||
const globalChatCommands = new CommandTree({
|
||||
@@ -13,7 +14,7 @@ globalChatCommands.add(new Command({
|
||||
},
|
||||
}));
|
||||
|
||||
export class ControlPlugin extends BaseControlPlugin {
|
||||
export class CtlPlugin extends BaseCtlPlugin {
|
||||
async addCommands(rootCommand: CommandTree) {
|
||||
rootCommand.add(globalChatCommands);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ const info: lib.PluginDeclaration = {
|
||||
title: "Global Chat",
|
||||
description: "Forwards chat between instances.",
|
||||
instanceEntrypoint: "dist/plugin/instance",
|
||||
controlEntrypoint: "dist/plugin/control",
|
||||
ctlEntrypoint: "dist/plugin/control",
|
||||
|
||||
messages: [
|
||||
ChatEvent,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { BaseInstancePlugin } from "@clusterio/host";
|
||||
import { ChatEvent } from "./messages";
|
||||
|
||||
/**
|
||||
@@ -11,7 +12,7 @@ function removeTags(content: string): string {
|
||||
return content.replace(/(\[gps=-?\d+,-?\d+\]|\[train=\d+\])/g, "");
|
||||
}
|
||||
|
||||
export class InstancePlugin extends lib.BaseInstancePlugin {
|
||||
export class InstancePlugin extends BaseInstancePlugin {
|
||||
messageQueue: string[] = [];
|
||||
|
||||
async init() {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type InstanceInfo from "@clusterio/controller/src/InstanceInfo";
|
||||
import type Controller from "@clusterio/controller/src/Controller";
|
||||
import { BaseControllerPlugin, type InstanceInfo } from "@clusterio/controller";
|
||||
import type { IpcPlayerData } from "./instance";
|
||||
|
||||
import fs from "fs-extra";
|
||||
@@ -36,7 +35,7 @@ async function saveDatabase(
|
||||
}
|
||||
}
|
||||
|
||||
export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
export class ControllerPlugin extends BaseControllerPlugin {
|
||||
acquiredPlayers!: Map<string, { instanceId: number, expires?: number }>;
|
||||
playerDatastore!: Map<string, IpcPlayerData>;
|
||||
autosaveId!: ReturnType<typeof setInterval>;
|
||||
@@ -48,7 +47,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
saveDatabase(this.controller.config, this.playerDatastore, this.logger).catch(err => {
|
||||
this.logger.error(`Unexpected error autosaving player data:\n${err.stack}`);
|
||||
});
|
||||
}, this.controller.config.get("inventory_sync.autosave_interval") * 1000);
|
||||
}, this.controller.config.get("inventory_sync.autosave_interval") as number * 1000);
|
||||
|
||||
this.controller.handle(msg.AcquireRequest, this.handleAcquireRequest.bind(this));
|
||||
this.controller.handle(msg.ReleaseRequest, this.handleReleaseRequest.bind(this));
|
||||
@@ -68,7 +67,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
}
|
||||
|
||||
if (["unknown", "stopped"].includes(instance.status)) {
|
||||
let timeout = this.controller.config.get("inventory_sync.player_lock_timeout") * 1000;
|
||||
let timeout = this.controller.config.get("inventory_sync.player_lock_timeout") as number * 1000;
|
||||
for (let acquisitonRecord of this.acquiredPlayers.values()) {
|
||||
if (acquisitonRecord.instanceId === instanceId && !acquisitonRecord.expires) {
|
||||
acquisitonRecord.expires = Date.now() + timeout;
|
||||
@@ -90,7 +89,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
if (
|
||||
!acquisitionRecord
|
||||
|| acquisitionRecord.instanceId === instanceId
|
||||
|| !this.controller.instances.has(acquisitionRecord.instanceId)
|
||||
|| !this.controller.instances!.has(acquisitionRecord.instanceId)
|
||||
|| acquisitionRecord.expires && acquisitionRecord.expires < Date.now()
|
||||
) {
|
||||
this.acquiredPlayers.set(playerName, { instanceId });
|
||||
@@ -104,7 +103,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
let { instanceId, playerName } = request;
|
||||
if (!this.acquire(instanceId, playerName)) {
|
||||
let acquisitionRecord = this.acquiredPlayers.get(playerName);
|
||||
let instance = this.controller.instances.get(acquisitionRecord!.instanceId);
|
||||
let instance = this.controller.instances!.get(acquisitionRecord!.instanceId)!;
|
||||
return {
|
||||
status: "busy",
|
||||
message: instance.config.get("instance.name"),
|
||||
@@ -133,7 +132,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
|
||||
async handleUploadRequest(request: msg.UploadRequest) {
|
||||
let { instanceId, playerName, playerData } = request;
|
||||
let instanceName = this.controller.instances.get(instanceId).config.get("instance.name");
|
||||
let instanceName = this.controller.instances!.get(instanceId)!.config.get("instance.name");
|
||||
let store = true;
|
||||
let acquisitionRecord = this.acquiredPlayers.get(playerName);
|
||||
if (!acquisitionRecord) {
|
||||
@@ -167,7 +166,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
|
||||
async handleDownloadRequest(request: msg.DownloadRequest) {
|
||||
let { instanceId, playerName } = request;
|
||||
let instanceName = this.controller.instances.get(instanceId).config.get("instance.name");
|
||||
let instanceName = this.controller.instances!.get(instanceId)!.config.get("instance.name");
|
||||
|
||||
let acquisitionRecord = this.acquiredPlayers.get(playerName);
|
||||
if (!acquisitionRecord) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { BaseInstancePlugin } from "@clusterio/host";
|
||||
import {
|
||||
AcquireRequest, AcquireResponse, ReleaseRequest, UploadRequest, DownloadRequest, DownloadResponse,
|
||||
} from "./messages";
|
||||
@@ -44,7 +45,7 @@ function chunkify(chunkSize: number, string: string): string[] {
|
||||
return string.match(new RegExp(`.{1,${chunkSize}}`, "g")) || [];
|
||||
}
|
||||
|
||||
export class InstancePlugin extends lib.BaseInstancePlugin {
|
||||
export class InstancePlugin extends BaseInstancePlugin {
|
||||
playersToRelease!: Set<string>;
|
||||
disconnecting!: boolean;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { PageLayout, ControlContext } from "@clusterio/web_ui";
|
||||
import { BaseWebPlugin, PageLayout, ControlContext } from "@clusterio/web_ui";
|
||||
import { DatabaseStatsRequest } from "../dist/plugin/messages";
|
||||
|
||||
import "./index.css";
|
||||
@@ -28,7 +27,7 @@ function InventoryPage() {
|
||||
</PageLayout>;
|
||||
}
|
||||
|
||||
export class WebPlugin extends lib.BaseWebPlugin {
|
||||
export class WebPlugin extends BaseWebPlugin {
|
||||
async init() {
|
||||
this.pages = [
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ import util from "util";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { BaseControllerPlugin } from "@clusterio/controller";
|
||||
const { basicType } = lib;
|
||||
|
||||
import { FetchPlayerCodeRequest, SetVerifyCodeRequest } from "./messages";
|
||||
@@ -27,7 +28,7 @@ async function generateCode(length: number): Promise<string> {
|
||||
|
||||
type PlayerCode = { playerCode: string, verifyCode: string | null, expires: number };
|
||||
|
||||
export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
export class ControllerPlugin extends BaseControllerPlugin {
|
||||
players!: Map<string, PlayerCode>;
|
||||
|
||||
async init() {
|
||||
@@ -46,7 +47,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
|
||||
this.controller.app.get("/api/player_auth/servers", (req: Request, res: Response) => {
|
||||
let servers = [];
|
||||
for (let instance of this.controller.instances.values()) {
|
||||
for (let instance of this.controller.instances!.values()) {
|
||||
if (instance.status === "running" && instance.config.get("player_auth.load_plugin")) {
|
||||
servers.push(instance.config.get("factorio.settings")["name"] || "unnamed server");
|
||||
}
|
||||
@@ -88,7 +89,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
|
||||
for (let entry of this.players.values()) {
|
||||
if (entry.playerCode === playerCode && entry.expires > Date.now()) {
|
||||
let verifyCode = await generateCode(this.controller.config.get("player_auth.code_length"));
|
||||
let verifyCode = await generateCode(this.controller.config.get("player_auth.code_length") as number);
|
||||
let secret = Buffer.from(this.controller.config.get("controller.auth_secret"), "base64");
|
||||
let verifyToken = jwt.sign(
|
||||
{
|
||||
@@ -172,8 +173,8 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
}
|
||||
|
||||
async handleFetchPlayerCodeRequest(request: FetchPlayerCodeRequest) {
|
||||
let playerCode = await generateCode(this.controller.config.get("player_auth.code_length"));
|
||||
let expires = Date.now() + this.controller.config.get("player_auth.code_timeout") * 1000;
|
||||
let playerCode = await generateCode(this.controller.config.get("player_auth.code_length") as number);
|
||||
let expires = Date.now() + (this.controller.config.get("player_auth.code_timeout") as number) * 1000;
|
||||
this.players.set(request.player, { playerCode, verifyCode: null, expires });
|
||||
return { player_code: playerCode, controller_url: this.controller.getControllerUrl() };
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { BaseInstancePlugin } from "@clusterio/host";
|
||||
|
||||
import { FetchPlayerCodeRequest, SetVerifyCodeRequest } from "./messages";
|
||||
|
||||
@@ -11,7 +11,7 @@ type IpcPlayerAuth = {
|
||||
verify_code: string,
|
||||
}
|
||||
|
||||
export class InstancePlugin extends lib.BaseInstancePlugin {
|
||||
export class InstancePlugin extends BaseInstancePlugin {
|
||||
async init() {
|
||||
if (!this.instance.config.get("factorio.enable_save_patching")) {
|
||||
throw new Error("player_auth plugin requires save patching.");
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { notifyErrorHandler } from "@clusterio/web_ui";
|
||||
import { BaseWebPlugin, notifyErrorHandler } from "@clusterio/web_ui";
|
||||
import { Button, Form, Input, Spin, Typography } from "antd";
|
||||
|
||||
import "./index.css";
|
||||
@@ -146,7 +145,7 @@ function LoginForm(props) {
|
||||
}
|
||||
|
||||
|
||||
export class WebPlugin extends lib.BaseWebPlugin {
|
||||
export class WebPlugin extends BaseWebPlugin {
|
||||
async init() {
|
||||
this.logger.info("Player Auth init");
|
||||
this.loginForms = [{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import { BaseControllerPlugin } from "@clusterio/controller";
|
||||
import { Static } from "@sinclair/typebox";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
@@ -48,7 +49,7 @@ async function saveTechnologies(
|
||||
await lib.safeOutputFile(filePath, JSON.stringify([...technologies.entries()], null, "\t"));
|
||||
}
|
||||
|
||||
export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
export class ControllerPlugin extends BaseControllerPlugin {
|
||||
technologies!: Map<string, Technology>;
|
||||
progressRateLimiter!: lib.RateLimiter;
|
||||
progressBroadcastId!: ReturnType<typeof setInterval> | null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { BaseInstancePlugin } from "@clusterio/host";
|
||||
import {
|
||||
ContributionEvent,
|
||||
ProgressEvent,
|
||||
@@ -18,7 +19,7 @@ type IpcFinished = {
|
||||
level: number,
|
||||
};
|
||||
|
||||
export class InstancePlugin extends lib.BaseInstancePlugin {
|
||||
export class InstancePlugin extends BaseInstancePlugin {
|
||||
syncStarted!: boolean;
|
||||
|
||||
unexpectedError(err: Error) {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { BaseInstancePlugin } from "@clusterio/host";
|
||||
|
||||
const { Gauge } = lib;
|
||||
|
||||
type IpcStats = {
|
||||
@@ -59,7 +61,7 @@ function setForceFlowStatistic(
|
||||
}
|
||||
|
||||
|
||||
export class InstancePlugin extends lib.BaseInstancePlugin {
|
||||
export class InstancePlugin extends BaseInstancePlugin {
|
||||
async init() {
|
||||
if (!this.instance.config.get("factorio.enable_save_patching")) {
|
||||
throw new Error("statistics_exporter plugin requires save patching.");
|
||||
@@ -107,7 +109,7 @@ export class InstancePlugin extends lib.BaseInstancePlugin {
|
||||
// take a long time for the command to go through if the command
|
||||
// stream is overloaded. Should the timeout be exceeded the
|
||||
// previous values for the metrics will end up being sent to controller.
|
||||
let timeout = this.instance.config.get("statistics_exporter.command_timeout") * 1000;
|
||||
let timeout = this.instance.config.get("statistics_exporter.command_timeout") as number * 1000;
|
||||
await lib.timeout(this.gatherMetrics(), timeout, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type ControlConnection from "@clusterio/controller/src/ControlConnection";
|
||||
import { BaseControllerPlugin, type ControlConnection } from "@clusterio/controller";
|
||||
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
@@ -70,7 +70,7 @@ async function saveDatabase(
|
||||
}
|
||||
}
|
||||
|
||||
export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
export class ControllerPlugin extends BaseControllerPlugin {
|
||||
items!: lib.ItemDatabase;
|
||||
itemUpdateRateLimiter!: lib.RateLimiter;
|
||||
itemsLastUpdate!: Map<string, number>;
|
||||
@@ -96,7 +96,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
saveDatabase(this.controller.config, this.items, this.logger).catch(err => {
|
||||
this.logger.error(`Unexpected error autosaving items:\n${err.stack}`);
|
||||
});
|
||||
}, this.controller.config.get("subspace_storage.autosave_interval") * 1000);
|
||||
}, this.controller.config.get("subspace_storage.autosave_interval") as number * 1000);
|
||||
|
||||
this.neuralDole = new dole.NeuralDole({ items: this.items });
|
||||
this.doleMagicId = setInterval(() => {
|
||||
@@ -180,7 +180,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
}
|
||||
|
||||
} else {
|
||||
let instance = this.controller.instances.get(instanceId);
|
||||
let instance = this.controller.instances!.get(instanceId);
|
||||
let instanceName = instance ? instance.config.get("instance.name") : "unkonwn";
|
||||
|
||||
// use fancy neural net to calculate a "fair" dole division rate.
|
||||
@@ -200,7 +200,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
let count = dole.doleDivider({
|
||||
object: { name: item.name, count: item.count, instanceId, instanceName },
|
||||
items: this.items,
|
||||
logItemTransfers: this.controller.config.get("subspace_storage.log_item_transfers"),
|
||||
logItemTransfers: this.controller.config.get("subspace_storage.log_item_transfers") as boolean,
|
||||
logger: this.logger,
|
||||
});
|
||||
if (count > 0) {
|
||||
@@ -230,7 +230,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
||||
}
|
||||
|
||||
async handleSetStorageSubscriptionRequest(request: SetStorageSubscriptionRequest, src: lib.Address) {
|
||||
let link = this.controller.wsServer.controlConnections.get(src.id);
|
||||
let link = this.controller.wsServer.controlConnections.get(src.id)!;
|
||||
if (request.storage) {
|
||||
this.subscribedControlLinks.add(link);
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { BaseInstancePlugin } from "@clusterio/host";
|
||||
|
||||
import {
|
||||
PlaceEvent,
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
|
||||
type IpcItems = [string, number][];
|
||||
|
||||
export class InstancePlugin extends lib.BaseInstancePlugin {
|
||||
export class InstancePlugin extends BaseInstancePlugin {
|
||||
pendingTasks!: Set<any>;
|
||||
pingId?: ReturnType<typeof setTimeout>;
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ import React, { useContext, useEffect, useState } from "react";
|
||||
import { Input, Table, Typography } from "antd";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
import { notifyErrorHandler, useItemMetadata, useLocale, PageLayout, ControlContext } from "@clusterio/web_ui";
|
||||
import {
|
||||
BaseWebPlugin, PageLayout, ControlContext,
|
||||
notifyErrorHandler, useItemMetadata, useLocale,
|
||||
} from "@clusterio/web_ui";
|
||||
import { GetStorageRequest, SetStorageSubscriptionRequest } from "../dist/plugin/messages";
|
||||
|
||||
import "./index.css";
|
||||
@@ -114,7 +117,7 @@ function StoragePage() {
|
||||
</PageLayout>;
|
||||
}
|
||||
|
||||
export class WebPlugin extends lib.BaseWebPlugin {
|
||||
export class WebPlugin extends BaseWebPlugin {
|
||||
async init() {
|
||||
this.pages = [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
const mock = require("../mock");
|
||||
const controller = require("@clusterio/controller");
|
||||
|
||||
describe("controller/src/BaseControllerPlugin", function() {
|
||||
describe("class BaseControllerPlugin", function() {
|
||||
let controllerPlugin;
|
||||
it("should be constructible", async function() {
|
||||
controllerPlugin = new controller.BaseControllerPlugin({}, {}, {}, new mock.MockLogger());
|
||||
await controllerPlugin.init();
|
||||
});
|
||||
it("should define defaults for hooks", async function() {
|
||||
await controllerPlugin.onInstanceStatusChanged({}, "running", "initialized");
|
||||
await controllerPlugin.onControllerConfigFieldChanged({}, "foo", null);
|
||||
await controllerPlugin.onInstanceConfigFieldChanged({}, {}, "foo", null);
|
||||
await controllerPlugin.onMetrics();
|
||||
await controllerPlugin.onShutdown();
|
||||
controllerPlugin.onHostConnectionEvent({}, "connect");
|
||||
await controllerPlugin.onPrepareHostDisconnect({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
"use strict";
|
||||
const assert = require("assert").strict;
|
||||
const mock = require("../mock");
|
||||
|
||||
const host = require("@clusterio/host");
|
||||
|
||||
describe("host/src/BaseInstancePlugin", function() {
|
||||
describe("class BaseInstancePlugin", function() {
|
||||
let instancePlugin;
|
||||
it("should be constructible", async function() {
|
||||
instancePlugin = new host.BaseInstancePlugin({}, new mock.MockInstance(), {});
|
||||
await instancePlugin.init();
|
||||
});
|
||||
it("should define defaults for hooks", async function() {
|
||||
await instancePlugin.onMetrics();
|
||||
await instancePlugin.onStart();
|
||||
await instancePlugin.onStop();
|
||||
instancePlugin.onExit();
|
||||
await instancePlugin.onOutput({});
|
||||
instancePlugin.onControllerConnectionEvent("connect");
|
||||
await instancePlugin.onPrepareControllerDisconnect();
|
||||
});
|
||||
describe("sendRcon", function() {
|
||||
it("should send commands out of order", async function() {
|
||||
instancePlugin.instance.server.rconCommandResults.set("a", { time: 100, response: "a" });
|
||||
instancePlugin.instance.server.rconCommandResults.set("b", { time: 50, response: "b" });
|
||||
|
||||
let a = instancePlugin.sendRcon("a");
|
||||
let b = instancePlugin.sendRcon("b");
|
||||
let result = await Promise.race([a, b]);
|
||||
assert.equal(result, "b");
|
||||
});
|
||||
it("should propagate errors", async function() {
|
||||
instancePlugin.instance.server.rconCommandResults.set("a", new Error("ref"));
|
||||
await assert.rejects(instancePlugin.sendRcon("a"), new Error("ref"));
|
||||
});
|
||||
});
|
||||
describe("sendOrderedRcon", function() {
|
||||
it("should send commands in order", async function() {
|
||||
instancePlugin.instance.server.rconCommandResults.set("a", { time: 100, response: "a" });
|
||||
instancePlugin.instance.server.rconCommandResults.set("b", { time: 50, response: "b" });
|
||||
|
||||
let a = instancePlugin.sendOrderedRcon("a");
|
||||
let b = instancePlugin.sendOrderedRcon("b");
|
||||
let result = await Promise.race([a, b]);
|
||||
assert.equal(result, "a");
|
||||
});
|
||||
it("should propagate errors", async function() {
|
||||
instancePlugin.instance.server.rconCommandResults.set("a", new Error("ref"));
|
||||
await assert.rejects(instancePlugin.sendOrderedRcon("a"), new Error("ref"));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,70 +6,6 @@ const lib = require("@clusterio/lib");
|
||||
|
||||
|
||||
describe("lib/plugin", function() {
|
||||
describe("class BaseInstancePlugin", function() {
|
||||
let instancePlugin;
|
||||
it("should be constructible", async function() {
|
||||
instancePlugin = new lib.BaseInstancePlugin({}, new mock.MockInstance(), {});
|
||||
await instancePlugin.init();
|
||||
});
|
||||
it("should define defaults for hooks", async function() {
|
||||
await instancePlugin.onMetrics();
|
||||
await instancePlugin.onStart();
|
||||
await instancePlugin.onStop();
|
||||
instancePlugin.onExit();
|
||||
await instancePlugin.onOutput({});
|
||||
instancePlugin.onControllerConnectionEvent("connect");
|
||||
await instancePlugin.onPrepareControllerDisconnect();
|
||||
});
|
||||
describe("sendRcon", function() {
|
||||
it("should send commands out of order", async function() {
|
||||
instancePlugin.instance.server.rconCommandResults.set("a", { time: 100, response: "a" });
|
||||
instancePlugin.instance.server.rconCommandResults.set("b", { time: 50, response: "b" });
|
||||
|
||||
let a = instancePlugin.sendRcon("a");
|
||||
let b = instancePlugin.sendRcon("b");
|
||||
let result = await Promise.race([a, b]);
|
||||
assert.equal(result, "b");
|
||||
});
|
||||
it("should propagate errors", async function() {
|
||||
instancePlugin.instance.server.rconCommandResults.set("a", new Error("ref"));
|
||||
await assert.rejects(instancePlugin.sendRcon("a"), new Error("ref"));
|
||||
});
|
||||
});
|
||||
describe("sendOrderedRcon", function() {
|
||||
it("should send commands in order", async function() {
|
||||
instancePlugin.instance.server.rconCommandResults.set("a", { time: 100, response: "a" });
|
||||
instancePlugin.instance.server.rconCommandResults.set("b", { time: 50, response: "b" });
|
||||
|
||||
let a = instancePlugin.sendOrderedRcon("a");
|
||||
let b = instancePlugin.sendOrderedRcon("b");
|
||||
let result = await Promise.race([a, b]);
|
||||
assert.equal(result, "a");
|
||||
});
|
||||
it("should propagate errors", async function() {
|
||||
instancePlugin.instance.server.rconCommandResults.set("a", new Error("ref"));
|
||||
await assert.rejects(instancePlugin.sendOrderedRcon("a"), new Error("ref"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("class BaseControllerPlugin", function() {
|
||||
let controllerPlugin;
|
||||
it("should be constructible", async function() {
|
||||
controllerPlugin = new lib.BaseControllerPlugin({}, {}, {}, new mock.MockLogger());
|
||||
await controllerPlugin.init();
|
||||
});
|
||||
it("should define defaults for hooks", async function() {
|
||||
await controllerPlugin.onInstanceStatusChanged({}, "running", "initialized");
|
||||
await controllerPlugin.onControllerConfigFieldChanged({}, "foo", null);
|
||||
await controllerPlugin.onInstanceConfigFieldChanged({}, {}, "foo", null);
|
||||
await controllerPlugin.onMetrics();
|
||||
await controllerPlugin.onShutdown();
|
||||
controllerPlugin.onHostConnectionEvent({}, "connect");
|
||||
await controllerPlugin.onPrepareHostDisconnect({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("invokeHook()", function() {
|
||||
let betaTestCalled = false;
|
||||
let plugins = new Map([
|
||||
|
||||
+14
-11
@@ -4,6 +4,7 @@ const fs = require("fs-extra");
|
||||
const path = require("path");
|
||||
|
||||
const lib = require("@clusterio/lib");
|
||||
const { BaseControllerPlugin } = require("@clusterio/controller");
|
||||
const { escapeRegExp } = lib;
|
||||
|
||||
|
||||
@@ -64,7 +65,7 @@ describe("lib/plugin_loader", function() {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("loadControllerPluginClass()", function() {
|
||||
describe("loadPluginClass()", 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");
|
||||
@@ -81,11 +82,12 @@ describe("lib/plugin_loader", function() {
|
||||
it("should throw if class is missing from entrypoint", async function() {
|
||||
const requirePath = path.resolve(missingClass);
|
||||
await assert.rejects(
|
||||
lib.loadControllerPluginClass({
|
||||
name: "test",
|
||||
controllerEntrypoint: "controller",
|
||||
requirePath,
|
||||
}),
|
||||
lib.loadPluginClass(
|
||||
"test",
|
||||
path.posix.join(requirePath, "controller"),
|
||||
"ControllerPlugin",
|
||||
BaseControllerPlugin,
|
||||
),
|
||||
{
|
||||
message:
|
||||
`PluginError: Expected ${path.posix.join(requirePath, "controller")} ` +
|
||||
@@ -96,11 +98,12 @@ describe("lib/plugin_loader", function() {
|
||||
it("should throw if class is not a subclass of BaseControllerPlugin", async function() {
|
||||
const requirePath = path.resolve(wrongParentClass);
|
||||
await assert.rejects(
|
||||
lib.loadControllerPluginClass({
|
||||
name: "test",
|
||||
controllerEntrypoint: "controller",
|
||||
requirePath,
|
||||
}),
|
||||
lib.loadPluginClass(
|
||||
"test",
|
||||
path.posix.join(requirePath, "controller"),
|
||||
"ControllerPlugin",
|
||||
BaseControllerPlugin,
|
||||
),
|
||||
{
|
||||
message:
|
||||
"PluginError: Expected ControllerPlugin exported from " +
|
||||
|
||||
Reference in new issue
Block a user