Merge pull request #279 from Hornwitser/ditch-socket-io

Ditch socket.io
This commit is contained in:
Hornwitser authored and GitHub committed 2020-03-23 02:49:31 +01:00
commit 277b78bf6d
20 files changed
+356 -332

No files matched your search

+1
View File
@@ -143,6 +143,7 @@ Version 2.0.0
- Removed `--databaseDirectory`, `--masterPort`, and `--sslPort` command line
arguments from the master server.
- Implemented a new config system that replaces the old. Breaks all plugins.
- Removed usage of socket.io entirely in favor of a plain WebSocket connection.
Version 1.2.4
+17 -11
View File
@@ -390,17 +390,19 @@ class InstanceConnection extends link.Link {
}
}
class SlaveConnector extends link.SocketIOClientConnector {
class SlaveConnector extends link.WebSocketClientConnector {
constructor(slaveConfig) {
super(slaveConfig.get("slave.master_url"), slaveConfig.get("slave.master_token"));
super(slaveConfig.get("slave.master_url"));
this.id = slaveConfig.get("slave.id");
this.name = slaveConfig.get("slave.name");
this._token = slaveConfig.get("slave.master_token");
}
register() {
console.log("SOCKET | registering slave");
this.send('register_slave', {
token: this._token,
agent: 'Clusterio Slave',
version,
id: this.id,
@@ -412,8 +414,7 @@ class SlaveConnector extends link.SocketIOClientConnector {
/**
* Handles running the slave
*
* Connects to the master server over the socket.io connection and manages
* intsances.
* Connects to the master server over the WebSocket and manages intsances.
*/
class Slave extends link.Link {
// I don't like God classes, but the alternative of putting all this state
@@ -639,10 +640,10 @@ class Slave extends link.Link {
}
async stop() {
for (let instanceConnection of this.instanceConnections.values()) {
// XXX this neeeds more thought to it
// await instance.stop();
for (let instanceId of this.instanceConnections.keys()) {
await this.stopInstance(instanceId);
}
this.connector.close("shutdown");
}
}
@@ -840,10 +841,15 @@ async function startClient() {
secondSigint = true;
console.log("Caught interrupt signal, shutting down");
slave.stop().then(() => {
// There's currently no shutdown mechanism for instance plugins so
// they keep the event loop alive.
process.exit();
slave.stop().catch(err => {
console.error(`
+---------------------------------------------------------------+
| Unexpected error occured while stopping slave, please report |
| it to https://github.com/clusterio/factorioClusterio/issues |
+---------------------------------------------------------------+`
);
console.error(err);
process.exit(1);
});
});
+8 -4
View File
@@ -1,4 +1,3 @@
const io = require("socket.io-client");
const jwt = require("jsonwebtoken");
const fs = require("fs-extra");
const yargs = require("yargs");
@@ -315,10 +314,16 @@ commands = new Map([...commands.map(command => [command.name, command])]);
/**
* Connector for control connection to master server
*/
class ControlConnector extends link.SocketIOClientConnector {
class ControlConnector extends link.WebSocketClientConnector {
constructor(url, token) {
super(url);
this._token = token;
}
register() {
console.log("SOCKET | registering control");
this.send('register_control', {
token: this._token,
agent: 'clusterctl',
version: version,
});
@@ -328,8 +333,7 @@ class ControlConnector extends link.SocketIOClientConnector {
/**
* Handles running the control
*
* Connects to the master server over the socket.io connection and sends
* commands to it.
* Connects to the master server over WebSocket and sends commands to it.
*/
class Control extends link.Link {
+4 -16
View File
@@ -5,21 +5,9 @@ Socket.io protocol
slaves, and unless you're developing Clusterio it's probably not going
to be very useful for you.</sub>
The protocol is built on the [socket.io](https://socket.io/) library
which lets mulitple streams be multiplexed in many different ways over
the same connection and abstracts away reconnection logic.
Unfortunately I've been unable to figure what the message delivery
guarantees of the library is, so whether or not messages can be lost is
still an unknown, I assume that messages are delivered as long as a
connection is maintained or manages to reconnect.
While there are many mechanisms to multiplex streams, the core
communication between the master server and slaves is done entirely by
sending single POJO payloads over the `message` event. There's a
shortcut for sending these via the `.send` method of the socket.io
sockets.
These messages contain the following three properties:
The protocol is built on WebSocket and uses JSON encoded text messages
to communicate to and from the master server. These messages contain
the following three properties:
- seq - integer - The sequence number of the message
- type - string - The type of message
- data - object - Message data
@@ -28,7 +16,7 @@ These messages contain the following three properties:
Handshake
---------
Upon connecting to the socket.io endpoint the server will respond with a
Upon connecting to the WebSocket endpoint the server will respond with a
`hello` message containing the master server version as the data. The
client is then expected to send a `register_slave` or `register_control`
message back depending on what kind of client it is. If the register
+31 -48
View File
@@ -1,6 +1,6 @@
"use strict";
const events = require("events");
const io = require("socket.io-client");
const WebSocket = require("ws");
const schema = require("lib/schema");
@@ -8,12 +8,11 @@ const schema = require("lib/schema");
/**
* Connector for master server clients
*/
class SocketIOClientConnector extends events.EventEmitter {
constructor(url, token) {
class WebSocketClientConnector extends events.EventEmitter {
constructor(url) {
super();
this._url = url;
this._token = token;
this._seq = 1;
this._state = "new";
@@ -33,7 +32,7 @@ class SocketIOClientConnector extends events.EventEmitter {
* @returns the sequence number of the message sent
*/
send(type, data) {
this._socket.send({ seq: this._seq, type, data });
this._socket.send(JSON.stringify({ seq: this._seq, type, data }));
return this._seq++;
}
@@ -44,7 +43,7 @@ class SocketIOClientConnector extends events.EventEmitter {
*/
close(reason) {
this.send('close', { reason });
this.disconnect();
this._socket.close();
}
/**
@@ -54,19 +53,13 @@ class SocketIOClientConnector extends events.EventEmitter {
this._check(["new"]);
this._state = "handshake";
// Open socket.io connection to master
console.log(`SOCKET | connecting to ${this._url}`)
let url = new URL(this._url);
url.searchParams.append('token', this._token);
url.pathname += "api/socket";
// I really have to question the choice of having the path connected
// to be passed as separate parameter instead of the namespace.
let urlPath = url.pathname + "socket.io";
url.pathname = "";
this._socket = io(url.href, {
path: urlPath,
reconnectionAttempts: 2,
// Open WebSocket to master
console.log(`SOCKET | connecting to ${url}`)
this._socket = new WebSocket(url, {
// For now we do not verify TLS certificates since the default setup is
// to create a self-signed certificate.
rejectUnauthorized: false,
@@ -79,45 +72,35 @@ class SocketIOClientConnector extends events.EventEmitter {
}
_attachSocketHandlers() {
this._socket.on("close", (code, reason) => {
console.log(`SOCKET | Close (code: ${code}, reason: ${reason})`);
});
this._socket.on("error", err => {
console.error("SOCKET | Error:", err);
this._socket.close();
});
this._socket.on("unexpected-response", (req, res) => {
let data = "";
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
console.error(`SOCKET | Unexpeced Response ${res.statusCode}`);
console.error(data);
});
});
/*this._socket.on("connect_error", err => {
console.log("SOCKET | Connect error:", err);
});*/
this._socket.on("connect_timeout", () => {
console.log("SOCKET | Connect timeout");
this._socket.on("open", () => {
console.log("SOCKET | Open");
});
this._socket.on("reconnect", attempt => {
console.log(`SOCKET | Reconnect no. ${attempt} succeeded`);
this._socket.on("ping", data => {
console.log(`SOCKET | Ping (data: ${data}`);
});
this._socket.on("reconnect_attempt", () => {
console.log("SOCKET | Attempting reconnect");
});
this._socket.on("reconnecting", attempt => {
console.log(`SOCKET | Attempting reconnect no. ${attempt}`);
});
/*this._socket.on("reconnect_error", err => {
console.log("SOCKET | Reconnect error:", err);
});*/
this._socket.on("reconnect_failed", () => {
console.log("SOCKET | Reconnecting failed");
});
/*this._socket.on("ping", () => {
console.log("SOCKET | Ping");
});
this._socket.on("pong", (ms) => {
// XXX Latency might be useful
console.log(`SOCKET | Pong latency ${ms}ms`);
});*/
this._socket.on("connect", () => {
console.log("SOCKET | connected");
this._socket.on("pong", data => {
console.log(`SOCKET | Pong (data: ${data}`);
});
// Handle messages
this._socket.on("message", message => {
this._socket.on("message", data => {
let message = JSON.parse(data);
if (this._state === "handshake") {
this._processHandshake(message);
} else if (this._state === "ready") {
@@ -132,7 +115,7 @@ class SocketIOClientConnector extends events.EventEmitter {
* Immediatly close the connection
*/
disconnect() {
this._socket.close();
this._socket.terminate();
}
_processHandshake(message) {
@@ -155,7 +138,7 @@ class SocketIOClientConnector extends events.EventEmitter {
} else if (type === 'close') {
console.log(`SOCKET | received close from server: ${data.reason}`);
this.emit('error', new Error(`server closed during handshake: ${data.reason}`));
this._socket.close();
this._socket.terminate();
}
}
@@ -224,6 +207,6 @@ class VirtualConnector extends events.EventEmitter {
}
module.exports = {
SocketIOClientConnector,
WebSocketClientConnector,
VirtualConnector,
};
+14
View File
@@ -140,6 +140,11 @@ class Request {
response = {};
}
if (!this._responseValidator({ seq: 0, type: this.responseType, response})) {
console.error(this._responseValidator.errors);
throw new Error(`Validation failed responding to ${this.requestType}`);
}
link.connector.send(this.responseType, { ...response, seq: message.seq });
}).catch(err => {
@@ -162,6 +167,10 @@ class Request {
*/
async send(link, data = {}) {
// XXX validate link target/source?
if (!this._requestValidator({ seq: 0, type: this.requestType, data})) {
console.error(this._requestValidator.errors);
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) {
@@ -454,6 +463,11 @@ class Event {
* Sends the given event data over the link.
*/
send(link, data = {}) {
if (!this._eventValidator({ seq: 0, type: this.eventType, data})) {
console.error(this._eventValidator.errors);
throw new Error(`Validation failed sending ${this.eventType}`);
}
link.connector.send(this.eventType, data);
}
}
-4
View File
@@ -87,10 +87,6 @@ let downloadList = [
url: "https://fonts.googleapis.com/css?family=Roboto",
file: "static/external/css/Roboto.css",
},
{
url: "https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.4/socket.io.js",
file: "static/external/socket.io.js",
},
]
// initiate folder creations and downloads
+4 -2
View File
@@ -105,8 +105,9 @@ const clientHandshake = ajv.compile({
"type": { const: "register_slave" },
"data": {
additionalProperties: false,
required: ["agent", "version", "name", "id"],
required: ["token", "agent", "version", "name", "id"],
properties: {
"token": { type: "string" },
"agent": { type: "string" },
"version": { type: "string" },
"name": { type: "string" },
@@ -120,8 +121,9 @@ const clientHandshake = ajv.compile({
"type": { const: "register_control" },
"data": {
additionalProperties: false,
required: ["agent", "version"],
required: ["token", "agent", "version"],
properties: {
"token": { type: "string" },
"agent": { type: "string" },
"version": { type: "string" },
}
+63 -47
View File
@@ -23,6 +23,7 @@ const request = require("request");
const setBlocking = require("set-blocking");
const events = require("events");
const yargs = require("yargs");
const WebSocket = require("ws");
const version = require("./package").version;
// ugly globals
@@ -292,9 +293,6 @@ app.get("/api/getFactorioLocale", function(req,res){
}
});
// socket.io connection for slaves
var io = require("socket.io")({});
class BaseConnection extends link.Link {
constructor(target, connector) {
super('master', target, connector);
@@ -541,17 +539,9 @@ class SlaveConnection extends BaseConnection {
}
}
// Authentication middleware for socket.io connections
io.use((socket, next) => {
let token = socket.handshake.query.token;
authenticate.check(token).then((result) => {
if (result.ok) {
next();
} else {
console.error(`SOCKET | authentication failed for ${socket.handshake.address}`);
next(new Error(result.msg));
}
});
const wss = new WebSocket.Server({
noServer: true,
path: "/api/socket",
});
/**
@@ -569,7 +559,7 @@ function isInteger(value) {
/**
* Connector for master server connections
*/
class SocketIOServerConnector extends events.EventEmitter {
class WebSocketServerConnector extends events.EventEmitter {
constructor(socket) {
super();
@@ -577,9 +567,9 @@ class SocketIOServerConnector extends events.EventEmitter {
this._socket = socket;
this._socket.on('message', message => {
this.emit('message', message);
this.emit('message', JSON.parse(message));
});
this._socket.on('disconnect', () => {
this._socket.on('close', (code, reason) => {
this.emit('disconnect');
});
}
@@ -590,7 +580,7 @@ class SocketIOServerConnector extends events.EventEmitter {
* @returns the sequence number of the message sent
*/
send(type, data = {}) {
this._socket.send({ seq: this._seq, type, data });
this._socket.send(JSON.stringify({ seq: this._seq, type, data }));
return this._seq++;
}
@@ -601,53 +591,73 @@ class SocketIOServerConnector extends events.EventEmitter {
*/
close(reason) {
this.send('close', { reason });
this.disconnect();
this._socket.close();
}
/**
* Immediatly close the connection
*/
disconnect() {
this._socket.disconnect(true);
this._socket.terminate();
}
}
io.on('connection', function (socket) {
console.log(`SOCKET | new connection from ${socket.handshake.address}`);
wss.on('connection', function (socket, req) {
console.log(`SOCKET | new connection from ${socket.remoteAddress}`);
// Start connection handshake
let connector = new SocketIOServerConnector(socket);
let connector = new WebSocketServerConnector(socket);
connector.send('hello', { version });
// Handle socket handshake
socket.once('message', (payload) => {
payload = JSON.parse(payload);
// XXX prometheusWsUsageCounter.labels('message', "other").inc();
if (!schema.clientHandshake(payload)) {
console.log(`SOCKET | closing ${socket.handshake.address} after receiving invalid handshake`, payload);
connector.send('close', { reason: "Invalid handshake" });
connector.disconnect(true);
console.log(`SOCKET | closing ${req.socket.remoteAddress} after receiving invalid handshake`, payload);
connector.close("Invalid handshake");
return;
}
let { seq, type, data } = payload;
if (type === "register_slave") {
let connection = slaveConnections.get(data.id);
if (connection) {
console.log(`SOCKET | disconecting existing connection for slave ${data.id}`);
connection.connector.close("Registered from another connection");
}
if (["register_slave", "register_control"].includes(type)) {
authenticate.check(data.token).then((result) => {
if (result.ok) {
if (type === "register_slave") {
let connection = slaveConnections.get(data.id);
if (connection) {
console.log(`SOCKET | disconecting existing connection for slave ${data.id}`);
connection.connector.close("Registered from another connection");
}
console.log(`SOCKET | registered slave ${data.id} version ${data.version}`);
slaveConnections.set(data.id, new SlaveConnection(data, connector));
connector.send('ready');
console.log(`SOCKET | registered slave ${data.id} version ${data.version}`);
slaveConnections.set(data.id, new SlaveConnection(data, connector));
connector.send('ready');
} else if (type === "register_control") {
console.log(`SOCKET | registered control from ${socket.handshake.address}`);
controlConnections.push(new ControlConnection(data, connector));
connector.send('ready');
} else { // type === "register_control"
console.log(`SOCKET | registered control from ${req.socket.remoteAddress}`);
controlConnections.push(new ControlConnection(data, connector));
connector.send('ready');
}
} else {
console.error(`SOCKET | authentication failed for ${req.socket.remoteAddress}`);
connector.close("Authentication failed");
}
}).catch((err) => {
console.error(`
+---------------------------------------------------------------------+
| Unexpected error occured while authenticating client, please report |
| it to https://github.com/clusterio/factorioClusterio/issues |
+---------------------------------------------------------------------+`
);
console.error(err);
connection.close("Unexpected error");
});
} else if (type === "close") {
console.log(`SOCKET | received close from ${socket.handshake.address}: ${data.reason}`);
console.log(`SOCKET | received close from ${req.socket.remoteAddress}: ${data.reason}`);
connector.disconnect();
return;
}
@@ -693,10 +703,21 @@ async function loadPlugins(pluginInfos) {
/**
* Calls listen on server capturing any errors that occurs
* binding to the port.
* binding to the port. Also adds handler for WebSocket
* upgrade event.
*/
function listen(server, ...args) {
return new Promise((resolve, reject) => {
server.on("upgrade", (req, socket, head) => {
console.log("handling upgrade");
// For reasons that defy common sense, the connection event has
// to be emitted explictly when using noServer.
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req);
});
});
function wrapError(err) {
reject(new errors.StartupError(
`Server listening failed: ${err.message}`
@@ -826,7 +847,6 @@ async function startServer() {
if (httpPort) {
httpServer = require("http").Server(app);
await listen(httpServer, httpPort);
io.attach(httpServer);
console.log("Listening for HTTP on port %s...", httpServer.address().port);
}
@@ -847,10 +867,6 @@ async function startServer() {
cert: certificate
}, app)
await listen(httpsServer, httpsPort);
// XXX I'm uncertain whether or not socket.io actually supports
// attaching to multiple servers at the same time.
io.attach(httpsServer);
console.log("Listening for HTTPS on port %s...", httpsServer.address().port);
}
}
@@ -861,7 +877,7 @@ module.exports = {
// For testing only
_db: db,
_setConfig,
_SocketIOServerConnector: SocketIOServerConnector,
_WebSocketServerConnector: WebSocketServerConnector,
_controlConnections: controlConnections,
_ControlConnection: ControlConnection,
_slaveConnections: slaveConnections,
+2 -4
View File
@@ -61,10 +61,9 @@
"set-blocking": "^2.0.0",
"simple-git": "^1.96.0",
"single-line-log": "^1.1.2",
"socket.io": "^2.2.0",
"socket.io-client": "^2.2.0",
"sync-request": "^4.0.1",
"typescript-collections": "^1.3.2",
"ws": "^7.2.3",
"yargs": "^14.2.0"
},
"devDependencies": {
@@ -75,7 +74,6 @@
"mocha.parallel": "^0.15.5",
"nock": "^11.3.0",
"nyc": "^14.1.1",
"sinon": "^7.4.1",
"supertest": "^4.0.2"
"sinon": "^7.4.1"
}
}
+2
View File
@@ -29,6 +29,7 @@ function addApiRoutes(app, items, endpointHitCounter) {
for (let [name, count] of items._items) {
inventory.push({ name, count });
}
res.type("json");
res.send(JSON.stringify(inventory));
});
@@ -45,6 +46,7 @@ function addApiRoutes(app, items, endpointHitCounter) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
// Check it and send it
res.type("json");
res.send(JSON.stringify(items.serialise()));
});
}
+12 -32
View File
@@ -1,44 +1,24 @@
const assert = require("assert").strict;
const express = require("express");
const bodyParser = require("body-parser");
const request = require("supertest");
const jwt = require("jsonwebtoken");
const authenticate = require('lib/authenticate');
const database = require("lib/database");
const prometheus = require("lib/prometheus");
const routes = require("../routes");
const { get } = require("../../../test/integration");
const testSecret = "TestSecretDoNotUse";
authenticate.setAuthSecret(testSecret);
const token = jwt.sign({ id: "api" }, testSecret, { expiresIn: 600 });
describe("subspace_storage endpoints", function() {
const app = express();
app.use(bodyParser.json({ limit: '10mb', }));
app.use(bodyParser.urlencoded({ parameterLimit: 100000, limit: '10mb', extended: true }));
const items = new database.ItemDatabase();
items.addItem("steel-plate", 20);
const gauge = new prometheus.Gauge("test_gauge", "test", { labels: ["route"] });
routes.addApiRoutes(app, items, gauge);
describe("GET /api/inventory", function() {
it("should return the masters current inventory", async function() {
let res = await get("/api/inventory");
let inventory = res.body;
assert.equal(typeof inventory, "object", "Inventory should be an object");
let persistentMaster = request(app);
describe("#GET /api/inventory", function() {
it("returns the masters current inventory", function() {
return persistentMaster.get("/api/inventory").expect(200).then(function(res) {
let inventory = JSON.parse(res.text);
assert.equal(typeof inventory, "object", "Inventory should be an object");
assert(inventory.length >= 1, "There should be at least 1 entry in the inventory");
return; // XXX Disabled until a suitable test interface is added.
assert(inventory.length >= 1, "There should be at least 1 entry in the inventory");
let contains20SteelPlate = false;
inventory.forEach(itemStack => {
if(itemStack.name == "steel-plate" && itemStack.count >= 20) contains20SteelPlate = true;
});
assert(contains20SteelPlate, "Please ensure there are at least 20 steel plate in the inventory")
let contains20SteelPlate = false;
inventory.forEach(itemStack => {
if(itemStack.name == "steel-plate" && itemStack.count >= 20) contains20SteelPlate = true;
});
assert(contains20SteelPlate, "Please ensure there are at least 20 steel plate in the inventory")
});
});
});
+4
View File
@@ -7,6 +7,10 @@ const config = require("lib/config");
const client = require("../client");
describe("Client testing", function() {
before(function() {
config.InstanceConfig.finalize();
});
describe("class Instance", function() {
let instance;
before(async function() {
+56 -81
View File
@@ -1,101 +1,76 @@
const assert = require("assert").strict;
const fs = require("fs-extra");
const path = require("path");
const validateHTML = require('html5-validator');
const parallel = require('mocha.parallel');
const link = require("lib/link");
const plugin = require("lib/plugin");
const config = require("lib/config");
const master = require("../../master");
const client = require("../../client");
const clusterctl = require("../../clusterctl");
const { version } = require("../../package");
const { get, exec, controlConfigPath, instancesDir } = require("./index");
// Mark that this test takes a lot of time, or depeneds on a test
// that takes a lot of time.
function slowTest(test) {
if (process.env.FAST_TEST) {
test.skip();
}
test.timeout(20000);
}
describe("Integration of Clusterio", function() {
let testControl;
let testControlConnection;
describe("master http api", function() {
describe("GET /api/getFactorioLocale", function() {
it("should get the basegame factorio locale", async function() {
let res = await get("/api/getFactorioLocale");
let object = res.body;
let testSlave;
let testSlaveConnection;
let instancesDir = path.join("test", "temp", "instances");
// Mark that this test takes a lot of time, or depeneds on a test
// that takes a lot of time.
function slowTest(test) {
if (process.env.FAST_TEST) {
test.skip();
}
test.timeout(20000);
}
async function createInstanceConfig(id, name, assignedSlave) {
let instanceConfig = new config.InstanceConfig()
await instanceConfig.init();
instanceConfig.set("instance.id", id);
instanceConfig.set("instance.name", name);
instanceConfig.set("instance.assigned_slave", assignedSlave);
return instanceConfig;
}
before(async function() {
master._db.slaves = new Map();
master._db.instances = new Map([
[11, await createInstanceConfig(11, "foo", 4)],
[21, await createInstanceConfig(21, "bar", 5)],
]);
let [controlClient, controlServer] = link.VirtualConnector.makePair();
testControl = new clusterctl._Control(controlClient);
testControlConnection = new master._ControlConnection({ agent: "test", version }, controlServer);
master._controlConnections.push(testControlConnection);
await fs.remove(instancesDir);
await fs.ensureDir(instancesDir)
let slaveConfig = new config.SlaveConfig();
await slaveConfig.init();
slaveConfig.set("slave.id", 4);
slaveConfig.set("slave.name", "slave");
slaveConfig.set("slave.instances_directory", instancesDir);
slaveConfig.set("slave.factorio_directory", "factorio");
slaveConfig.set("slave.master_url", "http://invalid");
slaveConfig.set("slave.master_token", "invalid");
slaveConfig.set("slave.public_address", "invalid");
let [slaveClient, slaveServer] = link.VirtualConnector.makePair();
testSlave = new client._Slave(slaveClient, slaveConfig, await plugin.loadPluginInfos("plugins"));
testSlaveConnection = new master._SlaveConnection({ agent: "test", version, name: "slave", id: 4}, slaveServer);
master._slaveConnections.set(4, testSlaveConnection);
// test that it is looks like a factorio locale
assert.equal(typeof object, "object");
assert.equal(object["entity-name"]["fish"], "Fish");
assert.equal(object["entity-name"]["small-lamp"], "Lamp");
});
});
});
parallel("master web interface", function() {
this.timeout(6000);
let paths = ["/", "/nodes", "/settings", "/nodeDetails"];
for (let path of paths) {
it(`sends some HTML when accessing ${path}`, async function() {
let res = await get(path);
let validation = await validateHTML(res.body);
let filtered = validation.messages.filter(msg => msg.type !== "info");
assert(
filtered.length === 0,
"there are HTML errors on the page, please fix: "+JSON.stringify(validation.messages, null, 4)
);
});
}
});
describe("clusterctl", function() {
describe("list-slaves", function() {
it("does not throw", async function() {
await clusterctl._commands.get("list-slaves").run({}, testControl);
it("runs", async function() {
await exec(`node clusterctl --config ${controlConfigPath} list-slaves`);
});
});
describe("list-instances", function() {
it("does not throw", async function() {
await clusterctl._commands.get("list-instances").run({}, testControl);
it("runs", async function() {
await exec(`node clusterctl --config ${controlConfigPath} list-instances`);
});
});
describe("create-instances", function() {
it("creates the instance", async function() {
await clusterctl._commands.get("create-instance").run({id: 44, name: "test"}, testControl);
assert(master._db.instances.has(44), "Instance was not created");
// XXX breaks tests.
master._db.instances.get(44).set("subspace_storage.enabled", false);
master._db.instances.get(44).set("research_sync.enabled", false);
it("runs", async function() {
await exec(`node clusterctl --config ${controlConfigPath} create-instance --id 44 --name test`);
});
});
describe("assign-instance", function() {
it("creates the instance", async function() {
await clusterctl._commands.get("assign-instance").run({instance: "test", slave: "slave"}, testControl);
it("creates the instance files", async function() {
await exec(`node clusterctl --config ${controlConfigPath} assign-instance --instance test --slave 4`);
assert(await fs.exists(path.join(instancesDir, "test")), "Instance was not created");
});
});
@@ -103,22 +78,22 @@ describe("Integration of Clusterio", function() {
describe("create-save", function() {
it("creates a save", async function() {
slowTest(this);
await clusterctl._commands.get("create-save").run({instance: "test"}, testControl);
await exec(`node clusterctl --config ${controlConfigPath} create-save --instance test`);
});
});
describe("start-instance", function() {
it("starts the instance", async function() {
slowTest(this);
await clusterctl._commands.get("start-instance").run({instance: "test"}, testControl);
// TODO check that the instance actually stopped
await exec(`node clusterctl --config ${controlConfigPath} start-instance --instance test`);
// TODO check that the instance actually started
});
});
describe("send-rcon", function() {
it("sends the command", async function() {
slowTest(this);
await clusterctl._commands.get("send-rcon").run({instance: "test", command: "test"}, testControl);
await exec(`node clusterctl --config ${controlConfigPath} send-rcon --instance test --command test`);
// TODO check that the command was received
});
});
@@ -126,7 +101,7 @@ describe("Integration of Clusterio", function() {
describe("stop-instance", function() {
it("stops the instance", async function() {
slowTest(this);
await clusterctl._commands.get("stop-instance").run({instance: "test"}, testControl);
await exec(`node clusterctl --config ${controlConfigPath} stop-instance --instance test`);
// TODO check that the instance actually stopped
});
});
@@ -134,7 +109,7 @@ describe("Integration of Clusterio", function() {
describe("delete-instance", function() {
it("deletes the instance", async function() {
slowTest(this);
await clusterctl._commands.get("delete-instance").run({instance: "test"}, testControl);
await exec(`node clusterctl --config ${controlConfigPath} delete-instance --instance test`);
assert(!await fs.exists(path.join(instancesDir, "test")), "Instance was not deleted");
});
});
+108
View File
@@ -0,0 +1,108 @@
const path = require("path");
const fs = require("fs-extra");
const child_process = require("child_process");
const jwt = require("jsonwebtoken");
const needle = require("needle");
const util = require("util");
const events = require("events");
const server = require("lib/factorio/server");
async function get(path) {
let res = await needle("get", `https://localhost:4443${path}`, { rejectUnauthorized: false });
if (res.statusCode != 200) {
throw new Error(`Got response code ${res.statusCode}, content: ${res.body}`);
}
return res;
}
let masterProcess;
let slaveProcess;
let instancesDir = path.join("test", "temp", "instances");
let databaseDir = path.join("test", "temp", "databse");
let masterConfigPath = path.join("test", "temp", "master-integration.json");
let slaveConfigPath = path.join("test", "temp", "slave-integration.json");
let controlConfigPath = path.join("test", "temp", "control-integration.json");
async function exec(...args) {
console.log(args[0]);
return await util.promisify(child_process.exec)(...args);
}
function spawn(name, cmd, waitFor) {
return new Promise((resolve, reject) => {
console.log(cmd);
let parts = cmd.split(" ");
let process = child_process.spawn(parts[0], parts.slice(1));
let stdout = new server._LineSplitter((line) => {
line = line.toString("utf8");
if (line.startsWith(waitFor)) {
resolve(process);
}
console.log(name, line);
})
let stderr = new server._LineSplitter((line) => { console.log(name, line.toString("utf8")); })
process.stdout.on("data", chunk => { stdout.data(chunk); });
process.stdout.on("close", () => { stdout.end(); });
process.stderr.on("data", chunk => { stderr.data(chunk); });
process.stderr.on("close", () => { stderr.end(); });
});
}
before(async function() {
this.timeout(20000);
await fs.remove(databaseDir);
await fs.remove(instancesDir);
await fs.remove(masterConfigPath);
await fs.remove(slaveConfigPath);
await fs.remove(controlConfigPath);
let token = jwt.sign({ id: "api" }, "TestSecretDoNotUse");
await exec(`node master --config ${masterConfigPath} config set master.database_directory ${databaseDir}`);
await exec(`node master --config ${masterConfigPath} config set master.auth_secret TestSecretDoNotUse`);
await exec(`node master --config ${masterConfigPath} config set master.http_port 8880`);
await exec(`node master --config ${masterConfigPath} config set master.https_port 4443`);
await exec(`node client --config ${slaveConfigPath} config set slave.id 4`);
await exec(`node client --config ${slaveConfigPath} config set slave.name slave`);
await exec(`node client --config ${slaveConfigPath} config set slave.instances_directory ${instancesDir}`);
await exec(`node client --config ${slaveConfigPath} config set slave.master_url "https://localhost:4443/"`);
await exec(`node client --config ${slaveConfigPath} config set slave.master_token "${token}"`);
await exec(`node clusterctl --config ${controlConfigPath} control-config set control.master_url "https://localhost:4443/"`);
await exec(`node clusterctl --config ${controlConfigPath} control-config set control.master_token "${token}"`);
masterProcess = await spawn("master:", `node master --config ${masterConfigPath} run`, "All plugins loaded");
slaveProcess = await spawn("slave:", `node client --config ${slaveConfigPath} start`, "SOCKET | received ready from master");
});
after(async function() {
this.timeout(20000);
if (slaveProcess) {
console.log("Shutting down slave");
slaveProcess.kill("SIGINT");
await events.once(slaveProcess, "exit");
}
if (masterProcess) {
console.log("Shutting down master");
masterProcess.kill("SIGINT");
await events.once(masterProcess, "exit");
}
});
module.exports = {
get,
exec,
instancesDir,
databaseDir,
masterConfigPath,
slaveConfigPath,
controlConfigPath,
};
+16 -15
View File
@@ -6,14 +6,14 @@ const link = require('lib/link');
describe("lib/link/connectors", function() {
describe("class SocketIOClientConnector", function() {
let testConnector = new link.SocketIOClientConnector('source', 'url', 'token');
describe("class WebSocketClientConnector", function() {
let testConnector = new link.WebSocketClientConnector('source', 'url');
testConnector._socket = new mock.MockSocket();
describe(".disconnect()", function() {
it("should call close on the socket", function() {
testConnector._socket.closeCalled = false;
it("should call terminate on the socket", function() {
testConnector._socket.terminateCalled = false;
testConnector.disconnect();
assert(testConnector._socket.closeCalled, "Close was not called");
assert(testConnector._socket.terminateCalled, "Close was not called");
});
});
@@ -30,21 +30,22 @@ describe("lib/link/connectors", function() {
it("calls send on the socket", function() {
testConnector._socket.sentMessages = [];
let seq = testConnector.send('test', { test: true });
assert.deepEqual(testConnector._socket.sentMessages, [{ seq, type: 'test', data: { test: true }}]);
assert.deepEqual(
testConnector._socket.sentMessages.map(JSON.parse),
[{ seq, type: 'test', data: { test: true }}]
);
});
});
describe(".close()", function() {
it("should send close and call disconnect", function() {
it("should send close", function() {
testConnector._socket.sentMessages = [];
let called = false;
testConnector.disconnect = () => { called = true; }
testConnector.close("test reason");
assert.deepEqual(
testConnector._socket.sentMessages,
testConnector._socket.sentMessages.map(JSON.parse),
[{ seq: testConnector._seq - 1, type: 'close', data: { reason: "test reason" } }]
)
assert(called, ".disconnect() was not called");
});
});
@@ -53,7 +54,7 @@ describe("lib/link/connectors", function() {
testConnector._socket.sentMessages = [];
testConnector._processHandshake({ data: "invalid message" }),
assert.deepEqual(
testConnector._socket.sentMessages,
testConnector._socket.sentMessages.map(JSON.parse),
[{seq: testConnector._seq - 1, type: 'close', data: {
reason: "Invalid handshake" }
}]
@@ -85,7 +86,7 @@ describe("lib/link/connectors", function() {
it("should throw on message received in invalid state", function() {
testConnector._state = "new";
assert.throws(
() => testConnector._socket.events.get('message')(),
() => testConnector._socket.events.get("message")("{}"),
new Error("Received message in unexpected state new")
);
});
@@ -93,15 +94,15 @@ describe("lib/link/connectors", function() {
testConnector._state = "handshake";
let called = false;
testConnector._processHandshake = () => { called = true; };
testConnector._socket.events.get('message')();
testConnector._socket.events.get("message")("{}");
assert(called, "_processHandshake was not called");
});
it("should emit message on message in ready state", async function() {
testConnector._socket.sentMessages = [];
testConnector._state = "ready";
let result = events.once(testConnector, 'message');
testConnector._socket.events.get('message')("message");
assert.deepEqual(await result, ["message"]);
testConnector._socket.events.get("message")('{"message":true}');
assert.deepEqual(await result, [{ message: true }]);
});
});
});
+8 -2
View File
@@ -16,6 +16,9 @@ describe("lib/link/messages", function() {
let testRequest = new link.Request({
type: 'test',
links: ['source-target'],
requestProperties: {
"test": { type: "string" },
},
});
describe("constructor", function() {
@@ -93,8 +96,8 @@ describe("lib/link/messages", function() {
testSourceLink.waitFor = (type, condition) => {
return { data: { error: "test error" }};
};
assert.rejects(
testRequest.send(testSourceLink),
await assert.rejects(
testRequest.send(testSourceLink, { test: "blah" }),
new errors.RequestError("test error")
);
delete testSourceLink.waitFor;
@@ -106,6 +109,9 @@ describe("lib/link/messages", function() {
let testEvent = new link.Event({
type: 'test',
links: ['source-target'],
eventProperties: {
"test": { type: "string" },
},
});
describe("constructor", function() {
+4 -62
View File
@@ -1,75 +1,17 @@
const assert = require('assert').strict;
const request = require('supertest');
const validateHTML = require('html5-validator');
const parallel = require('mocha.parallel');
const config = require("lib/config");
const plugin = require("lib/plugin");
const mock = require('./mock');
const master = require('../master');
master._db.slaves = new Map();
before(async function() {
let pluginInfos = await plugin.loadPluginInfos("plugins");
config.registerPluginConfigGroups(pluginInfos);
config.finalizeConfigs();
let masterConfig = new config.MasterConfig();
await masterConfig.load({
groups: [{
name: "master",
fields: {
auth_secret: "TestSecretDoNotUse",
http_port: 8880,
https_port: 4443,
},
}]
});
master._setConfig(masterConfig);
})
async function get(path) {
return await request(master.app).get(path).expect(200);
}
async function getValidate(path) {
let res = await get(path);
let validation = await validateHTML(res.text);
let filtered = validation.messages.filter(msg => msg.type !== "info");
assert(filtered.length === 0, "there are HTML errors on the page, please fix: "+JSON.stringify(validation.messages, null, 4));
}
describe('Master testing', function() {
describe('#GET /api/getFactorioLocale', function() {
it('should get the basegame factorio locale', async function() {
let res = await get('/api/getFactorioLocale');
let object = res.body;
// test that it is looks like a factorio locale
assert.equal(typeof object, "object");
assert.equal(object["entity-name"]["fish"], "Fish");
assert.equal(object["entity-name"]["small-lamp"], "Lamp");
});
});
// describe("#GET /api/")
parallel("#GET static website data", function() {
this.timeout(6000);
let paths = ["/", "/nodes", "/settings", "/nodeDetails"];
for (let path of paths) {
it(`sends some HTML when accessing ${path}`, () => getValidate(path));
}
});
describe("class SocketIOServerConnector", function() {
let testConnector = new master._SocketIOServerConnector(new mock.MockSocket());
describe("class WebSocketServerConnector", function() {
let testConnector = new master._WebSocketServerConnector(new mock.MockSocket());
describe(".disconnect()", function() {
it("should call disconnect on the socket", function() {
testConnector._socket.disconnectCalled = false;
testConnector._socket.terminateCalled = false;
testConnector.disconnect();
assert(testConnector._socket.disconnectCalled, "Disconnect was not called");
assert(testConnector._socket.terminateCalled, "Terminate was not called");
});
});
});
-1
View File
@@ -4,4 +4,3 @@
require('lib/manager/modManager');
require('lib/npmPostinstall.js');
require('lib/typedef');
require('../clusterctl.js');
+2 -3
View File
@@ -7,7 +7,6 @@ class MockSocket {
constructor() {
this.sentMessages = [];
this.events = new Map();
this.handshake = { address: "socket.test" };
}
send(message) {
@@ -18,8 +17,8 @@ class MockSocket {
this.events.set(event, fn);
}
disconnect() {
this.disconnectCalled = true;
terminate() {
this.terminateCalled = true;
}
close() {