mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 07:04:53 +09:00
Refactor link system to JSON serialisable class pattern
Reverse the direction of the API for sending messages from
messageDefinition.send(link, { content });
to
link.send(new MessageClass(content));
and convert all message definitions to use the JSON serialisable class
pattern and explicit registration of message handlers, along with a
end-to-end based routing of messages instead of link to link.
The experience I gained from the existing way messages were defined and
sent over links was that it was difficult to understand for people
unfamiliar with the system. The automatic registration of message
handlers ment that code was being called as if by magic from nowhere
with no easy way to discover it. The manual forwarding and rules for
what can be sent where and how obscured its working.
The use of pure JSON as both input and output from message handlers made
it tedious to work with. Data had to be manually converted between the
program representation and the over the wire format, which made handler
code unplesant to write. Particularly because the over the wire format
used snake_case while the program code uses camelCase for variables.
The idea behind the decision to do it this way was to make it more clear
which data came from the wire and which from the program, and while this
worked it didn't make message handlers any nicer to write.
The direction of the API and magic registration of message handlers also
meant that a future migration to TypeScript would be troublesome. While
it may be possible to implement, the degree of TypeScript magic required
would go starkly against the principle of keep it simple stupid.
By refactoring to using the JSON serialisable class pattern most of
these issues go away. There is no longer a need to distinguish the on
wire format of messages with the program representation because the only
place that deals with the wire format of messages are the toJSON and
fromJSON methods on the classes themselves. Message handlers will work
with proper instances of classes instead of raw data, something that
makes them easier to write, and trivial to type with TypeScript.
End-to-end routing also simplifies sending messages that need to do
mulitple hops to reach the destination. And removes the need for having
all messages be specified in plugin's info module, as they can be routed
without having to be parsed completely.
The explicit registration of message handlers is something that I
thought would be tedious boilerplate code that could be automated away.
But experience now shows it to be the opposite. Allthough dull
repetition, the purpose it serves is to link together the code in a way
that humans can easily find. With explicit registration searching the
name of the message handler in the editor will show the registration that
ties it together with the type of message it handles and the kind of
registration. With implicit registration searching the name of the
message handler turns up only the handler with no clue as to what calls
it.
This commit is contained in:
1 parent
9a967a5779
commit
bcd94709d2
106 files changed
+8173
-4928
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 }],
|
||||
|
||||
@@ -46,7 +46,7 @@ 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))`.
|
||||
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.
|
||||
|
||||
+3
-3
@@ -21,16 +21,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",
|
||||
|
||||
@@ -102,7 +102,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);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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.register(Request, handler); }
|
||||
for (let [Request, handler] of controller._fallbackedRequests) { this.fallbackRequest(Request, handler); }
|
||||
for (let [Event, handler] of controller._registeredEvents) { this.register(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.register(libData.ModPackGetRequest, this.handleModPackGetRequest.bind(this));
|
||||
this.register(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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,11 +30,13 @@ const queryLogTime = new libPrometheus.Summary(
|
||||
* @alias module:controller/src/ControlConnection
|
||||
*/
|
||||
class ControlConnection extends BaseConnection {
|
||||
constructor(registerData, connector, controller, user) {
|
||||
super("control", connector, controller);
|
||||
constructor(registerData, connector, controller, user, id) {
|
||||
super(connector, controller);
|
||||
|
||||
this._agent = registerData.agent;
|
||||
this._version = registerData.version;
|
||||
this.id = id;
|
||||
this.dst = new libData.Address(libData.Address.instance, id);
|
||||
|
||||
/**
|
||||
* The user making this connection.
|
||||
@@ -44,27 +46,27 @@ class ControlConnection extends BaseConnection {
|
||||
|
||||
this.hostSubscriptions = {
|
||||
all: false,
|
||||
host_ids: [],
|
||||
hostIds: [],
|
||||
};
|
||||
|
||||
this.instanceSubscriptions = {
|
||||
all: false,
|
||||
instance_ids: [],
|
||||
instanceIds: [],
|
||||
};
|
||||
|
||||
this.saveListSubscriptions = {
|
||||
all: false,
|
||||
instance_ids: [],
|
||||
instanceIds: [],
|
||||
};
|
||||
|
||||
this.modPackSubscriptions = {
|
||||
all: false,
|
||||
mod_pack_ids: [],
|
||||
modPackIds: [],
|
||||
};
|
||||
|
||||
this.modSubscriptions = {
|
||||
all: false,
|
||||
mod_names: [],
|
||||
modNames: [],
|
||||
};
|
||||
|
||||
this.userSubscriptions = {
|
||||
@@ -76,8 +78,8 @@ class ControlConnection extends BaseConnection {
|
||||
this.logSubscriptions = {
|
||||
all: false,
|
||||
controller: false,
|
||||
host_ids: [],
|
||||
instance_ids: [],
|
||||
hostIds: [],
|
||||
instanceIds: [],
|
||||
};
|
||||
|
||||
this.ws_dumper = null;
|
||||
@@ -101,183 +103,245 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.register(libData.ControllerConfigGetRequest, this.handleControllerConfigGetRequest.bind(this));
|
||||
this.register(libData.ControllerConfigSetFieldRequest, this.handleControllerConfigSetFieldRequest.bind(this));
|
||||
this.register(libData.ControllerConfigSetPropRequest, this.handleControllerConfigSetPropRequest.bind(this));
|
||||
this.register(libData.HostListRequest, this.handleHostListRequest.bind(this));
|
||||
this.register(libData.HostSetSubscriptionsRequest, this.handleHostSetSubscriptionsRequest.bind(this));
|
||||
this.register(libData.HostGenerateTokenRequest, this.handleHostGenerateTokenRequest.bind(this));
|
||||
this.register(libData.HostConfigCreateRequest, this.handleHostConfigCreateRequest.bind(this));
|
||||
this.register(libData.InstanceDetailsGetRequest, this.handleInstanceDetailsGetRequest.bind(this));
|
||||
this.register(libData.InstanceDetailsListRequest, this.handleInstanceDetailsListRequest.bind(this));
|
||||
this.register(
|
||||
libData.InstanceDetailsSetSubscriptionsRequest, this.handleInstanceDetailsSetSubscriptionsRequest.bind(this)
|
||||
);
|
||||
this.register(libData.InstanceCreateRequest, this.handleInstanceCreateRequest.bind(this));
|
||||
this.register(libData.InstanceDeleteRequest, this.handleInstanceDeleteRequest.bind(this));
|
||||
this.register(libData.InstanceConfigGetRequest, this.handleInstanceConfigGetRequest.bind(this));
|
||||
this.register(libData.InstanceConfigSetFieldRequest, this.handleInstanceConfigSetFieldRequest.bind(this));
|
||||
this.register(libData.InstanceConfigSetPropRequest, this.handleInstanceConfigSetPropRequest.bind(this));
|
||||
this.register(libData.InstanceAssignRequest, this.handleInstanceAssignRequest.bind(this));
|
||||
this.register(
|
||||
libData.InstanceSetSaveListSubscriptionsRequest,
|
||||
this.handleInstanceSetSaveListSubscriptionsRequest.bind(this)
|
||||
);
|
||||
this.register(
|
||||
libData.InstanceRenameSaveRequest, this._controller.sendToHostByInstanceId.bind(this._controller)
|
||||
);
|
||||
this.register(libData.InstanceCopySaveRequest, this._controller.sendToHostByInstanceId.bind(this._controller));
|
||||
this.register(
|
||||
libData.InstanceDeleteSaveRequest, this._controller.sendToHostByInstanceId.bind(this._controller)
|
||||
);
|
||||
this.register(libData.InstanceDownloadSaveRequest, this.handleInstanceDownloadSaveRequest.bind(this));
|
||||
this.register(libData.InstanceTransferSaveRequest, this.handleInstanceTransferSaveRequest.bind(this));
|
||||
this.register(libData.ModPackListRequest, this.handleModPackListRequest.bind(this));
|
||||
this.register(libData.ModPackSetSubscriptionsRequest, this.handleModPackSetSubscriptionsRequest.bind(this));
|
||||
this.register(libData.ModPackCreateRequest, this.handleModPackCreateRequest.bind(this));
|
||||
this.register(libData.ModPackUpdateRequest, this.handleModPackUpdateRequest.bind(this));
|
||||
this.register(libData.ModPackDeleteRequest, this.handleModPackDeleteRequest.bind(this));
|
||||
this.register(libData.ModGetRequest, this.handleModGetRequest.bind(this));
|
||||
this.register(libData.ModListRequest, this.handleModListRequest.bind(this));
|
||||
this.register(libData.ModSearchRequest, this.handleModSearchRequest.bind(this));
|
||||
this.register(libData.ModSetSubscriptionsRequest, this.handleModSetSubscriptionsRequest.bind(this));
|
||||
this.register(libData.ModDownloadRequest, this.handleModDownloadRequest.bind(this));
|
||||
this.register(libData.ModDeleteRequest, this.handleModDeleteRequest.bind(this));
|
||||
this.register(libData.LogSetSubscriptionsRequest, this.handleLogSetSubscriptionsRequest.bind(this));
|
||||
this.register(libData.LogQueryRequest, this.handleLogQueryRequest.bind(this));
|
||||
this.register(libData.PermissionListRequest, this.handlePermissionListRequest.bind(this));
|
||||
this.register(libData.RoleListRequest, this.handleRoleListRequest.bind(this));
|
||||
this.register(libData.RoleCreateRequest, this.handleRoleCreateRequest.bind(this));
|
||||
this.register(libData.RoleUpdateRequest, this.handleRoleUpdateRequest.bind(this));
|
||||
this.register(
|
||||
libData.RoleGrantDefaultPermissionsRequest, this.handleRoleGrantDefaultPermissionsRequest.bind(this)
|
||||
);
|
||||
this.register(libData.RoleDeleteRequest, this.handleRoleDeleteRequest.bind(this));
|
||||
this.register(libData.UserGetRequest, this.handleUserGetRequest.bind(this));
|
||||
this.register(libData.UserListRequest, this.handleUserListRequest.bind(this));
|
||||
this.register(libData.UserSetSubscriptionsRequest, this.handleUserSetSubscriptionsRequest.bind(this));
|
||||
this.register(libData.UserCreateRequest, this.handleUserCreateRequest.bind(this));
|
||||
this.register(libData.UserRevokeTokenRequest, this.handleUserRevokeTokenRequest.bind(this));
|
||||
this.register(libData.UserUpdateRolesRequest, this.handleUserUpdateRolesRequest.bind(this));
|
||||
this.register(libData.UserSetAdminRequest, this.handleUserSetAdminRequest.bind(this));
|
||||
this.register(libData.UserSetBannedRequest, this.handleUserSetBannedRequest.bind(this));
|
||||
this.register(libData.UserSetWhitelistedRequest, this.handleUserSetWhitelistedRequest.bind(this));
|
||||
this.register(libData.UserDeleteRequest, this.handleUserDeleteRequest.bind(this));
|
||||
this.register(libData.DebugDumpWsRequest, this.handleDebugDumpWsRequest.bind(this));
|
||||
}
|
||||
|
||||
async getControllerConfigRequestHandler() {
|
||||
return { serialized_config: this._controller.config.serialize("control") };
|
||||
async handleControllerConfigGetRequest() {
|
||||
return new libData.RawConfig(this._controller.config.serialize("control"));
|
||||
}
|
||||
|
||||
async setControllerConfigFieldRequestHandler(message) {
|
||||
this._controller.config.set(message.data.field, message.data.value, "control");
|
||||
async handleControllerConfigSetFieldRequest(request) {
|
||||
this._controller.config.set(request.field, request.value, "control");
|
||||
}
|
||||
|
||||
async setControllerConfigPropRequestHandler(message) {
|
||||
let { field, prop, value } = message.data;
|
||||
async handleControllerConfigSetPropRequest(request) {
|
||||
let { field, prop, value } = request;
|
||||
this._controller.config.setProp(field, prop, value, "control");
|
||||
}
|
||||
|
||||
async listHostsRequestHandler(message) {
|
||||
async handleHostListRequest() {
|
||||
let list = [];
|
||||
for (let host of this._controller.hosts.values()) {
|
||||
list.push({
|
||||
agent: host.agent,
|
||||
version: host.version,
|
||||
id: host.id,
|
||||
name: host.name,
|
||||
public_address: host.public_address || null,
|
||||
connected: this._controller.wsServer.hostConnections.has(host.id),
|
||||
});
|
||||
list.push(new libData.HostDetails(
|
||||
host.agent,
|
||||
host.version,
|
||||
host.name,
|
||||
host.id,
|
||||
this._controller.wsServer.hostConnections.has(host.id),
|
||||
host.public_address,
|
||||
));
|
||||
}
|
||||
return { list };
|
||||
return list;
|
||||
}
|
||||
|
||||
async setHostSubscriptionsRequestHandler(message) {
|
||||
this.hostSubscriptions = message.data;
|
||||
async handleHostSetSubscriptionsRequest(request) {
|
||||
this.hostSubscriptions = { ...request };
|
||||
}
|
||||
|
||||
hostUpdated(host, update) {
|
||||
if (
|
||||
this.hostSubscriptions.all
|
||||
|| this.hostSubscriptions.host_ids.includes(host.id)
|
||||
|| this.hostSubscriptions.hostIds.includes(host.id)
|
||||
) {
|
||||
libLink.messages.hostUpdate.send(this, update);
|
||||
this.send(new libData.HostUpdateEvent(update));
|
||||
}
|
||||
}
|
||||
|
||||
async generateHostTokenRequestHandler(message) {
|
||||
let hostId = message.data.host_id;
|
||||
async handleHostGenerateTokenRequest(message) {
|
||||
let hostId = message.host_id;
|
||||
if (hostId === null) {
|
||||
hostId = Math.random() * 2**31 | 0;
|
||||
}
|
||||
return { token: this._controller.generateHostToken(hostId) };
|
||||
return this._controller.generateHostToken(hostId);
|
||||
}
|
||||
|
||||
async createHostConfigRequestHandler(message) {
|
||||
async handleHostConfigCreateRequest(request) {
|
||||
let hostConfig = new libConfig.HostConfig("control");
|
||||
await hostConfig.init();
|
||||
|
||||
hostConfig.set("host.controller_url", this._controller.getControllerUrl());
|
||||
if (message.data.id !== null) {
|
||||
hostConfig.set("host.id", message.data.id);
|
||||
if (request.id !== null) {
|
||||
hostConfig.set("host.id", request.id);
|
||||
}
|
||||
if (message.data.name !== null) {
|
||||
hostConfig.set("host.name", message.data.name);
|
||||
if (request.name !== null) {
|
||||
hostConfig.set("host.name", request.name);
|
||||
}
|
||||
if (message.data.generate_token) {
|
||||
if (request.generateToken) {
|
||||
this.user.checkPermission("core.host.generate_token");
|
||||
hostConfig.set("host.controller_token", this._controller.generateHostToken(hostConfig.get("host.id")));
|
||||
}
|
||||
return { serialized_config: hostConfig.serialize() };
|
||||
return new libData.RawConfig(hostConfig.serialize());
|
||||
}
|
||||
|
||||
async getInstanceRequestHandler(message) {
|
||||
let id = message.data.id;
|
||||
let instance = this._controller.getRequestInstance(id);
|
||||
async handleInstanceDetailsGetRequest(request) {
|
||||
let instance = this._controller.getRequestInstance(request.instanceId);
|
||||
|
||||
return {
|
||||
id,
|
||||
name: instance.config.get("instance.name"),
|
||||
assigned_host: instance.config.get("instance.assigned_host"),
|
||||
game_port: instance.game_port || null,
|
||||
status: instance.status,
|
||||
};
|
||||
return new libData.InstanceDetails(
|
||||
instance.config.get("instance.name"),
|
||||
instance.id,
|
||||
instance.config.get("instance.assigned_host"),
|
||||
instance.game_port || null,
|
||||
instance.status,
|
||||
);
|
||||
}
|
||||
|
||||
async listInstancesRequestHandler(message) {
|
||||
async handleInstanceDetailsListRequest() {
|
||||
let list = [];
|
||||
for (let instance of this._controller.instances.values()) {
|
||||
list.push({
|
||||
id: instance.id,
|
||||
name: instance.config.get("instance.name"),
|
||||
assigned_host: instance.config.get("instance.assigned_host"),
|
||||
game_port: instance.game_port || null,
|
||||
status: instance.status,
|
||||
});
|
||||
list.push(new libData.InstanceDetails(
|
||||
instance.config.get("instance.name"),
|
||||
instance.id,
|
||||
instance.config.get("instance.assigned_host"),
|
||||
instance.game_port || null,
|
||||
instance.status,
|
||||
));
|
||||
}
|
||||
return { list };
|
||||
return list;
|
||||
}
|
||||
|
||||
async setInstanceSubscriptionsRequestHandler(message) {
|
||||
this.instanceSubscriptions = message.data;
|
||||
async handleInstanceDetailsSetSubscriptionsRequest(request) {
|
||||
this.instanceSubscriptions = { ...request };
|
||||
}
|
||||
|
||||
instanceUpdated(instance) {
|
||||
if (
|
||||
this.instanceSubscriptions.all
|
||||
|| this.instanceSubscriptions.instance_ids.includes(instance.id)
|
||||
|| this.instanceSubscriptions.instanceIds.includes(instance.id)
|
||||
) {
|
||||
libLink.messages.instanceUpdate.send(this, {
|
||||
id: instance.id,
|
||||
name: instance.config.get("instance.name"),
|
||||
assigned_host: instance.config.get("instance.assigned_host"),
|
||||
game_port: instance.game_port || null,
|
||||
status: instance.status,
|
||||
});
|
||||
this.send(new libData.InstanceDetailsUpdateEvent(
|
||||
new libData.InstanceDetails(
|
||||
instance.config.get("instance.name"),
|
||||
instance.id,
|
||||
instance.config.get("instance.assigned_host"),
|
||||
instance.game_port || null,
|
||||
instance.status,
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// XXX should probably add a hook for host reuqests?
|
||||
async createInstanceRequestHandler(message) {
|
||||
async handleInstanceCreateRequest(request) {
|
||||
let instanceConfig = new libConfig.InstanceConfig("controller");
|
||||
await instanceConfig.load(message.data.serialized_config);
|
||||
await instanceConfig.load(request.config);
|
||||
await this._controller.instanceCreate(instanceConfig);
|
||||
}
|
||||
|
||||
async deleteInstanceRequestHandler(message, request) {
|
||||
await this._controller.instanceDelete(message.data.instance_id);
|
||||
async handleInstanceDeleteRequest(request) {
|
||||
await this._controller.instanceDelete(request.instanceId);
|
||||
}
|
||||
|
||||
async getInstanceConfigRequestHandler(message) {
|
||||
let instance = this._controller.getRequestInstance(message.data.instance_id);
|
||||
return {
|
||||
serialized_config: instance.config.serialize("control"),
|
||||
};
|
||||
async handleInstanceConfigGetRequest(request) {
|
||||
let instance = this._controller.getRequestInstance(request.instanceId);
|
||||
return new libData.InstanceConfigGetRequest.Response(instance.config.serialize("control"));
|
||||
}
|
||||
|
||||
async setInstanceConfigFieldRequestHandler(message) {
|
||||
let instance = this._controller.getRequestInstance(message.data.instance_id);
|
||||
if (message.data.field === "instance.assigned_host") {
|
||||
async handleInstanceConfigSetFieldRequest(request) {
|
||||
let instance = this._controller.getRequestInstance(request.instanceId);
|
||||
if (request.field === "instance.assigned_host") {
|
||||
throw new libErrors.RequestError("instance.assigned_host must be set through the assign-host interface");
|
||||
}
|
||||
|
||||
if (message.data.field === "instance.id") {
|
||||
if (request.field === "instance.id") {
|
||||
// XXX is this worth implementing? It's race condition galore.
|
||||
throw new libErrors.RequestError("Setting instance.id is not supported");
|
||||
}
|
||||
|
||||
instance.config.set(message.data.field, message.data.value, "control");
|
||||
instance.config.set(request.field, request.value, "control");
|
||||
await this._controller.instanceConfigUpdated(instance);
|
||||
}
|
||||
|
||||
async setInstanceConfigPropRequestHandler(message) {
|
||||
let instance = this._controller.getRequestInstance(message.data.instance_id);
|
||||
let { field, prop, value } = message.data;
|
||||
async handleInstanceConfigSetPropRequest(request) {
|
||||
let instance = this._controller.getRequestInstance(request.instanceId);
|
||||
let { field, prop, value } = request;
|
||||
instance.config.setProp(field, prop, value, "control");
|
||||
await this._controller.instanceConfigUpdated(instance);
|
||||
}
|
||||
|
||||
async assignInstanceCommandRequestHandler(message, request) {
|
||||
let { host_id, instance_id } = message.data;
|
||||
await this._controller.instanceAssign(instance_id, host_id);
|
||||
async handleInstanceAssignRequest(request) {
|
||||
await this._controller.instanceAssign(request.instanceId, request.hostId);
|
||||
}
|
||||
|
||||
async setSaveListSubscriptionsRequestHandler(message) {
|
||||
this.saveListSubscriptions = message.data;
|
||||
async handleInstanceSetSaveListSubscriptionsRequest(request) {
|
||||
this.saveListSubscriptions = { ...request };
|
||||
}
|
||||
|
||||
saveListUpdate(data) {
|
||||
saveListUpdate(instanceId, saves) {
|
||||
if (
|
||||
this.saveListSubscriptions.all
|
||||
|| this.saveListSubscriptions.instance_ids.includes(data.instance_id)
|
||||
|| this.saveListSubscriptions.instanceIds.includes(instanceId)
|
||||
) {
|
||||
libLink.messages.saveListUpdate.send(this, data);
|
||||
this.send(new libData.InstanceSaveListUpdateEvent(instanceId, saves));
|
||||
}
|
||||
}
|
||||
|
||||
async downloadSaveRequestHandler(message) {
|
||||
let { instance_id, save } = message.data;
|
||||
async handleInstanceDownloadSaveRequest(request) {
|
||||
let { instanceId, name } = request;
|
||||
let stream = await routes.createProxyStream(this._controller.app);
|
||||
stream.filename = save;
|
||||
stream.filename = name;
|
||||
|
||||
let ready = new Promise((resolve, reject) => {
|
||||
stream.events.on("source", resolve);
|
||||
@@ -287,29 +351,29 @@ class ControlConnection extends BaseConnection {
|
||||
});
|
||||
ready.catch(() => {});
|
||||
|
||||
await this._controller.forwardRequestToInstance(libLink.messages.pushSave, {
|
||||
instance_id,
|
||||
stream_id: stream.id,
|
||||
save,
|
||||
});
|
||||
await this._controller.sendToHostByInstanceId(new libData.InstancePushSaveRequest(
|
||||
instanceId,
|
||||
stream.id,
|
||||
name,
|
||||
));
|
||||
|
||||
await ready;
|
||||
return { stream_id: stream.id };
|
||||
return stream.id;
|
||||
}
|
||||
|
||||
async transferSaveRequestHandler(message, request) {
|
||||
let { source_save, target_save, copy, instance_id, target_instance_id } = message.data;
|
||||
if (copy) {
|
||||
async handleInstanceTransferSaveRequest(request) {
|
||||
// XXX if control sends the request directly to a host these checks are bypassed
|
||||
if (request.copy) {
|
||||
this.user.checkPermission("core.instance.save.copy");
|
||||
} else if (source_save !== target_save) {
|
||||
} else if (request.sourceName !== request.targetName) {
|
||||
this.user.checkPermission("core.instance.save.rename");
|
||||
}
|
||||
|
||||
if (instance_id === target_instance_id) {
|
||||
if (request.sourceInstanceId === request.targetInstanceId) {
|
||||
throw new libErrors.RequestError("Source and target instance may not be the same");
|
||||
}
|
||||
let sourceInstance = this._controller.getRequestInstance(instance_id);
|
||||
let targetInstance = this._controller.getRequestInstance(target_instance_id);
|
||||
let sourceInstance = this._controller.getRequestInstance(request.sourceInstanceId);
|
||||
let targetInstance = this._controller.getRequestInstance(request.targetInstanceId);
|
||||
let sourceHostId = sourceInstance.config.get("instance.assigned_host");
|
||||
let targetHostId = targetInstance.config.get("instance.assigned_host");
|
||||
if (sourceHostId === null) {
|
||||
@@ -321,7 +385,7 @@ class ControlConnection extends BaseConnection {
|
||||
|
||||
// Let host handle request if source and target is on the same host.
|
||||
if (sourceHostId === targetHostId) {
|
||||
return await this.forwardRequestToInstance(message, request);
|
||||
return await this._controller.sendTo(request, { hostId: sourceHostId });
|
||||
}
|
||||
|
||||
// Check connectivity
|
||||
@@ -350,42 +414,40 @@ class ControlConnection extends BaseConnection {
|
||||
// Establish push from source host to stream, this is done first to
|
||||
// ensure the file size is known prior to the target host pull.
|
||||
await Promise.all([
|
||||
this._controller.forwardRequestToInstance(libLink.messages.pushSave, {
|
||||
instance_id,
|
||||
stream_id: stream.id,
|
||||
save: source_save,
|
||||
}),
|
||||
this._controller.sendTo(
|
||||
new libData.InstancePushSaveRequest(request.sourceInstanceId, stream.id, request.sourceName),
|
||||
{ hostId: sourceHostId }
|
||||
),
|
||||
events.once(stream.events, "source"),
|
||||
]);
|
||||
|
||||
// Establish pull from target host to stream and wait for completion.
|
||||
let result = await this._controller.forwardRequestToInstance(libLink.messages.pullSave, {
|
||||
instance_id: target_instance_id,
|
||||
stream_id: stream.id,
|
||||
filename: target_save,
|
||||
});
|
||||
let storedName = await this._controller.sendTo(
|
||||
new libData.InstancePullSaveRequest(request.targetInstanceId, stream.id, request.targetName),
|
||||
{ hostId: targetHostId },
|
||||
);
|
||||
|
||||
// Delete source save if this is not a copy
|
||||
if (!copy) {
|
||||
await this._controller.forwardRequestToInstance(libLink.messages.deleteSave, {
|
||||
instance_id,
|
||||
save: source_save,
|
||||
});
|
||||
if (!request.copy) {
|
||||
await this._controller.sendTo(
|
||||
new libData.InstanceDeleteSaveRequest(request.sourceInstanceId, request.sourceName),
|
||||
{ hostId: sourceHostId }
|
||||
);
|
||||
}
|
||||
|
||||
return { save: result.save };
|
||||
return storedName;
|
||||
}
|
||||
|
||||
async listModPacksRequestHandler(message) {
|
||||
return { list: [...this._controller.modPacks.values()].map(pack => pack.toJSON()) };
|
||||
async handleModPackListRequest() {
|
||||
return [...this._controller.modPacks.values()];
|
||||
}
|
||||
|
||||
async setModPackSubscriptionsRequestHandler(message) {
|
||||
this.modPackSubscriptions = message.data;
|
||||
async handleModPackSetSubscriptionsRequest(request) {
|
||||
this.modPackSubscriptions = { ...request };
|
||||
}
|
||||
|
||||
async createModPackRequestHandler(message) {
|
||||
let modPack = libData.ModPack.fromJSON(message.data.mod_pack);
|
||||
async handleModPackCreateRequest(request) {
|
||||
let modPack = request.modPack;
|
||||
if (this._controller.modPacks.has(modPack.id)) {
|
||||
throw new libErrors.RequestError(`Mod pack with ID ${modPack.id} already exist`);
|
||||
}
|
||||
@@ -393,8 +455,8 @@ class ControlConnection extends BaseConnection {
|
||||
this._controller.modPackUpdated(modPack);
|
||||
}
|
||||
|
||||
async updateModPackRequestHandler(message) {
|
||||
let modPack = libData.ModPack.fromJSON(message.data.mod_pack);
|
||||
async handleModPackUpdateRequest(request) {
|
||||
let modPack = request.modPack;
|
||||
if (!this._controller.modPacks.has(modPack.id)) {
|
||||
throw new libErrors.RequestError(`Mod pack with ID ${modPack.id} does not exist`);
|
||||
}
|
||||
@@ -402,8 +464,8 @@ class ControlConnection extends BaseConnection {
|
||||
this._controller.modPackUpdated(modPack);
|
||||
}
|
||||
|
||||
async deleteModPackRequestHandler(message) {
|
||||
let { id } = message.data;
|
||||
async handleModPackDeleteRequest(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`);
|
||||
@@ -416,26 +478,24 @@ class ControlConnection extends BaseConnection {
|
||||
modPackUpdated(modPack) {
|
||||
if (
|
||||
this.modPackSubscriptions.all
|
||||
|| this.modPackSubscriptions.mod_pack_ids.includes(modPack.id)
|
||||
|| this.modPackSubscriptions.modPackIds.includes(modPack.id)
|
||||
) {
|
||||
libLink.messages.modPackUpdate.send(this, { mod_pack: modPack.toJSON() });
|
||||
this.send(new libData.ModPackUpdateEvent(modPack));
|
||||
}
|
||||
}
|
||||
|
||||
async getModRequestHandler(message) {
|
||||
let { name, version } = message.data;
|
||||
async handleModGetRequest(request) {
|
||||
let { name, version } = request;
|
||||
let filename = `${name}_${version}.zip`;
|
||||
let mod = this._controller.mods.get(filename);
|
||||
if (!mod) {
|
||||
throw new libErrors.RequestError(`Mod ${filename} does not exist`);
|
||||
}
|
||||
return {
|
||||
mod: mod.toJSON(),
|
||||
};
|
||||
return mod;
|
||||
}
|
||||
|
||||
async listModsRequestHandler(message) {
|
||||
return { list: [...this._controller.mods.values()].map(mod => mod.toJSON()) };
|
||||
async handleModListRequest() {
|
||||
return [...this._controller.mods.values()];
|
||||
}
|
||||
|
||||
static termsMatchesMod(terms, mod) {
|
||||
@@ -456,8 +516,8 @@ class ControlConnection extends BaseConnection {
|
||||
return true;
|
||||
}
|
||||
|
||||
async searchModsRequestHandler(message) {
|
||||
let query = libHelpers.parseSearchString(message.data.query, {
|
||||
async handleModSearchRequest(request) {
|
||||
let query = libHelpers.parseSearchString(request.query, {
|
||||
name: "word",
|
||||
// version
|
||||
title: "word",
|
||||
@@ -471,7 +531,7 @@ class ControlConnection extends BaseConnection {
|
||||
// size
|
||||
sha1: "word",
|
||||
});
|
||||
let factorioVersion = message.data.factorio_version;
|
||||
let factorioVersion = request.factorioVersion;
|
||||
|
||||
let results = new Map();
|
||||
for (let mod of this._controller.mods.values()) {
|
||||
@@ -497,7 +557,7 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
let resultList = [...results.values()];
|
||||
|
||||
const sort = message.data.sort;
|
||||
const sort = request.sort;
|
||||
if (sort) {
|
||||
const sorters = {
|
||||
name: (a, b) => strcmp(a.versions[0].name, b.versions[0].name),
|
||||
@@ -508,30 +568,30 @@ class ControlConnection extends BaseConnection {
|
||||
throw new libErrors.RequestError(`Invalid value for sort: ${sort}`);
|
||||
}
|
||||
resultList.sort(sorters[sort]);
|
||||
let order = message.data.sort_order;
|
||||
let order = request.sortOrder;
|
||||
if (order === "desc") {
|
||||
resultList.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
const page = message.data.page;
|
||||
const pageSize = message.data.page_size || 10;
|
||||
const page = request.page;
|
||||
const pageSize = request.pageSize || 10;
|
||||
resultList = resultList.slice((page - 1) * pageSize, page * pageSize);
|
||||
|
||||
return {
|
||||
query_issues: query.issues,
|
||||
page_count: Math.ceil(results.size / pageSize),
|
||||
result_count: results.size,
|
||||
queryIssues: query.issues,
|
||||
pageCount: Math.ceil(results.size / pageSize),
|
||||
resultCount: results.size,
|
||||
results: resultList,
|
||||
};
|
||||
}
|
||||
|
||||
async setModSubscriptionsRequestHandler(message) {
|
||||
this.modSubscriptions = message.data;
|
||||
async handleModSetSubscriptionsRequest(request) {
|
||||
this.modSubscriptions = { ...request };
|
||||
}
|
||||
|
||||
async downloadModRequestHandler(message) {
|
||||
let { name, version } = message.data;
|
||||
async handleModDownloadRequest(request) {
|
||||
let { name, version } = request;
|
||||
let filename = `${name}_${version}.zip`;
|
||||
let mod = this._controller.mods.get(filename);
|
||||
if (!mod) {
|
||||
@@ -545,30 +605,30 @@ class ControlConnection extends BaseConnection {
|
||||
stream.mime = "application/zip";
|
||||
stream.size = mod.size;
|
||||
|
||||
return { stream_id: stream.id };
|
||||
return stream.id;
|
||||
}
|
||||
|
||||
async deleteModRequestHandler(message) {
|
||||
await this._controller.deleteMod(message.data.name, message.data.version);
|
||||
async handleModDeleteRequest(request) {
|
||||
await this._controller.deleteMod(request.name, request.version);
|
||||
}
|
||||
|
||||
async setLogSubscriptionsRequestHandler(message) {
|
||||
this.logSubscriptions = message.data;
|
||||
async handleLogSetSubscriptionsRequest(request) {
|
||||
this.logSubscriptions = { ...request };
|
||||
this.updateLogSubscriptions();
|
||||
}
|
||||
|
||||
modUpdated(mod) {
|
||||
if (
|
||||
this.modSubscriptions.all
|
||||
|| this.modSubscriptions.mod_names.includes(mod.name)
|
||||
|| this.modSubscriptions.modNames.includes(mod.name)
|
||||
) {
|
||||
libLink.messages.modUpdate.send(this, { mod: mod.toJSON() });
|
||||
this.send(new libData.ModUpdateEvent(mod));
|
||||
}
|
||||
}
|
||||
|
||||
updateLogSubscriptions() {
|
||||
let { all, controller, host_ids, instance_ids } = this.logSubscriptions;
|
||||
if (all || controller || host_ids.length || instance_ids.length) {
|
||||
let { all, controller, hostIds, instanceIds } = this.logSubscriptions;
|
||||
if (all || controller || hostIds.length || instanceIds.length) {
|
||||
if (!this.logTransport) {
|
||||
this.logTransport = new libLoggingUtils.LinkTransport({ link: this });
|
||||
this._controller.clusterLogger.add(this.logTransport);
|
||||
@@ -581,57 +641,57 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
}
|
||||
|
||||
async queryLogRequestHandler(message) {
|
||||
async handleLogQueryRequest(request) {
|
||||
let observeDuration = queryLogTime.startTimer();
|
||||
let { all, controller, host_ids, instance_ids } = message.data;
|
||||
let { all, controller, hostIds, instanceIds } = request;
|
||||
|
||||
let log;
|
||||
if (!all && controller && !host_ids.length && !instance_ids.length) {
|
||||
log = await this._controller.queryControllerLog(message.data);
|
||||
if (!all && controller && !hostIds.length && !instanceIds.length) {
|
||||
log = await this._controller.queryControllerLog(request);
|
||||
} else {
|
||||
log = await this._controller.queryClusterLog(message.data);
|
||||
log = await this._controller.queryClusterLog(request);
|
||||
}
|
||||
|
||||
observeDuration();
|
||||
return { log };
|
||||
}
|
||||
|
||||
async listPermissionsRequestHandler(message) {
|
||||
async handlePermissionListRequest() {
|
||||
let list = [];
|
||||
for (let permission of libUsers.permissions.values()) {
|
||||
list.push({
|
||||
name: permission.name,
|
||||
title: permission.title,
|
||||
description: permission.description,
|
||||
});
|
||||
list.push(new libData.RawPermission(
|
||||
permission.name,
|
||||
permission.title,
|
||||
permission.description,
|
||||
));
|
||||
}
|
||||
return { list };
|
||||
return list;
|
||||
}
|
||||
|
||||
async listRolesRequestHandler(message) {
|
||||
async handleRoleListRequest() {
|
||||
let list = [];
|
||||
for (let role of this._controller.userManager.roles.values()) {
|
||||
list.push({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
permissions: [...role.permissions],
|
||||
});
|
||||
list.push(new libData.RawRole(
|
||||
role.id,
|
||||
role.name,
|
||||
role.description,
|
||||
[...role.permissions],
|
||||
));
|
||||
}
|
||||
return { list };
|
||||
return list;
|
||||
}
|
||||
|
||||
async createRoleRequestHandler(message) {
|
||||
async handleRoleCreateRequest(request) {
|
||||
let lastId = Math.max.apply(null, [...this._controller.userManager.roles.keys()]);
|
||||
|
||||
// Start at 5 to leave space for future default roles
|
||||
let id = Math.max(5, lastId+1);
|
||||
this._controller.userManager.roles.set(id, new libUsers.Role({ id, ...message.data }));
|
||||
return { id };
|
||||
this._controller.userManager.roles.set(id, new libUsers.Role({ id, ...request }));
|
||||
return id;
|
||||
}
|
||||
|
||||
async updateRoleRequestHandler(message) {
|
||||
let { id, name, description, permissions } = message.data;
|
||||
async handleRoleUpdateRequest(request) {
|
||||
let { id, name, description, permissions } = request;
|
||||
let role = this._controller.userManager.roles.get(id);
|
||||
if (!role) {
|
||||
throw new libErrors.RequestError(`Role with ID ${id} does not exist`);
|
||||
@@ -643,18 +703,18 @@ class ControlConnection extends BaseConnection {
|
||||
this._controller.rolePermissionsUpdated(role);
|
||||
}
|
||||
|
||||
async grantDefaultRolePermissionsRequestHandler(message) {
|
||||
let role = this._controller.userManager.roles.get(message.data.id);
|
||||
async handleRoleGrantDefaultPermissionsRequest(request) {
|
||||
let role = this._controller.userManager.roles.get(request.id);
|
||||
if (!role) {
|
||||
throw new libErrors.RequestError(`Role with ID ${message.data.id} does not exist`);
|
||||
throw new libErrors.RequestError(`Role with ID ${request.id} does not exist`);
|
||||
}
|
||||
|
||||
role.grantDefaultPermissions();
|
||||
this._controller.rolePermissionsUpdated(role);
|
||||
}
|
||||
|
||||
async deleteRoleRequestHandler(message) {
|
||||
let id = message.data.id;
|
||||
async handleRoleDeleteRequest(request) {
|
||||
let id = request.id;
|
||||
let role = this._controller.userManager.roles.get(id);
|
||||
if (!role) {
|
||||
throw new libErrors.RequestError(`Role with ID ${id} does not exist`);
|
||||
@@ -667,44 +727,48 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
}
|
||||
|
||||
async getUserRequestHandler(message) {
|
||||
let name = message.data.name;
|
||||
async handleUserGetRequest(request) {
|
||||
let name = request.name;
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
throw new libErrors.RequestError(`User ${name} does not exist`);
|
||||
}
|
||||
|
||||
return {
|
||||
name: user.name,
|
||||
roles: [...user.roles].map(role => role.id),
|
||||
is_admin: user.isAdmin,
|
||||
is_banned: user.isBanned,
|
||||
is_whitelisted: user.isWhitelisted,
|
||||
ban_reason: user.banReason,
|
||||
instances: [...user.instances],
|
||||
player_stats: user.playerStats,
|
||||
instance_stats: [...user.instanceStats],
|
||||
};
|
||||
return new libData.RawUser(
|
||||
user.name,
|
||||
[...user.roles].map(role => role.id),
|
||||
[...user.instances],
|
||||
user.isAdmin,
|
||||
user.isBanned,
|
||||
user.isWhitelisted,
|
||||
user.banReason,
|
||||
undefined,
|
||||
user.playerStats,
|
||||
[...user.instanceStats],
|
||||
);
|
||||
}
|
||||
|
||||
async listUsersRequestHandler(message) {
|
||||
async handleUserListRequest() {
|
||||
let list = [];
|
||||
for (let user of this._controller.userManager.users.values()) {
|
||||
list.push({
|
||||
name: user.name,
|
||||
roles: [...user.roles].map(role => role.id),
|
||||
is_admin: user.isAdmin,
|
||||
is_banned: user.isBanned,
|
||||
is_whitelisted: user.isWhitelisted,
|
||||
instances: [...user.instances],
|
||||
player_stats: user.playerStats,
|
||||
});
|
||||
list.push(new libData.RawUser(
|
||||
user.name,
|
||||
[...user.roles].map(role => role.id),
|
||||
[...user.instances],
|
||||
user.isAdmin,
|
||||
user.isBanned,
|
||||
user.isWhitelisted,
|
||||
undefined,
|
||||
undefined,
|
||||
user.playerStats,
|
||||
undefined,
|
||||
));
|
||||
}
|
||||
return { list };
|
||||
return list;
|
||||
}
|
||||
|
||||
async setUserSubscriptionsRequestHandler(message) {
|
||||
this.userSubscriptions = message.data;
|
||||
async handleUserSetSubscriptionsRequest(request) {
|
||||
this.userSubscriptions = { ...request };
|
||||
}
|
||||
|
||||
userUpdated(user) {
|
||||
@@ -712,37 +776,39 @@ class ControlConnection extends BaseConnection {
|
||||
this.userSubscriptions.all
|
||||
|| this.userSubscriptions.names.includes(user.name)
|
||||
) {
|
||||
libLink.messages.userUpdate.send(this, {
|
||||
name: user.name,
|
||||
roles: [...user.roles].map(role => role.id),
|
||||
is_admin: user.isAdmin,
|
||||
is_banned: user.isBanned,
|
||||
is_whitelisted: user.isWhitelisted,
|
||||
ban_reason: user.banReason,
|
||||
instances: [...user.instances],
|
||||
player_stats: user.playerStats,
|
||||
instance_stats: [...user.instanceStats],
|
||||
is_deleted: user.isDeleted,
|
||||
});
|
||||
this.send(new libData.UserUpdateEvent(
|
||||
new libData.RawUser(
|
||||
user.name,
|
||||
[...user.roles].map(role => role.id),
|
||||
[...user.instances],
|
||||
user.isAdmin,
|
||||
user.isBanned,
|
||||
user.isWhitelisted,
|
||||
user.banReason,
|
||||
user.isDeleted,
|
||||
user.playerStats,
|
||||
[...user.instanceStats],
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
async createUserRequestHandler(message) {
|
||||
let user = this._controller.userManager.createUser(message.data.name);
|
||||
async handleUserCreateRequest(request) {
|
||||
let user = this._controller.userManager.createUser(request.name);
|
||||
this._controller.userUpdated(user);
|
||||
}
|
||||
|
||||
async revokeUserTokenRequestHandler(message) {
|
||||
let user = this._controller.userManager.users.get(message.data.name);
|
||||
async handleUserRevokeTokenRequest(request) {
|
||||
let user = this._controller.userManager.users.get(request.name);
|
||||
if (!user) {
|
||||
throw new libErrors.RequestError(`User '${message.data.name}' does not exist`);
|
||||
throw new libErrors.RequestError(`User '${request.name}' does not exist`);
|
||||
}
|
||||
if (user.name !== this.user.name) {
|
||||
this.user.checkPermission("core.user.revoke_other_token");
|
||||
}
|
||||
|
||||
user.invalidateToken();
|
||||
for (let controlConnection of this._controller.wsServer.controlConnections) {
|
||||
for (let controlConnection of this._controller.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.user.name === user.name) {
|
||||
controlConnection.connector.terminate();
|
||||
}
|
||||
@@ -750,14 +816,14 @@ class ControlConnection extends BaseConnection {
|
||||
this._controller.userUpdated(user);
|
||||
}
|
||||
|
||||
async updateUserRolesRequestHandler(message) {
|
||||
let user = this._controller.userManager.users.get(message.data.name);
|
||||
async handleUserUpdateRolesRequest(request) {
|
||||
let user = this._controller.userManager.users.get(request.name);
|
||||
if (!user) {
|
||||
throw new libErrors.RequestError(`User '${message.data.name}' does not exist`);
|
||||
throw new libErrors.RequestError(`User '${request.name}' does not exist`);
|
||||
}
|
||||
|
||||
let resolvedRoles = new Set();
|
||||
for (let roleId of message.data.roles) {
|
||||
for (let roleId of request.roles) {
|
||||
let role = this._controller.userManager.roles.get(roleId);
|
||||
if (!role) {
|
||||
throw new libErrors.RequestError(`Role with ID ${roleId} does not exist`);
|
||||
@@ -771,8 +837,8 @@ class ControlConnection extends BaseConnection {
|
||||
this._controller.userUpdated(user);
|
||||
}
|
||||
|
||||
async setUserAdminRequestHandler(message) {
|
||||
let { name, create, admin } = message.data;
|
||||
async handleUserSetAdminRequest(request) {
|
||||
let { name, create, admin } = request;
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
if (create) {
|
||||
@@ -785,11 +851,11 @@ class ControlConnection extends BaseConnection {
|
||||
|
||||
user.isAdmin = admin;
|
||||
this._controller.userUpdated(user);
|
||||
this.broadcastEventToHosts({ data: { name, admin }}, libLink.messages.adminlistUpdate);
|
||||
this._controller.sendTo(new libData.InstanceAdminlistUpdateEvent(name, admin), "allInstances");
|
||||
}
|
||||
|
||||
async setUserBannedRequestHandler(message) {
|
||||
let { name, create, banned, reason } = message.data;
|
||||
async handleUserSetBannedRequest(request) {
|
||||
let { name, create, banned, reason } = request;
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
if (create) {
|
||||
@@ -803,11 +869,11 @@ class ControlConnection extends BaseConnection {
|
||||
user.isBanned = banned;
|
||||
user.banReason = reason;
|
||||
this._controller.userUpdated(user);
|
||||
this.broadcastEventToHosts({ data: { name, banned, reason }}, libLink.messages.banlistUpdate);
|
||||
this._controller.sendTo(new libData.InstanceBanlistUpdateEvent(name, banned, reason), "allInstances");
|
||||
}
|
||||
|
||||
async setUserWhitelistedRequestHandler(message) {
|
||||
let { name, create, whitelisted } = message.data;
|
||||
async handleUserSetWhitelistedRequest(request) {
|
||||
let { name, create, whitelisted } = request;
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
if (create) {
|
||||
@@ -820,34 +886,35 @@ class ControlConnection extends BaseConnection {
|
||||
|
||||
user.isWhitelisted = whitelisted;
|
||||
this._controller.userUpdated(user);
|
||||
this.broadcastEventToHosts({ data: { name, whitelisted }}, libLink.messages.whitelistUpdate);
|
||||
this._controller.sendTo(new libData.InstanceWhitelistUpdateEvent(name, whitelisted), "allInstances");
|
||||
}
|
||||
|
||||
async deleteUserRequestHandler(message) {
|
||||
let user = this._controller.userManager.users.get(message.data.name);
|
||||
async handleUserDeleteRequest(request) {
|
||||
let name = request.name;
|
||||
let user = this._controller.userManager.users.get(name);
|
||||
if (!user) {
|
||||
throw new libErrors.RequestError(`User '${message.data.name}' does not exist`);
|
||||
throw new libErrors.RequestError(`User '${name}' does not exist`);
|
||||
}
|
||||
|
||||
user.isDeleted = true;
|
||||
this._controller.userManager.users.delete(message.data.name);
|
||||
this._controller.userManager.users.delete(name);
|
||||
this._controller.userUpdated(user);
|
||||
|
||||
if (user.is_admin) {
|
||||
this.broadcastEventToHosts({ data: { name, admin: false }}, libLink.messages.adminlistUpdate);
|
||||
this._controller.sendTo(new libData.InstanceAdminlistUpdateEvent(name, false), "allInstances");
|
||||
}
|
||||
if (user.is_whitelisted) {
|
||||
this.broadcastEventToHosts({ data: { name, whitelisted: false }}, libLink.messages.whitelistUpdate);
|
||||
this._controller.sendTo(new libData.InstanceWhitelistUpdateEvent(name, false), "allInstances");
|
||||
}
|
||||
if (user.is_banned) {
|
||||
this.broadcastEventToHosts({ data: { name, banned: false, reason: "" }}, libLink.messages.banlistUpdate);
|
||||
this._controller.sendTo(new libData.InstanceBanlistUpdateEvent(name, false, ""), "allInstances");
|
||||
}
|
||||
}
|
||||
|
||||
async debugDumpWsRequestHandler(message) {
|
||||
async handleDebugDumpWsRequest(request) {
|
||||
this.ws_dumper = data => {
|
||||
if (this.connector.connected) {
|
||||
libLink.messages.debugWsMessage.send(this, data);
|
||||
this.send(new libData.DebugWsMessageEvent(data.direction, data.content));
|
||||
}
|
||||
};
|
||||
this.connector._socket.clusterio_ignore_dump = true;
|
||||
|
||||
@@ -13,7 +13,6 @@ const libConfig = require("@clusterio/lib/config");
|
||||
const libData = require("@clusterio/lib/data");
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
const libFileOps = require("@clusterio/lib/file_ops");
|
||||
const libLink = require("@clusterio/lib/link");
|
||||
const libLoggingUtils = require("@clusterio/lib/logging_utils");
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
const libPluginLoader = require("@clusterio/lib/plugin_loader");
|
||||
@@ -114,6 +113,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;
|
||||
}
|
||||
|
||||
@@ -559,16 +563,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;
|
||||
}
|
||||
@@ -682,17 +686,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);
|
||||
@@ -709,8 +712,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(new libData.InstanceDeleteInternalRequest(instanceId), { hostId });
|
||||
}
|
||||
this.instances.delete(instanceId);
|
||||
|
||||
@@ -734,10 +738,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"))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -755,7 +758,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;
|
||||
}
|
||||
@@ -764,18 +767,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;
|
||||
}
|
||||
@@ -786,7 +789,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;
|
||||
}
|
||||
@@ -798,7 +801,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;
|
||||
}
|
||||
@@ -813,15 +816,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],
|
||||
})),
|
||||
});
|
||||
})))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -832,15 +835,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],
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -965,35 +970,184 @@ class Controller {
|
||||
};
|
||||
}
|
||||
|
||||
register(Class, handler) {
|
||||
if (Class.type === "request") {
|
||||
this.registerRequest(Class, handler);
|
||||
} else if (Class.type === "event") {
|
||||
this.registerEvent(Class, handler);
|
||||
} else {
|
||||
throw new Error(`Class ${Class.name} has unrecognized type ${Class.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
registerRequest(Request, handler) {
|
||||
if (this._registeredRequests.has(Request)) {
|
||||
throw new Error(`Request ${Request.name} is already registered`);
|
||||
}
|
||||
this._registeredRequests.set(Request, handler);
|
||||
}
|
||||
|
||||
fallbackRequest(Request, handler) {
|
||||
if (this._fallbackedRequests.has(Request)) {
|
||||
throw new Error(`Request ${Request.name} is already fallbacked`);
|
||||
}
|
||||
this._fallbackedRequests.set(Request, handler);
|
||||
}
|
||||
|
||||
registerEvent(Event, handler) {
|
||||
if (this._registeredEvents.has(Event)) {
|
||||
throw new Error(`Event ${Event.name} is already registered`);
|
||||
}
|
||||
this._registeredEvents.set(Event, handler);
|
||||
}
|
||||
|
||||
snoopEvent(Event, handler) {
|
||||
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 {*} requestOrEvent - The request or event to send.
|
||||
* @param {*|module:lib/data.Address} address - Where to send it.
|
||||
* @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(requestOrEvent, address) {
|
||||
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,123 @@
|
||||
"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) {
|
||||
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,6 @@
|
||||
"use strict";
|
||||
const libData = require("@clusterio/lib/data");
|
||||
const libConfig = require("@clusterio/lib/config");
|
||||
const libLink = require("@clusterio/lib/link");
|
||||
const { logger } = require("@clusterio/lib/logging");
|
||||
const libPlugin = require("@clusterio/lib/plugin");
|
||||
const PlayerStats = require("@clusterio/lib/PlayerStats");
|
||||
@@ -17,7 +17,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 +31,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 +57,12 @@ class HostConnection extends BaseConnection {
|
||||
libPlugin.invokeHook(this._controller.plugins, "onInstanceStatusChanged", instance, prev);
|
||||
}
|
||||
});
|
||||
|
||||
this.register(libData.InstanceStatusChangedEvent, this.handleInstanceStatusChangedEvent.bind(this));
|
||||
this.register(libData.InstancesUpdateRequest, this.handleInstancesUpdateRequest.bind(this));
|
||||
this.register(libData.InstanceSaveListUpdateEvent, this.handleInstanceSaveListUpdateEvent.bind(this));
|
||||
this.register(libData.LogMessageEvent, this.handleLogMessageEvent.bind(this));
|
||||
this.register(libData.InstancePlayerUpdateEvent, this.handleInstancePlayerUpdateEvent.bind(this));
|
||||
}
|
||||
|
||||
_checkPluginVersions() {
|
||||
@@ -83,6 +89,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 +103,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 +120,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 +162,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 +174,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 +199,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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+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}`);
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
+22
-39
@@ -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,11 @@ 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 +51,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,62 +69,46 @@ 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.register(libData.LogMessageEvent, this.handleLogMessageEvent.bind(this));
|
||||
this.register(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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +283,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 {
|
||||
|
||||
+187
-244
@@ -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(new libData.InstanceListSavesRequest(), { instanceId });
|
||||
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,10 @@ 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(
|
||||
new libData.InstanceCreateSaveRequest(args.name, seed, mapGenSettings, mapSettings), { instanceId }
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -575,11 +553,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 +565,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 +637,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 +657,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 +699,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 +710,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(new libData.InstanceExportDataRequest(), { instanceId });
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -757,10 +721,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(new libData.InstanceExtractPlayersRequest(), { instanceId });
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -774,11 +736,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(new libData.InstanceStartRequest(args.save), { instanceId });
|
||||
control.keepOpen = args.keepOpen;
|
||||
},
|
||||
}));
|
||||
@@ -797,15 +756,11 @@ 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(
|
||||
new libData.InstanceLoadScenarioRequest(args.scenario, seed, mapGenSettings, mapSettings), { instanceId }
|
||||
);
|
||||
control.keepOpen = args.keepOpen;
|
||||
},
|
||||
}));
|
||||
@@ -816,10 +771,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(new libData.InstanceStopRequest(args.save), { instanceId });
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -829,10 +782,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(new libData.InstanceKillRequest(args.save), { instanceId });
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -841,9 +792,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 +803,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(
|
||||
new libData.InstanceSendRconRequest(args.command),
|
||||
{ instanceId: await libCommand.resolveInstance(control, args.instance) },
|
||||
);
|
||||
|
||||
// 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 +820,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 +864,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));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1014,7 +966,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 +977,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 +987,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 = libData.ModPack.fromJSON(response.mod_pack);
|
||||
const modPack = await control.send(
|
||||
new libData.ModPackGetRequest(await libCommand.resolveModPack(control, args.modPack))
|
||||
);
|
||||
print(modPack.toModPackString());
|
||||
},
|
||||
}));
|
||||
@@ -1062,10 +1013,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 = libData.ModPack.fromJSON(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 +1053,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 +1063,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 +1074,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 +1100,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 +1110,7 @@ modCommands.add(new libCommand.Command({
|
||||
}
|
||||
}
|
||||
}
|
||||
print(asTable(response.list));
|
||||
print(asTable(mods));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1197,14 +1147,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 +1165,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));
|
||||
},
|
||||
}));
|
||||
@@ -1275,13 +1225,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 +1265,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 +1273,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 +1283,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 +1297,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 +1342,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 +1356,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 +1370,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 +1403,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 +1427,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 +1436,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 +1449,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 +1462,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 +1476,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 +1486,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 +1496,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 +1510,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 +1519,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 +1539,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 +1564,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 +1598,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;
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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,15 @@ class HostConnector extends libLink.WebSocketClientConnector {
|
||||
plugins[pluginInfo.name] = pluginInfo.version;
|
||||
}
|
||||
|
||||
this.sendHandshake("register_host", {
|
||||
token: this.hostConfig.get("host.controller_token"),
|
||||
agent: "Clusterio Host",
|
||||
this.sendHandshake(new libData.MessageRegisterHost(new libData.RegisterHostData(
|
||||
this.hostConfig.get("host.controller_token"),
|
||||
"Clusterio Host",
|
||||
version,
|
||||
id: this.hostConfig.get("host.id"),
|
||||
name: this.hostConfig.get("host.name"),
|
||||
public_address: this.hostConfig.get("host.public_address"),
|
||||
this.hostConfig.get("host.name"),
|
||||
this.hostConfig.get("host.id"),
|
||||
this.hostConfig.get("host.public_address"),
|
||||
plugins,
|
||||
});
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+291
-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,153 @@ 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);
|
||||
} 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) {
|
||||
try {
|
||||
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 +235,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 +300,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.register(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.register(libData.InstanceAssignInternalRequest, this.handleInstanceAssignInternalRequest.bind(this));
|
||||
this.register(libData.InstanceUnassignInternalRequest, this.handleInstanceUnassignInternalRequest.bind(this));
|
||||
this.register(libData.HostMetricsRequest, this.handleHostMetricsRequest.bind(this));
|
||||
this.fallbackRequest(libData.InstanceListSavesRequest, this.fallbackInstanceListSavesRequest.bind(this));
|
||||
this.register(libData.InstanceRenameSaveRequest, this.handleInstanceRenameSaveRequest.bind(this));
|
||||
this.register(libData.InstanceCopySaveRequest, this.handleInstanceCopySaveRequest.bind(this));
|
||||
this.register(libData.InstanceTransferSaveRequest, this.handleInstanceTransferSaveRequest.bind(this));
|
||||
this.register(libData.InstanceDeleteSaveRequest, this.handleInstanceDeleteSaveRequest.bind(this));
|
||||
this.register(libData.InstancePullSaveRequest, this.handleInstancePullSaveRequest.bind(this));
|
||||
this.register(libData.InstancePushSaveRequest, this.handleInstancePushSaveRequest.bind(this));
|
||||
this.register(libData.InstanceDeleteInternalRequest, this.handleInstanceDeleteInternalRequest.bind(this));
|
||||
}
|
||||
|
||||
async _createNewInstanceDir(name) {
|
||||
@@ -189,45 +348,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 +364,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 +387,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 +405,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 +414,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 +423,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 +448,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 +476,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 +518,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 +532,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 +558,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 +600,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 +689,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 +707,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 +773,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 +788,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 +800,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 +817,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 +834,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.register(libData.InstanceExtractPlayersRequest, this.handleInstanceExtractPlayersRequest.bind(this));
|
||||
this.register(libData.InstanceAdminlistUpdateEvent, this.handleInstanceAdminlistUpdateEvent.bind(this));
|
||||
this.register(libData.InstanceBanlistUpdateEvent, this.handleInstanceBanlistUpdateEvent.bind(this));
|
||||
this.register(libData.InstanceWhitelistUpdateEvent, this.handleInstanceWhitelistUpdateEvent.bind(this));
|
||||
this.register(libData.ControllerConnectionEvent, this.handleControllerConnectionEvent.bind(this));
|
||||
this.register(
|
||||
libData.PrepareControllerDisconnectRequest, this.handlePrepareControllerDisconnectRequest.bind(this)
|
||||
);
|
||||
this.register(libData.InstanceMetricsRequest, this.handleInstanceMetricsRequest.bind(this));
|
||||
this.register(libData.InstanceStartRequest, this.handleInstanceStartRequest.bind(this));
|
||||
this.register(libData.InstanceLoadScenarioRequest, this.handleInstanceLoadScenarioRequest.bind(this));
|
||||
this.register(libData.InstanceListSavesRequest, this.handleInstanceListSavesRequest.bind(this));
|
||||
this.register(libData.InstanceCreateSaveRequest, this.handleInstanceCreateSaveRequest.bind(this));
|
||||
this.register(libData.InstanceExportDataRequest, this.handleInstanceExportDataRequest.bind(this));
|
||||
this.register(libData.InstanceStopRequest, this.handleInstanceStopRequest.bind(this));
|
||||
this.register(libData.InstanceKillRequest, this.handleInstanceKillRequest.bind(this));
|
||||
this.register(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(new libData.InstancePlayerUpdateEvent("join", name, undefined, stats), "controller");
|
||||
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(new libData.InstancePlayerUpdateEvent("leave", name, reason, stats), "controller");
|
||||
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(new libData.InstancePlayerUpdateEvent("import", name, undefined, stats), "controller");
|
||||
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,24 @@ 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(new libData.InstanceSaveListUpdateEvent(
|
||||
this.id,
|
||||
await Instance.listSaves(this.path("saves"), this._loadedSave),
|
||||
), "controller");
|
||||
}
|
||||
|
||||
async _autosave(name) {
|
||||
@@ -415,11 +435,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(
|
||||
new libData.InstanceStatusChangedEvent(
|
||||
this.id,
|
||||
status,
|
||||
this.server && this.server.gamePort || this.config.get("factorio.game_port") || null,
|
||||
),
|
||||
"controller"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -467,7 +490,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 +550,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 +620,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(new libData.ModPackGetDefaultRequest(), "controller");
|
||||
} else {
|
||||
response = await libLink.messages.getModPack.send(this, { id: modPackId });
|
||||
modPack = await this.sendTo(new libData.ModPackGetRequest(modPackId), "controller");
|
||||
}
|
||||
this.activeModPack = libData.ModPack.fromJSON(response.mod_pack);
|
||||
this.activeModPack = modPack;
|
||||
|
||||
// TODO validate factorioVersion
|
||||
|
||||
@@ -698,21 +720,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 +929,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 +983,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 +1009,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 +1031,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 +1045,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 +1074,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 +1112,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.register(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(new libData.HostListRequest(), "controller");
|
||||
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(new libData.InstanceDetailsListRequest(), "controller");
|
||||
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(new libData.ModPackListRequest(), "controller");
|
||||
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(new libData.RoleListRequest(), "controller");
|
||||
|
||||
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;
|
||||
|
||||
@@ -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";
|
||||
static dst = "controller";
|
||||
|
||||
/** @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,880 @@
|
||||
"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 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 dest = ["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,688 @@
|
||||
"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" } },
|
||||
isAdmin: { type: "boolean" },
|
||||
isBanned: { type: "boolean" },
|
||||
isWhitelisted: { type: "boolean" },
|
||||
banReason: { type: "string" },
|
||||
instances: { type: "array", items: { type: "integer" } },
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* @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));
|
||||
|
||||
+227
-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,103 @@ 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 name = request.constructor.name;
|
||||
let hasData = Boolean(request.constructor.jsonSchema);
|
||||
const message = new libData.MessageRequest(seq, src, dst, name, hasData ? request : undefined);
|
||||
this.send(message);
|
||||
return seq;
|
||||
}
|
||||
|
||||
sendResponse(response, dst) {
|
||||
let seq = this._seq;
|
||||
this._seq += 1;
|
||||
const message = new libData.MessageResponse(seq, this.src, dst, response);
|
||||
this.send(message);
|
||||
return seq;
|
||||
}
|
||||
|
||||
sendResponseError(error, dst) {
|
||||
let seq = this._seq;
|
||||
this._seq += 1;
|
||||
const message = new libData.MessageResponseError(seq, this.src, dst, error);
|
||||
this.send(message);
|
||||
return seq;
|
||||
}
|
||||
|
||||
sendEvent(event, dst) {
|
||||
let seq = this._seq;
|
||||
this._seq += 1;
|
||||
let name = event.constructor.name;
|
||||
let hasData = Boolean(event.constructor.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 +124,7 @@ class WebSocketBaseConnector extends events.EventEmitter {
|
||||
}
|
||||
|
||||
_dropSendBufferSeq(seq) {
|
||||
if (seq === null) {
|
||||
if (seq === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,6 +139,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 +190,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 +219,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 +255,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 +315,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 +328,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 +466,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 +558,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 +575,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 +582,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 +592,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 +645,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 +658,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,11 @@
|
||||
*/
|
||||
"use strict";
|
||||
const link = require("./link");
|
||||
const messages = require("./messages");
|
||||
const connectors = require("./connectors");
|
||||
|
||||
module.exports = {
|
||||
...link,
|
||||
...messages,
|
||||
...connectors,
|
||||
Event: class Event {},
|
||||
Request: class Request {},
|
||||
};
|
||||
+364
-166
@@ -1,9 +1,10 @@
|
||||
// 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");
|
||||
|
||||
// Some definitions for the terminology used here:
|
||||
// link: Either side of a controller - client connection
|
||||
@@ -20,61 +21,65 @@ 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._registeredRequests = new Map();
|
||||
this._fallbackedRequests = new Map();
|
||||
this._registeredEvents = new Map();
|
||||
this._snoopedEvents = new Map();
|
||||
this._eventHandlers = new Map();
|
||||
this._pendingRequests = new Map();
|
||||
this._nextRequestId = 1;
|
||||
|
||||
this.register(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"));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
connector.on("invalidate", () => {
|
||||
for (let pending of this._pendingRequests.values()) {
|
||||
pending.reject(new libErrors.SessionLost("Session Lost"));
|
||||
}
|
||||
|
||||
this._waiters.clear();
|
||||
this._pendingRequests.clear();
|
||||
});
|
||||
|
||||
connector.on("close", () => {
|
||||
for (let waiterType of this._waiters.values()) {
|
||||
for (let waiter of waiterType) {
|
||||
waiter.reject(new libErrors.SessionLost("Session Closed"));
|
||||
}
|
||||
for (let pending of this._pendingRequests.values()) {
|
||||
pending.reject(new libErrors.SessionLost("Session Closed"));
|
||||
}
|
||||
|
||||
this._waiters.clear();
|
||||
this._pendingRequests.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,159 +89,163 @@ class 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}`);
|
||||
if (message.type === "event" && this._snoopedEvents.has(message.name)) {
|
||||
let event = this._snoopedEvents.get(message.name);
|
||||
event.handler(event.eventFromJSON(message.data), message.src, message.dst).catch(err => {
|
||||
logger.error(`Unexpected error snooping ${message.name}:\n${err.stack}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (!validator(message)) {
|
||||
throw new libErrors.InvalidMessage(`Validation failed for ${message.type}`, validator.errors);
|
||||
if (!message.dst.addressedTo(this.connector.src)) {
|
||||
this._routeMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
let hadHandlers = this._processHandler(message);
|
||||
let hadWaiters = this._processWaiters(message);
|
||||
if (message.type === "request") {
|
||||
let request = this._registeredRequests.get(message.name);
|
||||
if (!request) {
|
||||
this.connector.sendResponseError(
|
||||
new libData.ResponseError(`Unrecognized request ${message.name}`),
|
||||
message.src,
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._processRequest(message, request);
|
||||
|
||||
if (!hadWaiters && !hadHandlers) {
|
||||
// XXX console.error(`Unhandled message: ${JSON.stringify(message, null, 4)}`);
|
||||
throw new libErrors.InvalidMessage(`Unhandled message ${message.type}`);
|
||||
} else if (message.type === "response") {
|
||||
this._processResponse(message);
|
||||
|
||||
} else if (message.type === "responseError") {
|
||||
this._processResponseError(message);
|
||||
|
||||
} else if (message.type === "event") {
|
||||
this._processEvent(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the handler for this message if it exists
|
||||
*
|
||||
* @param {Object} message - Message to invoke handlers on.
|
||||
* @returns {Boolean} true if a handler was invoked.
|
||||
*/
|
||||
_processHandler(message) {
|
||||
let handler = this._handlers.get(message.type);
|
||||
if (handler) {
|
||||
handler(message);
|
||||
return true;
|
||||
_routeMessage(message) {
|
||||
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
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return false;
|
||||
|
||||
let fallback = this._fallbackedRequests.get(message.name);
|
||||
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, fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
_processRequest(message, request) {
|
||||
if (!request.handler) {
|
||||
this.connector.sendResponseError(
|
||||
new libData.ResponseError(`No handler registered for ${request.Request.name}`), message.src
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
let response;
|
||||
try {
|
||||
response = request.handler(request.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 (request.Response) {
|
||||
request.responseFromJSON(JSON.parse(JSON.stringify(result)));
|
||||
} else if (result !== undefined) {
|
||||
throw new Error(`Expected empty response from ${request.Request.name} handler`);
|
||||
}
|
||||
}
|
||||
this.connector.sendResponse(result, message.src);
|
||||
}
|
||||
|
||||
waiter.resolve(message);
|
||||
matched.push(index);
|
||||
}
|
||||
|
||||
matched.reverse();
|
||||
for (let index of matched) {
|
||||
waiters.splice(index, 1);
|
||||
}
|
||||
|
||||
return matched.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set handler for a message type
|
||||
*
|
||||
* Set function called upon receiving a message of the given type.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
setHandler(type, handler, validator) {
|
||||
if (this._handlers.has(type)) {
|
||||
throw new Error(`${type} already has a handler`);
|
||||
}
|
||||
|
||||
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);
|
||||
).catch(
|
||||
err => {
|
||||
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
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_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`
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
let event = this._registeredEvents.get(message.name);
|
||||
if (!event) {
|
||||
throw new libErrors.InvalidMessage(`Unrecognized event ${message.name}`);
|
||||
}
|
||||
|
||||
if (!event.handler) {
|
||||
return;
|
||||
}
|
||||
event.handler(event.eventFromJSON(message.data), message.src, message.dst).catch(err => {
|
||||
logger.error(`Unexpected error handling ${message.name}:\n${err.stack}`);
|
||||
});
|
||||
return waiter.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ping requests
|
||||
*/
|
||||
async pingRequestHandler() { }
|
||||
|
||||
/**
|
||||
* Prepare connection for disconnect
|
||||
*
|
||||
@@ -245,16 +254,205 @@ 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(requestOrEvent, this.connector.dst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request or event to the given address
|
||||
*
|
||||
* @param {*} requestOrEvent - Request or event to send
|
||||
* @param {*|module:lib/data.Address} destination - Where to send it
|
||||
* @returns {Promise<*>|undefined}
|
||||
* Promise that resolves to the response if a request was sent or
|
||||
* undefined if it was an event.
|
||||
*/
|
||||
sendTo(requestOrEvent, destination) {
|
||||
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 name = request.constructor.name;
|
||||
if (!this._registeredRequests.has(name)) {
|
||||
// XXX should this be allowed?
|
||||
this.registerRequest(request.constructor);
|
||||
}
|
||||
let registeredRequest = this._registeredRequests.get(name);
|
||||
if (this.validateSent) {
|
||||
registeredRequest.requestFromJSON(JSON.parse(JSON.stringify(request)));
|
||||
}
|
||||
|
||||
let pending = {
|
||||
request: registeredRequest,
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
sendEvent(event, dst) {
|
||||
if (this.validateSent) {
|
||||
let name = event.constructor.name;
|
||||
if (!this._registeredEvents.has(name)) {
|
||||
// XXX should this be allowed?
|
||||
this.registerEvent(event.constructor);
|
||||
}
|
||||
this._registeredEvents.get(name).eventFromJSON(JSON.parse(JSON.stringify(event)));
|
||||
}
|
||||
|
||||
this.connector.sendEvent(event, dst);
|
||||
}
|
||||
|
||||
register(Class, handler) {
|
||||
if (Class.type === "request") {
|
||||
this.registerRequest(Class, handler);
|
||||
} else if (Class.type === "event") {
|
||||
this.registerEvent(Class, handler);
|
||||
} else {
|
||||
throw new Error(`Class ${Class.name} has unrecognized type ${Class.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
_requestEntry(Request, handler) {
|
||||
let name = Request.name;
|
||||
let entry = {
|
||||
Request,
|
||||
handler,
|
||||
};
|
||||
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);
|
||||
entry.requestFromJSON = (json) => {
|
||||
if (!validate(json)) {
|
||||
throw new libErrors.InvalidMessage(`Request ${name} failed validation`, validate.errors);
|
||||
}
|
||||
return Request.fromJSON(json);
|
||||
};
|
||||
} else if (Request.jsonSchema) {
|
||||
throw new Error(`Request ${name} has static jsonSchema but is missing static fromJSON`);
|
||||
} else {
|
||||
entry.requestFromJSON = () => new Request();
|
||||
}
|
||||
|
||||
let Response = Request.Response;
|
||||
if (Response) {
|
||||
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);
|
||||
entry.responseFromJSON = (json) => {
|
||||
if (!validate(json)) {
|
||||
throw new libErrors.InvalidMessage(
|
||||
`Response for request ${name} failed validation`, validate.errors
|
||||
);
|
||||
}
|
||||
return Response.fromJSON(json);
|
||||
};
|
||||
|
||||
entry.Response = Response;
|
||||
} else {
|
||||
entry.responseFromJSON = () => undefined;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
registerRequest(Request, handler) {
|
||||
let name = Request.name;
|
||||
if (this._registeredRequests.has(name)) {
|
||||
throw new Error(`Request ${name} is already registered`);
|
||||
}
|
||||
let request = this._requestEntry(Request, handler);
|
||||
this._registeredRequests.set(name, request);
|
||||
}
|
||||
|
||||
fallbackRequest(Request, handler) {
|
||||
let name = Request.name;
|
||||
if (this._fallbackedRequests.has(name)) {
|
||||
throw new Error(`Request ${name} is already fallbacked`);
|
||||
}
|
||||
let request = this._requestEntry(Request, handler);
|
||||
this._fallbackedRequests.set(name, request);
|
||||
}
|
||||
|
||||
_createEventFromJSON(Event) {
|
||||
if (Event.fromJSON) {
|
||||
if (!Event.jsonSchema) {
|
||||
throw new Error(`Event ${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 ${Event.name} failed validation`, validate.errors);
|
||||
}
|
||||
return Event.fromJSON(json);
|
||||
};
|
||||
}
|
||||
if (Event.jsonSchema) {
|
||||
throw new Error(`Event ${Event.name} has static jsonSchema but is missing static fromJSON`);
|
||||
}
|
||||
return () => new Event();
|
||||
}
|
||||
|
||||
registerEvent(Event, handler) {
|
||||
if (this._registeredEvents.has(Event.name)) {
|
||||
throw new Error(`Event ${Event.name} is already registered`);
|
||||
}
|
||||
let event = {
|
||||
Event,
|
||||
handler,
|
||||
eventFromJSON: this._createEventFromJSON(Event),
|
||||
};
|
||||
this._registeredEvents.set(Event.name, event);
|
||||
}
|
||||
|
||||
snoopEvent(Event, handler) {
|
||||
let name = Event.name;
|
||||
if (this._snoopedEvents.has(name)) {
|
||||
throw new Error(`Event ${name} is already snooped`);
|
||||
}
|
||||
let event = {
|
||||
Event,
|
||||
handler,
|
||||
eventFromJSON: this._createEventFromJSON(Event),
|
||||
};
|
||||
this._snoopedEvents.set(Event.name, event);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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,
|
||||
};
|
||||
+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,
|
||||
};
|
||||
@@ -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(
|
||||
new libData.InstanceCreateSaveRequest(name, seed, mapGenSettings, mapSettings),
|
||||
{ instanceId: props.instance.id }
|
||||
).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(
|
||||
new libData.InstanceSendRconRequest(command),
|
||||
{ instanceId: props.id }
|
||||
);
|
||||
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,8 @@ export default function InstanceViewPage(props) {
|
||||
onClick={({ key }) => {
|
||||
if (key === "export") {
|
||||
setExportingData(true);
|
||||
libLink.messages.exportData.send(
|
||||
control, { instance_id: instanceId }
|
||||
control.sendTo(
|
||||
new libData.InstanceExportDataRequest(), { instanceId }
|
||||
).catch(
|
||||
notifyErrorHandler("Error exporting data")
|
||||
).finally(() => {
|
||||
@@ -140,13 +140,13 @@ export default function InstanceViewPage(props) {
|
||||
});
|
||||
|
||||
} else if (key === "extract") {
|
||||
libLink.messages.extractPlayers.send(
|
||||
control, { instance_id: instanceId }
|
||||
control.sendTo(
|
||||
new libData.InstanceExtractPlayersRequest(), { instanceId }
|
||||
).catch(notifyErrorHandler("Error extracting player data"));
|
||||
|
||||
} else if (key === "kill") {
|
||||
libLink.messages.killInstance.send(
|
||||
control, { instance_id: instanceId }
|
||||
control.sendTo(
|
||||
new libData.InstanceKillRequest(), { instanceId }
|
||||
).catch(notifyErrorHandler("Error killing instance"));
|
||||
|
||||
} else if (key === "delete") {
|
||||
@@ -156,8 +156,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 +185,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 +201,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,8 @@ 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(
|
||||
new libData.InstanceStopRequest(), { instanceId: instance.id }
|
||||
).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(
|
||||
new libData.InstanceLoadScenarioRequest(scenario, seed, mapGenSettings, mapSettings),
|
||||
{ instanceId: props.instance.id }
|
||||
).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>]);
|
||||
|
||||
@@ -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 => libData.ModInfo.fromJSON(mod)) })
|
||||
));
|
||||
setModResults(response.results);
|
||||
setModResultSelectedVersion(new Map(response.results.map(({ name, versions }) => [name, 0])));
|
||||
setModResultCount(response.result_count);
|
||||
});
|
||||
@@ -686,8 +684,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 +783,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"));
|
||||
}}
|
||||
@@ -99,7 +99,7 @@ function CreateModPackButton() {
|
||||
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,8 @@ 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(
|
||||
new libData.InstanceStartRequest(save.name), { instanceId: props.instance.id }
|
||||
).catch(
|
||||
notifyErrorHandler("Error loading save")
|
||||
).finally(
|
||||
@@ -245,11 +245,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 +265,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,15 @@ 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(
|
||||
new libData.InstanceStartRequest(undefined), { instanceId: props.instance["id"] }
|
||||
).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(
|
||||
new libData.InstanceStopRequest(), { instanceId: props.instance["id"] }
|
||||
).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 = libData.ModPack.fromJSON(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(libData.ModPack.fromJSON(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 => libData.ModPack.fromJSON(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 => libData.ModInfo.fromJSON(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(new libData.InstanceListSavesRequest(), { instanceId }).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,11 @@ 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 +35,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 +44,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 +113,20 @@ export class Control extends libLink.Link {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.register(libData.AccountUpdateEvent, this.handleAccountUpdateEvent.bind(this));
|
||||
this.register(libData.HostUpdateEvent, this.handleHostUpdateEvent.bind(this));
|
||||
this.register(libData.InstanceDetailsUpdateEvent, this.handleInstanceDetailsUpdateEvent.bind(this));
|
||||
this.register(libData.InstanceSaveListUpdateEvent, this.handleInstanceSaveListUpdateEvent.bind(this));
|
||||
this.register(libData.ModPackUpdateEvent, this.handleModPackUpdateEvent.bind(this));
|
||||
this.register(libData.ModUpdateEvent, this.handleModUpdateEvent.bind(this));
|
||||
this.register(libData.UserUpdateEvent, this.handleUserUpdateEvent.bind(this));
|
||||
this.register(libData.LogMessageEvent, this.handleLogMessageEvent.bind(this));
|
||||
this.register(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 +151,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 +202,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 +259,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 +313,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 = libData.ModPack.fromJSON(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 +371,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 = libData.ModInfo.fromJSON(message.data.mod);
|
||||
async handleModUpdateEvent(event) {
|
||||
let mod = event.mod;
|
||||
let handlers = [].concat(
|
||||
this.modUpdateHandlers.get(null) || [],
|
||||
this.modUpdateHandlers.get(mod.name) || []
|
||||
@@ -421,19 +429,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 +486,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 +541,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");
|
||||
}
|
||||
}
|
||||
@@ -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(new ChatEvent("Console", args.message), "allInstances");
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use strict";
|
||||
let { libLink } = require("@clusterio/lib");
|
||||
|
||||
module.exports = {
|
||||
name: "global_chat",
|
||||
@@ -7,17 +6,4 @@ module.exports = {
|
||||
description: "Forwards chat between instances.",
|
||||
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" },
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -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.register(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(new ChatEvent(this.instance.name, message), "allInstances");
|
||||
}
|
||||
|
||||
async onOutput(output) {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"use strict";
|
||||
|
||||
class ChatEvent {
|
||||
static type = "event";
|
||||
static src = ["control", "instance"];
|
||||
static dst = "instance";
|
||||
static plugin = "global_chat";
|
||||
|
||||
/** @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,
|
||||
};
|
||||
@@ -5,6 +5,7 @@ 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() {
|
||||
@@ -28,10 +29,10 @@ describe("global_chat plugin", function() {
|
||||
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.register(AcquireRequest, this.handleAcquireRequest.bind(this));
|
||||
this.controller.register(ReleaseRequest, this.handleReleaseRequest.bind(this));
|
||||
this.controller.register(UploadRequest, this.handleUploadRequest.bind(this));
|
||||
this.controller.register(DownloadRequest, this.handleDownloadRequest.bind(this));
|
||||
this.controller.register(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,5 @@
|
||||
"use strict";
|
||||
const { libConfig, libLink, libUsers } = require("@clusterio/lib");
|
||||
const { libConfig, libUsers } = require("@clusterio/lib");
|
||||
|
||||
class ControllerConfigGroup extends libConfig.PluginConfigGroup { }
|
||||
ControllerConfigGroup.defaultAccess = ["controller", "host", "control"];
|
||||
@@ -53,70 +53,4 @@ module.exports = {
|
||||
|
||||
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(new ReleaseRequest(this.instance.id, player_name), "controller");
|
||||
}
|
||||
})().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(
|
||||
new AcquireRequest(this.instance.id, request.player_name),
|
||||
"controller"
|
||||
);
|
||||
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(new ReleaseRequest(this.instance.id, request.player_name), "controller");
|
||||
} 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(
|
||||
new UploadRequest(this.instance.id, player_data.name, player_data),
|
||||
"controller"
|
||||
);
|
||||
|
||||
} 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(new DownloadRequest(this.instance.id, playerName), "controller");
|
||||
|
||||
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.register(FetchPlayerCodeRequest, this.handleFetchPlayerCodeRequest.bind(this));
|
||||
this.controller.register(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,5 @@
|
||||
"use strict";
|
||||
let { libConfig, libLink } = require("@clusterio/lib");
|
||||
let { libConfig } = require("@clusterio/lib");
|
||||
|
||||
|
||||
class ControllerConfigGroup extends libConfig.PluginConfigGroup {}
|
||||
@@ -29,28 +29,4 @@ module.exports = {
|
||||
instanceEntrypoint: "instance",
|
||||
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" },
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -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(new FetchPlayerCodeRequest(event.player), "controller");
|
||||
} 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(new SetVerifyCodeRequest(event.player, event.verify_code), "controller");
|
||||
|
||||
} 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.register(ContributionEvent, this.handleContributionEvent.bind(this));
|
||||
this.controller.register(FinishedEvent, this.handleFinishedEvent.bind(this));
|
||||
this.controller.register(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(new ProgressEvent(techs), "allInstances");
|
||||
}
|
||||
}
|
||||
|
||||
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(new FinishedEvent(name, tech.level), "allInstances");
|
||||
}
|
||||
}
|
||||
|
||||
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(new FinishedEvent(name, tech), "allInstances");
|
||||
}
|
||||
|
||||
} 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(new FinishedEvent(name, level - !researched), "allInstances");
|
||||
}
|
||||
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,4 @@
|
||||
"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" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
name: "research_sync",
|
||||
@@ -24,57 +6,4 @@ module.exports = {
|
||||
description: "Synchronises technology research progress between instances.",
|
||||
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,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -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.register(ProgressEvent, this.handleProgressEvent.bind(this));
|
||||
this.instance.register(FinishedEvent, this.handleFinishedEvent.bind(this));
|
||||
}
|
||||
|
||||
async researchContribution(tech) {
|
||||
this.info.messages.contribution.send(this.instance, tech);
|
||||
this.instance.sendTo(new ContributionEvent(tech.name, tech.level, tech.contribution), "controller");
|
||||
}
|
||||
|
||||
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(new FinishedEvent(tech.name, tech.level), "allInstances");
|
||||
}
|
||||
|
||||
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(new SyncTechnologiesRequest(techsToSend), "controller");
|
||||
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 { jsonArray } = require("@clusterio/lib/data");
|
||||
|
||||
class ContributionEvent {
|
||||
static type = "event";
|
||||
static src = "instance";
|
||||
static dst = "controller";
|
||||
static json = "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 json = "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";
|
||||
static dst = "instance";
|
||||
static json = "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 json = "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 = 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.register(GetStorageRequest, this.handleGetStorageRequest.bind(this));
|
||||
this.controller.register(PlaceEvent, this.handlePlaceEvent.bind(this));
|
||||
this.controller.register(RemoveRequest, this.handleRemoveRequest.bind(this));
|
||||
this.controller.register(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(update, "allInstances");
|
||||
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,5 @@
|
||||
"use strict";
|
||||
const { libConfig, libLink, libUsers } = require("@clusterio/lib");
|
||||
const { libConfig, libUsers } = require("@clusterio/lib");
|
||||
|
||||
class ControllerConfigGroup extends libConfig.PluginConfigGroup {}
|
||||
ControllerConfigGroup.defaultAccess = ["controller", "host", "control"];
|
||||
@@ -47,20 +47,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",
|
||||
@@ -73,55 +59,4 @@ module.exports = {
|
||||
|
||||
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.register(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(new GetStorageRequest(), "controller");
|
||||
// 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(new PlaceEvent(items), "controller");
|
||||
|
||||
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(new RemoveRequest(requestItems), "controller");
|
||||
|
||||
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 { jsonArray } = require("@clusterio/lib/data");
|
||||
|
||||
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 = 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 = 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 {
|
||||
|
||||
@@ -266,7 +266,7 @@ describe("controller/src/routes", function() {
|
||||
});
|
||||
|
||||
it("should respond with 500 if the transfer failed", async function() {
|
||||
controller.forwardRequestToInstance = async (data, request) => {
|
||||
controller.sendToHostByInstanceId = async (request) => {
|
||||
throw new Error("Something went wrong");
|
||||
};
|
||||
let response;
|
||||
@@ -294,7 +294,7 @@ describe("controller/src/routes", function() {
|
||||
});
|
||||
assert.equal(response.statusCode, 500);
|
||||
assert.deepEqual(JSON.parse(response.body), { errors: ["Something went wrong"], request_errors: [] });
|
||||
controller.forwardRequestToInstance = async (data, request) => {
|
||||
controller.sendToHostByInstanceId = async (request) => {
|
||||
await new Promise(() => {});
|
||||
};
|
||||
response = await phin({
|
||||
@@ -312,12 +312,12 @@ describe("controller/src/routes", function() {
|
||||
});
|
||||
|
||||
it("should complete a valid transfer", async function() {
|
||||
controller.forwardRequestToInstance = async (request, data) => {
|
||||
controller.sendToHostByInstanceId = async (request) => {
|
||||
let stream = await phin({
|
||||
url: `http://localhost:${port}/api/stream/${data.stream_id}`,
|
||||
url: `http://localhost:${port}/api/stream/${request.streamId}`,
|
||||
});
|
||||
assert.equal(stream.body.toString(), "totally a zip file");
|
||||
return { save: data.filename };
|
||||
return request.name;
|
||||
};
|
||||
let response;
|
||||
response = await phin({
|
||||
@@ -332,12 +332,12 @@ describe("controller/src/routes", function() {
|
||||
assert.equal(response.statusCode, 200);
|
||||
});
|
||||
it("should complete a valid transfer with non-standerd mime type", async function() {
|
||||
controller.forwardRequestToInstance = async (request, data) => {
|
||||
controller.sendToHostByInstanceId = async (request) => {
|
||||
let stream = await phin({
|
||||
url: `http://localhost:${port}/api/stream/${data.stream_id}`,
|
||||
url: `http://localhost:${port}/api/stream/${request.streamId}`,
|
||||
});
|
||||
assert.equal(stream.body.toString(), "totally a zip file");
|
||||
return { save: data.filename };
|
||||
return request.name;
|
||||
};
|
||||
let response;
|
||||
response = await phin({
|
||||
|
||||
+17
-18
@@ -3,6 +3,7 @@ const assert = require("assert").strict;
|
||||
const path = require("path");
|
||||
|
||||
const libConfig = require("@clusterio/lib/config");
|
||||
const libData = require("@clusterio/lib/data");
|
||||
const Host = require("@clusterio/host/src/Host");
|
||||
|
||||
describe("Host testing", function() {
|
||||
@@ -30,7 +31,7 @@ describe("Host testing", function() {
|
||||
});
|
||||
|
||||
describe("class Host", function() {
|
||||
describe(".syncUserListsEventHandler()", function() {
|
||||
describe(".handleSyncUserListsEvent()", function() {
|
||||
let mockHost;
|
||||
beforeEach(function() {
|
||||
mockHost = {
|
||||
@@ -38,16 +39,14 @@ describe("Host testing", function() {
|
||||
whitelist: new Set(),
|
||||
banlist: new Map(),
|
||||
broadcasts: [],
|
||||
broadcastEventToInstance(message, event) {
|
||||
this.broadcasts.push(message["data"]);
|
||||
broadcastEventToInstance(event) {
|
||||
this.broadcasts.push(event);
|
||||
},
|
||||
syncUserListsEventHandler: Host.prototype.syncUserListsEventHandler,
|
||||
handleSyncUserListsEvent: Host.prototype.handleSyncUserListsEvent,
|
||||
syncLists(adminlist, banlist, whitelist) {
|
||||
return this.syncUserListsEventHandler({ "data": {
|
||||
"adminlist": adminlist,
|
||||
"banlist": banlist,
|
||||
"whitelist": whitelist,
|
||||
}});
|
||||
return this.handleSyncUserListsEvent(
|
||||
new libData.SyncUserListsEvent(adminlist, banlist, whitelist)
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -57,8 +56,8 @@ describe("Host testing", function() {
|
||||
await mockHost.syncLists(["admin1", "admin2"], [], []);
|
||||
|
||||
assert.deepEqual(mockHost.broadcasts, [
|
||||
{ "name": "admin1", "admin": true },
|
||||
{ "name": "admin2", "admin": true },
|
||||
new libData.InstanceAdminlistUpdateEvent("admin1", true),
|
||||
new libData.InstanceAdminlistUpdateEvent("admin2", true),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -66,7 +65,7 @@ describe("Host testing", function() {
|
||||
mockHost.adminlist.add("admin1").add("admin2");
|
||||
await mockHost.syncLists(["admin1"], [], []);
|
||||
assert.deepEqual(mockHost.broadcasts, [
|
||||
{ "name": "admin2", "admin": false },
|
||||
new libData.InstanceAdminlistUpdateEvent("admin2", false),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -75,8 +74,8 @@ describe("Host testing", function() {
|
||||
await mockHost.syncLists([], [], ["player1", "player2"]);
|
||||
|
||||
assert.deepEqual(mockHost.broadcasts, [
|
||||
{ "name": "player1", "whitelisted": true },
|
||||
{ "name": "player2", "whitelisted": true },
|
||||
new libData.InstanceWhitelistUpdateEvent("player1", true),
|
||||
new libData.InstanceWhitelistUpdateEvent("player2", true),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -85,7 +84,7 @@ describe("Host testing", function() {
|
||||
await mockHost.syncLists([], [], ["player1"]);
|
||||
|
||||
assert.deepEqual(mockHost.broadcasts, [
|
||||
{ "name": "player2", "whitelisted": false },
|
||||
new libData.InstanceWhitelistUpdateEvent("player2", false),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -94,8 +93,8 @@ describe("Host testing", function() {
|
||||
await mockHost.syncLists([], [["badie1", "greifing"], ["badie2", "annoying"]], []);
|
||||
|
||||
assert.deepEqual(mockHost.broadcasts, [
|
||||
{ "name": "badie1", "banned": true, "reason": "greifing" },
|
||||
{ "name": "badie2", "banned": true, "reason": "annoying" },
|
||||
new libData.InstanceBanlistUpdateEvent("badie1", true, "greifing"),
|
||||
new libData.InstanceBanlistUpdateEvent("badie2", true, "annoying"),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -104,7 +103,7 @@ describe("Host testing", function() {
|
||||
await mockHost.syncLists([], [["badie1", "greifing"]], []);
|
||||
|
||||
assert.deepEqual(mockHost.broadcasts, [
|
||||
{ "name": "badie2", "banned": false, "reason": "" },
|
||||
new libData.InstanceBanlistUpdateEvent("badie2", false, ""),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
+38
-61
@@ -3,23 +3,30 @@ const assert = require("assert").strict;
|
||||
const path = require("path");
|
||||
|
||||
const libConfig = require("@clusterio/lib/config");
|
||||
const libData = require("@clusterio/lib/data");
|
||||
const { wait } = require("@clusterio/lib/helpers");
|
||||
const PlayerStats = require("@clusterio/lib/PlayerStats");
|
||||
const Instance = require("@clusterio/host/src/Instance");
|
||||
const { MockConnector, MockServer } = require("../mock");
|
||||
|
||||
const addr = libData.Address.fromShorthand;
|
||||
|
||||
|
||||
describe("class Instance", function() {
|
||||
before(function() {
|
||||
libConfig.InstanceConfig.finalize();
|
||||
});
|
||||
|
||||
let src;
|
||||
let dst = addr({ hostId: 1 });
|
||||
let instance;
|
||||
let connector;
|
||||
beforeEach(async function() {
|
||||
let instanceConfig = new libConfig.InstanceConfig("host");
|
||||
await instanceConfig.init();
|
||||
instanceConfig.set("instance.name", "foo");
|
||||
connector = new MockConnector();
|
||||
src = addr({ instanceId: instanceConfig.get("instance.id") });
|
||||
connector = new MockConnector(src, dst);
|
||||
instance = new Instance({}, connector, "dir", "factorioDir", instanceConfig);
|
||||
instance.server = new MockServer();
|
||||
});
|
||||
@@ -57,19 +64,12 @@ describe("class Instance", function() {
|
||||
let stats = instance.playerStats.get("player");
|
||||
assert.deepEqual(
|
||||
connector.sentMessages[0],
|
||||
{
|
||||
seq: 1,
|
||||
type: "player_event_event",
|
||||
data: {
|
||||
type: "join",
|
||||
instance_id: instance.id,
|
||||
name: "player",
|
||||
stats: {
|
||||
join_count: 1,
|
||||
last_join_at: stats.lastJoinAt.getTime(),
|
||||
},
|
||||
},
|
||||
},
|
||||
new libData.MessageEvent(1, src, addr("controller"), "InstancePlayerUpdateEvent",
|
||||
new libData.InstancePlayerUpdateEvent("join", "player", undefined, new PlayerStats({
|
||||
join_count: 1,
|
||||
last_join_at: stats.lastJoinAt.getTime(),
|
||||
}))
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -105,23 +105,15 @@ describe("class Instance", function() {
|
||||
let stats = instance.playerStats.get("player");
|
||||
assert.deepEqual(
|
||||
connector.sentMessages[1],
|
||||
{
|
||||
seq: 2,
|
||||
type: "player_event_event",
|
||||
data: {
|
||||
type: "leave",
|
||||
instance_id: instance.id,
|
||||
name: "player",
|
||||
reason: "quit",
|
||||
stats: {
|
||||
join_count: 1,
|
||||
online_time_ms: stats.onlineTimeMs,
|
||||
last_join_at: stats.lastJoinAt.getTime(),
|
||||
last_leave_at: stats.lastLeaveAt.getTime(),
|
||||
last_leave_reason: "quit",
|
||||
},
|
||||
},
|
||||
},
|
||||
new libData.MessageEvent(2, src, addr("controller"), "InstancePlayerUpdateEvent",
|
||||
new libData.InstancePlayerUpdateEvent("leave", "player", "quit", new PlayerStats({
|
||||
join_count: 1,
|
||||
online_time_ms: stats.onlineTimeMs,
|
||||
last_join_at: stats.lastJoinAt.getTime(),
|
||||
last_leave_at: stats.lastLeaveAt.getTime(),
|
||||
last_leave_reason: "quit",
|
||||
}))
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -159,19 +151,12 @@ describe("class Instance", function() {
|
||||
let stats = instance.playerStats.get("foo");
|
||||
assert.deepEqual(
|
||||
connector.sentMessages[1],
|
||||
{
|
||||
seq: 2,
|
||||
type: "player_event_event",
|
||||
data: {
|
||||
type: "join",
|
||||
instance_id: instance.id,
|
||||
name: "foo",
|
||||
stats: {
|
||||
join_count: 1,
|
||||
last_join_at: stats.lastJoinAt.getTime(),
|
||||
},
|
||||
},
|
||||
},
|
||||
new libData.MessageEvent(2, src, addr("controller"), "InstancePlayerUpdateEvent",
|
||||
new libData.InstancePlayerUpdateEvent("join", "foo", undefined, new PlayerStats({
|
||||
join_count: 1,
|
||||
last_join_at: stats.lastJoinAt.getTime(),
|
||||
}))
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -183,23 +168,15 @@ describe("class Instance", function() {
|
||||
let stats = instance.playerStats.get("player");
|
||||
assert.deepEqual(
|
||||
connector.sentMessages[1],
|
||||
{
|
||||
seq: 2,
|
||||
type: "player_event_event",
|
||||
data: {
|
||||
type: "leave",
|
||||
instance_id: instance.id,
|
||||
name: "player",
|
||||
reason: "quit",
|
||||
stats: {
|
||||
join_count: 1,
|
||||
online_time_ms: stats.onlineTimeMs,
|
||||
last_join_at: stats.lastJoinAt.getTime(),
|
||||
last_leave_at: stats.lastLeaveAt.getTime(),
|
||||
last_leave_reason: "quit",
|
||||
},
|
||||
},
|
||||
},
|
||||
new libData.MessageEvent(2, src, addr("controller"), "InstancePlayerUpdateEvent",
|
||||
new libData.InstancePlayerUpdateEvent("leave", "player", "quit", new PlayerStats({
|
||||
join_count: 1,
|
||||
online_time_ms: stats.onlineTimeMs,
|
||||
last_join_at: stats.lastJoinAt.getTime(),
|
||||
last_leave_at: stats.lastLeaveAt.getTime(),
|
||||
last_leave_reason: "quit",
|
||||
}))
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+80
-107
@@ -21,8 +21,8 @@ const {
|
||||
|
||||
|
||||
async function getInstances() {
|
||||
let result = await libLink.messages.listInstances.send(getControl());
|
||||
return new Map(result.list.map(instance => [instance.id, instance]));
|
||||
let instances = await getControl().send(new libData.InstanceDetailsListRequest());
|
||||
return new Map(instances.map(instance => [instance.id, instance]));
|
||||
}
|
||||
|
||||
async function checkInstanceStatus(id, status) {
|
||||
@@ -45,7 +45,7 @@ async function uploadSave(instanceId, name, content) {
|
||||
}
|
||||
|
||||
async function deleteSave(instanceId, save) {
|
||||
await libLink.messages.deleteSave.send(getControl(), { instance_id: instanceId, save });
|
||||
await getControl().send(new libData.InstanceDeleteSaveRequest(instanceId, save));
|
||||
}
|
||||
|
||||
describe("Integration of Clusterio", function() {
|
||||
@@ -76,6 +76,7 @@ describe("Integration of Clusterio", function() {
|
||||
|
||||
let connectorB = new TestControlConnector(url, 2, tlsCa);
|
||||
connectorB.token = controlToken;
|
||||
connectorB.src = connectorA.src;
|
||||
let controlB = new TestControl(connectorB);
|
||||
connectorB._sessionToken = connectorA._sessionToken;
|
||||
connectorB._sessionTimeout = connectorA._sessionTimeout;
|
||||
@@ -89,36 +90,12 @@ describe("Integration of Clusterio", function() {
|
||||
|
||||
describe("queryLogRequestHandler", function() {
|
||||
it("should honnor the limit", async function() {
|
||||
let result = await libLink.messages.queryLog.send(getControl(), {
|
||||
all: true,
|
||||
controller: false,
|
||||
host_ids: [],
|
||||
instance_ids: [],
|
||||
max_level: null,
|
||||
limit: 10,
|
||||
order: "asc",
|
||||
});
|
||||
let result = await getControl().send(new libData.LogQueryRequest(true, false, [], [], null, 10, "asc"));
|
||||
assert.equal(result.log.length, 10);
|
||||
});
|
||||
it("should return entries by order", async function() {
|
||||
let first = await libLink.messages.queryLog.send(getControl(), {
|
||||
all: true,
|
||||
controller: false,
|
||||
host_ids: [],
|
||||
instance_ids: [],
|
||||
max_level: null,
|
||||
limit: 1,
|
||||
order: "asc",
|
||||
});
|
||||
let last = await libLink.messages.queryLog.send(getControl(), {
|
||||
all: true,
|
||||
controller: false,
|
||||
host_ids: [],
|
||||
instance_ids: [],
|
||||
max_level: null,
|
||||
limit: 1,
|
||||
order: "desc",
|
||||
});
|
||||
let first = await getControl().send(new libData.LogQueryRequest(true, false, [], [], null, 1, "asc"));
|
||||
let last = await getControl().send(new libData.LogQueryRequest(true, false, [], [], null, 1, "desc"));
|
||||
assert(first.log[0].timestamp < last.log[0].timestamp, "first log entry happened after last");
|
||||
});
|
||||
});
|
||||
@@ -183,8 +160,8 @@ describe("Integration of Clusterio", function() {
|
||||
assert(sawConnected, "No host update with status connected was sent");
|
||||
assert(sawDisconnected, "No host update with status disconnected was sent");
|
||||
|
||||
let result = await libLink.messages.listHosts.send(getControl());
|
||||
let hosts = new Map(result.list.map(instance => [instance.id, instance]));
|
||||
let result = await getControl().send(new libData.HostListRequest());
|
||||
let hosts = new Map(result.map(instance => [instance.id, instance]));
|
||||
assert(hosts.has(5), "Host list was not updated");
|
||||
});
|
||||
});
|
||||
@@ -196,9 +173,9 @@ describe("Integration of Clusterio", function() {
|
||||
await execCtl("controller config list");
|
||||
});
|
||||
it("should not leak auth_secret", async function() {
|
||||
let result = await libLink.messages.getControllerConfig.send(getControl());
|
||||
let result = await getControl().send(new libData.ControllerConfigGetRequest());
|
||||
let done = false;
|
||||
for (let group of result.serialized_config.groups) {
|
||||
for (let group of result.serializedConfig.groups) {
|
||||
if (group.name === "controller") {
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(group.fields, "auth_secret"), false);
|
||||
done = true;
|
||||
@@ -211,9 +188,9 @@ describe("Integration of Clusterio", function() {
|
||||
describe("controller config set", function() {
|
||||
it("sets given config option", async function() {
|
||||
await execCtl('controller config set controller.name "Test Cluster"');
|
||||
let result = await libLink.messages.getControllerConfig.send(getControl());
|
||||
let result = await getControl().send(new libData.ControllerConfigGetRequest());
|
||||
let done = false;
|
||||
for (let group of result.serialized_config.groups) {
|
||||
for (let group of result.serializedConfig.groups) {
|
||||
if (group.name === "controller") {
|
||||
assert.equal(group.fields.name, "Test Cluster");
|
||||
done = true;
|
||||
@@ -289,7 +266,7 @@ describe("Integration of Clusterio", function() {
|
||||
describe("saveListUpdateEventHandler()", function() {
|
||||
it("should have triggered for the created save", function() {
|
||||
slowTest(this);
|
||||
assert.equal(getControl().saveListUpdates.slice(-1)[0].list[0].name, "world.zip");
|
||||
assert.equal(getControl().saveListUpdates.slice(-1)[0].saves[0].name, "world.zip");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -307,8 +284,7 @@ describe("Integration of Clusterio", function() {
|
||||
let exportPath = path.join("temp", "test", "static");
|
||||
await fs.remove(exportPath);
|
||||
await execCtl("instance export-data test");
|
||||
let result = await libLink.messages.getDefaultModPack.send(getControl());
|
||||
let modPack = libData.ModPack.fromJSON(result.mod_pack);
|
||||
let modPack = await getControl().send(new libData.ModPackGetDefaultRequest());
|
||||
let assets = modPack.exportManifest.assets;
|
||||
assert(Object.keys(assets).length > 1, "Export assets is empty");
|
||||
for (let key of ["settings", "prototypes", "item-metadata", "item-spritesheet", "locale"]) {
|
||||
@@ -357,8 +333,8 @@ describe("Integration of Clusterio", function() {
|
||||
let savesDir = path.join("temp", "test", "instances", "test", "saves");
|
||||
await fs.copy(path.join(savesDir, "world.zip"), path.join(savesDir, "_autosave1.zip"));
|
||||
await execCtl("instance start test");
|
||||
let result = await libLink.messages.listSaves.send(getControl(), { instance_id: 44 });
|
||||
let running = result.list.find(s => s.loaded);
|
||||
let saves = await getControl().sendTo(new libData.InstanceListSavesRequest(), { instanceId: 44 });
|
||||
let running = saves.find(s => s.loaded);
|
||||
assert(running.name !== "_autosave1.zip");
|
||||
});
|
||||
});
|
||||
@@ -367,15 +343,9 @@ describe("Integration of Clusterio", function() {
|
||||
it("sends the command", async function() {
|
||||
slowTest(this);
|
||||
await execCtl("instance send-rcon test technobabble");
|
||||
let { log } = await libLink.messages.queryLog.send(getControl(), {
|
||||
all: false,
|
||||
controller: false,
|
||||
host_ids: [],
|
||||
instance_ids: [44],
|
||||
max_level: null,
|
||||
limit: 10,
|
||||
order: "desc",
|
||||
});
|
||||
let { log } = await getControl().send(
|
||||
new libData.LogQueryRequest(false, false, [], [44], null, 10, "desc")
|
||||
);
|
||||
assert(log.some(info => /technobabble/.test(info.message)), "Command was not sent");
|
||||
});
|
||||
|
||||
@@ -486,41 +456,41 @@ describe("Integration of Clusterio", function() {
|
||||
|
||||
describe("user set-admin/whitelisted/banned", function() {
|
||||
async function getUsers() {
|
||||
let result = await libLink.messages.listUsers.send(getControl());
|
||||
return new Map(result.list.map(user => [user.name, user]));
|
||||
let users = await getControl().send(new libData.UserListRequest());
|
||||
return new Map(users.map(user => [user.name, user]));
|
||||
}
|
||||
|
||||
async function getUser(name) {
|
||||
return await libLink.messages.getUser.send(getControl(), { name });
|
||||
return await getControl().send(new libData.UserGetRequest(name));
|
||||
}
|
||||
|
||||
let lists = ["admin", "whitelisted", "banned"];
|
||||
let lists = [["admin", "isAdmin"], ["whitelisted", "isWhitelisted"], ["banned", "isBanned"]];
|
||||
it("should add and remove the given user to the list", async function() {
|
||||
slowTest(this);
|
||||
getControl().userUpdates = [];
|
||||
await libLink.messages.createUser.send(getControl(), { name: "list_test" });
|
||||
await getControl().send(new libData.UserCreateRequest("list_test"));
|
||||
let user = (await getUsers()).get("list_test");
|
||||
for (let list of lists) {
|
||||
assert.equal(user[`is_${list}`], false, `unexpected ${list} status`);
|
||||
await execCtl(`user set-${list} list_test`);
|
||||
for (let [listName, prop] of lists) {
|
||||
assert.equal(user[prop], false, `unexpected ${listName} status`);
|
||||
await execCtl(`user set-${listName} list_test`);
|
||||
}
|
||||
user = (await getUsers()).get("list_test");
|
||||
for (let list of lists) {
|
||||
assert.equal(user[`is_${list}`], true, `unexpected ${list} status`);
|
||||
let remove = { admin: "--revoke", whitelisted: "--remove", banned: "--pardon" }[list];
|
||||
await execCtl(`user set-${list} ${remove} list_test`);
|
||||
for (let [listName, prop] of lists) {
|
||||
assert.equal(user[prop], true, `unexpected ${listName} status`);
|
||||
let remove = { admin: "--revoke", whitelisted: "--remove", banned: "--pardon" }[listName];
|
||||
await execCtl(`user set-${listName} ${remove} list_test`);
|
||||
}
|
||||
user = (await getUsers()).get("list_test");
|
||||
for (let list of lists) {
|
||||
assert.equal(user[`is_${list}`], false, `unexpected ${list} status`);
|
||||
for (let [listName, prop] of lists) {
|
||||
assert.equal(user[prop], false, `unexpected ${listName} status`);
|
||||
}
|
||||
assert.equal(getControl().userUpdates.length, 7);
|
||||
});
|
||||
it("should not create the user if not instructed to", async function() {
|
||||
slowTest(this);
|
||||
for (let list of lists) {
|
||||
for (let [listName, prop] of lists) {
|
||||
try {
|
||||
await execCtl(`user set-${list} no_create_test`);
|
||||
await execCtl(`user set-${listName} no_create_test`);
|
||||
} catch (err) { /* ignore */ }
|
||||
}
|
||||
let user = (await getUsers()).get("no_create_test");
|
||||
@@ -529,10 +499,10 @@ describe("Integration of Clusterio", function() {
|
||||
it("should create the user if instructed to", async function() {
|
||||
slowTest(this);
|
||||
getControl().userUpdates = [];
|
||||
for (let list of lists) {
|
||||
await execCtl(`user set-${list} --create test_create_${list}`);
|
||||
let user = (await getUsers()).get(`test_create_${list}`);
|
||||
assert.equal(user && user[`is_${list}`], true, `user not created and added to ${list}`);
|
||||
for (let [listName, prop] of lists) {
|
||||
await execCtl(`user set-${listName} --create test_create_${listName}`);
|
||||
let user = (await getUsers()).get(`test_create_${listName}`);
|
||||
assert.equal(user && user[prop], true, `user not created and added to ${listName}`);
|
||||
}
|
||||
assert.equal(getControl().userUpdates.length, 3);
|
||||
});
|
||||
@@ -541,9 +511,9 @@ describe("Integration of Clusterio", function() {
|
||||
getControl().userUpdates = [];
|
||||
await execCtl("user set-banned --create test_ban_reason --reason a-reason");
|
||||
assert.equal(getControl().userUpdates.length, 1);
|
||||
assert.equal(getControl().userUpdates[0].ban_reason, "a-reason");
|
||||
assert.equal(getControl().userUpdates[0].banReason, "a-reason");
|
||||
let user = await getUser("test_ban_reason");
|
||||
assert.equal(user["ban_reason"], "a-reason");
|
||||
assert.equal(user.banReason, "a-reason");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -831,8 +801,8 @@ describe("Integration of Clusterio", function() {
|
||||
describe("mod-pack create", function() {
|
||||
it("should create a mod-pack", async function() {
|
||||
await execCtl("mod-pack create empty-pack 1.1.0");
|
||||
let response = await libLink.messages.listModPacks.send(getControl());
|
||||
assert(response.list.some(modPack => modPack.name === "empty-pack"), "created pack is not in the list");
|
||||
let modPacks = await getControl().send(new libData.ModPackListRequest());
|
||||
assert(modPacks.some(modPack => modPack.name === "empty-pack"), "created pack is not in the list");
|
||||
});
|
||||
it("should allow setting all fields", async function() {
|
||||
await execCtl(
|
||||
@@ -844,8 +814,8 @@ describe("Integration of Clusterio", function() {
|
||||
"--double-setting runtime-global MyDouble 12.25 " +
|
||||
"--string-setting runtime-per-user MyString a-string"
|
||||
);
|
||||
let response = await libLink.messages.listModPacks.send(getControl());
|
||||
let modPack = response.list.find(entry => entry.name === "full-pack");
|
||||
let modPacks = await getControl().send(new libData.ModPackListRequest());
|
||||
let modPack = modPacks.find(entry => entry.name === "full-pack");
|
||||
assert(modPack, "created mod pack not found");
|
||||
let reference = libData.ModPack.fromJSON({});
|
||||
reference.id = modPack.id;
|
||||
@@ -857,7 +827,7 @@ describe("Integration of Clusterio", function() {
|
||||
reference.settings["runtime-global"].set("MyInt", { value: 1235 });
|
||||
reference.settings["runtime-global"].set("MyDouble", { value: 12.25 });
|
||||
reference.settings["runtime-per-user"].set("MyString", { value: "a-string" });
|
||||
assert.deepEqual(libData.ModPack.fromJSON(modPack), reference);
|
||||
assert.deepEqual(modPack, reference);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -895,18 +865,18 @@ describe("Integration of Clusterio", function() {
|
||||
describe("mod-pack edit", function() {
|
||||
it("runs", async function() {
|
||||
await execCtl("mod-pack edit full-pack --factorio-version 1.2.0");
|
||||
let response = await libLink.messages.listModPacks.send(getControl());
|
||||
let modPack = response.list.find(entry => entry.name === "full-pack");
|
||||
let modPacks = await getControl().send(new libData.ModPackListRequest());
|
||||
let modPack = modPacks.find(entry => entry.name === "full-pack");
|
||||
assert(modPack, "created mod pack not found");
|
||||
assert.equal(modPack.factorio_version, "1.2.0");
|
||||
assert.equal(modPack.factorioVersion, "1.2.0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mod-pack delete", function() {
|
||||
it("deletes the pack", async function() {
|
||||
await execCtl("mod-pack delete full-pack");
|
||||
let response = await libLink.messages.listModPacks.send(getControl());
|
||||
let modPack = response.list.find(entry => entry.name === "full-pack");
|
||||
let modPacks = await getControl().send(new libData.ModPackListRequest());
|
||||
let modPack = modPacks.find(entry => entry.name === "full-pack");
|
||||
assert(!modPack, "mod pack not deleted");
|
||||
});
|
||||
});
|
||||
@@ -938,11 +908,14 @@ describe("Integration of Clusterio", function() {
|
||||
"version: 1.0.0\n" +
|
||||
"title: An Empty Mod\n" +
|
||||
"author: Me\n" +
|
||||
"contact: \n" +
|
||||
"homepage: \n" +
|
||||
"description: An empty mod for testing\n" +
|
||||
"factorio_version: 1.1\n" +
|
||||
"factorioVersion: 1.1\n" +
|
||||
"dependencies:\n" +
|
||||
`size: ${stat.size}\n` +
|
||||
`sha1: ${hash}\n`,
|
||||
`sha1: ${hash}\n` +
|
||||
"isDeleted: false\n",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -991,7 +964,7 @@ describe("Integration of Clusterio", function() {
|
||||
if (modUpdate.name !== "empty_mod" || modUpdate.version !== "1.0.0") {
|
||||
continue;
|
||||
}
|
||||
if (modUpdate.is_deleted) {
|
||||
if (modUpdate.isDeleted) {
|
||||
eventsNotSeen.delete("deleted");
|
||||
} else {
|
||||
eventsNotSeen.delete("updated");
|
||||
@@ -1018,11 +991,11 @@ describe("Integration of Clusterio", function() {
|
||||
it("should create the given role", async function() {
|
||||
let args = "--description \"A temp role\" --permissions core.control.connect";
|
||||
await execCtl(`role create temp ${args}`);
|
||||
let result = await libLink.messages.listRoles.send(getControl());
|
||||
let tempRole = result.list.find(role => role.name === "temp");
|
||||
let roles = await getControl().send(new libData.RoleListRequest());
|
||||
let tempRole = roles.find(role => role.name === "temp");
|
||||
assert.deepEqual(
|
||||
tempRole,
|
||||
{ id: 5, name: "temp", description: "A temp role", permissions: ["core.control.connect"] }
|
||||
new libData.RawRole(5, "temp", "A temp role", ["core.control.connect"])
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1031,28 +1004,28 @@ describe("Integration of Clusterio", function() {
|
||||
it("should modify the given role", async function() {
|
||||
let args = "--name new --description \"A new role\" --set-perms";
|
||||
await execCtl(`role edit temp ${args}`);
|
||||
let result = await libLink.messages.listRoles.send(getControl());
|
||||
let newRole = result.list.find(role => role.name === "new");
|
||||
assert.deepEqual(newRole, { id: 5, name: "new", description: "A new role", permissions: [] });
|
||||
let roles = await getControl().send(new libData.RoleListRequest());
|
||||
let newRole = roles.find(role => role.name === "new");
|
||||
assert.deepEqual(newRole, new libData.RawRole(5, "new", "A new role", []));
|
||||
});
|
||||
it("should add permissions with --add-perms", async function() {
|
||||
let args = "--name new --add-perms core.host.list core.instance.list";
|
||||
await execCtl(`role edit new ${args}`);
|
||||
let result = await libLink.messages.listRoles.send(getControl());
|
||||
let newRole = result.list.find(role => role.name === "new");
|
||||
let roles = await getControl().send(new libData.RoleListRequest());
|
||||
let newRole = roles.find(role => role.name === "new");
|
||||
assert.deepEqual(newRole.permissions, ["core.host.list", "core.instance.list"]);
|
||||
});
|
||||
it("should remove permissions with --remove-perms", async function() {
|
||||
let args = "--name new --remove-perms core.host.list";
|
||||
await execCtl(`role edit new ${args}`);
|
||||
let result = await libLink.messages.listRoles.send(getControl());
|
||||
let newRole = result.list.find(role => role.name === "new");
|
||||
let roles = await getControl().send(new libData.RoleListRequest());
|
||||
let newRole = roles.find(role => role.name === "new");
|
||||
assert.deepEqual(newRole.permissions, ["core.instance.list"]);
|
||||
});
|
||||
it("should grant default permissions with --grant-default", async function() {
|
||||
await execCtl("role edit new --grant-default");
|
||||
let result = await libLink.messages.listRoles.send(getControl());
|
||||
let newRole = result.list.find(role => role.name === "new");
|
||||
let roles = await getControl().send(new libData.RoleListRequest());
|
||||
let newRole = roles.find(role => role.name === "new");
|
||||
let defaultPermissions = [...libUsers.permissions.values()]
|
||||
.filter(p => p.grantByDefault)
|
||||
.map(p => p.name)
|
||||
@@ -1064,8 +1037,8 @@ describe("Integration of Clusterio", function() {
|
||||
describe("role delete", function() {
|
||||
it("should delete the given role", async function() {
|
||||
await execCtl("role delete new");
|
||||
let result = await libLink.messages.listRoles.send(getControl());
|
||||
let newRole = result.list.find(role => role.name === "new");
|
||||
let roles = await getControl().send(new libData.RoleListRequest());
|
||||
let newRole = roles.find(role => role.name === "new");
|
||||
assert(!newRole, "Role was not deleted");
|
||||
});
|
||||
});
|
||||
@@ -1080,8 +1053,8 @@ describe("Integration of Clusterio", function() {
|
||||
it("should create the given user", async function() {
|
||||
getControl().userUpdates = [];
|
||||
await execCtl("user create temp");
|
||||
let result = await libLink.messages.listUsers.send(getControl());
|
||||
let tempUser = result.list.find(user => user.name === "temp");
|
||||
let users = await getControl().send(new libData.UserListRequest());
|
||||
let tempUser = users.find(user => user.name === "temp");
|
||||
assert(tempUser, "user was not created");
|
||||
assert.equal(getControl().userUpdates.length, 1);
|
||||
assert.equal(getControl().userUpdates[0].name, "temp");
|
||||
@@ -1091,7 +1064,7 @@ describe("Integration of Clusterio", function() {
|
||||
describe("user revoke-token", function() {
|
||||
it("should kick existing sessions for the user", async function() {
|
||||
slowTest(this);
|
||||
await libLink.messages.createUser.send(getControl(), { name: "revokee" });
|
||||
await getControl().send(new libData.UserCreateRequest("revokee"));
|
||||
let tlsCa = await fs.readFile("test/file/tls/cert.pem");
|
||||
let connector = new TestControlConnector(url, 2, tlsCa);
|
||||
connector.token = jwt.sign(
|
||||
@@ -1112,8 +1085,8 @@ describe("Integration of Clusterio", function() {
|
||||
it("should set the roles on the user", async function() {
|
||||
getControl().userUpdates = [];
|
||||
await execCtl('user set-roles temp "Cluster Admin"');
|
||||
let result = await libLink.messages.listUsers.send(getControl());
|
||||
let tempUser = result.list.find(user => user.name === "temp");
|
||||
let users = await getControl().send(new libData.UserListRequest());
|
||||
let tempUser = users.find(user => user.name === "temp");
|
||||
assert.deepEqual(tempUser.roles, [0]);
|
||||
assert.equal(getControl().userUpdates.length, 1);
|
||||
});
|
||||
@@ -1123,11 +1096,11 @@ describe("Integration of Clusterio", function() {
|
||||
it("should delete the user", async function() {
|
||||
getControl().userUpdates = [];
|
||||
await execCtl("user delete temp");
|
||||
let result = await libLink.messages.listUsers.send(getControl());
|
||||
let tempUser = result.list.find(user => user.name === "temp");
|
||||
let users = await getControl().send(new libData.UserListRequest());
|
||||
let tempUser = users.find(user => user.name === "temp");
|
||||
assert(!tempUser, "user was note deleted");
|
||||
assert.equal(getControl().userUpdates.length, 1);
|
||||
assert.equal(getControl().userUpdates[0].is_deleted, true);
|
||||
assert.equal(getControl().userUpdates[0].isDeleted, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+44
-42
@@ -23,9 +23,8 @@ require("../../plugins/subspace_storage/info");
|
||||
|
||||
|
||||
class TestControl extends libLink.Link {
|
||||
constructor(connector) {
|
||||
super("control", "controller", connector);
|
||||
libLink.attachAllMessages(this);
|
||||
constructor(connector, subscribe = true) {
|
||||
super(connector);
|
||||
this.hostUpdates = [];
|
||||
this.instanceUpdates = [];
|
||||
this.saveListUpdates = [];
|
||||
@@ -34,63 +33,67 @@ class TestControl extends libLink.Link {
|
||||
this.userUpdates = [];
|
||||
|
||||
this.connector.on("connect", () => {
|
||||
libLink.messages.setHostSubscriptions.send(
|
||||
this, { all: true, host_ids: [] }
|
||||
if (!subscribe) {
|
||||
return;
|
||||
}
|
||||
this.send(
|
||||
new libData.HostSetSubscriptionsRequest(true, [])
|
||||
).catch(err => logger.error(`Error setting host subscriptions:\n${err.stack}`));
|
||||
libLink.messages.setInstanceSubscriptions.send(
|
||||
this, { all: true, instance_ids: [] }
|
||||
this.send(
|
||||
new libData.InstanceDetailsSetSubscriptionsRequest(true, [])
|
||||
).catch(err => logger.error(`Error setting instance subscriptions:\n${err.stack}`));
|
||||
libLink.messages.setSaveListSubscriptions.send(
|
||||
this, { all: true, instance_ids: [] }
|
||||
this.send(
|
||||
new libData.InstanceSetSaveListSubscriptionsRequest(true, [])
|
||||
).catch(err => logger.error(`Error setting save list subscriptions:\n${err.stack}`));
|
||||
libLink.messages.setModSubscriptions.send(
|
||||
this, { all: true, mod_names: [] }
|
||||
this.send(
|
||||
new libData.ModSetSubscriptionsRequest(true, [])
|
||||
).catch(err => logger.error(`Error setting mod subscriptions:\n${err.stack}`));
|
||||
libLink.messages.setUserSubscriptions.send(
|
||||
this, { all: true, names: [] }
|
||||
this.send(
|
||||
new libData.UserSetSubscriptionsRequest(true, [])
|
||||
).catch(err => logger.error(`Error setting user subscriptions:\n${err.stack}`));
|
||||
});
|
||||
|
||||
this.register(libData.AccountUpdateEvent);
|
||||
this.register(libData.HostUpdateEvent, this.handleHostUpdateEvent.bind(this));
|
||||
this.register(libData.InstanceDetailsUpdateEvent, this.handleInstanceDetailsUpdateEvent.bind(this));
|
||||
this.register(libData.InstanceSaveListUpdateEvent, this.handleInstanceSaveListUpdateEvent.bind(this));
|
||||
this.register(libData.ModUpdateEvent, this.handleModUpdateEvent.bind(this));
|
||||
this.register(libData.ModPackUpdateEvent, this.handleModPackUpdateEvent.bind(this));
|
||||
this.register(libData.UserUpdateEvent, this.handleUserUpdateEvent.bind(this));
|
||||
}
|
||||
|
||||
async prepareDisconnectRequestHandler(message, request) {
|
||||
this.connector.setClosing();
|
||||
return await super.prepareDisconnectRequestHandler(message, request);
|
||||
async handleHostUpdateEvent(event) {
|
||||
this.hostUpdates.push(event.update);
|
||||
}
|
||||
|
||||
async debugWsMessageEventHandler() { }
|
||||
|
||||
async accountUpdateEventHandler() { }
|
||||
|
||||
async hostUpdateEventHandler(message) {
|
||||
this.hostUpdates.push(message.data);
|
||||
async handleInstanceDetailsUpdateEvent(event) {
|
||||
this.instanceUpdates.push(event.details);
|
||||
}
|
||||
|
||||
async instanceUpdateEventHandler(message) {
|
||||
this.instanceUpdates.push(message.data);
|
||||
async handleInstanceSaveListUpdateEvent(event) {
|
||||
this.saveListUpdates.push(event);
|
||||
}
|
||||
|
||||
async saveListUpdateEventHandler(message) {
|
||||
this.saveListUpdates.push(message.data);
|
||||
async handleModUpdateEvent(event) {
|
||||
this.modUpdates.push(event.mod);
|
||||
}
|
||||
|
||||
async modUpdateEventHandler(message) {
|
||||
this.modUpdates.push(message.data.mod);
|
||||
async handleModPackUpdateEvent(event) {
|
||||
this.modPackUpdates.push(event.modPack);
|
||||
}
|
||||
|
||||
async modPackUpdateEventHandler(message) {
|
||||
this.modPackUpdates.push(message.data.mod_pack);
|
||||
async handleUserUpdateEvent(event) {
|
||||
this.userUpdates.push(event.user);
|
||||
}
|
||||
|
||||
async userUpdateEventHandler(message) {
|
||||
this.userUpdates.push(message.data);
|
||||
}
|
||||
|
||||
async logMessageEventHandler() { }
|
||||
}
|
||||
|
||||
class TestControlConnector extends libLink.WebSocketClientConnector {
|
||||
register() {
|
||||
this.sendHandshake("register_control", { token: this.token, agent: "clusterioctl", version: "test" });
|
||||
this.sendHandshake(new libData.MessageRegisterControl(new libData.RegisterControlData(
|
||||
this.token,
|
||||
"clusterioctl",
|
||||
"test",
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,8 +146,7 @@ async function execCtl(...args) {
|
||||
}
|
||||
|
||||
async function sendRcon(instanceId, command) {
|
||||
let response = await libLink.messages.sendRcon.send(control, { instance_id: instanceId, command });
|
||||
return response.result;
|
||||
return await control.sendTo(new libData.InstanceSendRconRequest(command), { instanceId });
|
||||
}
|
||||
|
||||
function getControl() {
|
||||
@@ -228,9 +230,9 @@ before(async function() {
|
||||
testPack.factorioVersion = "1.1.0";
|
||||
testPack.mods.set("clusterio_lib", { name: "clusterio_lib", enabled: true, version: "0.1.2" });
|
||||
testPack.mods.set("subspace_storage", { name: "subspace_storage", enabled: true, version: "1.99.8" });
|
||||
await libLink.messages.createModPack.send(control, { mod_pack: testPack.toJSON() });
|
||||
await libLink.messages.setControllerConfigField.send(
|
||||
control, { field: "controller.default_mod_pack_id", value: "12" }
|
||||
await control.sendTo(new libData.ModPackCreateRequest(testPack), "controller");
|
||||
await control.sendTo(
|
||||
new libData.ControllerConfigSetFieldRequest("controller.default_mod_pack_id", "12"), "controller"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,10 +15,17 @@ describe("Integration of lib/link", function() {
|
||||
before(async function() {
|
||||
tlsCa = await fs.readFile("test/file/tls/cert.pem");
|
||||
});
|
||||
it("should emitt an error if authentication failed", async function() {
|
||||
beforeEach(async function() {
|
||||
controlConnector = new TestControlConnector(url, 0.2, tlsCa);
|
||||
controlConnector.token = token;
|
||||
control = new TestControl(controlConnector, false);
|
||||
});
|
||||
afterEach(async function() {
|
||||
await controlConnector.disconnect();
|
||||
});
|
||||
|
||||
it("should emitt an error if authentication failed", async function() {
|
||||
controlConnector.token = "gibberish";
|
||||
control = new TestControl(controlConnector);
|
||||
await assert.rejects(
|
||||
controlConnector.connect(),
|
||||
new Error("Authentication failed: jwt malformed")
|
||||
@@ -26,28 +33,29 @@ describe("Integration of lib/link", function() {
|
||||
});
|
||||
|
||||
it("should connect with proper credentials", async function() {
|
||||
controlConnector = new TestControlConnector(url, 0.2, tlsCa);
|
||||
controlConnector.token = token;
|
||||
control = new TestControl(controlConnector);
|
||||
await controlConnector.connect();
|
||||
});
|
||||
|
||||
it("should reconnect on connection lost", async function() {
|
||||
await controlConnector.connect();
|
||||
controlConnector._socket.close(1008, "Test");
|
||||
await events.once(controlConnector, "resume");
|
||||
});
|
||||
|
||||
it("should reconnect on connection terminated", async function() {
|
||||
await controlConnector.connect();
|
||||
controlConnector._socket.terminate();
|
||||
await events.once(controlConnector, "resume");
|
||||
});
|
||||
|
||||
it("should reconnect on connection heartbeat timeout", async function() {
|
||||
await controlConnector.connect();
|
||||
controlConnector.stopHeartbeat();
|
||||
await events.once(controlConnector, "resume");
|
||||
});
|
||||
|
||||
it("should invalidate on reconnection with bad session token", async function() {
|
||||
await controlConnector.connect();
|
||||
controlConnector._sessionToken = "blah";
|
||||
controlConnector.stopHeartbeat();
|
||||
await events.once(controlConnector, "invalidate");
|
||||
@@ -55,6 +63,7 @@ describe("Integration of lib/link", function() {
|
||||
});
|
||||
|
||||
it("should properly close connector if close is called during reconnect wait", async function() {
|
||||
await controlConnector.connect();
|
||||
controlConnector._socket.close(1008, "Test");
|
||||
controlConnector.once("drop", () => {
|
||||
controlConnector.close();
|
||||
|
||||
+13
-33
@@ -2,46 +2,26 @@
|
||||
const assert = require("assert").strict;
|
||||
|
||||
const libCommand = require("@clusterio/lib/command");
|
||||
const libData = require("@clusterio/lib/data");
|
||||
const libErrors = require("@clusterio/lib/errors");
|
||||
const libLink = require("@clusterio/lib/link");
|
||||
const mock = require("../mock");
|
||||
|
||||
const addr = libData.Address.fromShorthand;
|
||||
|
||||
|
||||
describe("lib/command", function() {
|
||||
let testRole = { id: 28, name: "Test Role", description: "Test", permissions: [] };
|
||||
let mockConnector = new mock.MockConnector();
|
||||
mockConnector.on("send", function(message) {
|
||||
if (message.type === "list_hosts_request") {
|
||||
this.emit("message", {
|
||||
seq: 1, type: "list_hosts_response",
|
||||
data: {
|
||||
seq: message.seq,
|
||||
list: [{ agent: "test", version: "0.1", id: 11, name: "Test Host", connected: false }],
|
||||
},
|
||||
});
|
||||
} else if (message.type === "list_instances_request") {
|
||||
this.emit("message", {
|
||||
seq: 1, type: "list_instances_response",
|
||||
data: {
|
||||
seq: message.seq,
|
||||
list: [{ id: 57, assigned_host: 4, name: "Test Instance", status: "stopped" }],
|
||||
},
|
||||
});
|
||||
} else if (message.type === "list_roles_request") {
|
||||
this.emit("message", {
|
||||
seq: 1, type: "list_roles_response",
|
||||
data: {
|
||||
seq: message.seq,
|
||||
list: [testRole],
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
let testRole = libData.RawRole.fromJSON({ id: 28, name: "Test Role", description: "Test", permissions: [] });
|
||||
|
||||
let testControl = new mock.MockControl(mockConnector);
|
||||
libLink.messages.listHosts.attach(testControl);
|
||||
libLink.messages.listInstances.attach(testControl);
|
||||
libLink.messages.listRoles.attach(testControl);
|
||||
let loopbackConnector = new libLink.VirtualConnector(addr("controller"), addr("controller"));
|
||||
let testControl = new mock.MockControl(loopbackConnector);
|
||||
testControl.register(
|
||||
libData.HostListRequest, () => [new libData.HostDetails("test", "0.1", "Test Host", 11, false)]
|
||||
);
|
||||
testControl.register(
|
||||
libData.InstanceDetailsListRequest, () => [new libData.InstanceDetails("Test Instance", 57, 4, null, "stopped")]
|
||||
);
|
||||
testControl.register(libData.RoleListRequest, () => [testRole]);
|
||||
|
||||
describe("resolveHost", function() {
|
||||
it("should pass an integer like string back", async function() {
|
||||
|
||||
@@ -31,13 +31,13 @@ describe("lib/file_ops", function() {
|
||||
let newest = await libFileOps.getNewestFile(path.join(baseDir, "test"));
|
||||
assert.equal(typeof newest, "string");
|
||||
});
|
||||
it("returns null if all entries were filtered out", async function() {
|
||||
it("returns undefined if all entries were filtered out", async function() {
|
||||
let newest = await libFileOps.getNewestFile(path.join(baseDir, "test"), (name) => !name.endsWith(".txt"));
|
||||
assert.equal(newest, null);
|
||||
assert.equal(newest, undefined);
|
||||
});
|
||||
it("returns null if directory is empty", async function() {
|
||||
it("returns undefined if directory is empty", async function() {
|
||||
let newest = await libFileOps.getNewestFile(path.join(baseDir, "test", "folder"));
|
||||
assert.equal(newest, null);
|
||||
assert.equal(newest, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loaded 100 of 106 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user