mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 07:04:53 +09:00
Merge pull request #485 from Hornwitser/data-class-refactor
Data class refactor
This commit is contained in:
116 files changed
+9228
-5081
No files matched your search
+1
-1
@@ -136,7 +136,7 @@ module.exports = {
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"lines-around-comment": "off",
|
||||
"lines-around-directive": "off",
|
||||
"lines-between-class-members": ["error", "always"],
|
||||
"lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }],
|
||||
"max-classes-per-file": "off",
|
||||
"max-depth": "error",
|
||||
"max-len": ["error", { "code": 120 }],
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# JSON Serialisable Classes
|
||||
|
||||
To serialise and deserialise data the Clusterio codebase uses a JSON serialisable data class pattern.
|
||||
Instances of these classes can be serialised to JSON by passing them directly to `JSON.serialise` and deserialised by passing the serialised JSON representation to its static `fromJSON` method.
|
||||
The data class also has a static `jsonSchema` property describing the serialised JSON representation for validation purposes.
|
||||
See the following example code for a hypothetical `Data` class with a single property `member` defaulting to a value of `"foo"`.
|
||||
|
||||
|
||||
```js
|
||||
// Data.js
|
||||
"use strictc";
|
||||
|
||||
/**
|
||||
* A Data class example
|
||||
* @alias module:Data
|
||||
*/
|
||||
class Data {
|
||||
/** @type {string} */
|
||||
member = "foo";
|
||||
|
||||
constructor(member) {
|
||||
if (member !== undefined) { this.member = member; }
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
"member": { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.member);
|
||||
}
|
||||
|
||||
// toJSON is called by JSON.stringify when serialising this class
|
||||
toJSON() {
|
||||
let json = {};
|
||||
// Omit property if it's set to the default value
|
||||
if (this.member !== "foo") { json["member"] = this.member; }
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Data;
|
||||
```
|
||||
|
||||
Serialising an instance of `Data` for JSON is as simple as doing `serialised = JSON.stringify(data)` as the `toJSON` method is automatically called by `JSON.stringify` to create a suitable object.
|
||||
Deserialising is done by calling the static `fromJSON` method with the serialised JSON data, like `data = Data.fromJSON(JSON.parse(serialised))`.
|
||||
|
||||
By structuring the classes like this they can be easily composed together.
|
||||
For example a class holding a data property with an instance of the Data class from the previous example can be created as follows.
|
||||
|
||||
```js
|
||||
// Composed.js
|
||||
"use strict";
|
||||
const Data = require("./Data");
|
||||
|
||||
/**
|
||||
* Composed data class example
|
||||
* @alias module:Data
|
||||
*/
|
||||
class Composed {
|
||||
/** @type {module:Data|undefined} */
|
||||
data;
|
||||
|
||||
constructor(data) {
|
||||
if (data) { this.data = data; }
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
"data": Data.jsonSchema,
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
let data;
|
||||
if (json["data"]) { data = Data.fromJSON(json["data"]); }
|
||||
return new this(data);
|
||||
}
|
||||
|
||||
// toJSON is optional and not needed for this particular class
|
||||
}
|
||||
|
||||
module.exports = Composed;
|
||||
```
|
||||
|
||||
It's easy to create a new instance of Composed by calling `new Composed(new Data("spam"))` and serialising to and from JSON is the same as for Data.
|
||||
Meaning `serialised = JSON.stringify(composed)` and `composed = Composed.fromJSON(JSON.parse(serialised))` does as expected.
|
||||
|
||||
## Interface description
|
||||
|
||||
Data classes are ES6 style class declarations with the following methods and fields:
|
||||
|
||||
### toJSON(): any (optional)
|
||||
|
||||
Method returning an object or primitive suitable for passing to JSON.stringify in order to create a JSON representation of the class instance.
|
||||
The returned object may contain objects that have their own toJSON methods which JSON.stringify will then call recursively.
|
||||
|
||||
This method is optional if calling `JSON.stringify` on an instance without it would produce the desired serialised form, however this is rarely the case as often you'll have to provide it to convert the values fram Map/Set properites, omit default values or do similar actions.
|
||||
|
||||
Note that the primary use case for serialising data classes is to send them over the network, this method should therefore try to reduce the size of the resulting JSON by for example omitting properties who's values are the default value.
|
||||
|
||||
### static fromJSON(json: any): this
|
||||
|
||||
Method constructing an instance of this class from one that was previously serialised by passing an instance of it to `JSON.stringify`.
|
||||
|
||||
While not a requirement it is preferrable if the input is not mutated and that no references to any any structures in the input ends up in the returned object.
|
||||
In other words calling fromJSON twice with the same input should produce two independent objects.
|
||||
|
||||
Note that directly passing the output of `toJSON` to `fromJSON` is not valid, as the output of `toJSON` may require further expansion calling `toJSON` on objects returned by it.
|
||||
It has to go through `JSON.stringify` or an equivalent recursive expansion of `toJSON`.
|
||||
|
||||
### static jsonSchema
|
||||
|
||||
A [JSON schema](http://json-schema.org/) describing the format of the JSON resulting from passing an instance of this class to `JSON.stringify`.
|
||||
This should be lenient towards adding additional properties that would be ignored by `fromJSON` as it makes it easier to extend these classes in the future without breaking compatibility.
|
||||
+4
-3
@@ -8,6 +8,7 @@
|
||||
"fast-cover": "FAST_TEST=y nyc pnpm test",
|
||||
"ci-cover": "nyc --reporter=lcovonly pnpm run-script test",
|
||||
"lint": "eslint \"*.js\" packages plugins test",
|
||||
"lint-fix": "eslint --fix \"*.js\" packages plugins test",
|
||||
"docs": "jsdoc --configure docs/jsdoc.json",
|
||||
"bundle-dependencies": "bundle-dependencies"
|
||||
},
|
||||
@@ -21,16 +22,16 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clusterio/ctl": "workspace:*",
|
||||
"@clusterio/lib": "workspace:*",
|
||||
"@clusterio/controller": "workspace:*",
|
||||
"@clusterio/ctl": "workspace:*",
|
||||
"@clusterio/host": "workspace:*",
|
||||
"@clusterio/lib": "workspace:*",
|
||||
"eslint": "^8.18.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"express": "^4.16.2",
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"jsdoc": "^3.6.6",
|
||||
"jsdoc": "^4.0.2",
|
||||
"jsonwebtoken": "^8.2.1",
|
||||
"jszip": "^3.2.2",
|
||||
"mocha": "^9.2.2",
|
||||
|
||||
@@ -28,6 +28,7 @@ const jwt = require("jsonwebtoken");
|
||||
// homebrew modules
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
const libFileOps = require("@clusterio/lib/file_ops");
|
||||
const libLink = require("@clusterio/lib/link");
|
||||
const libPluginLoader = require("@clusterio/lib/plugin_loader");
|
||||
const libPrometheus = require("@clusterio/lib/prometheus");
|
||||
const libConfig = require("@clusterio/lib/config");
|
||||
@@ -102,7 +103,7 @@ void new libPrometheus.Gauge(
|
||||
{
|
||||
labels: ["type"], callback: async function(gauge) {
|
||||
gauge.labels("host").set(controller.wsServer.hostConnections.size);
|
||||
gauge.labels("control").set(controller.wsServer.controlConnections.length);
|
||||
gauge.labels("control").set(controller.wsServer.controlConnections.size);
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -290,6 +291,7 @@ async function initialize() {
|
||||
|
||||
logger.info("Loading Plugin info");
|
||||
parameters.pluginInfos = await libPluginLoader.loadPluginInfos(pluginList);
|
||||
libLink.registerPluginMessages(parameters.pluginInfos);
|
||||
libConfig.registerPluginConfigGroups(parameters.pluginInfos);
|
||||
libConfig.finalizeConfigs();
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use strict";
|
||||
const libData = require("@clusterio/lib/data");
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
const libLink = require("@clusterio/lib/link");
|
||||
const { logger } = require("@clusterio/lib/logging");
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
const ControllerRouter = require("./ControllerRouter");
|
||||
|
||||
|
||||
/**
|
||||
@@ -12,68 +14,28 @@ const libPlugin = require("@clusterio/lib/plugin");
|
||||
* @alias module:controller/src/BaseConnection
|
||||
*/
|
||||
class BaseConnection extends libLink.Link {
|
||||
constructor(target, connector, controller) {
|
||||
constructor(connector, controller) {
|
||||
/** @member {module:controller/src/WsServerConnector} module:controller/src/BaseConnection#connector */
|
||||
super("controller", target, connector);
|
||||
super(connector);
|
||||
this.router = new ControllerRouter(controller);
|
||||
this._controller = controller;
|
||||
this._disconnecting = false;
|
||||
libLink.attachAllMessages(this);
|
||||
for (let controllerPlugin of this._controller.plugins.values()) {
|
||||
libPlugin.attachPluginMessages(this, controllerPlugin);
|
||||
}
|
||||
}
|
||||
for (let [Request, handler] of controller._registeredRequests) { this.handle(Request, handler); }
|
||||
for (let [Request, handler] of controller._fallbackedRequests) { this.fallbackRequest(Request, handler); }
|
||||
for (let [Event, handler] of controller._registeredEvents) { this.handle(Event, handler); }
|
||||
for (let [Event, handler] of controller._snoopedEvents) { this.snoopEvent(Event, handler); }
|
||||
|
||||
async forwardRequestToInstance(message, request) {
|
||||
return await this._controller.forwardRequestToInstance(request, message.data);
|
||||
}
|
||||
|
||||
async forwardEventToInstance(message, event) {
|
||||
let instance = this._controller.instances.get(message.data.instance_id);
|
||||
if (!instance) { return; }
|
||||
|
||||
let hostId = instance.config.get("instance.assigned_host");
|
||||
if (hostId === null) { return; }
|
||||
|
||||
let connection = this._controller.wsServer.hostConnections.get(hostId);
|
||||
if (!connection || connection.closing) { return; }
|
||||
if (event.plugin && !connection.plugins.has(event.plugin)) { return; }
|
||||
|
||||
event.send(connection, message.data);
|
||||
}
|
||||
|
||||
async broadcastEventToHosts(message, event) {
|
||||
for (let hostConnection of this._controller.wsServer.hostConnections.values()) {
|
||||
// Do not broadcast back to the source
|
||||
if (hostConnection === this) { continue; }
|
||||
if (hostConnection.connector.closing) { continue; }
|
||||
if (event.plugin && !hostConnection.plugins.has(event.plugin)) { continue; }
|
||||
|
||||
event.send(hostConnection, message.data);
|
||||
}
|
||||
}
|
||||
|
||||
async broadcastEventToInstance(message, event) {
|
||||
await this.broadcastEventToHosts(message, event);
|
||||
}
|
||||
|
||||
async prepareDisconnectRequestHandler(message, request) {
|
||||
await libPlugin.invokeHook(this._controller.plugins, "onPrepareHostDisconnect", this);
|
||||
this._disconnecting = true;
|
||||
this.connector.setClosing();
|
||||
return await super.prepareDisconnectRequestHandler(message, request);
|
||||
this.handle(libData.ModPackGetRequest, this.handleModPackGetRequest.bind(this));
|
||||
this.handle(libData.ModPackGetDefaultRequest, this.handleModPackGetDefaultRequest.bind(this));
|
||||
}
|
||||
|
||||
async disconnect(code, reason) {
|
||||
this.connector.setClosing();
|
||||
try {
|
||||
await libLink.messages.prepareDisconnect.send(this);
|
||||
await this.connector.disconnect();
|
||||
} catch (err) {
|
||||
if (!(err instanceof libErrors.SessionLost)) {
|
||||
logger.error(`"Unexpected error preparing disconnect:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
await this.connector.close(code, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,19 +44,19 @@ class BaseConnection extends libLink.Link {
|
||||
* @type {boolean}
|
||||
*/
|
||||
get connected() {
|
||||
return !this._disconnecting && this.connector.connected;
|
||||
return this.connector.connected;
|
||||
}
|
||||
|
||||
async getModPackRequestHandler(message) {
|
||||
let { id } = message.data;
|
||||
async handleModPackGetRequest(request) {
|
||||
let { id } = request;
|
||||
let modPack = this._controller.modPacks.get(id);
|
||||
if (!modPack) {
|
||||
throw new libErrors.RequestError(`Mod pack with ID ${id} does not exist`);
|
||||
}
|
||||
return { mod_pack: modPack.toJSON() };
|
||||
return modPack;
|
||||
}
|
||||
|
||||
async getDefaultModPackRequestHandler(message) {
|
||||
async handleModPackGetDefaultRequest() {
|
||||
let id = this._controller.config.get("controller.default_mod_pack_id");
|
||||
if (id === null) {
|
||||
throw new libErrors.RequestError("Default mod pack not set on controller");
|
||||
@@ -103,7 +65,7 @@ class BaseConnection extends libLink.Link {
|
||||
if (!modPack) {
|
||||
throw new libErrors.RequestError(`Default mod pack configured (${id}) does not exist`);
|
||||
}
|
||||
return { mod_pack: modPack.toJSON() };
|
||||
return modPack;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -114,6 +114,11 @@ class Controller {
|
||||
this._state = "new";
|
||||
this._shouldStop = false;
|
||||
|
||||
this._fallbackedRequests = new Map();
|
||||
this._registeredRequests = new Map();
|
||||
this._registeredEvents = new Map();
|
||||
this._snoopedEvents = new Map();
|
||||
|
||||
this.devMiddleware = null;
|
||||
}
|
||||
|
||||
@@ -413,7 +418,7 @@ class Controller {
|
||||
}
|
||||
return new Map();
|
||||
}
|
||||
return new Map(json.map(e => [e.id, new libData.ModPack(e)]));
|
||||
return new Map(json.map(e => [e.id, libData.ModPack.fromJSON(e)]));
|
||||
}
|
||||
|
||||
static async saveModPacks(filePath, modPacks) {
|
||||
@@ -559,16 +564,16 @@ class Controller {
|
||||
}
|
||||
|
||||
hostUpdated(host) {
|
||||
let update = {
|
||||
agent: host.agent,
|
||||
version: host.version,
|
||||
id: host.id,
|
||||
name: host.name,
|
||||
public_address: host.public_address || null,
|
||||
connected: this.wsServer.hostConnections.has(host.id),
|
||||
};
|
||||
let update = new libData.HostDetails(
|
||||
host.agent,
|
||||
host.version,
|
||||
host.name,
|
||||
host.id,
|
||||
this.wsServer.hostConnections.has(host.id),
|
||||
host.public_address === null ? undefined : host.public_address,
|
||||
);
|
||||
|
||||
for (let controlConnection of this.wsServer.controlConnections) {
|
||||
for (let controlConnection of this.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.connector.closing) {
|
||||
continue;
|
||||
}
|
||||
@@ -602,7 +607,8 @@ class Controller {
|
||||
* let instanceConfig = new libConfig.InstanceConfig("controller");
|
||||
* await instanceConfig.init();
|
||||
* instanceConfig.set("instance.name", "My instance");
|
||||
* await controller.createInstance(instanceConfig);
|
||||
* let instance = await controller.instanceAssign(instanceConfig);
|
||||
* await controller.instanceAssign(instance.id, hostId);
|
||||
*
|
||||
* @param {module:lib/config.InstanceConfig} instanceConfig -
|
||||
* Config to base newly created instance on.
|
||||
@@ -681,17 +687,16 @@ class Controller {
|
||||
if (currentAssignedHost !== null && hostId !== currentAssignedHost) {
|
||||
let oldHostConnection = this.wsServer.hostConnections.get(currentAssignedHost);
|
||||
if (oldHostConnection && !oldHostConnection.connector.closing) {
|
||||
await libLink.messages.unassignInstance.send(oldHostConnection, { instance_id: instanceId });
|
||||
await oldHostConnection.send(new libData.InstanceUnassignInternalRequest(instanceId));
|
||||
}
|
||||
}
|
||||
|
||||
// Assign to target
|
||||
instance.config.set("instance.assigned_host", hostId);
|
||||
if (hostId !== null) {
|
||||
await libLink.messages.assignInstance.send(newHostConnection, {
|
||||
instance_id: instanceId,
|
||||
serialized_config: instance.config.serialize("host"),
|
||||
});
|
||||
await newHostConnection.send(
|
||||
new libData.InstanceAssignInternalRequest(instanceId, instance.config.serialize("host"))
|
||||
);
|
||||
} else {
|
||||
instance.status = "unassigned";
|
||||
this.instanceUpdated(instance);
|
||||
@@ -708,8 +713,9 @@ class Controller {
|
||||
*/
|
||||
async instanceDelete(instanceId) {
|
||||
let instance = this.getRequestInstance(instanceId);
|
||||
if (instance.config.get("instance.assigned_host") !== null) {
|
||||
await this.forwardRequestToInstance(libLink.messages.deleteInstance, { instance_id: instanceId });
|
||||
let hostId = instance.config.get("instance.assigned_host");
|
||||
if (hostId !== null) {
|
||||
await this.sendTo({ hostId }, new libData.InstanceDeleteInternalRequest(instanceId));
|
||||
}
|
||||
this.instances.delete(instanceId);
|
||||
|
||||
@@ -733,10 +739,9 @@ class Controller {
|
||||
if (hostId !== null) {
|
||||
let connection = this.wsServer.hostConnections.get(hostId);
|
||||
if (connection) {
|
||||
await libLink.messages.assignInstance.send(connection, {
|
||||
instance_id: instance.id,
|
||||
serialized_config: instance.config.serialize("host"),
|
||||
});
|
||||
await connection.send(
|
||||
new libData.InstanceAssignInternalRequest(instance.id, instance.config.serialize("host"))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -754,7 +759,7 @@ class Controller {
|
||||
}
|
||||
|
||||
instanceUpdated(instance) {
|
||||
for (let controlConnection of this.wsServer.controlConnections) {
|
||||
for (let controlConnection of this.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.connector.closing) {
|
||||
continue;
|
||||
}
|
||||
@@ -763,18 +768,18 @@ class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
saveListUpdate(data) {
|
||||
for (let controlConnection of this.wsServer.controlConnections) {
|
||||
saveListUpdate(instanceId, saves) {
|
||||
for (let controlConnection of this.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.connector.closing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
controlConnection.saveListUpdate(data);
|
||||
controlConnection.saveListUpdate(instanceId, saves);
|
||||
}
|
||||
}
|
||||
|
||||
modPackUpdated(modPack) {
|
||||
for (let controlConnection of this.wsServer.controlConnections) {
|
||||
for (let controlConnection of this.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.connector.closing) {
|
||||
continue;
|
||||
}
|
||||
@@ -785,7 +790,7 @@ class Controller {
|
||||
}
|
||||
|
||||
modUpdated(mod) {
|
||||
for (let controlConnection of this.wsServer.controlConnections) {
|
||||
for (let controlConnection of this.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.connector.closing) {
|
||||
continue;
|
||||
}
|
||||
@@ -797,7 +802,7 @@ class Controller {
|
||||
}
|
||||
|
||||
userUpdated(user) {
|
||||
for (let controlConnection of this.wsServer.controlConnections) {
|
||||
for (let controlConnection of this.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.connector.closing) {
|
||||
continue;
|
||||
}
|
||||
@@ -812,15 +817,15 @@ class Controller {
|
||||
* @param {module:lib/users.User} user - User permisions updated for.
|
||||
*/
|
||||
userPermissionsUpdated(user) {
|
||||
for (let controlConnection of this.wsServer.controlConnections) {
|
||||
for (let controlConnection of this.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.user === user) {
|
||||
libLink.messages.accountUpdate.send(controlConnection, {
|
||||
"roles": [...user.roles].map(r => ({
|
||||
controlConnection.send(
|
||||
new libData.AccountUpdateEvent([...user.roles].map(r => ({
|
||||
name: r.name,
|
||||
id: r.id,
|
||||
permissions: [...r.permissions],
|
||||
})),
|
||||
});
|
||||
})))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -831,15 +836,17 @@ class Controller {
|
||||
* @param {module:lib/users.Role} role - Role permisions updated for.
|
||||
*/
|
||||
rolePermissionsUpdated(role) {
|
||||
for (let controlConnection of this.wsServer.controlConnections) {
|
||||
for (let controlConnection of this.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.user.roles.has(role)) {
|
||||
libLink.messages.accountUpdate.send(controlConnection, {
|
||||
"roles": [...controlConnection.user.roles].map(r => ({
|
||||
name: r.name,
|
||||
id: r.id,
|
||||
permissions: [...r.permissions],
|
||||
})),
|
||||
});
|
||||
controlConnection.send(
|
||||
new libData.AccountUpdateEvent(
|
||||
[...controlConnection.user.roles].map(r => ({
|
||||
name: r.name,
|
||||
id: r.id,
|
||||
permissions: [...r.permissions],
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -964,35 +971,196 @@ class Controller {
|
||||
};
|
||||
}
|
||||
|
||||
handle(Class, handler) {
|
||||
if (Class.type === "request") {
|
||||
this.handleRequest(Class, handler);
|
||||
} else if (Class.type === "event") {
|
||||
this.handleEvent(Class, handler);
|
||||
} else {
|
||||
throw new Error(`Class ${Class.name} has unrecognized type ${Class.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
handleRequest(Request, handler) {
|
||||
if (!libLink.Link._requestsByClass.has(Request)) {
|
||||
throw new Error(`Unregistered Request class ${Request.name}`);
|
||||
}
|
||||
if (this._registeredRequests.has(Request)) {
|
||||
throw new Error(`Request ${Request.name} is already registered`);
|
||||
}
|
||||
this._registeredRequests.set(Request, handler);
|
||||
}
|
||||
|
||||
fallbackRequest(Request, handler) {
|
||||
if (!libLink.Link._requestsByClass.has(Request)) {
|
||||
throw new Error(`Unregistered Request class ${Request.name}`);
|
||||
}
|
||||
if (this._fallbackedRequests.has(Request)) {
|
||||
throw new Error(`Request ${Request.name} is already fallbacked`);
|
||||
}
|
||||
this._fallbackedRequests.set(Request, handler);
|
||||
}
|
||||
|
||||
handleEvent(Event, handler) {
|
||||
if (!libLink.Link._eventsByClass.has(Event)) {
|
||||
throw new Error(`Unregistered Event class ${Event.name}`);
|
||||
}
|
||||
if (this._registeredEvents.has(Event)) {
|
||||
throw new Error(`Event ${Event.name} is already registered`);
|
||||
}
|
||||
this._registeredEvents.set(Event, handler);
|
||||
}
|
||||
|
||||
snoopEvent(Event, handler) {
|
||||
if (!libLink.Link._eventsByClass.has(Event)) {
|
||||
throw new Error(`Unregistered Event class ${Event.name}`);
|
||||
}
|
||||
if (this._snoopedEvents.has(Event)) {
|
||||
throw new Error(`Event ${Event.name} is already snooped`);
|
||||
}
|
||||
this._snoopedEvents.set(Event, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward the given request to the host contaning the instance
|
||||
* Send request or event
|
||||
*
|
||||
* Finds the host which the instance with the given instance ID is
|
||||
* currently assigned to and forwards the request to that instance.
|
||||
* The request data must have an instance_id parameter.
|
||||
* Routes the given request or event to the given destination. The
|
||||
* destination argument supports address shorthands, see {@link
|
||||
* module:lib/data.Address.fromShorthand}
|
||||
*
|
||||
* @param {module:lib/link.Request} request - The request to send.
|
||||
* @param {object} data - Data to pass with the request.
|
||||
* @param {number} data.instance_id -
|
||||
* ID of instance to send this request towards
|
||||
* @returns {Promise<object>} data returned from the request.
|
||||
* @param {*|module:lib/data.Address} address - Where to send it.
|
||||
* @param {*} requestOrEvent - The request or event to send.
|
||||
* @returns {Promise<*>|undefined}
|
||||
* Promise that resolves to the response if a request was sent or
|
||||
* undefined if it was an event.
|
||||
*/
|
||||
async forwardRequestToInstance(request, data) {
|
||||
let instance = this.getRequestInstance(data.instance_id);
|
||||
sendTo(address, requestOrEvent) {
|
||||
let dst = libData.Address.fromShorthand(address);
|
||||
if (requestOrEvent.constructor.type === "request") {
|
||||
return this.sendRequest(requestOrEvent, dst);
|
||||
}
|
||||
if (requestOrEvent.constructor.type === "event") {
|
||||
return this.sendEvent(requestOrEvent, dst);
|
||||
}
|
||||
throw Error(`Unknown type ${requestOrEvent.constructor.type}.`);
|
||||
}
|
||||
|
||||
async sendRequest(request, dst) {
|
||||
let connection;
|
||||
if (dst.type === libData.Address.controller) {
|
||||
throw new Error("controller as dst is not supported.");
|
||||
|
||||
} else if (dst.type === libData.Address.control) {
|
||||
connection = this.wsServer.controlConnections.get(dst.id);
|
||||
if (!connection) {
|
||||
throw new libErrors.RequestError("Target control connection does not exist.");
|
||||
}
|
||||
|
||||
} else if (dst.type === libData.Address.instance) {
|
||||
let instance = this.getRequestInstance(dst.id);
|
||||
let hostId = instance.config.get("instance.assigned_host");
|
||||
if (hostId === null) {
|
||||
throw new libErrors.RequestError("Instance is not assigned to a host");
|
||||
}
|
||||
connection = this.wsServer.hostConnections.get(hostId);
|
||||
if (!connection) {
|
||||
throw new libErrors.RequestError("Host containing instance is not connected");
|
||||
}
|
||||
|
||||
} else if (dst.type === libData.Address.host) {
|
||||
connection = this.wsServer.hostConnections.get(dst.id);
|
||||
if (!connection) {
|
||||
throw new libErrors.RequestError("Host is not connected");
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`Unknown address type ${dst.type}`);
|
||||
}
|
||||
|
||||
return connection.sendRequest(request, dst);
|
||||
}
|
||||
|
||||
sendEvent(event, dst) {
|
||||
let connection;
|
||||
if (dst.type === libData.Address.controller) {
|
||||
throw new Error("controller as dst is not supported.");
|
||||
|
||||
} else if (dst.type === libData.Address.control) {
|
||||
connection = this.wsServer.controlConnections.get(dst.id);
|
||||
|
||||
} else if (dst.type === libData.Address.instance) {
|
||||
let instance = this.instances.get(dst.id);
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
let hostId = instance.config.get("instance.assigned_host");
|
||||
if (hostId === null) {
|
||||
return;
|
||||
}
|
||||
connection = this.wsServer.hostConnections.get(hostId);
|
||||
|
||||
} else if (dst.type === libData.Address.host) {
|
||||
connection = this.wsServer.hostConnections.get(dst.id);
|
||||
|
||||
} else if (dst.type === libData.Address.broadcast) {
|
||||
if (dst.id === libData.Address.control) {
|
||||
for (let controlConnection of this.wsServer.controlConnections.values()) {
|
||||
controlConnection.sendEvent(event, dst);
|
||||
}
|
||||
|
||||
} else if (dst.id === libData.Address.instance || dst.id === libData.Address.host) {
|
||||
for (let hostConnection of this.wsServer.hostConnections.values()) {
|
||||
hostConnection.sendEvent(event, dst);
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`Unexpected broacdast target ${dst.id}`);
|
||||
}
|
||||
return;
|
||||
|
||||
} else {
|
||||
throw new Error(`Unknown address type ${dst.type}`);
|
||||
}
|
||||
|
||||
if (connection) {
|
||||
connection.sendEvent(event, dst);
|
||||
}
|
||||
}
|
||||
|
||||
sendToHostByInstanceId(requestOrEvent) {
|
||||
if (requestOrEvent.constructor.type === "request") {
|
||||
return this.sendRequestToHostByInstanceId(requestOrEvent);
|
||||
}
|
||||
if (requestOrEvent.constructor.type === "event") {
|
||||
return this.sendEventToHostByInstanceId(requestOrEvent);
|
||||
}
|
||||
throw Error(`Unknown type ${requestOrEvent.constructor.type}.`);
|
||||
}
|
||||
|
||||
async sendRequestToHostByInstanceId(request) {
|
||||
let instance = this.getRequestInstance(request.instanceId);
|
||||
let hostId = instance.config.get("instance.assigned_host");
|
||||
if (hostId === null) {
|
||||
throw new libErrors.RequestError("Instance is not assigned to a host");
|
||||
}
|
||||
|
||||
let connection = this.wsServer.hostConnections.get(hostId);
|
||||
if (!connection) {
|
||||
throw new libErrors.RequestError("Host containing instance is not connected");
|
||||
}
|
||||
if (request.plugin && !connection.plugins.has(request.plugin)) {
|
||||
throw new libErrors.RequestError(`Host containing instance does not have ${request.plugin} plugin`);
|
||||
}
|
||||
return await connection.send(request);
|
||||
}
|
||||
|
||||
return await request.send(connection, data);
|
||||
sendEventToHostByInstanceId(event) {
|
||||
let instance = this.getRequestInstance(event.instanceId);
|
||||
let hostId = instance.config.get("instance.assigned_host");
|
||||
if (hostId === null) {
|
||||
return;
|
||||
}
|
||||
let connection = this.wsServer.hostConnections.get(hostId);
|
||||
if (!connection) {
|
||||
return;
|
||||
}
|
||||
connection.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"use strict";
|
||||
const libData = require("@clusterio/lib/data");
|
||||
|
||||
class ControllerRouter {
|
||||
/** @type {module:controller/src/Controller} */
|
||||
controller;
|
||||
|
||||
pendingRequests = new Map();
|
||||
|
||||
constructor(controller) {
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
addHostConnection(connection) {
|
||||
this.hostConnections.set(connection.id, connection);
|
||||
}
|
||||
|
||||
addControlConnection(connection) {
|
||||
this.controlConnections.set(connection.id, connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {module:lib/link.Link} origin - Source link of the message.
|
||||
* @param {module:lib/data.Message} message
|
||||
* Link the message originated from.
|
||||
* @param {boolean} hasFallback - true if fallback handling is available.
|
||||
* @returns {boolean} true if the message was handled
|
||||
*/
|
||||
forwardMessage(origin, message, hasFallback) {
|
||||
if (!["request", "response", "responseError", "event"].includes(message.type)) {
|
||||
throw new Error(`Message type ${message.type} can't be forwarded`);
|
||||
}
|
||||
|
||||
let dst = message.dst;
|
||||
let nextHop;
|
||||
let msg;
|
||||
if (dst.type === libData.Address.broadcast) {
|
||||
this.broadcastMessage(origin, message);
|
||||
return true;
|
||||
} else if (dst.type === libData.Address.host) {
|
||||
nextHop = this.controller.wsServer.hostConnections.get(dst.id);
|
||||
if (!nextHop) {
|
||||
msg = `Host ${dst.id} is offline`;
|
||||
}
|
||||
} else if (dst.type === libData.Address.instance) {
|
||||
let instance = this.controller.instances.get(dst.id);
|
||||
if (!instance) {
|
||||
msg = `Instance ${dst.id} does not exist`;
|
||||
} else {
|
||||
let assignedHost = instance.config.get("instance.assigned_host");
|
||||
if (assignedHost === null) {
|
||||
msg = `Instance ${dst.id} is not assigned a host`;
|
||||
} else {
|
||||
nextHop = this.controller.wsServer.hostConnections.get(assignedHost);
|
||||
if (!nextHop) {
|
||||
msg = `Assigned host for instance ${dst.id} is offline`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (dst.type === libData.Address.control) {
|
||||
nextHop = this.controller.wsServer.controlConnections.get(dst.id);
|
||||
if (!nextHop) {
|
||||
msg = "Control connection does not exist";
|
||||
}
|
||||
} else if (dst.type === libData.Address.controller) {
|
||||
msg = `Unexpected message forwarded to ${dst}`;
|
||||
}
|
||||
|
||||
if (nextHop === origin) {
|
||||
msg = `Message would return back to sender ${origin.dst}.`;
|
||||
nextHop = undefined;
|
||||
}
|
||||
|
||||
if (message.type === "request") {
|
||||
if (!nextHop) {
|
||||
if (hasFallback) {
|
||||
return false;
|
||||
}
|
||||
origin.connector.sendResponseError(
|
||||
new libData.ResponseError(msg || "Unroutable destination"), message.src
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextHop) {
|
||||
if (message.type === "request") {
|
||||
nextHop.forwardRequest(message, origin);
|
||||
} else {
|
||||
nextHop.connector.send(message);
|
||||
}
|
||||
} else {
|
||||
this.warnUnrouted(message, msg);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
broadcastMessage(origin, message) {
|
||||
let dst = message.dst;
|
||||
if (message.type !== "event") {
|
||||
this.warnUnrouted(message, `Unexpected broadcast of ${message.type}`);
|
||||
} else if (dst.id === libData.Address.host || dst.id === libData.Address.instance) {
|
||||
for (let hostConnection of this.controller.wsServer.hostConnections.values()) {
|
||||
if (hostConnection !== origin) {
|
||||
hostConnection.connector.send(message);
|
||||
}
|
||||
}
|
||||
} else if (dst.id === libData.Address.control) {
|
||||
for (let controlConnection of this.controller.wsServer.controlConnections.values()) {
|
||||
if (controlConnection !== origin) {
|
||||
controlConnection.connector.send(message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.warnUnrouted(message, `Unexpected broacdast target ${dst.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
warnUnrouted(message, msg) {
|
||||
let dst = message.dst;
|
||||
let baseMsg = `No destination for ${message.constructor.name} routed from ${message.src} to ${dst}`;
|
||||
logger.warn(msg ? `${baseMsg}: ${msg}.` : `${baseMsg}.`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ControllerRouter;
|
||||
@@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
const libData = require("@clusterio/lib/data");
|
||||
const libConfig = require("@clusterio/lib/config");
|
||||
const libLink = require("@clusterio/lib/link");
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
const { logger } = require("@clusterio/lib/logging");
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
const PlayerStats = require("@clusterio/lib/PlayerStats");
|
||||
@@ -17,7 +18,7 @@ const InstanceInfo = require("./InstanceInfo");
|
||||
*/
|
||||
class HostConnection extends BaseConnection {
|
||||
constructor(registerData, connector, controller) {
|
||||
super("host", connector, controller);
|
||||
super(connector, controller);
|
||||
|
||||
this._agent = registerData.agent;
|
||||
this._id = registerData.id;
|
||||
@@ -31,7 +32,7 @@ class HostConnection extends BaseConnection {
|
||||
id: this._id,
|
||||
name: this._name,
|
||||
version: this._version,
|
||||
public_address: registerData.public_address,
|
||||
public_address: registerData.publicAddress,
|
||||
plugins: registerData.plugins,
|
||||
});
|
||||
|
||||
@@ -57,6 +58,41 @@ class HostConnection extends BaseConnection {
|
||||
libPlugin.invokeHook(this._controller.plugins, "onInstanceStatusChanged", instance, prev);
|
||||
}
|
||||
});
|
||||
|
||||
this.handle(libData.InstanceStatusChangedEvent, this.handleInstanceStatusChangedEvent.bind(this));
|
||||
this.handle(libData.InstancesUpdateRequest, this.handleInstancesUpdateRequest.bind(this));
|
||||
this.handle(libData.InstanceSaveListUpdateEvent, this.handleInstanceSaveListUpdateEvent.bind(this));
|
||||
this.handle(libData.LogMessageEvent, this.handleLogMessageEvent.bind(this));
|
||||
this.handle(libData.InstancePlayerUpdateEvent, this.handleInstancePlayerUpdateEvent.bind(this));
|
||||
}
|
||||
|
||||
validateIngress(message) {
|
||||
let origin = this.connector.dst;
|
||||
switch (message.src.type) {
|
||||
case libData.Address.control:
|
||||
case libData.Address.controller:
|
||||
throw new libErrors.InvalidMessage(`Received message with invalid src ${message.src} from ${origin}`);
|
||||
|
||||
case libData.Address.host:
|
||||
if (message.src.id !== origin.id) {
|
||||
throw new libErrors.InvalidMessage(
|
||||
`Received message with invalid src ${message.src} from ${origin}`
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case libData.Address.instance:
|
||||
let instance = this._controller.instances.get(message.src.id);
|
||||
if (!instance || instance.config.get("instance.assigned_host") !== this._id) {
|
||||
throw new libErrors.InvalidMessage(
|
||||
`Received message with invalid src ${message.src} from ${origin}`
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Should be unreachable");
|
||||
}
|
||||
}
|
||||
|
||||
_checkPluginVersions() {
|
||||
@@ -83,6 +119,11 @@ class HostConnection extends BaseConnection {
|
||||
}
|
||||
}
|
||||
|
||||
async prepareDisconnect() {
|
||||
await libPlugin.invokeHook(this._controller.plugins, "onPrepareHostDisconnect", this);
|
||||
return await super.prepareDisconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* ID of the host this connection is connected to
|
||||
*
|
||||
@@ -92,15 +133,15 @@ class HostConnection extends BaseConnection {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
async instanceStatusChangedEventHandler(message, event) {
|
||||
let instance = this._controller.instances.get(message.data.instance_id);
|
||||
async handleInstanceStatusChangedEvent(request) {
|
||||
let instance = this._controller.instances.get(request.instanceId);
|
||||
|
||||
// It's possible to get updates from an instance that does not exist
|
||||
// or is not assigned to the host it originated from if it was
|
||||
// reassigned or deleted while the connection to the host it was
|
||||
// originally on was down at the time.
|
||||
if (!instance || instance.config.get("instance.assigned_host") !== this._id) {
|
||||
logger.warn(`Got bogus update for instance id ${message.data.instance_id}`);
|
||||
logger.warn(`Got bogus update for instance id ${request.instanceId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -109,41 +150,40 @@ class HostConnection extends BaseConnection {
|
||||
// changed. Hosts also send status updates on assignInstance which
|
||||
// for hacky reason is also used to push config changes and
|
||||
// restablish status after a connection loss.
|
||||
if (instance.status === message.data.status) {
|
||||
if (instance.status === request.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
let prev = instance.status;
|
||||
instance.status = message.data.status;
|
||||
instance.game_port = message.data.game_port;
|
||||
instance.status = request.status;
|
||||
instance.game_port = request.gamePort;
|
||||
logger.verbose(`Instance ${instance.config.get("instance.name")} State: ${instance.status}`);
|
||||
this._controller.instanceUpdated(instance);
|
||||
await libPlugin.invokeHook(this._controller.plugins, "onInstanceStatusChanged", instance, prev);
|
||||
}
|
||||
|
||||
async updateInstancesRequestHandler(message) {
|
||||
async handleInstancesUpdateRequest(request) {
|
||||
// Push updated instance configs
|
||||
for (let instance of this._controller.instances.values()) {
|
||||
if (instance.config.get("instance.assigned_host") === this._id) {
|
||||
await libLink.messages.assignInstance.send(this, {
|
||||
instance_id: instance.id,
|
||||
serialized_config: instance.config.serialize("host"),
|
||||
});
|
||||
await this.send(
|
||||
new libData.InstanceAssignInternalRequest(instance.id, instance.config.serialize("host"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Assign instances the host has but controller does not
|
||||
for (let instanceData of message.data.instances) {
|
||||
for (let instanceData of request.instances) {
|
||||
let instanceConfig = new libConfig.InstanceConfig("controller");
|
||||
await instanceConfig.load(instanceData.serialized_config, "host");
|
||||
await instanceConfig.load(instanceData.config, "host");
|
||||
|
||||
let controllerInstance = this._controller.instances.get(instanceConfig.get("instance.id"));
|
||||
if (controllerInstance) {
|
||||
// Check if this instance is assigned somewhere else.
|
||||
if (controllerInstance.config.get("instance.assigned_host") !== this._id) {
|
||||
await libLink.messages.unassignInstance.send(this, {
|
||||
instance_id: controllerInstance.id,
|
||||
});
|
||||
await this.send(
|
||||
new libData.InstanceUnassignInternalRequest(instanceConfig.get("instance.id"))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -152,7 +192,7 @@ class HostConnection extends BaseConnection {
|
||||
let prev = controllerInstance.status;
|
||||
controllerInstance.status = instanceData.status;
|
||||
logger.verbose(`Instance ${instanceConfig.get("instance.name")} State: ${instanceData.status}`);
|
||||
this._controller.instanceUpdated(instance);
|
||||
this._controller.instanceUpdated(controllerInstance);
|
||||
await libPlugin.invokeHook(
|
||||
this._controller.plugins, "onInstanceStatusChanged", controllerInstance, prev
|
||||
);
|
||||
@@ -164,10 +204,11 @@ class HostConnection extends BaseConnection {
|
||||
let newInstance = new InstanceInfo({ config: instanceConfig, status: instanceData.status });
|
||||
this._controller.instances.set(instanceConfig.get("instance.id"), newInstance);
|
||||
this._controller.addInstanceHooks(newInstance);
|
||||
await libLink.messages.assignInstance.send(this, {
|
||||
instance_id: instanceConfig.get("instance.id"),
|
||||
serialized_config: instanceConfig.serialize("host"),
|
||||
});
|
||||
await this.send(
|
||||
new libData.InstanceAssignInternalRequest(
|
||||
instanceConfig.get("instance.id"), instanceConfig.serialize("host")
|
||||
)
|
||||
);
|
||||
await libPlugin.invokeHook(this._controller.plugins, "onInstanceStatusChanged", newInstance, null);
|
||||
}
|
||||
|
||||
@@ -188,40 +229,43 @@ class HostConnection extends BaseConnection {
|
||||
}
|
||||
}
|
||||
|
||||
libLink.messages.syncUserLists.send(this, { adminlist, banlist, whitelist });
|
||||
await this.send(new libData.SyncUserListsEvent(adminlist, banlist, whitelist));
|
||||
}
|
||||
|
||||
async saveListUpdateEventHandler(message) {
|
||||
this._controller.saveListUpdate(message.data);
|
||||
async handleInstanceSaveListUpdateEvent(event) {
|
||||
this._controller.saveListUpdate(event.instanceId, event.saves);
|
||||
}
|
||||
|
||||
async logMessageEventHandler(message) {
|
||||
async handleLogMessageEvent(event) {
|
||||
this._controller.clusterLogger.log({
|
||||
...message.data.info,
|
||||
...event.info,
|
||||
host_id: this._id,
|
||||
host_name: this._name,
|
||||
});
|
||||
}
|
||||
|
||||
async playerEventEventHandler(message) {
|
||||
let { instance_id, name, type, stats } = message.data;
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
async handleInstancePlayerUpdateEvent(event, src) {
|
||||
let instanceId = src.id;
|
||||
let user = this._controller.userManager.users.get(event.name);
|
||||
if (!user) {
|
||||
user = this._controller.userManager.createUser(name);
|
||||
user = this._controller.userManager.createUser(event.name);
|
||||
}
|
||||
|
||||
if (type === "join") {
|
||||
user.notifyJoin(instance_id);
|
||||
} else if (type === "leave") {
|
||||
user.notifyLeave(instance_id);
|
||||
if (event.type === "join") {
|
||||
user.notifyJoin(instanceId);
|
||||
} else if (event.type === "leave") {
|
||||
user.notifyLeave(instanceId);
|
||||
}
|
||||
user.instanceStats.set(instance_id, new PlayerStats(stats));
|
||||
user.instanceStats.set(instanceId, new PlayerStats(event.stats));
|
||||
user.recalculatePlayerStats();
|
||||
this._controller.userUpdated(user);
|
||||
|
||||
delete message.data.stats;
|
||||
let instance = this._controller.instances.get(instance_id);
|
||||
await libPlugin.invokeHook(this._controller.plugins, "onPlayerEvent", instance, message.data);
|
||||
let instance = this._controller.instances.get(instanceId);
|
||||
await libPlugin.invokeHook(this._controller.plugins, "onPlayerEvent", instance, {
|
||||
type: event.type,
|
||||
name: event.name,
|
||||
reason: event.reason,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,11 +26,9 @@ class InstanceInfo {
|
||||
*/
|
||||
status;
|
||||
|
||||
// This is not actually json as a config can't be loaded synchronously but
|
||||
// that is likely to change in the future.
|
||||
constructor(json) {
|
||||
this.config = json["config"];
|
||||
this.status = json["status"];
|
||||
constructor({ config, status }) {
|
||||
this.config = config;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
|
||||
+150
-126
@@ -2,10 +2,10 @@
|
||||
const jwt = require("jsonwebtoken");
|
||||
const WebSocket = require("ws");
|
||||
|
||||
const libData = require("@clusterio/lib/data");
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
const { logger } = require("@clusterio/lib/logging");
|
||||
const libPrometheus = require("@clusterio/lib/prometheus");
|
||||
const libSchema = require("@clusterio/lib/schema");
|
||||
|
||||
const ControlConnection = require("./ControlConnection");
|
||||
const packageVersion = require("../package").version;
|
||||
@@ -40,7 +40,7 @@ class WsServer {
|
||||
this.stopAcceptingNewSessions = false;
|
||||
|
||||
/** @type {Array<module:controller/src/ControlConnection>} */
|
||||
this.controlConnections = [];
|
||||
this.controlConnections = new Map();
|
||||
/** @type {Map<number, module:controller/src/HostConnection>} */
|
||||
this.hostConnections = new Map();
|
||||
/** @type {Map<number, module:controller/src/WsServerConnector>} */
|
||||
@@ -51,6 +51,7 @@ class WsServer {
|
||||
// Unique string for the session token audience
|
||||
this.sessionAud = `session-${Date.now()}`;
|
||||
this.nextSessionId = 1;
|
||||
this.nextControlId = 1;
|
||||
|
||||
/** @type {module:ws.Server} */
|
||||
this.wss = new WebSocket.Server({
|
||||
@@ -64,7 +65,7 @@ class WsServer {
|
||||
this.stopAcceptingNewSessions = true;
|
||||
|
||||
let disconnectTasks = [];
|
||||
for (let controlConnection of this.controlConnections) {
|
||||
for (let controlConnection of this.controlConnections.values()) {
|
||||
disconnectTasks.push(controlConnection.disconnect(1001, "Server Quit"));
|
||||
}
|
||||
|
||||
@@ -122,11 +123,7 @@ class WsServer {
|
||||
loadedPlugins[name] = plugin.info.version;
|
||||
}
|
||||
|
||||
socket.send(JSON.stringify({ seq: null, type: "hello", data: {
|
||||
version: packageVersion,
|
||||
plugins: loadedPlugins,
|
||||
}}));
|
||||
|
||||
socket.send(JSON.stringify(new libData.MessageHello(new libData.HelloData(packageVersion, loadedPlugins))));
|
||||
this.attachHandler(socket, req);
|
||||
}
|
||||
|
||||
@@ -159,49 +156,31 @@ ${err.stack}`
|
||||
});
|
||||
}
|
||||
|
||||
async handleHandshake(message, socket, req) {
|
||||
async handleHandshake(rawMessage, socket, req) {
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(message);
|
||||
const json = JSON.parse(rawMessage);
|
||||
libData.Message.validate(json);
|
||||
message = libData.Message.fromJSON(json);
|
||||
} catch (err) {
|
||||
logger.verbose(`WsServer | closing ${req.socket.remoteAddress} after receiving invalid JSON`);
|
||||
logger.verbose(`WsServer | closing ${req.socket.remoteAddress} after receiving invalid message`);
|
||||
wsRejectedConnectionsCounter.inc();
|
||||
socket.close(1002, "Invalid JSON");
|
||||
socket.close(1002, `Invalid message: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!libSchema.clientHandshake(message)) {
|
||||
if (message.type === "resume") {
|
||||
this.resumeSession(message.data, socket, req);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type !== "registerHost" && message.type !== "registerControl") {
|
||||
logger.verbose(`WsServer | closing ${req.socket.remoteAddress} after receiving invalid handshake`);
|
||||
wsRejectedConnectionsCounter.inc();
|
||||
socket.close(1002, "Bad handshake");
|
||||
return;
|
||||
}
|
||||
|
||||
let { type, data } = message;
|
||||
|
||||
if (type === "resume") {
|
||||
let connector;
|
||||
try {
|
||||
let payload = jwt.verify(
|
||||
data.session_token,
|
||||
Buffer.from(this.controller.config.get("controller.auth_secret"), "base64"),
|
||||
{ audience: this.sessionAud }
|
||||
);
|
||||
|
||||
connector = this.activeConnectors.get(payload.sid);
|
||||
if (!connector) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
socket.send(JSON.stringify({ seq: null, type: "invalidate", data: {}}));
|
||||
this.attachHandler(socket, req);
|
||||
return;
|
||||
}
|
||||
|
||||
connector.continue(socket, data.last_seq);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.stopAcceptingNewSessions) {
|
||||
logger.verbose(`WsServer | closing ${req.socket.remoteAddress}, server is shutting down`);
|
||||
wsRejectedConnectionsCounter.inc();
|
||||
@@ -209,46 +188,20 @@ ${err.stack}`
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate token
|
||||
let user;
|
||||
try {
|
||||
if (type === "register_host") {
|
||||
let tokenPayload = jwt.verify(
|
||||
data.token,
|
||||
Buffer.from(this.controller.config.get("controller.auth_secret"), "base64"),
|
||||
// migrate: allow pre-rename tokens issued to hosts before alpha-14
|
||||
{ audience: ["host", "slave"] }
|
||||
);
|
||||
|
||||
// migrate: allow pre-rename tokens issued to hosts before alpha-14
|
||||
if ((tokenPayload.host !== undefined ? tokenPayload.host : tokenPayload.slave) !== data.id) {
|
||||
throw new Error("missmatched host id");
|
||||
}
|
||||
|
||||
} else if (type === "register_control") {
|
||||
let tokenPayload = jwt.verify(
|
||||
data.token,
|
||||
Buffer.from(this.controller.config.get("controller.auth_secret"), "base64"),
|
||||
{ audience: "user" }
|
||||
);
|
||||
|
||||
user = this.controller.userManager.users.get(tokenPayload.user);
|
||||
if (!user) {
|
||||
throw new Error("invalid user");
|
||||
}
|
||||
if (tokenPayload.iat < user.tokenValidAfter) {
|
||||
throw new Error("invalid token");
|
||||
}
|
||||
user.checkPermission("core.control.connect");
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.verbose(`WsServer | authentication failed for ${req.socket.remoteAddress}`);
|
||||
wsRejectedConnectionsCounter.inc();
|
||||
socket.close(4003, `Authentication failed: ${err.message}`);
|
||||
if (message.type === "registerHost") {
|
||||
await this.registerHost(message.data, socket, req);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === "registerControl") {
|
||||
await this.registerControl(message.data, socket, req);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("This should be unreachable");
|
||||
}
|
||||
|
||||
createSession(dst) {
|
||||
let sessionId = this.nextSessionId;
|
||||
this.nextSessionId += 1;
|
||||
let sessionToken = jwt.sign(
|
||||
@@ -257,66 +210,137 @@ ${err.stack}`
|
||||
);
|
||||
let sessionTimeout = this.controller.config.get("controller.session_timeout");
|
||||
let heartbeatInterval = this.controller.config.get("controller.heartbeat_interval");
|
||||
let connector = new WsServerConnector(sessionId, sessionTimeout, heartbeatInterval);
|
||||
let connector = new WsServerConnector(dst, sessionId, sessionTimeout, heartbeatInterval);
|
||||
this.activeConnectors.set(sessionId, connector);
|
||||
connector.on("close", () => {
|
||||
this.activeConnectors.delete(sessionId);
|
||||
});
|
||||
|
||||
let additionalReadyData = {};
|
||||
if (type === "register_host") {
|
||||
let connection = this.hostConnections.get(data.id);
|
||||
if (connection) {
|
||||
logger.verbose(`WsServer | disconnecting existing connection for host ${data.id}`);
|
||||
await connection.disconnect(1008, "Registered from another connection");
|
||||
}
|
||||
return [connector, sessionToken];
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`WsServer | registered host ${data.name} (${data.id}) using agent ${data.agent} ${data.version}`
|
||||
async registerHost(data, socket, req) {
|
||||
try {
|
||||
let tokenPayload = jwt.verify(
|
||||
data.token,
|
||||
Buffer.from(this.controller.config.get("controller.auth_secret"), "base64"),
|
||||
// migrate: allow pre-rename tokens issued to hosts before alpha-14
|
||||
{ audience: ["host", "slave"] }
|
||||
);
|
||||
if (data.agent === "Clusterio Host" && data.version !== packageVersion) {
|
||||
logger.warn(
|
||||
`Host ${data.name} (${data.id}) connected using version ${data.version} which does not match the ` +
|
||||
`version of the controller is currently running (${packageVersion}). It may not work as expected.`
|
||||
);
|
||||
|
||||
// migrate: allow pre-rename tokens issued to hosts before alpha-14
|
||||
if ((tokenPayload.host !== undefined ? tokenPayload.host : tokenPayload.slave) !== data.id) {
|
||||
throw new Error("missmatched host id");
|
||||
}
|
||||
|
||||
connection = new HostConnection(data, connector, this.controller);
|
||||
connector.on("close", () => {
|
||||
if (this.hostConnections.get(data.id) === connection) {
|
||||
this.hostConnections.delete(data.id);
|
||||
this.controller.hostUpdated(this.controller.hosts.get(data.id));
|
||||
} else {
|
||||
logger.warn("Unlisted HostConnection closed");
|
||||
}
|
||||
});
|
||||
this.hostConnections.set(data.id, connection);
|
||||
this.controller.hostUpdated(this.controller.hosts.get(data.id));
|
||||
|
||||
|
||||
} else if (type === "register_control") {
|
||||
logger.verbose(`WsServer | registered control from ${req.socket.remoteAddress}`);
|
||||
let connection = new ControlConnection(data, connector, this.controller, user);
|
||||
connector.on("close", () => {
|
||||
let index = this.controlConnections.indexOf(connection);
|
||||
if (index !== -1) {
|
||||
this.controlConnections.splice(index, 1);
|
||||
} else {
|
||||
logger.warn("Unlisted ControlConnection closed");
|
||||
}
|
||||
});
|
||||
this.controlConnections.push(connection);
|
||||
additionalReadyData["account"] = {
|
||||
name: user.name,
|
||||
roles: [...user.roles].map(r => ({
|
||||
name: r.name,
|
||||
id: r.id,
|
||||
permissions: [...r.permissions],
|
||||
})),
|
||||
};
|
||||
} catch (err) {
|
||||
logger.verbose(`WsServer | authentication failed for ${req.socket.remoteAddress}`);
|
||||
wsRejectedConnectionsCounter.inc();
|
||||
socket.close(4003, `Authentication failed: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
connector.ready(socket, sessionToken, additionalReadyData);
|
||||
let [connector, sessionToken] = this.createSession(new libData.Address(libData.Address.host, data.id));
|
||||
let connection = this.hostConnections.get(data.id);
|
||||
if (connection) {
|
||||
logger.verbose(`WsServer | disconnecting existing connection for host ${data.id}`);
|
||||
await connection.disconnect(1008, "Registered from another connection");
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`WsServer | registered host ${data.name} (${data.id}) using agent ${data.agent} ${data.version}`
|
||||
);
|
||||
if (data.agent === "Clusterio Host" && data.version !== packageVersion) {
|
||||
logger.warn(
|
||||
`Host ${data.name} (${data.id}) connected using version ${data.version} which does not match the ` +
|
||||
`version of the controller is currently running (${packageVersion}). It may not work as expected.`
|
||||
);
|
||||
}
|
||||
|
||||
connection = new HostConnection(data, connector, this.controller);
|
||||
connector.on("close", () => {
|
||||
if (this.hostConnections.get(data.id) === connection) {
|
||||
this.hostConnections.delete(data.id);
|
||||
this.controller.hostUpdated(this.controller.hosts.get(data.id));
|
||||
} else {
|
||||
logger.warn("Unlisted HostConnection closed");
|
||||
}
|
||||
});
|
||||
this.hostConnections.set(data.id, connection);
|
||||
this.controller.hostUpdated(this.controller.hosts.get(data.id));
|
||||
let src = new libData.Address(libData.Address.host, data.id);
|
||||
connector.ready(socket, src, sessionToken);
|
||||
}
|
||||
|
||||
async registerControl(data, socket, req) {
|
||||
let user;
|
||||
try {
|
||||
let tokenPayload = jwt.verify(
|
||||
data.token,
|
||||
Buffer.from(this.controller.config.get("controller.auth_secret"), "base64"),
|
||||
{ audience: "user" }
|
||||
);
|
||||
|
||||
user = this.controller.userManager.users.get(tokenPayload.user);
|
||||
if (!user) {
|
||||
throw new Error("invalid user");
|
||||
}
|
||||
if (tokenPayload.iat < user.tokenValidAfter) {
|
||||
throw new Error("invalid token");
|
||||
}
|
||||
user.checkPermission("core.control.connect");
|
||||
|
||||
} catch (err) {
|
||||
logger.verbose(`WsServer | authentication failed for ${req.socket.remoteAddress}`);
|
||||
wsRejectedConnectionsCounter.inc();
|
||||
socket.close(4003, `Authentication failed: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let id = this.nextControlId;
|
||||
this.nextControlId += 1;
|
||||
let [connector, sessionToken] = this.createSession(new libData.Address(libData.Address.control, id));
|
||||
|
||||
logger.verbose(`WsServer | registered control ${id} from ${req.socket.remoteAddress}`);
|
||||
let connection = new ControlConnection(data, connector, this.controller, user, id);
|
||||
connector.on("close", () => {
|
||||
this.controlConnections.delete(id);
|
||||
});
|
||||
this.controlConnections.set(id, connection);
|
||||
let account = new libData.AccountDetails(
|
||||
user.name,
|
||||
[...user.roles].map(r => ({
|
||||
name: r.name,
|
||||
id: r.id,
|
||||
permissions: [...r.permissions],
|
||||
}))
|
||||
);
|
||||
|
||||
let src = new libData.Address(libData.Address.control, id);
|
||||
connector.ready(socket, src, sessionToken, account);
|
||||
}
|
||||
|
||||
resumeSession(data, socket, req) {
|
||||
let connector;
|
||||
try {
|
||||
let payload = jwt.verify(
|
||||
data.sessionToken,
|
||||
Buffer.from(this.controller.config.get("controller.auth_secret"), "base64"),
|
||||
{ audience: this.sessionAud }
|
||||
);
|
||||
|
||||
connector = this.activeConnectors.get(payload.sid);
|
||||
if (!connector) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
socket.send(JSON.stringify(new libData.MessageInvalidate()));
|
||||
this.attachHandler(socket, req);
|
||||
return;
|
||||
}
|
||||
|
||||
connector.continue(socket, data.lastSeq);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
const events = require("events");
|
||||
|
||||
const libData = require("@clusterio/lib/data");
|
||||
const libLink = require("@clusterio/lib/link");
|
||||
const { logger } = require("@clusterio/lib/logging");
|
||||
|
||||
@@ -12,9 +13,10 @@ const { logger } = require("@clusterio/lib/logging");
|
||||
* @alias module:controller/src/WsServerConnector
|
||||
*/
|
||||
class WsServerConnector extends libLink.WebSocketBaseConnector {
|
||||
constructor(sessionId, sessionTimeout, heartbeatInterval) {
|
||||
super(sessionTimeout);
|
||||
constructor(dst, sessionId, sessionTimeout, heartbeatInterval) {
|
||||
super(new libData.Address(libData.Address.controller, 0), dst);
|
||||
|
||||
this._sessionTimeout = sessionTimeout;
|
||||
this._socket = null;
|
||||
this._sessionId = sessionId;
|
||||
this._heartbeatInterval = heartbeatInterval;
|
||||
@@ -38,27 +40,27 @@ class WsServerConnector extends libLink.WebSocketBaseConnector {
|
||||
* Sends the ready message over the socket to initiate the session.
|
||||
*
|
||||
* @param {Object} socket - WebSocket connection to client.
|
||||
* @param {module:lib/data.Address} src - Source address for this link.
|
||||
* @param {string} sessionToken -
|
||||
* the session token to send to the client.
|
||||
* @param {Object=} additionalData -
|
||||
* extra properties to send along the ready message.
|
||||
* @param {module:lib/data.AccountDetails=} account -
|
||||
* account data to provide to control connection
|
||||
*/
|
||||
ready(socket, sessionToken, additionalData) {
|
||||
ready(socket, src, sessionToken, account) {
|
||||
this._socket = socket;
|
||||
this._socket.send(JSON.stringify({
|
||||
seq: null,
|
||||
type: "ready",
|
||||
data: {
|
||||
session_token: sessionToken,
|
||||
session_timeout: this._sessionTimeout,
|
||||
heartbeat_interval: this._heartbeatInterval,
|
||||
...additionalData,
|
||||
},
|
||||
}));
|
||||
this._sendInternal(new libData.MessageReady(
|
||||
new libData.ReadyData(
|
||||
src,
|
||||
sessionToken,
|
||||
this._sessionTimeout,
|
||||
this._heartbeatInterval,
|
||||
account,
|
||||
),
|
||||
));
|
||||
|
||||
this._state = "connected";
|
||||
this._attachSocketHandlers();
|
||||
this.emit("connect");
|
||||
this.emit("connect", { src: this.src });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,7 +70,7 @@ class WsServerConnector extends libLink.WebSocketBaseConnector {
|
||||
* socket given from the message sequence given.
|
||||
*
|
||||
* @param {module:net.Socket} socket - New socket to continue on.
|
||||
* @param {number} lastSeq - The last message the client received.
|
||||
* @param {number=} lastSeq - The last message the client received.
|
||||
*/
|
||||
continue(socket, lastSeq) {
|
||||
|
||||
@@ -87,15 +89,9 @@ class WsServerConnector extends libLink.WebSocketBaseConnector {
|
||||
this._timeoutId = null;
|
||||
}
|
||||
|
||||
this._socket.send(JSON.stringify({
|
||||
seq: null,
|
||||
type: "continue",
|
||||
data: {
|
||||
last_seq: this._lastReceivedSeq,
|
||||
session_timeout: this._sessionTimeout,
|
||||
heartbeat_interval: this._heartbeatInterval,
|
||||
},
|
||||
}));
|
||||
this._sendInternal(new libData.MessageContinue(
|
||||
new libData.ContinueData(this._sessionTimeout, this._heartbeatInterval, this._lastReceivedSeq)
|
||||
));
|
||||
|
||||
this._state = "connected";
|
||||
this._attachSocketHandlers();
|
||||
@@ -118,6 +114,7 @@ class WsServerConnector extends libLink.WebSocketBaseConnector {
|
||||
this._socket.on("close", (code, reason) => {
|
||||
logger.verbose(`Connector | Close (code: ${code}, reason: ${reason})`);
|
||||
this.stopHeartbeat();
|
||||
this._socket = null;
|
||||
|
||||
if (this._closing) {
|
||||
this._reset();
|
||||
@@ -148,18 +145,12 @@ class WsServerConnector extends libLink.WebSocketBaseConnector {
|
||||
|
||||
// Handle messages
|
||||
this._socket.on("message", data => {
|
||||
let message = JSON.parse(data);
|
||||
if (["connected", "closing"].includes(this._state)) {
|
||||
if (message.seq !== null) {
|
||||
this._lastReceivedSeq = message.seq;
|
||||
}
|
||||
|
||||
if (message.type === "heartbeat") {
|
||||
this._processHeartbeat(message);
|
||||
|
||||
} else {
|
||||
this.emit("message", message);
|
||||
}
|
||||
let message = this._parseMessage(data);
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
if (this._state === "connected") {
|
||||
this._processMessage(message);
|
||||
|
||||
} else {
|
||||
throw new Error(`Received message in unexpected state ${this._state}`);
|
||||
|
||||
@@ -49,12 +49,12 @@ async function getMetrics(req, res, next) {
|
||||
|
||||
let requests = [];
|
||||
let timeout = req.app.locals.controller.config.get("controller.metrics_timeout") * 1000;
|
||||
for (let hostConnection of req.app.locals.controller.wsServer.hostConnections.values()) {
|
||||
for (let [hostId, hostConnection] of req.app.locals.controller.wsServer.hostConnections) {
|
||||
if (!hostConnection.connected) {
|
||||
continue;
|
||||
}
|
||||
requests.push(libHelpers.timeout(
|
||||
libLink.messages.getMetrics.send(hostConnection).catch(err => {
|
||||
hostConnection.send(new libData.HostMetricsRequest()).catch(err => {
|
||||
if (!(err instanceof libErrors.SessionLost)) {
|
||||
logger.error(`Unexpected error gathering metrics from host:\n${err.stack}`);
|
||||
}
|
||||
@@ -232,7 +232,7 @@ async function uploadExport(req, res) {
|
||||
await libFileOps.safeOutputFile(path.join("static", `${name}.${hash}${ext}`), await file.async("nodebuffer"));
|
||||
}
|
||||
|
||||
modPack.exportManifest = new libData.ExportManifest({ assets });
|
||||
modPack.exportManifest = new libData.ExportManifest(assets);
|
||||
modPack.fillDefaultSettings(settingPrototypes, logger);
|
||||
res.app.locals.controller.modPackUpdated(modPack);
|
||||
|
||||
@@ -361,15 +361,17 @@ async function uploadSave(req, res) {
|
||||
});
|
||||
|
||||
try {
|
||||
let result = await Promise.race([
|
||||
req.app.locals.controller.forwardRequestToInstance(libLink.messages.pullSave, {
|
||||
instance_id: instanceId,
|
||||
stream_id: proxyStream.id,
|
||||
filename,
|
||||
}),
|
||||
let storedName = await Promise.race([
|
||||
req.app.locals.controller.sendToHostByInstanceId(
|
||||
new libData.InstancePullSaveRequest(
|
||||
instanceId,
|
||||
proxyStream.id,
|
||||
filename,
|
||||
)
|
||||
),
|
||||
timeout,
|
||||
]);
|
||||
saves.push(result.save);
|
||||
saves.push(storedName);
|
||||
|
||||
} catch (err) {
|
||||
proxyStream.events.emit("close");
|
||||
|
||||
+36
-44
@@ -11,6 +11,7 @@ const version = require("./package").version;
|
||||
const setBlocking = require("set-blocking");
|
||||
|
||||
const libLink = require("@clusterio/lib/link");
|
||||
const libData = require("@clusterio/lib/data");
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
const libConfig = require("@clusterio/lib/config");
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
@@ -35,11 +36,15 @@ class ControlConnector extends libLink.WebSocketClientConnector {
|
||||
|
||||
register() {
|
||||
logger.verbose("Connector | registering control");
|
||||
this.sendHandshake("register_control", {
|
||||
token: this._token,
|
||||
agent: "clusterioctl",
|
||||
version: version,
|
||||
});
|
||||
this.sendHandshake(
|
||||
new libData.MessageRegisterControl(
|
||||
new libData.RegisterControlData(
|
||||
this._token,
|
||||
"clusterioctl",
|
||||
version,
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +55,8 @@ class ControlConnector extends libLink.WebSocketClientConnector {
|
||||
* @static
|
||||
*/
|
||||
class Control extends libLink.Link {
|
||||
|
||||
constructor(connector, controlConfig, tlsCa, controlPlugins) {
|
||||
super("control", "controller", connector);
|
||||
libLink.attachAllMessages(this);
|
||||
super(connector);
|
||||
|
||||
/**
|
||||
* Control config used for connecting to the controller.
|
||||
@@ -70,67 +73,54 @@ class Control extends libLink.Link {
|
||||
* @type {Map<string, module:lib/plugin.BaseControlPlugin>}
|
||||
*/
|
||||
this.plugins = controlPlugins;
|
||||
for (let controlPlugin of controlPlugins.values()) {
|
||||
libPlugin.attachPluginMessages(this, controlPlugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the control connection alive after the command completes.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.keepOpen = false;
|
||||
|
||||
this.handle(libData.LogMessageEvent, this.handleLogMessageEvent.bind(this));
|
||||
this.handle(libData.DebugWsMessageEvent, this.handleDebugWsMessageEvent.bind(this));
|
||||
}
|
||||
|
||||
async accountUpdateEventHandler() { }
|
||||
|
||||
async hostUpdateEventHandler() { }
|
||||
|
||||
async instanceUpdateEventHandler() { }
|
||||
|
||||
async saveListUpdateEventHandler() { }
|
||||
|
||||
async modPackUpdateEventHandler() { }
|
||||
|
||||
async modUpdateEventHandler() { }
|
||||
|
||||
async userUpdateEventHandler() { }
|
||||
|
||||
async setLogSubscriptions({
|
||||
all = false,
|
||||
controller = false,
|
||||
host_ids = [],
|
||||
instance_ids = [],
|
||||
max_level = null,
|
||||
hostIds = [],
|
||||
instanceIds = [],
|
||||
maxLevel = null,
|
||||
}) {
|
||||
await libLink.messages.setLogSubscriptions.send(this, {
|
||||
all, controller, host_ids, instance_ids, max_level,
|
||||
});
|
||||
await this.send(
|
||||
new libData.LogSetSubscriptionsRequest(
|
||||
all, controller, hostIds, instanceIds, maxLevel,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async logMessageEventHandler(message) {
|
||||
logger.log(message.data.info);
|
||||
async handleLogMessageEvent(event) {
|
||||
logger.log(event.info);
|
||||
}
|
||||
|
||||
async debugWsMessageEventHandler(message) {
|
||||
async handleDebugWsMessageEvent(event) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("WS", message.data.direction, message.data.content);
|
||||
console.log("WS", event.direction, event.content);
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
try {
|
||||
await libLink.messages.prepareDisconnect.send(this);
|
||||
await this.connector.disconnect();
|
||||
} catch (err) {
|
||||
if (!(err instanceof libErrors.SessionLost)) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
await this.connector.close(1000, "Control Quit");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPlugins(pluginList) {
|
||||
let pluginInfos = await libPluginLoader.loadPluginInfos(pluginList);
|
||||
libLink.registerPluginMessages(pluginInfos);
|
||||
libConfig.registerPluginConfigGroups(pluginInfos);
|
||||
libConfig.finalizeConfigs();
|
||||
|
||||
@@ -182,11 +172,13 @@ async function startControl() {
|
||||
let args = yargs.parse();
|
||||
|
||||
// Log stream for the ctl session.
|
||||
logger.add(new ConsoleTransport({
|
||||
errorLevels: Object.keys(levels),
|
||||
level: args.logLevel,
|
||||
format: new libLoggingUtils.TerminalFormat(),
|
||||
}));
|
||||
logger.add(
|
||||
new ConsoleTransport({
|
||||
errorLevels: Object.keys(levels),
|
||||
level: args.logLevel,
|
||||
format: new libLoggingUtils.TerminalFormat(),
|
||||
})
|
||||
);
|
||||
libLoggingUtils.handleUnhandledErrors(logger);
|
||||
|
||||
logger.verbose(`Loading available plugins from ${args.pluginList}`);
|
||||
@@ -300,7 +292,7 @@ async function startControl() {
|
||||
process.exitCode = 1;
|
||||
|
||||
} else if (err instanceof libErrors.RequestError) {
|
||||
logger.error(`Error sending request: ${err.message}`);
|
||||
logger.error(`Error sending request:\n${err.stack}`);
|
||||
process.exitCode = 1;
|
||||
|
||||
} else {
|
||||
|
||||
+191
-246
@@ -10,7 +10,6 @@ const child_process = require("child_process");
|
||||
const stream = require("stream");
|
||||
const util = require("util");
|
||||
|
||||
const libLink = require("@clusterio/lib/link");
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
const libConfig = require("@clusterio/lib/config");
|
||||
const libCommand = require("@clusterio/lib/command");
|
||||
@@ -91,9 +90,9 @@ const controllerConfigCommands = new libCommand.CommandTree({
|
||||
controllerConfigCommands.add(new libCommand.Command({
|
||||
definition: ["list", "List controller configuration"],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.getControllerConfig.send(control);
|
||||
let response = await control.send(new libData.ControllerConfigGetRequest());
|
||||
|
||||
for (let group of response.serialized_config.groups) {
|
||||
for (let group of response.serializedConfig.groups) {
|
||||
for (let [name, value] of Object.entries(group.fields)) {
|
||||
print(`${group.name}.${name} ${JSON.stringify(value)}`);
|
||||
}
|
||||
@@ -115,10 +114,7 @@ controllerConfigCommands.add(new libCommand.Command({
|
||||
} else if (args.value === undefined) {
|
||||
args.value = "";
|
||||
}
|
||||
await libLink.messages.setControllerConfigField.send(control, {
|
||||
field: args.field,
|
||||
value: args.value,
|
||||
});
|
||||
await control.send(new libData.ControllerConfigSetFieldRequest(args.field, args.value));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -135,22 +131,19 @@ controllerConfigCommands.add(new libCommand.Command({
|
||||
if (args.stdin) {
|
||||
args.value = (await libHelpers.readStream(process.stdin)).toString().replace(/\r?\n$/, "");
|
||||
}
|
||||
let request = {
|
||||
field: args.field,
|
||||
prop: args.prop,
|
||||
};
|
||||
let value;
|
||||
try {
|
||||
if (args.value !== undefined) {
|
||||
request.value = JSON.parse(args.value);
|
||||
value = JSON.parse(args.value);
|
||||
}
|
||||
} catch (err) {
|
||||
// See note for the instance version of set-prop
|
||||
if (args.stdin || /^(\[.*]|{.*}|".*")$/.test(args.value)) {
|
||||
throw new libErrors.CommandError(`In parsing value '${args.value}': ${err.message}`);
|
||||
}
|
||||
request.value = args.value;
|
||||
value = args.value;
|
||||
}
|
||||
await libLink.messages.setControllerConfigProp.send(control, request);
|
||||
await control.send(new libData.ControllerConfigSetPropRequest(args.field, args.prop, value));
|
||||
},
|
||||
}));
|
||||
controllerConfigCommands.add(new libCommand.Command({
|
||||
@@ -162,7 +155,7 @@ controllerConfigCommands.add(new libCommand.Command({
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.getControllerConfig.send(control);
|
||||
let response = await control.send(new ControllerConfigGetRequest());
|
||||
let tmpFile = await libFileOps.getTempFile("ctl-", "-tmp", os.tmpdir());
|
||||
let editor = await getEditor(args.editor);
|
||||
if (editor === -1) {
|
||||
@@ -170,7 +163,7 @@ controllerConfigCommands.add(new libCommand.Command({
|
||||
Try "ctl controller config edit <editor of choice>"`);
|
||||
}
|
||||
let allConfigElements = await serializedConfigToString(
|
||||
response.serialized_config, libConfig.ControllerConfig, {}
|
||||
response.serializedConfig, libConfig.ControllerConfig, {}
|
||||
);
|
||||
await fs.writeFile(tmpFile, allConfigElements, (err) => {
|
||||
if (err) {
|
||||
@@ -191,10 +184,10 @@ controllerConfigCommands.add(new libCommand.Command({
|
||||
for (let index in final) {
|
||||
if (index in final) {
|
||||
try {
|
||||
await libLink.messages.setControllerConfigField.send(control, {
|
||||
field: index,
|
||||
value: final[index],
|
||||
});
|
||||
await control.send(new ControllerConfigSetFieldRequest(
|
||||
index,
|
||||
final[index],
|
||||
));
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
print(`Attempt to set ${index} to ${final[index] || String(null)} failed; set back to previous value.`);
|
||||
@@ -241,8 +234,8 @@ const hostCommands = new libCommand.CommandTree({ name: "host", description: "Ho
|
||||
hostCommands.add(new libCommand.Command({
|
||||
definition: [["list", "l"], "List hosts connected to the controller"],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.listHosts.send(control);
|
||||
print(asTable(response.list));
|
||||
let hosts = await control.send(new libData.HostListRequest());
|
||||
print(asTable(hosts));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -252,7 +245,7 @@ hostCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let hostId = typeof args.id === "number" ? args.id : null;
|
||||
let response = await libLink.messages.generateHostToken.send(control, { host_id: hostId });
|
||||
let response = await control.send(new libData.HostGenerateTokenRequest(hostId));
|
||||
print(response.token);
|
||||
},
|
||||
}));
|
||||
@@ -269,11 +262,11 @@ hostCommands.add(new libCommand.Command({
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.createHostConfig.send(control, {
|
||||
id: args.id, name: args.name, generate_token: args.generateToken,
|
||||
});
|
||||
let rawConfig = await control.send(
|
||||
new libData.HostConfigCreateRequest(args.id, args.name, args.generateToken)
|
||||
);
|
||||
|
||||
let content = JSON.stringify(response.serialized_config, null, 4);
|
||||
let content = JSON.stringify(rawConfig.serializedConfig, null, 4);
|
||||
if (args.output === "-") {
|
||||
print(content);
|
||||
} else {
|
||||
@@ -297,8 +290,8 @@ const instanceCommands = new libCommand.CommandTree({
|
||||
instanceCommands.add(new libCommand.Command({
|
||||
definition: [["list", "l"], "List instances known to the controller"],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.listInstances.send(control);
|
||||
print(asTable(response.list));
|
||||
let list = await control.send(new libData.InstanceDetailsListRequest());
|
||||
print(asTable(list));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -317,8 +310,8 @@ instanceCommands.add(new libCommand.Command({
|
||||
instanceConfig.set("instance.id", args.id);
|
||||
}
|
||||
instanceConfig.set("instance.name", args.name);
|
||||
let serialized_config = instanceConfig.serialize("controller");
|
||||
await libLink.messages.createInstance.send(control, { serialized_config });
|
||||
let serializedConfig = instanceConfig.serialize("controller");
|
||||
await control.send(new libData.InstanceCreateRequest(serializedConfig));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -331,9 +324,9 @@ instanceConfigCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
let response = await libLink.messages.getInstanceConfig.send(control, { instance_id: instanceId });
|
||||
let response = await control.send(new libData.InstanceConfigGetRequest(instanceId));
|
||||
|
||||
for (let group of response.serialized_config.groups) {
|
||||
for (let group of response.config.groups) {
|
||||
for (let [name, value] of Object.entries(group.fields)) {
|
||||
print(`${group.name}.${name} ${JSON.stringify(value)}`);
|
||||
}
|
||||
@@ -357,11 +350,7 @@ instanceConfigCommands.add(new libCommand.Command({
|
||||
} else if (args.value === undefined) {
|
||||
args.value = "";
|
||||
}
|
||||
await libLink.messages.setInstanceConfigField.send(control, {
|
||||
instance_id: instanceId,
|
||||
field: args.field,
|
||||
value: args.value,
|
||||
});
|
||||
await control.send(new libData.InstanceConfigSetFieldRequest(instanceId, args.field, args.value));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -380,14 +369,10 @@ instanceConfigCommands.add(new libCommand.Command({
|
||||
if (args.stdin) {
|
||||
args.value = (await libHelpers.readStream(process.stdin)).toString().replace(/\r?\n$/, "");
|
||||
}
|
||||
let request = {
|
||||
instance_id: instanceId,
|
||||
field: args.field,
|
||||
prop: args.prop,
|
||||
};
|
||||
let value;
|
||||
try {
|
||||
if (args.value !== undefined) {
|
||||
request.value = JSON.parse(args.value);
|
||||
value = JSON.parse(args.value);
|
||||
}
|
||||
} catch (err) {
|
||||
// If this is from stdin or looks like an array, object or string
|
||||
@@ -406,9 +391,9 @@ instanceConfigCommands.add(new libCommand.Command({
|
||||
if (args.stdin || /^(\[.*]|{.*}|".*")$/.test(args.value)) {
|
||||
throw new libErrors.CommandError(`In parsing value '${args.value}': ${err.message}`);
|
||||
}
|
||||
request.value = args.value;
|
||||
value = args.value;
|
||||
}
|
||||
await libLink.messages.setInstanceConfigProp.send(control, request);
|
||||
await control.send(new libData.InstanceConfigSetPropRequest(instanceId, args.field, args.prop, value));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -423,7 +408,7 @@ instanceConfigCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
let response = await libLink.messages.getInstanceConfig.send(control, { instance_id: instanceId });
|
||||
let response = await control.send(new libData.InstanceConfigGetRequest(instanceId));
|
||||
let tmpFile = await libFileOps.getTempFile("ctl-", "-tmp", os.tmpdir());
|
||||
let editor = await getEditor(args.editor);
|
||||
if (editor === -1) {
|
||||
@@ -432,7 +417,7 @@ instanceConfigCommands.add(new libCommand.Command({
|
||||
}
|
||||
let disallowedList = {"instance.id": 0, "instance.assigned_host": 0, "factorio.settings": 0};
|
||||
let allConfigElements = await serializedConfigToString(
|
||||
response.serialized_config,
|
||||
response.serializedConfig,
|
||||
libConfig.InstanceConfig,
|
||||
disallowedList
|
||||
);
|
||||
@@ -455,11 +440,11 @@ instanceConfigCommands.add(new libCommand.Command({
|
||||
for (let index in final) {
|
||||
if (index in final) {
|
||||
try {
|
||||
await libLink.messages.setInstanceConfigField.send(control, {
|
||||
instance_id: instanceId,
|
||||
field: index,
|
||||
value: final[index],
|
||||
});
|
||||
await control.send(new libData.InstanceConfigSetField(
|
||||
instanceId,
|
||||
index,
|
||||
final[index],
|
||||
));
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
print(`\n\n\nAttempt to set ${index} to ${final[index] || String(null)} failed; set back to previous value.`);
|
||||
@@ -494,10 +479,7 @@ instanceCommands.add(new libCommand.Command({
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
let hostId = args.host ? await libCommand.resolveHost(control, args.host) : null;
|
||||
await libLink.messages.assignInstanceCommand.send(control, {
|
||||
instance_id: instanceId,
|
||||
host_id: hostId,
|
||||
});
|
||||
await control.send(new libData.InstanceAssignRequest(instanceId, hostId));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -533,12 +515,12 @@ instanceSaveCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
let response = await libLink.messages.listSaves.send(control, { instance_id: instanceId });
|
||||
for (let entry of response.list) {
|
||||
entry.mtime = new Date(entry.mtime_ms).toLocaleString();
|
||||
delete entry.mtime_ms;
|
||||
let saves = await control.sendTo({ instanceId }, new libData.InstanceListSavesRequest());
|
||||
for (let entry of saves) {
|
||||
entry.mtime = new Date(entry.mtimeMs).toLocaleString();
|
||||
delete entry.mtimeMs;
|
||||
}
|
||||
print(asTable(response.list));
|
||||
print(asTable(saves));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -556,14 +538,11 @@ instanceSaveCommands.add(new libCommand.Command({
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
let { seed, mapGenSettings, mapSettings } = await loadMapSettings(args);
|
||||
await control.setLogSubscriptions({ instance_ids: [instanceId] });
|
||||
await libLink.messages.createSave.send(control, {
|
||||
instance_id: instanceId,
|
||||
name: args.name,
|
||||
seed,
|
||||
map_gen_settings: mapGenSettings,
|
||||
map_settings: mapSettings,
|
||||
});
|
||||
await control.setLogSubscriptions({ instanceIds: [instanceId] });
|
||||
await control.sendTo(
|
||||
{ instanceId },
|
||||
new libData.InstanceCreateSaveRequest(args.name, seed, mapGenSettings, mapSettings),
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -575,11 +554,7 @@ instanceSaveCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
await libLink.messages.renameSave.send(control, {
|
||||
instance_id: instanceId,
|
||||
old_name: args.oldName,
|
||||
new_name: args.newName,
|
||||
});
|
||||
await control.send(new libData.InstanceRenameSaveRequest(instanceId, args.oldName, args.newName));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -591,11 +566,7 @@ instanceSaveCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
await libLink.messages.copySave.send(control, {
|
||||
instance_id: instanceId,
|
||||
source: args.source,
|
||||
destination: args.destination,
|
||||
});
|
||||
await control.send(new libData.InstanceCopySaveRequest(instanceId, args.source, args.destination));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -667,14 +638,16 @@ instanceSaveCommands.add(new libCommand.Command({
|
||||
handler: async function(args, control) {
|
||||
let sourceInstanceId = await libCommand.resolveInstance(control, args.sourceInstance);
|
||||
let targetInstanceId = await libCommand.resolveInstance(control, args.targetInstance);
|
||||
let result = await libLink.messages.transferSave.send(control, {
|
||||
instance_id: sourceInstanceId,
|
||||
source_save: args.sourceSave,
|
||||
target_instance_id: targetInstanceId,
|
||||
target_save: args.targetSave || args.sourceSave,
|
||||
copy: args.copy,
|
||||
});
|
||||
print(`Transferred as ${result.save} to ${args.targetInstance}.`);
|
||||
let storedName = await control.send(
|
||||
new libData.InstanceTransferSaveRequest(
|
||||
sourceInstanceId,
|
||||
args.sourceSave,
|
||||
targetInstanceId,
|
||||
args.targetSave || args.sourceSave,
|
||||
args.copy
|
||||
)
|
||||
);
|
||||
print(`Transferred as ${storedName} to ${args.targetInstance}.`);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -685,13 +658,10 @@ instanceSaveCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
let result = await libLink.messages.downloadSave.send(control, {
|
||||
instance_id: instanceId,
|
||||
save: args.save,
|
||||
});
|
||||
let streamId = await control.send(new libData.InstanceDownloadSaveRequest(instanceId, args.save));
|
||||
|
||||
let url = new URL(control.config.get("control.controller_url"));
|
||||
url.pathname += `api/stream/${result.stream_id}`;
|
||||
url.pathname += `api/stream/${streamId}`;
|
||||
let response = await phin({
|
||||
url, method: "GET",
|
||||
core: { ca: control.tlsCa },
|
||||
@@ -730,10 +700,7 @@ instanceSaveCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
await libLink.messages.deleteSave.send(control, {
|
||||
instance_id: instanceId,
|
||||
save: args.save,
|
||||
});
|
||||
await control.send(new libData.InstanceDeleteSaveRequest(instanceId, args.save));
|
||||
},
|
||||
}));
|
||||
instanceCommands.add(instanceSaveCommands);
|
||||
@@ -744,10 +711,8 @@ instanceCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
await control.setLogSubscriptions({ instance_ids: [instanceId] });
|
||||
await libLink.messages.exportData.send(control, {
|
||||
instance_id: instanceId,
|
||||
});
|
||||
await control.setLogSubscriptions({ instanceIds: [instanceId] });
|
||||
await control.sendTo({ instanceId }, new libData.InstanceExportDataRequest());
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -757,10 +722,8 @@ instanceCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
await control.setLogSubscriptions({ instance_ids: [instanceId] });
|
||||
await libLink.messages.extractPlayers.send(control, {
|
||||
instance_id: instanceId,
|
||||
});
|
||||
await control.setLogSubscriptions({ instanceIds: [instanceId] });
|
||||
await control.sendTo({ instanceId }, new libData.InstanceExtractPlayersRequest());
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -774,11 +737,8 @@ instanceCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
await control.setLogSubscriptions({ instance_ids: [instanceId] });
|
||||
await libLink.messages.startInstance.send(control, {
|
||||
instance_id: instanceId,
|
||||
save: args.save || null,
|
||||
});
|
||||
await control.setLogSubscriptions({ instanceIds: [instanceId] });
|
||||
await control.sendTo({ instanceId }, new libData.InstanceStartRequest(args.save));
|
||||
control.keepOpen = args.keepOpen;
|
||||
},
|
||||
}));
|
||||
@@ -797,15 +757,12 @@ instanceCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
await control.setLogSubscriptions({ instance_ids: [instanceId] });
|
||||
await control.setLogSubscriptions({ instanceIds: [instanceId] });
|
||||
let { seed, mapGenSettings, mapSettings } = await loadMapSettings(args);
|
||||
await libLink.messages.loadScenario.send(control, {
|
||||
instance_id: instanceId,
|
||||
scenario: args.scenario,
|
||||
seed,
|
||||
map_gen_settings: mapGenSettings,
|
||||
map_settings: mapSettings,
|
||||
});
|
||||
await control.sendTo(
|
||||
{ instanceId },
|
||||
new libData.InstanceLoadScenarioRequest(args.scenario, seed, mapGenSettings, mapSettings),
|
||||
);
|
||||
control.keepOpen = args.keepOpen;
|
||||
},
|
||||
}));
|
||||
@@ -816,10 +773,8 @@ instanceCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
await control.setLogSubscriptions({ instance_ids: [instanceId] });
|
||||
await libLink.messages.stopInstance.send(control, {
|
||||
instance_id: instanceId,
|
||||
});
|
||||
await control.setLogSubscriptions({ instanceIds: [instanceId] });
|
||||
await control.sendTo({ instanceId }, new libData.InstanceStopRequest(args.save));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -829,10 +784,8 @@ instanceCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
await control.setLogSubscriptions({ instance_ids: [instanceId] });
|
||||
await libLink.messages.killInstance.send(control, {
|
||||
instance_id: instanceId,
|
||||
});
|
||||
await control.setLogSubscriptions({ instanceIds: [instanceId] });
|
||||
await control.sendTo({ instanceId }, new libData.InstanceKillRequest(args.save));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -841,9 +794,8 @@ instanceCommands.add(new libCommand.Command({
|
||||
yargs.positional("instance", { describe: "Instance to delete", type: "string" });
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
await libLink.messages.deleteInstance.send(control, {
|
||||
instance_id: await libCommand.resolveInstance(control, args.instance),
|
||||
});
|
||||
let instanceId = await libCommand.resolveInstance(control, args.instance);
|
||||
await control.send(new libData.InstanceDeleteRequest(instanceId));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -853,13 +805,13 @@ instanceCommands.add(new libCommand.Command({
|
||||
yargs.positional("command", { describe: "command to send", type: "string" });
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.sendRcon.send(control, {
|
||||
instance_id: await libCommand.resolveInstance(control, args.instance),
|
||||
command: args.command,
|
||||
});
|
||||
let result = await control.sendTo(
|
||||
{ instanceId: await libCommand.resolveInstance(control, args.instance) },
|
||||
new libData.InstanceSendRconRequest(args.command),
|
||||
);
|
||||
|
||||
// Factorio includes a newline in it's response output.
|
||||
process.stdout.write(response.result);
|
||||
// Factorio includes a newline in its response output.
|
||||
process.stdout.write(result);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -870,36 +822,38 @@ modPackCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let mods = new Map(
|
||||
(await libLink.messages.listMods.send(control)).list.map(m => [`${m.name}_${m.version}`, m])
|
||||
(await control.send(new libData.ModListRequest())).map(m => [`${m.name}_${m.version}`, m])
|
||||
);
|
||||
let modPack = await control.send(
|
||||
new libData.ModPackGetRequest(await libCommand.resolveModPack(control, args.modPack))
|
||||
);
|
||||
let response = await libLink.messages.getModPack.send(control, {
|
||||
id: await libCommand.resolveModPack(control, args.modPack),
|
||||
});
|
||||
|
||||
for (let [field, value] of Object.entries(response.mod_pack)) {
|
||||
for (let [field, value] of Object.entries(modPack)) {
|
||||
if (field === "mods") {
|
||||
print(`${field}:`);
|
||||
for (let entry of value) {
|
||||
const mod = mods.get(`${entry.name}_${entry.version}`);
|
||||
for (let [name, entry] of value) {
|
||||
const mod = mods.get(`${name}_${entry.version}`);
|
||||
const missing = !mod && "? ";
|
||||
const badChecksum = mod && entry.sha1 && entry.sha1 !== mod.sha1 && "! ";
|
||||
const warn = missing || badChecksum || "";
|
||||
const enabled = entry.enabled ? "" : "(disabled) ";
|
||||
print(` ${warn}${enabled}${entry.name} ${entry.version}${entry.sha1 ? ` (${entry.sha1})` : ""}`);
|
||||
print(` ${warn}${enabled}${name} ${entry.version}${entry.sha1 ? ` (${entry.sha1})` : ""}`);
|
||||
}
|
||||
} else if (field === "settings") {
|
||||
print(`${field}:`);
|
||||
for (let [scope, settings] of Object.entries(value)) {
|
||||
print(` ${scope}:`);
|
||||
for (let [setting, settingValue] of Object.entries(settings)) {
|
||||
for (let [setting, settingValue] of settings) {
|
||||
print(` ${setting}: ${JSON.stringify(settingValue.value)}`);
|
||||
}
|
||||
}
|
||||
} else if (field === "export_manifest") {
|
||||
} else if (field === "exportManifest") {
|
||||
print(`${field}:`);
|
||||
print(" assets:");
|
||||
for (let [name, fileName] of Object.entries(value.assets)) {
|
||||
print(` ${name}: ${fileName}`);
|
||||
if (value && value.assets) {
|
||||
print(" assets:");
|
||||
for (let [name, fileName] of Object.entries(value.assets)) {
|
||||
print(` ${name}: ${fileName}`);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -912,16 +866,16 @@ modPackCommands.add(new libCommand.Command({
|
||||
modPackCommands.add(new libCommand.Command({
|
||||
definition: [["list", "l"], "List mod packs in the cluster"],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.listModPacks.send(control);
|
||||
let fields = ["id", "name", "factorio_version"];
|
||||
for (let entry of response.list) {
|
||||
let modPacks = await control.send(new libData.ModPackListRequest());
|
||||
let fields = ["id", "name", "factorioVersion"];
|
||||
for (let entry of modPacks) {
|
||||
for (let field of Object.keys(entry)) {
|
||||
if (!fields.includes(field)) {
|
||||
delete entry[field];
|
||||
}
|
||||
}
|
||||
}
|
||||
print(asTable(response.list));
|
||||
print(asTable(modPacks));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1002,7 +956,7 @@ modPackCommands.add(new libCommand.Command({
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
const modPack = new libData.ModPack();
|
||||
const modPack = libData.ModPack.fromJSON({});
|
||||
modPack.name = args.name;
|
||||
if (args.description) { modPack.description = args.description; }
|
||||
if (args.factorioVersion) {
|
||||
@@ -1014,7 +968,7 @@ modPackCommands.add(new libCommand.Command({
|
||||
setModPackMods(modPack, args.mods);
|
||||
setModPackModsEnabled(modPack, args.disabledMods, false);
|
||||
setModPackSettings(modPack, args);
|
||||
await libLink.messages.createModPack.send(control, { mod_pack: modPack.toJSON() });
|
||||
await control.send(new libData.ModPackCreateRequest(modPack));
|
||||
print(`Created mod pack ${modPack.name} (${modPack.id})`);
|
||||
},
|
||||
}));
|
||||
@@ -1025,7 +979,7 @@ modPackCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
const modPack = libData.ModPack.fromModPackString(args.string);
|
||||
await libLink.messages.createModPack.send(control, { mod_pack: modPack.toJSON() });
|
||||
await control.send(new libData.ModPackCreateRequest(modPack));
|
||||
print(`Created mod pack ${modPack.name} (${modPack.id})`);
|
||||
},
|
||||
}));
|
||||
@@ -1035,10 +989,9 @@ modPackCommands.add(new libCommand.Command({
|
||||
yargs.positional("string", { describe: "Mod pack to export", type: "string" });
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
const response = await libLink.messages.getModPack.send(control, {
|
||||
id: await libCommand.resolveModPack(control, args.modPack),
|
||||
});
|
||||
const modPack = new libData.ModPack(response.mod_pack);
|
||||
const modPack = await control.send(
|
||||
new libData.ModPackGetRequest(await libCommand.resolveModPack(control, args.modPack))
|
||||
);
|
||||
print(modPack.toModPackString());
|
||||
},
|
||||
}));
|
||||
@@ -1062,10 +1015,9 @@ modPackCommands.add(new libCommand.Command({
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.getModPack.send(control, {
|
||||
id: await libCommand.resolveModPack(control, args.modPack),
|
||||
});
|
||||
let modPack = new libData.ModPack(response.mod_pack);
|
||||
const modPack = await control.send(
|
||||
new libData.ModPackGetRequest(await libCommand.resolveModPack(control, args.modPack))
|
||||
);
|
||||
|
||||
if (args.name) { modPack.name = args.name; }
|
||||
if (args.description) { modPack.description = args.description; }
|
||||
@@ -1103,7 +1055,7 @@ modPackCommands.add(new libCommand.Command({
|
||||
}
|
||||
}
|
||||
}
|
||||
await libLink.messages.updateModPack.send(control, { mod_pack: modPack.toJSON() });
|
||||
await control.send(new libData.ModPackUpdateRequest(modPack));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1113,7 +1065,7 @@ modPackCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
const id = await libCommand.resolveModPack(control, args.modPack);
|
||||
await libLink.messages.deleteModPack.send(control, { id });
|
||||
await control.send(new libData.ModPackDeleteRequest(id));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1124,8 +1076,8 @@ modCommands.add(new libCommand.Command({
|
||||
yargs.positional("mod-version", { describe: "Version of the mod", type: "string" });
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.getMod.send(control, { name: args.name, version: args.modVersion });
|
||||
for (let [field, value] of Object.entries(response.mod)) {
|
||||
let modInfo = await control.send(new libData.ModGetRequest(args.name, args.modVersion));
|
||||
for (let [field, value] of Object.entries(modInfo)) {
|
||||
if (value instanceof Array) {
|
||||
print(`${field}:`);
|
||||
for (let entry of value) {
|
||||
@@ -1150,9 +1102,9 @@ modCommands.add(new libCommand.Command({
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.listMods.send(control);
|
||||
let mods = await control.send(new libData.ModListRequest());
|
||||
if (!args.fields.includes("all")) {
|
||||
for (let entry of response.list) {
|
||||
for (let entry of mods) {
|
||||
for (let field of Object.keys(entry)) {
|
||||
if (!args.fields.includes(field)) {
|
||||
delete entry[field];
|
||||
@@ -1160,7 +1112,7 @@ modCommands.add(new libCommand.Command({
|
||||
}
|
||||
}
|
||||
}
|
||||
print(asTable(response.list));
|
||||
print(asTable(mods));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1197,14 +1149,14 @@ modCommands.add(new libCommand.Command({
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.searchMods.send(control, {
|
||||
"query": args.query,
|
||||
"factorio_version": args.factorioVersion,
|
||||
"page_size": args.pageSize,
|
||||
"page": args.page,
|
||||
"sort": args.sort,
|
||||
"sort_order": args.sortOrder,
|
||||
});
|
||||
let response = await control.send(new libData.ModSearchRequest(
|
||||
args.query,
|
||||
args.factorioVersion,
|
||||
args.page,
|
||||
args.pageSize,
|
||||
args.sort,
|
||||
args.sortOrder
|
||||
));
|
||||
let results = response.results.flatMap(result => result.versions);
|
||||
if (!args.fields.includes("all")) {
|
||||
for (let entry of results) {
|
||||
@@ -1215,10 +1167,10 @@ modCommands.add(new libCommand.Command({
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let issue of response.query_issues) {
|
||||
for (let issue of response.queryIssues) {
|
||||
print(issue);
|
||||
}
|
||||
print(`page ${args.page} of ${response.page_count} (${response.result_count} results)`);
|
||||
print(`page ${args.page} of ${response.pageCount} (${response.resultCount} results)`);
|
||||
print(asTable(results));
|
||||
},
|
||||
}));
|
||||
@@ -1259,7 +1211,7 @@ modCommands.add(new libCommand.Command({
|
||||
}
|
||||
|
||||
if (result.body.mods && result.body.mods.length) {
|
||||
const mod = new libData.ModInfo(result.body.mods[0]);
|
||||
const mod = libData.ModInfo.fromJSON(result.body.mods[0]);
|
||||
logger.info(`Successfully uploaded ${mod.filename}`);
|
||||
}
|
||||
|
||||
@@ -1275,13 +1227,10 @@ modCommands.add(new libCommand.Command({
|
||||
yargs.positional("mod-version", { describe: "Version of mod to download", type: "string" });
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let result = await libLink.messages.downloadMod.send(control, {
|
||||
name: args.name,
|
||||
version: args.modVersion,
|
||||
});
|
||||
let streamId = await control.send(new libData.ModDownloadRequest(args.name, args.modVersion));
|
||||
|
||||
let url = new URL(control.config.get("control.controller_url"));
|
||||
url.pathname += `api/stream/${result.stream_id}`;
|
||||
url.pathname += `api/stream/${streamId}`;
|
||||
let response = await phin({
|
||||
url, method: "GET",
|
||||
core: { ca: control.tlsCa },
|
||||
@@ -1318,7 +1267,7 @@ modCommands.add(new libCommand.Command({
|
||||
yargs.positional("mod-version", { describe: "Version of mod to delete", type: "string" });
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
await libLink.messages.deleteMod.send(control, { name: args.name, version: args.modVersion });
|
||||
await control.send(new libData.ModDeleteRequest(args.name, args.modVersion));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1326,8 +1275,8 @@ const permissionCommands = new libCommand.CommandTree({ name: "permission", desc
|
||||
permissionCommands.add(new libCommand.Command({
|
||||
definition: [["list", "l"], "List permissions in the cluster"],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.listPermissions.send(control);
|
||||
print(asTable(response.list));
|
||||
let permissions = await control.send(new libData.PermissionListRequest());
|
||||
print(asTable(permissions));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1336,8 +1285,8 @@ const roleCommands = new libCommand.CommandTree({ name: "role", description: "Ro
|
||||
roleCommands.add(new libCommand.Command({
|
||||
definition: [["list", "l"], "List roles in the cluster"],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.listRoles.send(control);
|
||||
print(asTable(response.list));
|
||||
let roles = await control.send(new libData.RoleListRequest());
|
||||
print(asTable(roles));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1350,12 +1299,12 @@ roleCommands.add(new libCommand.Command({
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.createRole.send(control, {
|
||||
name: args.name,
|
||||
description: args.description,
|
||||
permissions: args.permissions,
|
||||
});
|
||||
logger.info(`Created role ID ${response.id}`);
|
||||
let id = await control.send(new libData.RoleCreateRequest(
|
||||
args.name,
|
||||
args.description,
|
||||
args.permissions,
|
||||
));
|
||||
logger.info(`Created role ID ${id}`);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1395,10 +1344,10 @@ roleCommands.add(new libCommand.Command({
|
||||
if (args.setPerms !== undefined) {
|
||||
role.permissions = args.setPerms;
|
||||
}
|
||||
await libLink.messages.updateRole.send(control, role);
|
||||
await control.send(new libData.RoleUpdateRequest(role.id, role.name, role.description, role.permissions));
|
||||
|
||||
if (args.grantDefault) {
|
||||
await libLink.messages.grantDefaultRolePermissions.send(control, { id: role.id });
|
||||
await control.send(new libData.RoleGrantDefaultPermissionsRequest(role.id));
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -1409,7 +1358,7 @@ roleCommands.add(new libCommand.Command({
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let role = await libCommand.retrieveRole(control, args.role);
|
||||
await libLink.messages.deleteRole.send(control, { id: role.id });
|
||||
await control.send(new libData.RoleDeleteRequest(role.id));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1423,16 +1372,15 @@ userCommands.add(new libCommand.Command({
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.getUser.send(control, { name: args.name });
|
||||
delete response["seq"];
|
||||
Object.assign(response, response["player_stats"]);
|
||||
delete response["player_stats"];
|
||||
let instanceStats = response["instance_stats"];
|
||||
delete response["instance_stats"];
|
||||
print(asTable(Object.entries(response).map(([property, value]) => ({ property, value }))));
|
||||
let user = await control.send(new libData.UserGetRequest(args.name));
|
||||
Object.assign(user, user.playerStats);
|
||||
delete user.playerStats;
|
||||
let instanceStats = user.instanceStats;
|
||||
delete user.instanceStats;
|
||||
print(asTable(Object.entries(user).map(([property, value]) => ({ property, value }))));
|
||||
|
||||
if (args.instanceStats) {
|
||||
let instances = (await libLink.messages.listInstances.send(control)).list;
|
||||
let instances = await control.send(new libData.InstanceDetailsListRequest());
|
||||
function instanceName(id) {
|
||||
let instance = instances.find(i => i.id === id);
|
||||
if (instance) {
|
||||
@@ -1457,19 +1405,22 @@ userCommands.add(new libCommand.Command({
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.listUsers.send(control);
|
||||
for (let user of response.list) {
|
||||
let users = await control.send(new libData.UserListRequest());
|
||||
for (let user of users) {
|
||||
if (args.stats) {
|
||||
Object.assign(user, user["player_stats"]);
|
||||
Object.assign(user, user.playerStats);
|
||||
}
|
||||
delete user["player_stats"];
|
||||
delete user.playerStats;
|
||||
delete user.isDeleted;
|
||||
delete user.banReason;
|
||||
delete user.instanceStats;
|
||||
if (!args.attributes) {
|
||||
delete user["is_admin"];
|
||||
delete user["is_whitelisted"];
|
||||
delete user["is_banned"];
|
||||
delete user.isAdmin;
|
||||
delete user.isWhitelisted;
|
||||
delete user.isBanned;
|
||||
}
|
||||
}
|
||||
print(asTable(response.list));
|
||||
print(asTable(users));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1478,7 +1429,7 @@ userCommands.add(new libCommand.Command({
|
||||
yargs.positional("name", { describe: "Name of user to create", type: "string" });
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
await libLink.messages.createUser.send(control, { name: args.name });
|
||||
await control.send(new libData.UserCreateRequest(args.name));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1487,7 +1438,7 @@ userCommands.add(new libCommand.Command({
|
||||
yargs.positional("name", { describe: "Name of user to revoke token for", type: "string" });
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
await libLink.messages.revokeUserToken.send(control, { name: args.name });
|
||||
await control.send(new libData.UserRevokeTokenRequest(args.name));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1500,9 +1451,7 @@ userCommands.add(new libCommand.Command({
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
await libLink.messages.setUserAdmin.send(control, {
|
||||
name: args.user, create: args.create, admin: !args.revoke,
|
||||
});
|
||||
await control.send(new libData.UserSetAdminRequest(args.user, args.create, !args.revoke));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1515,9 +1464,7 @@ userCommands.add(new libCommand.Command({
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
await libLink.messages.setUserWhitelisted.send(control, {
|
||||
name: args.user, create: args.create, whitelisted: !args.remove,
|
||||
});
|
||||
await control.send(new libData.UserSetWhitelistedRequest(args.user, args.create, !args.remove));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1531,9 +1478,7 @@ userCommands.add(new libCommand.Command({
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
await libLink.messages.setUserBanned.send(control, {
|
||||
name: args.user, create: args.create, banned: !args.pardon, reason: args.reason,
|
||||
});
|
||||
await control.send(new libData.UserSetBannedRequest(args.user, args.create, !args.pardon, args.reason));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1543,7 +1488,7 @@ userCommands.add(new libCommand.Command({
|
||||
yargs.positional("roles", { describe: "roles to assign", type: "string" });
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let response = await libLink.messages.listRoles.send(control);
|
||||
let roles = await control.send(new libData.RoleListRequest());
|
||||
|
||||
let resolvedRoles = [];
|
||||
for (let roleName of args.roles) {
|
||||
@@ -1553,7 +1498,7 @@ userCommands.add(new libCommand.Command({
|
||||
|
||||
} else {
|
||||
let found = false;
|
||||
for (let role of response.list) {
|
||||
for (let role of roles) {
|
||||
if (role.name === roleName) {
|
||||
resolvedRoles.push(role.id);
|
||||
found = true;
|
||||
@@ -1567,7 +1512,7 @@ userCommands.add(new libCommand.Command({
|
||||
}
|
||||
}
|
||||
|
||||
await libLink.messages.updateUserRoles.send(control, { name: args.user, roles: resolvedRoles });
|
||||
await control.send(new libData.UserUpdateRolesRequest(args.user, resolvedRoles));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1576,7 +1521,7 @@ userCommands.add(new libCommand.Command({
|
||||
yargs.positional("user", { describe: "Name of user to delete", type: "string" });
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
await libLink.messages.deleteUser.send(control, { name: args.user });
|
||||
await control.send(new libData.UserDeleteRequest(args.user));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1596,9 +1541,9 @@ logCommands.add(new libCommand.Command({
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
let instance_ids = args.instance ? [await libCommand.resolveInstance(control, args.instance)] : [];
|
||||
let host_ids = args.host ? [await libCommand.resolveHost(control, args.host)] : [];
|
||||
await control.setLogSubscriptions({ all: args.all, controller: args.controller, host_ids, instance_ids });
|
||||
let instanceIds = args.instance ? [await libCommand.resolveInstance(control, args.instance)] : [];
|
||||
let hostIds = args.host ? [await libCommand.resolveHost(control, args.host)] : [];
|
||||
await control.setLogSubscriptions({ all: args.all, controller: args.controller, hostIds, instanceIds });
|
||||
control.keepOpen = true;
|
||||
},
|
||||
}));
|
||||
@@ -1621,17 +1566,17 @@ logCommands.add(new libCommand.Command({
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
let instance_ids = args.instance ? [await libCommand.resolveInstance(control, args.instance)] : [];
|
||||
let host_ids = args.host ? [await libCommand.resolveHost(control, args.host)] : [];
|
||||
let result = await libLink.messages.queryLog.send(control, {
|
||||
all: args.all,
|
||||
controller: args.controller,
|
||||
host_ids,
|
||||
instance_ids,
|
||||
max_level: args.maxLevel,
|
||||
limit: args.limit,
|
||||
order: args.start ? "asc" : "desc",
|
||||
});
|
||||
let instanceIds = args.instance ? [await libCommand.resolveInstance(control, args.instance)] : [];
|
||||
let hostIds = args.host ? [await libCommand.resolveHost(control, args.host)] : [];
|
||||
let result = await control.send(new libData.LogQueryRequest(
|
||||
args.all,
|
||||
args.controller,
|
||||
hostIds,
|
||||
instanceIds,
|
||||
args.maxLevel,
|
||||
args.limit,
|
||||
args.start ? "asc" : "desc",
|
||||
));
|
||||
|
||||
if (!args.start) {
|
||||
result.log.reverse();
|
||||
@@ -1655,7 +1600,7 @@ const debugCommands = new libCommand.CommandTree({ name: "debug", description: "
|
||||
debugCommands.add(new libCommand.Command({
|
||||
definition: ["dump-ws", "Dump WebSocket messages sent and received by controller", (yargs) => { }],
|
||||
handler: async function(args, control) {
|
||||
await libLink.messages.debugDumpWs.send(control);
|
||||
await control.send(new libData.DebugDumpWsRequest());
|
||||
control.keepOpen = true;
|
||||
},
|
||||
}));
|
||||
|
||||
+15
-9
@@ -22,6 +22,7 @@ const winston = require("winston");
|
||||
require("winston-daily-rotate-file");
|
||||
|
||||
// internal libraries
|
||||
const libData = require("@clusterio/lib/data");
|
||||
const libLink = require("@clusterio/lib/link");
|
||||
const libPluginLoader = require("@clusterio/lib/plugin_loader");
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
@@ -51,15 +52,19 @@ class HostConnector extends libLink.WebSocketClientConnector {
|
||||
plugins[pluginInfo.name] = pluginInfo.version;
|
||||
}
|
||||
|
||||
this.sendHandshake("register_host", {
|
||||
token: this.hostConfig.get("host.controller_token"),
|
||||
agent: "Clusterio Host",
|
||||
version,
|
||||
id: this.hostConfig.get("host.id"),
|
||||
name: this.hostConfig.get("host.name"),
|
||||
public_address: this.hostConfig.get("host.public_address"),
|
||||
plugins,
|
||||
});
|
||||
this.sendHandshake(
|
||||
new libData.MessageRegisterHost(
|
||||
new libData.RegisterHostData(
|
||||
this.hostConfig.get("host.controller_token"),
|
||||
"Clusterio Host",
|
||||
version,
|
||||
this.hostConfig.get("host.name"),
|
||||
this.hostConfig.get("host.id"),
|
||||
this.hostConfig.get("host.public_address"),
|
||||
plugins,
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +145,7 @@ async function startHost() {
|
||||
|
||||
logger.info("Loading Plugin info");
|
||||
let pluginInfos = await libPluginLoader.loadPluginInfos(pluginList);
|
||||
libLink.registerPluginMessages(pluginInfos);
|
||||
libConfig.registerPluginConfigGroups(pluginInfos);
|
||||
libConfig.finalizeConfigs();
|
||||
|
||||
|
||||
+295
-232
@@ -10,6 +10,7 @@ const stream = require("stream");
|
||||
const util = require("util");
|
||||
|
||||
// internal libraries
|
||||
const libData = require("@clusterio/lib/data");
|
||||
const libFileOps = require("@clusterio/lib/file_ops");
|
||||
const libLink = require("@clusterio/lib/link");
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
@@ -80,6 +81,157 @@ async function discoverInstances(instancesDir) {
|
||||
return instanceInfos;
|
||||
}
|
||||
|
||||
const instanceStartingMessages = new Set([
|
||||
libData.InstanceStartRequest.name,
|
||||
libData.InstanceLoadScenarioRequest.name,
|
||||
libData.InstanceCreateSaveRequest.name,
|
||||
libData.InstanceExportDataRequest.name,
|
||||
]);
|
||||
|
||||
class HostRouter {
|
||||
/** @type {module:lib/host/src/Host} */
|
||||
host;
|
||||
|
||||
constructor(host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward a message to the next hop towards its destination.
|
||||
*
|
||||
* @param {module:lib/link.Link} origin -
|
||||
* Link the message originated from.
|
||||
* @param {module:lib/data.Message} message - Message to process.
|
||||
* @param {boolean} hasFallback - is fallback available
|
||||
* @returns {boolean} true if the message was handled, false if fallback
|
||||
* is requested
|
||||
*/
|
||||
forwardMessage(origin, message, hasFallback) {
|
||||
let dst = message.dst;
|
||||
let nextHop;
|
||||
let msg;
|
||||
if (dst.type === libData.Address.broadcast) {
|
||||
this.broadcastMessage(origin, message);
|
||||
return true;
|
||||
} else if (
|
||||
dst.type === libData.Address.controller
|
||||
|| dst.type === libData.Address.control
|
||||
|| dst.type === libData.Address.host && dst.id !== this.host.config.get("host.id")
|
||||
|| dst.type === libData.Address.instance && !this.host.instanceInfos.has(dst.id)
|
||||
) {
|
||||
nextHop = this.host;
|
||||
} else if (dst.type === libData.Address.instance && this.host.instanceInfos.has(dst.id)) {
|
||||
nextHop = this.host.instanceConnections.get(dst.id);
|
||||
}
|
||||
|
||||
if (nextHop === origin) {
|
||||
msg = `Message would return back to sender ${origin.dst}.`;
|
||||
nextHop = undefined;
|
||||
}
|
||||
|
||||
if (message.type === "request") {
|
||||
if (dst.type === libData.Address.instance && instanceStartingMessages.has(message.name)) {
|
||||
this.wakeInstance(origin, message, nextHop);
|
||||
return true;
|
||||
|
||||
} else if (!nextHop) {
|
||||
if (dst.type === libData.Address.instance) {
|
||||
if (hasFallback) {
|
||||
return false;
|
||||
}
|
||||
origin.connector.sendResponseError(
|
||||
new libData.ResponseError("Instance is not running."), message.src
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// XXX What if the session is invalidated and there is no
|
||||
// response? Need to track pending requests here.
|
||||
}
|
||||
|
||||
if (nextHop) {
|
||||
this.sendMessage(nextHop, message, origin);
|
||||
} else {
|
||||
this.warnUnrouted(message, msg);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
broadcastMessage(origin, message) {
|
||||
let dst = message.dst;
|
||||
if (message.type !== "event") {
|
||||
this.warnUnrouted(message, `Unexpected broadcast of ${message.type}`);
|
||||
} else if (dst.id === libData.Address.host || dst.id === libData.Address.instance) {
|
||||
for (let instanceConnection of this.host.instanceConnections.values()) {
|
||||
if (instanceConnection !== origin) {
|
||||
instanceConnection.connector.send(message);
|
||||
}
|
||||
}
|
||||
if (this.host !== origin) {
|
||||
this.host.connector.send(message);
|
||||
}
|
||||
} else if (dst.id === libData.Address.control) {
|
||||
if (this.host !== origin) {
|
||||
this.host.connector.send(message);
|
||||
} else {
|
||||
logger.warn(`Received control broadcast of ${message.name} from master.`);
|
||||
}
|
||||
} else {
|
||||
this.warnUnrouted(message, `Unexpected broacdast target ${dst.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
wakeInstance(origin, message, nextHop) {
|
||||
let dst = message.dst;
|
||||
if (nextHop) {
|
||||
origin.connector.sendResponseError(
|
||||
new libData.ResponseError("Instance is already running."), message.src
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this.host._shuttingDown) {
|
||||
origin.connector.sendResponseError(
|
||||
new libData.ResponseError("Host is shutting down."), message.src
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.host._connectInstance(dst.id).then(instanceConnection => {
|
||||
instanceConnection.connector.send(message);
|
||||
}).catch(err => {
|
||||
logger.error(`Error starting instance:\n${err.stack}`, this.host.instanceLogMeta(dst.id));
|
||||
origin.connector.sendResponseError(
|
||||
new libData.ResponseError(err.message, err.code, err.stack), message.src
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
sendMessage(nextHop, message, origin) {
|
||||
try {
|
||||
if (message.type === "request") {
|
||||
nextHop.forwardRequest(message, origin);
|
||||
} else {
|
||||
nextHop.connector.send(message);
|
||||
}
|
||||
} catch (err) {
|
||||
if (message.type === "request") {
|
||||
origin.connector.sendResponseError(
|
||||
new libData.ResponseError(err.message, err.code, err.stack), message.src
|
||||
);
|
||||
}
|
||||
logger.warn(`Failed to deliver ${message.name || "message"} (${message.type}): ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
warnUnrouted(message, msg) {
|
||||
let dst = message.dst;
|
||||
let baseMsg = `No destination for ${message.constructor.name} routed from ${message.src} to ${dst}`;
|
||||
logger.warn(msg ? `${baseMsg}: ${msg}.` : `${baseMsg}.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles running the host
|
||||
*
|
||||
@@ -87,17 +239,11 @@ async function discoverInstances(instancesDir) {
|
||||
* @alias module:host/src/Host
|
||||
*/
|
||||
class Host extends libLink.Link {
|
||||
// I don't like God classes, but the alternative of putting all this state
|
||||
// into global variables is not much better.
|
||||
constructor(connector, hostConfig, tlsCa, pluginInfos) {
|
||||
super("host", "controller", connector);
|
||||
libLink.attachAllMessages(this);
|
||||
super(connector);
|
||||
|
||||
this.router = new HostRouter(this);
|
||||
this.pluginInfos = pluginInfos;
|
||||
for (let pluginInfo of pluginInfos) {
|
||||
libPlugin.attachPluginMessages(this, { info: pluginInfo });
|
||||
}
|
||||
|
||||
this.config = hostConfig;
|
||||
|
||||
/**
|
||||
@@ -158,11 +304,28 @@ class Host extends libLink.Link {
|
||||
|
||||
for (let event of ["connect", "drop", "resume", "close"]) {
|
||||
this.connector.on(event, () => {
|
||||
let message = new libData.ControllerConnectionEvent(event);
|
||||
for (let instanceConnection of this.instanceConnections.values()) {
|
||||
libLink.messages.controllerConnectionEvent.send(instanceConnection, { event });
|
||||
instanceConnection.send(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.handle(libData.SyncUserListsEvent, this.handleSyncUserListsEvent.bind(this));
|
||||
this.snoopEvent(libData.InstanceAdminlistUpdateEvent, this.handleAdminlistUpdateEvent.bind(this));
|
||||
this.snoopEvent(libData.InstanceBanlistUpdateEvent, this.handleBanlistUpdateEvent.bind(this));
|
||||
this.snoopEvent(libData.InstanceWhitelistUpdateEvent, this.handleWhitelistUpdateEvent.bind(this));
|
||||
this.handle(libData.InstanceAssignInternalRequest, this.handleInstanceAssignInternalRequest.bind(this));
|
||||
this.handle(libData.InstanceUnassignInternalRequest, this.handleInstanceUnassignInternalRequest.bind(this));
|
||||
this.handle(libData.HostMetricsRequest, this.handleHostMetricsRequest.bind(this));
|
||||
this.fallbackRequest(libData.InstanceListSavesRequest, this.fallbackInstanceListSavesRequest.bind(this));
|
||||
this.handle(libData.InstanceRenameSaveRequest, this.handleInstanceRenameSaveRequest.bind(this));
|
||||
this.handle(libData.InstanceCopySaveRequest, this.handleInstanceCopySaveRequest.bind(this));
|
||||
this.handle(libData.InstanceTransferSaveRequest, this.handleInstanceTransferSaveRequest.bind(this));
|
||||
this.handle(libData.InstanceDeleteSaveRequest, this.handleInstanceDeleteSaveRequest.bind(this));
|
||||
this.handle(libData.InstancePullSaveRequest, this.handleInstancePullSaveRequest.bind(this));
|
||||
this.handle(libData.InstancePushSaveRequest, this.handleInstancePushSaveRequest.bind(this));
|
||||
this.handle(libData.InstanceDeleteInternalRequest, this.handleInstanceDeleteInternalRequest.bind(this));
|
||||
}
|
||||
|
||||
async _createNewInstanceDir(name) {
|
||||
@@ -189,45 +352,15 @@ class Host extends libLink.Link {
|
||||
throw Error("Unable to create instance dir, retry threshold reached");
|
||||
}
|
||||
|
||||
async forwardRequestToInstance(message, request) {
|
||||
let instanceId = message.data.instance_id;
|
||||
if (!this.instanceInfos.has(instanceId)) {
|
||||
throw new libErrors.RequestError(`Instance with ID ${instanceId} does not exist`);
|
||||
}
|
||||
|
||||
let instanceConnection = this.instanceConnections.get(instanceId);
|
||||
if (!instanceConnection) {
|
||||
throw new libErrors.RequestError(`Instance ID ${instanceId} is not running`);
|
||||
}
|
||||
|
||||
if (request.plugin && !instanceConnection.plugins.has(request.plugin)) {
|
||||
throw new libErrors.RequestError(`Instance ID ${instanceId} does not have ${request.plugin} plugin loaded`);
|
||||
}
|
||||
|
||||
return await request.send(instanceConnection, message.data);
|
||||
}
|
||||
|
||||
async forwardEventToInstance(message, event) {
|
||||
let instanceId = message.data.instance_id;
|
||||
if (!this.instanceInfos.has(instanceId)) { return; }
|
||||
|
||||
let instanceConnection = this.instanceConnections.get(instanceId);
|
||||
if (!instanceConnection) { return; }
|
||||
if (event.plugin && !instanceConnection.plugins.has(event.plugin)) { return; }
|
||||
|
||||
event.send(instanceConnection, message.data);
|
||||
}
|
||||
|
||||
async broadcastEventToInstance(message, event) {
|
||||
async broadcastEventToInstance(event) {
|
||||
for (let instanceConnection of this.instanceConnections.values()) {
|
||||
if (event.plugin && !instanceConnection.plugins.has(event.plugin)) { continue; }
|
||||
|
||||
event.send(instanceConnection, message.data);
|
||||
if (event.constructor.plugin && !instanceConnection.plugins.has(event.plugin)) { continue; }
|
||||
instanceConnection.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
async syncUserListsEventHandler(message) {
|
||||
let updateList = (list, updatedList, prop, event) => {
|
||||
async handleSyncUserListsEvent(request) {
|
||||
let updateList = (list, updatedList, Event) => {
|
||||
let added = new Set(updatedList);
|
||||
let removed = new Set(list);
|
||||
list.forEach(el => added.delete(el));
|
||||
@@ -235,23 +368,19 @@ class Host extends libLink.Link {
|
||||
|
||||
for (let name of added) {
|
||||
list.add(name);
|
||||
this.broadcastEventToInstance(
|
||||
{ data: { name, [prop]: true }}, event
|
||||
);
|
||||
this.broadcastEventToInstance(new Event(name, true));
|
||||
}
|
||||
|
||||
for (let name of removed) {
|
||||
list.delete(name);
|
||||
this.broadcastEventToInstance(
|
||||
{ data: { name, [prop]: false }}, event
|
||||
);
|
||||
this.broadcastEventToInstance(new Event(name, false));
|
||||
}
|
||||
};
|
||||
|
||||
updateList(this.adminlist, message.data.adminlist, "admin", libLink.messages.adminlistUpdate);
|
||||
updateList(this.whitelist, message.data.whitelist, "whitelisted", libLink.messages.whitelistUpdate);
|
||||
updateList(this.adminlist, request.adminlist, libData.InstanceAdminlistUpdateEvent);
|
||||
updateList(this.whitelist, request.whitelist, libData.InstanceWhitelistUpdateEvent);
|
||||
|
||||
let addedOrChanged = new Map(message.data.banlist);
|
||||
let addedOrChanged = new Map(request.banlist);
|
||||
let removed = new Set(this.banlist.keys());
|
||||
addedOrChanged.forEach((_, name) => removed.delete(name));
|
||||
this.banlist.forEach((reason, name) => {
|
||||
@@ -262,21 +391,17 @@ class Host extends libLink.Link {
|
||||
|
||||
for (let [name, reason] of addedOrChanged) {
|
||||
this.banlist.set(name, reason);
|
||||
this.broadcastEventToInstance(
|
||||
{ data: { name, banned: true, reason }}, libLink.messages.banlistUpdate
|
||||
);
|
||||
this.broadcastEventToInstance(new libData.InstanceBanlistUpdateEvent(name, true, reason));
|
||||
}
|
||||
|
||||
for (let name of removed) {
|
||||
this.banlist.delete(name);
|
||||
this.broadcastEventToInstance(
|
||||
{ data: { name, banned: false, reason: "" }}, libLink.messages.banlistUpdate
|
||||
);
|
||||
this.broadcastEventToInstance(new libData.InstanceBanlistUpdateEvent(name, false, ""));
|
||||
}
|
||||
}
|
||||
|
||||
async adminlistUpdateEventHandler(message) {
|
||||
let { name, admin } = message.data;
|
||||
async handleAdminlistUpdateEvent(request) {
|
||||
let { name, admin } = request;
|
||||
if (admin) {
|
||||
this.adminlist.add(name);
|
||||
} else {
|
||||
@@ -284,8 +409,8 @@ class Host extends libLink.Link {
|
||||
}
|
||||
}
|
||||
|
||||
async banlistUpdateEventHandler(message) {
|
||||
let { name, banned, reason } = message.data;
|
||||
async handleBanlistUpdateEvent(request) {
|
||||
let { name, banned, reason } = request;
|
||||
if (banned) {
|
||||
this.banlist.set(name, reason);
|
||||
} else {
|
||||
@@ -293,8 +418,8 @@ class Host extends libLink.Link {
|
||||
}
|
||||
}
|
||||
|
||||
async whitelistUpdateEventHandler(message) {
|
||||
let { name, whitelisted } = message.data;
|
||||
async handleWhitelistUpdateEvent(request) {
|
||||
let { name, whitelisted } = request;
|
||||
if (whitelisted) {
|
||||
this.whitelist.add(name);
|
||||
} else {
|
||||
@@ -302,21 +427,21 @@ class Host extends libLink.Link {
|
||||
}
|
||||
}
|
||||
|
||||
async assignInstanceRequestHandler(message) {
|
||||
let { instance_id, serialized_config } = message.data;
|
||||
let instanceInfo = this.instanceInfos.get(instance_id);
|
||||
async handleInstanceAssignInternalRequest(request) {
|
||||
let { instanceId, config } = request;
|
||||
let instanceInfo = this.instanceInfos.get(instanceId);
|
||||
if (instanceInfo) {
|
||||
instanceInfo.config.update(serialized_config, true, "controller");
|
||||
logger.verbose(`Updated config for ${instanceInfo.path}`, this.instanceLogMeta(instance_id, instanceInfo));
|
||||
instanceInfo.config.update(config, true, "controller");
|
||||
logger.verbose(`Updated config for ${instanceInfo.path}`, this.instanceLogMeta(instanceId, instanceInfo));
|
||||
|
||||
} else {
|
||||
instanceInfo = this.discoveredInstanceInfos.get(instance_id);
|
||||
instanceInfo = this.discoveredInstanceInfos.get(instanceId);
|
||||
if (instanceInfo) {
|
||||
instanceInfo.config.update(serialized_config, true, "controller");
|
||||
instanceInfo.config.update(config, true, "controller");
|
||||
|
||||
} else {
|
||||
let instanceConfig = new libConfig.InstanceConfig("host");
|
||||
await instanceConfig.load(serialized_config, "controller");
|
||||
await instanceConfig.load(config, "controller");
|
||||
|
||||
let instanceDir = await this._createNewInstanceDir(instanceConfig.get("instance.name"));
|
||||
|
||||
@@ -327,21 +452,22 @@ class Host extends libLink.Link {
|
||||
config: instanceConfig,
|
||||
};
|
||||
|
||||
this.discoveredInstanceInfos.set(instance_id, instanceInfo);
|
||||
this.discoveredInstanceInfos.set(instanceId, instanceInfo);
|
||||
}
|
||||
|
||||
this.instanceInfos.set(instance_id, instanceInfo);
|
||||
this.instanceInfos.set(instanceId, instanceInfo);
|
||||
logger.verbose(`assigned instance ${instanceInfo.config.get("instance.name")}`);
|
||||
}
|
||||
|
||||
// Somewhat hacky, but in the event of a lost session the status is
|
||||
// resent on assigment since the controller sends an assigment
|
||||
// request for all the instances it knows should be on this host.
|
||||
let instanceConnection = this.instanceConnections.get(instance_id);
|
||||
libLink.messages.instanceStatusChanged.send(this, {
|
||||
instance_id,
|
||||
status: instanceConnection ? instanceConnection.status : "stopped",
|
||||
});
|
||||
let instanceConnection = this.instanceConnections.get(instanceId);
|
||||
this.send(
|
||||
new libData.InstanceStatusChangedEvent(
|
||||
instanceId, instanceConnection ? instanceConnection.status : "stopped", null
|
||||
)
|
||||
);
|
||||
|
||||
// save a copy of the instance config
|
||||
let warnedOutput = {
|
||||
@@ -354,13 +480,13 @@ class Host extends libLink.Link {
|
||||
);
|
||||
}
|
||||
|
||||
async unassignInstanceRequestHandler(message) {
|
||||
let instanceId = message.data.instance_id;
|
||||
async handleInstanceUnassignInternalRequest(request) {
|
||||
let instanceId = request.instanceId;
|
||||
let instanceInfo = this.instanceInfos.get(instanceId);
|
||||
if (instanceInfo) {
|
||||
let instanceConnection = this.instanceConnections.get(instanceId);
|
||||
if (instanceConnection && ["starting", "running"].includes(instanceConnection.status)) {
|
||||
await libLink.messages.stopInstance.send(instanceConnection, { instance_id: instanceId });
|
||||
await instanceConnection.send(new libData.InstanceStopRequest());
|
||||
}
|
||||
|
||||
this.instanceInfos.delete(instanceId);
|
||||
@@ -396,7 +522,9 @@ class Host extends libLink.Link {
|
||||
throw new libErrors.RequestError(`Instance with ID ${instanceId} is running`);
|
||||
}
|
||||
|
||||
let [connectionClient, connectionServer] = libLink.VirtualConnector.makePair();
|
||||
let hostAddress = new libData.Address(libData.Address.host, this.id);
|
||||
let instanceAddress = new libData.Address(libData.Address.instance, instanceId);
|
||||
let [connectionClient, connectionServer] = libLink.VirtualConnector.makePair(instanceAddress, hostAddress);
|
||||
let instanceConnection = new InstanceConnection(connectionServer, this, instanceId);
|
||||
let instance = new Instance(
|
||||
this, connectionClient, instanceInfo.path, this.config.get("host.factorio_directory"), instanceInfo.config
|
||||
@@ -408,10 +536,10 @@ class Host extends libLink.Link {
|
||||
return instanceConnection;
|
||||
}
|
||||
|
||||
async getMetricsRequestHandler() {
|
||||
async handleHostMetricsRequest() {
|
||||
let requests = [];
|
||||
for (let instanceConnection of this.instanceConnections.values()) {
|
||||
requests.push(libLink.messages.getMetrics.send(instanceConnection));
|
||||
requests.push(instanceConnection.send(new libData.InstanceMetricsRequest()));
|
||||
}
|
||||
|
||||
let results = [];
|
||||
@@ -434,86 +562,36 @@ class Host extends libLink.Link {
|
||||
return { results };
|
||||
}
|
||||
|
||||
async startInstanceRequestHandler(message, request) {
|
||||
let instanceId = message.data.instance_id;
|
||||
try {
|
||||
let instanceConnection = await this._connectInstance(instanceId);
|
||||
return await request.send(instanceConnection, message.data);
|
||||
} catch (err) {
|
||||
if (!(err instanceof libErrors.RequestError)) {
|
||||
logger.error(`Error starting instance:\n${err.stack}`, this.instanceLogMeta(instanceId));
|
||||
throw new libErrors.RequestError(err.message);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
async fallbackInstanceListSavesRequest(request, src, dst) {
|
||||
let instanceInfo = this.getRequestInstanceInfo(dst.id);
|
||||
return await Instance.listSaves(path.join(instanceInfo.path, "saves"), null);
|
||||
}
|
||||
|
||||
async loadScenarioRequestHandler(message, request) {
|
||||
let instanceId = message.data.instance_id;
|
||||
try {
|
||||
let instanceConnection = await this._connectInstance(instanceId);
|
||||
return await request.send(instanceConnection, message.data);
|
||||
} catch (err) {
|
||||
if (!(err instanceof libErrors.RequestError)) {
|
||||
logger.error(`Error starting instance:\n${err.stack}`, this.instanceLogMeta(instanceId));
|
||||
throw new libErrors.RequestError(err.message);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async listSavesRequestHandler(message, request) {
|
||||
let instanceId = message.data.instance_id;
|
||||
let instanceConnection = this.instanceConnections.get(instanceId);
|
||||
if (instanceConnection) {
|
||||
return await request.send(instanceConnection, message.data);
|
||||
}
|
||||
|
||||
async handleInstanceRenameSaveRequest(request) {
|
||||
let { instanceId, oldName, newName } = request;
|
||||
checkRequestSaveName(oldName);
|
||||
checkRequestSaveName(newName);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instanceId);
|
||||
return {
|
||||
list: await Instance.listSaves(path.join(instanceInfo.path, "saves"), null),
|
||||
};
|
||||
}
|
||||
|
||||
async createSaveRequestHandler(message, request) {
|
||||
let instanceId = message.data.instance_id;
|
||||
try {
|
||||
let instanceConnection = await this._connectInstance(instanceId);
|
||||
await request.send(instanceConnection, message.data);
|
||||
} catch (err) {
|
||||
if (!(err instanceof libErrors.RequestError)) {
|
||||
logger.error(`Error creating save:\n${err.stack}`, this.instanceLogMeta(instanceId));
|
||||
throw new libErrors.RequestError(err.message);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async renameSaveRequestHandler(message) {
|
||||
let { instance_id, old_name, new_name } = message.data;
|
||||
checkRequestSaveName(old_name);
|
||||
checkRequestSaveName(new_name);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instance_id);
|
||||
try {
|
||||
await fs.move(
|
||||
path.join(instanceInfo.path, "saves", old_name),
|
||||
path.join(instanceInfo.path, "saves", new_name),
|
||||
path.join(instanceInfo.path, "saves", oldName),
|
||||
path.join(instanceInfo.path, "saves", newName),
|
||||
{ overwrite: false },
|
||||
);
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
throw new libErrors.RequestError(`${old_name} does not exist`);
|
||||
throw new libErrors.RequestError(`${oldName} does not exist`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
await this.sendSaveListUpdate(instance_id, path.join(instanceInfo.path, "saves"));
|
||||
await this.sendSaveListUpdate(instanceId, path.join(instanceInfo.path, "saves"));
|
||||
}
|
||||
|
||||
async copySaveRequestHandler(message) {
|
||||
let { instance_id, source, destination } = message.data;
|
||||
async handleInstanceCopySaveRequest(request) {
|
||||
let { instanceId, source, destination } = request;
|
||||
checkRequestSaveName(source);
|
||||
checkRequestSaveName(destination);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instance_id);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instanceId);
|
||||
try {
|
||||
await fs.copy(
|
||||
path.join(instanceInfo.path, "saves", source),
|
||||
@@ -526,83 +604,83 @@ class Host extends libLink.Link {
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
await this.sendSaveListUpdate(instance_id, path.join(instanceInfo.path, "saves"));
|
||||
await this.sendSaveListUpdate(instanceId, path.join(instanceInfo.path, "saves"));
|
||||
}
|
||||
|
||||
async transferSaveRequestHandler(message) {
|
||||
let { source_save, target_save, copy, instance_id, target_instance_id } = message.data;
|
||||
checkRequestSaveName(source_save);
|
||||
checkRequestSaveName(target_save);
|
||||
let sourceInstanceInfo = this.getRequestInstanceInfo(instance_id);
|
||||
let targetInstanceInfo = this.getRequestInstanceInfo(target_instance_id);
|
||||
async handleInstanceTransferSaveRequest(request) {
|
||||
let { sourceName, targetName, copy, sourceInstanceId, targetInstanceId } = request;
|
||||
checkRequestSaveName(sourceName);
|
||||
checkRequestSaveName(targetName);
|
||||
let sourceInstanceInfo = this.getRequestInstanceInfo(sourceInstanceId);
|
||||
let targetInstanceInfo = this.getRequestInstanceInfo(targetInstanceId);
|
||||
|
||||
// For consistency with remote transfer initiated through pullSave the
|
||||
// target is renamed if it already exists.
|
||||
target_save = await libFileOps.findUnusedName(
|
||||
path.join(targetInstanceInfo.path, "saves"), target_save, ".zip"
|
||||
targetName = await libFileOps.findUnusedName(
|
||||
path.join(targetInstanceInfo.path, "saves"), targetName, ".zip"
|
||||
);
|
||||
|
||||
try {
|
||||
if (copy) {
|
||||
await fs.copy(
|
||||
path.join(sourceInstanceInfo.path, "saves", source_save),
|
||||
path.join(targetInstanceInfo.path, "saves", target_save),
|
||||
path.join(sourceInstanceInfo.path, "saves", sourceName),
|
||||
path.join(targetInstanceInfo.path, "saves", targetName),
|
||||
{ overwrite: true },
|
||||
);
|
||||
} else {
|
||||
await fs.move(
|
||||
path.join(sourceInstanceInfo.path, "saves", source_save),
|
||||
path.join(targetInstanceInfo.path, "saves", target_save),
|
||||
path.join(sourceInstanceInfo.path, "saves", sourceName),
|
||||
path.join(targetInstanceInfo.path, "saves", targetName),
|
||||
{ overwrite: true },
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
throw new libErrors.RequestError(`${source_save} does not exist`);
|
||||
throw new libErrors.RequestError(`${sourceName} does not exist`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
await this.sendSaveListUpdate(instance_id, path.join(sourceInstanceInfo.path, "saves"));
|
||||
await this.sendSaveListUpdate(target_instance_id, path.join(targetInstanceInfo.path, "saves"));
|
||||
await this.sendSaveListUpdate(sourceInstanceId, path.join(sourceInstanceInfo.path, "saves"));
|
||||
await this.sendSaveListUpdate(targetInstanceId, path.join(targetInstanceInfo.path, "saves"));
|
||||
|
||||
return { save: target_save };
|
||||
return targetName;
|
||||
}
|
||||
|
||||
async sendSaveListUpdate(instance_id, savesDir) {
|
||||
let instanceConnection = this.instanceConnections.get(instance_id);
|
||||
async sendSaveListUpdate(instanceId, savesDir) {
|
||||
let instanceConnection = this.instanceConnections.get(instanceId);
|
||||
let saveList;
|
||||
if (instanceConnection) {
|
||||
saveList = (await libLink.messages.listSaves.send(instanceConnection, { instance_id })).list;
|
||||
saveList = await instanceConnection.send(new libData.InstanceListSavesRequest());
|
||||
} else {
|
||||
saveList = await Instance.listSaves(savesDir, null);
|
||||
}
|
||||
|
||||
libLink.messages.saveListUpdate.send(this, { instance_id, list: saveList });
|
||||
this.send(new libData.InstanceSaveListUpdateEvent(instanceId, saveList));
|
||||
}
|
||||
|
||||
async deleteSaveRequestHandler(message) {
|
||||
let { instance_id, save } = message.data;
|
||||
checkRequestSaveName(save);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instance_id);
|
||||
async handleInstanceDeleteSaveRequest(request) {
|
||||
let { instanceId, name } = request;
|
||||
checkRequestSaveName(name);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instanceId);
|
||||
|
||||
try {
|
||||
await fs.unlink(path.join(instanceInfo.path, "saves", save));
|
||||
await fs.unlink(path.join(instanceInfo.path, "saves", name));
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
throw new libErrors.RequestError(`${save} does not exist`);
|
||||
throw new libErrors.RequestError(`${name} does not exist`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
await this.sendSaveListUpdate(instance_id, path.join(instanceInfo.path, "saves"));
|
||||
await this.sendSaveListUpdate(instanceId, path.join(instanceInfo.path, "saves"));
|
||||
}
|
||||
|
||||
async pullSaveRequestHandler(message) {
|
||||
let { instance_id, stream_id, filename } = message.data;
|
||||
checkRequestSaveName(filename);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instance_id);
|
||||
async handleInstancePullSaveRequest(request) {
|
||||
let { instanceId, streamId, name } = request;
|
||||
checkRequestSaveName(name);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instanceId);
|
||||
|
||||
let url = new URL(this.config.get("host.controller_url"));
|
||||
url.pathname += `api/stream/${stream_id}`;
|
||||
url.pathname += `api/stream/${streamId}`;
|
||||
let response = await phin({
|
||||
url, method: "GET",
|
||||
core: { ca: this.tlsCa },
|
||||
@@ -615,7 +693,7 @@ class Host extends libLink.Link {
|
||||
}
|
||||
|
||||
let savesDir = path.join(instanceInfo.path, "saves");
|
||||
let tempFilename = filename.replace(/(\.zip)?$/, ".tmp.zip");
|
||||
let tempFilename = name.replace(/(\.zip)?$/, ".tmp.zip");
|
||||
let writeStream;
|
||||
while (true) {
|
||||
try {
|
||||
@@ -633,61 +711,47 @@ class Host extends libLink.Link {
|
||||
response.pipe(writeStream);
|
||||
await finished(writeStream);
|
||||
|
||||
filename = await libFileOps.findUnusedName(savesDir, filename, ".zip");
|
||||
await fs.rename(path.join(savesDir, tempFilename), path.join(savesDir, filename));
|
||||
name = await libFileOps.findUnusedName(savesDir, name, ".zip");
|
||||
await fs.rename(path.join(savesDir, tempFilename), path.join(savesDir, name));
|
||||
|
||||
await this.sendSaveListUpdate(instance_id, savesDir);
|
||||
return { save: filename };
|
||||
await this.sendSaveListUpdate(instanceId, savesDir);
|
||||
return name;
|
||||
}
|
||||
|
||||
async pushSaveRequestHandler(message) {
|
||||
let { instance_id, stream_id, save } = message.data;
|
||||
checkRequestSaveName(save);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instance_id);
|
||||
async handleInstancePushSaveRequest(request) {
|
||||
let { instanceId, streamId, name } = request;
|
||||
checkRequestSaveName(name);
|
||||
let instanceInfo = this.getRequestInstanceInfo(instanceId);
|
||||
|
||||
let content;
|
||||
try {
|
||||
// phin doesn't support streaming requests :(
|
||||
content = await fs.readFile(path.join(instanceInfo.path, "saves", save));
|
||||
content = await fs.readFile(path.join(instanceInfo.path, "saves", name));
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
throw new libErrors.RequestError(`${save} does not exist`);
|
||||
throw new libErrors.RequestError(`${name} does not exist`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
let url = new URL(this.config.get("host.controller_url"));
|
||||
url.pathname += `api/stream/${stream_id}`;
|
||||
url.pathname += `api/stream/${streamId}`;
|
||||
phin({
|
||||
url, method: "PUT",
|
||||
core: { ca: this.tlsCa },
|
||||
data: content,
|
||||
}).catch(err => {
|
||||
logger.error(`Error pushing save to controller:\n${err.stack}`, this.instanceLogMeta(instance_id));
|
||||
logger.error(`Error pushing save to controller:\n${err.stack}`, this.instanceLogMeta(instanceId));
|
||||
});
|
||||
}
|
||||
|
||||
async exportDataRequestHandler(message, request) {
|
||||
let instanceId = message.data.instance_id;
|
||||
try {
|
||||
let instanceConnection = await this._connectInstance(instanceId);
|
||||
await request.send(instanceConnection, message.data);
|
||||
} catch (err) {
|
||||
if (!(err instanceof libErrors.RequestError)) {
|
||||
logger.error(`Error exporting data:\n${err.stack}`, this.instanceLogMeta(instanceId));
|
||||
throw new libErrors.RequestError(err.message);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async stopInstance(instanceId) {
|
||||
let instanceConnection = this.instanceConnections.get(instanceId);
|
||||
await libLink.messages.stopInstance.send(instanceConnection, { instance_id: instanceId });
|
||||
await instanceConnection.send(new libData.InstanceStopRequest());
|
||||
}
|
||||
|
||||
async deleteInstanceRequestHandler(message) {
|
||||
let instanceId = message.data.instance_id;
|
||||
async handleInstanceDeleteInternalRequest(request) {
|
||||
let instanceId = request.instanceId;
|
||||
if (this.instanceConnections.has(instanceId)) {
|
||||
throw new libErrors.RequestError(`Instance with ID ${instanceId} is running`);
|
||||
}
|
||||
@@ -713,12 +777,12 @@ class Host extends libLink.Link {
|
||||
let list = [];
|
||||
for (let [instanceId, instanceInfo] of this.discoveredInstanceInfos) {
|
||||
let instanceConnection = this.instanceConnections.get(instanceId);
|
||||
list.push({
|
||||
serialized_config: instanceInfo.config.serialize("controller"),
|
||||
status: instanceConnection ? instanceConnection.status : "stopped",
|
||||
});
|
||||
list.push(new libData.RawInstanceInfo(
|
||||
instanceInfo.config.serialize("controller"),
|
||||
instanceConnection ? instanceConnection.status : "stopped",
|
||||
));
|
||||
}
|
||||
await libLink.messages.updateInstances.send(this, { instances: list });
|
||||
await this.send(new libData.InstancesUpdateRequest(list));
|
||||
|
||||
// Handle configured auto startup instances
|
||||
if (this._startup) {
|
||||
@@ -728,10 +792,7 @@ class Host extends libLink.Link {
|
||||
if (instanceInfo.config.get("instance.auto_start")) {
|
||||
try {
|
||||
let instanceConnection = await this._connectInstance(instanceId);
|
||||
await libLink.messages.startInstance.send(instanceConnection, {
|
||||
instance_id: instanceId,
|
||||
save: null,
|
||||
});
|
||||
await instanceConnection.send(new libData.InstanceStartRequest(null));
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Error during auto startup for ${instanceInfo.config.get("instance.name")}:\n${err.stack}`,
|
||||
@@ -743,13 +804,12 @@ class Host extends libLink.Link {
|
||||
}
|
||||
}
|
||||
|
||||
async prepareDisconnectRequestHandler(message, request) {
|
||||
async prepareDisconnect() {
|
||||
for (let instanceConnection of this.instanceConnections.values()) {
|
||||
await libLink.messages.prepareControllerDisconnect.send(instanceConnection);
|
||||
await instanceConnection.send(new libData.PrepareControllerDisconnectRequest());
|
||||
}
|
||||
this._disconnecting = true;
|
||||
this.connector.setClosing();
|
||||
return await super.prepareDisconnectRequestHandler(message, request);
|
||||
return await super.prepareDisconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -761,8 +821,16 @@ class Host extends libLink.Link {
|
||||
}
|
||||
this._shuttingDown = true;
|
||||
|
||||
for (let instanceId of this.instanceConnections.keys()) {
|
||||
try {
|
||||
await this.stopInstance(instanceId);
|
||||
} catch (err) {
|
||||
logger.error(`Unexpected error stopping instance:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await libLink.messages.prepareDisconnect.send(this);
|
||||
await this.connector.disconnect();
|
||||
} catch (err) {
|
||||
if (!(err instanceof libErrors.SessionLost)) {
|
||||
logger.error(`Unexpected error preparing disconnect:\n${err.stack}`);
|
||||
@@ -770,11 +838,6 @@ class Host extends libLink.Link {
|
||||
}
|
||||
|
||||
try {
|
||||
for (let instanceId of this.instanceConnections.keys()) {
|
||||
await this.stopInstance(instanceId);
|
||||
}
|
||||
await this.connector.close(1000, "Host Shutdown");
|
||||
|
||||
// Clear silly interval in pidfile library.
|
||||
pidusage.clear();
|
||||
} catch (err) {
|
||||
|
||||
@@ -102,8 +102,7 @@ const serverSettingsActions = {
|
||||
*/
|
||||
class Instance extends libLink.Link {
|
||||
constructor(host, connector, dir, factorioDir, instanceConfig) {
|
||||
super("instance", "host", connector);
|
||||
libLink.attachAllMessages(this);
|
||||
super(connector);
|
||||
this._host = host;
|
||||
this._dir = dir;
|
||||
|
||||
@@ -211,6 +210,24 @@ class Instance extends libLink.Link {
|
||||
this._playerCheckInterval = null;
|
||||
this._hadPlayersOnline = false;
|
||||
this._playerAutosaveSlot = 1;
|
||||
|
||||
this.handle(libData.InstanceExtractPlayersRequest, this.handleInstanceExtractPlayersRequest.bind(this));
|
||||
this.handle(libData.InstanceAdminlistUpdateEvent, this.handleInstanceAdminlistUpdateEvent.bind(this));
|
||||
this.handle(libData.InstanceBanlistUpdateEvent, this.handleInstanceBanlistUpdateEvent.bind(this));
|
||||
this.handle(libData.InstanceWhitelistUpdateEvent, this.handleInstanceWhitelistUpdateEvent.bind(this));
|
||||
this.handle(libData.ControllerConnectionEvent, this.handleControllerConnectionEvent.bind(this));
|
||||
this.handle(
|
||||
libData.PrepareControllerDisconnectRequest, this.handlePrepareControllerDisconnectRequest.bind(this)
|
||||
);
|
||||
this.handle(libData.InstanceMetricsRequest, this.handleInstanceMetricsRequest.bind(this));
|
||||
this.handle(libData.InstanceStartRequest, this.handleInstanceStartRequest.bind(this));
|
||||
this.handle(libData.InstanceLoadScenarioRequest, this.handleInstanceLoadScenarioRequest.bind(this));
|
||||
this.handle(libData.InstanceListSavesRequest, this.handleInstanceListSavesRequest.bind(this));
|
||||
this.handle(libData.InstanceCreateSaveRequest, this.handleInstanceCreateSaveRequest.bind(this));
|
||||
this.handle(libData.InstanceExportDataRequest, this.handleInstanceExportDataRequest.bind(this));
|
||||
this.handle(libData.InstanceStopRequest, this.handleInstanceStopRequest.bind(this));
|
||||
this.handle(libData.InstanceKillRequest, this.handleInstanceKillRequest.bind(this));
|
||||
this.handle(libData.InstanceSendRconRequest, this.handleInstanceSendRconRequest.bind(this));
|
||||
}
|
||||
|
||||
_watchPlayerJoinsByChat() {
|
||||
@@ -284,9 +301,9 @@ class Instance extends libLink.Link {
|
||||
instance_id: this.id,
|
||||
type: "join",
|
||||
name,
|
||||
stats: stats.toJSON(),
|
||||
stats,
|
||||
};
|
||||
libLink.messages.playerEvent.send(this, event);
|
||||
this.sendTo("controller", new libData.InstancePlayerUpdateEvent("join", name, undefined, stats));
|
||||
libPlugin.invokeHook(this.plugins, "onPlayerEvent", event);
|
||||
}
|
||||
|
||||
@@ -306,13 +323,13 @@ class Instance extends libLink.Link {
|
||||
type: "leave",
|
||||
name,
|
||||
reason,
|
||||
stats: stats.toJSON(),
|
||||
stats,
|
||||
};
|
||||
libLink.messages.playerEvent.send(this, event);
|
||||
this.sendTo("controller", new libData.InstancePlayerUpdateEvent("leave", name, reason, stats));
|
||||
libPlugin.invokeHook(this.plugins, "onPlayerEvent", event);
|
||||
}
|
||||
|
||||
async extractPlayersRequestHandler() {
|
||||
async handleInstanceExtractPlayersRequest() {
|
||||
const exportPlayerTimes = `/sc
|
||||
local players = {}
|
||||
for _, p in pairs(game.players) do
|
||||
@@ -320,6 +337,7 @@ for _, p in pairs(game.players) do
|
||||
end
|
||||
rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
let playerTimes = JSON.parse(await this.sendRcon(exportPlayerTimes));
|
||||
let count = 0;
|
||||
|
||||
for (let [name, onlineTimeTicks] of Object.entries(playerTimes)) {
|
||||
let stats = this.playerStats.get(name);
|
||||
@@ -335,9 +353,11 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
name,
|
||||
stats: stats.toJSON(),
|
||||
};
|
||||
libLink.messages.playerEvent.send(this, event);
|
||||
this.sendTo("controller", new libData.InstancePlayerUpdateEvent("import", name, undefined, stats));
|
||||
libPlugin.invokeHook(this.plugins, "onPlayerEvent", event);
|
||||
count += 1;
|
||||
}
|
||||
this.logger.info(`Extracted data for ${count} player(s)`);
|
||||
}
|
||||
|
||||
async sendRcon(message, expectEmpty, plugin = "") {
|
||||
@@ -371,24 +391,27 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
type = "special";
|
||||
}
|
||||
|
||||
list.push({
|
||||
name,
|
||||
list.push(new libData.SaveDetails(
|
||||
type,
|
||||
size: stat.size,
|
||||
mtime_ms: stat.mtimeMs,
|
||||
loaded: name === loadedSave,
|
||||
default: name === defaultSave,
|
||||
});
|
||||
name,
|
||||
stat.size,
|
||||
stat.mtimeMs,
|
||||
name === loadedSave,
|
||||
name === defaultSave,
|
||||
));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
async sendSaveListUpdate() {
|
||||
libLink.messages.saveListUpdate.send(this, {
|
||||
instance_id: this.id,
|
||||
list: await Instance.listSaves(this.path("saves"), this._loadedSave),
|
||||
});
|
||||
this.sendTo(
|
||||
"controller",
|
||||
new libData.InstanceSaveListUpdateEvent(
|
||||
this.id,
|
||||
await Instance.listSaves(this.path("saves"), this._loadedSave),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async _autosave(name) {
|
||||
@@ -415,11 +438,14 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
|
||||
notifyStatus(status) {
|
||||
this._status = status;
|
||||
libLink.messages.instanceStatusChanged.send(this, {
|
||||
instance_id: this.id,
|
||||
status,
|
||||
game_port: this.server && this.server.gamePort || this.config.get("factorio.game_port") || null,
|
||||
});
|
||||
this.sendTo(
|
||||
"controller",
|
||||
new libData.InstanceStatusChangedEvent(
|
||||
this.id,
|
||||
status,
|
||||
this.server && this.server.gamePort || this.config.get("factorio.game_port") || null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -467,7 +493,6 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
let instancePlugin = new InstancePluginClass(pluginInfo, this, host);
|
||||
this.plugins.set(pluginInfo.name, instancePlugin);
|
||||
await instancePlugin.init();
|
||||
libPlugin.attachPluginMessages(this, instancePlugin);
|
||||
|
||||
this.logger.info(`Loaded plugin ${pluginInfo.name} in ${Date.now() - pluginLoadStarted}ms`);
|
||||
}
|
||||
@@ -528,7 +553,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
for (let [name, plugin] of this.plugins) {
|
||||
plugins[name] = plugin.info.version;
|
||||
}
|
||||
libLink.messages.instanceInitialized.send(this, { instance_id: this.id, plugins });
|
||||
this.send(new libData.InstanceInitialisedEvent(plugins));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -598,13 +623,13 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
*/
|
||||
async syncMods() {
|
||||
const modPackId = this.config.get("factorio.mod_pack");
|
||||
let response;
|
||||
let modPack;
|
||||
if (modPackId === null) {
|
||||
response = await libLink.messages.getDefaultModPack.send(this);
|
||||
modPack = await this.sendTo("controller", new libData.ModPackGetDefaultRequest());
|
||||
} else {
|
||||
response = await libLink.messages.getModPack.send(this, { id: modPackId });
|
||||
modPack = await this.sendTo("controller", new libData.ModPackGetRequest(modPackId));
|
||||
}
|
||||
this.activeModPack = new libData.ModPack(response.mod_pack);
|
||||
this.activeModPack = modPack;
|
||||
|
||||
// TODO validate factorioVersion
|
||||
|
||||
@@ -698,21 +723,21 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
*
|
||||
* Creates a new save if no save is passed and patches it with modules.
|
||||
*
|
||||
* @param {string|null} saveName -
|
||||
* @param {string|undefined} saveName -
|
||||
* Save to prepare from the instance saves directory. Creates a new
|
||||
* save if null.
|
||||
* @returns {Promise<string>} Name of the save prepared.
|
||||
*/
|
||||
async prepareSave(saveName) {
|
||||
// Use latest save if no save was specified
|
||||
if (saveName === null) {
|
||||
if (saveName === undefined) {
|
||||
saveName = await libFileOps.getNewestFile(
|
||||
this.path("saves"), (name) => !name.endsWith(".tmp.zip")
|
||||
);
|
||||
}
|
||||
|
||||
// Create save if no save was found.
|
||||
if (saveName === null) {
|
||||
if (saveName === undefined) {
|
||||
this.logger.info("Creating new save");
|
||||
await this.server.create("world.zip");
|
||||
saveName = "world.zip";
|
||||
@@ -907,37 +932,36 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
}
|
||||
}
|
||||
|
||||
async adminlistUpdateEventHandler(message) {
|
||||
async handleInstanceAdminlistUpdateEvent(request) {
|
||||
if (!this.config.get("factorio.sync_adminlist")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { name, admin } = message.data;
|
||||
let { name, admin } = request;
|
||||
let command = admin ? `/promote ${name}` : `/demote ${name}`;
|
||||
await this.sendRcon(command);
|
||||
}
|
||||
|
||||
async banlistUpdateEventHandler(message) {
|
||||
async handleInstanceBanlistUpdateEvent(request) {
|
||||
if (!this.config.get("factorio.sync_banlist")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { name, banned, reason } = message.data;
|
||||
let { name, banned, reason } = request;
|
||||
let command = banned ? `/ban ${name} ${reason}` : `/unban ${name}`;
|
||||
await this.sendRcon(command);
|
||||
}
|
||||
|
||||
async whitelistUpdateEventHandler(message) {
|
||||
async handleInstanceWhitelistUpdateEvent(request) {
|
||||
if (!this.config.get("factorio.sync_whitelist")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { name, whitelisted } = message.data;
|
||||
let { name, whitelisted } = request;
|
||||
let command = whitelisted ? `/whitelist add ${name}` : `/whiteliste remove ${name}`;
|
||||
await this.sendRcon(command);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Stop the instance
|
||||
*/
|
||||
@@ -962,15 +986,15 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
await this.server.kill(true);
|
||||
}
|
||||
|
||||
async controllerConnectionEventEventHandler(message) {
|
||||
await libPlugin.invokeHook(this.plugins, "onControllerConnectionEvent", message.data.event);
|
||||
async handleControllerConnectionEvent(event) {
|
||||
await libPlugin.invokeHook(this.plugins, "onControllerConnectionEvent", event.event);
|
||||
}
|
||||
|
||||
async prepareControllerDisconnectRequestHandler() {
|
||||
async handlePrepareControllerDisconnectRequest() {
|
||||
await libPlugin.invokeHook(this.plugins, "onPrepareControllerDisconnect");
|
||||
}
|
||||
|
||||
async getMetricsRequestHandler() {
|
||||
async handleInstanceMetricsRequest() {
|
||||
let results = [];
|
||||
if (!["stopped", "stopping"].includes(this._status)) {
|
||||
let pluginResults = await libPlugin.invokeHook(this.plugins, "onMetrics");
|
||||
@@ -988,11 +1012,11 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
instanceFactorioMemoryUsage.labels(String(this.id)).set(stats.memory);
|
||||
}
|
||||
|
||||
return { results };
|
||||
return new libData.InstanceMetricsRequest.Response(results);
|
||||
}
|
||||
|
||||
async startInstanceRequestHandler(message) {
|
||||
let saveName = message.data.save;
|
||||
async handleInstanceStartRequest(request) {
|
||||
let saveName = request.save;
|
||||
try {
|
||||
await this.prepare();
|
||||
saveName = await this.prepareSave(saveName);
|
||||
@@ -1010,7 +1034,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
}
|
||||
}
|
||||
|
||||
async loadScenarioRequestHandler(message) {
|
||||
async handleInstanceLoadScenarioRequest(request) {
|
||||
if (this.config.get("factorio.enable_save_patching")) {
|
||||
this.notifyExit();
|
||||
throw new libErrors.RequestError("Load scenario cannot be used with save patching enabled");
|
||||
@@ -1024,22 +1048,20 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
throw err;
|
||||
}
|
||||
|
||||
let { scenario, seed, map_gen_settings, map_settings } = message.data;
|
||||
let { scenario, seed, mapGenSettings, mapSettings } = request;
|
||||
try {
|
||||
await this.startScenario(scenario, seed, map_gen_settings, map_settings);
|
||||
await this.startScenario(scenario, seed, mapGenSettings, mapSettings);
|
||||
} catch (err) {
|
||||
await this.stop();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async listSavesRequestHandler(message) {
|
||||
return {
|
||||
list: await Instance.listSaves(this.path("saves"), this._loadedSave),
|
||||
};
|
||||
async handleInstanceListSavesRequest() {
|
||||
return await Instance.listSaves(this.path("saves"), this._loadedSave);
|
||||
}
|
||||
|
||||
async createSaveRequestHandler(message) {
|
||||
async handleInstanceCreateSaveRequest(request) {
|
||||
this.notifyStatus("creating_save");
|
||||
try {
|
||||
this.logger.verbose("Writing server-settings.json");
|
||||
@@ -1055,13 +1077,13 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
}
|
||||
|
||||
this.server.on("exit", () => this.notifyExit());
|
||||
let { name, seed, map_gen_settings, map_settings } = message.data;
|
||||
await this.server.create(name, seed, map_gen_settings, map_settings);
|
||||
let { name, seed, mapGenSettings, mapSettings } = request;
|
||||
await this.server.create(name, seed, mapGenSettings, mapSettings);
|
||||
await this.sendSaveListUpdate();
|
||||
this.logger.info("Successfully created save");
|
||||
}
|
||||
|
||||
async exportDataRequestHandler() {
|
||||
async handleInstanceExportDataRequest() {
|
||||
this.notifyStatus("exporting_data");
|
||||
try {
|
||||
this.logger.verbose("Writing server-settings.json");
|
||||
@@ -1093,17 +1115,16 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
}
|
||||
}
|
||||
|
||||
async stopInstanceRequestHandler() {
|
||||
async handleInstanceStopRequest() {
|
||||
await this.stop();
|
||||
}
|
||||
|
||||
async killInstanceRequestHandler() {
|
||||
async handleInstanceKillRequest() {
|
||||
await this.kill();
|
||||
}
|
||||
|
||||
async sendRconRequestHandler(message) {
|
||||
let result = await this.sendRcon(message.data.command);
|
||||
return { result };
|
||||
async handleInstanceSendRconRequest(request) {
|
||||
return await this.sendRcon(request.command);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use strict";
|
||||
const libData = require("@clusterio/lib/data");
|
||||
const libLink = require("@clusterio/lib/link");
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
@@ -6,57 +7,15 @@ const libErrors = require("@clusterio/lib/errors");
|
||||
|
||||
class InstanceConnection extends libLink.Link {
|
||||
constructor(connector, host, instanceId) {
|
||||
super("host", "instance", connector);
|
||||
super(connector);
|
||||
this.host = host;
|
||||
this.router = this.host.router;
|
||||
this.instanceId = instanceId;
|
||||
this.plugins = new Map();
|
||||
this.status = "stopped";
|
||||
libLink.attachAllMessages(this);
|
||||
|
||||
for (let pluginInfo of host.pluginInfos) {
|
||||
libPlugin.attachPluginMessages(this, { info: pluginInfo });
|
||||
}
|
||||
}
|
||||
|
||||
async forwardRequestToController(message, request) {
|
||||
return await request.send(this.host, message.data);
|
||||
}
|
||||
|
||||
async forwardRequestToInstance(message, request) {
|
||||
let instanceId = message.data.instance_id;
|
||||
let instanceConnection = this.host.instanceConnections.get(instanceId);
|
||||
if (!instanceConnection) {
|
||||
// Instance is probably on another host
|
||||
return await this.forwardRequestToController(message, request);
|
||||
}
|
||||
|
||||
if (request.plugin && !instanceConnection.plugins.has(request.plugin)) {
|
||||
throw new libErrors.RequestError(`Instance ID ${instanceId} does not have ${request.plugin} plugin loaded`);
|
||||
}
|
||||
|
||||
return await request.send(instanceConnection, message.data);
|
||||
}
|
||||
|
||||
|
||||
async forwardEventToInstance(message, event) {
|
||||
let instanceId = message.data.instance_id;
|
||||
let instanceConnection = this.host.instanceConnections.get(instanceId);
|
||||
if (!instanceConnection) {
|
||||
// Instance is probably on another host
|
||||
await this.forwardEventToController(message, event);
|
||||
return;
|
||||
}
|
||||
if (event.plugin && !instanceConnection.plugins.has(event.plugin)) { return; }
|
||||
|
||||
event.send(instanceConnection, message.data);
|
||||
}
|
||||
|
||||
async forwardEventToController(message, event) {
|
||||
if (!this.host.connector.hasSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.send(this.host, message.data);
|
||||
this.handle(libData.InstanceInitialisedEvent, this.handleInstanceInitialisedEvent.bind(this));
|
||||
this.snoopEvent(libData.InstanceStatusChangedEvent, this.snoopInstanceStatusChangedEvent.bind(this));
|
||||
}
|
||||
|
||||
async broadcastEventToInstance(message, event) {
|
||||
@@ -69,16 +28,15 @@ class InstanceConnection extends libLink.Link {
|
||||
}
|
||||
}
|
||||
|
||||
async instanceInitializedEventHandler(message, event) {
|
||||
this.plugins = new Map(Object.entries(message.data.plugins));
|
||||
async handleInstanceInitialisedEvent(event) {
|
||||
this.plugins = new Map(Object.entries(event.plugins));
|
||||
}
|
||||
|
||||
async instanceStatusChangedEventHandler(message, event) {
|
||||
this.status = message.data.status;
|
||||
async snoopInstanceStatusChangedEvent(event) {
|
||||
this.status = event.status;
|
||||
if (this.status === "stopped") {
|
||||
this.host.instanceConnections.delete(this.instanceId);
|
||||
}
|
||||
this.forwardEventToController(message, event);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,10 @@ class PlayerStats {
|
||||
}
|
||||
}
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
let json = {};
|
||||
if (this.joinCount) {
|
||||
|
||||
+12
-11
@@ -7,6 +7,7 @@
|
||||
* @module lib/command
|
||||
*/
|
||||
"use strict";
|
||||
const libData = require("./data");
|
||||
const libLink = require("./link");
|
||||
const libErrors = require("./errors");
|
||||
|
||||
@@ -150,8 +151,8 @@ async function resolveHost(client, hostName) {
|
||||
if (/^-?\d+$/.test(hostName)) {
|
||||
hostId = parseInt(hostName, 10);
|
||||
} else {
|
||||
let response = await libLink.messages.listHosts.send(client);
|
||||
for (let host of response.list) {
|
||||
let hosts = await client.sendTo("controller", new libData.HostListRequest());
|
||||
for (let host of hosts) {
|
||||
if (host.name === hostName) {
|
||||
hostId = host.id;
|
||||
break;
|
||||
@@ -182,8 +183,8 @@ async function resolveInstance(client, instanceName) {
|
||||
if (/^-?\d+$/.test(instanceName)) {
|
||||
instanceId = parseInt(instanceName, 10);
|
||||
} else {
|
||||
let response = await libLink.messages.listInstances.send(client);
|
||||
for (let instance of response.list) {
|
||||
let instances = await client.sendTo("controller", new libData.InstanceDetailsListRequest());
|
||||
for (let instance of instances) {
|
||||
if (instance.name === instanceName) {
|
||||
instanceId = instance.id;
|
||||
break;
|
||||
@@ -215,10 +216,10 @@ async function resolveModPack(client, modPackName) {
|
||||
if (/^-?\d+/.test(modPackName)) {
|
||||
modPackId = parseInt(modPackName, 10);
|
||||
} else {
|
||||
let response = await libLink.messages.listModPacks.send(client);
|
||||
for (let mod_pack of response.list) {
|
||||
if (mod_pack["name"] === modPackName) {
|
||||
modPackId = mod_pack["id"];
|
||||
let modPacks = await client.sendTo("controller", new libData.ModPackListRequest());
|
||||
for (let modPack of modPacks) {
|
||||
if (modPack.name === modPackName) {
|
||||
modPackId = modPack.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -243,12 +244,12 @@ async function resolveModPack(client, modPackName) {
|
||||
* @static
|
||||
*/
|
||||
async function retrieveRole(client, roleName) {
|
||||
let response = await libLink.messages.listRoles.send(client);
|
||||
let roles = await client.sendTo("controller", new libData.RoleListRequest());
|
||||
|
||||
let resolvedRole;
|
||||
if (/^-?\d+$/.test(roleName)) {
|
||||
let roleId = parseInt(roleName, 10);
|
||||
for (let role of response.list) {
|
||||
for (let role of roles) {
|
||||
if (role.id === roleId) {
|
||||
resolvedRole = role;
|
||||
break;
|
||||
@@ -256,7 +257,7 @@ async function retrieveRole(client, roleName) {
|
||||
}
|
||||
|
||||
} else {
|
||||
for (let role of response.list) {
|
||||
for (let role of roles) {
|
||||
if (role.name === roleName) {
|
||||
resolvedRole = role;
|
||||
break;
|
||||
|
||||
@@ -22,6 +22,10 @@ class ExportManifest {
|
||||
*/
|
||||
assets = {};
|
||||
|
||||
constructor(assets) {
|
||||
if (assets) { this.assets = assets; }
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
@@ -34,12 +38,8 @@ class ExportManifest {
|
||||
},
|
||||
};
|
||||
|
||||
constructor(json = {}) {
|
||||
if (json.assets) { this.assets = json.assets; }
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return { assets: this.assets };
|
||||
static fromJSON(json) {
|
||||
return new this(json.assets);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -155,22 +155,26 @@ class ModInfo {
|
||||
|
||||
static validate = libSchema.compile(this.jsonSchema);
|
||||
|
||||
constructor(json = {}) {
|
||||
static fromJSON(json) {
|
||||
const modInfo = new this();
|
||||
|
||||
// info.json fields
|
||||
if (json.name) { this.name = json.name; }
|
||||
if (json.version) { this.version = json.version; }
|
||||
if (json.title) { this.title = json.title; }
|
||||
if (json.author) { this.author = json.author; }
|
||||
if (json.contact) { this.contact = json.contact; }
|
||||
if (json.homepage) { this.homepage = json.homepage; }
|
||||
if (json.description) { this.description = json.description; }
|
||||
if (json.factorio_version) { this.factorioVersion = json.factorio_version; }
|
||||
if (json.dependencies) { this.dependencies = json.dependencies; }
|
||||
if (json.name) { modInfo.name = json.name; }
|
||||
if (json.version) { modInfo.version = json.version; }
|
||||
if (json.title) { modInfo.title = json.title; }
|
||||
if (json.author) { modInfo.author = json.author; }
|
||||
if (json.contact) { modInfo.contact = json.contact; }
|
||||
if (json.homepage) { modInfo.homepage = json.homepage; }
|
||||
if (json.description) { modInfo.description = json.description; }
|
||||
if (json.factorio_version) { modInfo.factorioVersion = json.factorio_version; }
|
||||
if (json.dependencies) { modInfo.dependencies = json.dependencies; }
|
||||
|
||||
// Additional data
|
||||
if (json.size) { this.size = json.size; }
|
||||
if (json.sha1) { this.sha1 = json.sha1; }
|
||||
if (json.is_deleted) { this.isDeleted = json.is_deleted; }
|
||||
if (json.size) { modInfo.size = json.size; }
|
||||
if (json.sha1) { modInfo.sha1 = json.sha1; }
|
||||
if (json.is_deleted) { modInfo.isDeleted = json.is_deleted; }
|
||||
|
||||
return modInfo;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
@@ -218,7 +222,7 @@ class ModInfo {
|
||||
throw new Error(`Mod's version (${modInfo.version}) is invalid`);
|
||||
}
|
||||
|
||||
return new this({
|
||||
return this.fromJSON({
|
||||
...modInfo,
|
||||
|
||||
size: (await fs.stat(modPath)).size,
|
||||
|
||||
@@ -112,6 +112,19 @@ class ModPack {
|
||||
*/
|
||||
isDeleted = false;
|
||||
|
||||
shallowClone() {
|
||||
const clone = new this.constructor();
|
||||
clone.id = this.id;
|
||||
clone.name = this.name;
|
||||
clone.description = this.description;
|
||||
clone.factorioVersion = this.factorioVersion;
|
||||
clone.mods = this.mods;
|
||||
clone.settings = this.settings;
|
||||
clone.exportManifest = this.exportManifest;
|
||||
clone.isDeleted = this.isDeleted;
|
||||
return clone;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
@@ -142,29 +155,33 @@ class ModPack {
|
||||
|
||||
static validate = libSchema.compile(this.jsonSchema);
|
||||
|
||||
constructor(json = {}) {
|
||||
static fromJSON(json) {
|
||||
const modPack = new this();
|
||||
|
||||
if (json.id) {
|
||||
this.id = json.id;
|
||||
modPack.id = json.id;
|
||||
} else {
|
||||
this.id = Math.random() * 2**31 | 0;
|
||||
modPack.id = Math.random() * 2**31 | 0;
|
||||
}
|
||||
if (json.name) { this.name = json.name; }
|
||||
if (json.description) { this.description = json.description; }
|
||||
if (json.factorio_version) { this.factorioVersion = json.factorio_version; }
|
||||
if (json.mods) { this.mods = new Map(json.mods.map(m => [m.name, m])); }
|
||||
if (json.name) { modPack.name = json.name; }
|
||||
if (json.description) { modPack.description = json.description; }
|
||||
if (json.factorio_version) { modPack.factorioVersion = json.factorio_version; }
|
||||
if (json.mods) { modPack.mods = new Map(json.mods.map(m => [m.name, m])); }
|
||||
if (json.settings) {
|
||||
this.settings = {
|
||||
modPack.settings = {
|
||||
"startup": new Map(Object.entries(json.settings["startup"])),
|
||||
"runtime-global": new Map(Object.entries(json.settings["runtime-global"])),
|
||||
"runtime-per-user": new Map(Object.entries(json.settings["runtime-per-user"])),
|
||||
};
|
||||
}
|
||||
if (json.export_manifest) { this.exportManifest = new ExportManifest(json.export_manifest); }
|
||||
if (json.is_deleted) { this.isDeleted = json.is_deleted; }
|
||||
if (json.export_manifest) { modPack.exportManifest = ExportManifest.fromJSON(json.export_manifest); }
|
||||
if (json.is_deleted) { modPack.isDeleted = json.is_deleted; }
|
||||
|
||||
if (!this.mods.has("base")) {
|
||||
this.mods.set("base", { name: "base", enabled: true, version: this.factorioVersion });
|
||||
if (!modPack.mods.has("base")) {
|
||||
modPack.mods.set("base", { name: "base", enabled: true, version: modPack.factorioVersion });
|
||||
}
|
||||
|
||||
return modPack;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
@@ -180,7 +197,7 @@ class ModPack {
|
||||
"runtime-per-user": Object.fromEntries(this.settings["runtime-per-user"]),
|
||||
},
|
||||
};
|
||||
if (this.exportManifest) { json.export_manifest = this.exportManifest.toJSON(); }
|
||||
if (this.exportManifest) { json.export_manifest = this.exportManifest; }
|
||||
if (this.isDeleted) { json.is_deleted = this.isDeleted; }
|
||||
return json;
|
||||
}
|
||||
@@ -217,7 +234,7 @@ class ModPack {
|
||||
throw new Error("Malformed mod pack string: Schema validation failed");
|
||||
}
|
||||
|
||||
return new this(json);
|
||||
return this.fromJSON(json);
|
||||
}
|
||||
|
||||
toModSettingsDat() {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"use strict";
|
||||
|
||||
function jsonArray(ItemClass) {
|
||||
return {
|
||||
jsonSchema: {
|
||||
type: "array",
|
||||
items: ItemClass.jsonSchema,
|
||||
},
|
||||
|
||||
fromJSON(json) {
|
||||
return json.map(i => ItemClass.fromJSON(i));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function jsonPrimitive(type) {
|
||||
return {
|
||||
jsonSchema: { type },
|
||||
fromJSON(json) {
|
||||
return json;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
JsonBoolean: jsonPrimitive("boolean"),
|
||||
JsonNumber: jsonPrimitive("number"),
|
||||
JsonString: jsonPrimitive("string"),
|
||||
|
||||
jsonArray,
|
||||
jsonPrimitive,
|
||||
};
|
||||
@@ -7,11 +7,25 @@
|
||||
const ExportManifest = require("./ExportManifest");
|
||||
const ModInfo = require("./ModInfo");
|
||||
const ModPack = require("./ModPack");
|
||||
const composites = require("./composites");
|
||||
const messagesCore = require("./messages_core");
|
||||
const messagesController = require("./messages_controller");
|
||||
const messagesHost = require("./messages_host");
|
||||
const messagesInstance = require("./messages_instance");
|
||||
const messagesMod = require("./messages_mod");
|
||||
const messagesUser = require("./messages_user");
|
||||
const version = require("./version");
|
||||
|
||||
module.exports = {
|
||||
ExportManifest,
|
||||
ModInfo,
|
||||
ModPack,
|
||||
...composites,
|
||||
...messagesCore,
|
||||
...messagesController,
|
||||
...messagesHost,
|
||||
...messagesInstance,
|
||||
...messagesMod,
|
||||
...messagesUser,
|
||||
...version,
|
||||
};
|
||||
@@ -0,0 +1,384 @@
|
||||
"use strict";
|
||||
const { JsonString } = require("./composites");
|
||||
|
||||
// TODO: remove after config refactor
|
||||
/** @memberof module:lib/data */
|
||||
class RawConfig {
|
||||
/** @type {object} */
|
||||
serializedConfig;
|
||||
|
||||
constructor(serializedConfig) {
|
||||
this.serializedConfig = serializedConfig;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["serializedConfig"],
|
||||
properties: {
|
||||
"serializedConfig": { type: "object" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.serializedConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class ControllerConfigGetRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.controller.get_config";
|
||||
static Response = RawConfig;
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class ControllerConfigSetFieldRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.controller.update_config";
|
||||
|
||||
/** @type {string} */
|
||||
field;
|
||||
/** @type {string} */
|
||||
value;
|
||||
|
||||
constructor(field, value) {
|
||||
this.field = field;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["field", "value"],
|
||||
properties: {
|
||||
"field": { type: "string" },
|
||||
"value": { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.field, json.value);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class ControllerConfigSetPropRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.controller.update_config";
|
||||
|
||||
/** @type {string} */
|
||||
field;
|
||||
/** @type {string} */
|
||||
prop;
|
||||
/** @type {undefined} */
|
||||
value;
|
||||
|
||||
constructor(field, prop, value) {
|
||||
this.field = field;
|
||||
this.prop = prop;
|
||||
if (value) { this.value = value; }
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["field", "prop"],
|
||||
properties: {
|
||||
field: { type: "string" },
|
||||
prop: { type: "string" },
|
||||
value: {},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.field, json.prop, json.value);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class HostGenerateTokenRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.host.generate_token";
|
||||
|
||||
/** @type {?number} */
|
||||
hostId;
|
||||
|
||||
constructor(hostId) {
|
||||
this.hostId = hostId;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["hostId"],
|
||||
properties: {
|
||||
hostId: { type: ["integer", "null"] },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.hostId);
|
||||
}
|
||||
|
||||
static Response = JsonString;
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class HostConfigCreateRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.host.create_config";
|
||||
|
||||
/** @type {?number} */
|
||||
id;
|
||||
/** @type {?string} */
|
||||
name;
|
||||
/** @type {boolean} */
|
||||
generateToken;
|
||||
|
||||
constructor(id, name, generateToken) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.generateToken = generateToken;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["id", "name", "generateToken"],
|
||||
properties: {
|
||||
"id": { type: ["integer", "null"] },
|
||||
"name": { type: ["string", "null"] },
|
||||
"generateToken": { type: "boolean" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.id, json.name, json.generateToken);
|
||||
}
|
||||
|
||||
static Response = RawConfig;
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class LogSetSubscriptionsRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.log.follow";
|
||||
|
||||
/** @type {boolean} */
|
||||
all;
|
||||
/** @type {boolean} */
|
||||
controller;
|
||||
/** @type {array} */
|
||||
hostIds;
|
||||
/** @type {array} */
|
||||
instanceIds;
|
||||
/** @type {?string} */
|
||||
maxLevel;
|
||||
|
||||
constructor(all, controller, hostIds, instanceIds, maxLevel) {
|
||||
this.all = all;
|
||||
this.controller = controller;
|
||||
this.hostIds = hostIds;
|
||||
this.instanceIds = instanceIds;
|
||||
this.maxLevel = maxLevel;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["all", "controller", "hostIds", "instanceIds", "maxLevel"],
|
||||
properties: {
|
||||
all: { type: "boolean" },
|
||||
controller: { type: "boolean" },
|
||||
hostIds: {
|
||||
type: "array",
|
||||
items: { type: "integer" },
|
||||
},
|
||||
instanceIds: {
|
||||
type: "array",
|
||||
items: { type: "integer" },
|
||||
},
|
||||
maxLevel: { type: ["string", "null"] },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.all, json.controller, json.hostIds, json.instanceIds, json.maxLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class LogQueryRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.log.query";
|
||||
|
||||
/** @type {boolean} */
|
||||
all;
|
||||
/** @type {boolean} */
|
||||
controller;
|
||||
/** @type {Array<number>} */
|
||||
hostIds;
|
||||
/** @type {Array<number>} */
|
||||
instanceIds;
|
||||
/** @type {?string} */
|
||||
maxLevel;
|
||||
/** @type {integer} */
|
||||
limit;
|
||||
/** @type {undefined} */
|
||||
order;
|
||||
|
||||
constructor(all, controller, hostIds, instanceIds, maxLevel, limit, order) {
|
||||
this.all = all;
|
||||
this.controller = controller;
|
||||
this.hostIds = hostIds;
|
||||
this.instanceIds = instanceIds;
|
||||
this.maxLevel = maxLevel;
|
||||
this.limit = limit;
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["all", "controller", "hostIds", "instanceIds", "maxLevel", "limit", "order"],
|
||||
properties: {
|
||||
all: { type: "boolean" },
|
||||
controller: { type: "boolean" },
|
||||
hostIds: {
|
||||
type: "array",
|
||||
items: { type: "integer" },
|
||||
},
|
||||
instanceIds: {
|
||||
type: "array",
|
||||
items: { type: "integer" },
|
||||
},
|
||||
maxLevel: { type: ["string", "null"] },
|
||||
limit: { type: "integer" },
|
||||
order: {
|
||||
enum: ["asc", "desc"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(
|
||||
json.all, json.controller, json.hostIds, json.instanceIds, json.maxLevel, json.limit, json.order
|
||||
);
|
||||
}
|
||||
}
|
||||
LogQueryRequest.Response = class Response {
|
||||
/** @type {Array<object>} */
|
||||
log;
|
||||
|
||||
constructor(log) {
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["log"],
|
||||
properties: {
|
||||
log: {
|
||||
type: "array",
|
||||
items: { type: "object" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.log);
|
||||
}
|
||||
};
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class LogMessageEvent {
|
||||
static type = "event";
|
||||
static src = ["host", "controller"];
|
||||
static dst = ["controller", "control"];
|
||||
|
||||
/** @type {object} */
|
||||
info;
|
||||
|
||||
constructor(info) {
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["info"],
|
||||
properties: {
|
||||
"info": {
|
||||
type: "object",
|
||||
required: ["level", "message"],
|
||||
properties: {
|
||||
"level": { type: "string" },
|
||||
"message": { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.info);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class DebugDumpWsRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.debug.dump_ws";
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class DebugWsMessageEvent {
|
||||
static type = "event";
|
||||
static src = "controller";
|
||||
static dst = "control";
|
||||
|
||||
/** @type {string} */
|
||||
direction;
|
||||
/** @type {string} */
|
||||
content;
|
||||
|
||||
constructor(direction, content) {
|
||||
this.direction = direction;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["direction", "content"],
|
||||
properties: {
|
||||
direction: { type: "string" },
|
||||
content: { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.direction, json.content);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RawConfig,
|
||||
ControllerConfigGetRequest,
|
||||
ControllerConfigSetFieldRequest,
|
||||
ControllerConfigSetPropRequest,
|
||||
HostGenerateTokenRequest,
|
||||
HostConfigCreateRequest,
|
||||
LogSetSubscriptionsRequest,
|
||||
LogQueryRequest,
|
||||
LogMessageEvent,
|
||||
DebugDumpWsRequest,
|
||||
DebugWsMessageEvent,
|
||||
};
|
||||
@@ -0,0 +1,888 @@
|
||||
"use strict";
|
||||
const libSchema = require("../schema");
|
||||
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class Address {
|
||||
/** Controller address */
|
||||
static controller = 0;
|
||||
/** Instance address */
|
||||
static instance = 1;
|
||||
/** Host address */
|
||||
static host = 2;
|
||||
/** Control address */
|
||||
static control = 3;
|
||||
/** Broadcast */
|
||||
static broadcast = 4;
|
||||
|
||||
/** @type {number} */
|
||||
type;
|
||||
|
||||
/** @type {string} */
|
||||
id;
|
||||
|
||||
/** @type {string|undefined} */
|
||||
requestId;
|
||||
|
||||
constructor(type, id, requestId) {
|
||||
if (typeof type !== "number") {
|
||||
throw Error(`Invalid type ${type}`);
|
||||
}
|
||||
this.type = type;
|
||||
this.id = id;
|
||||
this.requestId = requestId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert from convenient shorthand notation to address.
|
||||
*
|
||||
* Supported notations and their corresponding address:
|
||||
* - `"controller"`: new Address(Address.controller, 0)
|
||||
* - `{ hostId: id }`: new Address(Address.host, id)
|
||||
* - `{ instanceId: id }`: new Address(Address.instance, id)
|
||||
* - `{ controlId: id }`: new Address(Address.control, id)
|
||||
* `"allHosts"`: new Address(Address.broadcast, Address.host)
|
||||
* `"allInstances"`: new Address(Address.broadcast, Address.instance)
|
||||
* `"allControls"`: new Address(Address.broadcast, Address.control)
|
||||
* @param {module:lib/data.Address|string|object} shorthand -
|
||||
* Shorthand to translate with.
|
||||
* @returns {module:lib/data.Address} Translated address.
|
||||
*/
|
||||
static fromShorthand(shorthand) {
|
||||
if (shorthand instanceof Address) { return shorthand; }
|
||||
if (shorthand === "controller") { return new Address(Address.controller, 0); }
|
||||
if (shorthand === "allHosts") { return new Address(Address.broadcast, Address.host); }
|
||||
if (shorthand === "allInstances") { return new Address(Address.broadcast, Address.instance); }
|
||||
if (shorthand === "allControls") { return new Address(Address.broadcast, Address.control); }
|
||||
let entries = Object.entries(shorthand);
|
||||
if (entries.length === 1) {
|
||||
let [name, value] = entries[0];
|
||||
if (name === "hostId") { return new Address(Address.host, value); }
|
||||
if (name === "instanceId") { return new Address(Address.instance, value); }
|
||||
if (name === "controlId") { return new Address(Address.control, value); }
|
||||
}
|
||||
throw Error("Unrecognized shorthand");
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "array",
|
||||
minItems: 2,
|
||||
maxItems: 3,
|
||||
items: [
|
||||
{ type: "integer", minimum: 0, maximum: 4 },
|
||||
{ type: "integer" },
|
||||
{ type: "integer" },
|
||||
],
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json[0], json[1], json[2]);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
if (this.requestId !== undefined) {
|
||||
return [this.type, this.id, this.requestId];
|
||||
}
|
||||
return [this.type, this.id];
|
||||
}
|
||||
|
||||
toString() {
|
||||
const typeMap = ["controller", "instance", "host", "control"];
|
||||
const type = typeMap[this.type] || this.type;
|
||||
if (this.type === Address.broadcast) {
|
||||
return `[Address ${type}:${typeMap[this.id] || this.id}]`;
|
||||
}
|
||||
if (this.requestId === undefined) {
|
||||
return `[Address ${type}:${this.id}]`;
|
||||
}
|
||||
return `[Address ${type}:${this.id}:${this.requestId}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string suitable for indexing the address in a Map
|
||||
* @returns {string} unique string for this address
|
||||
*/
|
||||
index() {
|
||||
return `${this.type}:${this.id}:${this.requestId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this address targets the given destination
|
||||
* @param {module:lib/Data.Address} dst - Destination to check
|
||||
* @returns {boolean} true if this address is addressed to the given destination
|
||||
*/
|
||||
addressedTo(dst) {
|
||||
if (this.type !== Address.broadcast) {
|
||||
return (
|
||||
this.type === dst.type
|
||||
&& this.id === dst.id
|
||||
);
|
||||
}
|
||||
return dst.type !== Address.broadcast && this.id === dst.type;
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
return (
|
||||
this.type === other.type
|
||||
&& this.id === other.id
|
||||
&& this.requestId === other.requestId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class Message {
|
||||
/** @type {string} */
|
||||
type;
|
||||
|
||||
constructor(type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
static jsonSchema;
|
||||
static validate;
|
||||
|
||||
static fromJSON(json) {
|
||||
/* eslint-disable no-use-before-define */
|
||||
if (json.type === "hello") { return MessageHello.fromJSON(json); }
|
||||
if (json.type === "registerHost") { return MessageRegisterHost.fromJSON(json); }
|
||||
if (json.type === "registerControl") { return MessageRegisterControl.fromJSON(json); }
|
||||
if (json.type === "ready") { return MessageReady.fromJSON(json); }
|
||||
if (json.type === "resume") { return MessageResume.fromJSON(json); }
|
||||
if (json.type === "continue") { return MessageContinue.fromJSON(json); }
|
||||
if (json.type === "invalidate") { return MessageInvalidate.fromJSON(json); }
|
||||
if (json.type === "heartbeat") { return MessageHeartbeat.fromJSON(json); }
|
||||
if (json.type === "request") { return MessageRequest.fromJSON(json); }
|
||||
if (json.type === "response") { return MessageResponse.fromJSON(json); }
|
||||
if (json.type === "responseError") { return MessageResponseError.fromJSON(json); }
|
||||
if (json.type === "event") { return MessageEvent.fromJSON(json); }
|
||||
if (json.type === "disconnect") { return MessageDisconnect.fromJSON(json); }
|
||||
/* eslint-enable no-use-before-define */
|
||||
throw new Error(`Unrecognized message type ${json.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class HelloData {
|
||||
/** @type {string} */
|
||||
version;
|
||||
/** @type {object} */
|
||||
plugins;
|
||||
|
||||
constructor(version, plugins) {
|
||||
this.version = version;
|
||||
this.plugins = plugins;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
required: ["version", "plugins"],
|
||||
properties: {
|
||||
"version": { type: "string" },
|
||||
"plugins": {
|
||||
type: "object",
|
||||
additionalProperties: { type: "string" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.version, json.plugins);
|
||||
}
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class MessageHello extends Message {
|
||||
/** @type {module:lib/data.HelloData} */
|
||||
data;
|
||||
|
||||
constructor(data) {
|
||||
super("hello");
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["type", "data"],
|
||||
properties: {
|
||||
"type": { const: "hello" },
|
||||
"data": HelloData.jsonSchema,
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(HelloData.fromJSON(json.data));
|
||||
}
|
||||
};
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class RegisterHostData {
|
||||
/** @type {string} */
|
||||
token;
|
||||
/** @type {string} */
|
||||
agent;
|
||||
/** @type {string} */
|
||||
version;
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {string} */
|
||||
id;
|
||||
/** @type {string|undefined} */
|
||||
publicAddress;
|
||||
/** @type {Object<string, string>} */
|
||||
plugins;
|
||||
|
||||
constructor(token, agent, version, name, id, publicAddress, plugins) {
|
||||
this.token = token;
|
||||
this.agent = agent;
|
||||
this.version = version;
|
||||
this.name = name;
|
||||
this.id = id;
|
||||
this.publicAddress = publicAddress;
|
||||
this.plugins = plugins;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["token", "agent", "version", "name", "id", "plugins"],
|
||||
properties: {
|
||||
"token": { type: "string" },
|
||||
"agent": { type: "string" },
|
||||
"version": { type: "string" },
|
||||
"name": { type: "string" },
|
||||
"id": { type: "integer" },
|
||||
"publicAddress": { type: "string" },
|
||||
"plugins": {
|
||||
type: "object",
|
||||
additionalProperties: { type: "string" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.token, json.agent, json.version, json.name, json.id, json.publicAddress, json.plugins);
|
||||
}
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class MessageRegisterHost extends Message {
|
||||
/** @type {module:lib/data.RegisterHostData} */
|
||||
data;
|
||||
|
||||
constructor(data) {
|
||||
super("registerHost");
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["type", "data"],
|
||||
properties: {
|
||||
"type": { const: "registerHost" },
|
||||
"data": RegisterHostData.jsonSchema,
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(RegisterHostData.fromJSON(json.data));
|
||||
}
|
||||
};
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class RegisterControlData {
|
||||
/** @type {string} */
|
||||
token;
|
||||
/** @type {string} */
|
||||
agent;
|
||||
/** @type {string} */
|
||||
version;
|
||||
|
||||
constructor(token, agent, version) {
|
||||
this.token = token;
|
||||
this.agent = agent;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["token", "agent", "version"],
|
||||
properties: {
|
||||
"token": { type: "string" },
|
||||
"agent": { type: "string" },
|
||||
"version": { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.token, json.agent, json.version);
|
||||
}
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class MessageRegisterControl extends Message {
|
||||
/** @type {module:lib/data.RegisterControlData} */
|
||||
data;
|
||||
|
||||
constructor(data) {
|
||||
super("registerControl");
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["data"],
|
||||
properties: {
|
||||
"type": { const: "registerControl" },
|
||||
"data": RegisterControlData.jsonSchema,
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(RegisterControlData.fromJSON(json.data));
|
||||
}
|
||||
};
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class AccountDetails {
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {Array} */
|
||||
roles;
|
||||
|
||||
constructor(name, roles) {
|
||||
this.name = name;
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
additionalProperties: false,
|
||||
required: ["name", "roles"],
|
||||
properties: {
|
||||
"name": { type: "string" },
|
||||
"roles": {
|
||||
type: "array",
|
||||
items: {
|
||||
additionalProperties: false,
|
||||
type: "object",
|
||||
required: ["name", "id", "permissions"],
|
||||
properties: {
|
||||
"name": { type: "string" },
|
||||
"id": { type: "integer" },
|
||||
"permissions": {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.name, json.roles);
|
||||
}
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class ReadyData {
|
||||
/** @type {module:lib/data.Address} */
|
||||
src;
|
||||
/** @type {string} */
|
||||
sessionToken;
|
||||
/** @type {number} */
|
||||
sessionTimeout;
|
||||
/** @type {number} */
|
||||
heartbeatInterval;
|
||||
/** @type {AccountDetails|undefined} */
|
||||
account;
|
||||
|
||||
constructor(src, sessionToken, sessionTimeout, heartbeatInterval, account) {
|
||||
this.src = src;
|
||||
this.sessionToken = sessionToken;
|
||||
this.sessionTimeout = sessionTimeout;
|
||||
this.heartbeatInterval = heartbeatInterval;
|
||||
if (account) { this.account = account; }
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
additionalProperties: false,
|
||||
required: ["src", "sessionToken", "sessionTimeout", "heartbeatInterval"],
|
||||
properties: {
|
||||
"src": Address.jsonSchema,
|
||||
"sessionToken": { type: "string" },
|
||||
"sessionTimeout": { type: "number" },
|
||||
"heartbeatInterval": { type: "number" },
|
||||
"account": AccountDetails.jsonSchema,
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
let account;
|
||||
if (json.account) { account = AccountDetails.fromJSON(json.account); }
|
||||
return new this(
|
||||
Address.fromJSON(json.src),
|
||||
json.sessionToken,
|
||||
json.sessionTimeout,
|
||||
json.heartbeatInterval,
|
||||
account
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class MessageReady extends Message {
|
||||
/** @type {module:lib/data.ReadyData} */
|
||||
data;
|
||||
|
||||
constructor(data) {
|
||||
super("ready");
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["data"],
|
||||
properties: {
|
||||
"type": { const: "ready" },
|
||||
"data": ReadyData.jsonSchema,
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(ReadyData.fromJSON(json.data));
|
||||
}
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class ResumeData {
|
||||
/** @type {string} */
|
||||
sessionToken;
|
||||
/** @type {integer|undefined} */
|
||||
lastSeq;
|
||||
|
||||
constructor(sessionToken, lastSeq) {
|
||||
this.sessionToken = sessionToken;
|
||||
if (lastSeq !== undefined) { this.lastSeq = lastSeq; }
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["sessionToken"],
|
||||
properties: {
|
||||
"sessionToken": { type: "string" },
|
||||
"lastSeq": { type: "integer" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.sessionToken, json.lastSeq);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class MessageResume extends Message {
|
||||
/** @type {module:lib/data.ResumeData} */
|
||||
data;
|
||||
|
||||
constructor(data) {
|
||||
super("resume");
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["data"],
|
||||
properties: {
|
||||
"type": { const: "resume" },
|
||||
"data": ResumeData.jsonSchema,
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(ResumeData.fromJSON(json.data));
|
||||
}
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class ContinueData {
|
||||
/** @type {number} */
|
||||
sessionTimeout;
|
||||
/** @type {number} */
|
||||
heartbeatInterval;
|
||||
/** @type {integer|undefined} */
|
||||
lastSeq;
|
||||
|
||||
constructor(sessionTimeout, heartbeatInterval, lastSeq) {
|
||||
this.sessionTimeout = sessionTimeout;
|
||||
this.heartbeatInterval = heartbeatInterval;
|
||||
if (lastSeq !== undefined) { this.lastSeq = lastSeq; }
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
required: ["sessionTimeout", "heartbeatInterval"],
|
||||
properties: {
|
||||
"sessionTimeout": { type: "number" },
|
||||
"heartbeatInterval": { type: "number" },
|
||||
"lastSeq": { type: "integer" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.sessionTimeout, json.heartbeatInterval, json.lastSeq);
|
||||
}
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class MessageContinue extends Message {
|
||||
/** @type {module:lib/data.ContinueData} */
|
||||
data;
|
||||
|
||||
constructor(data) {
|
||||
super("continue");
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["data"],
|
||||
properties: {
|
||||
"type": { const: "continue" },
|
||||
"data": ContinueData.jsonSchema,
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(ContinueData.fromJSON(json.data));
|
||||
}
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class MessageInvalidate extends Message {
|
||||
constructor() {
|
||||
super("invalidate");
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
"type": { const: "invalidate" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this();
|
||||
}
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class MessageHeartbeat extends Message {
|
||||
/** @type {integer|undefined} */
|
||||
seq;
|
||||
|
||||
constructor(seq) {
|
||||
super("heartbeat");
|
||||
this.seq = seq;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["type"],
|
||||
properties: {
|
||||
"type": { const: "heartbeat" },
|
||||
"seq": { type: "integer" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.seq);
|
||||
}
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class MessageRequest extends Message {
|
||||
/** @type {number} */
|
||||
seq;
|
||||
/** @type {Address} */
|
||||
src;
|
||||
/** @type {Address} */
|
||||
dst;
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {*} */
|
||||
data;
|
||||
|
||||
constructor(seq, src, dst, name, data) {
|
||||
super("request");
|
||||
this.seq = seq;
|
||||
this.src = src;
|
||||
this.dst = dst;
|
||||
this.name = name;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["type", "seq", "src", "dst", "name"],
|
||||
properties: {
|
||||
"type": { const: "request" },
|
||||
"seq": { type: "integer" },
|
||||
"src": Address.jsonSchema,
|
||||
"dst": Address.jsonSchema,
|
||||
"name": { type: "string" },
|
||||
"data": {},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(
|
||||
json.seq,
|
||||
Address.fromJSON(json.src),
|
||||
Address.fromJSON(json.dst),
|
||||
json.name,
|
||||
json.data,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class MessageResponse extends Message {
|
||||
/** @type {number} */
|
||||
seq;
|
||||
/** @type {Address} */
|
||||
src;
|
||||
/** @type {Address} */
|
||||
dst;
|
||||
/** @type {*} */
|
||||
data;
|
||||
|
||||
constructor(seq, src, dst, data) {
|
||||
super("response");
|
||||
this.seq = seq;
|
||||
this.src = src;
|
||||
this.dst = dst;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["type", "seq", "src", "dst"],
|
||||
properties: {
|
||||
"type": { const: "response" },
|
||||
"seq": { type: "integer" },
|
||||
"src": Address.jsonSchema,
|
||||
"dst": Address.jsonSchema,
|
||||
"data": {},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(
|
||||
json.seq,
|
||||
Address.fromJSON(json.src),
|
||||
Address.fromJSON(json.dst),
|
||||
json.data,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class ResponseError {
|
||||
/** @type {string} */
|
||||
message;
|
||||
/** @type {string|undefined} */
|
||||
code;
|
||||
/** @type {string|undefined} */
|
||||
stack;
|
||||
|
||||
constructor(message, code, stack) {
|
||||
this.message = message;
|
||||
if (code) { this.code = code; }
|
||||
if (stack) { this.stack = stack; }
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
required: ["message"],
|
||||
properties: {
|
||||
"message": { type: "string" },
|
||||
"code": { type: "string" },
|
||||
"stack": { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.message, json.code, json.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class MessageResponseError extends Message {
|
||||
/** @type {number} */
|
||||
seq;
|
||||
/** @type {Address} */
|
||||
src;
|
||||
/** @type {Address} */
|
||||
dst;
|
||||
/** @type {module:lib/data.ResponseError} */
|
||||
data;
|
||||
|
||||
constructor(seq, src, dst, data) {
|
||||
super("responseError");
|
||||
this.seq = seq;
|
||||
this.src = src;
|
||||
this.dst = dst;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["type", "seq", "src", "dst", "data"],
|
||||
properties: {
|
||||
"type": { const: "responseError" },
|
||||
"seq": { type: "integer" },
|
||||
"src": Address.jsonSchema,
|
||||
"dst": Address.jsonSchema,
|
||||
"data": ResponseError.jsonSchema,
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(
|
||||
json.seq,
|
||||
Address.fromJSON(json.src),
|
||||
Address.fromJSON(json.dst),
|
||||
ResponseError.fromJSON(json.data),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class MessageEvent extends Message {
|
||||
/** @type {number} */
|
||||
seq;
|
||||
/** @type {Address} */
|
||||
src;
|
||||
/** @type {Address} */
|
||||
dst;
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {*} */
|
||||
data;
|
||||
|
||||
constructor(seq, src, dst, name, data) {
|
||||
super("event");
|
||||
this.seq = seq;
|
||||
this.src = src;
|
||||
this.dst = dst;
|
||||
this.name = name;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["type", "seq", "src", "dst", "name"],
|
||||
properties: {
|
||||
"type": { const: "event" },
|
||||
"seq": { type: "integer" },
|
||||
"src": Address.jsonSchema,
|
||||
"dst": Address.jsonSchema,
|
||||
"name": { type: "string" },
|
||||
"data": {},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(
|
||||
json.seq,
|
||||
Address.fromJSON(json.src),
|
||||
Address.fromJSON(json.dst),
|
||||
json.name,
|
||||
json.data,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class MessageDisconnect extends Message {
|
||||
/** @type {string} */
|
||||
data;
|
||||
|
||||
constructor(stage) {
|
||||
super("disconnect");
|
||||
this.data = stage;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["type"],
|
||||
properties: {
|
||||
"type": { const: "disconnect" },
|
||||
"data": { enum: ["prepare", "ready"] },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.data);
|
||||
}
|
||||
};
|
||||
|
||||
Message.jsonSchema = {
|
||||
type: "object",
|
||||
required: ["type"],
|
||||
properties: {
|
||||
"type": { enum: [
|
||||
"hello", "registerHost", "registerControl", "ready", "resume", "continue", "invalidate",
|
||||
"heartbeat", "request", "response", "responseError", "event", "disconnect",
|
||||
] },
|
||||
},
|
||||
oneOf: [
|
||||
MessageHello.jsonSchema,
|
||||
MessageRegisterHost.jsonSchema,
|
||||
MessageRegisterControl.jsonSchema,
|
||||
MessageReady.jsonSchema,
|
||||
MessageResume.jsonSchema,
|
||||
MessageContinue.jsonSchema,
|
||||
MessageInvalidate.jsonSchema,
|
||||
MessageHeartbeat.jsonSchema,
|
||||
MessageRequest.jsonSchema,
|
||||
MessageResponse.jsonSchema,
|
||||
MessageResponseError.jsonSchema,
|
||||
MessageEvent.jsonSchema,
|
||||
MessageDisconnect.jsonSchema,
|
||||
],
|
||||
};
|
||||
Message.validate = libSchema.compile(Message.jsonSchema);
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class PingRequest {
|
||||
static type = "request";
|
||||
static src = ["controller", "host", "control"];
|
||||
static dst = ["controller", "host", "control"];
|
||||
static permission = null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Message,
|
||||
HelloData,
|
||||
MessageHello,
|
||||
RegisterHostData,
|
||||
MessageRegisterHost,
|
||||
RegisterControlData,
|
||||
MessageRegisterControl,
|
||||
AccountDetails,
|
||||
ReadyData,
|
||||
MessageReady,
|
||||
ResumeData,
|
||||
MessageResume,
|
||||
ContinueData,
|
||||
MessageContinue,
|
||||
MessageInvalidate,
|
||||
MessageHeartbeat,
|
||||
Address,
|
||||
MessageRequest,
|
||||
MessageResponse,
|
||||
ResponseError,
|
||||
MessageResponseError,
|
||||
MessageEvent,
|
||||
MessageDisconnect,
|
||||
PingRequest,
|
||||
};
|
||||
@@ -0,0 +1,240 @@
|
||||
"use strict";
|
||||
const { jsonArray } = require("./composites");
|
||||
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class HostDetails {
|
||||
/** @type {string} */
|
||||
agent;
|
||||
/** @type {string} */
|
||||
version;
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {number} */
|
||||
id;
|
||||
/** @type {boolean} */
|
||||
connected;
|
||||
/** @type {string|undefined} */
|
||||
publicAddress;
|
||||
|
||||
constructor(agent, version, name, id, connected, publicAddress) {
|
||||
this.agent = agent;
|
||||
this.version = version;
|
||||
this.name = name;
|
||||
this.id = id;
|
||||
this.connected = connected;
|
||||
if (publicAddress !== undefined) { this.publicAddress = publicAddress; }
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
required: ["agent", "version", "name", "id", "connected"],
|
||||
properties: {
|
||||
"agent": { type: "string" },
|
||||
"version": { type: "string" },
|
||||
"name": { type: "string" },
|
||||
"id": { type: "integer" },
|
||||
"connected": { type: "boolean" },
|
||||
"publicAddress": { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.agent, json.version, json.name, json.id, json.connected, json.publicAddress);
|
||||
}
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class HostListRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.host.list";
|
||||
static Response = jsonArray(HostDetails);
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class HostSetSubscriptionsRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.host.subscribe";
|
||||
|
||||
/** @type {boolean} */
|
||||
all;
|
||||
/** @type {Array<number>} */
|
||||
hostIds;
|
||||
|
||||
constructor(all, hostIds) {
|
||||
this.all = all;
|
||||
this.hostIds = hostIds;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["all", "hostIds"],
|
||||
properties: {
|
||||
all: { type: "boolean" },
|
||||
hostIds: {
|
||||
type: "array",
|
||||
items: { type: "integer" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.all, json.hostIds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class HostUpdateEvent {
|
||||
static type = "event";
|
||||
static src = "controller";
|
||||
static dst = "control";
|
||||
|
||||
/** @type {module:lib/data.HostDetails} */
|
||||
update;
|
||||
|
||||
constructor(update) {
|
||||
this.update = update;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["update"],
|
||||
properties: {
|
||||
update: HostDetails.jsonSchema,
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(HostDetails.fromJSON(json.update));
|
||||
}
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class HostMetricsRequest {
|
||||
static type = "request";
|
||||
static src = "controller";
|
||||
static dst = "host";
|
||||
}
|
||||
HostMetricsRequest.Response = class Response { // TODO: Use JSON class pattern in Prometheus
|
||||
/** @type {array} */
|
||||
results;
|
||||
|
||||
constructor(results) {
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["results"],
|
||||
properties: {
|
||||
"results": {
|
||||
type: "array",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.results);
|
||||
}
|
||||
};
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class ControllerConnectionEvent {
|
||||
static type = "event";
|
||||
static src = "host";
|
||||
static dst = "instance";
|
||||
|
||||
/** @type {string} */
|
||||
event;
|
||||
|
||||
constructor(event) {
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
required: ["event"],
|
||||
properties: {
|
||||
"event": { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.event);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class PrepareControllerDisconnectRequest {
|
||||
static type = "request";
|
||||
static src = "host";
|
||||
static dst = "instance";
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class SyncUserListsEvent {
|
||||
static type = "event";
|
||||
static src = "controller";
|
||||
static dst = "host";
|
||||
|
||||
/** @type {Set<string>} */
|
||||
adminlist;
|
||||
/** @type {Map<string, string>} */
|
||||
banlist;
|
||||
/** @type {Set<string>} */
|
||||
whitelist;
|
||||
|
||||
constructor(adminlist, banlist, whitelist) {
|
||||
this.adminlist = adminlist;
|
||||
this.banlist = banlist;
|
||||
this.whitelist = whitelist;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
required: ["adminlist", "banlist", "whitelist"],
|
||||
properties: {
|
||||
"adminlist": {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
"banlist": {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "array",
|
||||
additionalItems: false,
|
||||
items: [{ type: "string" }, { type: "string" }],
|
||||
},
|
||||
},
|
||||
"whitelist": {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(new Set(json.adminlist), new Set(json.banlist), new Set(json.whitelist));
|
||||
}
|
||||
|
||||
static toJSON() {
|
||||
return {
|
||||
adminlist: [...this.adminlist],
|
||||
banlist: [...this.banlist],
|
||||
whitelist: [...this.whitelist],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
HostDetails,
|
||||
HostListRequest,
|
||||
HostSetSubscriptionsRequest,
|
||||
HostUpdateEvent,
|
||||
HostMetricsRequest,
|
||||
ControllerConnectionEvent,
|
||||
PrepareControllerDisconnectRequest,
|
||||
SyncUserListsEvent,
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,474 @@
|
||||
"use strict";
|
||||
const ModPack = require("./ModPack");
|
||||
const ModInfo = require("./ModInfo");
|
||||
const { jsonArray, JsonString } = require("./composites");
|
||||
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class ModPackGetRequest {
|
||||
static type = "request";
|
||||
static src = ["instance", "control"];
|
||||
static dst = "controller";
|
||||
static permission = "core.mod_pack.get";
|
||||
|
||||
/** @type {number} */
|
||||
id;
|
||||
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["id"],
|
||||
properties: {
|
||||
id: { type: "integer" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.id);
|
||||
}
|
||||
|
||||
static Response = ModPack;
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class ModPackGetDefaultRequest {
|
||||
static type = "request";
|
||||
static src = ["instance", "control"];
|
||||
static dst = "controller";
|
||||
static permission = "core.mod_pack.get";
|
||||
static Response = ModPack;
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class ModPackListRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.mod_pack.list";
|
||||
static Response = jsonArray(ModPack);
|
||||
}
|
||||
|
||||
/** @memberof module:lib/data */
|
||||
class ModPackCreateRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.mod_pack.create";
|
||||
|
||||
/** @type {module:lib/data.ModPack} */
|
||||
modPack;
|
||||
|
||||
constructor(modPack) {
|
||||
this.modPack = modPack;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["modPack"],
|
||||
properties: {
|
||||
"modPack": ModPack.jsonSchema,
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(ModPack.fromJSON(json.modPack));
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class ModPackUpdateRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.mod_pack.update";
|
||||
|
||||
/** @type {module:lib/data.ModPack} */
|
||||
modPack;
|
||||
|
||||
constructor(modPack) {
|
||||
this.modPack = modPack;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["modPack"],
|
||||
properties: {
|
||||
"modPack": ModPack.jsonSchema,
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(ModPack.fromJSON(json.modPack));
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class ModPackDeleteRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.mod_pack.delete";
|
||||
|
||||
/** @type {number} */
|
||||
id;
|
||||
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["id"],
|
||||
properties: {
|
||||
id: { type: "integer" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.id);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class ModPackSetSubscriptionsRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.mod_pack.subscribe";
|
||||
|
||||
/** @type {boolean} */
|
||||
all;
|
||||
/** @type {Array<number>} */
|
||||
modPackIds;
|
||||
|
||||
constructor(all, modPackIds) {
|
||||
this.all = all;
|
||||
this.modPackIds = modPackIds;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["all", "modPackIds"],
|
||||
properties: {
|
||||
all: { type: "boolean" },
|
||||
modPackIds: {
|
||||
type: "array",
|
||||
items: { type: "integer" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.all, json.modPackIds);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class ModGetRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.mod.get";
|
||||
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {string} */
|
||||
version;
|
||||
|
||||
constructor(name, version) {
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["name", "version"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
version: { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.name, json.version);
|
||||
}
|
||||
|
||||
static Response = ModInfo;
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class ModListRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.mod.list";
|
||||
static Response = jsonArray(ModInfo);
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class ModSearchRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.mod.search";
|
||||
|
||||
/** @type {string} */
|
||||
query;
|
||||
/** @type {string} */
|
||||
factorioVersion;
|
||||
/** @type {number} */
|
||||
page;
|
||||
/** @type {number|undefined} */
|
||||
pageSize;
|
||||
/** @type {string|undefined} */
|
||||
sort;
|
||||
/** @type {string|undefined} */
|
||||
sortOrder;
|
||||
|
||||
constructor(query, factorioVersion, page, pageSize, sort, sortOrder) {
|
||||
this.query = query;
|
||||
this.factorioVersion = factorioVersion;
|
||||
this.page = page;
|
||||
if (pageSize) { this.pageSize = pageSize; }
|
||||
if (sort) { this.sort = sort; }
|
||||
if (sortOrder) { this.sortOrder = sortOrder; }
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["query", "factorioVersion", "page"],
|
||||
properties: {
|
||||
query: { type: "string" },
|
||||
factorioVersion: { type: "string" },
|
||||
pageSize: { type: "integer" },
|
||||
page: { type: "integer" },
|
||||
sort: { type: "string" },
|
||||
sortOrder: { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.query, json.factorioVersion, json.page, json.pageSize, json.sort, json.sortOrder);
|
||||
}
|
||||
}
|
||||
ModSearchRequest.Response = class Response {
|
||||
/** @type {Array<string>} */
|
||||
queryIssues;
|
||||
/** @type {number} */
|
||||
pageCount;
|
||||
/** @type {number} */
|
||||
resultCount;
|
||||
/** @type {Array<object>} */
|
||||
results;
|
||||
|
||||
constructor(queryIssues, pageCount, resultCount, results) {
|
||||
this.queryIssues = queryIssues;
|
||||
this.pageCount = pageCount;
|
||||
this.resultCount = resultCount;
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["queryIssues", "pageCount", "resultCount", "results"],
|
||||
properties: {
|
||||
queryIssues: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
pageCount: { type: "integer" },
|
||||
resultCount: { type: "integer" },
|
||||
results: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
versions: {
|
||||
type: "array",
|
||||
items: ModInfo.jsonSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
let results = json.results.map(
|
||||
({ name, versions }) => ({ name, versions: versions.map(mod => ModInfo.fromJSON(mod)) })
|
||||
);
|
||||
return new this(json.queryIssues, json.pageCount, json.resultCount, results);
|
||||
}
|
||||
};
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class ModSetSubscriptionsRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.mod.subscribe";
|
||||
|
||||
/** @type {boolean} */
|
||||
all;
|
||||
/** @type {Array<string>} */
|
||||
modNames;
|
||||
|
||||
constructor(all, modNames) {
|
||||
this.all = all;
|
||||
this.modNames = modNames;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["all", "modNames"],
|
||||
properties: {
|
||||
all: { type: "boolean" },
|
||||
modNames: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.all, json.modNames);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class ModDownloadRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.mod.download";
|
||||
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {string} */
|
||||
version;
|
||||
|
||||
constructor(name, version) {
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["name", "version"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
version: { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.name, json.version);
|
||||
}
|
||||
|
||||
static Response = JsonString;
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class ModDeleteRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.mod.delete";
|
||||
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {string} */
|
||||
version;
|
||||
|
||||
constructor(name, version) {
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["name", "version"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
version: { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.name, json.version);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class ModPackUpdateEvent {
|
||||
static type = "event";
|
||||
static src = "controller";
|
||||
static dst = "control";
|
||||
|
||||
/** @type {module:lib/data.ModPack} */
|
||||
modPack;
|
||||
|
||||
constructor(modPack) {
|
||||
this.modPack = modPack;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["modPack"],
|
||||
properties: {
|
||||
modPack: ModPack.jsonSchema,
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(ModPack.fromJSON(json.modPack));
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class ModUpdateEvent {
|
||||
static type = "event";
|
||||
static src = "controller";
|
||||
static dst = "control";
|
||||
|
||||
/** @type {module:lib/data.ModInfo} */
|
||||
mod;
|
||||
|
||||
constructor(mod) {
|
||||
this.mod = mod;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["mod"],
|
||||
properties: {
|
||||
mod: ModInfo.jsonSchema,
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(ModInfo.fromJSON(json.mod));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ModPackGetRequest,
|
||||
ModPackGetDefaultRequest,
|
||||
ModPackListRequest,
|
||||
ModPackCreateRequest,
|
||||
ModPackUpdateRequest,
|
||||
ModPackDeleteRequest,
|
||||
ModPackSetSubscriptionsRequest,
|
||||
ModGetRequest,
|
||||
ModListRequest,
|
||||
ModSearchRequest,
|
||||
ModSetSubscriptionsRequest,
|
||||
ModDownloadRequest,
|
||||
ModDeleteRequest,
|
||||
ModPackUpdateEvent,
|
||||
ModUpdateEvent,
|
||||
};
|
||||
@@ -0,0 +1,706 @@
|
||||
"use strict";
|
||||
const PlayerStats = require("../PlayerStats");
|
||||
const { jsonArray, JsonNumber } = require("./composites");
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class RawPermission { // TODO refactor into lib/user.Permission
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {string} */
|
||||
title;
|
||||
/** @type {string} */
|
||||
description;
|
||||
|
||||
constructor(name, title, description) {
|
||||
this.name = name;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
required: ["name", "title", "description"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
title: { type: "string" },
|
||||
description: { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.name, json.title, json.description);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class PermissionListRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.permission.list";
|
||||
static Response = jsonArray(RawPermission);
|
||||
}
|
||||
|
||||
class RawRole { // TODO refactor into lib/user.Role
|
||||
/** @type {number} */
|
||||
id;
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {string} */
|
||||
description;
|
||||
/** @type {Array<string>} */
|
||||
permissions;
|
||||
|
||||
constructor(id, name, description, permissions) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.permissions = permissions;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["id", "name", "description", "permissions"],
|
||||
properties: {
|
||||
id: { type: "integer" },
|
||||
name: { type: "string" },
|
||||
description: { type: "string" },
|
||||
permissions: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.id, json.name, json.description, json.permissions);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class RoleListRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.role.list";
|
||||
static Response = jsonArray(RawRole);
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class RoleCreateRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.role.create";
|
||||
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {string} */
|
||||
description;
|
||||
/** @type {Array<string>} */
|
||||
permissions;
|
||||
|
||||
constructor(name, description, permissions) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.permissions = permissions;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["name", "description", "permissions"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
description: { type: "string" },
|
||||
permissions: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.name, json.description, json.permissions);
|
||||
}
|
||||
|
||||
static Response = JsonNumber;
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class RoleUpdateRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.role.update";
|
||||
|
||||
/** @type {number} */
|
||||
id;
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {string} */
|
||||
description;
|
||||
/** @type {Array<string>} */
|
||||
permissions;
|
||||
|
||||
constructor(id, name, description, permissions) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.permissions = permissions;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["id", "name", "description", "permissions"],
|
||||
properties: {
|
||||
id: { type: "integer" },
|
||||
name: { type: "string" },
|
||||
description: { type: "string" },
|
||||
permissions: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.id, json.name, json.description, json.permissions);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class RoleGrantDefaultPermissionsRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.role.update";
|
||||
|
||||
/** @type {number} */
|
||||
id;
|
||||
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["id"],
|
||||
properties: {
|
||||
id: { type: "integer" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.id);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class RoleDeleteRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.role.delete";
|
||||
|
||||
/** @type {number} */
|
||||
id;
|
||||
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["id"],
|
||||
properties: {
|
||||
id: { type: "integer" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.id);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class RawUser { // TODO refactor into lib/user.User
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {Array<number>} */
|
||||
roles;
|
||||
/** @type {Array<number>} */
|
||||
instances;
|
||||
/** @type {boolean|undefined} */
|
||||
isAdmin;
|
||||
/** @type {boolean|undefined} */
|
||||
isBanned;
|
||||
/** @type {boolean|undefined} */
|
||||
isWhitelisted;
|
||||
/** @type {string|undefined} */
|
||||
banReason;
|
||||
/** @type {boolean|undefined} */
|
||||
isDeleted;
|
||||
/** @type {module:lib/PlayerStats} */
|
||||
playerStats;
|
||||
/** @type {Map<number, module:lib/PlayerStats>|undefined} */
|
||||
instanceStats;
|
||||
|
||||
constructor(
|
||||
name, roles, instances, isAdmin, isBanned, isWhitelisted, banReason,
|
||||
isDeleted, playerStats, instanceStats
|
||||
) {
|
||||
this.name = name;
|
||||
this.roles = roles;
|
||||
this.instances = instances;
|
||||
if (isAdmin !== undefined) { this.isAdmin = isAdmin; }
|
||||
if (isBanned !== undefined) { this.isBanned = isBanned; }
|
||||
if (isWhitelisted !== undefined) { this.isWhitelisted = isWhitelisted; }
|
||||
if (banReason !== undefined) { this.banReason = banReason; }
|
||||
if (isDeleted !== undefined) { this.isDeleted = isDeleted; }
|
||||
if (playerStats !== undefined) { this.playerStats = playerStats; }
|
||||
if (instanceStats !== undefined) { this.instanceStats = instanceStats; }
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["name", "roles", "instances"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
roles: { type: "array", items: { type: "integer" } },
|
||||
instances: { type: "array", items: { type: "integer" } },
|
||||
isAdmin: { type: "boolean" },
|
||||
isBanned: { type: "boolean" },
|
||||
isWhitelisted: { type: "boolean" },
|
||||
banReason: { type: "string" },
|
||||
isDeleted: { type: "boolean" },
|
||||
playerStats: PlayerStats.jsonSchema,
|
||||
instanceStats: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "array",
|
||||
items: [{ type: "integer" }, PlayerStats.jsonSchema],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
let playerStats;
|
||||
if (json.playerStats) {
|
||||
playerStats = PlayerStats.fromJSON(json.playerStats);
|
||||
}
|
||||
let instanceStats;
|
||||
if (json.instanceStats) {
|
||||
instanceStats = new Map(
|
||||
json.instanceStats.map(([id, stats]) => [id, PlayerStats.fromJSON(stats)])
|
||||
);
|
||||
}
|
||||
return new this(
|
||||
json.name, json.roles, json.instances, json.isAdmin, json.isBanned, json.isWhitelisted, json.banReason,
|
||||
json.isDeleted, playerStats, instanceStats
|
||||
);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const json = {
|
||||
name: this.name,
|
||||
roles: this.roles,
|
||||
instances: this.instances,
|
||||
isAdmin: this.isAdmin,
|
||||
isBanned: this.isBanned,
|
||||
isWhitelisted: this.isWhitelisted,
|
||||
banReason: this.banReason,
|
||||
isDeleted: this.isDeleted,
|
||||
playerStats: this.playerStats,
|
||||
};
|
||||
if (this.instanceStats) {
|
||||
json.instanceStats = [...this.instanceStats];
|
||||
}
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class UserGetRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.user.get";
|
||||
|
||||
/** @type {string} */
|
||||
name;
|
||||
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["name"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.name);
|
||||
}
|
||||
|
||||
static Response = RawUser;
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class UserListRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.user.list";
|
||||
static Response = jsonArray(RawUser);
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class UserSetSubscriptionsRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.user.subscribe";
|
||||
|
||||
/** @type {boolean} */
|
||||
all;
|
||||
/** @type {Array<string>} */
|
||||
names;
|
||||
|
||||
constructor(all, names) {
|
||||
this.all = all;
|
||||
this.names = names;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["all", "names"],
|
||||
properties: {
|
||||
all: { type: "boolean" },
|
||||
names: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.all, json.names);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class UserCreateRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.user.create";
|
||||
|
||||
/** @type {string} */
|
||||
name;
|
||||
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["name"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.name);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class UserRevokeTokenRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.user.revoke_token";
|
||||
|
||||
/** @type {string} */
|
||||
name;
|
||||
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["name"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.name);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class UserUpdateRolesRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.user.update_roles";
|
||||
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {Array<number>} */
|
||||
roles;
|
||||
|
||||
constructor(name, roles) {
|
||||
this.name = name;
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["name", "roles"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
roles: {
|
||||
type: "array",
|
||||
items: { type: "integer" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.name, json.roles);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class UserSetAdminRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.user.set_admin";
|
||||
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {boolean} */
|
||||
create;
|
||||
/** @type {boolean} */
|
||||
admin;
|
||||
|
||||
constructor(name, create, admin) {
|
||||
this.name = name;
|
||||
this.create = create;
|
||||
this.admin = admin;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["name", "create", "admin"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
create: { type: "boolean" },
|
||||
admin: { type: "boolean" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.name, json.create, json.admin);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class UserSetWhitelistedRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.user.set_whitelisted";
|
||||
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {boolean} */
|
||||
create;
|
||||
/** @type {boolean} */
|
||||
whitelisted;
|
||||
|
||||
constructor(name, create, whitelisted) {
|
||||
this.name = name;
|
||||
this.create = create;
|
||||
this.whitelisted = whitelisted;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["name", "create", "whitelisted"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
create: { type: "boolean" },
|
||||
whitelisted: { type: "boolean" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.name, json.create, json.whitelisted);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class UserSetBannedRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.user.set_banned";
|
||||
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {boolean} */
|
||||
create;
|
||||
/** @type {boolean} */
|
||||
banned;
|
||||
/** @type {string} */
|
||||
reason;
|
||||
|
||||
constructor(name, create, banned, reason) {
|
||||
this.name = name;
|
||||
this.create = create;
|
||||
this.banned = banned;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["name", "create", "banned", "reason"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
create: { type: "boolean" },
|
||||
banned: { type: "boolean" },
|
||||
reason: { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.name, json.create, json.banned, json.reason);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class UserDeleteRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static permission = "core.user.delete";
|
||||
|
||||
/** @type {string} */
|
||||
name;
|
||||
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["name"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class AccountUpdateEvent {
|
||||
static type = "event";
|
||||
static src = "controller";
|
||||
static dst = "control";
|
||||
|
||||
/** @type {Array<object>|undefined} */
|
||||
roles;
|
||||
|
||||
constructor(roles) {
|
||||
if (roles) { this.roles = roles; }
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
roles: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
required: ["name", "id", "permissions"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
id: { type: "integer" },
|
||||
permissions: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.roles);
|
||||
}
|
||||
}
|
||||
|
||||
/* @memberof module:lib/data */
|
||||
class UserUpdateEvent {
|
||||
static type = "event";
|
||||
static src = "controller";
|
||||
static dst = "control";
|
||||
|
||||
/** @type {module:lib/data.RawUser} */
|
||||
user;
|
||||
|
||||
constructor(user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["user"],
|
||||
properties: {
|
||||
user: RawUser.jsonSchema,
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(RawUser.fromJSON(json.user));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RawPermission,
|
||||
PermissionListRequest,
|
||||
RawRole,
|
||||
RoleListRequest,
|
||||
RoleCreateRequest,
|
||||
RoleUpdateRequest,
|
||||
RoleGrantDefaultPermissionsRequest,
|
||||
RoleDeleteRequest,
|
||||
RawUser,
|
||||
UserGetRequest,
|
||||
UserListRequest,
|
||||
UserSetSubscriptionsRequest,
|
||||
UserCreateRequest,
|
||||
UserRevokeTokenRequest,
|
||||
UserUpdateRolesRequest,
|
||||
UserSetAdminRequest,
|
||||
UserSetWhitelistedRequest,
|
||||
UserSetBannedRequest,
|
||||
UserDeleteRequest,
|
||||
AccountUpdateEvent,
|
||||
UserUpdateEvent,
|
||||
};
|
||||
@@ -14,7 +14,13 @@ class CommandError extends Error { }
|
||||
* Thrown from requests sent when an error occured handling it
|
||||
* @static
|
||||
*/
|
||||
class RequestError extends Error { }
|
||||
class RequestError extends Error {
|
||||
constructor(message, code, stack) {
|
||||
super(message);
|
||||
if (code) { this.code = code; }
|
||||
if (stack) { this.stack = stack; }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when a permission check fails.
|
||||
|
||||
@@ -13,13 +13,13 @@ const crypto = require("crypto"); // needed for getTempFile
|
||||
* @param {function(string): boolean} filter -
|
||||
* Optional function to filter out files to ignore. Should return true
|
||||
* if the file is to be considered.
|
||||
* @returns {Promise<?string>}
|
||||
* @returns {Promise<string|undefined>}
|
||||
* Name of the file with the newest timestamp or null if the
|
||||
* directory contains no files.
|
||||
*/
|
||||
async function getNewestFile(directory, filter = (name) => true) {
|
||||
let newestTime = new Date(0);
|
||||
let newestFile = null;
|
||||
let newestFile;
|
||||
for (let entry of await fs.readdir(directory, { withFileTypes: true })) {
|
||||
if (entry.isFile()) {
|
||||
let stat = await fs.stat(path.join(directory, entry.name));
|
||||
|
||||
+229
-100
@@ -1,9 +1,10 @@
|
||||
// Connection adapters for Links
|
||||
"use strict";
|
||||
const assert = require("assert").strict;
|
||||
const events = require("events");
|
||||
const WebSocket = require("isomorphic-ws");
|
||||
|
||||
const libSchema = require("../schema");
|
||||
const libData = require("../data");
|
||||
const libErrors = require("../errors");
|
||||
const { logger } = require("../logging");
|
||||
const ExponentialBackoff = require("../ExponentialBackoff");
|
||||
@@ -15,34 +16,105 @@ const ExponentialBackoff = require("../ExponentialBackoff");
|
||||
* @extends events.EventEmitter
|
||||
* @memberof module:lib/link
|
||||
*/
|
||||
class WebSocketBaseConnector extends events.EventEmitter {
|
||||
constructor(sessionTimeout) {
|
||||
class BaseConnector extends events.EventEmitter {
|
||||
constructor(src, dst) {
|
||||
super();
|
||||
this._seq = 1;
|
||||
this.src = src;
|
||||
this.dst = dst;
|
||||
}
|
||||
|
||||
this._sessionTimeout = sessionTimeout;
|
||||
_reset() {
|
||||
this._seq = 1;
|
||||
}
|
||||
|
||||
_invalidate() {
|
||||
this._seq = 1;
|
||||
}
|
||||
|
||||
sendRequest(request, requestId, dst) {
|
||||
let seq = this._seq;
|
||||
this._seq += 1;
|
||||
let src = new libData.Address(this.src.type, this.src.id, requestId);
|
||||
let Request = request.constructor;
|
||||
let name = Request.plugin ? `${Request.plugin}:${Request.name}` : Request.name;
|
||||
let hasData = Boolean(Request.jsonSchema);
|
||||
const message = new libData.MessageRequest(seq, src, dst, name, hasData ? request : undefined);
|
||||
this.send(message);
|
||||
return seq;
|
||||
}
|
||||
|
||||
sendResponse(response, dst, src = this.src) {
|
||||
let seq = this._seq;
|
||||
this._seq += 1;
|
||||
const message = new libData.MessageResponse(seq, src, dst, response);
|
||||
this.send(message);
|
||||
return seq;
|
||||
}
|
||||
|
||||
sendResponseError(error, dst, src = this.src) {
|
||||
let seq = this._seq;
|
||||
this._seq += 1;
|
||||
const message = new libData.MessageResponseError(seq, src, dst, error);
|
||||
this.send(message);
|
||||
return seq;
|
||||
}
|
||||
|
||||
sendEvent(event, dst) {
|
||||
let seq = this._seq;
|
||||
this._seq += 1;
|
||||
let Event = event.constructor;
|
||||
let name = Event.plugin ? `${Event.plugin}:${Event.name}` : Event.name;
|
||||
let hasData = Boolean(Event.jsonSchema);
|
||||
const message = new libData.MessageEvent(seq, this.src, dst, name, hasData ? event : undefined);
|
||||
this.send(message);
|
||||
return seq;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base connector for links
|
||||
*
|
||||
* @extends module:lib/link.BaseConnector
|
||||
* @memberof module:lib/link
|
||||
*/
|
||||
class WebSocketBaseConnector extends BaseConnector {
|
||||
constructor(src, dst) {
|
||||
super(src, dst);
|
||||
|
||||
// One of closed, connecting (client only), connected and resuming.
|
||||
this._state = "closed";
|
||||
this._closing = false;
|
||||
this._socket = null;
|
||||
this._seq = 1;
|
||||
this._lastHeartbeat = null;
|
||||
this._heartbeatId = null;
|
||||
this._heartbeatInterval = null;
|
||||
this._lastReceivedSeq = null;
|
||||
this._lastReceivedSeq = undefined;
|
||||
this._sendBuffer = [];
|
||||
}
|
||||
|
||||
_reset() {
|
||||
this._state = "closed";
|
||||
this._closing = false;
|
||||
this._socket = null;
|
||||
this._seq = 1;
|
||||
assert(this._socket === null);
|
||||
this._lastHeartbeat = null;
|
||||
this._heartbeatId = null;
|
||||
assert(this._heartbeatId === null);
|
||||
this._heartbeatInterval = null;
|
||||
this._lastReceivedSeq = null;
|
||||
this._lastReceivedSeq = undefined;
|
||||
this._sendBuffer.length = 0;
|
||||
super._reset();
|
||||
}
|
||||
|
||||
_invalidate() {
|
||||
this._state = "connecting";
|
||||
assert(this._closing === false);
|
||||
assert(this._socket);
|
||||
this._lastHeartbeat = null;
|
||||
assert(this._heartbeatId === null);
|
||||
this._heartbeatInterval = null;
|
||||
this._lastReceivedSeq = undefined;
|
||||
this._sendBuffer.length = 0;
|
||||
super._invalidate();
|
||||
}
|
||||
|
||||
_check(...expectedStates) {
|
||||
@@ -54,7 +126,7 @@ class WebSocketBaseConnector extends events.EventEmitter {
|
||||
}
|
||||
|
||||
_dropSendBufferSeq(seq) {
|
||||
if (seq === null) {
|
||||
if (seq === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,6 +141,45 @@ class WebSocketBaseConnector extends events.EventEmitter {
|
||||
this._sendBuffer.splice(0, dropCount);
|
||||
}
|
||||
|
||||
_parseMessage(text) {
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(text);
|
||||
} catch (err) {
|
||||
this.close(4000, "Malformed JSON");
|
||||
return undefined;
|
||||
}
|
||||
if (!libData.Message.validate(json)) {
|
||||
logger.error(`Received malformed message: ${text}`);
|
||||
// logger.error(JSON.stringify(libData.Message.validate.errors, null, 4));
|
||||
this.close(4000, "Malformed message");
|
||||
return undefined;
|
||||
}
|
||||
return libData.Message.fromJSON(json);
|
||||
}
|
||||
|
||||
_processMessage(message) {
|
||||
if (message.type === "heartbeat") {
|
||||
this._processHeartbeat(message);
|
||||
|
||||
} else if (message.type === "disconnect") {
|
||||
if (message.data === "ready") {
|
||||
this.emit("disconnectReady");
|
||||
|
||||
} else if (message.data === "prepare") {
|
||||
this.setClosing();
|
||||
this.emit("disconnectPrepare");
|
||||
}
|
||||
|
||||
} else {
|
||||
if (message.seq !== undefined) {
|
||||
this._lastReceivedSeq = message.seq;
|
||||
}
|
||||
|
||||
this.emit("message", message);
|
||||
}
|
||||
}
|
||||
|
||||
_doHeartbeat() {
|
||||
this._check("connected");
|
||||
if (Date.now() - this._lastHeartbeat > 2000 * this._heartbeatInterval) {
|
||||
@@ -81,23 +192,13 @@ class WebSocketBaseConnector extends events.EventEmitter {
|
||||
}
|
||||
|
||||
} else {
|
||||
this._socket.send(JSON.stringify({
|
||||
seq: null,
|
||||
type: "heartbeat",
|
||||
data: { seq: this._lastReceivedSeq },
|
||||
}));
|
||||
this._sendInternal(new libData.MessageHeartbeat(this._lastReceivedSeq));
|
||||
}
|
||||
}
|
||||
|
||||
_processHeartbeat(message) {
|
||||
if (!libSchema.heartbeat(message)) {
|
||||
logger.warn("Connector | closing after received invalid heartbeat");
|
||||
this._socket.close(1002, "Invalid heartbeat");
|
||||
return;
|
||||
}
|
||||
|
||||
this._lastHeartbeat = Date.now();
|
||||
this._dropSendBufferSeq(message.data.seq);
|
||||
this._dropSendBufferSeq(message.seq);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,29 +221,30 @@ class WebSocketBaseConnector extends events.EventEmitter {
|
||||
if (this._heartbeatId) {
|
||||
clearInterval(this._heartbeatId);
|
||||
this._heartbeatId = null;
|
||||
this._lastHeartbeat = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message over the socket
|
||||
*
|
||||
* @param {string} type - Message type to send.
|
||||
* @param {Object} data - Data to send with message.
|
||||
* @returns {number} the sequence number of the message sent
|
||||
* This is a low level method that should only be used for implementing
|
||||
* links. See sendTo for sending requests and events.
|
||||
*
|
||||
* @param {module:lib/data.Message} message - Message to send.
|
||||
*/
|
||||
send(type, data) {
|
||||
send(message) {
|
||||
if (!["connected", "resuming"].includes(this._state)) {
|
||||
throw new libErrors.SessionLost("No session");
|
||||
}
|
||||
let seq = this._seq;
|
||||
this._seq += 1;
|
||||
let message = { seq, type, data };
|
||||
|
||||
this._sendBuffer.push(message);
|
||||
if (this._state === "connected") {
|
||||
this._socket.send(JSON.stringify(message));
|
||||
this._sendInternal(message);
|
||||
}
|
||||
return seq;
|
||||
}
|
||||
|
||||
_sendInternal(message) {
|
||||
this._socket.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,11 +257,40 @@ class WebSocketBaseConnector extends events.EventEmitter {
|
||||
throw new Error("Abstract function");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gracefully disconnect
|
||||
*
|
||||
* Sets closing flag an initiates the disconnect sequence.
|
||||
*/
|
||||
async disconnect() {
|
||||
if (this._state !== "connected") {
|
||||
await this.close(1000, "Disconnect");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setClosing();
|
||||
this.send(new libData.MessageDisconnect("prepare"));
|
||||
|
||||
let timeout;
|
||||
let waitTimeout = new Promise(resolve => { timeout = setTimeout(resolve, 10000); }); // Make Configurable?
|
||||
try {
|
||||
await Promise.race([
|
||||
waitTimeout,
|
||||
events.once(this, "close"),
|
||||
events.once(this, "disconnectReady"),
|
||||
]);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
await this.close(1000, "Disconnect");
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the connection is established and active
|
||||
*/
|
||||
get connected() {
|
||||
return this._state === "connected";
|
||||
return !this._closing && this._state === "connected";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,7 +317,7 @@ class WebSocketBaseConnector extends events.EventEmitter {
|
||||
*/
|
||||
class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
constructor(url, maxReconnectDelay, tlsCa = null) {
|
||||
super(null);
|
||||
super(undefined, new libData.Address(libData.Address.controller, 0));
|
||||
|
||||
this._url = url;
|
||||
this._backoff = new ExponentialBackoff({ max: maxReconnectDelay });
|
||||
@@ -199,32 +330,40 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
// connected: Connection is online
|
||||
this._reconnectId = null;
|
||||
this._sessionToken = null;
|
||||
this._sessionTimeout = null;
|
||||
this._startedResuming = null;
|
||||
}
|
||||
|
||||
_reset() {
|
||||
clearTimeout(this._reconnectId);
|
||||
this._reconnectId = null;
|
||||
this._lastReceivedSeq = null;
|
||||
this._sessionToken = null;
|
||||
this._sessionTimeout = null;
|
||||
this._sendBuffer.length = 0;
|
||||
this._startedResuming = null;
|
||||
super._reset();
|
||||
this.src = undefined;
|
||||
}
|
||||
|
||||
_invalidate() {
|
||||
assert(this._reconnectId === null);
|
||||
this._sessionToken = null;
|
||||
this._sessionTimeout = null;
|
||||
this._startedResuming = null;
|
||||
super._invalidate();
|
||||
this.src = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a handshake message over the socket
|
||||
*
|
||||
* Used in the register function in order to send the register handshake
|
||||
* message over the WebSocket.
|
||||
* Used in the register function in order to send the handshake messages
|
||||
* over the WebSocket.
|
||||
*
|
||||
* @param {string} type - Message type to send.
|
||||
* @param {Object} data - Data to send with message.
|
||||
* @param {module:lib/data.Message} message - Message to send.
|
||||
*/
|
||||
sendHandshake(type, data) {
|
||||
sendHandshake(message) {
|
||||
this._check("connecting", "resuming");
|
||||
this._socket.send(JSON.stringify({ seq: null, type, data }));
|
||||
this._sendInternal(message);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -329,8 +468,13 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
) {
|
||||
logger.error("Connector | Session timed out trying to resume");
|
||||
this._reset();
|
||||
this._state = "connecting";
|
||||
this.emit("invalidate");
|
||||
if (this._disconnecting) {
|
||||
this._reset();
|
||||
this.emit("close");
|
||||
} else {
|
||||
this._state = "connecting";
|
||||
this.emit("invalidate");
|
||||
}
|
||||
}
|
||||
logger.verbose(
|
||||
`Connector | waiting ${(Math.round(delay / 10) / 100)} seconds for reconnect`
|
||||
@@ -416,21 +560,15 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
|
||||
// Handle messages
|
||||
this._socket.onmessage = event => {
|
||||
let message = JSON.parse(event.data);
|
||||
let message = this._parseMessage(event.data);
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
if (["connecting", "resuming"].includes(this._state)) {
|
||||
this._processHandshake(message);
|
||||
|
||||
} else if (this._state === "connected") {
|
||||
if (message.seq !== null) {
|
||||
this._lastReceivedSeq = message.seq;
|
||||
}
|
||||
|
||||
if (message.type === "heartbeat") {
|
||||
this._processHeartbeat(message);
|
||||
|
||||
} else {
|
||||
this.emit("message", message);
|
||||
}
|
||||
this._processMessage(message);
|
||||
|
||||
} else {
|
||||
throw new Error(`Received message in unexpected state ${this._state}`);
|
||||
@@ -439,11 +577,6 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
}
|
||||
|
||||
_processHandshake(message) {
|
||||
if (!libSchema.serverHandshake(message)) {
|
||||
logger.warn("Connector | closing after received invalid handshake");
|
||||
this._socket.close(1002, "Invalid handshake");
|
||||
return;
|
||||
}
|
||||
|
||||
let { type, data } = message;
|
||||
if (type === "hello") {
|
||||
@@ -451,10 +584,9 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
this.emit("hello", data);
|
||||
if (this._sessionToken) {
|
||||
logger.verbose("Connector | Attempting resume");
|
||||
this.sendHandshake("resume", {
|
||||
session_token: this._sessionToken,
|
||||
last_seq: this._lastReceivedSeq,
|
||||
});
|
||||
this.sendHandshake(new libData.MessageResume(
|
||||
new libData.ResumeData(this._sessionToken, this._lastReceivedSeq)
|
||||
));
|
||||
} else {
|
||||
this.register();
|
||||
}
|
||||
@@ -462,39 +594,39 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
} else if (type === "ready") {
|
||||
logger.verbose("Connector | received ready from controller");
|
||||
this._state = "connected";
|
||||
this._sessionToken = data.session_token;
|
||||
this._sessionTimeout = data.session_timeout;
|
||||
this._heartbeatInterval = data.heartbeat_interval;
|
||||
this.src = data.src;
|
||||
this._sessionToken = data.sessionToken;
|
||||
this._sessionTimeout = data.sessionTimeout;
|
||||
this._heartbeatInterval = data.heartbeatInterval;
|
||||
this.startHeartbeat();
|
||||
for (let bufferedMessage of this._sendBuffer) {
|
||||
this._socket.send(JSON.stringify(bufferedMessage));
|
||||
this._sendInternal(bufferedMessage);
|
||||
}
|
||||
this.emit("connect", data);
|
||||
|
||||
} else if (type === "continue") {
|
||||
logger.info("Connector | resuming existing session");
|
||||
this._state = "connected";
|
||||
this._heartbeatInterval = data.heartbeat_interval;
|
||||
this._sessionTimeout = data.session_timeout;
|
||||
this._heartbeatInterval = data.heartbeatInterval;
|
||||
this._sessionTimeout = data.sessionTimeout;
|
||||
this.startHeartbeat();
|
||||
this._dropSendBufferSeq(data.last_seq);
|
||||
this._dropSendBufferSeq(data.lastSeq);
|
||||
for (let bufferedMessage of this._sendBuffer) {
|
||||
this._socket.send(JSON.stringify(bufferedMessage));
|
||||
this._sendInternal(bufferedMessage);
|
||||
}
|
||||
this._startedResuming = null;
|
||||
this.emit("resume");
|
||||
|
||||
} else if (type === "invalidate") {
|
||||
logger.warn("Connector | session invalidated by controller");
|
||||
this._state = "connecting";
|
||||
this._seq = 1;
|
||||
this._lastReceivedSeq = null;
|
||||
this._sessionToken = null;
|
||||
this._sessionTimeout = null;
|
||||
this._sendBuffer.length = 0;
|
||||
this._startedResuming = null;
|
||||
this.emit("invalidate");
|
||||
this.register();
|
||||
if (this._disconnecting) {
|
||||
this._reset();
|
||||
this.emit("close");
|
||||
} else {
|
||||
this._invalidate();
|
||||
this.emit("invalidate");
|
||||
this.register();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,12 +647,11 @@ class WebSocketClientConnector extends WebSocketBaseConnector {
|
||||
* @extends events.EventEmitter
|
||||
* @memberof module:lib/link
|
||||
*/
|
||||
class VirtualConnector extends events.EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
class VirtualConnector extends BaseConnector {
|
||||
constructor(src, dst) {
|
||||
super(src, dst);
|
||||
|
||||
this.other = this;
|
||||
this._seq = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -529,32 +660,30 @@ class VirtualConnector extends events.EventEmitter {
|
||||
* Creates two virtual connectors that are liked to each othes such that
|
||||
* a message sent on one is received by the other.
|
||||
*
|
||||
* @param {module:lib/data.Address} src - Source for obverse connector
|
||||
* @param {module:lib/data.Address} dst - destination for obverse connector
|
||||
* @returns {Array} two virtuarl connectors.
|
||||
*/
|
||||
static makePair() {
|
||||
let first = new this();
|
||||
let second = new this();
|
||||
first.other = second;
|
||||
second.other = first;
|
||||
return [first, second];
|
||||
static makePair(src, dst) {
|
||||
let obverse = new this(src, dst);
|
||||
let reverse = new this(dst, src);
|
||||
obverse.other = reverse;
|
||||
reverse.other = obverse;
|
||||
return [obverse, reverse];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the other end of the connector
|
||||
*
|
||||
* @param {string} type - Message type to send.
|
||||
* @param {Object} data - Data to send with message.
|
||||
* @returns {number} the sequence number of the message sent
|
||||
* @param {module:lib/data.Message} message - Message type to send.
|
||||
*/
|
||||
send(type, data) {
|
||||
let seq = this._seq;
|
||||
this._seq += 1;
|
||||
this.other.emit("message", { seq, type, data });
|
||||
return seq;
|
||||
send(message) {
|
||||
this.other.emit("message", message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BaseConnector,
|
||||
WebSocketBaseConnector,
|
||||
WebSocketClientConnector,
|
||||
VirtualConnector,
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
*/
|
||||
"use strict";
|
||||
const link = require("./link");
|
||||
const messages = require("./messages");
|
||||
const connectors = require("./connectors");
|
||||
|
||||
module.exports = {
|
||||
...link,
|
||||
...messages,
|
||||
...connectors,
|
||||
|
||||
// migrate: Allow info for plugins from before link refactor to load.
|
||||
Event: class Event {},
|
||||
Request: class Request {},
|
||||
};
|
||||
+584
-161
@@ -1,9 +1,11 @@
|
||||
// Implementation of Link class
|
||||
"use strict";
|
||||
|
||||
const libSchema = require("../schema");
|
||||
const libData = require("../data");
|
||||
const libErrors = require("../errors");
|
||||
const { logger } = require("../logging");
|
||||
const libSchema = require("../schema");
|
||||
const { dataClasses } = require("./messages");
|
||||
|
||||
// Some definitions for the terminology used here:
|
||||
// link: Either side of a controller - client connection
|
||||
@@ -11,8 +13,6 @@ const { logger } = require("../logging");
|
||||
// connection: the controller side of a link
|
||||
// client: the side that is not the controller of a link
|
||||
// message: the complete object sent using the 'message' event
|
||||
// data: the data property of a message, essentially the payload.
|
||||
// type: the type property of a message, identifies what to expect in data.
|
||||
|
||||
/**
|
||||
* Common interface for server and client connections
|
||||
@@ -20,222 +20,334 @@ const { logger } = require("../logging");
|
||||
* @memberof module:lib/link
|
||||
*/
|
||||
class Link {
|
||||
constructor(source, target, connector) {
|
||||
this.source = source;
|
||||
this.target = target;
|
||||
constructor(connector) {
|
||||
this.connector = connector;
|
||||
this.router = undefined;
|
||||
this.validateSent = true;
|
||||
|
||||
this._waiters = new Map();
|
||||
this._handlers = new Map();
|
||||
this._validators = new Map();
|
||||
this._requestHandlers = new Map();
|
||||
this._requestFallbacks = new Map();
|
||||
this._eventHandlers = new Map();
|
||||
this._eventSnoopers = new Map();
|
||||
this._pendingRequests = new Map();
|
||||
this._forwardedRequests = new Map();
|
||||
this._nextRequestId = 1;
|
||||
|
||||
this.handle(libData.PingRequest, () => {});
|
||||
|
||||
// Process messages received by the connector
|
||||
connector.on("message", payload => {
|
||||
try {
|
||||
this.processMessage(payload);
|
||||
this._processMessage(payload);
|
||||
} catch (err) {
|
||||
if (err instanceof libErrors.InvalidMessage) {
|
||||
logger.error(`Invalid message on ${this.source}-${this.target} link: ${err.message}`);
|
||||
logger.error(err.message);
|
||||
if (err.errors) {
|
||||
logger.error(JSON.stringify(err.errors, null, 4));
|
||||
}
|
||||
|
||||
// Send back an error response if this was a request.
|
||||
if (
|
||||
typeof payload.type === "string"
|
||||
&& payload.type.endsWith("_request")
|
||||
&& Number.isInteger(payload.seq)
|
||||
) {
|
||||
this.connector.send(
|
||||
`${payload.type.slice(0, -8)}_response`,
|
||||
{ seq: payload.seq, error: err.message }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Unexpected error, bubble back to connector
|
||||
this.connector.emit("error", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connector.on("invalidate", () => {
|
||||
for (let waiterType of this._waiters.values()) {
|
||||
for (let waiter of waiterType) {
|
||||
waiter.reject(new libErrors.SessionLost("Session Lost"));
|
||||
connector.on("disconnectPrepare", () => {
|
||||
this.prepareDisconnect().catch(
|
||||
err => {
|
||||
logger.error(`Unexpected error preparing disconnect:\n${err.stack}`);
|
||||
}
|
||||
}
|
||||
).finally(
|
||||
() => {
|
||||
this.connector.send(new libData.MessageDisconnect("ready"));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
this._waiters.clear();
|
||||
connector.on("invalidate", () => {
|
||||
this._clearPendingRequests(new libErrors.SessionLost("Session Lost"));
|
||||
});
|
||||
|
||||
connector.on("close", () => {
|
||||
for (let waiterType of this._waiters.values()) {
|
||||
for (let waiter of waiterType) {
|
||||
waiter.reject(new libErrors.SessionLost("Session Closed"));
|
||||
}
|
||||
}
|
||||
|
||||
this._waiters.clear();
|
||||
this._clearPendingRequests(new libErrors.SessionLost("Session Closed"));
|
||||
});
|
||||
}
|
||||
|
||||
_clearPendingRequests(err) {
|
||||
for (let pending of this._pendingRequests.values()) {
|
||||
pending.reject(err);
|
||||
}
|
||||
for (let pending of this._forwardedRequests.values()) {
|
||||
if (pending.origin.connector.hasSession) {
|
||||
pending.origin.connector.sendResponseError(
|
||||
new libData.ResponseError(err.message, err.code), pending.src, pending.dst
|
||||
);
|
||||
}
|
||||
}
|
||||
this._pendingRequests.clear();
|
||||
this._forwardedRequests.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a received message on the link
|
||||
*
|
||||
* Validates and invokes the handler and/or waiters for a message that has
|
||||
* been received. An unhandled message is considered to be an error.
|
||||
*
|
||||
* @param {Object} message - Message to process.
|
||||
* @throws {module:lib/errors.InvalidMessage}
|
||||
* if validation failed, is missing, the message is invalid, or no
|
||||
* handlers/waiters were invoked to handle the message.
|
||||
* @param {module:lib/data.Message} message - Message to process.
|
||||
* @throws {module:lib/errors.InvalidMessage} if the message is invalid or not handled.
|
||||
*/
|
||||
processMessage(message) {
|
||||
if (!libSchema.message(message)) {
|
||||
throw new libErrors.InvalidMessage("Malformed", libSchema.message.errors);
|
||||
_processMessage(message) {
|
||||
if (!["request", "response", "responseError", "event"].includes(message.type)) {
|
||||
throw new libErrors.InvalidMessage(`Unhandled message type ${message.type}`);
|
||||
}
|
||||
|
||||
let validator = this._validators.get(message.type);
|
||||
if (!validator) {
|
||||
throw new libErrors.InvalidMessage(`No validator for ${message.type} on ${this.source}-${this.target}`);
|
||||
let entry = this._validateMessage(message);
|
||||
if (entry && this.connector.dst.type === libData.Address.control) {
|
||||
try {
|
||||
this.validatePermission(message, entry); // Somewhat hacky, defined in ControlConnection
|
||||
} catch (err) {
|
||||
if (err instanceof libErrors.PermissionError) {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (!validator(message)) {
|
||||
throw new libErrors.InvalidMessage(`Validation failed for ${message.type}`, validator.errors);
|
||||
if (message.type === "event" && this._eventSnoopers.has(entry.Event)) {
|
||||
let handler = this._eventSnoopers.get(entry.Event);
|
||||
handler(entry.eventFromJSON(message.data), message.src, message.dst).catch(err => {
|
||||
logger.error(`Unexpected error snooping ${message.name}:\n${err.stack}`);
|
||||
});
|
||||
}
|
||||
|
||||
let hadHandlers = this._processHandler(message);
|
||||
let hadWaiters = this._processWaiters(message);
|
||||
if (!message.dst.addressedTo(this.connector.src)) {
|
||||
if (message.type === "response" || message.type === "responseError") {
|
||||
this._forwardedRequests.delete(message.dst.index());
|
||||
}
|
||||
this._routeMessage(message, entry);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hadWaiters && !hadHandlers) {
|
||||
// XXX console.error(`Unhandled message: ${JSON.stringify(message, null, 4)}`);
|
||||
throw new libErrors.InvalidMessage(`Unhandled message ${message.type}`);
|
||||
if (message.type === "request") {
|
||||
this._processRequest(message, entry, this._requestHandlers.get(entry.Request));
|
||||
|
||||
} else if (message.type === "response") {
|
||||
this._processResponse(message);
|
||||
|
||||
} else if (message.type === "responseError") {
|
||||
this._processResponseError(message);
|
||||
|
||||
} else if (message.type === "event") {
|
||||
this._processEvent(message, entry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the handler for this message if it exists
|
||||
* Ingress message validation
|
||||
*
|
||||
* @param {Object} message - Message to invoke handlers on.
|
||||
* @returns {Boolean} true if a handler was invoked.
|
||||
* Should be overridden by sub-classes to validate messages received.
|
||||
*
|
||||
* @param {module:lib/data.Message} message - Message to check.
|
||||
* @throws {module:lib/errors.InvalidMessage} if the message is invalid.
|
||||
*/
|
||||
_processHandler(message) {
|
||||
let handler = this._handlers.get(message.type);
|
||||
if (handler) {
|
||||
handler(message);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
validateIngress(message) { }
|
||||
|
||||
/**
|
||||
* Invokes the waiters for this message if any
|
||||
*
|
||||
* @param {Object} message - Message to invoke waiters on.
|
||||
* @returns {Boolean} true if a waiter was triggered.
|
||||
*/
|
||||
_processWaiters(message) {
|
||||
let { type, data } = message;
|
||||
let waiters = this._waiters.get(type);
|
||||
if (!waiters || !waiters.length) {
|
||||
return false;
|
||||
_validateMessage(message) {
|
||||
try {
|
||||
if (message.src.type === libData.Address.broadcast) {
|
||||
throw new libErrors.InvalidMessage("Message src may not be broadcast");
|
||||
}
|
||||
this.validateIngress(message);
|
||||
} catch (err) {
|
||||
if (message.type === "request") {
|
||||
this.connector.sendResponseError(new libData.ResponseError(err.message, err.code), message.src);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
let matched = [];
|
||||
waitersLoop: for (let index = 0; index < waiters.length; index++) {
|
||||
let waiter = waiters[index];
|
||||
for (let [key, value] of Object.entries(waiter.data)) {
|
||||
if (data[key] !== value) {
|
||||
continue waitersLoop;
|
||||
if (message.type === "request") {
|
||||
let entry = this.constructor._requestsByName.get(message.name);
|
||||
if (!entry) {
|
||||
let err = new libErrors.InvalidMessage(`Unrecognized request ${message.name}`);
|
||||
this.connector.sendResponseError(new libData.ResponseError(err.message, err.code), message.src);
|
||||
throw err;
|
||||
}
|
||||
if (!entry.allowedSrcTypes.has(message.src.type)) {
|
||||
let err = new libErrors.InvalidMessage(`Source ${message.src} is not allowed for ${message.name}`);
|
||||
this.connector.sendResponseError(new libData.ResponseError(err.message, err.code), message.src);
|
||||
throw err;
|
||||
}
|
||||
if (!entry.allowedDstTypes.has(message.dst.type)) {
|
||||
let err = new libErrors.InvalidMessage(`Destination ${message.dst} is not allowed for ${message.name}`);
|
||||
this.connector.sendResponseError(new libData.ResponseError(err.message, err.code), message.src);
|
||||
throw err;
|
||||
}
|
||||
return entry;
|
||||
|
||||
} else if (message.type === "response") {
|
||||
return undefined;
|
||||
|
||||
} else if (message.type === "responseError") {
|
||||
return undefined;
|
||||
|
||||
} else if (message.type === "event") {
|
||||
let entry = this.constructor._eventsByName.get(message.name);
|
||||
if (!entry) {
|
||||
throw new libErrors.InvalidMessage(`Unrecognized event ${message.name}`);
|
||||
}
|
||||
if (!entry.allowedSrcTypes.has(message.src.type)) {
|
||||
throw new libErrors.InvalidMessage(`Source ${message.src} is not allowed for ${message.name}`);
|
||||
}
|
||||
if (message.dst.type === libData.Address.broadcast) {
|
||||
if (!entry.allowedDstTypes.has(message.dst.id)) {
|
||||
throw new libErrors.InvalidMessage(`Destination ${message.dst} is not allowed for ${message.name}`);
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line no-lonely-if
|
||||
if (!entry.allowedDstTypes.has(message.dst.type)) {
|
||||
throw new libErrors.InvalidMessage(`Destination ${message.dst} is not allowed for ${message.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
waiter.resolve(message);
|
||||
matched.push(index);
|
||||
return entry;
|
||||
}
|
||||
|
||||
matched.reverse();
|
||||
for (let index of matched) {
|
||||
waiters.splice(index, 1);
|
||||
}
|
||||
|
||||
return matched.length > 0;
|
||||
throw new Error("Should be unreachable");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set handler for a message type
|
||||
* Message permission validation
|
||||
*
|
||||
* Set function called upon receiving a message of the given type.
|
||||
* Called when an request or event is received by a control connector.
|
||||
* Should be overridden by sub-classes to validate messages received.
|
||||
*
|
||||
* @param {string} type - Type of message to listen to
|
||||
* @param {function} handler -
|
||||
* Callack to invoke on messages of the given type.
|
||||
* @param {function} validator -
|
||||
* Function validating the message have the correct format.
|
||||
* @param {module:lib/data.Message} message - Message to check.
|
||||
* @param {object} entry - Request or Event entry for this Message.
|
||||
* @throws {module:lib/errors.PermissionError} if unauthorized.
|
||||
*/
|
||||
setHandler(type, handler, validator) {
|
||||
if (this._handlers.has(type)) {
|
||||
throw new Error(`${type} already has a handler`);
|
||||
}
|
||||
validatePermission(message, entry) { }
|
||||
|
||||
if (!validator) {
|
||||
throw new Error("validator is required");
|
||||
}
|
||||
|
||||
this.setValidator(type, validator);
|
||||
this._handlers.set(type, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set validator for a message type
|
||||
*
|
||||
* Set function called upon receiving a message of the given type in
|
||||
* order to validate the message. The function should return true if
|
||||
* the message is valid and false otherwise.
|
||||
*
|
||||
* @param {string} type - Type of message to validate
|
||||
* @param {function} validator - Validation function.
|
||||
*/
|
||||
setValidator(type, validator) {
|
||||
if (this._validators.has(type)) {
|
||||
throw new Error(`${type} already has a validator`);
|
||||
}
|
||||
|
||||
this._validators.set(type, validator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a message matching given type and data
|
||||
*
|
||||
* Waits for a message over the link that matches the type given as well
|
||||
* as any properties specified in data. For example specifying { foo:
|
||||
* 21 } as the data will cause it to wait until a message with the right
|
||||
* type and a foo property value of 21 in the data payload.
|
||||
*
|
||||
* @param {string} type - Message type to wait for.
|
||||
* @param {Object} data - Properties to match in the message data.
|
||||
*/
|
||||
async waitFor(type, data) {
|
||||
if (!this._validators.has(type)) {
|
||||
throw new Error(`No validator for ${type} on ${this.source}-${this.target}`);
|
||||
}
|
||||
|
||||
let waiter = { data };
|
||||
waiter.promise = new Promise((resolve, reject) => {
|
||||
waiter.resolve = resolve;
|
||||
waiter.reject = reject;
|
||||
if (!this._waiters.has(type)) {
|
||||
this._waiters.set(type, [waiter]);
|
||||
} else {
|
||||
this._waiters.get(type).push(waiter);
|
||||
_routeMessage(message, entry) {
|
||||
if (!this.router) {
|
||||
let err = new libErrors.InvalidMessage(
|
||||
`Received message addressed to ${message.dst} but this link does not route messages`
|
||||
);
|
||||
if (message.type === "request") {
|
||||
this.connector.sendResponseError(
|
||||
new libData.ResponseError(err.message, err.code), message.src
|
||||
);
|
||||
}
|
||||
});
|
||||
return waiter.promise;
|
||||
throw err;
|
||||
}
|
||||
|
||||
let fallback = message.type === "request" ? this._requestFallbacks.get(entry.Request) : undefined;
|
||||
if (this.router.forwardMessage(this, message, Boolean(fallback))) {
|
||||
return;
|
||||
}
|
||||
if (!fallback) {
|
||||
throw new Error("Router requested fallback handling when fallback is unavailable");
|
||||
}
|
||||
if (message.type !== "request") {
|
||||
throw new Error(`Router requested fallback handling of unsupported message type ${message.type}`);
|
||||
}
|
||||
this._processRequest(message, entry, fallback, message.dst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ping requests
|
||||
*/
|
||||
async pingRequestHandler() { }
|
||||
_processRequest(message, entry, handler, spoofedSrc) {
|
||||
if (!handler) {
|
||||
this.connector.sendResponseError(
|
||||
new libData.ResponseError(`No handler for ${entry.Request.name}`), message.src
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = handler(entry.requestFromJSON(message.data), message.src, message.dst);
|
||||
} catch (err) {
|
||||
if (err.errors) {
|
||||
logger.error(JSON.stringify(err.errors, null, 4));
|
||||
}
|
||||
this.connector.sendResponseError(
|
||||
new libData.ResponseError(err.message, err.code, err.stack), message.src
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(response instanceof Promise)) {
|
||||
response = Promise.resolve(response);
|
||||
}
|
||||
|
||||
response.then(
|
||||
result => {
|
||||
if (this.validateSent) {
|
||||
if (entry.Response) {
|
||||
entry.responseFromJSON(JSON.parse(JSON.stringify(result)));
|
||||
} else if (result !== undefined) {
|
||||
throw new Error(`Expected empty response from ${entry.Request.name} handler`);
|
||||
}
|
||||
}
|
||||
this.connector.sendResponse(result, message.src, spoofedSrc);
|
||||
}
|
||||
).catch(
|
||||
err => {
|
||||
if (err instanceof libErrors.InvalidMessage) {
|
||||
logger.error(err.message);
|
||||
if (err.errors) {
|
||||
logger.error(JSON.stringify(err.errors, null, 4));
|
||||
}
|
||||
} else if (!(err instanceof libErrors.RequestError)) {
|
||||
logger.error(`Unexpected error responding to ${message.name}:\n${err.stack}`);
|
||||
}
|
||||
this.connector.sendResponseError(
|
||||
new libData.ResponseError(err.message, err.code, err.stack), message.src, spoofedSrc
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_processResponse(message) {
|
||||
let pending = this._pendingRequests.get(message.dst.requestId);
|
||||
if (!pending) {
|
||||
throw new libErrors.InvalidMessage(
|
||||
`Received response ${message.dst.requestId} without a pending request`
|
||||
);
|
||||
}
|
||||
|
||||
if (!pending.dst.equals(message.src)) {
|
||||
throw new libErrors.InvalidMessage(`Received reply from ${message.src} for message sent to ${pending.dst}`);
|
||||
}
|
||||
|
||||
try {
|
||||
pending.resolve(pending.request.responseFromJSON(message.data));
|
||||
} catch (err) {
|
||||
// An invalid response object was likely received
|
||||
pending.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
_processResponseError(message) {
|
||||
let pending = this._pendingRequests.get(message.dst.requestId);
|
||||
if (!pending) {
|
||||
throw new libErrors.InvalidMessage(
|
||||
`Received error response ${message.dst.requestId} without a pending request`
|
||||
);
|
||||
}
|
||||
|
||||
pending.reject(new libErrors.RequestError(message.data.message, message.data.code, message.data.stack));
|
||||
}
|
||||
|
||||
_processEvent(message, entry) {
|
||||
let handler = this._eventHandlers.get(entry.Event);
|
||||
if (!handler) {
|
||||
throw new libErrors.InvalidMessage(`Unhandled event ${message.name}`);
|
||||
}
|
||||
|
||||
handler(entry.eventFromJSON(message.data), message.src, message.dst).catch(err => {
|
||||
logger.error(`Unexpected error handling ${message.name}:\n${err.stack}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare connection for disconnect
|
||||
@@ -245,19 +357,330 @@ class Link {
|
||||
* additional requests to be sent out before calling the super class
|
||||
* method.
|
||||
*/
|
||||
async prepareDisconnectRequestHandler() {
|
||||
async prepareDisconnect() {
|
||||
let promises = [];
|
||||
for (let waiterType of this._waiters.values()) {
|
||||
for (let waiter of waiterType) {
|
||||
promises.push(waiter.promise.then(() => {}, () => {}));
|
||||
}
|
||||
for (let pending of this._pendingRequests.values()) {
|
||||
promises.push(pending.promise.then(() => {}, () => {}));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request or event to the other side of this link
|
||||
* @param {*} requestOrEvent - Request or event to send
|
||||
* @returns {Promise<*>|undefined}
|
||||
* Promise that resolves to the response if a request was sent or
|
||||
* undefined if it was an event.
|
||||
*/
|
||||
send(requestOrEvent) {
|
||||
return this.sendTo(this.connector.dst, requestOrEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request or event to the given address
|
||||
*
|
||||
* @param {*|module:lib/data.Address} destination - Where to send it
|
||||
* @param {*} requestOrEvent - Request or event to send
|
||||
* @returns {Promise<*>|undefined}
|
||||
* Promise that resolves to the response if a request was sent or
|
||||
* undefined if it was an event.
|
||||
*/
|
||||
sendTo(destination, requestOrEvent) {
|
||||
let dst = libData.Address.fromShorthand(destination);
|
||||
|
||||
if (requestOrEvent.constructor.type === "request") {
|
||||
return this.sendRequest(requestOrEvent, dst);
|
||||
}
|
||||
if (requestOrEvent.constructor.type === "event") {
|
||||
return this.sendEvent(requestOrEvent, dst);
|
||||
}
|
||||
throw Error(`Expected request or event but got type ${requestOrEvent.constructor.type}`);
|
||||
}
|
||||
|
||||
sendRequest(request, dst) {
|
||||
let entry = this.constructor._requestsByClass.get(request.constructor);
|
||||
if (!entry) {
|
||||
throw new Error(`Attempt to send unregistered Request ${request.constructor.name}`);
|
||||
}
|
||||
if (this.validateSent) {
|
||||
entry.requestFromJSON(JSON.parse(JSON.stringify(request)));
|
||||
}
|
||||
|
||||
let pending = {
|
||||
request: entry,
|
||||
dst,
|
||||
};
|
||||
pending.promise = new Promise((resolve, reject) => {
|
||||
pending.resolve = resolve;
|
||||
pending.reject = reject;
|
||||
});
|
||||
let requestId = this._nextRequestId;
|
||||
this._nextRequestId += 1;
|
||||
this._pendingRequests.set(requestId, pending);
|
||||
this.connector.sendRequest(request, requestId, dst);
|
||||
return pending.promise;
|
||||
}
|
||||
|
||||
forwardRequest(message, origin) {
|
||||
if (this.validateSent) {
|
||||
let entry = this.constructor._requestsByName.get(message.name);
|
||||
if (!entry) {
|
||||
throw new Error(`Attempt to forward unregistered Request ${message.name}`);
|
||||
}
|
||||
entry.requestFromJSON(message.data);
|
||||
}
|
||||
|
||||
let pending = {
|
||||
origin,
|
||||
src: message.src,
|
||||
dst: message.dst,
|
||||
};
|
||||
this._forwardedRequests.set(message.src.index(), pending);
|
||||
this.connector.send(message);
|
||||
}
|
||||
|
||||
sendEvent(event, dst) {
|
||||
let entry = this.constructor._eventsByClass.get(event.constructor);
|
||||
if (!entry) {
|
||||
throw new Error(`Attempt to send unregistered Event ${event.constructor.name}`);
|
||||
}
|
||||
if (this.validateSent) {
|
||||
entry.eventFromJSON(JSON.parse(JSON.stringify(event)));
|
||||
}
|
||||
|
||||
this.connector.sendEvent(event, dst);
|
||||
}
|
||||
|
||||
handle(Class, handler) {
|
||||
if (Class.type === "request") {
|
||||
this.handleRequest(Class, handler);
|
||||
} else if (Class.type === "event") {
|
||||
this.handleEvent(Class, handler);
|
||||
} else {
|
||||
throw new Error(`Class ${Class.name} has unrecognized type ${Class.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
handleRequest(Request, handler) {
|
||||
let entry = this.constructor._requestsByClass.get(Request);
|
||||
if (!entry) {
|
||||
throw new Error(`Unregistered Request class ${Request.name}`);
|
||||
}
|
||||
if (this._requestHandlers.has(Request)) {
|
||||
throw new Error(`Request ${entry.name} is already registered`);
|
||||
}
|
||||
this._requestHandlers.set(Request, handler);
|
||||
}
|
||||
|
||||
fallbackRequest(Request, handler) {
|
||||
let entry = this.constructor._requestsByClass.get(Request);
|
||||
if (!entry) {
|
||||
throw new Error(`Unregistered Request class ${Request.name}`);
|
||||
}
|
||||
if (this._requestFallbacks.has(Request)) {
|
||||
throw new Error(`Request ${entry.name} is already fallbacked`);
|
||||
}
|
||||
this._requestFallbacks.set(Request, handler);
|
||||
}
|
||||
|
||||
handleEvent(Event, handler) {
|
||||
let entry = this.constructor._eventsByClass.get(Event);
|
||||
if (!entry) {
|
||||
throw new Error(`Unregistered Event class ${Event.name}`);
|
||||
}
|
||||
if (this._eventHandlers.has(Event)) {
|
||||
throw new Error(`Event ${entry.name} is already registered`);
|
||||
}
|
||||
this._eventHandlers.set(Event, handler);
|
||||
}
|
||||
|
||||
snoopEvent(Event, handler) {
|
||||
let entry = this.constructor._eventsByClass.get(Event);
|
||||
if (!entry) {
|
||||
throw new Error(`Unregistered Event class ${Event.name}`);
|
||||
}
|
||||
if (this._eventSnoopers.has(Event)) {
|
||||
throw new Error(`Event ${entry.name} is already snooped`);
|
||||
}
|
||||
this._eventSnoopers.set(Event, handler);
|
||||
}
|
||||
|
||||
static register(Class) {
|
||||
if (Class.type === "request") {
|
||||
this.registerRequest(Class);
|
||||
} else if (Class.type === "event") {
|
||||
this.registerEvent(Class);
|
||||
} else {
|
||||
throw new Error(`Data class ${Class.name} has unknown type ${Class.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
static requestFromJSON(Request, name) {
|
||||
if (Request.fromJSON) {
|
||||
if (!Request.jsonSchema) {
|
||||
throw new Error(`Request ${name} has static fromJSON but is missing static jsonSchema`);
|
||||
}
|
||||
let validate = libSchema.compile(Request.jsonSchema);
|
||||
return (json) => {
|
||||
if (!validate(json)) {
|
||||
throw new libErrors.InvalidMessage(`Request ${name} failed validation`, validate.errors);
|
||||
}
|
||||
return Request.fromJSON(json);
|
||||
};
|
||||
}
|
||||
if (Request.jsonSchema) {
|
||||
throw new Error(`Request ${name} has static jsonSchema but is missing static fromJSON`);
|
||||
}
|
||||
return () => new Request();
|
||||
}
|
||||
|
||||
static responseFromJSON(Response, name) {
|
||||
if (!Response.jsonSchema) {
|
||||
throw new Error(`Response for Request ${name} is missing static jsonSchema`);
|
||||
}
|
||||
if (!Response.fromJSON) {
|
||||
throw new Error(`Response for Request ${name} is missing static fromJSON`);
|
||||
}
|
||||
|
||||
let validate = libSchema.compile(Response.jsonSchema);
|
||||
return (json) => {
|
||||
if (!validate(json)) {
|
||||
throw new libErrors.InvalidMessage(
|
||||
`Response for request ${name} failed validation`, validate.errors
|
||||
);
|
||||
}
|
||||
return Response.fromJSON(json);
|
||||
};
|
||||
}
|
||||
|
||||
static allowedTypes(types, name, side) {
|
||||
if (types === undefined) {
|
||||
throw new Error(`Missing ${side} specification in ${name}`);
|
||||
}
|
||||
|
||||
if (!(types instanceof Array)) {
|
||||
types = [types];
|
||||
}
|
||||
|
||||
let allowed = new Set();
|
||||
for (let type of types) {
|
||||
let id = libData.Address[type];
|
||||
if (typeof id !== "number") {
|
||||
throw new Error(`Invalid type ${type} in ${side} specification of ${name}`);
|
||||
}
|
||||
if (id === libData.Address.broadcast) {
|
||||
throw new Error(`${side} specification may not use the broadcast type in ${name}`);
|
||||
}
|
||||
allowed.add(id);
|
||||
}
|
||||
return allowed;
|
||||
}
|
||||
|
||||
static _requestsByName = new Map();
|
||||
static _requestsByClass = new Map();
|
||||
|
||||
static registerRequest(Request) {
|
||||
const name = Request.plugin ? `${Request.plugin}:${Request.name}` : Request.name;
|
||||
if (this._requestsByName.has(name)) {
|
||||
throw new Error(`Request ${name} is already registered`);
|
||||
}
|
||||
|
||||
let entry = {
|
||||
Request,
|
||||
name,
|
||||
requestFromJSON: this.requestFromJSON(Request, name),
|
||||
allowedSrcTypes: this.allowedTypes(Request.src, name, "src"),
|
||||
allowedDstTypes: this.allowedTypes(Request.dst, name, "dst"),
|
||||
};
|
||||
|
||||
if (
|
||||
entry.allowedSrcTypes.has(libData.Address.control)
|
||||
&& !(Request.permission === null || ["function", "string"].includes(typeof Request.permission))
|
||||
) {
|
||||
throw new Error(`Invalid permission specification ${typeof Request.permission} on ${name}`);
|
||||
}
|
||||
|
||||
let Response = Request.Response;
|
||||
if (Response) {
|
||||
entry.Response = Response;
|
||||
entry.responseFromJSON = this.responseFromJSON(Response, name);
|
||||
} else {
|
||||
entry.responseFromJSON = () => undefined;
|
||||
}
|
||||
|
||||
this._requestsByName.set(name, entry);
|
||||
this._requestsByClass.set(Request, entry);
|
||||
}
|
||||
|
||||
static eventFromJSON(Event, name) {
|
||||
if (Event.fromJSON) {
|
||||
if (!Event.jsonSchema) {
|
||||
throw new Error(`Event ${name} has static fromJSON but is missing static jsonSchema`);
|
||||
}
|
||||
let validate = libSchema.compile(Event.jsonSchema);
|
||||
return (json) => {
|
||||
if (!validate(json)) {
|
||||
throw new libErrors.InvalidMessage(`Event ${name} failed validation`, validate.errors);
|
||||
}
|
||||
return Event.fromJSON(json);
|
||||
};
|
||||
}
|
||||
if (Event.jsonSchema) {
|
||||
throw new Error(`Event ${name} has static jsonSchema but is missing static fromJSON`);
|
||||
}
|
||||
return () => new Event();
|
||||
}
|
||||
|
||||
static _eventsByName = new Map();
|
||||
static _eventsByClass = new Map();
|
||||
|
||||
static registerEvent(Event) {
|
||||
const name = Event.plugin ? `${Event.plugin}:${Event.name}` : Event.name;
|
||||
if (this._eventsByName.has(name)) {
|
||||
throw new Error(`Event ${name} is already registered`);
|
||||
}
|
||||
|
||||
let entry = {
|
||||
Event,
|
||||
name,
|
||||
eventFromJSON: this.eventFromJSON(Event, name),
|
||||
allowedSrcTypes: this.allowedTypes(Event.src, name, "src"),
|
||||
allowedDstTypes: this.allowedTypes(Event.dst, name, "dst"),
|
||||
};
|
||||
|
||||
if (
|
||||
entry.allowedSrcTypes.has(libData.Address.control)
|
||||
&& !(Event.permission === null || ["function", "string"].includes(typeof Event.permission))
|
||||
) {
|
||||
throw new Error(`Invalid permission specification ${typeof Event.permission} on ${name}`);
|
||||
}
|
||||
|
||||
this._eventsByName.set(name, entry);
|
||||
this._eventsByClass.set(Event, entry);
|
||||
}
|
||||
}
|
||||
|
||||
for (let Class of dataClasses) {
|
||||
Link.register(Class);
|
||||
}
|
||||
|
||||
function registerPluginMessages(pluginInfos) {
|
||||
for (let pluginInfo of pluginInfos) {
|
||||
for (let Class of pluginInfo.messages || []) {
|
||||
if (Class.plugin !== pluginInfo.name) {
|
||||
throw new Error(
|
||||
`Expected message ${Class.name} from ${pluginInfo.name} to have a static ` +
|
||||
`plugin property set to "${pluginInfo.name}"`
|
||||
);
|
||||
}
|
||||
Link.register(Class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
Link,
|
||||
registerPluginMessages,
|
||||
};
|
||||
+110
-1582
File diff suppressed because it is too large.
Load diff
@@ -85,16 +85,16 @@ class WebConsoleFormat {
|
||||
/**
|
||||
* Filter object for logs
|
||||
* @typedef {Object} module:lib/logging~LogFilter
|
||||
* @property {string} [max_level] -
|
||||
* @property {string} [maxLevel] -
|
||||
* Maximum log level to include. Higher levels are more verbose.
|
||||
* @property {boolean} [all] -
|
||||
* Include log entries from controller, all hosts and all instances.
|
||||
* @property {boolean} [controller] -
|
||||
* Include log entries from the controller.
|
||||
* @property {Array<number>} [host_ids] -
|
||||
* @property {Array<number>} [hostIds] -
|
||||
* Include log entries for the given hosts and instances of those
|
||||
* hosts by id.
|
||||
* @property {Array<number>} [instance_ids] -
|
||||
* @property {Array<number>} [instanceIds] -
|
||||
* Include log entries for the given instances by id.
|
||||
*/
|
||||
|
||||
@@ -107,10 +107,10 @@ class WebConsoleFormat {
|
||||
* filter returning true for log entries that match it.
|
||||
* @static
|
||||
*/
|
||||
function logFilter({ all, controller, host_ids, instance_ids, max_level }) {
|
||||
function logFilter({ all, controller, hostIds, instanceIds, maxLevel }) {
|
||||
return info => {
|
||||
// Note: reversed to filter out undefined levels
|
||||
if (max_level && !(levels[info.level] <= levels[max_level])) {
|
||||
if (maxLevel && !(levels[info.level] <= levels[maxLevel])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -121,14 +121,14 @@ function logFilter({ all, controller, host_ids, instance_ids, max_level }) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
host_ids
|
||||
hostIds
|
||||
&& info.host_id !== undefined
|
||||
&& info.instance_id === undefined
|
||||
&& host_ids.includes(info.host_id)
|
||||
&& hostIds.includes(info.host_id)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (instance_ids && info.instance_id !== undefined && instance_ids.includes(info.instance_id)) {
|
||||
if (instanceIds && info.instance_id !== undefined && instanceIds.includes(info.instance_id)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -15,7 +15,7 @@ const Transport = require("winston-transport");
|
||||
const { LEVEL, MESSAGE } = require("triple-beam");
|
||||
const chalk = require("chalk");
|
||||
|
||||
const libLink = require("./link");
|
||||
const libData = require("./data");
|
||||
const libErrors = require("./errors");
|
||||
const libFileOps = require("./file_ops");
|
||||
const { levels, logFilter, logger } = require("./logging");
|
||||
@@ -128,7 +128,7 @@ class LinkTransport extends Transport {
|
||||
}
|
||||
|
||||
try {
|
||||
libLink.messages.logMessage.send(this.link, { info });
|
||||
this.link.send(new libData.LogMessageEvent(info));
|
||||
} catch (err) {
|
||||
// Ignore session lost errors.
|
||||
if (!(err instanceof libErrors.SessionLost)) {
|
||||
@@ -140,7 +140,7 @@ class LinkTransport extends Transport {
|
||||
}
|
||||
|
||||
const logFileGlob = /^[a-z]+-(\d{4}-\d{2}-\d{2})\.log$/;
|
||||
const logIndexVersion = 1;
|
||||
const logIndexVersion = 2;
|
||||
|
||||
/**
|
||||
* Keeps an index over a log directory to speed up queries to it
|
||||
@@ -159,8 +159,8 @@ class LogIndex {
|
||||
this.index.set(file, {
|
||||
levels: new Set(serializedEntry.levels),
|
||||
controller: serializedEntry.controller,
|
||||
host_ids: new Set(serializedEntry.host_ids),
|
||||
instance_ids: new Set(serializedEntry.instance_ids),
|
||||
hostIds: new Set(serializedEntry.hostIds),
|
||||
instanceIds: new Set(serializedEntry.instanceIds),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -175,8 +175,8 @@ class LogIndex {
|
||||
serialized.files[file] = {
|
||||
levels: [...entry.levels],
|
||||
controller: entry.controller,
|
||||
host_ids: [...entry.host_ids],
|
||||
instance_ids: [...entry.instance_ids],
|
||||
hostIds: [...entry.hostIds],
|
||||
instanceIds: [...entry.instanceIds],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -226,8 +226,8 @@ class LogIndex {
|
||||
let entry = {
|
||||
levels: new Set(),
|
||||
controller: false,
|
||||
host_ids: new Set(),
|
||||
instance_ids: new Set(),
|
||||
hostIds: new Set(),
|
||||
instanceIds: new Set(),
|
||||
};
|
||||
for await (let line of lineStream) {
|
||||
let info;
|
||||
@@ -244,10 +244,10 @@ class LogIndex {
|
||||
if (info.host_id === undefined) {
|
||||
entry.controller = true;
|
||||
} else if (info.instance_id === undefined) {
|
||||
entry.host_ids.add(info.host_id);
|
||||
entry.hostIds.add(info.host_id);
|
||||
}
|
||||
if (info.instance_id !== undefined) {
|
||||
entry.instance_ids.add(info.instance_id);
|
||||
entry.instanceIds.add(info.instance_id);
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
@@ -280,18 +280,18 @@ class LogIndex {
|
||||
* @returns {boolean}
|
||||
* true if the file may contain entries included by the filter.
|
||||
*/
|
||||
filterIncludesFile(file, { max_level, all, controller, instance_ids, host_ids }) {
|
||||
filterIncludesFile(file, { maxLevel, all, controller, instanceIds, hostIds }) {
|
||||
let entry = this.index.get(file);
|
||||
if (!entry) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
if (max_level && hasOwnProperty.call(levels, max_level)) {
|
||||
if (maxLevel && hasOwnProperty.call(levels, maxLevel)) {
|
||||
let fileLevels = [...entry.levels].map(
|
||||
fileLevel => (hasOwnProperty.call(levels, fileLevel) ? levels[fileLevel] : levels.verbose)
|
||||
);
|
||||
if (fileLevels.every(level => level > levels[max_level])) {
|
||||
if (fileLevels.every(level => level > levels[maxLevel])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -302,10 +302,10 @@ class LogIndex {
|
||||
if (controller && entry.controller) {
|
||||
return true;
|
||||
}
|
||||
if (host_ids && host_ids.some(id => entry.host_ids.has(id))) {
|
||||
if (hostIds && hostIds.some(id => entry.hostIds.has(id))) {
|
||||
return true;
|
||||
}
|
||||
if (instance_ids && instance_ids.some(id => entry.instance_ids.has(id))) {
|
||||
if (instanceIds && instanceIds.some(id => entry.instanceIds.has(id))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -319,16 +319,16 @@ class LogIndex {
|
||||
* @property {string} [order="asc"] -
|
||||
* Return entries in ascending ("asc") date order or desceding ("desc")
|
||||
* date order.
|
||||
* @property {string} [max_level] -
|
||||
* @property {string} [maxLevel] -
|
||||
* Maximum log level to include. Higher levels are more verbose.
|
||||
* @property {boolean} [all] -
|
||||
* Include log entries from controller, all hosts and all instances.
|
||||
* @property {boolean} [controller] -
|
||||
* Include log entries from the controller.
|
||||
* @property {Array<number>} [host_ids] -
|
||||
* @property {Array<number>} [hostIds] -
|
||||
* Include log entries for the given hosts and instances of those
|
||||
* hosts by id.
|
||||
* @property {Array<number>} [instance_ids] -
|
||||
* @property {Array<number>} [instanceIds] -
|
||||
* Include log entries for the given instances by id.
|
||||
*/
|
||||
|
||||
|
||||
+5
-39
@@ -545,10 +545,14 @@ class BaseControllerPlugin {
|
||||
* This does not include hosts that are in the process of closing the
|
||||
* connection, which typically happens when they are shutting down.
|
||||
*
|
||||
* @param {module:lib/link.Event} event - Event to send
|
||||
* @param {*} event - Event to send
|
||||
* @param {Object} data - Data ta pass with the event.
|
||||
*/
|
||||
broadcastEventToHosts(event, data={}) {
|
||||
if (event.type !== "event") {
|
||||
this.logger.warn(`Ignoring broadcast of ${event.type}`);
|
||||
return;
|
||||
}
|
||||
for (let hostConnection of this.controller.wsServer.hostConnections.values()) {
|
||||
if (
|
||||
!hostConnection.connector.closing
|
||||
@@ -771,43 +775,6 @@ class BaseWebPlugin {
|
||||
onControllerConnectionEvent(event) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach plugin messages
|
||||
*
|
||||
* Attaches all messages defined in the `.info.message` property of the
|
||||
* `plugin` passed with handlers taken from the `pluin`.
|
||||
*
|
||||
* @param {module:lib/link.Link} link - Link to attach handlers to.
|
||||
* @param {module:lib/plugin.BasePlugin} plugin -
|
||||
* The instance of the plugin attach messages and handlers from.
|
||||
*/
|
||||
function attachPluginMessages(link, plugin) {
|
||||
let messageDefinitions = plugin.info.messages || [];
|
||||
for (let [name, messageFormat] of Object.entries(messageDefinitions)) {
|
||||
if (messageFormat.plugin !== plugin.info.name) {
|
||||
throw new Error(`Type of ${name} message must start with "${plugin.info.name}:"`);
|
||||
}
|
||||
|
||||
let handler = name + messageFormat.handlerSuffix;
|
||||
try {
|
||||
if (plugin === null || !plugin[handler]) {
|
||||
messageFormat.attach(link);
|
||||
|
||||
} else {
|
||||
messageFormat.attach(link, async (message, format) => await plugin[handler](message, format, link));
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
if (err.code === "MISSING_LINK_HANDLER") {
|
||||
err.plugin = plugin.info.name;
|
||||
err.handler = handler;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the given hook on all plugins
|
||||
*
|
||||
@@ -847,6 +814,5 @@ module.exports = {
|
||||
BaseControlPlugin,
|
||||
BaseWebPlugin,
|
||||
|
||||
attachPluginMessages,
|
||||
invokeHook,
|
||||
};
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
const libErrors = require("./errors");
|
||||
const libPlugin = require("./plugin");
|
||||
const { logger } = require("./logging");
|
||||
const path = require("path");
|
||||
|
||||
|
||||
@@ -42,6 +43,12 @@ async function loadPluginInfos(pluginList) {
|
||||
);
|
||||
}
|
||||
|
||||
// migrate: ignore incompatible old plugins
|
||||
if (pluginInfo.messages && !(pluginInfo.messages instanceof Array)) {
|
||||
logger.warn(`Ignoring incompatible pre alpha.14 plugin ${pluginName}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
pluginInfo.requirePath = pluginPath;
|
||||
pluginInfo.version = pluginPackage.version;
|
||||
plugins.push(pluginInfo);
|
||||
|
||||
+1
-213
@@ -8,216 +8,10 @@ const ajv = new Ajv({
|
||||
verbose: true,
|
||||
format: "full",
|
||||
extendRefs: "fail",
|
||||
strictDefaluts: true,
|
||||
strictDefaults: true,
|
||||
strictKeywords: true,
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* The base format for any message sent over the link
|
||||
*/
|
||||
const messageSchema = {
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
title: "Message",
|
||||
description: "Format for messages on a link",
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["seq", "type", "data"],
|
||||
properties: {
|
||||
"seq": { type: ["integer", "null"] },
|
||||
"type": { type: "string" },
|
||||
"data": { type: "object" },
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Validator for message schemas
|
||||
* @function
|
||||
* @param {Object} obj - Object to validate
|
||||
* @returns {boolean} True if object passed validation.
|
||||
*/
|
||||
const message = ajv.compile(messageSchema);
|
||||
|
||||
/**
|
||||
* Validator for heartbeat messages
|
||||
* @function
|
||||
* @param {Object} obj - Object to validate
|
||||
* @returns {boolean} True if object passed validation.
|
||||
*/
|
||||
const heartbeat = ajv.compile({
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
title: "Heartbeat",
|
||||
description: "Heartbeat sent over link",
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["seq", "type", "data"],
|
||||
properties: {
|
||||
"seq": { const: null },
|
||||
"type": { const: "heartbeat" },
|
||||
"data": {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["seq"],
|
||||
properties: {
|
||||
"seq": { type: ["integer", "null"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Validator for server sent part of handshake
|
||||
* @function
|
||||
* @param {Object} obj - Object to validate
|
||||
* @returns {boolean} True if object passed validation.
|
||||
*/
|
||||
const serverHandshake = ajv.compile({
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
title: "Server Handshake",
|
||||
description: "Message sent from the server to a client during the handshake",
|
||||
allOf: [messageSchema],
|
||||
anyOf: [
|
||||
{
|
||||
properties: {
|
||||
"type": { const: "hello" },
|
||||
"data": {
|
||||
additionalProperties: false,
|
||||
required: ["version", "plugins"],
|
||||
properties: {
|
||||
"version": { type: "string" },
|
||||
"plugins": {
|
||||
type: "object",
|
||||
additionalProperties: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
"type": { const: "ready" },
|
||||
"data": {
|
||||
additionalProperties: false,
|
||||
required: ["session_token", "session_timeout", "heartbeat_interval"],
|
||||
properties: {
|
||||
"session_token": { type: "string" },
|
||||
"session_timeout": { type: "number" },
|
||||
"heartbeat_interval": { type: "number" },
|
||||
"account": {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["name", "roles"],
|
||||
properties: {
|
||||
"name": { type: "string" },
|
||||
"roles": {
|
||||
type: "array",
|
||||
items: {
|
||||
additionalProperties: false,
|
||||
type: "object",
|
||||
required: ["name", "id", "permissions"],
|
||||
properties: {
|
||||
"name": { type: "string" },
|
||||
"id": { type: "integer" },
|
||||
"permissions": {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
"type": { const: "continue" },
|
||||
"data": {
|
||||
additionalProperties: false,
|
||||
required: ["last_seq", "session_timeout", "heartbeat_interval"],
|
||||
properties: {
|
||||
"last_seq": { type: ["integer", "null"] },
|
||||
"session_timeout": { type: "number" },
|
||||
"heartbeat_interval": { type: "number" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
"type": { const: "invalidate" },
|
||||
"data": { const: {} },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
* Validator for client sent part of handshake
|
||||
* @function
|
||||
* @param {Object} obj - Object to validate
|
||||
* @returns {boolean} True if object passed validation.
|
||||
*/
|
||||
const clientHandshake = ajv.compile({
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
title: "Client Message",
|
||||
description: "Message sent from a client to the server",
|
||||
allOf: [messageSchema],
|
||||
anyOf: [
|
||||
{
|
||||
properties: {
|
||||
"type": { const: "register_host" },
|
||||
"data": {
|
||||
additionalProperties: false,
|
||||
required: ["token", "agent", "version", "name", "id", "plugins"],
|
||||
properties: {
|
||||
"token": { type: "string" },
|
||||
"agent": { type: "string" },
|
||||
"version": { type: "string" },
|
||||
"name": { type: "string" },
|
||||
"id": { type: "integer" },
|
||||
"public_address": { type: "string" },
|
||||
"plugins": {
|
||||
type: "object",
|
||||
additionalProperties: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
"type": { const: "register_control" },
|
||||
"data": {
|
||||
additionalProperties: false,
|
||||
required: ["token", "agent", "version"],
|
||||
properties: {
|
||||
"token": { type: "string" },
|
||||
"agent": { type: "string" },
|
||||
"version": { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
"type": { const: "resume" },
|
||||
"data": {
|
||||
additionalProperties: false,
|
||||
required: ["session_token", "last_seq"],
|
||||
properties: {
|
||||
"session_token": { type: "string" },
|
||||
"last_seq": { type: ["integer", "null"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Compile JSON schema into validator
|
||||
@@ -226,10 +20,4 @@ module.exports = {
|
||||
* @returns {Function} Validator for the schema.
|
||||
*/
|
||||
compile: ajv.compile.bind(ajv),
|
||||
|
||||
message,
|
||||
heartbeat,
|
||||
|
||||
serverHandshake,
|
||||
clientHandshake,
|
||||
};
|
||||
@@ -37,6 +37,7 @@
|
||||
"react-router-dom": "^5.2.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^2.0.0",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"util": "^0.12.3",
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
|
||||
@@ -3,7 +3,7 @@ import "./index.css";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import { libConfig, libLogging, libPlugin } from "@clusterio/lib";
|
||||
import { libConfig, libLink, libLogging, libPlugin } from "@clusterio/lib";
|
||||
const { ConsoleTransport, WebConsoleFormat, logger } = libLogging;
|
||||
|
||||
import App from "./components/App";
|
||||
@@ -102,6 +102,7 @@ export default async function bootstrap() {
|
||||
format: new WebConsoleFormat(),
|
||||
}));
|
||||
let pluginInfos = await loadPluginInfos();
|
||||
libLink.registerPluginMessages(pluginInfos);
|
||||
libConfig.registerPluginConfigGroups(pluginInfos);
|
||||
libConfig.finalizeConfigs();
|
||||
let plugins = await loadPlugins(pluginInfos);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Button, Form, Modal, Select, Typography } from "antd";
|
||||
|
||||
import { libLink } from "@clusterio/lib";
|
||||
import { libData } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
@@ -33,10 +33,9 @@ export default function AssignInstanceModal(props) {
|
||||
}
|
||||
|
||||
setApplying(true);
|
||||
libLink.messages.assignInstanceCommand.send(control, {
|
||||
instance_id: props.id,
|
||||
host_id: hostId,
|
||||
}).then(() => {
|
||||
control.send(
|
||||
new libData.InstanceAssignRequest(props.id, hostId)
|
||||
).then(() => {
|
||||
setVisible(false);
|
||||
if (props.onFinish) {
|
||||
props.onFinish();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import { libConfig, libLink } from "@clusterio/lib";
|
||||
import { libConfig, libData } from "@clusterio/lib";
|
||||
|
||||
import BaseConfigTree from "./BaseConfigTree";
|
||||
import ControlContext from "./ControlContext";
|
||||
@@ -10,27 +10,26 @@ export default function ControllerConfigTree(props) {
|
||||
let control = useContext(ControlContext);
|
||||
|
||||
async function retrieveConfig() {
|
||||
let result = await libLink.messages.getControllerConfig.send(control);
|
||||
return result.serialized_config;
|
||||
let result = await control.send(new libData.ControllerConfigGetRequest());
|
||||
return result.serializedConfig;
|
||||
}
|
||||
|
||||
async function setField(field, value) {
|
||||
await libLink.messages.setControllerConfigField.send(control, {
|
||||
await control.send(new libData.ControllerConfigSetFieldRequest(
|
||||
field,
|
||||
value: String(value),
|
||||
});
|
||||
String(value),
|
||||
));
|
||||
}
|
||||
|
||||
async function setProp(field, prop, value) {
|
||||
let data = { field, prop };
|
||||
if (value) {
|
||||
try {
|
||||
data.value = JSON.parse(value);
|
||||
value = JSON.parse(value);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await libLink.messages.setControllerConfigProp.send(control, data);
|
||||
await control.send(new libData.ControllerConfigSetPropRequest(field, prop, value));
|
||||
}
|
||||
|
||||
return <BaseConfigTree
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Button, Form, Input, Modal } from "antd";
|
||||
|
||||
import { libFactorio, libLink } from "@clusterio/lib";
|
||||
import { libFactorio, libData } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
@@ -51,13 +51,10 @@ export default function CreateSaveModal(props) {
|
||||
}
|
||||
|
||||
setCreatingSave(true);
|
||||
libLink.messages.createSave.send(control, {
|
||||
instance_id: props.instance.id,
|
||||
name,
|
||||
seed,
|
||||
map_gen_settings: mapGenSettings,
|
||||
map_settings: mapSettings,
|
||||
}).then(() => {
|
||||
control.sendTo(
|
||||
{ instanceId: props.instance.id },
|
||||
new libData.InstanceCreateSaveRequest(name, seed, mapGenSettings, mapSettings),
|
||||
).then(() => {
|
||||
form.resetFields();
|
||||
setVisible(false);
|
||||
}).catch(
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function HostViewPage(props) {
|
||||
let [instanceList] = useInstanceList();
|
||||
let [host] = useHost(hostId);
|
||||
|
||||
instanceList = instanceList.filter(instance => instance["assigned_host"] === hostId);
|
||||
instanceList = instanceList.filter(instance => instance.assignedHost === hostId);
|
||||
|
||||
let nav = [{ name: "Hosts", path: "/hosts" }, { name: host.name || hostId }];
|
||||
if (host.loading) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useHistory } from "react-router-dom";
|
||||
import { Button, Form, InputNumber, Modal, PageHeader, Table, Tag, Typography } from "antd";
|
||||
import CopyOutlined from "@ant-design/icons/lib/icons/CopyOutlined";
|
||||
|
||||
import { libLink } from "@clusterio/lib";
|
||||
import { libData } from "@clusterio/lib";
|
||||
|
||||
import { useAccount } from "../model/account";
|
||||
import ControlContext from "./ControlContext";
|
||||
@@ -35,7 +35,7 @@ function GenerateHostTokenButton(props) {
|
||||
}, []);
|
||||
|
||||
async function generateToken() {
|
||||
let id;
|
||||
let id = null;
|
||||
let values = form.getFieldsValue();
|
||||
if (values.hostId) {
|
||||
id = Number.parseInt(values.hostId, 10);
|
||||
@@ -46,8 +46,8 @@ function GenerateHostTokenButton(props) {
|
||||
form.setFields([{ name: "hostId", errors: [] }]);
|
||||
}
|
||||
|
||||
let result = await libLink.messages.generateHostToken.send(control, { host_id: id || null });
|
||||
setToken(result.token);
|
||||
let newToken = await control.send(new libData.HostGenerateTokenRequest(id));
|
||||
setToken(newToken);
|
||||
setHostId(id);
|
||||
}
|
||||
|
||||
@@ -192,37 +192,37 @@ export default function HostsPage() {
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
defaultSortOrder: "ascend",
|
||||
sorter: (a, b) => strcmp(a["name"], b["name"]),
|
||||
sorter: (a, b) => strcmp(a.name, b.name),
|
||||
},
|
||||
{
|
||||
title: "Agent",
|
||||
dataIndex: "agent",
|
||||
sorter: (a, b) => strcmp(a["agent"], b["agent"]),
|
||||
sorter: (a, b) => strcmp(a.agent, b.agent),
|
||||
responsive: ["lg"],
|
||||
},
|
||||
{
|
||||
title: "Version",
|
||||
dataIndex: "version",
|
||||
sorter: (a, b) => strcmp(a["version"], b["version"]),
|
||||
sorter: (a, b) => strcmp(a.version, b.version),
|
||||
},
|
||||
{
|
||||
title: "Public address",
|
||||
dataIndex: "public_address",
|
||||
sorter: (a, b) => strcmp(a["public_address"], b["public_address"]),
|
||||
dataIndex: "publicAddress",
|
||||
sorter: (a, b) => strcmp(a.publicAddress, b.publicAddress),
|
||||
},
|
||||
{
|
||||
title: "Connected",
|
||||
key: "connected",
|
||||
render: host => <Tag
|
||||
color={host["connected"] ? "#389e0d" : "#cf1322"}
|
||||
color={host.connected ? "#389e0d" : "#cf1322"}
|
||||
>
|
||||
{host["connected"] ? "Connected" : "Disconnected"}
|
||||
{host.connected ? "Connected" : "Disconnected"}
|
||||
</Tag>,
|
||||
sorter: (a, b) => a["connected"] - b["connected"],
|
||||
sorter: (a, b) => a.connected - b.connected,
|
||||
},
|
||||
]}
|
||||
dataSource={hostList}
|
||||
rowKey={host => host["id"]}
|
||||
rowKey={host => host.id}
|
||||
pagination={false}
|
||||
onRow={(record, rowIndex) => ({
|
||||
onClick: event => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import { libConfig, libLink } from "@clusterio/lib";
|
||||
import { libConfig, libData } from "@clusterio/lib";
|
||||
|
||||
import BaseConfigTree from "./BaseConfigTree";
|
||||
import ControlContext from "./ControlContext";
|
||||
@@ -10,34 +10,23 @@ export default function InstanceConfigTree(props) {
|
||||
let control = useContext(ControlContext);
|
||||
|
||||
async function retrieveConfig() {
|
||||
let result = await libLink.messages.getInstanceConfig.send(control, {
|
||||
instance_id: props.id,
|
||||
});
|
||||
return result.serialized_config;
|
||||
let result = await control.send(new libData.InstanceConfigGetRequest(props.id));
|
||||
return result.config;
|
||||
}
|
||||
|
||||
async function setField(field, value) {
|
||||
await libLink.messages.setInstanceConfigField.send(control, {
|
||||
instance_id: props.id,
|
||||
field,
|
||||
value,
|
||||
});
|
||||
await control.send(new libData.InstanceConfigSetFieldRequest(props.id, field, value));
|
||||
}
|
||||
|
||||
async function setProp(field, prop, value) {
|
||||
let data = {
|
||||
instance_id: props.id,
|
||||
field,
|
||||
prop,
|
||||
};
|
||||
if (value) {
|
||||
try {
|
||||
data.value = JSON.parse(value);
|
||||
value = JSON.parse(value);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await libLink.messages.setInstanceConfigProp.send(control, data);
|
||||
await control.send(new libData.InstanceConfigSetPropRequest(props.id, field, prop, value));
|
||||
}
|
||||
|
||||
return <BaseConfigTree
|
||||
|
||||
@@ -27,12 +27,12 @@ export default function InstanceList(props) {
|
||||
}
|
||||
|
||||
function instancePublicAddress(instance) {
|
||||
let host = hostList.find(s => s.id === instance.assigned_host);
|
||||
if (host && host.public_address) {
|
||||
if (instance.game_port) {
|
||||
return `${host.public_address}:${instance.game_port}`;
|
||||
let host = hostList.find(s => s.id === instance.assignedHost);
|
||||
if (host && host.publicAddress) {
|
||||
if (instance.gamePort) {
|
||||
return `${host.publicAddress}:${instance.gamePort}`;
|
||||
}
|
||||
return host.public_address;
|
||||
return host.publicAddress;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -46,30 +46,30 @@ export default function InstanceList(props) {
|
||||
},
|
||||
{
|
||||
title: "Assigned Host",
|
||||
key: "assigned_host",
|
||||
render: instance => hostName(instance["assigned_host"]),
|
||||
sorter: (a, b) => strcmp(hostName(a["assigned_host"]), hostName(b["assigned_host"])),
|
||||
key: "assignedHost",
|
||||
render: instance => hostName(instance.assignedHost),
|
||||
sorter: (a, b) => strcmp(hostName(a.assignedHost), hostName(b.assignedHost)),
|
||||
responsive: ["sm"],
|
||||
},
|
||||
{
|
||||
title: "Public address",
|
||||
key: "public_address",
|
||||
key: "publicAddress",
|
||||
render: instance => {
|
||||
let public_address = instancePublicAddress(instance);
|
||||
return public_address ? <>
|
||||
{public_address}
|
||||
let publicAddress = instancePublicAddress(instance);
|
||||
return publicAddress ? <>
|
||||
{publicAddress}
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CopyOutlined/>}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(public_address);
|
||||
navigator.clipboard.writeText(publicAddress);
|
||||
message.success("Copied public address!");
|
||||
}}
|
||||
/>
|
||||
</> : "";
|
||||
},
|
||||
sorter: (a, b) => strcmp(hostPublicAddress(a["assigned_host"]), hostPublicAddress(b["assigned_host"])),
|
||||
sorter: (a, b) => strcmp(hostPublicAddress(a.assignedHost), hostPublicAddress(b.assignedHost)),
|
||||
responsive: ["lg"],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Input, Typography } from "antd";
|
||||
|
||||
import { libLink } from "@clusterio/lib";
|
||||
import { libData } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
@@ -22,11 +22,11 @@ export default function InstanceRcon(props) {
|
||||
|
||||
setRunning(true);
|
||||
try {
|
||||
let result = await libLink.messages.sendRcon.send(control, {
|
||||
instance_id: props.id,
|
||||
command: command,
|
||||
});
|
||||
setOutput(result.result);
|
||||
let result = await control.sendTo(
|
||||
{ instanceId: props.id },
|
||||
new libData.InstanceSendRconRequest(command),
|
||||
);
|
||||
setOutput(result);
|
||||
} finally {
|
||||
setRunning(false);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Alert, Button, Descriptions, Dropdown, Menu, Modal, PageHeader, Space,
|
||||
import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
|
||||
import DownOutlined from "@ant-design/icons/DownOutlined";
|
||||
|
||||
import { libLink } from "@clusterio/lib";
|
||||
import { libData } from "@clusterio/lib";
|
||||
|
||||
import { useAccount } from "../model/account";
|
||||
import ControlContext from "./ControlContext";
|
||||
@@ -29,7 +29,7 @@ function InstanceDescription(props) {
|
||||
let account = useAccount();
|
||||
|
||||
const { host, instance } = props;
|
||||
let assigned = instance["assigned_host"] !== null;
|
||||
let assigned = instance.assignedHost !== null;
|
||||
return <Descriptions
|
||||
bordered
|
||||
size="small"
|
||||
@@ -37,21 +37,21 @@ function InstanceDescription(props) {
|
||||
<Descriptions.Item label="Host">
|
||||
{!assigned
|
||||
? <em>Unassigned</em>
|
||||
: host["name"] || instance["assigned_host"]
|
||||
: host.name || instance.assignedHost
|
||||
}
|
||||
{account.hasPermission("core.instance.assign") && <AssignInstanceModal
|
||||
id={instance["id"]}
|
||||
hostId={instance["assigned_host"]}
|
||||
id={instance.id}
|
||||
hostId={instance.assignedHost}
|
||||
buttonProps={{
|
||||
size: "small",
|
||||
style: { float: "Right" },
|
||||
type: assigned ? "default" : "primary",
|
||||
disabled: !["unknown", "unassigned", "stopped"].includes(instance["status"]),
|
||||
disabled: !["unknown", "unassigned", "stopped"].includes(instance.status),
|
||||
}}
|
||||
buttonContent={assigned ? "Reassign" : "Assign"}
|
||||
/>}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Status"><InstanceStatusTag status={instance["status"]} /></Descriptions.Item>
|
||||
<Descriptions.Item label="Status"><InstanceStatusTag status={instance.status} /></Descriptions.Item>
|
||||
</Descriptions>;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function InstanceViewPage(props) {
|
||||
let account = useAccount();
|
||||
let control = useContext(ControlContext);
|
||||
let [instance] = useInstance(instanceId);
|
||||
let [host] = useHost(Number(instance["assigned_host"]));
|
||||
let [host] = useHost(Number(instance.assignedHost));
|
||||
|
||||
let [exportingData, setExportingData] = useState(false);
|
||||
|
||||
@@ -73,10 +73,10 @@ export default function InstanceViewPage(props) {
|
||||
return <PageLayout nav={nav}><Spin size="large" /></PageLayout>;
|
||||
}
|
||||
|
||||
if (instance.missing || instance["status"] === "deleted") {
|
||||
if (instance.missing || instance.status === "deleted") {
|
||||
return <PageLayout nav={nav}>
|
||||
<Alert
|
||||
message={instance["status"] === "deleted" ? "Instance has been deleted" : "Instance not found" }
|
||||
message={instance.status === "deleted" ? "Instance has been deleted" : "Instance not found" }
|
||||
showIcon
|
||||
description={<>Instance with id {instanceId} was not found on the controller.</>}
|
||||
type="warning"
|
||||
@@ -102,14 +102,14 @@ export default function InstanceViewPage(props) {
|
||||
}
|
||||
if (account.hasPermission("core.instance.extract_players")) {
|
||||
instanceButtonMenuItems.push({
|
||||
disabled: instance["status"] !== "running",
|
||||
disabled: instance.status !== "running",
|
||||
key: "extract",
|
||||
label: "Extract players from save",
|
||||
});
|
||||
}
|
||||
if (account.hasPermission("core.instance.kill")) {
|
||||
instanceButtonMenuItems.push({
|
||||
disabled: ["unknown", "unassigned", "stopped"].includes(instance["status"]),
|
||||
disabled: ["unknown", "unassigned", "stopped"].includes(instance.status),
|
||||
key: "kill",
|
||||
label: "Kill process",
|
||||
});
|
||||
@@ -119,7 +119,7 @@ export default function InstanceViewPage(props) {
|
||||
instanceButtonMenuItems.push({ type: "divider" });
|
||||
}
|
||||
instanceButtonMenuItems.push({
|
||||
disabled: !["unknown", "unassigned", "stopped"].includes(instance["status"]),
|
||||
disabled: !["unknown", "unassigned", "stopped"].includes(instance.status),
|
||||
danger: true,
|
||||
key: "delete",
|
||||
icon: <DeleteOutlined />,
|
||||
@@ -131,8 +131,9 @@ export default function InstanceViewPage(props) {
|
||||
onClick={({ key }) => {
|
||||
if (key === "export") {
|
||||
setExportingData(true);
|
||||
libLink.messages.exportData.send(
|
||||
control, { instance_id: instanceId }
|
||||
control.sendTo(
|
||||
{ instanceId },
|
||||
new libData.InstanceExportDataRequest(),
|
||||
).catch(
|
||||
notifyErrorHandler("Error exporting data")
|
||||
).finally(() => {
|
||||
@@ -140,13 +141,15 @@ export default function InstanceViewPage(props) {
|
||||
});
|
||||
|
||||
} else if (key === "extract") {
|
||||
libLink.messages.extractPlayers.send(
|
||||
control, { instance_id: instanceId }
|
||||
control.sendTo(
|
||||
{ instanceId },
|
||||
new libData.InstanceExtractPlayersRequest(),
|
||||
).catch(notifyErrorHandler("Error extracting player data"));
|
||||
|
||||
} else if (key === "kill") {
|
||||
libLink.messages.killInstance.send(
|
||||
control, { instance_id: instanceId }
|
||||
control.sendTo(
|
||||
{ instanceId },
|
||||
new libData.InstanceKillRequest(),
|
||||
).catch(notifyErrorHandler("Error killing instance"));
|
||||
|
||||
} else if (key === "delete") {
|
||||
@@ -156,8 +159,8 @@ export default function InstanceViewPage(props) {
|
||||
okText: "Delete",
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => {
|
||||
libLink.messages.deleteInstance.send(
|
||||
control, { instance_id: instanceId }
|
||||
control.send(
|
||||
new libData.InstanceDeleteRequest(instanceId)
|
||||
).then(() => {
|
||||
history.push("/instances");
|
||||
}).catch(notifyErrorHandler("Error deleting instance"));
|
||||
@@ -185,7 +188,7 @@ export default function InstanceViewPage(props) {
|
||||
return <PageLayout nav={nav}>
|
||||
<PageHeader
|
||||
className="site-page-header"
|
||||
title={instance["name"]}
|
||||
title={instance.name}
|
||||
extra={instanceButtons}
|
||||
/>
|
||||
<InstanceDescription host={host} instance={instance} />
|
||||
@@ -201,7 +204,7 @@ export default function InstanceViewPage(props) {
|
||||
{account.hasPermission("core.log.follow") && <LogConsole instances={[instanceId]} />}
|
||||
{
|
||||
account.hasPermission("core.instance.send_rcon")
|
||||
&& <InstanceRcon id={instanceId} disabled={instance["status"] !== "running"} />
|
||||
&& <InstanceRcon id={instanceId} disabled={instance.status !== "running"} />
|
||||
}
|
||||
|
||||
{account.hasPermission("core.instance.get_config") && <InstanceConfigTree id={instanceId} />}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useContext, useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Button, Form, Input, Modal, PageHeader } from "antd";
|
||||
|
||||
import { libConfig, libLink } from "@clusterio/lib";
|
||||
import { libConfig, libData } from "@clusterio/lib";
|
||||
|
||||
import { useAccount } from "../model/account";
|
||||
import ControlContext from "./ControlContext";
|
||||
@@ -28,8 +28,8 @@ function CreateInstanceButton(props) {
|
||||
let instanceConfig = new libConfig.InstanceConfig("control");
|
||||
await instanceConfig.init();
|
||||
instanceConfig.set("instance.name", values.instanceName);
|
||||
let serialized_config = instanceConfig.serialize("controller");
|
||||
await libLink.messages.createInstance.send(control, { serialized_config });
|
||||
let serializedConfig = instanceConfig.serialize("controller");
|
||||
await control.send(new libData.InstanceCreateRequest(serializedConfig));
|
||||
setVisible(false);
|
||||
history.push(`/instances/${instanceConfig.get("instance.id")}/view`);
|
||||
}
|
||||
@@ -72,8 +72,8 @@ export default function InstancesPage() {
|
||||
{account.hasPermission("core.instance.start")
|
||||
&& <Button onClick={e => instanceList.forEach(instance => {
|
||||
if (instance.status === "stopped") {
|
||||
libLink.messages.startInstance.send(
|
||||
control, { instance_id: instance.id, save: null }
|
||||
control.send(
|
||||
new libData.InstanceStartRequest(undefined), { instanceId: instance.id }
|
||||
).catch(notifyErrorHandler("Error starting instance"));
|
||||
}
|
||||
})
|
||||
@@ -83,8 +83,9 @@ export default function InstancesPage() {
|
||||
{account.hasPermission("core.instance.stop")
|
||||
&& <Button onClick={e => instanceList.forEach(instance => {
|
||||
if (["starting", "running"].includes(instance.status)) {
|
||||
libLink.messages.stopInstance.send(
|
||||
control, { instance_id: instance.id }
|
||||
control.sendTo(
|
||||
{ instanceId: instance.id },
|
||||
new libData.InstanceStopRequest(),
|
||||
).catch(notifyErrorHandler("Error stopping instance"));
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Button, Form, Input, Modal } from "antd";
|
||||
|
||||
import { libFactorio, libLink } from "@clusterio/lib";
|
||||
import { libFactorio, libData } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
@@ -51,13 +51,10 @@ export default function LoadScenarioModal(props) {
|
||||
}
|
||||
|
||||
setLoadingScenario(true);
|
||||
libLink.messages.loadScenario.send(control, {
|
||||
instance_id: props.instance.id,
|
||||
scenario,
|
||||
seed,
|
||||
map_gen_settings: mapGenSettings,
|
||||
map_settings: mapSettings,
|
||||
}).then(() => {
|
||||
control.sendTo(
|
||||
{ instanceId: props.instance.id },
|
||||
new libData.InstanceLoadScenarioRequest(scenario, seed, mapGenSettings, mapSettings),
|
||||
).then(() => {
|
||||
setVisible(false);
|
||||
}).catch(
|
||||
notifyErrorHandler("Error loading scenario")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useContext, useRef, useState } from "react";
|
||||
import { Typography } from "antd";
|
||||
|
||||
import { libLink } from "@clusterio/lib";
|
||||
import { libData } from "@clusterio/lib";
|
||||
|
||||
import { useAccount } from "../model/account";
|
||||
import ControlContext from "./ControlContext";
|
||||
@@ -60,8 +60,8 @@ export default function LogConsole(props) {
|
||||
let logFilter = {
|
||||
all: props.all || false,
|
||||
controller: props.controller || false,
|
||||
host_ids: props.hosts || [],
|
||||
instance_ids: props.instances || [],
|
||||
hostIds: props.hosts || [],
|
||||
instanceIds: props.instances || [],
|
||||
};
|
||||
|
||||
// Scroll view to the anchor so it sticks to the bottom
|
||||
@@ -70,12 +70,15 @@ export default function LogConsole(props) {
|
||||
|
||||
if (account.hasPermission("core.log.query")) {
|
||||
setPastLines([<span key={0}>{"Loading past entries..."}<br/></span>]);
|
||||
libLink.messages.queryLog.send(control, {
|
||||
...logFilter,
|
||||
max_level: null,
|
||||
limit: 400,
|
||||
order: "desc",
|
||||
}).then(result => {
|
||||
control.send(new libData.LogQueryRequest(
|
||||
logFilter.all,
|
||||
logFilter.controller,
|
||||
logFilter.hostIds,
|
||||
logFilter.instanceIds,
|
||||
null,
|
||||
400,
|
||||
"desc",
|
||||
)).then(result => {
|
||||
setPastLines(result.log.map((info, index) => formatLog(info, -index - 1)).reverse());
|
||||
}).catch(err => {
|
||||
setPastLines([<span key={0}>{`Error loading log: ${err.message}`}<br/></span>]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Fragment, useEffect, useContext, useRef, useState } from "react";
|
||||
import React, { Fragment, memo, useCallback, useEffect, useContext, useRef, useState } from "react";
|
||||
import { useHistory, useParams } from "react-router-dom";
|
||||
import {
|
||||
Button, Card, Checkbox, Col, ConfigProvider, Descriptions, Form, Input, PageHeader, Pagination,
|
||||
@@ -13,7 +13,7 @@ import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
|
||||
import PlusOutlined from "@ant-design/icons/PlusOutlined";
|
||||
import UploadOutlined from "@ant-design/icons/UploadOutlined";
|
||||
|
||||
import { libData, libLink, libLogging } from "@clusterio/lib";
|
||||
import { libData, libLogging } from "@clusterio/lib";
|
||||
import ControlContext from "./ControlContext";
|
||||
import { useAccount } from "../model/account";
|
||||
import { useModPack } from "../model/mod_pack";
|
||||
@@ -43,22 +43,20 @@ function SearchModsTable(props) {
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
libLink.messages.searchMods.send(control, {
|
||||
query: searchText,
|
||||
factorio_version: factorioVersion,
|
||||
page_size: modResultPageSize,
|
||||
page: modResultPage,
|
||||
sort: modResultSort,
|
||||
sort_order: modResultSortOrder,
|
||||
}).then(response => {
|
||||
control.send(new libData.ModSearchRequest(
|
||||
searchText,
|
||||
factorioVersion,
|
||||
modResultPage,
|
||||
modResultPageSize,
|
||||
modResultSort,
|
||||
modResultSortOrder,
|
||||
)).then(response => {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
// In React < v18 this causes 3 renders and a partial state necessitating the use
|
||||
// of || 0 on modResultSelectedVersion.get() calls. Remove when updating React.
|
||||
setModResults(response.results.map(
|
||||
({ name, versions }) => ({ name, versions: versions.map(mod => new libData.ModInfo(mod)) })
|
||||
));
|
||||
setModResults(response.results);
|
||||
setModResultSelectedVersion(new Map(response.results.map(({ name, versions }) => [name, 0])));
|
||||
setModResultCount(response.result_count);
|
||||
});
|
||||
@@ -189,7 +187,7 @@ function SearchModsTable(props) {
|
||||
onChange={modResultsTableChanged}
|
||||
dataSource={modResults}
|
||||
pagination={false}
|
||||
rowKey={result => result.name}
|
||||
rowKey="name"
|
||||
/>
|
||||
</ConfigProvider>
|
||||
</>;
|
||||
@@ -329,7 +327,7 @@ function ModsTable(props) {
|
||||
}}
|
||||
dataSource={mods}
|
||||
pagination={false}
|
||||
rowKey={mod => mod.name}
|
||||
rowKey="name"
|
||||
rowClassName={mod => {
|
||||
if (deletedMods.has(mod.name)) {
|
||||
return "deleted";
|
||||
@@ -365,6 +363,45 @@ function groupToMap(array, fn) {
|
||||
return map;
|
||||
}
|
||||
|
||||
const SettingsTableField = memo((props) => {
|
||||
const name = props.name;
|
||||
const isBoolean = props.type === "bool-setting";
|
||||
const isNumber = ["int-setting", "double-setting"].includes(props.type);
|
||||
const isSelect = Boolean(props.allowedValues);
|
||||
let input;
|
||||
if (isBoolean) {
|
||||
input = <Checkbox />;
|
||||
} else if (isSelect) {
|
||||
const options = Object.values(props.allowedValues).map(value => ({
|
||||
value,
|
||||
label: props.locale.get(`string-mod-setting.${name}-${value}`) || value,
|
||||
}));
|
||||
input = <Select options={options} />;
|
||||
} else {
|
||||
input = <Input />;
|
||||
}
|
||||
|
||||
return <Form.Item
|
||||
name={[props.scope, name]}
|
||||
label={props.locale.get(`mod-setting-name.${name}`) || name}
|
||||
tooltip={props.locale.get(`mod-setting-description.${name}`)}
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
valuePropName={isBoolean ? "checked" : "value"}
|
||||
rules={[...isNumber ? [{
|
||||
validator(rule, value) {
|
||||
if (Number.isNaN(Number(value))) {
|
||||
return Promise.reject(new Error("Must be a number"));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
}] : []]}
|
||||
className={ props.changed && "changed" }
|
||||
>
|
||||
{input}
|
||||
</Form.Item>;
|
||||
});
|
||||
|
||||
function SettingsTable(props) {
|
||||
const [modList] = useModList();
|
||||
const modsInPack = new Set([...props.modPack.mods.values()].map(mod => `${mod.name}_${mod.version}`));
|
||||
@@ -405,47 +442,17 @@ function SettingsTable(props) {
|
||||
<Typography.Title level={5}>
|
||||
{props.locale.get(`mod-name.${mod}`) || modTitles.get(mod) || mod}
|
||||
</Typography.Title>
|
||||
{[...fieldsForMod].map(prototype => {
|
||||
const name = prototype.name;
|
||||
const isBoolean = prototype.type === "bool-setting";
|
||||
const isNumber = ["int-setting", "double-setting"].includes(prototype.type);
|
||||
const isSelect = Boolean(prototype.allowed_values);
|
||||
let input;
|
||||
if (isBoolean) {
|
||||
input = <Checkbox />;
|
||||
} else if (isSelect) {
|
||||
const options = Object.values(prototype.allowed_values).map(value => ({
|
||||
value,
|
||||
label: props.locale.get(`string-mod-setting.${name}-${value}`) || value,
|
||||
}));
|
||||
input = <Select options={options} />;
|
||||
} else {
|
||||
input = <Input />;
|
||||
}
|
||||
|
||||
return <Form.Item
|
||||
name={[scope, name]}
|
||||
key={`${scope} ${name}`}
|
||||
label={props.locale.get(`mod-setting-name.${name}`) || name}
|
||||
tooltip={props.locale.get(`mod-setting-description.${name}`)}
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
valuePropName={isBoolean ? "checked" : "value"}
|
||||
rules={[...isNumber ? [{
|
||||
validator(rule, value) {
|
||||
if (Number.isNaN(Number(value))) {
|
||||
return Promise.reject(new Error("Must be a number"));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
}] : []]}
|
||||
className={
|
||||
hasChange(props.changes, { type: "settings.set", scope, name }) && "changed"
|
||||
}
|
||||
>
|
||||
{input}
|
||||
</Form.Item>;
|
||||
})}
|
||||
{[...fieldsForMod].map(
|
||||
prototype => <SettingsTableField
|
||||
key={`${scope} ${prototype.name}`}
|
||||
name={prototype.name}
|
||||
type={prototype.type}
|
||||
scope={scope}
|
||||
allowedValues={prototype.allowed_values}
|
||||
locale={props.locale}
|
||||
changed={hasChange(props.changes, { type: "settings.set", scope, name: prototype.name })}
|
||||
/>
|
||||
)}
|
||||
</Fragment>);
|
||||
}
|
||||
|
||||
@@ -501,7 +508,8 @@ function ExportButton(props) {
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
let exportString = props.modPack.toModPackString();
|
||||
let exportString;
|
||||
if (visible) { exportString = props.modPack.toModPackString(); }
|
||||
|
||||
return <>
|
||||
<Button icon={<ExportOutlined />} onClick={() => { setVisible(true); }}>Export to string</Button>
|
||||
@@ -550,6 +558,43 @@ function useExportedAsset(modPack, asset) {
|
||||
return assetData;
|
||||
}
|
||||
|
||||
function applyModPackChanges(modPack, changes) {
|
||||
let modifiedModPack = modPack.shallowClone();
|
||||
for (let change of changes) {
|
||||
// Only modify fields that change to reduce re-renders
|
||||
if (["mods.set", "mods.delete"].includes(change.type) && modifiedModPack.mods === modPack.mods) {
|
||||
modifiedModPack.mods = new Map(modifiedModPack.mods);
|
||||
}
|
||||
if (
|
||||
["settings.set", "settings.delete"].includes(change.type)
|
||||
&& modifiedModPack.settings[change.scope] === modPack.settings[change.scope]
|
||||
) {
|
||||
if (modifiedModPack.settings === modPack.settings) {
|
||||
modifiedModPack.settings = { ...modPack.settings };
|
||||
}
|
||||
modifiedModPack.settings[change.scope] = new Map(modifiedModPack.settings[change.scope]);
|
||||
}
|
||||
|
||||
if (change.type === "name") {
|
||||
modifiedModPack.name = change.value;
|
||||
} else if (change.type === "description") {
|
||||
modifiedModPack.description = change.value;
|
||||
} else if (change.type === "factorioVersion") {
|
||||
modifiedModPack.factorioVersion = change.value;
|
||||
} else if (change.type === "mods.set") {
|
||||
modifiedModPack.mods.set(change.name, change.value);
|
||||
} else if (change.type === "mods.delete") {
|
||||
modifiedModPack.mods.delete(change.name);
|
||||
} else if (change.type === "settings.set") {
|
||||
modifiedModPack.settings[change.scope].set(change.name, change.value);
|
||||
} else if (change.type === "settings.delete") {
|
||||
modifiedModPack.settings[change.scope].delete(change.name);
|
||||
} else {
|
||||
throw new Error(`Unknown change type ${change.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function ModPackViewPage() {
|
||||
let account = useAccount();
|
||||
let history = useHistory();
|
||||
@@ -562,12 +607,20 @@ export default function ModPackViewPage() {
|
||||
let prototypes = useExportedAsset(modPack, "settings");
|
||||
let locale = useExportedAsset(modPack, "locale");
|
||||
let [changes, setChanges] = useState([]);
|
||||
let [modifiedModPack, setModifiedModPack] = useState(modPack);
|
||||
let syncTimeout = useRef();
|
||||
|
||||
let modifiedModPack;
|
||||
if (!(modPack instanceof libData.ModPack)) {
|
||||
// Loading or invalid id
|
||||
modifiedModPack = modPack;
|
||||
|
||||
} else {
|
||||
modifiedModPack = applyModPackChanges(modPack, changes);
|
||||
}
|
||||
|
||||
useEffect(() => () => { clearTimeout(syncTimeout.current); });
|
||||
|
||||
function pushChange(change) {
|
||||
const pushChange = useCallback((change) => {
|
||||
setChanges(oldChanges => {
|
||||
const newChanges = [...oldChanges];
|
||||
if (
|
||||
@@ -582,20 +635,22 @@ export default function ModPackViewPage() {
|
||||
}
|
||||
return newChanges;
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
function revertChange(change) {
|
||||
const newChanges = [...changes];
|
||||
const index = newChanges.findLastIndex(reference => (
|
||||
change.type === reference.type && change.name === reference.name
|
||||
));
|
||||
if (index === -1) {
|
||||
logger.error(`Unable to revert ${change.type} ${change.name}: change not found`);
|
||||
return;
|
||||
}
|
||||
newChanges.splice(index, 1);
|
||||
setChanges(newChanges);
|
||||
}
|
||||
const revertChange = useCallback((change) => {
|
||||
setChanges(oldChanges => {
|
||||
const newChanges = [...oldChanges];
|
||||
const index = newChanges.findLastIndex(reference => (
|
||||
change.type === reference.type && change.name === reference.name
|
||||
));
|
||||
if (index === -1) {
|
||||
logger.error(`Unable to revert ${change.type} ${change.name}: change not found`);
|
||||
return oldChanges;
|
||||
}
|
||||
newChanges.splice(index, 1);
|
||||
return newChanges;
|
||||
});
|
||||
}, []);
|
||||
|
||||
function syncPack(fields) {
|
||||
for (let field of fields) {
|
||||
@@ -623,36 +678,6 @@ export default function ModPackViewPage() {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Loading or invalid id
|
||||
if (!(modPack instanceof libData.ModPack)) {
|
||||
setModifiedModPack(modPack);
|
||||
return;
|
||||
}
|
||||
|
||||
const modified = new libData.ModPack(modPack.toJSON());
|
||||
for (let change of changes) {
|
||||
if (change.type === "name") {
|
||||
modified.name = change.value;
|
||||
} else if (change.type === "description") {
|
||||
modified.description = change.value;
|
||||
} else if (change.type === "factorioVersion") {
|
||||
modified.factorioVersion = change.value;
|
||||
} else if (change.type === "mods.set") {
|
||||
modified.mods.set(change.name, change.value);
|
||||
} else if (change.type === "mods.delete") {
|
||||
modified.mods.delete(change.name);
|
||||
} else if (change.type === "settings.set") {
|
||||
modified.settings[change.scope].set(change.name, change.value);
|
||||
} else if (change.type === "settings.delete") {
|
||||
modified.settings[change.scope].delete(change.name);
|
||||
} else {
|
||||
throw new Error(`Unknown change type ${change.type}`);
|
||||
}
|
||||
}
|
||||
setModifiedModPack(modified);
|
||||
}, [modPack, changes]);
|
||||
|
||||
let nav = [{ name: "Mods", path: "/mods" }, { name: "Mod Packs" }, { name: modPack.name || modPackId }];
|
||||
if (modifiedModPack.loading) {
|
||||
return <PageLayout nav={nav}>
|
||||
@@ -686,8 +711,8 @@ export default function ModPackViewPage() {
|
||||
okText="Delete"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => {
|
||||
libLink.messages.deleteModPack.send(
|
||||
control, { id: modPack.id }
|
||||
control.send(
|
||||
new libData.ModPackDeleteRequest(modPack.id)
|
||||
).then(() => {
|
||||
history.push("/mods");
|
||||
}).catch(notifyErrorHandler("Error deleting mod pack"));
|
||||
@@ -785,8 +810,8 @@ export default function ModPackViewPage() {
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
libLink.messages.updateModPack.send(
|
||||
control, { mod_pack: modifiedModPack.toJSON() }
|
||||
control.send(
|
||||
new libData.ModPackUpdateRequest(modifiedModPack)
|
||||
).then(() => {
|
||||
form.resetFields();
|
||||
setChanges([]);
|
||||
|
||||
@@ -5,7 +5,7 @@ import ImportOutlined from "@ant-design/icons/ImportOutlined";
|
||||
import PlusOutlined from "@ant-design/icons/PlusOutlined";
|
||||
import UploadOutlined from "@ant-design/icons/UploadOutlined";
|
||||
|
||||
import { libData, libHelpers, libLink, libLogging } from "@clusterio/lib";
|
||||
import { libData, libHelpers, libLogging } from "@clusterio/lib";
|
||||
|
||||
import { useAccount } from "../model/account";
|
||||
import { useModList } from "../model/mods";
|
||||
@@ -47,7 +47,7 @@ function ImportModPackButton() {
|
||||
return; // Validation failed
|
||||
}
|
||||
const modPack = libData.ModPack.fromModPackString(values.string);
|
||||
await libLink.messages.createModPack.send(control, { mod_pack: modPack.toJSON() });
|
||||
await control.send(new libData.ModPackCreateRequest(modPack));
|
||||
history.push(`/mods/mod-packs/${modPack.id}/view`);
|
||||
})().catch(notifyErrorHandler("Error creating mod pack"));
|
||||
}}
|
||||
@@ -95,11 +95,11 @@ function CreateModPackButton() {
|
||||
} catch {
|
||||
return; // Validation failed
|
||||
}
|
||||
const modPack = new libData.ModPack();
|
||||
const modPack = libData.ModPack.fromJSON({});
|
||||
modPack.name = values.name;
|
||||
modPack.factorioVersion = values.factorioVersion;
|
||||
if (values.description) { modPack.description = values.description; }
|
||||
await libLink.messages.createModPack.send(control, { mod_pack: modPack.toJSON() });
|
||||
await control.send(new libData.ModPackCreateRequest(modPack.toJSON()));
|
||||
history.push(`/mods/mod-packs/${modPack.id}/view`);
|
||||
})().catch(notifyErrorHandler("Error creating mod pack"));
|
||||
}}
|
||||
@@ -139,11 +139,11 @@ export default function ModsPage() {
|
||||
{account.hasPermission("core.mod.download")
|
||||
&& <Typography.Link
|
||||
onClick={() => {
|
||||
libLink.messages.downloadMod.send(
|
||||
control, { name: mod.name, version: mod.version }
|
||||
).then(response => {
|
||||
control.send(
|
||||
new libData.ModDownloadRequest(mod.name, mod.version)
|
||||
).then(streamId => {
|
||||
let url = new URL(webRoot, document.location);
|
||||
url.pathname += `api/stream/${response.stream_id}`;
|
||||
url.pathname += `api/stream/${streamId}`;
|
||||
document.location = url;
|
||||
}).catch(
|
||||
notifyErrorHandler("Error downloading save")
|
||||
@@ -153,10 +153,10 @@ export default function ModsPage() {
|
||||
}
|
||||
{account.hasPermission("core.mod.delete")
|
||||
&& <Popconfirm
|
||||
title="Are you sure you want to delete this mod?"
|
||||
title={`Delete ${mod.name}_${mod.version}.zip?`}
|
||||
onConfirm={event => {
|
||||
libLink.messages.deleteMod.send(
|
||||
control, { name: mod.name, version: mod.version }
|
||||
control.send(
|
||||
new libData.ModDeleteRequest(mod.name, mod.version)
|
||||
).catch(notifyErrorHandler("Error deleting mod"));
|
||||
}}
|
||||
okText="Delete"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useHistory, useParams } from "react-router-dom";
|
||||
import { Button, Checkbox, Form, Input, PageHeader, Popconfirm, Spin } from "antd";
|
||||
import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
|
||||
|
||||
import { libLink, libUsers } from "@clusterio/lib";
|
||||
import { libData, libUsers } from "@clusterio/lib";
|
||||
|
||||
import { useAccount } from "../model/account";
|
||||
import ControlContext from "./ControlContext";
|
||||
@@ -18,8 +18,8 @@ function useRole(id) {
|
||||
|
||||
function updateRole() {
|
||||
// XXX optimize by requesting only the role in question
|
||||
libLink.messages.listRoles.send(control).then(result => {
|
||||
let match = result.list.find(u => u.id === id);
|
||||
control.send(new libData.RoleListRequest()).then(roles => {
|
||||
let match = roles.find(u => u.id === id);
|
||||
if (!match) {
|
||||
setRole({ missing: true });
|
||||
} else {
|
||||
@@ -92,12 +92,9 @@ export default function RoleViewPage() {
|
||||
if (value) { newPermissions.push(name); }
|
||||
}
|
||||
|
||||
libLink.messages.updateRole.send(control, {
|
||||
id: roleId,
|
||||
name: values.name || "",
|
||||
description: values.description || "",
|
||||
permissions: newPermissions,
|
||||
}).then(() => {
|
||||
control.send(
|
||||
new libData.RoleUpdateRequest(roleId, values.name || "", values.description || "", newPermissions)
|
||||
).then(() => {
|
||||
setEdited(false);
|
||||
updateRole();
|
||||
}).catch(notifyErrorHandler("Error applying changes"));
|
||||
@@ -114,8 +111,8 @@ export default function RoleViewPage() {
|
||||
okText="Delete"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => {
|
||||
libLink.messages.deleteRole.send(
|
||||
control, { id: roleId }
|
||||
control.send(
|
||||
new libData.RoleDeleteRequest(roleId)
|
||||
).then(() => {
|
||||
history.push("/roles");
|
||||
}).catch(notifyErrorHandler("Error deleting role"));
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useContext, useEffect, useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Button, Form, Input, Modal, PageHeader, Table } from "antd";
|
||||
|
||||
import { libLink } from "@clusterio/lib";
|
||||
import { libData } from "@clusterio/lib";
|
||||
|
||||
import { useAccount } from "../model/account";
|
||||
import ControlContext from "./ControlContext";
|
||||
@@ -26,11 +26,9 @@ function CreateRoleButton() {
|
||||
return;
|
||||
}
|
||||
|
||||
let result = await libLink.messages.createRole.send(control, {
|
||||
name: values.roleName,
|
||||
description: values.description || "",
|
||||
permissions: [],
|
||||
});
|
||||
let result = await control.send(
|
||||
new libData.RoleCreateRequest(values.roleName, values.description || "", [])
|
||||
);
|
||||
setVisible(false);
|
||||
history.push(`/roles/${result.id}/view`);
|
||||
}
|
||||
@@ -68,8 +66,8 @@ export default function RolesPage() {
|
||||
let [roles, setRoles] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
libLink.messages.listRoles.send(control).then(result => {
|
||||
setRoles(result["list"]);
|
||||
control.send(new libData.RoleListRequest()).then(newRoles => {
|
||||
setRoles(newRoles);
|
||||
}).catch(notifyErrorHandler("Error fetching role list"));
|
||||
}, []);
|
||||
|
||||
@@ -84,7 +82,7 @@ export default function RolesPage() {
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
sorter: (a, b) => strcmp(a["name"], b["name"]),
|
||||
sorter: (a, b) => strcmp(a.name, b.name),
|
||||
},
|
||||
{
|
||||
title: "Description",
|
||||
@@ -93,10 +91,10 @@ export default function RolesPage() {
|
||||
]}
|
||||
dataSource={roles}
|
||||
pagination={false}
|
||||
rowKey={role => role["id"]}
|
||||
rowKey={role => role.id}
|
||||
onRow={(role, rowIndex) => ({
|
||||
onClick: event => {
|
||||
history.push(`/roles/${role["id"]}/view`);
|
||||
history.push(`/roles/${role.id}/view`);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import CaretLeftOutlined from "@ant-design/icons/CaretLeftOutlined";
|
||||
import LeftOutlined from "@ant-design/icons/LeftOutlined";
|
||||
|
||||
import { libLink, libHelpers } from "@clusterio/lib";
|
||||
import { libData, libHelpers } from "@clusterio/lib";
|
||||
|
||||
import { useAccount } from "../model/account";
|
||||
import ControlContext from "./ControlContext";
|
||||
@@ -36,8 +36,8 @@ function RenameModal(props) {
|
||||
layout="vertical"
|
||||
initialValues={{ newName: props.save.name }}
|
||||
onFinish={values => {
|
||||
libLink.messages.renameSave.send(control,
|
||||
{ instance_id: props.instanceId, old_name: props.save.name, new_name: values.newName }
|
||||
control.send(
|
||||
new libData.InstanceRenameSaveRequest(props.instanceId, props.save.name, values.newName)
|
||||
).then(() => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
@@ -75,8 +75,8 @@ function CopyModal(props) {
|
||||
layout="vertical"
|
||||
initialValues={{ newName: props.save.name }}
|
||||
onFinish={values => {
|
||||
libLink.messages.copySave.send(control,
|
||||
{ instance_id: props.instanceId, source: props.save.name, destination: values.newName }
|
||||
control.send(
|
||||
new libData.InstanceCopySaveRequest(props.instanceId, props.save.name, values.newName)
|
||||
).then(() => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
@@ -120,13 +120,13 @@ function TransferModal(props) {
|
||||
onFinish={values => {
|
||||
setVisible(false);
|
||||
let hide = message.loading("Transferring save...", 0);
|
||||
libLink.messages.transferSave.send(control, {
|
||||
instance_id: props.instanceId,
|
||||
source_save: props.save.name,
|
||||
target_instance_id: values.targetInstanceId,
|
||||
target_save: values.transferredName || props.save.name,
|
||||
copy: values.copy,
|
||||
}).then(() => {
|
||||
control.send(new libData.InstanceTransferSaveRequest(
|
||||
props.instanceId,
|
||||
props.save.name,
|
||||
values.targetInstanceId,
|
||||
values.transferredName || props.save.name,
|
||||
values.copy,
|
||||
)).then(() => {
|
||||
message.success("Transfer complete");
|
||||
form.resetFields();
|
||||
}).catch(
|
||||
@@ -209,9 +209,9 @@ export default function SavesList(props) {
|
||||
},
|
||||
{
|
||||
title: "Last Modified",
|
||||
key: "mtime_ms",
|
||||
render: save => new Date(save.mtime_ms).toLocaleString(),
|
||||
sorter: (a, b) => a.mtime_ms - b.mtime_ms,
|
||||
key: "mtimeMs",
|
||||
render: save => new Date(save.mtimeMs).toLocaleString(),
|
||||
sorter: (a, b) => a.mtimeMs - b.mtimeMs,
|
||||
defaultSortOrder: "descend",
|
||||
},
|
||||
]}
|
||||
@@ -227,8 +227,9 @@ export default function SavesList(props) {
|
||||
disabled={props.instance.status !== "stopped"}
|
||||
onClick={() => {
|
||||
setStarting(true);
|
||||
libLink.messages.startInstance.send(
|
||||
control, { instance_id: props.instance.id, save: save.name }
|
||||
control.sendTo(
|
||||
{ instanceId: props.instance.id },
|
||||
new libData.InstanceStartRequest(save.name),
|
||||
).catch(
|
||||
notifyErrorHandler("Error loading save")
|
||||
).finally(
|
||||
@@ -245,11 +246,11 @@ export default function SavesList(props) {
|
||||
{account.hasPermission("core.instance.save.download") && <Button
|
||||
disabled={hostOffline}
|
||||
onClick={() => {
|
||||
libLink.messages.downloadSave.send(
|
||||
control, { instance_id: props.instance.id, save: save.name }
|
||||
).then(response => {
|
||||
control.send(
|
||||
new libData.InstanceDownloadSaveRequest(props.instance.id, save.name)
|
||||
).then(streamId => {
|
||||
let url = new URL(webRoot, document.location);
|
||||
url.pathname += `api/stream/${response.stream_id}`;
|
||||
url.pathname += `api/stream/${streamId}`;
|
||||
document.location = url;
|
||||
}).catch(
|
||||
notifyErrorHandler("Error downloading save")
|
||||
@@ -265,8 +266,8 @@ export default function SavesList(props) {
|
||||
placement="top"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => {
|
||||
libLink.messages.deleteSave.send(
|
||||
control, { instance_id: props.instance.id, save: save.name }
|
||||
control.send(
|
||||
new libData.InstanceDeleteSaveRequest(props.instance.id, save.name)
|
||||
).catch(notifyErrorHandler("Error deleting save"));
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { Button } from "antd";
|
||||
|
||||
import { libLink } from "@clusterio/lib";
|
||||
import { libData } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "./ControlContext";
|
||||
import { notifyErrorHandler } from "../util/notify";
|
||||
@@ -16,15 +16,17 @@ export default function StartStopInstanceButton(props) {
|
||||
setSwitching(true);
|
||||
let action;
|
||||
if (props.instance["status"] === "stopped") {
|
||||
action = libLink.messages.startInstance.send(
|
||||
control, { instance_id: props.instance["id"], save: null }
|
||||
action = control.sendTo(
|
||||
{ instanceId: props.instance["id"] },
|
||||
new libData.InstanceStartRequest(undefined),
|
||||
).catch(
|
||||
notifyErrorHandler("Error starting instance")
|
||||
);
|
||||
|
||||
} else if (["starting", "running"].includes(props.instance["status"])) {
|
||||
action = libLink.messages.stopInstance.send(
|
||||
control, { instance_id: props.instance["id"] }
|
||||
action = control.sendTo(
|
||||
{ instanceId: props.instance["id"] },
|
||||
new libData.InstanceStopRequest(),
|
||||
).catch(
|
||||
notifyErrorHandler("Error stopping instance")
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "antd";
|
||||
import DeleteOutlined from "@ant-design/icons/DeleteOutlined";
|
||||
|
||||
import { libErrors, libLink } from "@clusterio/lib";
|
||||
import { libErrors, libData } from "@clusterio/lib";
|
||||
|
||||
import { useAccount } from "../model/account";
|
||||
import { useInstanceList } from "../model/instance";
|
||||
@@ -42,8 +42,8 @@ export default function UserViewPage() {
|
||||
let [rolesError, setRolesError] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
libLink.messages.listRoles.send(control).then(result => {
|
||||
setRoles(new Map(result["list"].map(role => [role["id"], role])));
|
||||
control.send(new libData.RoleListRequest()).then(newRoles => {
|
||||
setRoles(new Map(newRoles.map(role => [role.id, role])));
|
||||
}).catch(() => {
|
||||
setRoles(new Map());
|
||||
});
|
||||
@@ -53,9 +53,9 @@ export default function UserViewPage() {
|
||||
if (
|
||||
account.hasPermission("core.user.set_banned")
|
||||
&& !banReasonDirty
|
||||
&& user["ban_reason"] !== form.getFieldValue("ban_reason")
|
||||
&& user.banReason !== form.getFieldValue("banReason")
|
||||
) {
|
||||
form.setFieldsValue({ "ban_reason": user["ban_reason"] });
|
||||
form.setFieldsValue({ "banReason": user.banReason });
|
||||
}
|
||||
}, [account, form, user, banReasonDirty]);
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function UserViewPage() {
|
||||
let roleSelector = <Form.Item
|
||||
noStyle
|
||||
name="roles"
|
||||
initialValue={user["roles"]}
|
||||
initialValue={user.roles}
|
||||
rules={[
|
||||
{ validator: () => (rolesError ? Promise.reject(rolesError) : Promise.resolve()) },
|
||||
]}
|
||||
@@ -95,19 +95,19 @@ export default function UserViewPage() {
|
||||
showArrow={true}
|
||||
filterOption={(inputValue, option) => {
|
||||
let role = roles.get(option.value);
|
||||
return role && role["name"].toLowerCase().includes(inputValue.toLowerCase());
|
||||
return role && role.name.toLowerCase().includes(inputValue.toLowerCase());
|
||||
}}
|
||||
>
|
||||
{roles && [...roles.values()].map(r => <Select.Option
|
||||
key={r["id"]}
|
||||
value={r["id"]}
|
||||
>{r["name"]}</Select.Option>)}
|
||||
key={r.id}
|
||||
value={r.id}
|
||||
>{r.name}</Select.Option>)}
|
||||
</Select>
|
||||
</Form.Item>;
|
||||
|
||||
function instanceName(id) {
|
||||
let instance = instanceList.find(i => i.id === id);
|
||||
return instance ? instance["name"] : id;
|
||||
return instance ? instance.name : id;
|
||||
}
|
||||
|
||||
return <PageLayout nav={nav}>
|
||||
@@ -116,9 +116,9 @@ export default function UserViewPage() {
|
||||
title={<Space>
|
||||
{userName}
|
||||
<span>
|
||||
{user["is_admin"] && <Tag color="gold">Admin</Tag>}
|
||||
{user["is_whitelisted"] && <Tag>Whitelisted</Tag>}
|
||||
{user["is_banned"] && <Tag color="red">Banned</Tag>}
|
||||
{user.isAdmin && <Tag color="gold">Admin</Tag>}
|
||||
{user.isWhitelisted && <Tag>Whitelisted</Tag>}
|
||||
{user.isBanned && <Tag color="red">Banned</Tag>}
|
||||
</span>
|
||||
</Space>}
|
||||
extra={<>
|
||||
@@ -127,8 +127,8 @@ export default function UserViewPage() {
|
||||
) && <Button
|
||||
danger
|
||||
onClick={() => {
|
||||
libLink.messages.revokeUserToken.send(
|
||||
control, { name: userName }
|
||||
control.send(
|
||||
new libData.UserRevokeTokenRequest(userName)
|
||||
).then(() => {
|
||||
notify("User token revoked");
|
||||
}, err => {
|
||||
@@ -144,14 +144,14 @@ export default function UserViewPage() {
|
||||
{account.hasPermission("core.user.delete") && <Popconfirm
|
||||
title={<>
|
||||
Delete user account and all data associated with it?
|
||||
{(user["is_banned"] && <><br/>This will remove the user ban!</>)}
|
||||
{(user.isBanned && <><br/>This will remove the user ban!</>)}
|
||||
</>}
|
||||
placement="bottomRight"
|
||||
okText="Delete"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => {
|
||||
libLink.messages.deleteUser.send(
|
||||
control, { name: userName }
|
||||
control.send(
|
||||
new libData.UserDeleteRequest(userName)
|
||||
).then(() => {
|
||||
history.push("/users");
|
||||
}).catch(notifyErrorHandler("Error deleting user"));
|
||||
@@ -187,8 +187,8 @@ export default function UserViewPage() {
|
||||
<Col flex="auto">
|
||||
{account.hasPermission("core.user.update_roles")
|
||||
? roleSelector
|
||||
: [...user["roles"]].map(id => <Tag key={id}>{
|
||||
roles ? (roles.get(id) || { name: id })["name"] : id
|
||||
: [...user.roles].map(id => <Tag key={id}>{
|
||||
roles ? (roles.get(id) || { name: id }).name : id
|
||||
}</Tag>)
|
||||
}
|
||||
</Col>
|
||||
@@ -200,10 +200,9 @@ export default function UserViewPage() {
|
||||
onClick={() => {
|
||||
let newRoles = form.getFieldValue("roles");
|
||||
setApplyingRoles(true);
|
||||
libLink.messages.updateUserRoles.send(control, {
|
||||
name: userName,
|
||||
roles: newRoles,
|
||||
}).then(() => {
|
||||
control.send(
|
||||
new libData.UserUpdateRolesRequest(userName, newRoles)
|
||||
).then(() => {
|
||||
setRolesDirty(false);
|
||||
setRolesError();
|
||||
}).catch(err => {
|
||||
@@ -219,25 +218,21 @@ export default function UserViewPage() {
|
||||
</Form.Item>
|
||||
{account.hasPermission("core.user.set_admin") && <Form.Item label="In-game Admin">
|
||||
<Switch
|
||||
checked={user["is_admin"]}
|
||||
checked={user.isAdmin}
|
||||
onClick={() => {
|
||||
libLink.messages.setUserAdmin.send(control, {
|
||||
name: userName,
|
||||
admin: !user["is_admin"],
|
||||
create: false,
|
||||
}).catch(notifyErrorHandler("Error toggling user admin status"));
|
||||
control.send(
|
||||
new libData.UserSetAdminRequest(userName, false, !user.isAdmin)
|
||||
).catch(notifyErrorHandler("Error toggling user admin status"));
|
||||
}}
|
||||
/>
|
||||
</Form.Item>}
|
||||
{account.hasPermission("core.user.set_whitelisted") && <Form.Item label="Whitelisted">
|
||||
<Switch
|
||||
checked={user["is_whitelisted"]}
|
||||
checked={user.isWhitelisted}
|
||||
onClick={() => {
|
||||
libLink.messages.setUserWhitelisted.send(control, {
|
||||
name: userName,
|
||||
whitelisted: !user["is_whitelisted"],
|
||||
create: false,
|
||||
}).catch(notifyErrorHandler("Error toggling user whitelisted status"));
|
||||
control.send(
|
||||
new libData.UserSetWhitelistedRequest(userName, false, !user.isWhitelisted)
|
||||
).catch(notifyErrorHandler("Error toggling user whitelisted status"));
|
||||
}}
|
||||
/>
|
||||
</Form.Item>}
|
||||
@@ -245,50 +240,45 @@ export default function UserViewPage() {
|
||||
? <Form.Item label="Ban reason">
|
||||
<Row gutter={[8, 8]} justify={"end"}>
|
||||
<Col flex="auto" style={{ minWidth: "20em" }}>
|
||||
<Form.Item noStyle name="ban_reason" initialValue={user["ban_reason"]}>
|
||||
<Form.Item noStyle name="banReason" initialValue={user.banReason}>
|
||||
<Input
|
||||
onChange={e => {
|
||||
setBanReasonDirty(e.target.value !== user["ban_reason"]);
|
||||
setBanReasonDirty(e.target.value !== user.banReason);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col flex="0 0 auto">
|
||||
{user["is_banned"]
|
||||
{user.isBanned
|
||||
? <Space>
|
||||
<Button
|
||||
type={banReasonDirty ? "primary" : "default"}
|
||||
onClick={() => {
|
||||
libLink.messages.setUserBanned.send(control, {
|
||||
name: userName,
|
||||
create: false,
|
||||
banned: true,
|
||||
reason: form.getFieldValue("ban_reason"),
|
||||
}).then(() => {
|
||||
control.send(
|
||||
new libData.UserSetBannedRequest(
|
||||
userName, false, true, form.getFieldValue("banReason")
|
||||
)
|
||||
).then(() => {
|
||||
setBanReasonDirty(false);
|
||||
}).catch(notifyErrorHandler("Error updating ban"));
|
||||
}}
|
||||
>Update</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
libLink.messages.setUserBanned.send(control, {
|
||||
name: userName,
|
||||
create: false,
|
||||
banned: false,
|
||||
reason: "",
|
||||
}).catch(notifyErrorHandler("Error unbanning user"));
|
||||
control.send(
|
||||
new libData.UserSetBannedRequest(userName, false, false, "")
|
||||
).catch(notifyErrorHandler("Error unbanning user"));
|
||||
}}
|
||||
>Unban</Button>
|
||||
</Space>
|
||||
: <Button
|
||||
type={banReasonDirty ? "primary" : "default"}
|
||||
onClick={() => {
|
||||
libLink.messages.setUserBanned.send(control, {
|
||||
name: userName,
|
||||
create: false,
|
||||
banned: true,
|
||||
reason: form.getFieldValue("ban_reason"),
|
||||
}).then(() => {
|
||||
control.send(
|
||||
new libData.UserSetBannedRequest(
|
||||
userName, false, true, form.getFieldValue("banReason")
|
||||
)
|
||||
).then(() => {
|
||||
setBanReasonDirty(false);
|
||||
}).catch(notifyErrorHandler("Error banning user"));
|
||||
}}
|
||||
@@ -297,17 +287,17 @@ export default function UserViewPage() {
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
: user["is_banned"]
|
||||
&& <Form.Item label="Ban reason">{user["ban_reason"]}</Form.Item>
|
||||
: user.isBanned
|
||||
&& <Form.Item label="Ban reason">{user.banReason}</Form.Item>
|
||||
}
|
||||
</Form>
|
||||
<SectionHeader title="Player stats" />
|
||||
<Descriptions size="small" bordered column={{ xs: 1, sm: 2, lg: 3 }}>
|
||||
<Descriptions.Item label="Total online time">
|
||||
{formatDuration(user["player_stats"]["online_time_ms"] || 0)}
|
||||
{formatDuration(user.playerStats.onlineTimeMs || 0)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Total join count">
|
||||
{user["player_stats"]["join_count"] || 0}
|
||||
{user.playerStats.joinCount || 0}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Last seen">
|
||||
{formatLastSeen(user) || " "}
|
||||
@@ -327,14 +317,14 @@ export default function UserViewPage() {
|
||||
{
|
||||
title: "Online time",
|
||||
key: "onlineTime",
|
||||
render: ([, stats]) => formatDuration(stats["online_time_ms"] || 0),
|
||||
sorter: (a, b) => (a[1]["online_time_ms"] || 0) - (b[1]["online_time_ms"] || 0),
|
||||
render: ([, stats]) => formatDuration(stats.onlineTimeMs || 0),
|
||||
sorter: (a, b) => (a[1].onlineTimeMs || 0) - (b[1].onlineTimeMs || 0),
|
||||
},
|
||||
{
|
||||
title: "Join count",
|
||||
key: "joinCoint",
|
||||
render: ([, stats]) => stats["join_count"] || 0,
|
||||
sorter: (a, b) => (a[1]["join_count"] || 0) - (b[1]["join_count"] || 0),
|
||||
render: ([, stats]) => stats.joinCount || 0,
|
||||
sorter: (a, b) => (a[1].joinCount || 0) - (b[1].joinCount || 0),
|
||||
responsive: ["sm"],
|
||||
},
|
||||
{
|
||||
@@ -344,7 +334,7 @@ export default function UserViewPage() {
|
||||
sorter: (a, b) => sortLastSeen(user, user, a[0], b[0]),
|
||||
},
|
||||
]}
|
||||
dataSource={user["instance_stats"]}
|
||||
dataSource={[...user.instanceStats.entries()]}
|
||||
pagination={false}
|
||||
rowKey={([id]) => id}
|
||||
onRow={([id], rowIndex) => ({
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useContext, useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { Button, Form, Input, Modal, PageHeader, Space, Table, Tag } from "antd";
|
||||
|
||||
import { libLink } from "@clusterio/lib";
|
||||
import { libData } from "@clusterio/lib";
|
||||
|
||||
import { useAccount } from "../model/account";
|
||||
import ControlContext from "./ControlContext";
|
||||
@@ -28,7 +28,7 @@ function CreateUserButton() {
|
||||
return;
|
||||
}
|
||||
|
||||
await libLink.messages.createUser.send(control, { name: values.userName });
|
||||
await control.send(new libData.UserCreateRequest(values.userName));
|
||||
setVisible(false);
|
||||
history.push(`/users/${values.userName}/view`);
|
||||
}
|
||||
@@ -64,8 +64,8 @@ export default function UsersPage() {
|
||||
let [roles, setRoles] = useState(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
libLink.messages.listRoles.send(control).then(result => {
|
||||
setRoles(new Map(result["list"].map(role => [role["id"], role])));
|
||||
control.send(new libData.RoleListRequest()).then(newRoles => {
|
||||
setRoles(new Map(newRoles.map(role => [role.id, role])));
|
||||
}).catch(() => {
|
||||
// ignore
|
||||
});
|
||||
@@ -83,28 +83,28 @@ export default function UsersPage() {
|
||||
title: "Name",
|
||||
key: "name",
|
||||
render: user => <Space>
|
||||
{user["name"]}
|
||||
{user.name}
|
||||
<span>
|
||||
{user["is_admin"] && <Tag color="gold">Admin</Tag>}
|
||||
{user["is_whitelisted"] && <Tag>Whitelisted</Tag>}
|
||||
{user["is_banned"] && <Tag color="red">Banned</Tag>}
|
||||
{user.isAdmin && <Tag color="gold">Admin</Tag>}
|
||||
{user.isWhitelisted && <Tag>Whitelisted</Tag>}
|
||||
{user.isBanned && <Tag color="red">Banned</Tag>}
|
||||
</span>
|
||||
</Space>,
|
||||
defaultSortOrder: "ascend",
|
||||
sorter: (a, b) => strcmp(a["name"], b["name"]),
|
||||
sorter: (a, b) => strcmp(a.name, b.name),
|
||||
},
|
||||
{
|
||||
title: "Roles",
|
||||
key: "roles",
|
||||
render: user => user.roles.map(id => <Tag key={id}>{(roles.get(id) || { name: id })["name"]}</Tag>),
|
||||
render: user => user.roles.map(id => <Tag key={id}>{(roles.get(id) || { name: id }).name}</Tag>),
|
||||
},
|
||||
{
|
||||
title: "Online time",
|
||||
key: "onlineTime",
|
||||
render: user => user["player_stats"]["online_time_ms"]
|
||||
&& formatDuration(user["player_stats"]["online_time_ms"]),
|
||||
sorter: (a, b) => (a["player_stats"]["online_time_ms"] || 0) -
|
||||
(b["player_stats"]["online_time_ms"] || 0),
|
||||
render: user => (user.playerStats.onlineTimeMs
|
||||
? formatDuration(user.playerStats.onlineTimeMs) : null),
|
||||
sorter: (a, b) => (a.playerStats.onlineTimeMs || 0) -
|
||||
(b.playerStats.onlineTimeMs || 0),
|
||||
responsive: ["lg"],
|
||||
},
|
||||
{
|
||||
@@ -117,10 +117,10 @@ export default function UsersPage() {
|
||||
]}
|
||||
dataSource={userList}
|
||||
pagination={false}
|
||||
rowKey={user => user["name"]}
|
||||
rowKey={user => user.name}
|
||||
onRow={(user, rowIndex) => ({
|
||||
onClick: event => {
|
||||
history.push(`/users/${user["name"]}/view`);
|
||||
history.push(`/users/${user.name}/view`);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useContext, useState } from "react";
|
||||
|
||||
import { libData, libLink } from "@clusterio/lib";
|
||||
import { libData } from "@clusterio/lib";
|
||||
import ControlContext from "../components/ControlContext";
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ export function useExportManifest() {
|
||||
let [exportManifest, setExportManifest] = useState(exportManifestCache || emptyCache);
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
let result = await libLink.messages.getDefaultModPack.send(control);
|
||||
let modPack = new libData.ModPack(result.mod_pack);
|
||||
let modPack = await control.send(new libData.ModPackGetDefaultRequest());
|
||||
if (modPack.exportManifest) {
|
||||
exportManifestCache = modPack.exportManifest;
|
||||
setExportManifest(exportManifestCache);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useContext, useState } from "react";
|
||||
import ControlContext from "../components/ControlContext";
|
||||
|
||||
import { libLink } from "@clusterio/lib";
|
||||
import { libData } from "@clusterio/lib";
|
||||
|
||||
|
||||
export function useHost(id) {
|
||||
@@ -10,8 +10,8 @@ export function useHost(id) {
|
||||
|
||||
function updateHost() {
|
||||
// XXX optimize by requesting only the host in question
|
||||
libLink.messages.listHosts.send(control).then(result => {
|
||||
let match = result.list.find(i => i.id === id);
|
||||
control.send(new libData.HostListRequest()).then(hosts => {
|
||||
let match = hosts.find(i => i.id === id);
|
||||
if (!match) {
|
||||
setHost({ missing: true });
|
||||
} else {
|
||||
@@ -45,8 +45,8 @@ export function useHostList() {
|
||||
let [hostList, setHostList] = useState([]);
|
||||
|
||||
function updateHostList() {
|
||||
libLink.messages.listHosts.send(control).then(result => {
|
||||
setHostList(result.list);
|
||||
control.send(new libData.HostListRequest()).then(hosts => {
|
||||
setHostList(hosts);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useContext, useState } from "react";
|
||||
import ControlContext from "../components/ControlContext";
|
||||
|
||||
import { libLink, libLogging } from "@clusterio/lib";
|
||||
import { libData, libLogging } from "@clusterio/lib";
|
||||
const { logger } = libLogging;
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ export function useInstance(id) {
|
||||
let [instance, setInstance] = useState({ loading: true });
|
||||
|
||||
function updateInstance() {
|
||||
libLink.messages.getInstance.send(control, { id }).then(result => {
|
||||
setInstance({ ...result, present: true });
|
||||
control.send(new libData.InstanceDetailsGetRequest(id)).then(updatedInstance => {
|
||||
setInstance({ ...updatedInstance, present: true });
|
||||
}).catch(err => {
|
||||
logger.error(`Failed to get instance: ${err}`);
|
||||
setInstance({ missing: true });
|
||||
@@ -44,8 +44,8 @@ export function useInstanceList() {
|
||||
let [instanceList, setInstanceList] = useState([]);
|
||||
|
||||
function updateInstanceList() {
|
||||
libLink.messages.listInstances.send(control).then(result => {
|
||||
setInstanceList(result.list);
|
||||
control.send(new libData.InstanceDetailsListRequest()).then(instances => {
|
||||
setInstanceList(instances);
|
||||
}).catch(err => {
|
||||
logger.error(`Failed to list instances:\n${err}`);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ export function useItemMetadata() {
|
||||
async function load() {
|
||||
if (
|
||||
!exportManifest
|
||||
|| !exportManifest.assets
|
||||
|| !exportManifest.assets["item-metadata"]
|
||||
|| !exportManifest.assets["item-spritesheet"]
|
||||
) {
|
||||
|
||||
@@ -9,7 +9,7 @@ export function useLocale() {
|
||||
let [locale, setLocale] = useState(localeCache || new Map());
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
if (!exportManifest || !exportManifest.assets["locale"]) {
|
||||
if (!exportManifest || !exportManifest.assets || !exportManifest.assets["locale"]) {
|
||||
return;
|
||||
}
|
||||
let response = await fetch(`${staticRoot}static/${exportManifest.assets["locale"]}`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
|
||||
import { libData, libLink, libLogging } from "@clusterio/lib";
|
||||
import { libData, libLogging } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "../components/ControlContext";
|
||||
|
||||
@@ -11,8 +11,8 @@ export function useModPack(id) {
|
||||
let [modPack, setModPack] = useState({ loading: true });
|
||||
|
||||
function updateModPack() {
|
||||
libLink.messages.getModPack.send(control, { id }).then(result => {
|
||||
setModPack(new libData.ModPack(result.mod_pack));
|
||||
control.send(new libData.ModPackGetRequest(id)).then(updatedModPack => {
|
||||
setModPack(updatedModPack);
|
||||
}).catch(err => {
|
||||
logger.error(`Failed to get mod pack: ${err}`);
|
||||
setModPack({ missing: true });
|
||||
@@ -40,8 +40,8 @@ export function useModPackList() {
|
||||
let [modPackList, setModPackList] = useState([]);
|
||||
|
||||
function updateModPackList() {
|
||||
libLink.messages.listModPacks.send(control).then(result => {
|
||||
setModPackList(result.list.map(pack => new libData.ModPack(pack)));
|
||||
control.send(new libData.ModPackListRequest()).then(modPacks => {
|
||||
setModPackList(modPacks);
|
||||
}).catch(err => {
|
||||
logger.error(`Failed to list mod packs:\n${err}`);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
|
||||
import { libData, libLink, libLogging } from "@clusterio/lib";
|
||||
import { libData, libLogging } from "@clusterio/lib";
|
||||
|
||||
import ControlContext from "../components/ControlContext";
|
||||
|
||||
@@ -12,8 +12,8 @@ export function useModList() {
|
||||
let [modList, setModList] = useState([]);
|
||||
|
||||
function updateModList() {
|
||||
libLink.messages.listMods.send(control).then(result => {
|
||||
setModList(result.list.map(mod => new libData.ModInfo(mod)));
|
||||
control.send(new libData.ModListRequest()).then(mods => {
|
||||
setModList(mods);
|
||||
}).catch(err => {
|
||||
logger.error(`Failed to list mods:\n${err}`);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useContext, useState } from "react";
|
||||
import ControlContext from "../components/ControlContext";
|
||||
|
||||
import { libLink, libLogging } from "@clusterio/lib";
|
||||
import { libData, libLogging } from "@clusterio/lib";
|
||||
const { logger } = libLogging;
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ export function useSaves(instanceId) {
|
||||
let [saves, setSaves] = useState([]);
|
||||
|
||||
function updateSaves() {
|
||||
libLink.messages.listSaves.send(control, { instance_id: instanceId }).then(result => {
|
||||
setSaves(result.list);
|
||||
control.sendTo({ instanceId }, new libData.InstanceListSavesRequest()).then(updatedSaves => {
|
||||
setSaves(updatedSaves);
|
||||
}).catch(err => {
|
||||
logger.error(`Failed to list instance saves: ${err}`);
|
||||
setSaves([]);
|
||||
@@ -26,7 +26,7 @@ export function useSaves(instanceId) {
|
||||
updateSaves();
|
||||
|
||||
function updateHandler(data) {
|
||||
setSaves(data.list);
|
||||
setSaves(data.saves);
|
||||
}
|
||||
|
||||
control.onSaveListUpdate(instanceId, updateHandler);
|
||||
|
||||
@@ -2,31 +2,30 @@ import React, { useEffect, useContext, useState } from "react";
|
||||
import { Tag } from "antd";
|
||||
import ControlContext from "../components/ControlContext";
|
||||
|
||||
import { libLink, libLogging } from "@clusterio/lib";
|
||||
import { libData, libLogging } from "@clusterio/lib";
|
||||
const { logger } = libLogging;
|
||||
|
||||
function calculateLastSeen(user, instanceId) {
|
||||
let stats;
|
||||
if (instanceId === undefined) {
|
||||
stats = user["player_stats"];
|
||||
stats = user.playerStats;
|
||||
} else {
|
||||
let entry = (user["instance_stats"] || []).find(([id]) => id === instanceId);
|
||||
if (!entry) {
|
||||
stats = (user.instanceStats || new Map()).get(instanceId);
|
||||
if (!stats) {
|
||||
return undefined;
|
||||
}
|
||||
stats = entry[1];
|
||||
}
|
||||
if (stats["last_leave_at"] > stats["last_join_at"]) {
|
||||
return stats["last_leave_at"];
|
||||
if (stats.lastLeaveAt > stats.lastJoinAt) {
|
||||
return stats.lastLeaveAt;
|
||||
}
|
||||
if (stats["last_join_at"]) {
|
||||
return stats["last_leave_at"];
|
||||
if (stats.lastJoinAt) {
|
||||
return stats.lastLeaveAt;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function formatLastSeen(user, instanceId = undefined) {
|
||||
if (user["instances"].some(id => instanceId === undefined || id === instanceId)) {
|
||||
if (user.instances.some(id => instanceId === undefined || id === instanceId)) {
|
||||
return <Tag color="green">Online</Tag>;
|
||||
}
|
||||
let lastSeen = calculateLastSeen(user, instanceId);
|
||||
@@ -38,7 +37,7 @@ export function formatLastSeen(user, instanceId = undefined) {
|
||||
|
||||
export function sortLastSeen(userA, userB, instanceIdA = undefined, instanceIdB = undefined) {
|
||||
function epoch(user, instanceId) {
|
||||
return user["instances"].some(id => instanceId === undefined || id === instanceId);
|
||||
return user.instances.some(id => instanceId === undefined || id === instanceId);
|
||||
}
|
||||
|
||||
let epochA = epoch(userA, instanceIdA);
|
||||
@@ -57,8 +56,8 @@ export function useUser(name) {
|
||||
let [user, setUser] = useState({ loading: true });
|
||||
|
||||
function updateUser() {
|
||||
libLink.messages.getUser.send(control, { name }).then(result => {
|
||||
setUser({ ...result, present: true });
|
||||
control.send(new libData.UserGetRequest(name)).then(updatedUser => {
|
||||
setUser({ ...updatedUser, present: true });
|
||||
}).catch(err => {
|
||||
logger.error(`Failed to get user: ${err}`);
|
||||
setUser({ missing: true });
|
||||
@@ -91,8 +90,8 @@ export function useUserList() {
|
||||
let [userList, setUserList] = useState([]);
|
||||
|
||||
function updateUserList() {
|
||||
libLink.messages.listUsers.send(control).then(result => {
|
||||
setUserList(result.list);
|
||||
control.send(new libData.UserListRequest()).then(users => {
|
||||
setUserList(users);
|
||||
}).catch(err => {
|
||||
logger.error(`Failed to list users:\n${err}`);
|
||||
});
|
||||
@@ -105,7 +104,7 @@ export function useUserList() {
|
||||
setUserList(oldList => {
|
||||
let newList = oldList.concat();
|
||||
let index = newList.findIndex(u => u.name === newUser.name);
|
||||
if (!newUser.is_deleted) {
|
||||
if (!newUser.isDeleted) {
|
||||
if (index !== -1) {
|
||||
newList[index] = newUser;
|
||||
} else {
|
||||
|
||||
@@ -19,11 +19,15 @@ export class ControlConnector extends libLink.WebSocketClientConnector {
|
||||
}
|
||||
|
||||
logger.verbose("Connector | registering control");
|
||||
this.sendHandshake("register_control", {
|
||||
token: this.token,
|
||||
agent: "Clusterio Web UI",
|
||||
version: packageJson.version,
|
||||
});
|
||||
this.sendHandshake(
|
||||
new libData.MessageRegisterControl(
|
||||
new libData.RegisterControlData(
|
||||
this.token,
|
||||
"Clusterio Web UI",
|
||||
packageJson.version,
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +39,7 @@ export class ControlConnector extends libLink.WebSocketClientConnector {
|
||||
*/
|
||||
export class Control extends libLink.Link {
|
||||
constructor(connector, plugins) {
|
||||
super("control", "controller", connector);
|
||||
libLink.attachAllMessages(this);
|
||||
super(connector);
|
||||
|
||||
/**
|
||||
* Mapping of plugin names to their instance for loaded plugins.
|
||||
@@ -45,7 +48,6 @@ export class Control extends libLink.Link {
|
||||
this.plugins = plugins;
|
||||
for (let plugin of plugins.values()) {
|
||||
plugin.control = this;
|
||||
libPlugin.attachPluginMessages(this, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,10 +117,20 @@ export class Control extends libLink.Link {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.handle(libData.AccountUpdateEvent, this.handleAccountUpdateEvent.bind(this));
|
||||
this.handle(libData.HostUpdateEvent, this.handleHostUpdateEvent.bind(this));
|
||||
this.handle(libData.InstanceDetailsUpdateEvent, this.handleInstanceDetailsUpdateEvent.bind(this));
|
||||
this.handle(libData.InstanceSaveListUpdateEvent, this.handleInstanceSaveListUpdateEvent.bind(this));
|
||||
this.handle(libData.ModPackUpdateEvent, this.handleModPackUpdateEvent.bind(this));
|
||||
this.handle(libData.ModUpdateEvent, this.handleModUpdateEvent.bind(this));
|
||||
this.handle(libData.UserUpdateEvent, this.handleUserUpdateEvent.bind(this));
|
||||
this.handle(libData.LogMessageEvent, this.handleLogMessageEvent.bind(this));
|
||||
this.handle(libData.DebugWsMessageEvent, this.handleDebugWsMessageEvent.bind(this));
|
||||
}
|
||||
|
||||
async accountUpdateEventHandler(message) {
|
||||
this.accountRoles = message.data.roles;
|
||||
async handleAccountUpdateEvent(event) {
|
||||
this.accountRoles = event.roles;
|
||||
this.emitAccountUpdate();
|
||||
}
|
||||
|
||||
@@ -143,13 +155,13 @@ export class Control extends libLink.Link {
|
||||
this.accountUpdateHandlers.splice(index, 1);
|
||||
}
|
||||
|
||||
async hostUpdateEventHandler(message) {
|
||||
async handleHostUpdateEvent(event) {
|
||||
let handlers = [].concat(
|
||||
this.hostUpdateHandlers.get(null) || [],
|
||||
this.hostUpdateHandlers.get(message.data.id) || [],
|
||||
this.hostUpdateHandlers.get(event.update.id) || [],
|
||||
);
|
||||
for (let handler of handlers) {
|
||||
handler(message.data);
|
||||
handler(event.update);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,19 +206,19 @@ export class Control extends libLink.Link {
|
||||
return;
|
||||
}
|
||||
|
||||
await libLink.messages.setHostSubscriptions.send(this, {
|
||||
all: this.hostUpdateHandlers.has(null),
|
||||
host_ids: [...this.hostUpdateHandlers.keys()].filter(e => e !== null),
|
||||
});
|
||||
await this.send(new libData.HostSetSubscriptionsRequest(
|
||||
this.hostUpdateHandlers.has(null),
|
||||
[...this.hostUpdateHandlers.keys()].filter(e => e !== null),
|
||||
));
|
||||
}
|
||||
|
||||
async instanceUpdateEventHandler(message) {
|
||||
async handleInstanceDetailsUpdateEvent(event) {
|
||||
let handlers = [].concat(
|
||||
this.instanceUpdateHandlers.get(null) || [],
|
||||
this.instanceUpdateHandlers.get(message.data.id) || [],
|
||||
this.instanceUpdateHandlers.get(event.details.id) || [],
|
||||
);
|
||||
for (let handler of handlers) {
|
||||
handler(message.data);
|
||||
handler(event.details);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -251,16 +263,16 @@ export class Control extends libLink.Link {
|
||||
return;
|
||||
}
|
||||
|
||||
await libLink.messages.setInstanceSubscriptions.send(this, {
|
||||
all: this.instanceUpdateHandlers.has(null),
|
||||
instance_ids: [...this.instanceUpdateHandlers.keys()].filter(e => e !== null),
|
||||
});
|
||||
await this.send(new libData.InstanceDetailsSetSubscriptionsRequest(
|
||||
this.instanceUpdateHandlers.has(null),
|
||||
[...this.instanceUpdateHandlers.keys()].filter(e => e !== null),
|
||||
));
|
||||
}
|
||||
|
||||
async saveListUpdateEventHandler(message) {
|
||||
let handlers = this.saveListUpdateHandlers.get(message.data.instance_id);
|
||||
async handleInstanceSaveListUpdateEvent(event) {
|
||||
let handlers = this.saveListUpdateHandlers.get(event.instanceId);
|
||||
for (let handler of handlers || []) {
|
||||
handler(message.data);
|
||||
handler(event);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -305,14 +317,14 @@ export class Control extends libLink.Link {
|
||||
return;
|
||||
}
|
||||
|
||||
await libLink.messages.setSaveListSubscriptions.send(this, {
|
||||
all: false,
|
||||
instance_ids: [...this.saveListUpdateHandlers.keys()],
|
||||
});
|
||||
await this.send(new libData.InstanceSetSaveListSubscriptionsRequest(
|
||||
false,
|
||||
[...this.saveListUpdateHandlers.keys()],
|
||||
));
|
||||
}
|
||||
|
||||
async modPackUpdateEventHandler(message) {
|
||||
let modPack = new libData.ModPack(message.data.mod_pack);
|
||||
async handleModPackUpdateEvent(event) {
|
||||
let modPack = event.modPack;
|
||||
let handlers = [].concat(
|
||||
this.modPackUpdateHandlers.get(null) || [],
|
||||
this.modPackUpdateHandlers.get(modPack.id) || [],
|
||||
@@ -363,14 +375,14 @@ export class Control extends libLink.Link {
|
||||
return;
|
||||
}
|
||||
|
||||
await libLink.messages.setModPackSubscriptions.send(this, {
|
||||
all: this.modPackUpdateHandlers.has(null),
|
||||
mod_pack_ids: [...this.modPackUpdateHandlers.keys()].filter(k => k !== null),
|
||||
});
|
||||
await this.send(new libData.ModPackSetSubscriptionsRequest(
|
||||
this.modPackUpdateHandlers.has(null),
|
||||
[...this.modPackUpdateHandlers.keys()].filter(k => k !== null),
|
||||
));
|
||||
}
|
||||
|
||||
async modUpdateEventHandler(message) {
|
||||
let mod = new libData.ModInfo(message.data.mod);
|
||||
async handleModUpdateEvent(event) {
|
||||
let mod = event.mod;
|
||||
let handlers = [].concat(
|
||||
this.modUpdateHandlers.get(null) || [],
|
||||
this.modUpdateHandlers.get(mod.name) || []
|
||||
@@ -421,19 +433,19 @@ export class Control extends libLink.Link {
|
||||
return;
|
||||
}
|
||||
|
||||
await libLink.messages.setModSubscriptions.send(this, {
|
||||
all: this.modUpdateHandlers.has(null),
|
||||
mod_names: [...this.modUpdateHandlers.keys()].filter(k => k !== null),
|
||||
});
|
||||
await this.send(new libData.ModSetSubscriptionsRequest(
|
||||
this.modUpdateHandlers.has(null),
|
||||
[...this.modUpdateHandlers.keys()].filter(k => k !== null),
|
||||
));
|
||||
}
|
||||
|
||||
async userUpdateEventHandler(message) {
|
||||
async handleUserUpdateEvent(event) {
|
||||
let handlers = [].concat(
|
||||
this.userUpdateHandlers.get(null) || [],
|
||||
this.userUpdateHandlers.get(message.data.name) || [],
|
||||
this.userUpdateHandlers.get(event.user.name) || [],
|
||||
);
|
||||
for (let handler of handlers) {
|
||||
handler(message.data);
|
||||
handler(event.user);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -478,14 +490,14 @@ export class Control extends libLink.Link {
|
||||
return;
|
||||
}
|
||||
|
||||
await libLink.messages.setUserSubscriptions.send(this, {
|
||||
all: this.userUpdateHandlers.has(null),
|
||||
names: [...this.userUpdateHandlers.keys()].filter(e => e !== null),
|
||||
});
|
||||
await this.send(new libData.UserSetSubscriptionsRequest(
|
||||
this.userUpdateHandlers.has(null),
|
||||
[...this.userUpdateHandlers.keys()].filter(e => e !== null),
|
||||
));
|
||||
}
|
||||
|
||||
async logMessageEventHandler(message) {
|
||||
let info = message.data.info;
|
||||
async handleLogMessageEvent(event) {
|
||||
let info = event.info;
|
||||
|
||||
for (let [filter, handlers] of this.logHandlers) {
|
||||
if (logFilter(filter)(info)) {
|
||||
@@ -533,45 +545,41 @@ export class Control extends libLink.Link {
|
||||
return;
|
||||
}
|
||||
|
||||
let combinedFilter = {
|
||||
all: false,
|
||||
controller: false,
|
||||
host_ids: [],
|
||||
instance_ids: [],
|
||||
};
|
||||
let all = false;
|
||||
let controller = false;
|
||||
let hostIds = [];
|
||||
let instanceIds = [];
|
||||
|
||||
for (let filter of this.logHandlers.keys()) {
|
||||
if (filter.all) { combinedFilter.all = true; }
|
||||
if (filter.controller) { combinedFilter.controller = true; }
|
||||
for (let hostId of filter.host_ids || []) {
|
||||
if (!combinedFilter.host_ids.includes(hostId)) {
|
||||
combinedFilter.host_ids.push(hostId);
|
||||
if (filter.all) { all = true; }
|
||||
if (filter.controller) { controller = true; }
|
||||
for (let hostId of filter.hostIds || []) {
|
||||
if (!hostIds.includes(hostId)) {
|
||||
hostIds.push(hostId);
|
||||
}
|
||||
}
|
||||
for (let instanceId of filter.instance_ids || []) {
|
||||
if (!combinedFilter.instance_ids.includes(instanceId)) {
|
||||
combinedFilter.instance_ids.push(instanceId);
|
||||
for (let instanceId of filter.instanceIds || []) {
|
||||
if (!instanceIds.includes(instanceId)) {
|
||||
instanceIds.push(instanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await libLink.messages.setLogSubscriptions.send(this, { ...combinedFilter, max_level: null });
|
||||
await this.send(new libData.LogSetSubscriptionsRequest(all, controller, hostIds, instanceIds, null));
|
||||
}
|
||||
|
||||
async debugWsMessageEventHandler(message) {
|
||||
async handleDebugWsMessageEvent(message) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("WS", message.data.direction, message.data.content);
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
try {
|
||||
await libLink.messages.prepareDisconnect.send(this);
|
||||
await this.connector.disconnect();
|
||||
} catch (err) {
|
||||
if (!(err instanceof libErrors.SessionLost)) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
await this.connector.close(1000, "Control Quit");
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
const webpack = require("webpack");
|
||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
|
||||
module.exports = (env = {}) => ({
|
||||
mode: env.production ? "production" : "development",
|
||||
@@ -113,5 +114,18 @@ module.exports = (env = {}) => ({
|
||||
},
|
||||
optimization: {
|
||||
moduleIds: "deterministic",
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
terserOptions: {
|
||||
compress: {
|
||||
keep_classnames: true,
|
||||
passes: 2,
|
||||
},
|
||||
mangle: {
|
||||
keep_classnames: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
const info = require("./info");
|
||||
const { ChatEvent } = require("./messages");
|
||||
const libCommand = require("@clusterio/lib/command");
|
||||
|
||||
|
||||
@@ -12,10 +12,7 @@ globalChatCommands.add(new libCommand.Command({
|
||||
yargs.positional("message", { describe: "message to send", type: "string" });
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
await info.messages.chat.send(control, {
|
||||
instance_name: "Console",
|
||||
content: args.message,
|
||||
});
|
||||
await control.sendTo("allInstances", new ChatEvent("Console", args.message));
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use strict";
|
||||
let { libLink } = require("@clusterio/lib");
|
||||
const messages = require("./messages");
|
||||
|
||||
module.exports = {
|
||||
name: "global_chat",
|
||||
@@ -8,16 +8,7 @@ module.exports = {
|
||||
instanceEntrypoint: "instance",
|
||||
controlEntrypoint: "control",
|
||||
|
||||
messages: {
|
||||
chat: new libLink.Event({
|
||||
type: "global_chat:chat",
|
||||
links: ["instance-host", "host-controller", "controller-host", "host-instance", "control-controller"],
|
||||
forwardTo: "controller",
|
||||
broadcastTo: "instance",
|
||||
eventProperties: {
|
||||
"instance_name": { type: "string" },
|
||||
"content": { type: "string" },
|
||||
},
|
||||
}),
|
||||
},
|
||||
messages: [
|
||||
messages.ChatEvent,
|
||||
],
|
||||
};
|
||||
@@ -4,6 +4,7 @@
|
||||
"use strict";
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
const libLuaTools = require("@clusterio/lib/lua_tools");
|
||||
const { ChatEvent } = require("./messages");
|
||||
|
||||
|
||||
/**
|
||||
@@ -19,6 +20,7 @@ function removeTags(content) {
|
||||
class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
async init() {
|
||||
this.messageQueue = [];
|
||||
this.instance.handle(ChatEvent, this.handleChatEvent.bind(this));
|
||||
}
|
||||
|
||||
onControllerConnectionEvent(event) {
|
||||
@@ -30,17 +32,14 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
}
|
||||
}
|
||||
|
||||
async chatEventHandler(message) {
|
||||
async handleChatEvent(event) {
|
||||
// TODO check if cross server chat is enabled
|
||||
let content = `[${message.data.instance_name}] ${removeTags(message.data.content)}`;
|
||||
let content = `[${event.instanceName}] ${removeTags(event.content)}`;
|
||||
await this.sendRcon(`/sc game.print('${libLuaTools.escapeString(content)}')`, true);
|
||||
}
|
||||
|
||||
sendChat(message) {
|
||||
this.info.messages.chat.send(this.instance, {
|
||||
instance_name: this.instance.name,
|
||||
content: message,
|
||||
});
|
||||
this.instance.sendTo("allInstances", new ChatEvent(this.instance.name, message));
|
||||
}
|
||||
|
||||
async onOutput(output) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"use strict";
|
||||
|
||||
class ChatEvent {
|
||||
static type = "event";
|
||||
static src = ["control", "instance"];
|
||||
static dst = "instance";
|
||||
static plugin = "global_chat";
|
||||
static permission = null;
|
||||
|
||||
/** @type {string} */
|
||||
instanceName;
|
||||
/** @type {string} */
|
||||
content;
|
||||
|
||||
constructor(instanceName, content) {
|
||||
this.instanceName = instanceName;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["instanceName", "content"],
|
||||
properties: {
|
||||
instanceName: { type: "string" },
|
||||
content: { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.instanceName, json.content);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ChatEvent,
|
||||
};
|
||||
@@ -1,10 +1,12 @@
|
||||
"use strict";
|
||||
const assert = require("assert").strict;
|
||||
const libLink = require("@clusterio/lib/link");
|
||||
|
||||
const mock = require("../../../test/mock");
|
||||
const lines = require("../../../test/lib/factorio/lines");
|
||||
const instance = require("../instance");
|
||||
const info = require("../info");
|
||||
const { ChatEvent } = require("../messages");
|
||||
|
||||
|
||||
describe("global_chat plugin", function() {
|
||||
@@ -24,14 +26,15 @@ describe("global_chat plugin", function() {
|
||||
let instancePlugin;
|
||||
|
||||
before(async function() {
|
||||
libLink.Link.register(ChatEvent);
|
||||
instancePlugin = new instance.InstancePlugin(info, new mock.MockInstance(), new mock.MockHost());
|
||||
await instancePlugin.init();
|
||||
});
|
||||
|
||||
describe(".chatEventHandler()", function() {
|
||||
describe(".handleChatEvent()", function() {
|
||||
it("should send received chat as command", async function() {
|
||||
instancePlugin.instance.server.rconCommands = [];
|
||||
await instancePlugin.chatEventHandler({ data: { instance_name: "test", content: "User: message" } });
|
||||
await instancePlugin.handleChatEvent(new ChatEvent("test", "User: message"));
|
||||
assert.deepEqual(
|
||||
instancePlugin.instance.server.rconCommands,
|
||||
["/sc game.print('[test] User: message')"],
|
||||
|
||||
@@ -5,6 +5,14 @@ const path = require("path");
|
||||
const libFileOps = require("@clusterio/lib/file_ops");
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
|
||||
const {
|
||||
AcquireRequest,
|
||||
ReleaseRequest,
|
||||
UploadRequest,
|
||||
DownloadRequest,
|
||||
DatabaseStatsRequest,
|
||||
} = require("./messages");
|
||||
|
||||
async function loadDatabase(config, logger) {
|
||||
let itemsPath = path.resolve(config.get("controller.database_directory"), "inventories.json");
|
||||
logger.verbose(`Loading ${itemsPath}`);
|
||||
@@ -39,6 +47,12 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
this.logger.error(`Unexpected error autosaving player data:\n${err.stack}`);
|
||||
});
|
||||
}, this.controller.config.get("inventory_sync.autosave_interval") * 1000);
|
||||
|
||||
this.controller.handle(AcquireRequest, this.handleAcquireRequest.bind(this));
|
||||
this.controller.handle(ReleaseRequest, this.handleReleaseRequest.bind(this));
|
||||
this.controller.handle(UploadRequest, this.handleUploadRequest.bind(this));
|
||||
this.controller.handle(DownloadRequest, this.handleDownloadRequest.bind(this));
|
||||
this.controller.handle(DatabaseStatsRequest, this.handleDatabaseStatsRequest.bind(this));
|
||||
}
|
||||
|
||||
async onInstanceStatusChanged(instance) {
|
||||
@@ -84,10 +98,10 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
return false;
|
||||
}
|
||||
|
||||
async acquireRequestHandler(message) {
|
||||
let { instance_id, player_name } = message.data;
|
||||
if (!this.acquire(instance_id, player_name)) {
|
||||
let acquisitionRecord = this.acquiredPlayers.get(player_name);
|
||||
async handleAcquireRequest(request) {
|
||||
let { instanceId, playerName } = request;
|
||||
if (!this.acquire(instanceId, playerName)) {
|
||||
let acquisitionRecord = this.acquiredPlayers.get(playerName);
|
||||
let instance = this.controller.instances.get(acquisitionRecord.instanceId);
|
||||
return {
|
||||
status: "busy",
|
||||
@@ -95,75 +109,73 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
};
|
||||
}
|
||||
|
||||
let playerData = this.playerDatastore.get(player_name);
|
||||
return {
|
||||
status: "acquired",
|
||||
has_data: Boolean(playerData),
|
||||
generation: playerData ? playerData.generation : 0,
|
||||
};
|
||||
let playerData = this.playerDatastore.get(playerName);
|
||||
return new AcquireRequest.Response(
|
||||
"acquired",
|
||||
playerData ? playerData.generation : 0,
|
||||
Boolean(playerData),
|
||||
);
|
||||
}
|
||||
|
||||
async releaseRequestHandler(message) {
|
||||
let { instance_id, player_name } = message.data;
|
||||
let acquisitionRecord = this.acquiredPlayers.get(player_name);
|
||||
async handleReleaseRequest(request) {
|
||||
let { instanceId, playerName } = request;
|
||||
let acquisitionRecord = this.acquiredPlayers.get(playerName);
|
||||
if (!acquisitionRecord) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (acquisitionRecord.instanceId === instance_id) {
|
||||
this.acquiredPlayers.delete(player_name);
|
||||
if (acquisitionRecord.instanceId === instanceId) {
|
||||
this.acquiredPlayers.delete(playerName);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadRequestHandler(message) {
|
||||
let { instance_id, player_name, player_data } = message.data;
|
||||
let instanceName = this.controller.instances.get(instance_id).config.get("instance.name");
|
||||
async handleUploadRequest(request) {
|
||||
let { instanceId, playerName, playerData } = request;
|
||||
let instanceName = this.controller.instances.get(instanceId).config.get("instance.name");
|
||||
let store = true;
|
||||
let acquisitionRecord = this.acquiredPlayers.get(player_name);
|
||||
let acquisitionRecord = this.acquiredPlayers.get(playerName);
|
||||
if (!acquisitionRecord) {
|
||||
this.logger.warn(`${instanceName} uploaded ${player_name} without an acquisition`);
|
||||
this.logger.warn(`${instanceName} uploaded ${playerName} without an acquisition`);
|
||||
// Allow upload in this case as it might come from a crashed instance that restarted and is now
|
||||
// uploading the player data for all the players that were online during the last autosave.
|
||||
|
||||
} else if (acquisitionRecord.instanceId !== instance_id) {
|
||||
this.logger.warn(`${instanceName} uploaded ${player_name} while another instance has acquired it`);
|
||||
} else if (acquisitionRecord.instanceId !== instanceId) {
|
||||
this.logger.warn(`${instanceName} uploaded ${playerName} while another instance has acquired it`);
|
||||
store = false;
|
||||
|
||||
} else {
|
||||
this.acquiredPlayers.delete(player_name);
|
||||
this.acquiredPlayers.delete(playerName);
|
||||
}
|
||||
|
||||
this.acquiredPlayers.delete(player_name);
|
||||
let oldPlayerData = this.playerDatastore.get(player_name);
|
||||
if (store && oldPlayerData && oldPlayerData.generation >= player_data.generation) {
|
||||
this.acquiredPlayers.delete(playerName);
|
||||
let oldPlayerData = this.playerDatastore.get(playerName);
|
||||
if (store && oldPlayerData && oldPlayerData.generation >= playerData.generation) {
|
||||
this.logger.warn(
|
||||
`${instanceName} uploaded generation ${player_data.generation} while the stored` +
|
||||
`generation is ${oldPlayerData.generation} for ${player_name}`
|
||||
`${instanceName} uploaded generation ${playerData.generation} while the stored` +
|
||||
`generation is ${oldPlayerData.generation} for ${playerName}`
|
||||
);
|
||||
store = false;
|
||||
}
|
||||
|
||||
if (store) {
|
||||
this.logger.verbose(`Received player data for ${player_name} from ${instanceName}`);
|
||||
this.playerDatastore.set(player_name, player_data);
|
||||
this.logger.verbose(`Received player data for ${playerName} from ${instanceName}`);
|
||||
this.playerDatastore.set(playerName, playerData);
|
||||
}
|
||||
}
|
||||
|
||||
async downloadRequestHandler(message) {
|
||||
let { instance_id, player_name } = message.data;
|
||||
let instanceName = this.controller.instances.get(instance_id).config.get("instance.name");
|
||||
async handleDownloadRequest(request) {
|
||||
let { instanceId, playerName } = request;
|
||||
let instanceName = this.controller.instances.get(instanceId).config.get("instance.name");
|
||||
|
||||
let acquisitionRecord = this.acquiredPlayers.get(player_name);
|
||||
let acquisitionRecord = this.acquiredPlayers.get(playerName);
|
||||
if (!acquisitionRecord) {
|
||||
this.logger.warn(`${instanceName} downloaded ${player_name} without an acquisition`);
|
||||
} else if (acquisitionRecord.instanceId !== instance_id) {
|
||||
this.logger.warn(`${instanceName} downloaded ${player_name} while another instance has acquired it`);
|
||||
this.logger.warn(`${instanceName} downloaded ${playerName} without an acquisition`);
|
||||
} else if (acquisitionRecord.instanceId !== instanceId) {
|
||||
this.logger.warn(`${instanceName} downloaded ${playerName} while another instance has acquired it`);
|
||||
}
|
||||
|
||||
this.logger.verbose(`Sending player data for ${player_name} to ${instanceName}`);
|
||||
return {
|
||||
player_data: this.playerDatastore.get(player_name) || null,
|
||||
};
|
||||
this.logger.verbose(`Sending player data for ${playerName} to ${instanceName}`);
|
||||
return new DownloadRequest.Response(this.playerDatastore.get(playerName) || null);
|
||||
}
|
||||
|
||||
async onShutdown() {
|
||||
@@ -171,21 +183,21 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
await saveDatabase(this.controller.config, this.playerDatastore, this.logger);
|
||||
}
|
||||
|
||||
async databaseStatsRequestHandler(message) {
|
||||
async handleDatabaseStatsRequest() {
|
||||
let playerDatastore = Array.from(this.playerDatastore.keys())
|
||||
.map(name => ({
|
||||
name,
|
||||
length: JSON.stringify(this.playerDatastore.get(name)).length,
|
||||
}))
|
||||
.sort((a, b) => a.length - b.length);
|
||||
return {
|
||||
database_size: playerDatastore.map(x => x.length).reduce((a, b) => b - a, 0),
|
||||
database_entries: playerDatastore.length,
|
||||
largest_entry: {
|
||||
.sort((a, b) => b.length - a.length);
|
||||
return new DatabaseStatsRequest.Response(
|
||||
playerDatastore.map(x => x.length).reduce((a, b) => b - a, 0),
|
||||
playerDatastore.length,
|
||||
{
|
||||
name: playerDatastore[0] && playerDatastore[0].name || "-",
|
||||
size: playerDatastore[0] && playerDatastore[0].length || 0,
|
||||
},
|
||||
};
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use strict";
|
||||
const { libConfig, libLink, libUsers } = require("@clusterio/lib");
|
||||
const { libConfig, libUsers } = require("@clusterio/lib");
|
||||
let messages = require("./messages");
|
||||
|
||||
class ControllerConfigGroup extends libConfig.PluginConfigGroup { }
|
||||
ControllerConfigGroup.defaultAccess = ["controller", "host", "control"];
|
||||
@@ -51,72 +52,13 @@ module.exports = {
|
||||
controllerEntrypoint: "controller",
|
||||
ControllerConfigGroup,
|
||||
|
||||
messages: [
|
||||
messages.AcquireRequest,
|
||||
messages.ReleaseRequest,
|
||||
messages.UploadRequest,
|
||||
messages.DownloadRequest,
|
||||
messages.DatabaseStatsRequest,
|
||||
],
|
||||
webEntrypoint: "./web",
|
||||
routes: ["/inventory"],
|
||||
|
||||
messages: {
|
||||
acquire: new libLink.Request({
|
||||
type: "inventory_sync:acquire",
|
||||
links: ["instance-host", "host-controller"],
|
||||
forwardTo: "controller",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"player_name": { type: "string" },
|
||||
},
|
||||
responseRequired: ["status"],
|
||||
responseProperties: {
|
||||
"status": { enum: ["acquired", "error", "busy"] },
|
||||
"generation": { type: "integer" },
|
||||
"has_data": { type: "boolean" },
|
||||
"message": { type: "string" },
|
||||
},
|
||||
}),
|
||||
release: new libLink.Request({
|
||||
type: "inventory_sync:release",
|
||||
links: ["instance-host", "host-controller"],
|
||||
forwardTo: "controller",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"player_name": { type: "string" },
|
||||
},
|
||||
}),
|
||||
upload: new libLink.Request({
|
||||
type: "inventory_sync:upload",
|
||||
links: ["instance-host", "host-controller"],
|
||||
forwardTo: "controller",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"player_name": { type: "string" },
|
||||
"player_data": { type: "object" },
|
||||
},
|
||||
}),
|
||||
download: new libLink.Request({
|
||||
type: "inventory_sync:download",
|
||||
links: ["instance-host", "host-controller"],
|
||||
forwardTo: "controller",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"player_name": { type: "string" },
|
||||
},
|
||||
responseProperties: {
|
||||
"player_data": { type: ["object", "null"] },
|
||||
},
|
||||
}),
|
||||
databaseStats: new libLink.Request({
|
||||
type: "inventory_sync:databaseStats",
|
||||
links: ["control-controller"],
|
||||
permission: "inventory_sync.inventory.view",
|
||||
responseProperties: {
|
||||
"database_size": { type: "integer" },
|
||||
"database_entries": { type: "integer" },
|
||||
"largest_entry": {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
size: { type: "number" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -6,6 +6,14 @@ const libErrors = require("@clusterio/lib/errors");
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
const libLuaTools = require("@clusterio/lib/lua_tools");
|
||||
|
||||
const {
|
||||
AcquireRequest,
|
||||
ReleaseRequest,
|
||||
UploadRequest,
|
||||
DownloadRequest,
|
||||
DatabaseStatsRequest,
|
||||
} = require("./messages");
|
||||
|
||||
/**
|
||||
* Splits string into array of strings with max of a certain length
|
||||
* @param {Number} chunkSize - Max length of each chunk
|
||||
@@ -45,10 +53,7 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
return;
|
||||
}
|
||||
this.playersToRelease.delete(player);
|
||||
await this.info.messages.release.send(this.instance, {
|
||||
instance_id: this.instance.id,
|
||||
player_name,
|
||||
});
|
||||
await this.instance.sendTo("controller", new ReleaseRequest(this.instance.id, player_name));
|
||||
}
|
||||
})().catch(
|
||||
err => this.logger.error(`Unpexpected error releasing queued up players:\n${err.stack}`)
|
||||
@@ -65,12 +70,16 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
|
||||
if (this.host.connector.connected && !this.disconnecting) {
|
||||
try {
|
||||
let acquireResponse = await this.instance.sendTo(
|
||||
"controller",
|
||||
new AcquireRequest(this.instance.id, request.player_name),
|
||||
);
|
||||
response = {
|
||||
player_name: request.player_name,
|
||||
...await this.info.messages.acquire.send(this.instance, {
|
||||
instance_id: this.instance.id,
|
||||
player_name: request.player_name,
|
||||
}),
|
||||
status: acquireResponse.status,
|
||||
generation: acquireResponse.generation,
|
||||
has_data: acquireResponse.hasData,
|
||||
message: acquireResponse.message,
|
||||
};
|
||||
} catch (err) {
|
||||
if (!(err instanceof libErrors.SessionLost)) {
|
||||
@@ -90,10 +99,7 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.info.messages.release.send(this.instance, {
|
||||
instance_id: this.instance.id,
|
||||
player_name: request.player_name,
|
||||
});
|
||||
await this.instance.sendTo("controller", new ReleaseRequest(this.instance.id, request.player_name));
|
||||
} catch (err) {
|
||||
if (err instanceof libErrors.SessionLost) {
|
||||
this.playersToRelease.set(request.player_name);
|
||||
@@ -110,11 +116,10 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
|
||||
this.logger.verbose(`Uploading ${player_data.name} (${JSON.stringify(player_data).length / 1000}kB)`);
|
||||
try {
|
||||
await this.info.messages.upload.send(this.instance, {
|
||||
instance_id: this.instance.id,
|
||||
player_name: player_data.name,
|
||||
player_data: player_data,
|
||||
});
|
||||
await this.instance.sendTo(
|
||||
"controller",
|
||||
new UploadRequest(this.instance.id, player_data.name, player_data),
|
||||
);
|
||||
|
||||
} catch (err) {
|
||||
if (!(err instanceof libErrors.SessionLost)) {
|
||||
@@ -129,27 +134,24 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
}
|
||||
|
||||
async handleDownload(request) {
|
||||
const player_name = request.player_name;
|
||||
this.logger.verbose(`Downloading ${player_name}`);
|
||||
const playerName = request.player_name;
|
||||
this.logger.verbose(`Downloading ${playerName}`);
|
||||
|
||||
let response = await this.info.messages.download.send(this.instance, {
|
||||
instance_id: this.instance.id,
|
||||
player_name,
|
||||
});
|
||||
let response = await this.instance.sendTo("controller", new DownloadRequest(this.instance.id, playerName));
|
||||
|
||||
if (!response.player_data) {
|
||||
await this.sendRcon(`/sc inventory_sync.download_inventory('${player_name}',nil,0,0)`, true);
|
||||
await this.sendRcon(`/sc inventory_sync.download_inventory('${playerName}',nil,0,0)`, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const chunkSize = this.instance.config.get("inventory_sync.rcon_chunk_size");
|
||||
const chunks = chunkify(chunkSize, JSON.stringify(response.player_data));
|
||||
this.logger.verbose(`Sending inventory for ${player_name} in ${chunks.length} chunks`);
|
||||
this.logger.verbose(`Sending inventory for ${playerName} in ${chunks.length} chunks`);
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
// this.logger.verbose(`Sending chunk ${i+1} of ${chunks.length}`)
|
||||
const chunk = libLuaTools.escapeString(chunks[i]);
|
||||
await this.sendRcon(
|
||||
`/sc inventory_sync.download_inventory('${player_name}','${chunk}',${i + 1},${chunks.length})`,
|
||||
`/sc inventory_sync.download_inventory('${playerName}','${chunk}',${i + 1},${chunks.length})`,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
"use strict";
|
||||
|
||||
|
||||
class AcquireRequest {
|
||||
static type = "request";
|
||||
static src = "instance";
|
||||
static dst = "controller";
|
||||
static plugin = "inventory_sync";
|
||||
|
||||
/** @type {number} */
|
||||
instanceId;
|
||||
/** @type {string} */
|
||||
playerName;
|
||||
|
||||
constructor(instanceId, playerName) {
|
||||
this.instanceId = instanceId;
|
||||
this.playerName = playerName;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
"instanceId": { type: "integer" },
|
||||
"playerName": { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.instanceId, json.playerName);
|
||||
}
|
||||
}
|
||||
AcquireRequest.Response = class Response {
|
||||
/** @type {string} */
|
||||
status;
|
||||
/** @type {number} */
|
||||
generation;
|
||||
/** @type {boolean} */
|
||||
hasData;
|
||||
/** @type {string} */
|
||||
message;
|
||||
|
||||
constructor(status, generation, hasData, message) {
|
||||
this.status = status;
|
||||
if (generation !== undefined) { this.generation = generation; }
|
||||
if (hasData !== undefined) { this.hasData = hasData; }
|
||||
if (message !== undefined) { this.message = message; }
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
required: ["status"],
|
||||
properties: {
|
||||
"status": {
|
||||
type: "string",
|
||||
enum: ["acquired", "error", "busy"],
|
||||
},
|
||||
"generation": { type: "integer" },
|
||||
"hasData": { type: "boolean" },
|
||||
"message": { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.status, json.generation, json.hasData, json.message);
|
||||
}
|
||||
};
|
||||
|
||||
class ReleaseRequest {
|
||||
static type = "request";
|
||||
static src = "instance";
|
||||
static dst = "controller";
|
||||
static plugin = "inventory_sync";
|
||||
|
||||
/** @type {boolean} */
|
||||
instanceId;
|
||||
/** @type {boolean} */
|
||||
playerName;
|
||||
|
||||
constructor(instanceId, playerName) {
|
||||
this.instanceId = instanceId;
|
||||
this.playerName = playerName;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
"instanceId": { type: "integer" },
|
||||
"playerName": { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.instanceId, json.playerName);
|
||||
}
|
||||
}
|
||||
|
||||
class UploadRequest {
|
||||
static type = "request";
|
||||
static src = "instance";
|
||||
static dst = "controller";
|
||||
static plugin = "inventory_sync";
|
||||
|
||||
/** @type {boolean} */
|
||||
instanceId;
|
||||
/** @type {boolean} */
|
||||
playerName;
|
||||
/** @type {object} */
|
||||
playerData;
|
||||
|
||||
constructor(instanceId, playerName, playerData) {
|
||||
this.instanceId = instanceId;
|
||||
this.playerName = playerName;
|
||||
this.playerData = playerData;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["instanceId", "playerName", "playerData"],
|
||||
properties: {
|
||||
"instanceId": { type: "integer" },
|
||||
"playerName": { type: "string" },
|
||||
"playerData": { type: "object" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.instanceId, json.playerName, json.playerData);
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadRequest {
|
||||
static type = "request";
|
||||
static src = "instance";
|
||||
static dst = "controller";
|
||||
static plugin = "inventory_sync";
|
||||
|
||||
/** @type {number} */
|
||||
instanceId;
|
||||
/** @type {string} */
|
||||
playerName;
|
||||
|
||||
constructor(instanceId, playerName) {
|
||||
this.instanceId = instanceId;
|
||||
this.playerName = playerName;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["instanceId", "playerName"],
|
||||
properties: {
|
||||
"instanceId": { type: "integer" },
|
||||
"playerName": { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.instanceId, json.playerName);
|
||||
}
|
||||
}
|
||||
DownloadRequest.Response = class Response {
|
||||
/** @type {?object} */
|
||||
playerData;
|
||||
|
||||
constructor(playerData) {
|
||||
this.playerData = playerData;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
required: ["playerData"],
|
||||
properties: {
|
||||
"playerData": { type: ["object", "null"] },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.playerData);
|
||||
}
|
||||
};
|
||||
|
||||
class DatabaseStatsRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static plugin = "inventory_sync";
|
||||
static permission = "inventory_sync.inventory.view";
|
||||
}
|
||||
DatabaseStatsRequest.Response = class Response {
|
||||
/** @type {boolean} */
|
||||
databaseSize;
|
||||
/** @type {boolean} */
|
||||
databaseEntries;
|
||||
/** @type {object} */
|
||||
largestEntry;
|
||||
|
||||
constructor(databaseSize, databaseEntries, largestEntry) {
|
||||
this.databaseSize = databaseSize;
|
||||
this.databaseEntries = databaseEntries;
|
||||
this.largestEntry = largestEntry;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["databaseSize", "databaseEntries", "largestEntry"],
|
||||
properties: {
|
||||
"databaseSize": { type: "integer" },
|
||||
"databaseEntries": { type: "integer" },
|
||||
"largestEntry": {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
size: { type: "number" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.databaseSize, json.databaseEntries, json.largestEntry);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
AcquireRequest,
|
||||
ReleaseRequest,
|
||||
UploadRequest,
|
||||
DownloadRequest,
|
||||
DatabaseStatsRequest,
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import React, { useContext, useEffect, useState } from "react";
|
||||
|
||||
import { libPlugin } from "@clusterio/lib";
|
||||
import { PageLayout, ControlContext } from "@clusterio/web_ui";
|
||||
import info from "../info";
|
||||
import { DatabaseStatsRequest } from "../messages";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
@@ -14,16 +14,16 @@ function InventoryPage() {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
// Get statistics
|
||||
updateStatsData(await info.messages.databaseStats.send(control, {}));
|
||||
updateStatsData(await control.send(new DatabaseStatsRequest()));
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <PageLayout nav={[{ name: "Inventory sync" }]}>
|
||||
<h2>Inventory sync</h2>
|
||||
{statsData && <>
|
||||
<p>Database size: {Math.round(statsData.database_size / 1000)}kB</p>
|
||||
<p>Database entries: {statsData.database_entries}</p>
|
||||
<p>Largest entry is {statsData.largest_entry.name} with {(statsData.largest_entry.size / 1000)}kB</p>
|
||||
<p>Database size: {Math.round(statsData.databaseSize / 1000)}kB</p>
|
||||
<p>Database entries: {statsData.databaseEntries}</p>
|
||||
<p>Largest entry is {statsData.largestEntry.name} with {(statsData.largestEntry.size / 1000)}kB</p>
|
||||
</>}
|
||||
</PageLayout>;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ const libPlugin = require("@clusterio/lib/plugin");
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
const { basicType } = require("@clusterio/lib/helpers");
|
||||
|
||||
const { FetchPlayerCodeRequest, SetVerifyCodeRequest } = require("./messages");
|
||||
|
||||
|
||||
async function generateCode(length) {
|
||||
// ji1lI, 0oOQ, and 2Z are not present to ease reading.
|
||||
@@ -60,6 +62,9 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
this.controller.app.post("/api/player_auth/verify", express.json(), (req, res, next) => {
|
||||
this.handleVerify(req, res).catch(next);
|
||||
});
|
||||
|
||||
this.controller.handle(FetchPlayerCodeRequest, this.handleFetchPlayerCodeRequest.bind(this));
|
||||
this.controller.handle(SetVerifyCodeRequest, this.handleSetVerifyCodeRequest.bind(this));
|
||||
}
|
||||
|
||||
async handlePlayerCode(req, res) {
|
||||
@@ -159,15 +164,15 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
res.send({ error: true, message: "invalid player_code" });
|
||||
}
|
||||
|
||||
async fetchPlayerCodeRequestHandler(message) {
|
||||
async handleFetchPlayerCodeRequest(request) {
|
||||
let playerCode = await generateCode(this.controller.config.get("player_auth.code_length"));
|
||||
let expires = Date.now() + this.controller.config.get("player_auth.code_timeout") * 1000;
|
||||
this.players.set(message.data.player, { playerCode, verifyCode: null, expires });
|
||||
this.players.set(request.player, { playerCode, verifyCode: null, expires });
|
||||
return { player_code: playerCode, controller_url: this.controller.getControllerUrl() };
|
||||
}
|
||||
|
||||
async setVerifyCodeRequestHandler(message) {
|
||||
let { player, verify_code } = message.data;
|
||||
async handleSetVerifyCodeRequest(request) {
|
||||
let { player, verifyCode } = request;
|
||||
if (!this.players.has(player)) {
|
||||
throw new libErrors.RequestError("invalid player");
|
||||
}
|
||||
@@ -177,7 +182,7 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
throw new libErrors.RequestError("invalid player");
|
||||
}
|
||||
|
||||
entry.verifyCode = verify_code;
|
||||
entry.verifyCode = verifyCode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use strict";
|
||||
let { libConfig, libLink } = require("@clusterio/lib");
|
||||
let { libConfig } = require("@clusterio/lib");
|
||||
let messages = require("./messages");
|
||||
|
||||
|
||||
class ControllerConfigGroup extends libConfig.PluginConfigGroup {}
|
||||
@@ -30,27 +31,8 @@ module.exports = {
|
||||
webEntrypoint: "./web",
|
||||
ControllerConfigGroup,
|
||||
|
||||
messages: {
|
||||
fetchPlayerCode: new libLink.Request({
|
||||
type: "player_auth:fetch_player_code",
|
||||
links: ["instance-host", "host-controller"],
|
||||
forwardTo: "controller",
|
||||
requestProperties: {
|
||||
"player": { type: "string" },
|
||||
},
|
||||
responseProperties: {
|
||||
"player_code": { type: "string" },
|
||||
"controller_url": { type: "string" },
|
||||
},
|
||||
}),
|
||||
setVerifyCode: new libLink.Request({
|
||||
type: "player_auth:set_verify_code",
|
||||
links: ["instance-host", "host-controller"],
|
||||
forwardTo: "controller",
|
||||
requestProperties: {
|
||||
"player": { type: "string" },
|
||||
"verify_code": { type: "string" },
|
||||
},
|
||||
}),
|
||||
},
|
||||
messages: [
|
||||
messages.FetchPlayerCodeRequest,
|
||||
messages.SetVerifyCodeRequest,
|
||||
],
|
||||
};
|
||||
@@ -4,6 +4,8 @@
|
||||
"use strict";
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
|
||||
const { FetchPlayerCodeRequest, SetVerifyCodeRequest } = require("./messages");
|
||||
|
||||
|
||||
class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
async init() {
|
||||
@@ -25,19 +27,16 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await this.info.messages.fetchPlayerCode.send(this.instance, { player: event.player });
|
||||
response = await this.instance.sendTo("controller", new FetchPlayerCodeRequest(event.player));
|
||||
} catch (err) {
|
||||
await this.sendRcon(`/web-login error ${event.player} ${err.message}`);
|
||||
return;
|
||||
}
|
||||
await this.sendRcon(`/web-login open ${event.player} ${response.controller_url} ${response.player_code}`);
|
||||
await this.sendRcon(`/web-login open ${event.player} ${response.controllerUrl} ${response.playerCode}`);
|
||||
|
||||
} else if (event.type === "set_verify_code") {
|
||||
try {
|
||||
await this.info.messages.setVerifyCode.send(this.instance, {
|
||||
player: event.player,
|
||||
verify_code: event.verify_code,
|
||||
});
|
||||
await this.instance.sendTo("controller", new SetVerifyCodeRequest(event.player, event.verify_code));
|
||||
|
||||
} catch (err) {
|
||||
await this.sendRcon(`/web-login error ${event.player} ${err.message}`);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"use strict";
|
||||
|
||||
|
||||
class FetchPlayerCodeRequest {
|
||||
static type = "request";
|
||||
static src = "instance";
|
||||
static dst = "controller";
|
||||
static plugin = "player_auth";
|
||||
|
||||
/** @type {string} */
|
||||
player;
|
||||
|
||||
constructor(player) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["player"],
|
||||
properties: {
|
||||
"player": { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.player);
|
||||
}
|
||||
}
|
||||
FetchPlayerCodeRequest.Response = class Response {
|
||||
/** @type {string} */
|
||||
playerCode;
|
||||
/** @type {string} */
|
||||
controllerUrl;
|
||||
|
||||
constructor(playerCode, controllerUrl) {
|
||||
this.playerCode = playerCode;
|
||||
this.controllerUrl = controllerUrl;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["playerCode", "controllerUrl"],
|
||||
properties: {
|
||||
"playerCode": { type: "string" },
|
||||
"controllerUrl": { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.playerCode, json.controllerUrl);
|
||||
}
|
||||
};
|
||||
|
||||
class SetVerifyCodeRequest {
|
||||
static type = "request";
|
||||
static src = "instance";
|
||||
static dst = "controller";
|
||||
static plugin = "player_auth";
|
||||
|
||||
/** @type {string} */
|
||||
player;
|
||||
/** @type {string} */
|
||||
verifyCode;
|
||||
|
||||
constructor(player, verifyCode) {
|
||||
this.player = player;
|
||||
this.verifyCode = verifyCode;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["player", "verifyCode"],
|
||||
properties: {
|
||||
"player": { type: "string" },
|
||||
"verifyCode": { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.playerCode, json.verifyCode);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
FetchPlayerCodeRequest,
|
||||
SetVerifyCodeRequest,
|
||||
};
|
||||
@@ -8,6 +8,8 @@ const mock = require("../../../test/mock");
|
||||
const controller = require("../controller");
|
||||
const instance = require("../instance");
|
||||
const info = require("../info");
|
||||
const { FetchPlayerCodeRequest, SetVerifyCodeRequest } = require("../messages");
|
||||
const libData = require("@clusterio/lib/data");
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
|
||||
|
||||
@@ -291,9 +293,11 @@ describe("player_auth", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe(".fetchPlayerCodeRequestHandler()", function() {
|
||||
describe(".handleFetchPlayerCodeRequest()", function() {
|
||||
it("should return a code", async function() {
|
||||
let result = await controllerPlugin.fetchPlayerCodeRequestHandler({ data: { player: "test" }});
|
||||
let result = await controllerPlugin.handleFetchPlayerCodeRequest(
|
||||
new FetchPlayerCodeRequest("test")
|
||||
);
|
||||
assert(typeof result.player_code === "string", "no code returned");
|
||||
assert(result.player_code.length === 10, "incorrect code length returned");
|
||||
let expires = controllerPlugin.players.get("test").expires;
|
||||
@@ -302,11 +306,11 @@ describe("player_auth", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe(".setVerifyCodeRequestHandler()", function() {
|
||||
describe(".handleSetVerifyCodeRequest()", function() {
|
||||
it("should throw if player does not exist", async function() {
|
||||
await assert.rejects(
|
||||
controllerPlugin.setVerifyCodeRequestHandler(
|
||||
{ data: { player: "invalid", verify_code: "invalid" }}
|
||||
controllerPlugin.handleSetVerifyCodeRequest(
|
||||
new SetVerifyCodeRequest("invalid", "invalid")
|
||||
),
|
||||
new libErrors.RequestError("invalid player")
|
||||
);
|
||||
@@ -315,8 +319,8 @@ describe("player_auth", function() {
|
||||
let expires = Date.now() - 1000;
|
||||
controllerPlugin.players.set("expired", { playerCode: "expried", verifyCode: null, expires });
|
||||
await assert.rejects(
|
||||
controllerPlugin.setVerifyCodeRequestHandler(
|
||||
{ data: { player: "expired", verify_code: "expired" }}
|
||||
controllerPlugin.handleSetVerifyCodeRequest(
|
||||
new SetVerifyCodeRequest("expired", "expired")
|
||||
),
|
||||
new libErrors.RequestError("invalid player")
|
||||
);
|
||||
@@ -326,8 +330,8 @@ describe("player_auth", function() {
|
||||
describe("integration", function() {
|
||||
it("should verify a full login flow", async function() {
|
||||
let app = controllerPlugin.controller.app;
|
||||
let { player_code } = await controllerPlugin.fetchPlayerCodeRequestHandler(
|
||||
{ data: { player: "test" }}
|
||||
let { player_code } = await controllerPlugin.handleFetchPlayerCodeRequest(
|
||||
new FetchPlayerCodeRequest("test")
|
||||
);
|
||||
|
||||
let playerCodeResult = await phin({
|
||||
@@ -338,7 +342,9 @@ describe("player_auth", function() {
|
||||
});
|
||||
|
||||
let { verify_code, verify_token } = playerCodeResult.body;
|
||||
await controllerPlugin.setVerifyCodeRequestHandler({ data: { player: "test", verify_code }});
|
||||
await controllerPlugin.handleSetVerifyCodeRequest(
|
||||
new SetVerifyCodeRequest("test", verify_code)
|
||||
);
|
||||
|
||||
let verifyResult = await phin({
|
||||
url: `${controllerUrl}/api/player_auth/verify`,
|
||||
@@ -372,14 +378,11 @@ describe("player_auth", function() {
|
||||
it("should call /web-login error after error from the controller", async function() {
|
||||
instancePlugin.instance.server.reset();
|
||||
instancePlugin.instance.connector.once("send", message => {
|
||||
instancePlugin.instance.connector.emit("message", {
|
||||
seq: 1,
|
||||
type: "player_auth:fetch_player_code_response",
|
||||
data: {
|
||||
seq: message.seq,
|
||||
error: "controller error",
|
||||
},
|
||||
});
|
||||
instancePlugin.instance.connector.emit("message",
|
||||
new libData.MessageResponseError(1, message.dst, message.src,
|
||||
new libData.ResponseError("controller error")
|
||||
)
|
||||
);
|
||||
});
|
||||
await instancePlugin.handleEvent({ type: "open_dialog", player: "test" });
|
||||
let command = instancePlugin.instance.server.rconCommands[0];
|
||||
@@ -388,15 +391,11 @@ describe("player_auth", function() {
|
||||
it("should call /web-login open after a valid response from the controller", async function() {
|
||||
instancePlugin.instance.server.reset();
|
||||
instancePlugin.instance.connector.once("send", message => {
|
||||
instancePlugin.instance.connector.emit("message", {
|
||||
seq: 1,
|
||||
type: "player_auth:fetch_player_code_response",
|
||||
data: {
|
||||
seq: message.seq,
|
||||
player_code: "code",
|
||||
controller_url: "controller-url",
|
||||
},
|
||||
});
|
||||
instancePlugin.instance.connector.emit("message",
|
||||
new libData.MessageResponse(1, message.dst, message.src,
|
||||
new FetchPlayerCodeRequest.Response("code", "controller-url")
|
||||
)
|
||||
);
|
||||
});
|
||||
await instancePlugin.handleEvent({ type: "open_dialog", player: "test" });
|
||||
let command = instancePlugin.instance.server.rconCommands[0];
|
||||
@@ -407,11 +406,9 @@ describe("player_auth", function() {
|
||||
it("should call /web-login code_set after a valid response from the controller", async function() {
|
||||
instancePlugin.instance.server.reset();
|
||||
instancePlugin.instance.connector.once("send", message => {
|
||||
instancePlugin.instance.connector.emit("message", {
|
||||
seq: 1,
|
||||
type: "player_auth:set_verify_code_response",
|
||||
data: { seq: message.seq },
|
||||
});
|
||||
instancePlugin.instance.connector.emit("message",
|
||||
new libData.MessageResponse(1, message.dst, message.src)
|
||||
);
|
||||
});
|
||||
await instancePlugin.handleEvent(
|
||||
{ type: "set_verify_code", player: "test", verify_code: "verify" }
|
||||
@@ -422,11 +419,11 @@ describe("player_auth", function() {
|
||||
it("should call /web-login error after error from the controller", async function() {
|
||||
instancePlugin.instance.server.reset();
|
||||
instancePlugin.instance.connector.once("send", message => {
|
||||
instancePlugin.instance.connector.emit("message", {
|
||||
seq: 1,
|
||||
type: "player_auth:set_verify_code_response",
|
||||
data: { seq: message.seq, error: "controller error" },
|
||||
});
|
||||
instancePlugin.instance.connector.emit("message",
|
||||
new libData.MessageResponseError(1, message.dst, message.src,
|
||||
new libData.ResponseError("controller error")
|
||||
)
|
||||
);
|
||||
});
|
||||
await instancePlugin.handleEvent(
|
||||
{ type: "set_verify_code", player: "test", verify_code: "verify" }
|
||||
|
||||
@@ -6,6 +6,14 @@ const libFileOps = require("@clusterio/lib/file_ops");
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
const RateLimiter = require("@clusterio/lib/RateLimiter");
|
||||
|
||||
const {
|
||||
ContributionEvent,
|
||||
ProgressEvent,
|
||||
FinishedEvent,
|
||||
Technology,
|
||||
SyncTechnologiesRequest,
|
||||
} = require("./messages");
|
||||
|
||||
|
||||
async function loadTechnologies(controllerConfig, logger) {
|
||||
let filePath = path.join(controllerConfig.get("controller.database_directory"), "technologies.json");
|
||||
@@ -40,6 +48,10 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
this.lastProgressBroadcast = Date.now();
|
||||
this.progressBroadcastId = null;
|
||||
this.progressToBroadcast = new Set();
|
||||
|
||||
this.controller.handle(ContributionEvent, this.handleContributionEvent.bind(this));
|
||||
this.controller.handle(FinishedEvent, this.handleFinishedEvent.bind(this));
|
||||
this.controller.handle(SyncTechnologiesRequest, this.handleSyncTechnologiesRequest.bind(this));
|
||||
}
|
||||
|
||||
async onShutdown() {
|
||||
@@ -59,12 +71,12 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
|
||||
this.lastProgressBroadcast = Date.now();
|
||||
if (techs.length) {
|
||||
this.broadcastEventToHosts(this.info.messages.progress, { technologies: techs });
|
||||
this.controller.sendTo("allInstances", new ProgressEvent(techs));
|
||||
}
|
||||
}
|
||||
|
||||
async contributionEventHandler(message) {
|
||||
let { name, level, contribution } = message.data;
|
||||
async handleContributionEvent(event) {
|
||||
let { name, level, contribution } = event;
|
||||
let tech = this.technologies.get(name);
|
||||
if (!tech) {
|
||||
tech = { level, progress: 0, researched: false };
|
||||
@@ -97,12 +109,12 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
tech.progress = null;
|
||||
this.progressToBroadcast.delete(name);
|
||||
|
||||
this.broadcastEventToHosts(this.info.messages.finished, { name, level: tech.level });
|
||||
this.controller.sendTo("allInstances", new FinishedEvent(name, tech.level));
|
||||
}
|
||||
}
|
||||
|
||||
async finishedEventHandler(message) {
|
||||
let { name, level } = message.data;
|
||||
async handleFinishedEvent(event) {
|
||||
let { name, level } = event;
|
||||
let tech = this.technologies.get(name);
|
||||
if (!tech || tech.level <= level) {
|
||||
this.progressToBroadcast.delete(name);
|
||||
@@ -110,7 +122,7 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
async syncTechnologiesRequestHandler(message) {
|
||||
async handleSyncTechnologiesRequest(request) {
|
||||
function baseLevel(name) {
|
||||
let match = /-(\d+)$/.exec(name);
|
||||
if (!match) {
|
||||
@@ -119,15 +131,15 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
return Number.parseInt(match[1], 10);
|
||||
}
|
||||
|
||||
for (let instanceTech of message.data.technologies) {
|
||||
let [name, level, progress, researched] = instanceTech;
|
||||
for (let instanceTech of request.technologies) {
|
||||
let { name, level, progress, researched } = instanceTech;
|
||||
let tech = this.technologies.get(name);
|
||||
if (!tech) {
|
||||
this.technologies.set(name, { level, progress, researched });
|
||||
if (progress) {
|
||||
this.progressToBroadcast.add(name);
|
||||
} else if (researched || baseLevel(name) !== level) {
|
||||
this.broadcastEventToHosts(this.info.messages.finished, { name, level });
|
||||
this.controller.sendTo("allInstances", new FinishedEvent(name, tech));
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -138,7 +150,7 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
if (tech.level < level || researched) {
|
||||
// Send update if the unlocked level is greater
|
||||
if (level - !researched > tech.level - !tech.researched) {
|
||||
this.broadcastEventToHosts(this.info.messages.finished, { name, level: level - !researched });
|
||||
this.controller.sendTo("allInstances", new FinishedEvent(name, level - !researched));
|
||||
}
|
||||
tech.level = level;
|
||||
tech.progress = progress;
|
||||
@@ -160,10 +172,10 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
|
||||
let technologies = [];
|
||||
for (let [name, tech] of this.technologies) {
|
||||
technologies.push([name, tech.level, tech.progress, tech.researched]);
|
||||
technologies.push(new Technology(name, tech.level, tech.progress, tech.researched));
|
||||
}
|
||||
|
||||
return { technologies };
|
||||
return technologies;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,5 @@
|
||||
"use strict";
|
||||
const { libLink } = require("@clusterio/lib");
|
||||
|
||||
|
||||
// schema used for syncing technologies to and from the controller
|
||||
const technologies = {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "array",
|
||||
minItems: 4,
|
||||
maxItems: 4,
|
||||
items: [
|
||||
{ type: "string" },
|
||||
{ type: "integer" },
|
||||
{ type: ["null", "number"] },
|
||||
{ type: "boolean" },
|
||||
],
|
||||
},
|
||||
};
|
||||
const messages = require("./messages");
|
||||
|
||||
module.exports = {
|
||||
name: "research_sync",
|
||||
@@ -25,56 +8,10 @@ module.exports = {
|
||||
instanceEntrypoint: "instance",
|
||||
controllerEntrypoint: "controller",
|
||||
|
||||
messages: {
|
||||
contribution: new libLink.Event({
|
||||
type: "research_sync:contribution",
|
||||
links: ["instance-host", "host-controller"],
|
||||
forwardTo: "controller",
|
||||
eventProperties: {
|
||||
"name": { type: "string" },
|
||||
"level": { type: "integer" },
|
||||
"contribution": { type: "number" },
|
||||
},
|
||||
}),
|
||||
progress: new libLink.Event({
|
||||
type: "research_sync:progress",
|
||||
links: ["controller-host", "host-instance"],
|
||||
broadcastTo: "instance",
|
||||
eventProperties: {
|
||||
"technologies": {
|
||||
type: "array",
|
||||
items: {
|
||||
additionalProperties: false,
|
||||
required: ["name", "level", "progress"],
|
||||
properties: {
|
||||
"name": { type: "string" },
|
||||
"level": { type: "integer" },
|
||||
"progress": { type: "number" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
finished: new libLink.Event({
|
||||
type: "research_sync:finished",
|
||||
links: ["instance-host", "host-controller", "controller-host", "host-instance"],
|
||||
forwardTo: "controller",
|
||||
broadcastTo: "instance",
|
||||
eventProperties: {
|
||||
"name": { type: "string" },
|
||||
"level": { type: "integer" },
|
||||
},
|
||||
}),
|
||||
syncTechnologies: new libLink.Request({
|
||||
type: "research_sync:sync_technologies",
|
||||
links: ["instance-host", "host-controller"],
|
||||
forwardTo: "controller",
|
||||
requestProperties: {
|
||||
"technologies": technologies,
|
||||
},
|
||||
responseProperties: {
|
||||
"technologies": technologies,
|
||||
},
|
||||
}),
|
||||
},
|
||||
messages: [
|
||||
messages.ContributionEvent,
|
||||
messages.ProgressEvent,
|
||||
messages.FinishedEvent,
|
||||
messages.SyncTechnologiesRequest,
|
||||
],
|
||||
};
|
||||
@@ -1,6 +1,13 @@
|
||||
"use strict";
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
const libLuaTools = require("@clusterio/lib/lua_tools");
|
||||
const {
|
||||
ContributionEvent,
|
||||
ProgressEvent,
|
||||
FinishedEvent,
|
||||
Technology,
|
||||
SyncTechnologiesRequest,
|
||||
} = require("./messages");
|
||||
|
||||
|
||||
class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
@@ -21,29 +28,31 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
});
|
||||
|
||||
this.syncStarted = false;
|
||||
this.instance.handle(ProgressEvent, this.handleProgressEvent.bind(this));
|
||||
this.instance.handle(FinishedEvent, this.handleFinishedEvent.bind(this));
|
||||
}
|
||||
|
||||
async researchContribution(tech) {
|
||||
this.info.messages.contribution.send(this.instance, tech);
|
||||
this.instance.sendTo("controller", new ContributionEvent(tech.name, tech.level, tech.contribution));
|
||||
}
|
||||
|
||||
async progressEventHandler(message) {
|
||||
async handleProgressEvent(event) {
|
||||
if (!this.syncStarted || !["starting", "running"].includes(this.instance.status)) {
|
||||
return;
|
||||
}
|
||||
let techsJson = libLuaTools.escapeString(JSON.stringify(message.data.technologies));
|
||||
let techsJson = libLuaTools.escapeString(JSON.stringify(event.technologies));
|
||||
await this.sendOrderedRcon(`/sc research_sync.update_progress("${techsJson}")`, true);
|
||||
}
|
||||
|
||||
async researchFinished(tech) {
|
||||
this.info.messages.finished.send(this.instance, tech);
|
||||
this.instance.sendTo("allInstances", new FinishedEvent(tech.name, tech.level));
|
||||
}
|
||||
|
||||
async finishedEventHandler(message) {
|
||||
async handleFinishedEvent(event) {
|
||||
if (!this.syncStarted || !["starting", "running"].includes(this.instance.status)) {
|
||||
return;
|
||||
}
|
||||
let { name, level } = message.data;
|
||||
let { name, level } = event;
|
||||
await this.sendOrderedRcon(
|
||||
`/sc research_sync.research_technology("${libLuaTools.escapeString(name)}", ${level})`, true
|
||||
);
|
||||
@@ -54,20 +63,20 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
let techsToSend = [];
|
||||
let instanceTechs = new Map();
|
||||
for (let tech of JSON.parse(dumpJson)) {
|
||||
techsToSend.push([
|
||||
techsToSend.push(new Technology(
|
||||
tech.name,
|
||||
tech.level,
|
||||
tech.progress || null,
|
||||
tech.researched,
|
||||
]);
|
||||
));
|
||||
instanceTechs.set(tech.name, tech);
|
||||
}
|
||||
|
||||
let response = await this.info.messages.syncTechnologies.send(this.instance, { technologies: techsToSend });
|
||||
let controllerTechs = await this.instance.sendTo("controller", new SyncTechnologiesRequest(techsToSend));
|
||||
this.syncStarted = true;
|
||||
let techsToSync = [];
|
||||
for (let controllerTech of response.technologies) {
|
||||
let [name, level, progress, researched] = controllerTech;
|
||||
for (let controllerTech of controllerTechs) {
|
||||
let { name, level, progress, researched } = controllerTech;
|
||||
let instanceTech = instanceTechs.get(name);
|
||||
if (
|
||||
!instanceTech
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
"use strict";
|
||||
const { libData } = require("@clusterio/lib");
|
||||
|
||||
class ContributionEvent {
|
||||
static type = "event";
|
||||
static src = "instance";
|
||||
static dst = "controller";
|
||||
static plugin = "research_sync";
|
||||
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {number} */
|
||||
level;
|
||||
/** @type {number} */
|
||||
contribution;
|
||||
|
||||
constructor(name, level, contribution) {
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
this.contribution = contribution;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["name", "level", "contribution"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
level: { type: "integer" },
|
||||
contribution: { type: "number" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.name, json.level, json.contribution);
|
||||
}
|
||||
}
|
||||
|
||||
class ProgressEvent {
|
||||
static type = "event";
|
||||
static src = "controller";
|
||||
static dst = "instance";
|
||||
static plugin = "research_sync";
|
||||
|
||||
/** @type {Array<>} */
|
||||
technologies;
|
||||
|
||||
constructor(technologies) {
|
||||
this.technologies = technologies;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["technologies"],
|
||||
properties: {
|
||||
technologies: {
|
||||
type: "array",
|
||||
items: {
|
||||
required: ["name", "level", "progress"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
level: { type: "integer" },
|
||||
progress: { type: "number" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.technologies);
|
||||
}
|
||||
}
|
||||
|
||||
class FinishedEvent {
|
||||
static type = "event";
|
||||
static src = ["instance", "controller"];
|
||||
static dst = "instance";
|
||||
static plugin = "research_sync";
|
||||
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {number} */
|
||||
level;
|
||||
|
||||
constructor(name, level) {
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "object",
|
||||
required: ["name", "level"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
level: { type: "integer" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.name, json.level);
|
||||
}
|
||||
}
|
||||
|
||||
class Technology {
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {number} */
|
||||
level;
|
||||
/** @type {?number} */
|
||||
progress;
|
||||
/** @type {boolean} */
|
||||
researched;
|
||||
|
||||
constructor(name, level, progress, researched) {
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
this.progress = progress;
|
||||
this.researched = researched;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "array",
|
||||
minItems: 4,
|
||||
maxItems: 4,
|
||||
items: [
|
||||
{ type: "string" },
|
||||
{ type: "integer" },
|
||||
{ type: ["null", "number"] },
|
||||
{ type: "boolean" },
|
||||
],
|
||||
};
|
||||
|
||||
toJSON() {
|
||||
return [this.name, this.level, this.progress, this.researched];
|
||||
}
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json[0], json[1], json[2], json[3]);
|
||||
}
|
||||
}
|
||||
|
||||
class SyncTechnologiesRequest {
|
||||
static type = "request";
|
||||
static src = "instance";
|
||||
static dst = "controller";
|
||||
static plugin = "research_sync";
|
||||
|
||||
/** @type {Array<Technology>} */
|
||||
technologies;
|
||||
|
||||
constructor(technologies) {
|
||||
this.technologies = technologies;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "array",
|
||||
items: Technology.jsonSchema,
|
||||
};
|
||||
|
||||
toJSON() {
|
||||
return this.technologies;
|
||||
}
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.map(e => Technology.fromJSON(e)));
|
||||
}
|
||||
|
||||
static Response = libData.jsonArray(Technology);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ContributionEvent,
|
||||
ProgressEvent,
|
||||
FinishedEvent,
|
||||
Technology,
|
||||
SyncTechnologiesRequest,
|
||||
};
|
||||
@@ -11,6 +11,15 @@ const RateLimiter = require("@clusterio/lib/RateLimiter");
|
||||
const routes = require("./routes");
|
||||
const dole = require("./dole");
|
||||
|
||||
const {
|
||||
Item,
|
||||
PlaceEvent,
|
||||
RemoveRequest,
|
||||
GetStorageRequest,
|
||||
UpdateStorageEvent,
|
||||
SetStorageSubscriptionRequest,
|
||||
} = require("./messages");
|
||||
|
||||
|
||||
const exportCounter = new Counter(
|
||||
"clusterio_subspace_storage_export_total",
|
||||
@@ -87,6 +96,11 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
this.subscribedControlLinks = new Set();
|
||||
|
||||
routes.addApiRoutes(this.controller.app, this.items);
|
||||
|
||||
this.controller.handle(GetStorageRequest, this.handleGetStorageRequest.bind(this));
|
||||
this.controller.handle(PlaceEvent, this.handlePlaceEvent.bind(this));
|
||||
this.controller.handle(RemoveRequest, this.handleRemoveRequest.bind(this));
|
||||
this.controller.handle(SetStorageSubscriptionRequest, this.handleSetStorageSubscriptionRequest.bind(this));
|
||||
}
|
||||
|
||||
updateStorage() {
|
||||
@@ -107,48 +121,49 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
}
|
||||
|
||||
itemsToUpdate = [...itemsToUpdate.entries()];
|
||||
this.broadcastEventToHosts(this.info.messages.updateStorage, { items: itemsToUpdate });
|
||||
let update = new UpdateStorageEvent(itemsToUpdate);
|
||||
this.controller.sendTo("allInstances", update);
|
||||
for (let link of this.subscribedControlLinks) {
|
||||
this.info.messages.updateStorage.send(link, { items: itemsToUpdate });
|
||||
link.send(update);
|
||||
}
|
||||
this.itemsLastUpdate = new Map(this.items._items.entries());
|
||||
}
|
||||
|
||||
async getStorageRequestHandler(message) {
|
||||
return { items: [...this.items._items.entries()] };
|
||||
async handleGetStorageRequest() {
|
||||
return [...this.items._items.entries()];
|
||||
}
|
||||
|
||||
async placeEventHandler(message) {
|
||||
let instanceId = message.data.instance_id;
|
||||
async handlePlaceEvent(request, src) {
|
||||
let instanceId = src.id;
|
||||
|
||||
for (let item of message.data.items) {
|
||||
this.items.addItem(item[0], item[1]);
|
||||
exportCounter.labels(String(instanceId), item[0]).inc(item[1]);
|
||||
for (let item of request.items) {
|
||||
this.items.addItem(item.name, item.count);
|
||||
exportCounter.labels(String(instanceId), item.name).inc(item.count);
|
||||
}
|
||||
|
||||
this.updateStorage();
|
||||
|
||||
if (this.controller.config.get("subspace_storage.log_item_transfers")) {
|
||||
this.logger.verbose(
|
||||
`Imported the following from ${message.data.instance_id}:\n${JSON.stringify(message.data.items)}`
|
||||
`Imported the following from ${instanceId}:\n${JSON.stringify(request.items)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async removeRequestHandler(message) {
|
||||
async handleRemoveRequest(request, src) {
|
||||
let method = this.controller.config.get("subspace_storage.division_method");
|
||||
let instanceId = message.data.instance_id;
|
||||
let instanceId = src.id;
|
||||
|
||||
let itemsRemoved = [];
|
||||
if (method === "simple") {
|
||||
// Give out as much items as possible until there are 0 left. This
|
||||
// might lead to one host getting all the items and the rest nothing.
|
||||
for (let item of message.data.items) {
|
||||
let count = this.items.getItemCount(item[0]);
|
||||
let toRemove = Math.min(count, item[1]);
|
||||
for (let item of request.items) {
|
||||
let count = this.items.getItemCount(item.name);
|
||||
let toRemove = Math.min(count, item.count);
|
||||
if (toRemove > 0) {
|
||||
this.items.removeItem(item[0], toRemove);
|
||||
itemsRemoved.push([item[0], toRemove]);
|
||||
this.items.removeItem(item.name, toRemove);
|
||||
itemsRemoved.push(new Item(item.name, toRemove));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,24 +173,24 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
|
||||
// use fancy neural net to calculate a "fair" dole division rate.
|
||||
if (method === "neural_dole") {
|
||||
for (let item of message.data.items) {
|
||||
let count = neuralDole.divider({ name: item[0], count: item[1], instanceId, instanceName });
|
||||
for (let item of request.items) {
|
||||
let count = neuralDole.divider({ name: item.name, count: item.count, instanceId, instanceName });
|
||||
if (count > 0) {
|
||||
itemsRemoved.push([item[0], count]);
|
||||
itemsRemoved.push(new Item(item.name, count));
|
||||
}
|
||||
}
|
||||
|
||||
// Use dole division. Makes it really slow to drain out the last little bit.
|
||||
} else if (method === "dole") {
|
||||
for (let item of message.data.items) {
|
||||
for (let item of request.items) {
|
||||
let count = dole.doleDivider({
|
||||
object: { name: item[0], count: item[1], instanceId, instanceName },
|
||||
object: { name: item.name, count: item.count, instanceId, instanceName },
|
||||
items: this.items,
|
||||
logItemTransfers: this.controller.config.get("subspace_storage.log_item_transfers"),
|
||||
logger: this.logger,
|
||||
});
|
||||
if (count > 0) {
|
||||
itemsRemoved.push([item[0], count]);
|
||||
itemsRemoved.push(new Item(item.name, count));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +202,7 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
|
||||
if (itemsRemoved.length) {
|
||||
for (let item of itemsRemoved) {
|
||||
importCounter.labels(String(instanceId), item[0]).inc(item[1]);
|
||||
importCounter.labels(String(instanceId), item.name).inc(item.count);
|
||||
}
|
||||
|
||||
this.updateStorage();
|
||||
@@ -197,13 +212,12 @@ class ControllerPlugin extends libPlugin.BaseControllerPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
items: itemsRemoved,
|
||||
};
|
||||
return itemsRemoved;
|
||||
}
|
||||
|
||||
async setStorageSubscriptionRequestHandler(message, request, link) {
|
||||
if (message.data.storage) {
|
||||
async handleSetStorageSubscriptionRequest(request, src) {
|
||||
let link = this.controller.wsServer.controlConnections.get(src.id);
|
||||
if (request.storage) {
|
||||
this.subscribedControlLinks.add(link);
|
||||
} else {
|
||||
this.subscribedControlLinks.delete(link);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use strict";
|
||||
const { libConfig, libLink, libUsers } = require("@clusterio/lib");
|
||||
const { libConfig, libUsers } = require("@clusterio/lib");
|
||||
const messages = require("./messages");
|
||||
|
||||
class ControllerConfigGroup extends libConfig.PluginConfigGroup {}
|
||||
ControllerConfigGroup.defaultAccess = ["controller", "host", "control"];
|
||||
@@ -47,20 +48,6 @@ libUsers.definePermission({
|
||||
grantByDefault: true,
|
||||
});
|
||||
|
||||
// JSON schema for subspace storage items
|
||||
const items = {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "array",
|
||||
minItems: 2,
|
||||
maxItems: 2,
|
||||
items: [
|
||||
{ type: "string" },
|
||||
{ type: "integer" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
name: "subspace_storage",
|
||||
title: "Subspace Storage",
|
||||
@@ -71,57 +58,13 @@ module.exports = {
|
||||
controllerEntrypoint: "controller",
|
||||
ControllerConfigGroup,
|
||||
|
||||
messages: [
|
||||
messages.PlaceEvent,
|
||||
messages.RemoveRequest,
|
||||
messages.GetStorageRequest,
|
||||
messages.UpdateStorageEvent,
|
||||
messages.SetStorageSubscriptionRequest,
|
||||
],
|
||||
webEntrypoint: "./web",
|
||||
routes: ["/storage"],
|
||||
|
||||
messages: {
|
||||
// XXX this should be a request to be reliable
|
||||
place: new libLink.Event({
|
||||
type: "subspace_storage:place",
|
||||
links: ["instance-host", "host-controller"],
|
||||
forwardTo: "controller",
|
||||
eventProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"items": items,
|
||||
},
|
||||
}),
|
||||
remove: new libLink.Request({
|
||||
type: "subspace_storage:remove",
|
||||
links: ["instance-host", "host-controller"],
|
||||
forwardTo: "controller",
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"items": items,
|
||||
},
|
||||
responseProperties: {
|
||||
"items": items,
|
||||
},
|
||||
}),
|
||||
getStorage: new libLink.Request({
|
||||
type: "subspace_storage:get_storage",
|
||||
links: ["instance-host", "host-controller", "control-controller"],
|
||||
permission: "subspace_storage.storage.view",
|
||||
forwardTo: "controller",
|
||||
responseProperties: {
|
||||
"items": items,
|
||||
},
|
||||
}),
|
||||
updateStorage: new libLink.Event({
|
||||
type: "subspace_storage:update_storage",
|
||||
links: ["controller-host", "host-instance", "controller-control"],
|
||||
broadcastTo: "instance",
|
||||
eventProperties: {
|
||||
"items": items,
|
||||
},
|
||||
}),
|
||||
|
||||
setStorageSubscription: new libLink.Request({
|
||||
type: "subspace_storage:set_storage_subscription",
|
||||
links: ["control-controller"],
|
||||
permission: "subspace_storage.storage.view",
|
||||
requestProperties: {
|
||||
"storage": { type: "boolean" },
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -2,6 +2,15 @@
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
const libLuaTools = require("@clusterio/lib/lua_tools");
|
||||
|
||||
const {
|
||||
Item,
|
||||
PlaceEvent,
|
||||
RemoveRequest,
|
||||
GetStorageRequest,
|
||||
UpdateStorageEvent,
|
||||
SetStorageSubscriptionRequest,
|
||||
} = require("./messages");
|
||||
|
||||
class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
unexpectedError(err) {
|
||||
this.logger.error(`Unexpected error:\n${err.stack}`);
|
||||
@@ -21,6 +30,8 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
this.pendingTasks.add(task);
|
||||
task.finally(() => { this.pendingTasks.delete(task); });
|
||||
});
|
||||
|
||||
this.instance.handle(UpdateStorageEvent, this.handleUpdateStorageEvent.bind(this));
|
||||
}
|
||||
|
||||
async onStart() {
|
||||
@@ -33,9 +44,9 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
).catch(err => this.unexpectedError(err));
|
||||
}, 5000);
|
||||
|
||||
let response = await this.info.messages.getStorage.send(this.instance);
|
||||
let items = await this.instance.sendTo("controller", new GetStorageRequest());
|
||||
// TODO Diff with dump of invdata produce minimal command to sync
|
||||
let itemsJson = libLuaTools.escapeString(JSON.stringify(response.items));
|
||||
let itemsJson = libLuaTools.escapeString(JSON.stringify(items));
|
||||
await this.sendRcon(`/sc __subspace_storage__ UpdateInvData("${itemsJson}", true)`, true);
|
||||
}
|
||||
|
||||
@@ -60,10 +71,7 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
this.info.messages.place.send(this.instance, {
|
||||
items,
|
||||
instance_id: this.instance.id,
|
||||
});
|
||||
this.instance.sendTo("controller", new PlaceEvent(items));
|
||||
|
||||
if (this.instance.config.get("subspace_storage.log_item_transfers")) {
|
||||
this.logger.verbose("Exported the following to controller:");
|
||||
@@ -72,32 +80,29 @@ class InstancePlugin extends libPlugin.BaseInstancePlugin {
|
||||
}
|
||||
|
||||
// request items --------------------------------------------------------------
|
||||
async requestItems(items) {
|
||||
async requestItems(requestItems) {
|
||||
// Request the items all at once
|
||||
let response = await this.info.messages.remove.send(this.instance, {
|
||||
instance_id: this.instance.id,
|
||||
items,
|
||||
});
|
||||
let items = await this.instance.sendTo("controller", new RemoveRequest(requestItems));
|
||||
|
||||
if (!response.items.length) {
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.instance.config.get("subspace_storage.log_item_transfers")) {
|
||||
this.logger.verbose("Imported following from controller:");
|
||||
this.logger.verbose(JSON.stringify(response.items));
|
||||
this.logger.verbose(JSON.stringify(items));
|
||||
}
|
||||
|
||||
let itemsJson = libLuaTools.escapeString(JSON.stringify(response.items));
|
||||
let itemsJson = libLuaTools.escapeString(JSON.stringify(items));
|
||||
await this.sendRcon(`/sc __subspace_storage__ Import("${itemsJson}")`, true);
|
||||
}
|
||||
|
||||
// combinator signals ---------------------------------------------------------
|
||||
async updateStorageEventHandler(message) {
|
||||
async handleUpdateStorageEvent(request) {
|
||||
if (this.instance.status !== "running") {
|
||||
return;
|
||||
}
|
||||
let items = message.data.items;
|
||||
let items = request.items;
|
||||
|
||||
// XXX this should be moved to instance/clusterio api
|
||||
items.push(["signal-unixtime", Math.floor(Date.now()/1000)]);
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
"use strict";
|
||||
const { libData } = require("@clusterio/lib");
|
||||
|
||||
class Item {
|
||||
/** @type {string} */
|
||||
name;
|
||||
/** @type {number} */
|
||||
count;
|
||||
|
||||
constructor(name, count) {
|
||||
this.name = name;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
type: "array",
|
||||
minItems: 2,
|
||||
maxItems: 2,
|
||||
items: [
|
||||
{ type: "string" },
|
||||
{ type: "integer" },
|
||||
],
|
||||
};
|
||||
|
||||
toJSON() {
|
||||
return [this.name, this.count];
|
||||
}
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json[0], json[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX this should be a request to be reliable
|
||||
class PlaceEvent {
|
||||
static type = "event";
|
||||
static src = "instance";
|
||||
static dst = "controller";
|
||||
static plugin = "subspace_storage";
|
||||
|
||||
/** @type {Array<Item>} */
|
||||
items;
|
||||
|
||||
constructor(items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
required: ["items"],
|
||||
properties: {
|
||||
"items": {
|
||||
type: "array",
|
||||
items: Item.jsonSchema,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.items);
|
||||
}
|
||||
}
|
||||
|
||||
class RemoveRequest {
|
||||
static type = "request";
|
||||
static src = "instance";
|
||||
static dst = "controller";
|
||||
static plugin = "subspace_storage";
|
||||
|
||||
/** @type {Array<Item>} */
|
||||
items;
|
||||
|
||||
constructor(items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
required: ["items"],
|
||||
properties: {
|
||||
"items": {
|
||||
type: "array",
|
||||
items: Item.jsonSchema,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.items);
|
||||
}
|
||||
|
||||
static Response = libData.jsonArray(Item);
|
||||
}
|
||||
|
||||
class GetStorageRequest {
|
||||
static type = "request";
|
||||
static src = ["instance", "control"];
|
||||
static dst = "controller";
|
||||
static plugin = "subspace_storage";
|
||||
static permission = "subspace_storage.storage.view";
|
||||
static Response = libData.jsonArray(Item);
|
||||
}
|
||||
|
||||
class UpdateStorageEvent {
|
||||
static type = "request";
|
||||
static src = "controller";
|
||||
static dst = ["instance", "control"];
|
||||
static plugin = "subspace_storage";
|
||||
|
||||
/** @type {Array<Item>} */
|
||||
items;
|
||||
|
||||
constructor(items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
required: ["items"],
|
||||
properties: {
|
||||
"items": {
|
||||
type: "array",
|
||||
items: Item.jsonSchema,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.items);
|
||||
}
|
||||
}
|
||||
|
||||
class SetStorageSubscriptionRequest {
|
||||
static type = "request";
|
||||
static src = "control";
|
||||
static dst = "controller";
|
||||
static plugin = "subspace_storage";
|
||||
static permission = "subspace_storage.storage.view";
|
||||
|
||||
/** @type {boolean} */
|
||||
storage;
|
||||
|
||||
constructor(storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
static jsonSchema = {
|
||||
required: ["storage"],
|
||||
properties: {
|
||||
"storage": { type: "boolean" },
|
||||
},
|
||||
};
|
||||
|
||||
static fromJSON(json) {
|
||||
return new this(json.storage);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Item,
|
||||
PlaceEvent,
|
||||
RemoveRequest,
|
||||
GetStorageRequest,
|
||||
UpdateStorageEvent,
|
||||
SetStorageSubscriptionRequest,
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { Input, Table, Typography } from "antd";
|
||||
|
||||
import { libPlugin } from "@clusterio/lib";
|
||||
import { notifyErrorHandler, useItemMetadata, useLocale, PageLayout, ControlContext } from "@clusterio/web_ui";
|
||||
import info from "../info";
|
||||
import { GetStorageRequest, SetStorageSubscriptionRequest } from "../messages";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
@@ -135,8 +135,8 @@ export class WebPlugin extends libPlugin.BaseWebPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
async updateStorageEventHandler(message) {
|
||||
this.updateStorage(message.data.items);
|
||||
async updateStorageEventHandler(event) {
|
||||
this.updateStorage(event.items);
|
||||
}
|
||||
|
||||
onUpdate(callback) {
|
||||
@@ -159,8 +159,8 @@ export class WebPlugin extends libPlugin.BaseWebPlugin {
|
||||
}
|
||||
|
||||
updateStorage(items) {
|
||||
for (let [name, amount] of items) {
|
||||
this.storage.set(name, amount);
|
||||
for (let item of items) {
|
||||
this.storage.set(item.name, item.count);
|
||||
}
|
||||
for (let callback of this.callbacks) {
|
||||
callback();
|
||||
@@ -172,14 +172,14 @@ export class WebPlugin extends libPlugin.BaseWebPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
info.messages.setStorageSubscription.send(
|
||||
this.control, { storage: Boolean(this.callbacks.length) }
|
||||
this.control.send(
|
||||
new SetStorageSubscriptionRequest(Boolean(this.callbacks.length))
|
||||
).catch(notifyErrorHandler("Error subscribing to storage"));
|
||||
|
||||
if (this.callbacks.length) {
|
||||
info.messages.getStorage.send(this.control).then(
|
||||
result => {
|
||||
this.updateStorage(result.items);
|
||||
this.control.send(new GetStorageRequest()).then(
|
||||
items => {
|
||||
this.updateStorage(items);
|
||||
}
|
||||
).catch(notifyErrorHandler("Error updating storage"));
|
||||
} else {
|
||||
|
||||
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user