mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-16 07:24:53 +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.
|
Subclass of `PluginConfigGroup` for defining the controller configuration fields for this plugin.
|
||||||
See [Plugin Configuration](#plugin-configuration)
|
See [Plugin Configuration](#plugin-configuration)
|
||||||
|
|
||||||
**controlEntrypoint**:
|
**ctlEntrypoint**:
|
||||||
Path to a Node.js module relative to the plugin directory which contains the ControlPlugin class definition for this plugin.
|
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.
|
This is an optional paramater.
|
||||||
A plugin can be made that only runs on the clusterioctl side.
|
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:
|
For example, to define a ControllerPlugin class the following code can be used:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const lib = require("@clusterio/lib");
|
const { BaseControllerPlugin } = require("@clusterio/controller");
|
||||||
|
|
||||||
class ControllerPlugin extends lib.BaseControllerPlugin {
|
class ControllerPlugin extends BaseControllerPlugin {
|
||||||
async init() {
|
async init() {
|
||||||
this.foo = 42;
|
this.foo = 42;
|
||||||
await this.startFrobnication();
|
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).
|
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.
|
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.
|
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
|
```js
|
||||||
const lib = require("@clusterio/lib");
|
const { BaseCtlPlugin } = require("@clusterio/ctl");
|
||||||
|
|
||||||
class ControlPlugin extends lib.BaseControlPlugin {
|
class CtlPlugin extends BaseCtlPlugin {
|
||||||
async addCommands(rootCommand) {
|
async addCommands(rootCommand) {
|
||||||
rootCommand.add(fooFrobberCommands);
|
rootCommand.add(fooFrobberCommands);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ControlPlugin,
|
CtlPlugin,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -420,10 +420,7 @@ async function startup() {
|
|||||||
await controller.start(args);
|
await controller.start(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
export function bootstrap() {
|
||||||
};
|
|
||||||
|
|
||||||
if (module === require.main) {
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn(`
|
console.warn(`
|
||||||
+==========================================================+
|
+==========================================================+
|
||||||
@@ -460,3 +457,7 @@ ${err.stack}`
|
|||||||
process.exitCode = 1;
|
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",
|
"author": "Hornwitser",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "2.0.0-alpha.13",
|
"version": "2.0.0-alpha.13",
|
||||||
"main": "dist/controller.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "tsc --build && webpack-cli --env production",
|
"prepare": "tsc --build && webpack-cli --env production",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"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 UserManager from "./UserManager";
|
||||||
import WsServer from "./WsServer";
|
import WsServer from "./WsServer";
|
||||||
import HostConnection, { type HostInfo } from "./HostConnection";
|
import HostConnection, { type HostInfo } from "./HostConnection";
|
||||||
|
import BaseControllerPlugin from "./BaseControllerPlugin";
|
||||||
|
|
||||||
const endpointDurationSummary = new Summary(
|
const endpointDurationSummary = new Summary(
|
||||||
"clusterio_controller_http_endpoint_duration_seconds",
|
"clusterio_controller_http_endpoint_duration_seconds",
|
||||||
@@ -64,7 +65,7 @@ export default class Controller {
|
|||||||
httpsServerCloser: HttpCloser | null = null;
|
httpsServerCloser: HttpCloser | null = null;
|
||||||
|
|
||||||
/** Mapping of plugin name to loaded plugin */
|
/** Mapping of plugin name to loaded plugin */
|
||||||
plugins: Map<string, lib.BaseControllerPlugin> = new Map();
|
plugins: Map<string, BaseControllerPlugin> = new Map();
|
||||||
|
|
||||||
/** WebSocket server */
|
/** WebSocket server */
|
||||||
wsServer: WsServer;
|
wsServer: WsServer;
|
||||||
@@ -877,10 +878,15 @@ export default class Controller {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ControllerPluginClass = lib.BaseControllerPlugin;
|
let ControllerPluginClass = BaseControllerPlugin;
|
||||||
try {
|
try {
|
||||||
if (pluginInfo.controllerEntrypoint) {
|
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);
|
let controllerPlugin = new ControllerPluginClass(pluginInfo, this, metrics as any, logger);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"references": [
|
"references": [
|
||||||
{ "path": "../lib" },
|
{ "path": "../lib" },
|
||||||
],
|
],
|
||||||
"include": ["controller.ts", "package.json", "src/**/*.ts"],
|
"include": ["index.ts", "controller.ts", "package.json", "src/**/*.ts"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
},
|
},
|
||||||
|
|||||||
+25
-15
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import yargs from "yargs";
|
import yargs from "yargs";
|
||||||
|
import path from "path";
|
||||||
import { version } from "./package.json";
|
import { version } from "./package.json";
|
||||||
import setBlocking from "set-blocking";
|
import setBlocking from "set-blocking";
|
||||||
import { strict as assert } from "assert";
|
import { strict as assert } from "assert";
|
||||||
@@ -16,6 +17,7 @@ import * as lib from "@clusterio/lib";
|
|||||||
import { ConsoleTransport, levels, logger } from "@clusterio/lib";
|
import { ConsoleTransport, levels, logger } from "@clusterio/lib";
|
||||||
|
|
||||||
import * as commands from "./src/commands";
|
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. */
|
/** Certificate authority used to validate TLS connections to the controller. */
|
||||||
tlsCa?: string;
|
tlsCa?: string;
|
||||||
/** Mapping of plugin names to their instance for loaded plugins. */
|
/** 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. */
|
/** Keep the control connection alive after the command completes. */
|
||||||
keepOpen = false;
|
keepOpen = false;
|
||||||
|
|
||||||
@@ -64,12 +66,12 @@ export class Control extends lib.Link {
|
|||||||
connector: ControlConnector,
|
connector: ControlConnector,
|
||||||
controlConfig: lib.ControlConfig,
|
controlConfig: lib.ControlConfig,
|
||||||
tlsCa: string | undefined,
|
tlsCa: string | undefined,
|
||||||
controlPlugins: Map<string, lib.BaseControlPlugin>
|
ctlPlugins: Map<string, BaseCtlPlugin>
|
||||||
) {
|
) {
|
||||||
super(connector);
|
super(connector);
|
||||||
this.config = controlConfig;
|
this.config = controlConfig;
|
||||||
this.tlsCa = tlsCa;
|
this.tlsCa = tlsCa;
|
||||||
this.plugins = controlPlugins;
|
this.plugins = ctlPlugins;
|
||||||
|
|
||||||
this.handle(lib.LogMessageEvent, this.handleLogMessageEvent.bind(this));
|
this.handle(lib.LogMessageEvent, this.handleLogMessageEvent.bind(this));
|
||||||
this.handle(lib.DebugWsMessageEvent, this.handleDebugWsMessageEvent.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.registerPluginConfigGroups(pluginInfos);
|
||||||
lib.finalizeConfigs();
|
lib.finalizeConfigs();
|
||||||
|
|
||||||
let controlPlugins = new Map<string, lib.BaseControlPlugin>();
|
let ctlPlugins = new Map<string, BaseCtlPlugin>();
|
||||||
for (let pluginInfo of pluginInfos) {
|
for (let pluginInfo of pluginInfos) {
|
||||||
if (!pluginInfo.controlEntrypoint) {
|
if (!pluginInfo.ctlEntrypoint) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ControlPluginClass = await lib.loadControlPluginClass(pluginInfo);
|
let CtlPlugin = await lib.loadPluginClass(
|
||||||
let controlPlugin = new ControlPluginClass(pluginInfo, logger);
|
pluginInfo.name,
|
||||||
controlPlugins.set(pluginInfo.name, controlPlugin);
|
path.posix.join(pluginInfo.requirePath, pluginInfo.ctlEntrypoint),
|
||||||
await controlPlugin.init();
|
"CtlPlugin",
|
||||||
|
BaseCtlPlugin,
|
||||||
|
);
|
||||||
|
let ctlPlugin = new CtlPlugin(pluginInfo, logger);
|
||||||
|
ctlPlugins.set(pluginInfo.name, ctlPlugin);
|
||||||
|
await ctlPlugin.init();
|
||||||
}
|
}
|
||||||
return controlPlugins;
|
return ctlPlugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CtlArguments {
|
interface CtlArguments {
|
||||||
@@ -199,10 +206,10 @@ async function startControl() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.verbose("Loading Plugins");
|
logger.verbose("Loading Plugins");
|
||||||
let controlPlugins = await loadPlugins(pluginList);
|
let ctlPlugins = await loadPlugins(pluginList);
|
||||||
|
|
||||||
// Add all cluster management commands including ones from plugins
|
// 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.
|
// Reparse after commands have been added with help and strict checking.
|
||||||
args = yargs
|
args = yargs
|
||||||
@@ -264,7 +271,7 @@ async function startControl() {
|
|||||||
tlsCa,
|
tlsCa,
|
||||||
controlConfig.get("control.controller_token") as string,
|
controlConfig.get("control.controller_token") as string,
|
||||||
);
|
);
|
||||||
let control = new Control(controlConnector, controlConfig, tlsCa, controlPlugins);
|
let control = new Control(controlConnector, controlConfig, tlsCa, ctlPlugins);
|
||||||
try {
|
try {
|
||||||
await controlConnector.connect();
|
await controlConnector.connect();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -312,8 +319,7 @@ async function startControl() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function bootstrap() {
|
||||||
if (module === require.main) {
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn(`
|
console.warn(`
|
||||||
+==========================================================+
|
+==========================================================+
|
||||||
@@ -346,3 +352,7 @@ ${err.stack}`
|
|||||||
process.exitCode = 1;
|
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",
|
"author": "Hornwitser",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "2.0.0-alpha.13",
|
"version": "2.0.0-alpha.13",
|
||||||
"main": "dist/ctl.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "tsc --build",
|
"prepare": "tsc --build",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"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 * as lib from "@clusterio/lib";
|
||||||
import { ConsoleTransport, levels, logger } from "@clusterio/lib";
|
import { ConsoleTransport, levels, logger } from "@clusterio/lib";
|
||||||
import type { Control } from "../ctl";
|
import type { Control } from "../ctl";
|
||||||
|
import type BaseCtlPlugin from "./BaseCtlPlugin";
|
||||||
|
|
||||||
const asTable = asTableModule.configure({ delimiter: " | " });
|
const asTable = asTableModule.configure({ delimiter: " | " });
|
||||||
const finished = util.promisify(stream.finished);
|
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" });
|
const rootCommands = new lib.CommandTree({ name: "clusterioctl", description: "Manage cluster" });
|
||||||
rootCommands.add(controllerCommands);
|
rootCommands.add(controllerCommands);
|
||||||
rootCommands.add(hostCommands);
|
rootCommands.add(hostCommands);
|
||||||
@@ -1735,7 +1736,7 @@ export async function registerCommands(controlPlugins: Map<string, lib.BaseContr
|
|||||||
rootCommands.add(logCommands);
|
rootCommands.add(logCommands);
|
||||||
rootCommands.add(debugCommands);
|
rootCommands.add(debugCommands);
|
||||||
|
|
||||||
for (let controlPlugin of controlPlugins.values()) {
|
for (let controlPlugin of ctlPlugins.values()) {
|
||||||
await controlPlugin.addCommands(rootCommands);
|
await controlPlugin.addCommands(rootCommands);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"references": [
|
"references": [
|
||||||
{ "path": "../lib" },
|
{ "path": "../lib" },
|
||||||
],
|
],
|
||||||
"include": ["package.json", "ctl.ts", "src/**/*.ts"],
|
"include": ["index.ts", "package.json", "ctl.ts", "src/**/*.ts"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ async function startHost() {
|
|||||||
logger.info("Started host");
|
logger.info("Started host");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (module === require.main) {
|
export function bootstrap() {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn(`
|
console.warn(`
|
||||||
+==========================================================+
|
+==========================================================+
|
||||||
@@ -287,3 +287,7 @@ ${err.stack}`
|
|||||||
process.exitCode = 1;
|
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",
|
"author": "Hornwitser",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "2.0.0-alpha.13",
|
"version": "2.0.0-alpha.13",
|
||||||
"main": "dist/host.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "tsc --build",
|
"prepare": "tsc --build",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"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 { SaveModule, patch } from "./patch";
|
||||||
import { exportData } from "./export";
|
import { exportData } from "./export";
|
||||||
import type Host from "./Host";
|
import type Host from "./Host";
|
||||||
|
import BaseInstancePlugin from "./BaseInstancePlugin";
|
||||||
|
|
||||||
|
|
||||||
const instanceRconCommandDuration = new lib.Histogram(
|
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")`.
|
* ID of this instance, equivalenet to `instance.config.get("instance.id")`.
|
||||||
*/
|
*/
|
||||||
readonly id: number;
|
readonly id: number;
|
||||||
plugins: Map<string, lib.BaseInstancePlugin>;
|
plugins: Map<string, BaseInstancePlugin>;
|
||||||
config: lib.InstanceConfig;
|
config: lib.InstanceConfig;
|
||||||
logger: lib.Logger;
|
logger: lib.Logger;
|
||||||
server: FactorioServer;
|
server: FactorioServer;
|
||||||
@@ -492,7 +493,12 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
|||||||
|
|
||||||
async _loadPlugin(pluginInfo: lib.PluginNodeEnvInfo, host: Host) {
|
async _loadPlugin(pluginInfo: lib.PluginNodeEnvInfo, host: Host) {
|
||||||
let pluginLoadStarted = Date.now();
|
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);
|
let instancePlugin = new InstancePluginClass(pluginInfo, this, host);
|
||||||
this.plugins.set(pluginInfo.name, instancePlugin);
|
this.plugins.set(pluginInfo.name, instancePlugin);
|
||||||
await instancePlugin.init();
|
await instancePlugin.init();
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { Type, Static } from "@sinclair/typebox";
|
|||||||
|
|
||||||
import * as lib from "@clusterio/lib";
|
import * as lib from "@clusterio/lib";
|
||||||
|
|
||||||
|
import BaseInstancePlugin from "./BaseInstancePlugin";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes a module that can be patched into a save
|
* Describes a module that can be patched into a save
|
||||||
@@ -81,7 +83,7 @@ export class SaveModule {
|
|||||||
return module;
|
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 pluginPackagePath = require.resolve(path.posix.join(plugin.info.requirePath, "package.json"));
|
||||||
let moduleDirectory = path.join(path.dirname(pluginPackagePath), "module");
|
let moduleDirectory = path.join(path.dirname(pluginPackagePath), "module");
|
||||||
if (!await fs.pathExists(moduleDirectory)) {
|
if (!await fs.pathExists(moduleDirectory)) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"references": [
|
"references": [
|
||||||
{ "path": "../lib" },
|
{ "path": "../lib" },
|
||||||
],
|
],
|
||||||
"include": ["host.ts", "package.json", "src/**/*.ts", "src/**/*.js"],
|
"include": ["index.ts", "host.ts", "package.json", "src/**/*.ts", "src/**/*.js"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { TSchema, Type, Static } from "@sinclair/typebox";
|
|||||||
// eslint-disable-next-line node/no-missing-import
|
// eslint-disable-next-line node/no-missing-import
|
||||||
import messageValidate from "./message_validate"; // generated file
|
import messageValidate from "./message_validate"; // generated file
|
||||||
import { StringEnum } from "./composites";
|
import { StringEnum } from "./composites";
|
||||||
import { AccountRole } from "../plugin";
|
|
||||||
|
|
||||||
export type AddressType = "controller" | "host" | "instance" | "control" | "broadcast";
|
export type AddressType = "controller" | "host" | "instance" | "control" | "broadcast";
|
||||||
export type AddressShorthand =
|
export type AddressShorthand =
|
||||||
@@ -287,6 +286,12 @@ export class MessageRegisterControl extends Message {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AccountRole = {
|
||||||
|
name: string;
|
||||||
|
id: number;
|
||||||
|
permissions: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export class AccountDetails {
|
export class AccountDetails {
|
||||||
constructor(
|
constructor(
|
||||||
public name: string,
|
public name: string,
|
||||||
@@ -632,3 +637,28 @@ export class PingRequest {
|
|||||||
static dst = ["controller", "host", "control"] as const;
|
static dst = ["controller", "host", "control"] as const;
|
||||||
static permission = null;
|
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 { Type, Static } from "@sinclair/typebox";
|
||||||
import PlayerStats from "../PlayerStats";
|
import PlayerStats from "../PlayerStats";
|
||||||
import { JsonNumber, jsonArray } from "./composites";
|
import { JsonNumber, jsonArray } from "./composites";
|
||||||
import { AccountRole } from "../plugin";
|
|
||||||
|
|
||||||
export class RawPermission { // TODO refactor into lib/user.Permission
|
export class RawPermission { // TODO refactor into lib/user.Permission
|
||||||
constructor(
|
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 {
|
export class UserUpdateEvent {
|
||||||
declare ["constructor"]: typeof UserUpdateEvent;
|
declare ["constructor"]: typeof UserUpdateEvent;
|
||||||
static type = "event" as const;
|
static type = "event" as const;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type { RequestClass, EventClass } from "./link";
|
|||||||
*/
|
*/
|
||||||
export const dataClasses: (RequestClass<unknown, unknown> | EventClass<unknown>)[] = [
|
export const dataClasses: (RequestClass<unknown, unknown> | EventClass<unknown>)[] = [
|
||||||
core.PingRequest,
|
core.PingRequest,
|
||||||
|
core.AccountUpdateEvent,
|
||||||
|
|
||||||
controller.ControllerConfigGetRequest,
|
controller.ControllerConfigGetRequest,
|
||||||
controller.ControllerConfigSetFieldRequest,
|
controller.ControllerConfigSetFieldRequest,
|
||||||
@@ -105,6 +106,5 @@ export const dataClasses: (RequestClass<unknown, unknown> | EventClass<unknown>)
|
|||||||
user.UserSetWhitelistedRequest,
|
user.UserSetWhitelistedRequest,
|
||||||
user.UserSetBannedRequest,
|
user.UserSetBannedRequest,
|
||||||
user.UserDeleteRequest,
|
user.UserDeleteRequest,
|
||||||
user.AccountUpdateEvent,
|
|
||||||
user.UserUpdateEvent,
|
user.UserUpdateEvent,
|
||||||
];
|
];
|
||||||
+6
-811
@@ -3,22 +3,7 @@
|
|||||||
* @module lib/plugin
|
* @module lib/plugin
|
||||||
*/
|
*/
|
||||||
import * as libHelpers from "./helpers";
|
import * as libHelpers from "./helpers";
|
||||||
import type { Logger } from "./logging";
|
import type { PluginConfigGroup } from "./config";
|
||||||
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
|
|
||||||
;
|
|
||||||
|
|
||||||
// TODO Add proper typing for plugins
|
// TODO Add proper typing for plugins
|
||||||
/* Used to define the export of info.ts from plugins */
|
/* Used to define the export of info.ts from plugins */
|
||||||
@@ -33,14 +18,14 @@ export type PluginDeclaration = {
|
|||||||
controllerEntrypoint?: string;
|
controllerEntrypoint?: string;
|
||||||
ControllerConfigGroup?: typeof PluginConfigGroup;
|
ControllerConfigGroup?: typeof PluginConfigGroup;
|
||||||
|
|
||||||
controlEntrypoint?: string;
|
ctlEntrypoint?: string;
|
||||||
|
webEntrypoint?: string;
|
||||||
ControlConfigGroup?: typeof PluginConfigGroup;
|
ControlConfigGroup?: typeof PluginConfigGroup;
|
||||||
|
|
||||||
messages?: any[];
|
messages?: any[];
|
||||||
|
|
||||||
webEntrypoint?: string;
|
|
||||||
routes?: string[];
|
routes?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PluginNodeEnvInfo = PluginDeclaration & {
|
export type PluginNodeEnvInfo = PluginDeclaration & {
|
||||||
requirePath: string;
|
requirePath: string;
|
||||||
version: string;
|
version: string;
|
||||||
@@ -53,19 +38,10 @@ export type PluginWebpackEnvInfo = PluginDeclaration & {
|
|||||||
enabled?: boolean;
|
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.
|
* Information about the event.
|
||||||
*/
|
*/
|
||||||
interface PlayerEvent {
|
export interface PlayerEvent {
|
||||||
type: "join" | "leave" | "import";
|
type: "join" | "leave" | "import";
|
||||||
/** Name of the player that joined/left */
|
/** Name of the player that joined/left */
|
||||||
name: string,
|
name: string,
|
||||||
@@ -76,787 +52,6 @@ interface PlayerEvent {
|
|||||||
*/
|
*/
|
||||||
reason?: string;
|
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
|
* Invokes the given hook on all plugins
|
||||||
@@ -867,7 +62,7 @@ export class BaseWebPlugin {
|
|||||||
* @param args - Arguments to pass on to the hook.
|
* @param args - Arguments to pass on to the hook.
|
||||||
* @returns Non-undefined return values from the hooks.
|
* @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[] = [];
|
let results: unknown[] = [];
|
||||||
for (let [name, plugin] of plugins) {
|
for (let [name, plugin] of plugins) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -52,65 +52,22 @@ export async function loadPluginInfos(pluginList: Map<string, string>) {
|
|||||||
return plugins;
|
return plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadPluginClass(
|
export async function loadPluginClass<Class extends { new (...args: any): any }>(
|
||||||
entrypointName: "controllerEntrypoint"|"instanceEntrypoint"|"controlEntrypoint",
|
pluginName: string,
|
||||||
|
requirePath: string,
|
||||||
className: string,
|
className: string,
|
||||||
pluginClass: any,
|
pluginClass: Class,
|
||||||
pluginInfo: libPlugin.PluginNodeEnvInfo
|
): Promise<Class> {
|
||||||
) {
|
let entrypoint = require(requirePath);
|
||||||
let resolvedPath = path.posix.join(pluginInfo.requirePath, pluginInfo[entrypointName]!);
|
|
||||||
let entrypoint = require(resolvedPath);
|
|
||||||
if (!entrypoint[className]) {
|
if (!entrypoint[className]) {
|
||||||
throw new libErrors.PluginError(pluginInfo.name,
|
throw new libErrors.PluginError(pluginName,
|
||||||
new Error(`Expected ${resolvedPath} to export a class named ${className}`)
|
new Error(`Expected ${requirePath} to export a class named ${className}`)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!(entrypoint[className].prototype instanceof pluginClass)) {
|
if (!(entrypoint[className].prototype instanceof pluginClass)) {
|
||||||
throw new libErrors.PluginError(pluginInfo.name,
|
throw new libErrors.PluginError(pluginName,
|
||||||
new Error(`Expected ${className} exported from ${resolvedPath} to be a subclass of ${pluginClass.name}`)
|
new Error(`Expected ${className} exported from ${requirePath} to be a subclass of ${pluginClass.name}`)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return entrypoint[className];
|
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 type { PluginWebApi } from "@clusterio/controller/src/routes";
|
||||||
|
|
||||||
import App from "./components/App";
|
import App from "./components/App";
|
||||||
|
import BaseWebPlugin from "./BaseWebPlugin";
|
||||||
import { Control, ControlConnector } from "./util/websocket";
|
import { Control, ControlConnector } from "./util/websocket";
|
||||||
|
|
||||||
const { ConsoleTransport, WebConsoleFormat, logger } = lib;
|
const { ConsoleTransport, WebConsoleFormat, logger } = lib;
|
||||||
@@ -74,7 +75,7 @@ async function loadPlugins(pluginInfos: lib.PluginWebpackEnvInfo[]) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
let WebPluginClass = lib.BaseWebPlugin;
|
let WebPluginClass = BaseWebPlugin;
|
||||||
if (pluginInfo.webEntrypoint) {
|
if (pluginInfo.webEntrypoint) {
|
||||||
let webModule = (await pluginInfo.container.get(pluginInfo.webEntrypoint))();
|
let webModule = (await pluginInfo.container.get(pluginInfo.webEntrypoint))();
|
||||||
if (!webModule.WebPlugin) {
|
if (!webModule.WebPlugin) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, { useContext } from "react";
|
|||||||
import * as lib from "@clusterio/lib";
|
import * as lib from "@clusterio/lib";
|
||||||
|
|
||||||
import ControlContext from "./ControlContext";
|
import ControlContext from "./ControlContext";
|
||||||
|
import BaseWebPlugin from "../BaseWebPlugin";
|
||||||
import { RawUserState } from "../model/user";
|
import { RawUserState } from "../model/user";
|
||||||
import { HostState } from "../model/host";
|
import { HostState } from "../model/host";
|
||||||
import { InstanceState } from "../model/instance";
|
import { InstanceState } from "../model/instance";
|
||||||
@@ -22,7 +23,7 @@ export default function PluginExtra(props: PluginExtraProps) {
|
|||||||
let components = [];
|
let components = [];
|
||||||
for (let [name, plugin] of control.plugins) {
|
for (let [name, plugin] of control.plugins) {
|
||||||
if (Object.prototype.hasOwnProperty.call(plugin.componentExtra, props.component)) {
|
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];
|
let ComponentExtra = (plugin.componentExtra as ComponentExtra)[props.component];
|
||||||
components.push(<ComponentExtra key={name} plugin={plugin} {...props}/>);
|
components.push(<ComponentExtra key={name} plugin={plugin} {...props}/>);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export { default as bootstrap } from "./bootstrap";
|
export { default as bootstrap } from "./bootstrap";
|
||||||
export { default as notify, notifyErrorHandler } from "./util/notify";
|
export { default as notify, notifyErrorHandler } from "./util/notify";
|
||||||
|
export { default as BaseWebPlugin } from "./BaseWebPlugin";
|
||||||
|
|
||||||
export * from "./model/instance";
|
export * from "./model/instance";
|
||||||
export * from "./model/host";
|
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 AssignInstanceModal } from "./components/AssignInstanceModal";
|
||||||
export { default as InstanceList } from "./components/InstanceList";
|
export { default as InstanceList } from "./components/InstanceList";
|
||||||
export { default as InstanceRcon } from "./components/InstanceRcon";
|
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 { default as StartStopInstanceButton } from "./components/StartStopInstanceButton";
|
||||||
export { statusColors } from "./components/InstanceStatusTag";
|
|
||||||
|
|
||||||
export { default as PageHeader } from "./components/PageHeader";
|
export { default as PageHeader } from "./components/PageHeader";
|
||||||
export { default as PageLayout } from "./components/PageLayout";
|
export { default as PageLayout } from "./components/PageLayout";
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import { UserAccount } from "../BaseWebPlugin";
|
||||||
import * as lib from "@clusterio/lib";
|
import * as lib from "@clusterio/lib";
|
||||||
|
|
||||||
import { notifyErrorHandler } from "../util/notify";
|
import { notifyErrorHandler } from "../util/notify";
|
||||||
import ControlContext from "../components/ControlContext";
|
import ControlContext from "../components/ControlContext";
|
||||||
|
|
||||||
|
|
||||||
export function useAccount(): lib.UserAccount {
|
export function useAccount(): UserAccount {
|
||||||
let control = useContext(ControlContext);
|
let control = useContext(ControlContext);
|
||||||
let [name, setName] = useState<string>(control.accountName || "");
|
let [name, setName] = useState<string>(control.accountName || "");
|
||||||
let [roles, setRoles] = useState<any[]>(control.accountRoles || []);
|
let [roles, setRoles] = useState<any[]>(control.accountRoles || []);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { ReactElement } from "react";
|
import React, { ReactElement } from "react";
|
||||||
|
|
||||||
import type * as lib from "@clusterio/lib";
|
import { PluginPage, UserAccount } from "./BaseWebPlugin";
|
||||||
|
|
||||||
import ControllerPage from "./components/ControllerPage";
|
import ControllerPage from "./components/ControllerPage";
|
||||||
import HostsPage from "./components/HostsPage";
|
import HostsPage from "./components/HostsPage";
|
||||||
@@ -16,7 +16,7 @@ import RoleViewPage from "./components/RoleViewPage";
|
|||||||
import PluginsPage from "./components/PluginsPage";
|
import PluginsPage from "./components/PluginsPage";
|
||||||
import PluginViewPage from "./components/PluginViewPage";
|
import PluginViewPage from "./components/PluginViewPage";
|
||||||
|
|
||||||
export const pages: lib.PluginPage[] = [
|
export const pages: PluginPage[] = [
|
||||||
{
|
{
|
||||||
path: "/controller",
|
path: "/controller",
|
||||||
sidebarName: "Controller",
|
sidebarName: "Controller",
|
||||||
@@ -47,7 +47,7 @@ export const pages: lib.PluginPage[] = [
|
|||||||
{
|
{
|
||||||
path: "/mods",
|
path: "/mods",
|
||||||
sidebarName: "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 />,
|
content: <ModsPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as lib from "@clusterio/lib";
|
import * as lib from "@clusterio/lib";
|
||||||
import packageJson from "../../package.json";
|
import packageJson from "../../package.json";
|
||||||
|
import BaseWebPlugin from "../BaseWebPlugin";
|
||||||
|
|
||||||
const { logFilter, logger } = lib;
|
const { logFilter, logger } = lib;
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ export class Control extends lib.Link {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
connector: ControlConnector,
|
connector: ControlConnector,
|
||||||
public plugins: Map<string, lib.BaseWebPlugin>,
|
public plugins: Map<string, BaseWebPlugin>,
|
||||||
) {
|
) {
|
||||||
super(connector);
|
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";
|
import { ChatEvent } from "./messages";
|
||||||
|
|
||||||
const globalChatCommands = new CommandTree({
|
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) {
|
async addCommands(rootCommand: CommandTree) {
|
||||||
rootCommand.add(globalChatCommands);
|
rootCommand.add(globalChatCommands);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const info: lib.PluginDeclaration = {
|
|||||||
title: "Global Chat",
|
title: "Global Chat",
|
||||||
description: "Forwards chat between instances.",
|
description: "Forwards chat between instances.",
|
||||||
instanceEntrypoint: "dist/plugin/instance",
|
instanceEntrypoint: "dist/plugin/instance",
|
||||||
controlEntrypoint: "dist/plugin/control",
|
ctlEntrypoint: "dist/plugin/control",
|
||||||
|
|
||||||
messages: [
|
messages: [
|
||||||
ChatEvent,
|
ChatEvent,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as lib from "@clusterio/lib";
|
import * as lib from "@clusterio/lib";
|
||||||
|
import { BaseInstancePlugin } from "@clusterio/host";
|
||||||
import { ChatEvent } from "./messages";
|
import { ChatEvent } from "./messages";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,7 +12,7 @@ function removeTags(content: string): string {
|
|||||||
return content.replace(/(\[gps=-?\d+,-?\d+\]|\[train=\d+\])/g, "");
|
return content.replace(/(\[gps=-?\d+,-?\d+\]|\[train=\d+\])/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InstancePlugin extends lib.BaseInstancePlugin {
|
export class InstancePlugin extends BaseInstancePlugin {
|
||||||
messageQueue: string[] = [];
|
messageQueue: string[] = [];
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type InstanceInfo from "@clusterio/controller/src/InstanceInfo";
|
import { BaseControllerPlugin, type InstanceInfo } from "@clusterio/controller";
|
||||||
import type Controller from "@clusterio/controller/src/Controller";
|
|
||||||
import type { IpcPlayerData } from "./instance";
|
import type { IpcPlayerData } from "./instance";
|
||||||
|
|
||||||
import fs from "fs-extra";
|
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 }>;
|
acquiredPlayers!: Map<string, { instanceId: number, expires?: number }>;
|
||||||
playerDatastore!: Map<string, IpcPlayerData>;
|
playerDatastore!: Map<string, IpcPlayerData>;
|
||||||
autosaveId!: ReturnType<typeof setInterval>;
|
autosaveId!: ReturnType<typeof setInterval>;
|
||||||
@@ -48,7 +47,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
|||||||
saveDatabase(this.controller.config, this.playerDatastore, this.logger).catch(err => {
|
saveDatabase(this.controller.config, this.playerDatastore, this.logger).catch(err => {
|
||||||
this.logger.error(`Unexpected error autosaving player data:\n${err.stack}`);
|
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.AcquireRequest, this.handleAcquireRequest.bind(this));
|
||||||
this.controller.handle(msg.ReleaseRequest, this.handleReleaseRequest.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)) {
|
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()) {
|
for (let acquisitonRecord of this.acquiredPlayers.values()) {
|
||||||
if (acquisitonRecord.instanceId === instanceId && !acquisitonRecord.expires) {
|
if (acquisitonRecord.instanceId === instanceId && !acquisitonRecord.expires) {
|
||||||
acquisitonRecord.expires = Date.now() + timeout;
|
acquisitonRecord.expires = Date.now() + timeout;
|
||||||
@@ -90,7 +89,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
|||||||
if (
|
if (
|
||||||
!acquisitionRecord
|
!acquisitionRecord
|
||||||
|| acquisitionRecord.instanceId === instanceId
|
|| acquisitionRecord.instanceId === instanceId
|
||||||
|| !this.controller.instances.has(acquisitionRecord.instanceId)
|
|| !this.controller.instances!.has(acquisitionRecord.instanceId)
|
||||||
|| acquisitionRecord.expires && acquisitionRecord.expires < Date.now()
|
|| acquisitionRecord.expires && acquisitionRecord.expires < Date.now()
|
||||||
) {
|
) {
|
||||||
this.acquiredPlayers.set(playerName, { instanceId });
|
this.acquiredPlayers.set(playerName, { instanceId });
|
||||||
@@ -104,7 +103,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
|||||||
let { instanceId, playerName } = request;
|
let { instanceId, playerName } = request;
|
||||||
if (!this.acquire(instanceId, playerName)) {
|
if (!this.acquire(instanceId, playerName)) {
|
||||||
let acquisitionRecord = this.acquiredPlayers.get(playerName);
|
let acquisitionRecord = this.acquiredPlayers.get(playerName);
|
||||||
let instance = this.controller.instances.get(acquisitionRecord!.instanceId);
|
let instance = this.controller.instances!.get(acquisitionRecord!.instanceId)!;
|
||||||
return {
|
return {
|
||||||
status: "busy",
|
status: "busy",
|
||||||
message: instance.config.get("instance.name"),
|
message: instance.config.get("instance.name"),
|
||||||
@@ -133,7 +132,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
|||||||
|
|
||||||
async handleUploadRequest(request: msg.UploadRequest) {
|
async handleUploadRequest(request: msg.UploadRequest) {
|
||||||
let { instanceId, playerName, playerData } = request;
|
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 store = true;
|
||||||
let acquisitionRecord = this.acquiredPlayers.get(playerName);
|
let acquisitionRecord = this.acquiredPlayers.get(playerName);
|
||||||
if (!acquisitionRecord) {
|
if (!acquisitionRecord) {
|
||||||
@@ -167,7 +166,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
|||||||
|
|
||||||
async handleDownloadRequest(request: msg.DownloadRequest) {
|
async handleDownloadRequest(request: msg.DownloadRequest) {
|
||||||
let { instanceId, playerName } = request;
|
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);
|
let acquisitionRecord = this.acquiredPlayers.get(playerName);
|
||||||
if (!acquisitionRecord) {
|
if (!acquisitionRecord) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as lib from "@clusterio/lib";
|
import * as lib from "@clusterio/lib";
|
||||||
|
import { BaseInstancePlugin } from "@clusterio/host";
|
||||||
import {
|
import {
|
||||||
AcquireRequest, AcquireResponse, ReleaseRequest, UploadRequest, DownloadRequest, DownloadResponse,
|
AcquireRequest, AcquireResponse, ReleaseRequest, UploadRequest, DownloadRequest, DownloadResponse,
|
||||||
} from "./messages";
|
} from "./messages";
|
||||||
@@ -44,7 +45,7 @@ function chunkify(chunkSize: number, string: string): string[] {
|
|||||||
return string.match(new RegExp(`.{1,${chunkSize}}`, "g")) || [];
|
return string.match(new RegExp(`.{1,${chunkSize}}`, "g")) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InstancePlugin extends lib.BaseInstancePlugin {
|
export class InstancePlugin extends BaseInstancePlugin {
|
||||||
playersToRelease!: Set<string>;
|
playersToRelease!: Set<string>;
|
||||||
disconnecting!: boolean;
|
disconnecting!: boolean;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useContext, useEffect, useState } from "react";
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
import * as lib from "@clusterio/lib";
|
import { BaseWebPlugin, PageLayout, ControlContext } from "@clusterio/web_ui";
|
||||||
import { PageLayout, ControlContext } from "@clusterio/web_ui";
|
|
||||||
import { DatabaseStatsRequest } from "../dist/plugin/messages";
|
import { DatabaseStatsRequest } from "../dist/plugin/messages";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
@@ -28,7 +27,7 @@ function InventoryPage() {
|
|||||||
</PageLayout>;
|
</PageLayout>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebPlugin extends lib.BaseWebPlugin {
|
export class WebPlugin extends BaseWebPlugin {
|
||||||
async init() {
|
async init() {
|
||||||
this.pages = [
|
this.pages = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import util from "util";
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
import * as lib from "@clusterio/lib";
|
import * as lib from "@clusterio/lib";
|
||||||
|
import { BaseControllerPlugin } from "@clusterio/controller";
|
||||||
const { basicType } = lib;
|
const { basicType } = lib;
|
||||||
|
|
||||||
import { FetchPlayerCodeRequest, SetVerifyCodeRequest } from "./messages";
|
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 };
|
type PlayerCode = { playerCode: string, verifyCode: string | null, expires: number };
|
||||||
|
|
||||||
export class ControllerPlugin extends lib.BaseControllerPlugin {
|
export class ControllerPlugin extends BaseControllerPlugin {
|
||||||
players!: Map<string, PlayerCode>;
|
players!: Map<string, PlayerCode>;
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@@ -46,7 +47,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
|||||||
|
|
||||||
this.controller.app.get("/api/player_auth/servers", (req: Request, res: Response) => {
|
this.controller.app.get("/api/player_auth/servers", (req: Request, res: Response) => {
|
||||||
let servers = [];
|
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")) {
|
if (instance.status === "running" && instance.config.get("player_auth.load_plugin")) {
|
||||||
servers.push(instance.config.get("factorio.settings")["name"] || "unnamed server");
|
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()) {
|
for (let entry of this.players.values()) {
|
||||||
if (entry.playerCode === playerCode && entry.expires > Date.now()) {
|
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 secret = Buffer.from(this.controller.config.get("controller.auth_secret"), "base64");
|
||||||
let verifyToken = jwt.sign(
|
let verifyToken = jwt.sign(
|
||||||
{
|
{
|
||||||
@@ -172,8 +173,8 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleFetchPlayerCodeRequest(request: FetchPlayerCodeRequest) {
|
async handleFetchPlayerCodeRequest(request: FetchPlayerCodeRequest) {
|
||||||
let playerCode = await generateCode(this.controller.config.get("player_auth.code_length"));
|
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") * 1000;
|
let expires = Date.now() + (this.controller.config.get("player_auth.code_timeout") as number) * 1000;
|
||||||
this.players.set(request.player, { playerCode, verifyCode: null, expires });
|
this.players.set(request.player, { playerCode, verifyCode: null, expires });
|
||||||
return { player_code: playerCode, controller_url: this.controller.getControllerUrl() };
|
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";
|
import { FetchPlayerCodeRequest, SetVerifyCodeRequest } from "./messages";
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ type IpcPlayerAuth = {
|
|||||||
verify_code: string,
|
verify_code: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InstancePlugin extends lib.BaseInstancePlugin {
|
export class InstancePlugin extends BaseInstancePlugin {
|
||||||
async init() {
|
async init() {
|
||||||
if (!this.instance.config.get("factorio.enable_save_patching")) {
|
if (!this.instance.config.get("factorio.enable_save_patching")) {
|
||||||
throw new Error("player_auth plugin requires save patching.");
|
throw new Error("player_auth plugin requires save patching.");
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import * as lib from "@clusterio/lib";
|
import { BaseWebPlugin, notifyErrorHandler } from "@clusterio/web_ui";
|
||||||
import { notifyErrorHandler } from "@clusterio/web_ui";
|
|
||||||
import { Button, Form, Input, Spin, Typography } from "antd";
|
import { Button, Form, Input, Spin, Typography } from "antd";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
@@ -146,7 +145,7 @@ function LoginForm(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class WebPlugin extends lib.BaseWebPlugin {
|
export class WebPlugin extends BaseWebPlugin {
|
||||||
async init() {
|
async init() {
|
||||||
this.logger.info("Player Auth init");
|
this.logger.info("Player Auth init");
|
||||||
this.loginForms = [{
|
this.loginForms = [{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { BaseControllerPlugin } from "@clusterio/controller";
|
||||||
import { Static } from "@sinclair/typebox";
|
import { Static } from "@sinclair/typebox";
|
||||||
|
|
||||||
import * as lib from "@clusterio/lib";
|
import * as lib from "@clusterio/lib";
|
||||||
@@ -48,7 +49,7 @@ async function saveTechnologies(
|
|||||||
await lib.safeOutputFile(filePath, JSON.stringify([...technologies.entries()], null, "\t"));
|
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>;
|
technologies!: Map<string, Technology>;
|
||||||
progressRateLimiter!: lib.RateLimiter;
|
progressRateLimiter!: lib.RateLimiter;
|
||||||
progressBroadcastId!: ReturnType<typeof setInterval> | null;
|
progressBroadcastId!: ReturnType<typeof setInterval> | null;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as lib from "@clusterio/lib";
|
import * as lib from "@clusterio/lib";
|
||||||
|
import { BaseInstancePlugin } from "@clusterio/host";
|
||||||
import {
|
import {
|
||||||
ContributionEvent,
|
ContributionEvent,
|
||||||
ProgressEvent,
|
ProgressEvent,
|
||||||
@@ -18,7 +19,7 @@ type IpcFinished = {
|
|||||||
level: number,
|
level: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class InstancePlugin extends lib.BaseInstancePlugin {
|
export class InstancePlugin extends BaseInstancePlugin {
|
||||||
syncStarted!: boolean;
|
syncStarted!: boolean;
|
||||||
|
|
||||||
unexpectedError(err: Error) {
|
unexpectedError(err: Error) {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import * as lib from "@clusterio/lib";
|
import * as lib from "@clusterio/lib";
|
||||||
|
import { BaseInstancePlugin } from "@clusterio/host";
|
||||||
|
|
||||||
const { Gauge } = lib;
|
const { Gauge } = lib;
|
||||||
|
|
||||||
type IpcStats = {
|
type IpcStats = {
|
||||||
@@ -59,7 +61,7 @@ function setForceFlowStatistic(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class InstancePlugin extends lib.BaseInstancePlugin {
|
export class InstancePlugin extends BaseInstancePlugin {
|
||||||
async init() {
|
async init() {
|
||||||
if (!this.instance.config.get("factorio.enable_save_patching")) {
|
if (!this.instance.config.get("factorio.enable_save_patching")) {
|
||||||
throw new Error("statistics_exporter plugin requires 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
|
// take a long time for the command to go through if the command
|
||||||
// stream is overloaded. Should the timeout be exceeded the
|
// stream is overloaded. Should the timeout be exceeded the
|
||||||
// previous values for the metrics will end up being sent to controller.
|
// 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);
|
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 fs from "fs-extra";
|
||||||
import path from "path";
|
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;
|
items!: lib.ItemDatabase;
|
||||||
itemUpdateRateLimiter!: lib.RateLimiter;
|
itemUpdateRateLimiter!: lib.RateLimiter;
|
||||||
itemsLastUpdate!: Map<string, number>;
|
itemsLastUpdate!: Map<string, number>;
|
||||||
@@ -96,7 +96,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
|||||||
saveDatabase(this.controller.config, this.items, this.logger).catch(err => {
|
saveDatabase(this.controller.config, this.items, this.logger).catch(err => {
|
||||||
this.logger.error(`Unexpected error autosaving items:\n${err.stack}`);
|
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.neuralDole = new dole.NeuralDole({ items: this.items });
|
||||||
this.doleMagicId = setInterval(() => {
|
this.doleMagicId = setInterval(() => {
|
||||||
@@ -180,7 +180,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
let instance = this.controller.instances.get(instanceId);
|
let instance = this.controller.instances!.get(instanceId);
|
||||||
let instanceName = instance ? instance.config.get("instance.name") : "unkonwn";
|
let instanceName = instance ? instance.config.get("instance.name") : "unkonwn";
|
||||||
|
|
||||||
// use fancy neural net to calculate a "fair" dole division rate.
|
// 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({
|
let count = dole.doleDivider({
|
||||||
object: { name: item.name, count: item.count, instanceId, instanceName },
|
object: { name: item.name, count: item.count, instanceId, instanceName },
|
||||||
items: this.items,
|
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,
|
logger: this.logger,
|
||||||
});
|
});
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
@@ -230,7 +230,7 @@ export class ControllerPlugin extends lib.BaseControllerPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleSetStorageSubscriptionRequest(request: SetStorageSubscriptionRequest, src: lib.Address) {
|
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) {
|
if (request.storage) {
|
||||||
this.subscribedControlLinks.add(link);
|
this.subscribedControlLinks.add(link);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as lib from "@clusterio/lib";
|
import * as lib from "@clusterio/lib";
|
||||||
|
import { BaseInstancePlugin } from "@clusterio/host";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PlaceEvent,
|
PlaceEvent,
|
||||||
@@ -10,7 +11,7 @@ import {
|
|||||||
|
|
||||||
type IpcItems = [string, number][];
|
type IpcItems = [string, number][];
|
||||||
|
|
||||||
export class InstancePlugin extends lib.BaseInstancePlugin {
|
export class InstancePlugin extends BaseInstancePlugin {
|
||||||
pendingTasks!: Set<any>;
|
pendingTasks!: Set<any>;
|
||||||
pingId?: ReturnType<typeof setTimeout>;
|
pingId?: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import React, { useContext, useEffect, useState } from "react";
|
|||||||
import { Input, Table, Typography } from "antd";
|
import { Input, Table, Typography } from "antd";
|
||||||
|
|
||||||
import * as lib from "@clusterio/lib";
|
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 { GetStorageRequest, SetStorageSubscriptionRequest } from "../dist/plugin/messages";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
@@ -114,7 +117,7 @@ function StoragePage() {
|
|||||||
</PageLayout>;
|
</PageLayout>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebPlugin extends lib.BaseWebPlugin {
|
export class WebPlugin extends BaseWebPlugin {
|
||||||
async init() {
|
async init() {
|
||||||
this.pages = [
|
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("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() {
|
describe("invokeHook()", function() {
|
||||||
let betaTestCalled = false;
|
let betaTestCalled = false;
|
||||||
let plugins = new Map([
|
let plugins = new Map([
|
||||||
|
|||||||
+14
-11
@@ -4,6 +4,7 @@ const fs = require("fs-extra");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const lib = require("@clusterio/lib");
|
const lib = require("@clusterio/lib");
|
||||||
|
const { BaseControllerPlugin } = require("@clusterio/controller");
|
||||||
const { escapeRegExp } = lib;
|
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 baseDir = path.join("temp", "test", "plugin");
|
||||||
let missingClass = path.join(baseDir, "missing_class_plugin");
|
let missingClass = path.join(baseDir, "missing_class_plugin");
|
||||||
let wrongParentClass = path.join(baseDir, "wrong_parent_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() {
|
it("should throw if class is missing from entrypoint", async function() {
|
||||||
const requirePath = path.resolve(missingClass);
|
const requirePath = path.resolve(missingClass);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
lib.loadControllerPluginClass({
|
lib.loadPluginClass(
|
||||||
name: "test",
|
"test",
|
||||||
controllerEntrypoint: "controller",
|
path.posix.join(requirePath, "controller"),
|
||||||
requirePath,
|
"ControllerPlugin",
|
||||||
}),
|
BaseControllerPlugin,
|
||||||
|
),
|
||||||
{
|
{
|
||||||
message:
|
message:
|
||||||
`PluginError: Expected ${path.posix.join(requirePath, "controller")} ` +
|
`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() {
|
it("should throw if class is not a subclass of BaseControllerPlugin", async function() {
|
||||||
const requirePath = path.resolve(wrongParentClass);
|
const requirePath = path.resolve(wrongParentClass);
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
lib.loadControllerPluginClass({
|
lib.loadPluginClass(
|
||||||
name: "test",
|
"test",
|
||||||
controllerEntrypoint: "controller",
|
path.posix.join(requirePath, "controller"),
|
||||||
requirePath,
|
"ControllerPlugin",
|
||||||
}),
|
BaseControllerPlugin,
|
||||||
|
),
|
||||||
{
|
{
|
||||||
message:
|
message:
|
||||||
"PluginError: Expected ControllerPlugin exported from " +
|
"PluginError: Expected ControllerPlugin exported from " +
|
||||||
|
|||||||
Reference in new issue
Block a user