From c40362fa7fea39c910972d9d7de0093958c448bd Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Wed, 29 Sep 2021 00:09:21 +0200 Subject: [PATCH] Add sendOrderedRcon for plugins Add BaseInstancePlugin method for sending RCON commands in order to simplify logic in plugins that depend on commands being delivered in order. --- packages/lib/plugin.js | 68 +++++++++++++++++++++++++++++++++++++++++- test/lib/plugin.js | 32 +++++++++++++++++++- test/mock.js | 16 +++++++++- 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/packages/lib/plugin.js b/packages/lib/plugin.js index 834d2c96..fa438da5 100644 --- a/packages/lib/plugin.js +++ b/packages/lib/plugin.js @@ -60,6 +60,9 @@ class BaseInstancePlugin { * `error`, `warn`, `audit`, `info` and `verbose`. */ this.logger = instance.logger.child({ plugin: this.info.name }); + + this._pendingRconMessages = []; + this._sendingRconMessages = false; } /** @@ -207,9 +210,72 @@ class BaseInstancePlugin { */ async onPlayerEvent(event) { } - async sendRcon(message, expectEmpty) { + /** + * Send RCON message to instance + * + * Send a message or command to the server which can potentially be + * executed out of order in relation to other RCON commands. + * + * This should not be called before onStart or after onStop. A simple + * way to achieve this is to check that the instance status is running + * before sending commands. Note that the instance status is set to + * running some time after `onStart` and some time before `onStop` is + * invoked. + * + * @param {string} message - message to send to server over RCON. + * @param {boolean=} [expectEmpty=false] - + * if true throw if the response is not empty. Useful for detecting + * errors that might have been sent in response. + * @returns {Promise} response from server. + */ + async sendRcon(message, expectEmpty=false) { return await this.instance.sendRcon(message, expectEmpty, this.info.name); } + + /** + * Send serially ordered RCON message to instance + * + * Send a message or command to the server which will not be executed + * out of order in relation to other RCON commands sent with this + * method. The ordering applies per plugin, and two plugins sending + * commands with this method may execute out of order in relation to + * each other. + * + * This should not be called before onStart or after onStop. A simple + * way to achieve this is to check that the instance status is running + * before sending commands. Note that the instance status is set to + * running some time after `onStart` and some time before `onStop` is + * invoked. + * + * @param {string} message - message to send to server over RCON. + * @param {boolean=} [expectEmpty=false] - + * if true throw if the response is not empty. Useful for detecting + * errors that might have been sent in response. + * @returns {Promise} response from server. + */ + async sendOrderedRcon(message, expectEmpty=false) { + let promise = new Promise((resolve, reject) => { + this._pendingRconMessages.push({resolve, reject, message, expectEmpty}); + }); + if (!this._sendingRconMessages) { + this._sendPendingRconMessages(); + } + return await promise; + } + + async _sendPendingRconMessages() { + this._sendingRconMessages = true; + while (this._pendingRconMessages.length) { + let task = this._pendingRconMessages.shift(); + try { + let result = await this.sendRcon(task.message, task.expectEmpty); + task.resolve(result); + } catch (err) { + task.reject(err); + } + } + this._sendingRconMessages = false; + } } /** diff --git a/test/lib/plugin.js b/test/lib/plugin.js index 6ea1d9ac..55159f4f 100644 --- a/test/lib/plugin.js +++ b/test/lib/plugin.js @@ -10,7 +10,7 @@ describe("lib/plugin", function() { describe("class BaseInstancePlugin", function() { let instancePlugin; it("should be constructible", async function() { - instancePlugin = new libPlugin.BaseInstancePlugin({}, { logger: new mock.MockLogger() }, {}); + instancePlugin = new libPlugin.BaseInstancePlugin({}, new mock.MockInstance(), {}); await instancePlugin.init(); }); it("should define defaults for hooks", async function() { @@ -22,6 +22,36 @@ describe("lib/plugin", function() { instancePlugin.onMasterConnectionEvent("connect"); await instancePlugin.onPrepareMasterDisconnect(); }); + describe("sendRcon", function() { + it("should send commands out of order", async function() { + instancePlugin.instance.server.rconCommandResults.set("a", { time: 100, response: "a" }); + instancePlugin.instance.server.rconCommandResults.set("b", { time: 50, response: "b" }); + + let a = instancePlugin.sendRcon("a"); + let b = instancePlugin.sendRcon("b"); + let result = await Promise.race([a, b]); + assert.equal(result, "b"); + }); + it("should propagate errors", async function() { + instancePlugin.instance.server.rconCommandResults.set("a", new Error("ref")); + await assert.rejects(instancePlugin.sendRcon("a"), new Error("ref")); + }); + }); + describe("sendOrderedRcon", function() { + it("should send commands in order", async function() { + instancePlugin.instance.server.rconCommandResults.set("a", { time: 100, response: "a" }); + instancePlugin.instance.server.rconCommandResults.set("b", { time: 50, response: "b" }); + + let a = instancePlugin.sendOrderedRcon("a"); + let b = instancePlugin.sendOrderedRcon("b"); + let result = await Promise.race([a, b]); + assert.equal(result, "a"); + }); + it("should propagate errors", async function() { + instancePlugin.instance.server.rconCommandResults.set("a", new Error("ref")); + await assert.rejects(instancePlugin.sendOrderedRcon("a"), new Error("ref")); + }); + }); }); describe("class BaseMasterPlugin", function() { diff --git a/test/mock.js b/test/mock.js index 0787c403..4eec7794 100644 --- a/test/mock.js +++ b/test/mock.js @@ -3,6 +3,7 @@ const events = require("events"); const http = require("http"); const express = require("express"); +const libHelpers = require("@clusterio/lib/helpers"); const libLink = require("@clusterio/lib/link"); const libPlugin = require("@clusterio/lib/plugin"); const libUsers = require("@clusterio/lib/users"); @@ -105,7 +106,20 @@ class MockServer extends events.EventEmitter { async sendRcon(command) { this.rconCommands.push(command); - return this.rconCommandResults.get(command) || ""; + let result = this.rconCommandResults.get(command); + if (!result) { + result = { response: "" }; + } + if (result instanceof Error || typeof result === "string") { + result = { response: result }; + } + if (result.time) { + await libHelpers.wait(result.time); + } + if (result.response instanceof Error) { + throw result.response; + } + return result.response || ""; } }