Refactor instance communication

Change Instance class to be a subclass of Link and refactor the
communication to and establisment of instances to use a virtual
connection between the slave and the instance.  This is simplifies the
implementation of plugin communication for instances.

Replace InstanceRequest with a forwardTo option to Request which does
the task of forwarding in a much cleaner way.
This commit is contained in:
Hornwitser committed 2019-12-13 21:57:05 +01:00
1 parent 9f24d200b7
commit 5d8fa4e4ce
7 files changed
+381 -265

No files matched your search

+209 -130
View File
@@ -18,6 +18,7 @@ const hashFile = require('lib/hash').hashFile;
const factorio = require("lib/factorio");
const schema = require("lib/schema");
const link = require("lib/link");
const errors = require("lib/errors");
// Uhm...
var global = {};
@@ -25,8 +26,10 @@ var global = {};
/**
* Keeps track of the runtime parameters of an instance
*/
class Instance {
constructor(dir, factorioDir, instanceConfig) {
class Instance extends link.Link{
constructor(connector, dir, factorioDir, instanceConfig) {
super('instance', 'slave', connector);
link.attachAllMessages(this);
this._dir = dir;
// This is expected to change with the config system rewrite
@@ -47,12 +50,29 @@ class Instance {
this.server = new factorio.FactorioServer(
path.join(factorioDir, "data"), this._dir, serverOptions
);
this.server.on('output', (output) => {
link.messages.instanceOutput.send(this, { instance_id: this.config.id, output })
});
}
async init() {
await this.server.init();
}
/**
* Creates a new empty instance directory
*
* Creates the neccessary files for starting up a new instance into the
* provided instance directory.
*
* @param {Number} id -
* ID of the new instance. Must be unique to the cluster.
* @param {String} instanceDir -
* Directory to create the new instance into.
* @param {String} factorioDir - Path to factorio installation.
* @param {Object} options - Options for new instance.
*/
static async create(id, instanceDir, factorioDir, options) {
let instanceConfig = {
id,
@@ -62,23 +82,21 @@ class Instance {
clientPassword: null,
}
let instance = new this(instanceDir, factorioDir, instanceConfig);
await instance.init();
console.log(`Creating ${instance.path()}`);
await fs.ensureDir(instance.path());
await fs.ensureDir(instance.path("script-output"));
console.log(`Creating ${instanceDir}`);
await fs.ensureDir(instanceDir);
await fs.ensureDir(path.join(instanceDir, "script-output"));
await fs.ensureDir(path.join(instanceDir, "saves"));
await symlinkMods(instance, "sharedMods", console);
console.log("Clusterio | Created instance with settings:")
console.log(instanceConfig);
// save instance config
await fs.outputFile(instance.path("config.json"), JSON.stringify(instanceConfig, null, 4));
await fs.outputFile(path.join(instanceDir, "config.json"), JSON.stringify(instanceConfig, null, 4));
let serverSettings = await factorio.FactorioServer.exampleSettings(path.join(factorioDir, "data"));
let gameName = "Clusterio instance: " + instance.name;
let gameName = "Clusterio instance: " + options.name;
if (options.username) {
gameName = options.username + "'s clusterio " + instance.name;
gameName = options.username + "'s clusterio " + options.name;
}
let overrides = {
@@ -101,26 +119,13 @@ class Instance {
serverSettings[key] = value;
}
await fs.writeFile(instance.path("server-settings.json"), JSON.stringify(serverSettings, null, 4));
return instance;
await fs.writeFile(
path.join(instanceDir, "server-settings.json"),
JSON.stringify(serverSettings, null, 4)
);
}
async createSave() {
console.log("Creating save .....");
await this.server.create("world");
console.log("Clusterio | Successfully created save");
}
async start(slaveConfig, socket) {
console.log("Deleting .tmp.zip files");
let savefiles = fs.readdirSync(this.path("saves"));
for(let i = 0; i < savefiles.length; i++){
if(savefiles[i].substr(savefiles[i].length - 8, 8) == ".tmp.zip") {
fs.unlinkSync(this.path("saves", savefiles[i]));
}
}
async start(saveName, slaveConfig, socket) {
console.log("Clusterio | Rotating old logs...");
// clean old log file to avoid crash
try{
@@ -137,17 +142,6 @@ class Instance {
await symlinkMods(this, "sharedMods", console);
// Spawn factorio server
let latestSave = await fileOps.getNewestFile(this.path("saves"));
if (latestSave === null) {
throw new Error(
"Your savefile seems to be missing. This might because you created an\n"+
"instance without having factorio installed and configured properly.\n"+
"Try installing factorio and adding your savefile to\n"+
"instances/[instancename]/saves/"
);
}
// Patch save with lua modules from plugins
console.log("Clusterio | Patching save");
@@ -181,7 +175,7 @@ class Instance {
modules.push(module);
}
await factorio.patch(this.path("saves", latestSave), modules);
await factorio.patch(this.path("saves", saveName), modules);
this.server.on('rcon-ready', () => {
console.log("Clusterio | RCON connection established");
@@ -200,7 +194,7 @@ class Instance {
//instanceManagement(slaveConfig, this, compatConfig, this.server, socket); // XXX async function
});
await this.server.start(latestSave);
await this.server.start(saveName);
}
/**
@@ -213,6 +207,36 @@ class Instance {
}
}
async startInstanceRequestHandler() {
// Find save to start
let latestSave = await fileOps.getNewestFile(
this.path("saves"), (name) => !name.endsWith('.tmp.zip')
);
if (latestSave === null) {
throw new errors.RequestError(
"No savefile was found to start the instance with."
);
}
await this.start(latestSave, this.config, this.socket);
}
async createSaveRequestHandler() {
console.log("Creating save .....");
await this.server.create("world");
console.log("Clusterio | Successfully created save");
}
async stopInstanceRequestHandler() {
await this.stop();
}
async sendRconRequestHandler(message) {
let result = await this.server.sendRcon(message.data.command);
return { result };
}
/**
* Name of the instance
*
@@ -235,6 +259,67 @@ class Instance {
}
}
/**
* Searches for instances in the provided directory
*
* Looks through all sub-dirs of the provided directory for valid
* instance definitions and returns a mapping of instance id to
* instance info objects.
*
* @returns {Map<integer, Object>} mapping between instance
*/
async function discoverInstances(instancesDir, logger) {
let instanceInfos = new Map();
for (let entry of await fs.readdir(instancesDir, { withFileTypes: true })) {
if (entry.isDirectory()) {
let instanceConfig;
let configPath = path.join(instancesDir, entry.name, "config.json");
try {
instanceConfig = JSON.parse(await fs.readFile(configPath));
} catch (err) {
if (err.code === "ENOENT") {
continue; // Ignore folders without config.json
}
logger.error(`Error occured while parsing ${configPath}: ${err}`);
continue;
}
// XXX should probably validate the entire config with a JSON Schema.
if (typeof instanceConfig.id !== "number" || isNaN(instanceConfig.id)) {
logger.error(`${configPath} is missing id`);
continue;
}
if (typeof instanceConfig.name !== "string") {
logger.error(`${configPath} is missing name`);
continue;
}
let instancePath = path.join(instancesDir, entry.name);
logger.log(`found instance ${instanceConfig.name} in ${instancePath}`);
instanceInfos.set(instanceConfig.id, {
path: instancePath,
config: instanceConfig,
});
}
}
return instanceInfos;
}
class InstanceConnection extends link.Link {
constructor(connector, slave) {
super('slave', 'instance', connector);
this.slave = slave;
link.attachAllMessages(this);
}
async forwardEventToMaster(message, event) {
event.send(this.slave, message.data);
}
}
class SlaveConnector extends link.SocketIOClientConnector {
constructor(slaveConfig) {
super(slaveConfig.masterURL, slaveConfig.masterAuthToken);
@@ -276,6 +361,8 @@ class Slave extends link.Link {
publicAddress: slaveConfig.publicIP,
}
this.instanceConnections = new Map();
this.instanceInfos = new Map();
}
async _findNewInstanceDir(name) {
@@ -294,125 +381,116 @@ class Slave extends link.Link {
return dir;
}
/**
* Looks up the instance for an instance request handler
*
* Called by the handler for InstanceRequest.
*/
async forwardInstanceRequest(handler, message) {
let instance = this.instances.get(message.data.instance_id);
if (!instance) {
async forwardRequestToInstance(message, request) {
let instanceId = message.data.instance_id;
if (!this.instanceInfos.has(instanceId)) {
throw new errors.RequestError(`Instance with ID ${instanceId} does not exist`);
}
return await handler.call(this, instance, message);
let instanceConnection = this.instanceConnections.get(instanceId);
if (!instanceConnection) {
throw new errors.RequestError(`Instance ID ${instanceId} is not active`);
}
return await request.send(instanceConnection, message.data);
}
async createInstanceRequestHandler(message) {
let { id, options } = message.data;
if (this.instances.has(id)) {
if (this.instanceInfos.has(id)) {
throw new Error(`Instance with ID ${id} already exists`);
}
let instanceDir = await this._findNewInstanceDir(options.name);
// XXX: race condition on multiple simultanious calls
let instance = await Instance.create(id, instanceDir, this.config.factorioDir, options);
this.instances.set(id, instance);
this.hookInstance(instance);
this.updateInstances();
let instanceDir = await this._findNewInstanceDir(options.name);
await Instance.create(id, instanceDir, this.config.factorioDir, options);
await this.updateInstances();
}
async createSaveInstanceRequestHandler(instance, message) {
await instance.createSave();
}
async startInstanceInstanceRequestHandler(instance, message) {
await instance.start(this.config, this.socket);
}
async stopInstanceInstanceRequestHandler(instance, message) {
await instance.stop();
}
async deleteInstanceInstanceRequestHandler(instance, message) {
await instance.stop();
this.instances.delete(instance.config.id);
await fs.remove(instance.path());
this.updateInstances();
}
async sendRconInstanceRequestHandler(instance, message) {
let result = await instance.server.sendRcon(message.data.command);
return { result };
}
async findInstances() {
let instances = new Map();
for (let entry of await fs.readdir(this.config.instancesDir, { withFileTypes: true })) {
if (entry.isDirectory()) {
let instanceConfig;
let configPath = path.join(this.config.instancesDir, entry.name, "config.json");
try {
instanceConfig = JSON.parse(await fs.readFile(configPath));
} catch (err) {
if (err.code === "ENOENT") {
continue; // Ignore folders without config.json
}
console.error(`Error occured while parsing ${configPath}: ${err}`);
}
// XXX should probably validate the entire config with a JSON Schema.
if (typeof instanceConfig.id !== "number" || isNaN(instanceConfig.id)) {
console.error(`${configPath} is missing id`);
continue;
}
if (typeof instanceConfig.name !== "string") {
console.error(`${configPath} is missing name`);
continue;
}
let instancePath = path.join(this.config.instancesDir, entry.name);
console.log(`found instance ${instanceConfig.name} in ${instancePath}`);
let instance = new Instance(instancePath, this.config.factorioDir, instanceConfig);
await instance.init();
this.hookInstance(instance); // XXX this is the wrong place for this
instances.set(instanceConfig.id, instance);
}
/**
* Initialize and connect an unloaded instance
*/
async _connectInstance(instanceId) {
let instanceInfo = this.instanceInfos.get(instanceId);
if (!instanceInfo) {
throw new errors.RequestError(`Instance with ID ${instanceId} does not exist`);
}
return instances;
if (this.instanceConnections.has(instanceId)) {
throw new errors.RequestError(`Instance with ID ${instanceId} is active`);
}
let [connectionClient, connectionServer] = link.VirtualConnector.makePair();
let instanceConnection = new InstanceConnection(connectionServer, this);
let instance = new Instance(
connectionClient, instanceInfo.path, this.config.factorioDir, instanceInfo.config
);
await instance.init();
// XXX: race condition on multiple simultanious calls
this.instanceConnections.set(instanceId, instanceConnection);
return instanceConnection;
}
hookInstance(instance) {
instance.server.on('output', (output) => {
link.messages.instanceOutput.send(this, { instance_id: instance.config.id, output })
});
async startInstanceRequestHandler(message, request) {
let instanceConnection = await this._connectInstance(message.data.instance_id);
return await request.send(instanceConnection, message.data);
}
async createSaveRequestHandler(message, request) {
let instanceId = message.data.instance_id;
let instanceConnection = await this._connectInstance(instanceId);
await request.send(instanceConnection, message.data);
this.instanceConnections.delete(instanceId);
}
updateInstances() {
async stopInstanceRequestHandler(message, request) {
await this.forwardRequestToInstance(message, request);
this.instanceConnections.delete(message.data.instance_id);
}
async deleteInstanceRequestHandler(message) {
let instanceId = message.data.instance_id;
if (this.instanceConnections.has(instanceId)) {
throw new errors.RequestError(`Instance with ID ${instanceId} is active`);
}
let instanceInfo = this.instanceInfos.get(instanceId);
if (!instanceInfo) {
throw new errors.RequestError(`Instance with ID ${instanceId} does not exist`);
}
await fs.remove(instanceInfo.path);
await this.updateInstances();
}
/**
* Discover available instances
*
* Looks through the instances directory for instances and updates
* the slave and master server with the new list of instances.
*/
async updateInstances() {
this.instanceInfos = await discoverInstances(this.config.instancesDir, console);
let list = [];
for (let instance of this.instances.values()) {
for (let instanceInfo of this.instanceInfos.values()) {
list.push({
id: instance.config.id,
name: instance.config.name,
id: instanceInfo.config.id,
name: instanceInfo.config.name,
});
}
link.messages.updateInstances.send(this, { instances: list });
}
async start() {
this.instances = await this.findInstances();
await this.connect();
this.updateInstances();
await this.updateInstances();
}
async stop() {
for (let instance of this.instances.values()) {
await instance.stop();
for (let instanceConnection of this.instanceConnections.values()) {
// XXX this neeeds more thought to it
// await instance.stop();
}
}
}
@@ -820,6 +898,7 @@ module.exports = {
_Instance: Instance,
_checkFilename: checkFilename,
_symlinkMods: symlinkMods,
_discoverInstances: discoverInstances,
_Slave: Slave,
};
+120 -113
View File
@@ -9,15 +9,47 @@ const errors = require("lib/errors");
* link.
*/
class Request {
/**
* 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<string>} links -
* Links this request is valid on. Takes an array of strings
* containing "<source>-<target>" specifications.
* @param {?string} forwardTo -
* Optional target to forward this request to. Only 'instance' is
* currently supported and adds instance_id into the
* requestProperties.
* @param {Object<string, Object>} requestProperties -
* Mapping of property values to JSON schema specifications for the
* properties that are valid in the data payload of this requst.
* @param {Object<string, 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, links, requestProperties = {}, responseProperties = {}
type, links, forwardTo = null, requestProperties = {}, responseProperties = {}
}) {
this.type = type;
this.links = links;
this.forwardTo = forwardTo;
this.requestType = type + '_request';
this.responseType = type + '_response';
if (forwardTo === 'instance') {
requestProperties = Object.assign(
{"instance_id": { type: "integer" }}, requestProperties
);
} else if (forwardTo !== null) {
throw new Error(`Invalid forwardTo value ${forwardTo}`);
}
this._requestValidator = schema.compile({
$schema: "http://json-schema.org/draft-07/schema#",
properties: {
@@ -64,6 +96,9 @@ class Request {
* 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.
*
* Does nothing if the link is neither a source nor a target for this
* request.
*
@@ -81,21 +116,26 @@ class Request {
// 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;
}
}
if (!handler) {
throw new Error(`Missing handler for ${this.requestType} on ${link.source}-${link.target} link`);
}
link.setHandler(this.requestType, message => {
handler.call(link, message).then(response => {
handler.call(link, message, this).then(response => {
if (response === undefined) {
// XXX Should we allow implicit responses like this?
response = {};
}
if (Object.prototype.hasOwnProperty.call(response, 'seq')) {
throw new errors.RequestError("response contains reserved property 'seq'");
}
link.connector.send(this.responseType, { seq: message.seq, ...response });
link.connector.send(this.responseType, { ...response, seq: message.seq });
}).catch(err => {
if (!(err instanceof errors.RequestError)) {
@@ -126,95 +166,6 @@ class Request {
}
}
/**
* Represents a request sent to the master that is forwarded to an instance
*
* This is a two part request with a master part and a slave part. The
* master part is done by the forwardInstanceRequest method on the master
* side of link which finds the slave the requuested instance is on and
* forwards it there using the foward method.
*
* The slave part is done by the forwardInstanceRequest on the slave side of
* the link and finds the instance and then calls the handler with it and
* the message.
*/
class InstanceRequest {
constructor({
type, sources, requestProperties = {}, responseProperties = {}
}) {
this._masterRequest = new Request({
type: type + '_master',
links: sources.map(source => `${source}-master`),
requestProperties: {
"instance_id": { type: "integer" },
...requestProperties,
},
responseProperties,
});
this._slaveRequest = new Request({
type: type + '_slave',
links: ['master-slave'],
requestProperties: {
"instance_id": { type: "integer" },
...requestProperties,
},
responseProperties,
});
}
/**
* Attach to a link
*
* Attach master/slave part of the instance request. If this is the
* slave side the handler argument is required.
*
* Does nothing if the link is neither a source nor a target for the
* slave or master request.
*
* @param {Link} link - The link to attach to.
* @param {Function} handler -
* Async function to to handle the instance request. It is passed on
* to forwardInstanceRequest on the slave side.
* @throws {Error} if the handler is needed and not defined.
*/
attach(link, handler) {
this._masterRequest.attach(link, async (message) => {
return await link.forwardInstanceRequest(this, message);
});
if (handler) {
this._slaveRequest.attach(link, async (message) => {
return await link.forwardInstanceRequest(handler, message);
});
} else {
this._slaveRequest.attach(link);
}
}
/**
* Forward the message over the link
*
* This method is expected to be called by forwardInstanceRequest on the
* master with the slave link that the instance is on.
*/
async forward(link, message) {
let response = await this._slaveRequest.send(link, message.data);
delete response.seq;
return response;
}
/**
* Send request over the given link
*
* Sends the given data over the link to the master and waits for it to
* forward the request to the slave with the instance on and return the
* result of it.
*
* @returns {object} response data
*/
async send(link, data = {}) {
return await this._masterRequest.send(link, data);
}
}
let messages = {}
@@ -305,30 +256,34 @@ messages.createInstance = new Request({
},
});
messages.createSave = new InstanceRequest({
type: 'create_save',
sources: ['control'],
});
messages.startInstance = new InstanceRequest({
messages.startInstance = new Request({
type: 'start_instance',
sources: ['control'],
links: ['control-master', 'master-slave', 'slave-instance'],
forwardTo: 'instance',
});
messages.stopInstance = new InstanceRequest({
messages.createSave = new Request({
type: 'create_save',
links: ['control-master', 'master-slave', 'slave-instance'],
forwardTo: 'instance',
});
messages.stopInstance = new Request({
type: 'stop_instance',
sources: ['control'],
links: ['control-master', 'master-slave', 'slave-instance'],
forwardTo: 'instance',
});
messages.deleteInstance = new InstanceRequest({
messages.deleteInstance = new Request({
type: 'delete_instance',
sources: ['control'],
links: ['control-master', 'master-slave'],
forwardTo: 'instance',
});
messages.sendRcon = new InstanceRequest({
messages.sendRcon = new Request({
type: 'send_rcon',
sources: ['control'],
links: ['control-master', 'master-slave', 'slave-instance'],
forwardTo: 'instance',
requestProperties: {
"command": { type: "string" },
},
@@ -338,10 +293,38 @@ messages.sendRcon = new InstanceRequest({
});
/**
* 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.
*/
class Event {
constructor({ type, links, eventProperties = {} }) {
/**
* 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<string>} links -
* Links this event is valid on. Takes an array of strings
* containing "<source>-<target>" specifications.
* @param {?string} forwardTo -
* Optional target to forward this request to. Only 'master' is
* currently supported.
* @param {Object<string, 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, eventProperties = {} }) {
this.type = type;
this.links = links;
this.forwardTo = forwardTo;
if (forwardTo !== 'master' && forwardTo !== null) {
throw new Error(`Invalid forwardTo value ${forwardTo}`);
}
this.eventType = type + '_event';
this._eventValidator = schema.compile({
@@ -357,16 +340,40 @@ class Event {
});
}
/**
* 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 'master' the `forwardEventToMaster` method on
* the link is used as the default 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 === 'master') {
handler = link.forwardEventToMaster;
}
}
if (!handler) {
throw new Error(`Missing handler for ${this.eventType} on ${link.source}-${link.target} link`);
}
link.setHandler(this.eventType, message => {
// XXX Should event handlers even be allowed to be async?
handler.call(link, message).catch(err => {
handler.call(link, message, this).catch(err => {
console.error(`Unexpected error while handling ${this.eventType}`, err);
});
}, this._eventValidator);
@@ -404,7 +411,8 @@ messages.updateInstances = new Event({
messages.instanceOutput = new Event({
type: 'instance_output',
links: ['slave-master', 'master-control'],
links: ['instance-slave', 'slave-master', 'master-control'],
forwardTo: 'master',
eventProperties: {
"instance_id": { type: "integer" },
"output": {
@@ -444,7 +452,6 @@ function attachAllMessages(link) {
module.exports = {
Request,
InstanceRequest,
Event,
attachAllMessages,
+2 -2
View File
@@ -255,7 +255,7 @@ class BaseConnection extends link.Link {
link.attachAllMessages(this);
}
async forwardInstanceRequest(request, message) {
async forwardRequestToInstance(message, request) {
let instance = db.instances.get(message.data.instance_id);
if (!instance) {
throw new errors.RequestError(`Instance with ID ${instance_id} does not exist`);
@@ -266,7 +266,7 @@ class BaseConnection extends link.Link {
throw new errors.RequestError("Slave containing instance is not connected");
}
return await request.forward(connection, message);
return await request.send(connection, message.data);
}
}
+25 -2
View File
@@ -2,11 +2,12 @@ const assert = require('assert').strict;
const path = require('path');
const fs = require('fs-extra');
const link = require("lib/link");
const client = require("../client");
describe("Client testing", function() {
describe("class Instance", function() {
let instance = new client._Instance("dir", "factorioDir", { name:"foo" })
let instance = new client._Instance(new link.VirtualConnector(), "dir", "factorioDir", { name:"foo" });
it("should give the name on .name", function() {
assert.equal(instance.name, "foo");
})
@@ -81,7 +82,9 @@ describe("Client testing", function() {
fs.outputFileSync(path.join(testDir, "shared", "mod_a.zip"), "a");
fs.outputFileSync(path.join(testDir, "shared", "mod_b.zip"), "b");
fs.outputFileSync(path.join(testDir, "shared", "mod.dat"), "c");
let instance = new client._Instance(path.join(testDir, "instance"), "factorioDir", { name: "test" });
let instance = new client._Instance(
new link.VirtualConnector(), path.join(testDir, "instance"), "factorioDir", { name: "test" }
);
fs.outputFileSync(instance.path("mods", "mod_i.zip"), "i");
let discardingLogger = {
@@ -127,4 +130,24 @@ describe("Client testing", function() {
assert.equal(await fs.readFile(instance.path("mods", "mod_i.zip"), "utf-8"), "i");
});
});
describe("discoverInstances()", function() {
it("should discover test instance", async function() {
let logger = { log: () => {}, error: () => {} };
let instancePath = path.join("test", "file", "instances");
let instanceInfos = await client._discoverInstances(instancePath, logger);
assert.deepEqual(instanceInfos, new Map([
[1, {
config: {
id: 1,
name: "test",
factorioPort: null,
clientPort: null,
clientPassword: null
},
path: path.join(instancePath, "test"),
}],
]));
});
});
});
+7
View File
@@ -0,0 +1,7 @@
{
"id": 1,
"name": "test",
"factorioPort": null,
"clientPort": null,
"clientPassword": null
}
-1
View File
@@ -63,7 +63,6 @@ describe("Integration of Clusterio", function() {
masterAuthToken: "invalid",
publicIP: "invalid",
});
testSlave.instances = await testSlave.findInstances();
testSlaveConnection = new master._SlaveConnection({ agent: "test", version, name: "slave", id: 4}, slaveServer);
master._slaveConnections.set(4, testSlaveConnection);
});
+18 -17
View File
@@ -18,6 +18,15 @@ describe("lib/link/messages", function() {
links: ['source-target'],
});
describe("constructor", function() {
it("should throw on invalid forwardTo", function() {
assert.throws(
() => new link.Request({ forwardTo: 'invalid'}),
new Error("Invalid forwardTo value invalid")
);
});
});
describe(".attach()", function() {
it("should attach validator to a source link", function() {
testRequest.attach(testSourceLink);
@@ -35,20 +44,6 @@ describe("lib/link/messages", function() {
testRequest.attach(testTargetLink, async (message) => handlerResult );
assert(testTargetLink._handlers.has('test_request'), "Handler was not set");
});
it("should throw on handler returning object with seq", async function() {
handlerResult = { seq: 3 };
testTargetLink._handlers.get('test_request')({ seq: 2 });
let result = await new Promise(resolve => {
testTargetLink.connector.send = (type, data) => resolve({ type, data });
});
assert.deepEqual(result, {
type: 'test_response',
data: {
error: "response contains reserved property 'seq'",
seq: 2,
},
});
});
it("should send result of calling the handler", async function() {
handlerResult = { test: "handler" };
testTargetLink._handlers.get('test_request')({ seq: 2 });
@@ -107,15 +102,21 @@ describe("lib/link/messages", function() {
});
});
describe("class InstanceRequest", function() {
});
describe("class Event", function() {
let testEvent = new link.Event({
type: 'test',
links: ['source-target'],
});
describe("constructor", function() {
it("should throw on invalid forwardTo", function() {
assert.throws(
() => new link.Event({ forwardTo: 'invalid'}),
new Error("Invalid forwardTo value invalid")
);
});
});
describe(".attach()", function() {
it("should throw if missing handler on target link", function() {
assert.throws(