From bcd94709d28492c35581a3354c4632bafbd84f4b Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 22 Apr 2023 22:37:15 +0200 Subject: [PATCH] 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. --- .eslintrc.js | 2 +- docs/devs/json-serialisable-classes.md | 2 +- package.json | 6 +- packages/controller/controller.js | 2 +- packages/controller/src/BaseConnection.js | 74 +- packages/controller/src/ControlConnection.js | 595 ++++--- packages/controller/src/Controller.js | 268 ++- packages/controller/src/ControllerRouter.js | 123 ++ packages/controller/src/HostConnection.js | 96 +- packages/controller/src/WsServer.js | 276 +-- packages/controller/src/WsServerConnector.js | 67 +- packages/controller/src/routes.js | 20 +- packages/ctl/ctl.js | 61 +- packages/ctl/src/commands.js | 431 ++--- packages/host/host.js | 15 +- packages/host/src/Host.js | 523 +++--- packages/host/src/Instance.js | 144 +- packages/host/src/InstanceConnection.js | 60 +- packages/lib/PlayerStats.js | 4 + packages/lib/command.js | 23 +- packages/lib/data/composites.js | 32 + packages/lib/data/index.js | 14 + packages/lib/data/messages_controller.js | 384 ++++ packages/lib/data/messages_core.js | 880 +++++++++ packages/lib/data/messages_host.js | 240 +++ packages/lib/data/messages_instance.js | 1319 ++++++++++++++ packages/lib/data/messages_mod.js | 474 +++++ packages/lib/data/messages_user.js | 688 +++++++ packages/lib/errors.js | 8 +- packages/lib/file_ops.js | 4 +- packages/lib/link/connectors.js | 327 ++-- packages/lib/link/index.js | 4 +- packages/lib/link/link.js | 530 ++++-- packages/lib/link/messages.js | 1583 ----------------- packages/lib/logging.js | 16 +- packages/lib/logging_utils.js | 38 +- packages/lib/plugin.js | 44 +- packages/lib/schema.js | 214 +-- .../src/components/AssignInstanceModal.jsx | 9 +- .../src/components/ControllerConfigTree.jsx | 17 +- .../web_ui/src/components/CreateSaveModal.jsx | 13 +- .../web_ui/src/components/HostViewPage.jsx | 2 +- packages/web_ui/src/components/HostsPage.jsx | 26 +- .../src/components/InstanceConfigTree.jsx | 23 +- .../web_ui/src/components/InstanceList.jsx | 28 +- .../web_ui/src/components/InstanceRcon.jsx | 12 +- .../src/components/InstanceViewPage.jsx | 46 +- .../web_ui/src/components/InstancesPage.jsx | 14 +- .../src/components/LoadScenarioModal.jsx | 13 +- packages/web_ui/src/components/LogConsole.jsx | 21 +- .../web_ui/src/components/ModPackViewPage.jsx | 30 +- packages/web_ui/src/components/ModsPage.jsx | 20 +- .../web_ui/src/components/RoleViewPage.jsx | 19 +- packages/web_ui/src/components/RolesPage.jsx | 20 +- packages/web_ui/src/components/SavesList.jsx | 46 +- .../components/StartStopInstanceButton.jsx | 10 +- .../web_ui/src/components/UserViewPage.jsx | 124 +- packages/web_ui/src/components/UsersPage.jsx | 32 +- packages/web_ui/src/model/export_manifest.jsx | 5 +- packages/web_ui/src/model/host.jsx | 10 +- packages/web_ui/src/model/instance.jsx | 10 +- packages/web_ui/src/model/item_metadata.jsx | 1 + packages/web_ui/src/model/locale.jsx | 2 +- packages/web_ui/src/model/mod_pack.jsx | 10 +- packages/web_ui/src/model/mods.jsx | 6 +- packages/web_ui/src/model/saves.jsx | 8 +- packages/web_ui/src/model/user.jsx | 31 +- packages/web_ui/src/util/websocket.jsx | 146 +- plugins/global_chat/control.js | 7 +- plugins/global_chat/info.js | 14 - plugins/global_chat/instance.js | 11 +- plugins/global_chat/messages.js | 35 + plugins/global_chat/test/plugin.js | 5 +- plugins/inventory_sync/controller.js | 108 +- plugins/inventory_sync/info.js | 68 +- plugins/inventory_sync/instance.js | 54 +- plugins/inventory_sync/messages.js | 227 +++ plugins/inventory_sync/web/index.jsx | 10 +- plugins/player_auth/controller.js | 15 +- plugins/player_auth/info.js | 26 +- plugins/player_auth/instance.js | 11 +- plugins/player_auth/messages.js | 87 + plugins/player_auth/test/plugin.js | 71 +- plugins/research_sync/controller.js | 38 +- plugins/research_sync/info.js | 71 - plugins/research_sync/instance.js | 31 +- plugins/research_sync/messages.js | 178 ++ plugins/subspace_storage/controller.js | 72 +- plugins/subspace_storage/info.js | 67 +- plugins/subspace_storage/instance.js | 37 +- plugins/subspace_storage/messages.js | 163 ++ plugins/subspace_storage/web/index.jsx | 20 +- test/controller/routes.js | 16 +- test/host.js | 35 +- test/host/Instance.js | 99 +- test/integration/clusterio.js | 187 +- test/integration/index.js | 86 +- test/integration/link.js | 19 +- test/lib/command.js | 46 +- test/lib/file_ops.js | 8 +- test/lib/link/connector.js | 48 +- test/lib/link/link.js | 517 ++++-- test/lib/link/messages.js | 192 -- test/lib/logging_utils.js | 14 +- test/lib/plugin.js | 28 - test/mock.js | 35 +- 106 files changed, 8173 insertions(+), 4928 deletions(-) create mode 100644 packages/controller/src/ControllerRouter.js create mode 100644 packages/lib/data/composites.js create mode 100644 packages/lib/data/messages_controller.js create mode 100644 packages/lib/data/messages_core.js create mode 100644 packages/lib/data/messages_host.js create mode 100644 packages/lib/data/messages_instance.js create mode 100644 packages/lib/data/messages_mod.js create mode 100644 packages/lib/data/messages_user.js delete mode 100644 packages/lib/link/messages.js create mode 100644 plugins/global_chat/messages.js create mode 100644 plugins/inventory_sync/messages.js create mode 100644 plugins/player_auth/messages.js create mode 100644 plugins/research_sync/messages.js create mode 100644 plugins/subspace_storage/messages.js delete mode 100644 test/lib/link/messages.js diff --git a/.eslintrc.js b/.eslintrc.js index 8adba765..76d4a5b5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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 }], diff --git a/docs/devs/json-serialisable-classes.md b/docs/devs/json-serialisable-classes.md index 51101b43..ca152c2a 100644 --- a/docs/devs/json-serialisable-classes.md +++ b/docs/devs/json-serialisable-classes.md @@ -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. diff --git a/package.json b/package.json index 9e54a877..0d1e6e83 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/controller/controller.js b/packages/controller/controller.js index 6eaa943d..86f7cedd 100755 --- a/packages/controller/controller.js +++ b/packages/controller/controller.js @@ -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); }, }, ); diff --git a/packages/controller/src/BaseConnection.js b/packages/controller/src/BaseConnection.js index 3206b805..fa0e46ba 100644 --- a/packages/controller/src/BaseConnection.js +++ b/packages/controller/src/BaseConnection.js @@ -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; } } diff --git a/packages/controller/src/ControlConnection.js b/packages/controller/src/ControlConnection.js index bc8a1bde..776ae636 100644 --- a/packages/controller/src/ControlConnection.js +++ b/packages/controller/src/ControlConnection.js @@ -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; diff --git a/packages/controller/src/Controller.js b/packages/controller/src/Controller.js index 0e57db8b..53636cf1 100644 --- a/packages/controller/src/Controller.js +++ b/packages/controller/src/Controller.js @@ -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} 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); } } diff --git a/packages/controller/src/ControllerRouter.js b/packages/controller/src/ControllerRouter.js new file mode 100644 index 00000000..2b4b1f6e --- /dev/null +++ b/packages/controller/src/ControllerRouter.js @@ -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; diff --git a/packages/controller/src/HostConnection.js b/packages/controller/src/HostConnection.js index c37830a8..aa14e366 100644 --- a/packages/controller/src/HostConnection.js +++ b/packages/controller/src/HostConnection.js @@ -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, + }); } } diff --git a/packages/controller/src/WsServer.js b/packages/controller/src/WsServer.js index bad09f17..483fca5c 100644 --- a/packages/controller/src/WsServer.js +++ b/packages/controller/src/WsServer.js @@ -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} */ - this.controlConnections = []; + this.controlConnections = new Map(); /** @type {Map} */ this.hostConnections = new Map(); /** @type {Map} */ @@ -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); + } } diff --git a/packages/controller/src/WsServerConnector.js b/packages/controller/src/WsServerConnector.js index 004ef841..0b3fda8f 100644 --- a/packages/controller/src/WsServerConnector.js +++ b/packages/controller/src/WsServerConnector.js @@ -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}`); diff --git a/packages/controller/src/routes.js b/packages/controller/src/routes.js index 7392bebd..04a98bf1 100644 --- a/packages/controller/src/routes.js +++ b/packages/controller/src/routes.js @@ -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"); diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.js index 8eeb56c4..b1ee3472 100755 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.js @@ -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} */ 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 { diff --git a/packages/ctl/src/commands.js b/packages/ctl/src/commands.js index a89e702d..fe847467 100644 --- a/packages/ctl/src/commands.js +++ b/packages/ctl/src/commands.js @@ -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 "`); } 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; }, })); diff --git a/packages/host/host.js b/packages/host/host.js index a8c4ecd1..cee1f688 100755 --- a/packages/host/host.js +++ b/packages/host/host.js @@ -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, - }); + ))); } } diff --git a/packages/host/src/Host.js b/packages/host/src/Host.js index 169e1e9a..da87711a 100644 --- a/packages/host/src/Host.js +++ b/packages/host/src/Host.js @@ -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) { diff --git a/packages/host/src/Instance.js b/packages/host/src/Instance.js index 3da1c9f9..f442565f 100644 --- a/packages/host/src/Instance.js +++ b/packages/host/src/Instance.js @@ -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} 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); } /** diff --git a/packages/host/src/InstanceConnection.js b/packages/host/src/InstanceConnection.js index b7a94eb3..51e82d72 100644 --- a/packages/host/src/InstanceConnection.js +++ b/packages/host/src/InstanceConnection.js @@ -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); } } diff --git a/packages/lib/PlayerStats.js b/packages/lib/PlayerStats.js index 19e36818..c13fa1cb 100644 --- a/packages/lib/PlayerStats.js +++ b/packages/lib/PlayerStats.js @@ -62,6 +62,10 @@ class PlayerStats { } } + static fromJSON(json) { + return new this(json); + } + toJSON() { let json = {}; if (this.joinCount) { diff --git a/packages/lib/command.js b/packages/lib/command.js index f66e70c7..ad9b1bd0 100644 --- a/packages/lib/command.js +++ b/packages/lib/command.js @@ -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; diff --git a/packages/lib/data/composites.js b/packages/lib/data/composites.js new file mode 100644 index 00000000..de62d1c6 --- /dev/null +++ b/packages/lib/data/composites.js @@ -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, +}; diff --git a/packages/lib/data/index.js b/packages/lib/data/index.js index f3856763..5522a49a 100644 --- a/packages/lib/data/index.js +++ b/packages/lib/data/index.js @@ -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, }; diff --git a/packages/lib/data/messages_controller.js b/packages/lib/data/messages_controller.js new file mode 100644 index 00000000..f35b6640 --- /dev/null +++ b/packages/lib/data/messages_controller.js @@ -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} */ + hostIds; + /** @type {Array} */ + 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} */ + 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, +}; diff --git a/packages/lib/data/messages_core.js b/packages/lib/data/messages_core.js new file mode 100644 index 00000000..3028ad43 --- /dev/null +++ b/packages/lib/data/messages_core.js @@ -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} */ + 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, +}; diff --git a/packages/lib/data/messages_host.js b/packages/lib/data/messages_host.js new file mode 100644 index 00000000..40b0b855 --- /dev/null +++ b/packages/lib/data/messages_host.js @@ -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} */ + 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} */ + adminlist; + /** @type {Map} */ + banlist; + /** @type {Set} */ + 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, +}; diff --git a/packages/lib/data/messages_instance.js b/packages/lib/data/messages_instance.js new file mode 100644 index 00000000..f7ace486 --- /dev/null +++ b/packages/lib/data/messages_instance.js @@ -0,0 +1,1319 @@ +"use strict"; +const PlayerStats = require("../PlayerStats"); +const { JsonString, jsonArray } = require("./composites"); + +/** @memberof module:lib/data */ +class InstanceDetails { + /** @type {string} */ + name; + /** @type {number} */ + id; + /** @type {?integer} */ + assignedHost; + /** @type {?integer} */ + gamePort; + /** @type {string} */ + status; + + constructor(name, id, assignedHost, gamePort, status) { + this.name = name; + this.id = id; + this.assignedHost = assignedHost; + this.gamePort = gamePort; + this.status = status; + } + + static jsonSchema = { + type: "object", + required: ["name", "id", "assignedHost", "gamePort", "status"], + properties: { + "name": { type: "string" }, + "id": { type: "integer" }, + "assignedHost": { type: ["null", "integer"] }, + "gamePort": { type: ["null", "integer"] }, + "status": { enum: [ + "unknown", "unassigned", "stopped", "starting", "running", "stopping", + "creating_save", "exporting_data", "deleted", + ]}, + }, + }; + + static fromJSON(json) { + return new this(json.name, json.id, json.assignedHost, json.gamePort, json.status); + } +} + +/** @memberof module:lib/data */ +class InstanceDetailsGetRequest { + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.get"; + + /** @type {number} */ + instanceId; + + constructor(instanceId) { + this.instanceId = instanceId; + } + + static jsonSchema = { + type: "object", + required: ["instanceId"], + properties: { + "instanceId": { type: "integer" }, + }, + }; + + static fromJSON(json) { + return new this(json.instanceId); + } + + static Response = InstanceDetails; +} + +/** @memberof module:lib/data */ +class InstanceDetailsListRequest { + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.list"; + static Response = jsonArray(InstanceDetails); +}; + +/** @memberof module:lib/data */ +class InstanceDetailsSetSubscriptionsRequest { + static type = "request"; + static src = "control"; + static dst = "controller"; + static premission = "core.instance.subscribe"; + + /** @type {boolean} */ + all; + + /** @type {Array} */ + instanceIds; + + constructor(all, instanceIds) { + this.all = all || false; + this.instanceIds = instanceIds || []; + } + + static jsonSchema = { + type: "object", + properties: { + "all": { type: "boolean" }, + "instanceIds": { + type: "array", + items: { type: "integer" }, + }, + }, + }; + + static fromJSON(json) { + return new this(json.all, json.instanceIds); + } +}; + +/** @memberof module:lib/data */ +class InstanceDetailsUpdateEvent { + static type = "event"; + static src = "controller"; + static dst = "control"; + + /** @type {module:lib/data.InstanceDetails} */ + details; + + constructor(details) { + this.details = details; + } + + static jsonSchema = InstanceDetails.jsonSchema; + + toJSON() { + return this.details; + } + + static fromJSON(json) { + return new this(InstanceDetails.fromJSON(json)); + } +}; + +/** @memberof module:lib/data */ +class InstanceCreateRequest { + static type = "request"; + static src = "control"; + static dst = "controller"; + static premission = "core.instance.create"; + + /** @type {object} */ + config; + + constructor(config) { + this.config = config; + } + + static jsonSchema = { + type: "object", + required: ["config"], + properties: { + "config": { type: "object" }, + }, + }; + + static fromJSON(json) { + // TODO deserialise config here after config refactor + return new this(json.config); + } +} + +/** @memberof module:lib/data */ +class InstanceConfigGetRequest { + static type = "request"; + static src = "control"; + static dst = "controller"; + static premission = "core.instance.get_config"; + + /** @type {number} */ + instanceId; + + constructor(instanceId) { + this.instanceId = instanceId; + } + + static jsonSchema = { + required: ["instanceId"], + properties: { + "instanceId": { type: "integer" }, + }, + }; + + static fromJSON(json) { + return new this(json.instanceId); + } +} +// TODO replace with InstanceConfig after config refactor +InstanceConfigGetRequest.Response = class Response { + /** @type {object} */ + config; + + constructor(config) { + this.config = config; + } + + static jsonSchema = { + type: "object", + required: ["config"], + properties: { + "config": { type: "object" }, + }, + }; + + static fromJSON(json) { + return new this(json.config); + } +}; + +/** @memberof module:lib/data */ +class InstanceConfigSetFieldRequest { + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.update_config"; + + /** @type {number} */ + instanceId; + /** @type {string} */ + field; + /** @type {string} */ + value; + + constructor(instanceId, field, value) { + this.instanceId = instanceId; + this.field = field; + this.value = value; + } + + static jsonSchema = { + required: ["instanceId", "field", "value"], + properties: { + "instanceId": { type: "integer" }, + "field": { type: "string" }, + "value": { type: "string" }, + }, + }; + + static fromJSON(json) { + return new this(json.instanceId, json.field, json.value); + } +} + +/** @memberof module:lib/data */ +class InstanceConfigSetPropRequest { + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.update_config"; + + /** @type {number} */ + instanceId; + /** @type {string} */ + field; + /** @type {string} */ + prop; + /** @type {*} */ + value; + + constructor(instanceId, field, prop, value) { + this.instanceId = instanceId; + this.field = field; + this.prop = prop; + if (value !== undefined) { + this.value = value; + } + } + + static jsonSchema = { + required: ["instanceId", "field", "prop"], + properties: { + "instanceId": { type: "integer" }, + "field": { type: "string" }, + "prop": { type: "string" }, + "value": {}, + }, + }; + + static fromJSON(json) { + return new this(json.instanceId, json.field, json.prop, json.value); + } +} + +/** @memberof module:lib/data */ +class InstanceAssignRequest { + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.assign"; + + /** @type {number} */ + instanceId; + /** @type {?number} */ + hostId; + + constructor(instanceId, hostId) { + this.instanceId = instanceId; + this.hostId = hostId; + } + + static jsonSchema = { + required: ["instanceId", "hostId"], + properties: { + "instanceId": { type: "number" }, + "hostId": { type: ["number", "null"] }, + }, + }; + + static fromJSON(json) { + return new this(json.instanceId, json.hostId); + } +} + +/** @memberof module:lib/data */ +class InstanceMetricsRequest { + static type = "request"; + static src = "host"; + static dst = "instance"; +} +InstanceMetricsRequest.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 InstanceStartRequest { + static type = "request"; + static src = ["control", "controller"]; + static dst = "instance"; + static permission = "core.instance.start"; + + /** @type {string|undefined} */ + save; + + constructor(save) { + if (save !== undefined) { this.save = save; }; + } + + static jsonSchema = { + type: "object", + properties: { + "save": { type: "string" }, + }, + }; + + static fromJSON(json) { + return new this(json.save); + } +} + +/** @memberof module:lib/data */ +class SaveDetails { + /** @type {string} */ + type; + /** @type {string} */ + name; + /** @type {number} */ + size; + /** @type {number} */ + mtimeMs; + /** @type {boolean} */ + loaded; + /** @type {boolean} */ + loadByDefault; + + constructor(type, name, size, mtimeMs, loaded, loadByDefault) { + this.type = type; + this.name = name; + this.size = size; + this.mtimeMs = mtimeMs; + this.loaded = loaded; + this.loadByDefault = loadByDefault; + } + + static jsonSchema = { + type: "object", + required: ["type", "name", "size", "mtimeMs", "loaded"], + properties: { + "type": { enum: ["file", "directory", "special"] }, + "name": { type: "string" }, + "size": { type: "integer" }, + "mtimeMs": { type: "number" }, + "loaded": { type: "boolean" }, + "loadByDefault": { type: "boolean" }, + }, + }; + + static fromJSON(json) { + return new this(json.type, json.name, json.size, json.mtimeMs, json.loaded, json.loadByDefault); + } +} + +/** @memberof module:lib/data */ +class InstanceListSavesRequest { + static type = "request"; + static src = "control"; + static dst = "instance"; + static permission = "core.instance.save.list"; + static Response = jsonArray(SaveDetails); +} + +/** @memberof module:lib/data */ +class InstanceSetSaveListSubscriptionsRequest { + static type = "request"; + static src = "control"; + static dst = "controller"; + static premission = "core.instance.save.list_subscribe"; + + /** @type {boolean} */ + all; + + /** @type {Array} */ + instanceIds; + + constructor(all, instanceIds) { + this.all = all || false; + this.instanceIds = instanceIds || []; + } + + static jsonSchema = { + type: "object", + properties: { + "all": { type: "boolean" }, + "instanceIds": { + type: "array", + items: { type: "integer" }, + }, + }, + }; + + static fromJSON(json) { + return new this(json.all, json.instanceIds); + } +}; + +/** @memberof module:lib/data */ +class InstanceSaveListUpdateEvent { + static type = "event"; + static src = ["instance", "host"]; + static dst = ["controller", "control"]; + + /** @type {number} */ + instanceId; + /** @type {module:lib/data.SaveDetails} */ + saves; + + constructor(instanceId, saves) { + this.instanceId = instanceId; + this.saves = saves; + } + + static jsonSchema = { + type: "object", + required: ["instanceId", "saves"], + properties: { + "instanceId": { type: "integer" }, + "saves": { + type: "array", + items: SaveDetails.jsonSchema, + }, + }, + }; + + static fromJSON(json) { + return new this(json.instanceId, json.saves.map(i => SaveDetails.fromJSON(i))); + } +} + +/** @memberof module:lib/data */ +class InstanceCreateSaveRequest { + static type = "request"; + static src = "control"; + static dst = "instance"; + static permission = "core.instance.save.create"; + + /** @type {string} */ + name; + /** @type {?number} */ + seed; + /** @type {?object} */ + mapGenSettings; + /** @type {?object} */ + mapSettings; + + constructor(name, seed, mapGenSettings, mapSettings) { + this.name = name; + if (seed) { this.seed = seed; } + if (mapGenSettings) { this.mapGenSettings = mapGenSettings; } + if (mapSettings) { this.mapSettings = mapSettings; } + } + + static jsonSchema = { + type: "object", + required: ["name"], + properties: { + "name": { type: "string" }, + "seed": { type: "integer" }, + "mapGenSettings": { type: "object" }, + "mapSettings": { type: "object" }, + }, + }; + + static fromJSON(json) { + return new this(json.name, json.seed, json.mapGenSettings, json.mapSettings); + } +} + +/** @memberof module:lib/data */ +class InstanceRenameSaveRequest { + static type = "request"; + static src = "control"; + static dst = "host"; + static permission = "core.instance.save.rename"; + + /** @type {number} */ + instanceId; + /** @type {string} */ + oldName; + /** @type {string} */ + newName; + + constructor(instanceId, oldName, newName) { + this.instanceId = instanceId; + this.oldName = oldName; + this.newName = newName; + } + + static jsonSchema = { + type: "object", + required: ["instanceId", "oldName", "newName"], + properties: { + "instanceId": { type: "integer" }, + "oldName": { type: "string" }, + "newname": { type: "string" }, + }, + }; + + static fromJSON(json) { + return new this(json.instanceId, json.oldName, json.newName); + } +} + +/** @memberof module:lib/data */ +class InstanceCopySaveRequest { + static type = "request"; + static src = "control"; + static dst = "host"; + static permission = "core.instance.save.copy"; + + /** @type {number} */ + instanceId; + /** @type {string} */ + source; + /** @type {string} */ + destination; + + constructor(instanceId, source, destination) { + this.instanceId = instanceId; + this.source = source; + this.destination = destination; + } + + static jsonSchema = { + type: "object", + required: ["instanceId", "source", "destination"], + properties: { + "instanceId": { type: "integer" }, + "source": { type: "string" }, + "destination": { type: "string" }, + }, + }; + + static fromJSON(json) { + return new this(json.instanceId, json.source, json.destination); + } +} + +/** @memberof module:lib/data */ +class InstanceDeleteSaveRequest { + static type = "request"; + static src = "control"; + static dst = "host"; + static permission = "core.instance.save.delete"; + + /** @type {number} */ + instanceId; + /** @type {string} */ + name; + + constructor(instanceId, name) { + this.instanceId = instanceId; + this.name = name; + } + + static jsonSchema = { + type: "object", + required: ["instanceId", "name"], + properties: { + "instanceId": { type: "integer" }, + "name": { type: "string" }, + }, + }; + + static fromJSON(json) { + return new this(json.instanceId, json.name); + } +} + +/** @memberof module:lib/data */ +class InstanceDownloadSaveRequest { + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.save.download"; + + /** @type {number} */ + instanceId; + /** @type {string} */ + name; + + constructor(instanceId, name) { + this.instanceId = instanceId; + this.name = name; + } + + static jsonSchema = { + type: "object", + required: ["name"], + properties: { + "instanceId": { type: "integer" }, + "name": { type: "string" }, + }, + }; + + static fromJSON(json) { + return new this(json.instanceId, json.name); + } + + static Response = JsonString; +} + +/** @memberof module:lib/data */ +class InstanceTransferSaveRequest { + static type = "request"; + static src = "control"; + static dst = ["controller", "host"]; + static permission = "core.instance.save.transfer"; + + /** @type {number} */ + sourceInstanceId; + /** @type {string} */ + sourceName; + /** @type {number} */ + targetInstanceId; + /** @type {string} */ + targetName; + /** @type {boolean} */ + copy; + + constructor(sourceInstanceId, sourceName, targetInstanceId, targetName, copy) { + this.sourceInstanceId = sourceInstanceId; + this.sourceName = sourceName; + this.targetInstanceId = targetInstanceId; + this.targetName = targetName; + this.copy = copy; + } + + static jsonSchema = { + type: "object", + required: ["sourceInstanceId", "sourceName", "targetInstanceId", "targetName", "copy"], + properties: { + "sourceInstanceId": { type: "number" }, + "sourceName": { type: "string" }, + "targetInstanceId": { type: "number" }, + "targetName": { type: "string" }, + "copy": { type: "boolean" }, + }, + }; + + static fromJSON(json) { + return new this( + json.sourceInstanceId, json.sourceName, json.targetInstanceId, json.targetName, json.copy + ); + } + + static Response = JsonString; +} + + +/** @memberof module:lib/data */ +class InstancePullSaveRequest { + static type = "request"; + static src = "controller"; + static dst = "host"; + + /** @type {number} */ + instanceId; + /** @type {string} */ + streamId; + /** @type {string} */ + name; + + constructor(instanceId, streamId, name) { + this.instanceId = instanceId; + this.streamId = streamId; + this.name = name; + } + + static jsonSchema = { + type: "object", + required: ["instanceId", "streamId", "name"], + properties: { + "instanceId": { type: "integer" }, + "streamId": { type: "string" }, + "name": { type: "string" }, + }, + }; + + static fromJSON(json) { + return new this(json.instanceId, json.streamId, json.name); + } + + static Response = JsonString; +} + +/** @memberof module:lib/data */ +class InstancePushSaveRequest { + static type = "request"; + static src = "controller"; + static dst = "host"; + + /** @type {number} */ + instanceId; + /** @type {string} */ + streamId; + /** @type {string} */ + name; + + constructor(instanceId, streamId, name) { + this.instanceId = instanceId; + this.streamId = streamId; + this.name = name; + } + + static jsonSchema = { + type: "object", + required: ["instanceId", "streamId", "name"], + properties: { + "instanceId": { type: "integer" }, + "streamId": { type: "string" }, + "name": { type: "string" }, + }, + }; + + static fromJSON(json) { + return new this(json.instanceId, json.streamId, json.name); + } +} + +/** @memberof module:lib/data */ +class InstanceLoadScenarioRequest { + static type = "request"; + static src = "control"; + static dst = "instance"; + static permission = "core.instance.load_scenario"; + + /** @type {string} */ + scenario; + /** @type {?number} */ + seed; + /** @type {?object} */ + mapGenSettings; + /** @type {?object} */ + mapSettings; + + constructor(scenario, seed, mapGenSettings, mapSettings) { + this.scenario = scenario; + if (seed) { this.seed = seed; } + if (mapGenSettings) { this.mapGenSettings = mapGenSettings; } + if (mapSettings) { this.mapSettings = mapSettings; } + } + + static jsonSchema = { + type: "object", + required: ["scenario"], + properties: { + "scenario": { type: "string" }, + "seed": { type: "integer" }, + "mapGenSettings": { type: "object" }, + "mapSettings": { type: "object" }, + }, + }; + + static fromJSON(json) { + return new this(json.scenario, json.seed, json.mapGenSettings, json.mapSettings); + } +} + +/** @memberof module:lib/data */ +class InstanceExportDataRequest { + static type = "request"; + static src = "control"; + static dst = "instance"; + static permission = "core.instance.export_data"; +} + +/** @memberof module:lib/data */ +class InstanceExtractPlayersRequest { + static type = "request"; + static src = "control"; + static dst = "instance"; + static permission = "core.instance.extract_players"; +} + +/** @memberof module:lib/data */ +class InstanceStopRequest { + static type = "request"; + static src = "control"; + static dst = "instance"; + static permission = "core.instance.stop"; +} + +/** @memberof module:lib/data */ +class InstanceKillRequest { + static type = "request"; + static src = "control"; + static dst = "instance"; + static permission = "core.instance.kill"; +} + +/** @memberof module:lib/data */ +class InstanceDeleteRequest { + static type = "request"; + static src = "control"; + static dst = "controller"; + static permission = "core.instance.delete"; + + /** @type {number} */ + instanceId; + + constructor(instanceId) { + this.instanceId = instanceId; + } + + static jsonSchema = { + type: "object", + required: ["instanceId"], + properties: { + instanceId: { type: "integer" }, + }, + }; + + static fromJSON(json) { + return new this(json.instanceId); + } +} + +/** @memberof module:lib/data */ +class InstanceDeleteInternalRequest { + static type = "request"; + static src = "controller"; + static dst = "host"; + + /** @type {number} */ + instanceId; + + constructor(instanceId) { + this.instanceId = instanceId; + } + + static jsonSchema = { + type: "object", + required: ["instanceId"], + properties: { + instanceId: { type: "integer" }, + }, + }; + + static fromJSON(json) { + return new this(json.instanceId); + } +} + +/** @memberof module:lib/data */ +class InstanceSendRconRequest { + static type = "request"; + static src = "control"; + static dst = "instance"; + static permission = "core.instance.send_rcon"; + + /** @type {string} */ + command; + + constructor(command) { + this.command = command; + } + + static jsonSchema = { + type: "object", + required: ["command"], + properties: { + "command": { type: "string" }, + }, + }; + + static fromJSON(json) { + return new this(json.command); + } + + static Response = JsonString; +} + +// TODO remove this after config refactor +/** @memberof module:lib/data */ +class RawInstanceInfo { + /** @type {object} */ + config; + + /** @type {string} */ + status; + + constructor(config, status) { + this.config = config; + this.status = status; + } + + static jsonSchema = { + type: "object", + required: ["config", "status"], + properties: { + "config": { type: "object" }, + "status": { enum: [ + "stopped", "starting", "running", "stopping", "creating_save", "exporting_data", + ]}, + }, + }; + + static fromJSON(json) { + return new this(json.config, json.status); + } +} + +/** @memberof module:lib/data */ +class InstancesUpdateRequest { + static type = "request"; + static src = "host"; + static dst = "controller"; + + /** @type {Array} */ + instances; + + constructor(instances) { + this.instances = instances; + } + + static jsonSchema = { + type: "array", + items: RawInstanceInfo.jsonSchema, + }; + + toJSON() { + return this.instances; + } + + static fromJSON(json) { + return new this(json.map(i => RawInstanceInfo.fromJSON(i))); + } +} + +/** @memberof module:lib/data */ +class InstanceAssignInternalRequest { + static type = "request"; + static src = "controller"; + static dst = "host"; + + /** @type {number} */ + instanceId; + /** @type {object} */ + config; + + constructor(instanceId, config) { + this.instanceId = instanceId; + this.config = config; + } + + static jsonSchema = { + type: "object", + required: ["instanceId", "config"], + properties: { + "instanceId": { type: "integer" }, + "config": { type: "object" }, + }, + }; + + static fromJSON(json) { + // TODO deserialise config here after config refactor + return new this(json.instanceId, json.config); + } +} + +/** @memberof module:lib/data */ +class InstanceUnassignInternalRequest { + static type = "request"; + static src = "controller"; + static dst = "host"; + + /** @type {number} */ + instanceId; + + constructor(instanceId) { + this.instanceId = instanceId; + } + + static jsonSchema = { + type: "object", + required: ["instanceId"], + properties: { + "instanceId": { type: "integer" }, + }, + }; + + static fromJSON(json) { + return new this(json.instanceId); + } +} + +/** @memberof module:lib/data */ +class InstanceInitialisedEvent { + static type = "event"; + static src = "instance"; + static dst = "host"; + + /** @type {Object} */ + plugins; + + constructor(plugins) { + this.plugins = plugins; + } + + static jsonSchema = { + type: "object", + required: ["plugins"], + properties: { + "plugins": { + type: "object", + additionalProperties: { type: "string" }, + }, + }, + }; + + static fromJSON(json) { + return new this(json.plugins); + } +} + +/** @memberof module:lib/data */ +class InstanceStatusChangedEvent { + static type = "event"; + static src = "instance"; + static dst = "controller"; + + /** @type {integer} */ + instanceId; + /** @type {string} */ + status; + /** @type {?number} */ + gamePort; + + constructor(instanceId, status, gamePort) { + this.instanceId = instanceId; + this.status = status; + this.gamePort = gamePort; + } + + static jsonSchema = { + type: "object", + required: ["instanceId", "status", "gamePort"], + properties: { + "instanceId": { type: "integer" }, + "status": { + type: "string", + enum: [ + "stopped", "starting", "running", "stopping", "creating_save", "exporting_data", + ], + }, + "gamePort": { type: ["null", "integer"] }, + }, + }; + + static fromJSON(json) { + return new this(json.instanceId, json.status, json.gamePort); + } +} + +/** @memberof module:lib/data */ +class InstanceDetailsChangedEvent { + static type = "event"; + static src = "instance"; + static dst = "controller"; + + /** @type {module:lib/data.InstanceDetails} */ + details; + + constructor(details) { + this.details = details; + } + + static jsonSchema = InstanceDetails.jsonSchema; + + static fromJSON(json) { + return new this(InstanceDetails.fromJSON(json)); + } +} + +/** @memberof module:lib/data */ +class InstanceBanlistUpdateEvent { + static type = "event"; + static src = "controller"; + static dst = "instance"; + + /** @type {string} */ + name; + + /** @type {boolean} */ + banned; + + /** @type {string} */ + reason; + + constructor(name, banned, reason) { + this.name = name; + this.banned = banned; + this.reason = reason; + } + + static jsonSchema = { + required: ["name", "banned", "reason"], + properties: { + "name": { type: "string" }, + "banned": { type: "boolean" }, + "reason": { type: "string" }, + }, + }; + + static fromJSON(json) { + return new this(json.name, json.banned, json.reason); + } +} + +/** @memberof module:lib/data */ +class InstanceAdminlistUpdateEvent { + static type = "event"; + static src = "controller"; + static dst = "instance"; + + /** @type {string} */ + name; + + /** @type {boolean} */ + admin; + + constructor(name, admin) { + this.name = name; + this.admin = admin; + } + + static jsonSchema = { + required: ["name", "admin"], + properties: { + "name": { type: "string" }, + "admin": { type: "boolean" }, + }, + }; + + static fromJSON(json) { + return new this(json.name, json.admin); + } +} + +/** @memberof module:lib/data */ +class InstanceWhitelistUpdateEvent { + static type = "event"; + static src = "controller"; + static dst = "instance"; + + /** @type {string} */ + name; + + /** @type {boolean} */ + whitelisted; + + constructor(name, whitelisted) { + this.name = name; + this.whitelisted = whitelisted; + } + + static jsonSchema = { + required: ["name", "whitelisted"], + properties: { + "name": { type: "string" }, + "whitelisted": { type: "boolean" }, + }, + }; + + static fromJSON(json) { + return new this(json.name, json.whitelisted); + } +} + +/** @memberof module:lib/data */ +class InstancePlayerUpdateEvent { + static type = "event"; + static src = "instance"; + static dst = "controller"; + + /** @type {string} */ + type; + + /** @type {string} */ + name; + + /** @type {?boolean} */ + reason; + + /** @type {?string} */ + stats; + + constructor(type, name, reason, stats) { + this.type = type; + this.name = name; + if (reason !== undefined) { this.reason = reason; } + if (stats !== undefined) { this.stats = stats; } + } + + static jsonSchema = { + required: ["type", "name"], + properties: { + "type": { type: "string", enum: ["join", "leave", "import"] }, + "name": { type: "string" }, + "reason": { type: "string" }, + "stats": PlayerStats.jsonSchema, + }, + }; + + static fromJSON(json) { + return new this(json.name, json.whitelisted); + } +} + +module.exports = { + InstanceDetails, + InstanceDetailsGetRequest, + InstanceDetailsListRequest, + InstanceDetailsSetSubscriptionsRequest, + InstanceDetailsUpdateEvent, + InstanceCreateRequest, + InstanceConfigGetRequest, + InstanceConfigSetFieldRequest, + InstanceConfigSetPropRequest, + InstanceAssignRequest, + InstanceMetricsRequest, + InstanceStartRequest, + SaveDetails, + InstanceListSavesRequest, + InstanceSetSaveListSubscriptionsRequest, + InstanceSaveListUpdateEvent, + InstanceCreateSaveRequest, + InstanceRenameSaveRequest, + InstanceCopySaveRequest, + InstanceDeleteSaveRequest, + InstanceDownloadSaveRequest, + InstanceTransferSaveRequest, + InstancePullSaveRequest, + InstancePushSaveRequest, + InstanceLoadScenarioRequest, + InstanceExportDataRequest, + InstanceExtractPlayersRequest, + InstanceStopRequest, + InstanceKillRequest, + InstanceDeleteRequest, + InstanceDeleteInternalRequest, + InstanceSendRconRequest, + RawInstanceInfo, + InstancesUpdateRequest, + InstanceAssignInternalRequest, + InstanceUnassignInternalRequest, + InstanceInitialisedEvent, + InstanceStatusChangedEvent, + InstanceDetailsChangedEvent, + InstanceBanlistUpdateEvent, + InstanceAdminlistUpdateEvent, + InstanceWhitelistUpdateEvent, + InstancePlayerUpdateEvent, +}; diff --git a/packages/lib/data/messages_mod.js b/packages/lib/data/messages_mod.js new file mode 100644 index 00000000..da311226 --- /dev/null +++ b/packages/lib/data/messages_mod.js @@ -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} */ + 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} */ + queryIssues; + /** @type {number} */ + pageCount; + /** @type {number} */ + resultCount; + /** @type {Array} */ + 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} */ + 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, +}; diff --git a/packages/lib/data/messages_user.js b/packages/lib/data/messages_user.js new file mode 100644 index 00000000..c657a698 --- /dev/null +++ b/packages/lib/data/messages_user.js @@ -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} */ + 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} */ + 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} */ + 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} */ + roles; + /** @type {Array} */ + 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|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} */ + 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} */ + 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|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, +}; diff --git a/packages/lib/errors.js b/packages/lib/errors.js index 9e1d2f1c..e7de55df 100644 --- a/packages/lib/errors.js +++ b/packages/lib/errors.js @@ -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. diff --git a/packages/lib/file_ops.js b/packages/lib/file_ops.js index 2d869e41..3a19ba2f 100644 --- a/packages/lib/file_ops.js +++ b/packages/lib/file_ops.js @@ -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} + * @returns {Promise} * 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)); diff --git a/packages/lib/link/connectors.js b/packages/lib/link/connectors.js index 68f8653d..45159a2f 100644 --- a/packages/lib/link/connectors.js +++ b/packages/lib/link/connectors.js @@ -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, diff --git a/packages/lib/link/index.js b/packages/lib/link/index.js index a156979c..dcc14b7f 100644 --- a/packages/lib/link/index.js +++ b/packages/lib/link/index.js @@ -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 {}, }; diff --git a/packages/lib/link/link.js b/packages/lib/link/link.js index cb292889..f49cf253 100644 --- a/packages/lib/link/link.js +++ b/packages/lib/link/link.js @@ -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); + } } diff --git a/packages/lib/link/messages.js b/packages/lib/link/messages.js deleted file mode 100644 index d4fbc7c5..00000000 --- a/packages/lib/link/messages.js +++ /dev/null @@ -1,1583 +0,0 @@ -// Message definitions for links -"use strict"; -const libData = require("../data"); -const libErrors = require("../errors"); -const { logger } = require("../logging"); -const libSchema = require("../schema"); -const PlayerStats = require("../PlayerStats"); - -class MissingLinkHandlerError extends Error { - constructor(type, source, target) { - super(); - this.code = "MISSING_LINK_HANDLER"; - this.type = type; - this.source = source; - this.target = target; - this.handler = null; - this.plugin = null; - } - - get message() { - let handler = this.handler ? `${this.handler}` : "handler"; - let plugin = this.plugin ? ` on plugin ${this.plugin}` : ""; - - return `Missing ${handler}${plugin} for ${this.type} on ${this.source}-${this.target} link`; - } -} - -/** - * Represents a message that can be sent over the link - * - * @memberof module:lib/link - */ -class Message { - /** - * Name of the plugin this message belongs to, or null. - * @returns {?string} - plugin name or null if not from a plugin. - */ - get plugin() { - if (this._plugin === undefined) { - let index = this.type.indexOf(":"); - this._plugin = index === -1 ? null : this.type.slice(0, index); - } - - return this._plugin; - } -} - -/** - * Represents a request sent over the link - * - * A request-response message that can be innitated by either party of the - * link. - * - * @extends module:lib/link.Message - * @memberof module:lib/link - */ -class Request extends Message { - - /** - * Creates a request - * - * @param {string} type - - * message type used. Will have suffix '_request' for the request - * message and suffix '_response' for the response message. - * @param {Array} links - - * Links this request is valid on. Takes an array of strings - * containing "-" specifications. - * @param {?string} permission - - * Permission required to send this request. Only applies to - * requests sent from control to controller. - * @param {?string} forwardTo - - * Optional target to forward this request to. 'instance' add - * instance_id into the requestProperties and forward to the given - * instance. 'controller' forwards it to the controller. - * @param {?Array} requestRequired - - * List of properties that are required to be present in the data - * payload of this request. Defaults to all. - * @param {Object} requestProperties - - * Mapping of property values to JSON schema specifications for the - * properties that are valid in the data payload of this requst. - * @param {?Array} responseRequired - - * List of properties that are required to be present in the data - * payload of the repsonse to this request. Defaults to all. - * @param {Object} responseProperties - - * Mapping of property values to JSON schema specifications for the - * properties that are valid in the data payload of the response to - * this requst. - */ - constructor({ - type, permission, links, forwardTo = null, - requestRequired = null, requestProperties = {}, - responseRequired = null, responseProperties = {}, - }) { - super(); - this.type = type; - this.links = links; - this.permission = permission || null; - this.forwardTo = forwardTo; - this.handlerSuffix = "RequestHandler"; - - this.requestType = `${type}_request`; - this.responseType = `${type}_response`; - - if (permission === undefined && links.includes("control-controller")) { - throw new Error(`permission is required for ${this.type} request over control-controller links`); - } - if (permission && !links.includes("control-controller")) { - throw new Error( - `permission is not allowed on ${this.type} request as it is not over control-controller link` - ); - } - - if (!requestRequired) { - requestRequired = Object.keys(requestProperties); - } - - if (forwardTo === "instance") { - requestRequired = ["instance_id", ...requestRequired]; - requestProperties = { - "instance_id": { type: "integer" }, - ...requestProperties, - }; - - } else if (forwardTo !== "controller" && forwardTo !== null) { - throw new Error(`Invalid forwardTo value ${forwardTo}`); - } - - this._requestValidator = libSchema.compile({ - $schema: "http://json-schema.org/draft-07/schema#", - properties: { - "type": { const: this.requestType }, - "data": { - additionalProperties: false, - required: requestRequired, - properties: requestProperties, - }, - }, - }); - - if (!responseRequired) { - responseRequired = Object.keys(responseProperties); - } - - this._responseValidator = libSchema.compile({ - $schema: "http://json-schema.org/draft-07/schema#", - properties: { - "type": { const: this.responseType }, - "data": { - anyOf: [ - { - additionalProperties: false, - required: ["seq", ...responseRequired], - properties: { - "seq": { type: "integer" }, - ...responseProperties, - }, - }, - { - additionalProperties: false, - required: ["seq", "error"], - properties: { - "seq": { type: "integer" }, - "error": { type: "string" }, - }, - }, - ], - }, - }, - }); - } - - /** - * Attach to a link - * - * Set a the validator for the response on the given link if it's a - * source and sets the handler for the request if it is a target. - * - * If forwardTo was set to 'instance' the `forwardRequestToInstance` - * method on the link is used as the default handler. - * - * If forwardTo was set to 'controller' the `forwardRequesToController` method - * on the link is used as the default handler. - * - * Does nothing if the link is neither a source nor a target for this - * request. - * - * @param {Link} link - The link to attach to. - * @param {Function} handler - - * Async function to invoke with link set as this to handle the - * request request. Only used on the target side. - * @throws {Error} if the handler is needed and not defined. - */ - attach(link, handler) { - // Source side. - if (this.links.includes(`${link.source}-${link.target}`)) { - link.setValidator(this.responseType, this._responseValidator); - } - - // Target side. Note: Source and target is reversed here. - if (this.links.includes(`${link.target}-${link.source}`)) { - - // Use forwarder if handler is not present. - if (!handler) { - if (this.forwardTo === "instance") { - handler = link.forwardRequestToInstance; - } else if (this.forwardTo === "controller") { - handler = link.forwardRequestToController; - } - } - - if (!handler) { - throw new MissingLinkHandlerError(this.requestType, link.source, link.target); - } - - // Check permission if this is a handler for a control connection on the controller - if (this.permission !== null && `${link.source}-${link.target}` === "controller-control") { - let origHandler = handler; - handler = async function(message, request) { - // Abuse this binding to get a hold of the ControlConnection instance - // eslint-disable-next-line no-invalid-this, consistent-this - let controlConnection = this; - controlConnection.user.checkPermission(request.permission); - return await origHandler.call(controlConnection, message, request); - }; - } - - link.setHandler(this.requestType, message => { - handler.call(link, message, this).then(response => { - if (response === undefined) { - // XXX Should we allow implicit responses like this? - response = {}; - } - - let data = { ...response, seq: message.seq }; - if (!this._responseValidator({ seq: 0, type: this.responseType, data })) { - logger.error(JSON.stringify(this._responseValidator.errors, null, 4)); - throw new Error(`Validation failed responding to ${this.requestType}`); - } - - link.connector.send(this.responseType, data); - - }).catch(err => { - if (!(err instanceof libErrors.RequestError)) { - logger.error(`Unexpected error while responding to ${this.requestType}:\n${err.stack}`); - } - link.connector.send(this.responseType, { seq: message.seq, error: err.message }); - }); - }, this._requestValidator); - } - } - - /** - * Send request over the given link - * - * Sends the given data over the link as the request payload and waits - * for the response. - * - * @param {module:lib/link.Link} link - Link to send request over. - * @param {Object} data - Data to send with the request. - * @returns {Promise} response data - */ - async send(link, data = {}) { - // XXX validate link target/source? - if (!this._requestValidator({ seq: 0, type: this.requestType, data })) { - logger.error(JSON.stringify(this._requestValidator.errors, null, 4)); - throw new Error(`Validation failed sending ${this.requestType}`); - } - let seq = link.connector.send(this.requestType, data); - let responseMessage = await link.waitFor(this.responseType, { seq }); - if (responseMessage.data.error) { - throw new libErrors.RequestError(responseMessage.data.error); - } - return responseMessage.data; - } -} - - -let messages = {}; - -// Connection requests -let wsLinks = ["controller-control", "control-controller", "controller-host", "host-controller"]; -messages.prepareDisconnect = new Request({ - type: "prepare_disconnect", - links: wsLinks, - permission: null, -}); - -messages.prepareControllerDisconnect = new Request({ - type: "prepare_controller_disconnect", - links: ["host-instance"], -}); - -messages.ping = new Request({ - type: "ping", - links: wsLinks, - permission: null, -}); - -messages.debugDumpWs = new Request({ - type: "debug_dump_ws", - links: ["control-controller"], - permission: "core.debug.dump_ws", -}); - - -// Management requests -messages.getControllerConfig = new Request({ - type: "get_controller_config", - links: ["control-controller"], - permission: "core.controller.get_config", - responseProperties: { - "serialized_config": { type: "object" }, - }, -}); - -messages.setControllerConfigField = new Request({ - type: "set_controller_config_field", - links: ["control-controller"], - permission: "core.controller.update_config", - requestProperties: { - "field": { type: "string" }, - "value": { type: "string" }, - }, -}); - -messages.setControllerConfigProp = new Request({ - type: "set_controller_config_prop", - links: ["control-controller"], - permission: "core.controller.update_config", - requestRequired: ["field", "prop"], - requestProperties: { - "field": { type: "string" }, - "prop": { type: "string" }, - "value": {}, - }, -}); - -let hostProperties = { - "agent": { type: "string" }, - "version": { type: "string" }, - "name": { type: "string" }, - "id": { type: "integer" }, - "connected": { type: "boolean" }, - "public_address": { type: ["null", "string"] }, -}; - -messages.listHosts = new Request({ - type: "list_hosts", - links: ["control-controller"], - permission: "core.host.list", - responseProperties: { - "list": { - type: "array", - items: { - additionalProperties: false, - required: ["agent", "version", "name", "id", "connected"], - properties: hostProperties, - }, - }, - }, -}); - -messages.setHostSubscriptions = new Request({ - type: "set_host_subscriptions", - links: ["control-controller"], - permission: "core.host.subscribe", - requestProperties: { - "all": { type: "boolean" }, - "host_ids": { - type: "array", - items: { type: "integer" }, - }, - }, -}); - -let instanceProperties = { - "name": { type: "string" }, - "id": { type: "integer" }, - "assigned_host": { type: ["null", "integer"] }, - "game_port": { type: ["null", "integer"] }, - "status": { enum: [ - "unknown", "unassigned", "stopped", "starting", "running", "stopping", - "creating_save", "exporting_data", "deleted", - ]}, -}; - -messages.getInstance = new Request({ - type: "get_instance", - links: ["control-controller"], - permission: "core.instance.get", - requestProperties: { - "id": { type: "integer" }, - }, - responseProperties: instanceProperties, -}); - -messages.listInstances = new Request({ - type: "list_instances", - links: ["control-controller"], - permission: "core.instance.list", - responseProperties: { - "list": { - type: "array", - items: { - additionalProperties: false, - required: ["name", "id", "assigned_host", "status"], - properties: instanceProperties, - }, - }, - }, -}); - -messages.setInstanceSubscriptions = new Request({ - type: "set_instance_subscriptions", - links: ["control-controller"], - permission: "core.instance.subscribe", - requestProperties: { - "all": { type: "boolean" }, - "instance_ids": { - type: "array", - items: { type: "integer" }, - }, - }, -}); - -messages.generateHostToken = new Request({ - type: "generate_host_token", - links: ["control-controller"], - permission: "core.host.generate_token", - requestProperties: { - "host_id": { type: ["integer", "null"] }, - }, - responseProperties: { - "token": { type: "string" }, - }, -}); - -messages.createHostConfig = new Request({ - type: "create_host_config", - links: ["control-controller"], - permission: "core.host.create_config", - requestProperties: { - "id": { type: ["integer", "null"] }, - "name": { type: ["string", "null"] }, - "generate_token": { type: "boolean" }, - }, - responseProperties: { - "serialized_config": { type: "object" }, - }, -}); - -messages.createInstance = new Request({ - type: "create_instance", - links: ["control-controller"], - permission: "core.instance.create", - requestProperties: { - "serialized_config": { type: "object" }, - }, -}); - -messages.getInstanceConfig = new Request({ - type: "get_instance_config", - links: ["control-controller"], - permission: "core.instance.get_config", - requestProperties: { - "instance_id": { type: "integer" }, - }, - responseProperties: { - "serialized_config": { type: "object" }, - }, -}); - -messages.setInstanceConfigField = new Request({ - type: "set_instance_config_field", - links: ["control-controller"], - permission: "core.instance.update_config", - requestProperties: { - "instance_id": { type: "integer" }, - "field": { type: "string" }, - "value": { type: "string" }, - }, -}); - -messages.setInstanceConfigProp = new Request({ - type: "set_instance_config_prop", - links: ["control-controller"], - permission: "core.instance.update_config", - requestRequired: ["instance_id", "field", "prop"], - requestProperties: { - "instance_id": { type: "integer" }, - "field": { type: "string" }, - "prop": { type: "string" }, - "value": {}, - }, -}); - -messages.assignInstanceCommand = new Request({ - type: "assign_instance_command", - links: ["control-controller"], - permission: "core.instance.assign", - requestProperties: { - "instance_id": { type: "integer" }, - "host_id": { type: ["integer", "null"] }, - }, -}); - -messages.startInstance = new Request({ - type: "start_instance", - links: ["control-controller", "controller-host", "host-instance"], - permission: "core.instance.start", - requestProperties: { - "save": { type: ["string", "null"] }, - }, - forwardTo: "instance", -}); - -let saveList = { - type: "array", - items: { - type: "object", - additionalProperties: false, - required: ["type", "name", "size", "mtime_ms", "loaded"], - properties: { - "type": { enum: ["file", "directory", "special"] }, - "name": { type: "string" }, - "size": { type: "integer" }, - "mtime_ms": { type: "number" }, - "loaded": { type: "boolean" }, - "default": { type: "boolean" }, - }, - }, -}; - -messages.listSaves = new Request({ - type: "list_saves", - links: ["control-controller", "controller-host", "host-instance"], - permission: "core.instance.save.list", - forwardTo: "instance", - responseProperties: { - "list": saveList, - }, -}); - -messages.createSave = new Request({ - type: "create_save", - links: ["control-controller", "controller-host", "host-instance"], - permission: "core.instance.save.create", - forwardTo: "instance", - requestProperties: { - "name": { type: "string" }, - "seed": { type: ["integer", "null"] }, - "map_gen_settings": { type: ["object", "null"] }, - "map_settings": { type: ["object", "null"] }, - }, -}); - -messages.renameSave = new Request({ - type: "rename_save", - links: ["control-controller", "controller-host"], - permission: "core.instance.save.rename", - forwardTo: "instance", - requestProperties: { - "old_name": { type: "string" }, - "new_name": { type: "string" }, - }, -}); - -messages.copySave = new Request({ - type: "copy_save", - links: ["control-controller", "controller-host"], - permission: "core.instance.save.copy", - forwardTo: "instance", - requestProperties: { - "source": { type: "string" }, - "destination": { type: "string" }, - }, -}); - -messages.deleteSave = new Request({ - type: "delete_save", - links: ["control-controller", "controller-host"], - permission: "core.instance.save.delete", - forwardTo: "instance", - requestProperties: { - "save": { type: "string" }, - }, -}); - -messages.downloadSave = new Request({ - type: "download_save", - links: ["control-controller"], - permission: "core.instance.save.download", - requestProperties: { - "instance_id": { type: "integer" }, - "save": { type: "string" }, - }, - responseProperties: { - "stream_id": { type: "string" }, - }, -}); - -messages.transferSave = new Request({ - type: "transfer_save", - links: ["control-controller", "controller-host"], - permission: "core.instance.save.transfer", - requestProperties: { - "instance_id": { type: "integer" }, - "source_save": { type: "string" }, - "target_instance_id": { type: "integer" }, - "target_save": { type: "string" }, - "copy": { type: "boolean" }, - }, - responseProperties: { - "save": { type: "string" }, - }, -}); - -messages.pullSave = new Request({ - type: "pull_save", - links: ["controller-host"], - requestProperties: { - "instance_id": { type: "integer" }, - "stream_id": { type: "string" }, - "filename": { type: "string" }, - }, - responseProperties: { - "save": { type: "string" }, - }, -}); - -messages.pushSave = new Request({ - type: "push_save", - links: ["controller-host"], - requestProperties: { - "instance_id": { type: "integer" }, - "stream_id": { type: "string" }, - "save": { type: "string" }, - }, -}); - -messages.setSaveListSubscriptions = new Request({ - type: "set_save_list_subscriptions", - links: ["control-controller"], - permission: "core.instance.save.list_subscribe", - requestProperties: { - "all": { type: "boolean" }, - "instance_ids": { - type: "array", - items: { type: "integer" }, - }, - }, -}); - -messages.loadScenario = new Request({ - type: "load_scenario", - links: ["control-controller", "controller-host", "host-instance"], - permission: "core.instance.load_scenario", - forwardTo: "instance", - requestProperties: { - "scenario": { type: "string" }, - "seed": { type: ["integer", "null"] }, - "map_gen_settings": { type: ["object", "null"] }, - "map_settings": { type: ["object", "null"] }, - }, -}); - -messages.exportData = new Request({ - type: "export_data", - links: ["control-controller", "controller-host", "host-instance"], - permission: "core.instance.export_data", - forwardTo: "instance", -}); - -messages.extractPlayers = new Request({ - type: "extract_players", - links: ["control-controller", "controller-host", "host-instance"], - permission: "core.instance.extract_players", - forwardTo: "instance", -}); - -messages.stopInstance = new Request({ - type: "stop_instance", - links: ["control-controller", "controller-host", "host-instance"], - permission: "core.instance.stop", - forwardTo: "instance", -}); - -messages.killInstance = new Request({ - type: "kill_instance", - links: ["control-controller", "controller-host", "host-instance"], - permission: "core.instance.kill", - forwardTo: "instance", -}); - -messages.deleteInstance = new Request({ - type: "delete_instance", - links: ["control-controller", "controller-host"], - permission: "core.instance.delete", - forwardTo: "instance", -}); - -messages.sendRcon = new Request({ - type: "send_rcon", - links: ["control-controller", "controller-host", "host-instance"], - permission: "core.instance.send_rcon", - forwardTo: "instance", - requestProperties: { - "command": { type: "string" }, - }, - responseProperties: { - "result": { type: "string" }, - }, -}); - -messages.getModPack = new Request({ - type: "get_mod_pack", - links: ["control-controller", "instance-host", "host-controller"], - permission: "core.mod_pack.get", - forwardTo: "controller", - requestProperties: { - "id": { type: "integer" }, - }, - responseProperties: { - "mod_pack": libData.ModPack.jsonSchema, - }, -}); - -messages.getDefaultModPack = new Request({ - type: "get_default_mod_pack", - links: ["control-controller", "instance-host", "host-controller"], - permission: "core.mod_pack.get", - forwardTo: "controller", - responseProperties: { - "mod_pack": libData.ModPack.jsonSchema, - }, -}); - -messages.listModPacks = new Request({ - type: "list_mod_packs", - links: ["control-controller"], - permission: "core.mod_pack.list", - responseProperties: { - "list": { - type: "array", - items: libData.ModPack.jsonSchema, - }, - }, -}); - -messages.createModPack = new Request({ - type: "create_mod_pack", - links: ["control-controller"], - permission: "core.mod_pack.create", - requestProperties: { - "mod_pack": libData.ModPack.jsonSchema, - }, -}); - -messages.updateModPack = new Request({ - type: "update_mod_pack", - links: ["control-controller"], - permission: "core.mod_pack.update", - requestProperties: { - "mod_pack": libData.ModPack.jsonSchema, - }, -}); - -messages.deleteModPack = new Request({ - type: "delete_mod_pack", - links: ["control-controller"], - permission: "core.mod_pack.delete", - requestProperties: { - "id": { type: "integer" }, - }, -}); - -messages.setModPackSubscriptions = new Request({ - type: "set_mod_pack_subscriptions", - links: ["control-controller"], - permission: "core.mod_pack.subscribe", - requestProperties: { - "all": { type: "boolean" }, - "mod_pack_ids": { - type: "array", - items: { type: "integer" }, - }, - }, -}); - -messages.getMod = new Request({ - type: "get_mod", - links: ["control-controller"], - permission: "core.mod.get", - requestProperties: { - "name": { type: "string" }, - "version": { type: "string" }, - }, - responseProperties: { - "mod": libData.ModInfo.jsonSchema, - }, -}); - -messages.listMods = new Request({ - type: "list_mods", - links: ["control-controller"], - permission: "core.mod.list", - responseProperties: { - "list": { - type: "array", - items: libData.ModInfo.jsonSchema, - }, - }, -}); - -messages.searchMods = new Request({ - type: "search_mods", - links: ["control-controller"], - permission: "core.mod.search", - requestRequired: ["query", "factorio_version", "page"], - requestProperties: { - "query": { type: "string" }, - "factorio_version": { type: "string" }, - "page_size": { type: "integer" }, - "page": { type: "integer" }, - "sort": { type: "string" }, - "sort_order": { type: "string" }, - }, - responseProperties: { - "query_issues": { - type: "array", - items: { type: "string" }, - }, - "page_count": { type: "integer" }, - "result_count": { type: "integer" }, - "results": { - type: "array", - items: { - type: "object", - properties: { - "name": { type: "string" }, - "versions": { - type: "array", - items: libData.ModInfo.jsonSchema, - }, - }, - }, - }, - }, -}); - -messages.setModSubscriptions = new Request({ - type: "set_mod_subscriptions", - links: ["control-controller"], - permission: "core.mod.subscribe", - requestProperties: { - "all": { type: "boolean" }, - "mod_names": { - type: "array", - items: { type: "string" }, - }, - }, -}); - -messages.downloadMod = new Request({ - type: "download_mod", - links: ["control-controller"], - permission: "core.mod.download", - requestProperties: { - "name": { type: "string" }, - "version": { type: "string" }, - }, - responseProperties: { - "stream_id": { type: "string" }, - }, -}); - -messages.deleteMod = new Request({ - type: "delete_mod", - links: ["control-controller"], - permission: "core.mod.delete", - requestProperties: { - "name": { type: "string" }, - "version": { type: "string" }, - }, -}); - -messages.listPermissions = new Request({ - type: "list_permissions", - links: ["control-controller"], - permission: "core.permission.list", - responseProperties: { - "list": { - type: "array", - items: { - additionalProperties: false, - required: ["name", "title", "description"], - properties: { - "name": { type: "string" }, - "title": { type: "string" }, - "description": { type: "string" }, - }, - }, - }, - }, -}); - -messages.listRoles = new Request({ - type: "list_roles", - links: ["control-controller"], - permission: "core.role.list", - responseProperties: { - "list": { - type: "array", - items: { - additionalProperties: false, - required: ["id", "name", "description", "permissions"], - properties: { - "id": { type: "integer" }, - "name": { type: "string" }, - "description": { type: "string" }, - "permissions": { type: "array", items: { type: "string" }}, - }, - }, - }, - }, -}); - -messages.createRole = new Request({ - type: "create_role", - links: ["control-controller"], - permission: "core.role.create", - requestProperties: { - "name": { type: "string" }, - "description": { type: "string" }, - "permissions": { type: "array", items: { type: "string" }}, - }, - responseProperties: { - "id": { type: "integer" }, - }, -}); - -messages.updateRole = new Request({ - type: "update_role", - links: ["control-controller"], - permission: "core.role.update", - requestProperties: { - "id": { type: "integer" }, - "name": { type: "string" }, - "description": { type: "string" }, - "permissions": { type: "array", items: { type: "string" }}, - }, -}); - -messages.grantDefaultRolePermissions = new Request({ - type: "grant_default_role_permissions", - links: ["control-controller"], - permission: "core.role.update", - requestProperties: { - "id": { type: "integer" }, - }, -}); - -messages.deleteRole = new Request({ - type: "delete_role", - links: ["control-controller"], - permission: "core.role.delete", - requestProperties: { - "id": { type: "integer" }, - }, -}); - -const userRequired = ["name", "roles", "instances"]; -const userProperties = { - "name": { type: "string" }, - "roles": { type: "array", items: { type: "integer" }}, - "is_admin": { type: "boolean" }, - "is_banned": { type: "boolean" }, - "is_whitelisted": { type: "boolean" }, - "ban_reason": { type: "string" }, - "instances": { type: "array", items: { type: "integer" }}, - "is_deleted": { type: "boolean" }, - "player_stats": PlayerStats.jsonSchema, - "instance_stats": { - type: "array", - items: { - type: "array", - items: [{ type: "integer" }, PlayerStats.jsonSchema], - }, - }, -}; - -messages.getUser = new Request({ - type: "get_user", - links: ["control-controller"], - permission: "core.user.get", - requestProperties: { - "name": { type: "string" }, - }, - responseRequired: userRequired, - responseProperties: userProperties, -}); - -messages.listUsers = new Request({ - type: "list_users", - links: ["control-controller"], - permission: "core.user.list", - responseProperties: { - "list": { - type: "array", - items: { - additionalProperties: false, - required: userRequired, - properties: userProperties, - }, - }, - }, -}); - -messages.setUserSubscriptions = new Request({ - type: "set_user_subscriptions", - links: ["control-controller"], - permission: "core.user.subscribe", - requestProperties: { - "all": { type: "boolean" }, - "names": { - type: "array", - items: { type: "string" }, - }, - }, -}); - -messages.createUser = new Request({ - type: "create_user", - links: ["control-controller"], - permission: "core.user.create", - requestProperties: { - "name": { type: "string" }, - }, -}); - -messages.revokeUserToken = new Request({ - type: "revoke_user_token", - links: ["control-controller"], - permission: "core.user.revoke_token", - requestProperties: { - "name": { type: "string" }, - }, -}); - -messages.updateUserRoles = new Request({ - type: "update_user_roles", - links: ["control-controller"], - permission: "core.user.update_roles", - requestProperties: { - "name": { type: "string" }, - "roles": { type: "array", items: { type: "integer" }}, - }, -}); - -messages.setUserAdmin = new Request({ - type: "set_user_admin", - links: ["control-controller"], - permission: "core.user.set_admin", - requestProperties: { - "name": { type: "string" }, - "create": { type: "boolean" }, - "admin": { type: "boolean" }, - }, -}); - -messages.setUserWhitelisted = new Request({ - type: "set_user_whitelisted", - links: ["control-controller"], - permission: "core.user.set_whitelisted", - requestProperties: { - "name": { type: "string" }, - "create": { type: "boolean" }, - "whitelisted": { type: "boolean" }, - }, -}); - -messages.setUserBanned = new Request({ - type: "set_user_banned", - links: ["control-controller"], - permission: "core.user.set_banned", - requestProperties: { - "name": { type: "string" }, - "create": { type: "boolean" }, - "banned": { type: "boolean" }, - "reason": { type: "string" }, - }, -}); - -messages.deleteUser = new Request({ - type: "delete_user", - links: ["control-controller"], - permission: "core.user.delete", - requestProperties: { - "name": { type: "string" }, - }, -}); - -messages.setLogSubscriptions = new Request({ - type: "set_log_subscriptions", - links: ["control-controller"], - permission: "core.log.follow", - requestProperties: { - "all": { type: "boolean" }, - "controller": { type: "boolean" }, - "host_ids": { - type: "array", - items: { type: "integer" }, - }, - "instance_ids": { - type: "array", - items: { type: "integer" }, - }, - "max_level": { type: ["string", "null"] }, - }, -}); - -messages.queryLog = new Request({ - type: "query_log", - links: ["control-controller"], - permission: "core.log.query", - requestProperties: { - "all": { type: "boolean" }, - "controller": { type: "boolean" }, - "host_ids": { - type: "array", - items: { type: "integer" }, - }, - "instance_ids": { - type: "array", - items: { type: "integer" }, - }, - "max_level": { type: ["string", "null"] }, - "limit": { type: "integer" }, - "order": { enum: ["asc", "desc"] }, - }, - responseProperties: { - "log": { - type: "array", - items: { type: "object" }, - }, - }, -}); - - -// Internal requests -messages.updateInstances = new Request({ - type: "update_instances", - links: ["host-controller"], - requestProperties: { - "instances": { - type: "array", - items: { - type: "object", - additionalProperties: false, - required: ["serialized_config", "status"], - properties: { - "serialized_config": { type: "object" }, - "status": { enum: [ - "stopped", "starting", "running", "stopping", "creating_save", "exporting_data", - ]}, - }, - }, - }, - }, -}); - -messages.assignInstance = new Request({ - type: "assign_instance", - links: ["controller-host"], - requestProperties: { - "serialized_config": { type: "object" }, - }, - forwardTo: "instance", -}); - -messages.unassignInstance = new Request({ - type: "unassigne_instance", - links: ["controller-host"], - forwardTo: "instance", -}); - -messages.getMetrics = new Request({ - type: "get_metrics", - links: ["controller-host", "host-instance"], - responseProperties: { - "results": { type: "array" }, - }, -}); - - -/** - * Represents an event sent over the link - * - * A one way message without any response or recipt confirmation that - * can be innitiated by either party of the link. - * - * @extends module:lib/link.Message - * @memberof module:lib/link - */ -class Event extends Message { - - /** - * Creates an event - * - * @param {string} type - - * message type used. Will have suffix '_event' appended to it for - * the messages sent through this event. - * @param {Array} links - - * Links this event is valid on. Takes an array of strings - * containing "-" specifications. - * @param {?string} forwardTo - - * Optional target to forward this request to. Supported values are - * 'controller' and 'instance'. If set to 'instance' an instance_id - * property is implicity added to eventProperties. - * @param {?string} broadcastTo - - * Optional target to broadcast this event to. Only 'instance' is - * currently supported. An event will only be broadcast towards - * links that are downstream, so sending an event set to instance - * broadcast that's sent to a host will only be broadcast to that - * host's instances. - * @param {?Array} eventRequired - - * List of properties required to be present in the event payload. - * Defaults to all. - * @param {Object} eventProperties - - * Mapping of property values to JSON schema specifications for the - * properties that are valid in the data payload of this event. - */ - constructor({ type, links, forwardTo = null, broadcastTo = null, eventRequired = null, eventProperties = {} }) { - super(); - this.type = type; - this.links = links; - this.forwardTo = forwardTo; - this.broadcastTo = broadcastTo; - this.handlerSuffix = "EventHandler"; - - if (!eventRequired) { - eventRequired = Object.keys(eventProperties); - } - - if (forwardTo === "instance") { - eventRequired = ["instance_id", ...eventRequired]; - eventProperties = { - "instance_id": { type: "integer" }, - ...eventProperties, - }; - - } else if (forwardTo !== "controller" && forwardTo !== null) { - throw new Error(`Invalid forwardTo value ${forwardTo}`); - } - - if (broadcastTo !== "instance" && broadcastTo !== null) { - throw new Error(`Invalid broadcastTo value ${broadcastTo}`); - } - - this.eventType = `${type}_event`; - this._eventValidator = libSchema.compile({ - $schema: "http://json-schema.org/draft-07/schema#", - properties: { - "type": { const: this.eventType }, - "data": { - additionalProperties: false, - required: eventRequired, - properties: eventProperties, - }, - }, - }); - } - - /** - * Attach to a link - * - * Set a the handler for the event on the given link if it's a target - * for the event. - * - * If forwardTo was set to 'controller' the `forwardEventToController` method on - * the link is used as the default handler. If it was set to 'instance' - * the `forwardEventToInstance` method on the link is used by default. - * - * If broadcastTo was set to 'instance' the `broadcastEventToInstance` - * method on the link is invoked before the handler. - * - * Does nothing if the link is not a target for this event. - * - * @param {Link} link - The link to attach to. - * @param {Function} handler - - * Async function to invoke with link set as this to handle the - * event. Only used on the target side. - * @throws {Error} if the handler is needed and not defined. - */ - attach(link, handler) { - // Target side. Note: Source and target is reversed here. - if (this.links.includes(`${link.target}-${link.source}`)) { - // Use forwarder if handler is not present. - if (!handler) { - if (this.forwardTo === "instance") { - handler = link.forwardEventToInstance; - } else if (this.forwardTo === "controller") { - handler = link.forwardEventToController; - } - } - - if ( - this.broadcastTo === "instance" - && [ - "instance-host", "host-controller", "control-controller", "controller-host", - ].includes(`${link.target}-${link.source}`) - ) { - if (!handler) { - handler = link.broadcastEventToInstance; - } else { - let originalHandler = handler; - handler = async (message, event) => { - await link.broadcastEventToInstance(message, event); - await originalHandler.call(link, message, event); - }; - } - } - - if (!handler) { - throw new MissingLinkHandlerError(this.eventType, link.source, link.target); - } - - link.setHandler(this.eventType, message => { - // XXX Should event handlers even be allowed to be async? - handler.call(link, message, this).catch(err => { - logger.error(`Unexpected error while handling ${this.eventType}:\n${err.stack}`); - }); - }, this._eventValidator); - } - } - - /** - * Send event over the given link - * - * Sends the given event data over the link. - * - * @param {module:lib/link.Link} link - Link to send event over. - * @param {Object} data - Data to send with the event. - */ - send(link, data = {}) { - if (!this._eventValidator({ seq: 0, type: this.eventType, data })) { - logger.error(JSON.stringify(this._eventValidator.errors, null, 4)); - throw new Error(`Validation failed sending ${this.eventType}`); - } - - link.connector.send(this.eventType, data); - } -} - -messages.debugWsMessage = new Event({ - type: "debug_ws_message", - links: ["controller-control"], - eventProperties: { - "direction": { type: "string" }, - "content": { type: "string" }, - }, -}); - -messages.accountUpdate = new Event({ - type: "account_update", - links: ["controller-control"], - eventRequired: [], - eventProperties: { - "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" }, - }, - }, - }, - }, - }, -}); - -messages.logMessage = new Event({ - type: "log_message", - links: ["host-controller", "controller-control"], - eventProperties: { - "info": { - type: "object", - required: ["level", "message"], - properties: { - "level": { type: "string" }, - "message": { type: "string" }, - }, - }, - }, -}); - -messages.hostUpdate = new Event({ - type: "host_update", - links: ["controller-control"], - eventProperties: hostProperties, -}); - - -messages.instanceInitialized = new Event({ - type: "instance_initialized", - links: ["instance-host"], - eventProperties: { - "instance_id": { type: "integer" }, - "plugins": { - type: "object", - additionalProperties: { type: "string" }, - }, - }, -}); -messages.instanceStatusChanged = new Event({ - type: "instance_status_changed", - links: ["instance-host", "host-controller"], - eventRequired: ["instance_id", "status"], - eventProperties: { - "instance_id": { type: "integer" }, - "status": { enum: [ - "stopped", "starting", "running", "stopping", "creating_save", "exporting_data", - ]}, - "game_port": { type: ["null", "integer"] }, - }, -}); - -messages.instanceUpdate = new Event({ - type: "instance_update", - links: ["controller-control"], - eventProperties: instanceProperties, -}); - -messages.saveListUpdate = new Event({ - type: "save_list_update", - links: ["instance-host", "host-controller", "controller-control"], - forwardTo: "controller", - eventProperties: { - "instance_id": { type: "integer" }, - "list": saveList, - }, -}); - -messages.modPackUpdate = new Event({ - type: "mod_pack_update", - links: ["controller-control"], - eventProperties: { - "mod_pack": libData.ModPack.jsonSchema, - }, -}); - -messages.modUpdate = new Event({ - type: "mod_update", - links: ["controller-control"], - eventProperties: { - "mod": libData.ModInfo.jsonSchema, - }, -}); - -messages.userUpdate = new Event({ - type: "user_update", - links: ["controller-control"], - eventRequired: userRequired, - eventProperties: userProperties, -}); - -messages.controllerConnectionEvent = new Event({ - type: "controller_connection_event", - links: ["host-instance"], - eventProperties: { - "event": { type: "string" }, - }, -}); - -messages.syncUserLists = new Event({ - type: "sync_user_lists", - links: ["controller-host"], - eventProperties: { - "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" }, - }, - }, -}); - -messages.banlistUpdate = new Event({ - type: "banlist_update", - links: ["controller-host", "host-instance"], - broadcastTo: "instance", - eventProperties: { - "name": { type: "string" }, - "banned": { type: "boolean" }, - "reason": { type: "string" }, - }, -}); - -messages.adminlistUpdate = new Event({ - type: "adminlist_update", - links: ["controller-host", "host-instance"], - eventProperties: { - "name": { type: "string" }, - "admin": { type: "boolean" }, - }, - broadcastTo: "instance", -}); - -messages.whitelistUpdate = new Event({ - type: "whitelist_update", - links: ["controller-host", "host-instance"], - eventProperties: { - "name": { type: "string" }, - "whitelisted": { type: "boolean" }, - }, - broadcastTo: "instance", -}); - -messages.playerEvent = new Event({ - type: "player_event", - links: ["instance-host", "host-controller"], - forwardTo: "controller", - eventRequired: ["instance_id", "type", "name"], - eventProperties: { - "instance_id": { type: "integer" }, - "type": { type: "string", enum: ["join", "leave", "import"] }, - "name": { type: "string" }, - "reason": { type: "string" }, - "stats": PlayerStats.jsonSchema, - }, -}); - -/** - * Attaches all requests and events to the given link - * - * Loops through all builtin messages defined and attaches all of them - * to the link. The handler used for the messageis looked up on the - * link instance as the concatenation of the name of the message, the - * name of message class, and 'Handler'. - * - * @param {module:lib/link.Link} link - Link to attach messages to. - * @memberof module:lib/link - */ -function attachAllMessages(link) { - for (let [name, message] of Object.entries(messages)) { - let handler = `${name}${message.handlerSuffix}`; - try { - message.attach(link, link[handler]); - } catch (err) { - if (err.code === "MISSING_LINK_HANDLER") { - err.handler = handler; - } - throw err; - } - } -} - - -module.exports = { - Message, - Request, - Event, - - attachAllMessages, - - messages, -}; diff --git a/packages/lib/logging.js b/packages/lib/logging.js index 94135133..56c9a729 100644 --- a/packages/lib/logging.js +++ b/packages/lib/logging.js @@ -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} [host_ids] - + * @property {Array} [hostIds] - * Include log entries for the given hosts and instances of those * hosts by id. - * @property {Array} [instance_ids] - + * @property {Array} [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; diff --git a/packages/lib/logging_utils.js b/packages/lib/logging_utils.js index be604fd6..dea8baee 100644 --- a/packages/lib/logging_utils.js +++ b/packages/lib/logging_utils.js @@ -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} [host_ids] - + * @property {Array} [hostIds] - * Include log entries for the given hosts and instances of those * hosts by id. - * @property {Array} [instance_ids] - + * @property {Array} [instanceIds] - * Include log entries for the given instances by id. */ diff --git a/packages/lib/plugin.js b/packages/lib/plugin.js index fe627a86..eebfd072 100644 --- a/packages/lib/plugin.js +++ b/packages/lib/plugin.js @@ -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, }; diff --git a/packages/lib/schema.js b/packages/lib/schema.js index 572b7846..a9e362c7 100644 --- a/packages/lib/schema.js +++ b/packages/lib/schema.js @@ -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, }; diff --git a/packages/web_ui/src/components/AssignInstanceModal.jsx b/packages/web_ui/src/components/AssignInstanceModal.jsx index 97f49576..f64edfbc 100644 --- a/packages/web_ui/src/components/AssignInstanceModal.jsx +++ b/packages/web_ui/src/components/AssignInstanceModal.jsx @@ -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(); diff --git a/packages/web_ui/src/components/ControllerConfigTree.jsx b/packages/web_ui/src/components/ControllerConfigTree.jsx index 5197f8c6..40dd7947 100644 --- a/packages/web_ui/src/components/ControllerConfigTree.jsx +++ b/packages/web_ui/src/components/ControllerConfigTree.jsx @@ -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 { + control.sendTo( + new libData.InstanceCreateSaveRequest(name, seed, mapGenSettings, mapSettings), + { instanceId: props.instance.id } + ).then(() => { form.resetFields(); setVisible(false); }).catch( diff --git a/packages/web_ui/src/components/HostViewPage.jsx b/packages/web_ui/src/components/HostViewPage.jsx index 23589ef0..51f1db69 100644 --- a/packages/web_ui/src/components/HostViewPage.jsx +++ b/packages/web_ui/src/components/HostViewPage.jsx @@ -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) { diff --git a/packages/web_ui/src/components/HostsPage.jsx b/packages/web_ui/src/components/HostsPage.jsx index f941602a..afb8c2dd 100644 --- a/packages/web_ui/src/components/HostsPage.jsx +++ b/packages/web_ui/src/components/HostsPage.jsx @@ -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 => - {host["connected"] ? "Connected" : "Disconnected"} + {host.connected ? "Connected" : "Disconnected"} , - 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 => { diff --git a/packages/web_ui/src/components/InstanceConfigTree.jsx b/packages/web_ui/src/components/InstanceConfigTree.jsx index a2d05a71..74afaa31 100644 --- a/packages/web_ui/src/components/InstanceConfigTree.jsx +++ b/packages/web_ui/src/components/InstanceConfigTree.jsx @@ -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 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} :