Compare commits

...
98 Commits
Author SHA1 Message Date
Daniel dbd9876b8b Merge pull request #120 from Alauraa/patch-2
Updated to reflect Bug #118
2018-06-24 19:19:08 +02:00
Daniel cfa52dbcca Clarified a few things 2018-06-24 19:18:52 +02:00
Alaura 8f8436a809 Updated to reflect Bug #118
Updated to reflect Bug #118 

Adds the node client.js manage shared mods add clusterio To line 95
2018-06-24 11:48:59 -05:00
Daniel Vestøl 8267dae10e #85 update config.ini with absolute paths 2018-06-24 15:00:48 +02:00
Daniel Vestøl d177fb7304 client.js exits after creating and instance, better comments 2018-06-24 14:24:36 +02:00
Daniel Vestøl 7d99162072 Merge branch '#85' 2018-06-24 13:57:24 +02:00
Daniel 2a8228c5a9 Merge pull request #117 from Alauraa/patch-1
Updated Readme with npmPostinstall.js.
2018-06-23 13:56:39 +02:00
Alaura b86d4fa2ab Updated Readme with npmPostinstall.js.
Updated Readme with npmPostinstall.js.

Made it so after you npm install, it adds all the required files the server needs and allows you to run it using node. Without errors.
2018-06-23 06:55:43 -05:00
Daniel Vestøl b783ccf2cb Changed default config values 2018-06-20 14:24:15 +02:00
Daniel Vestøl 010e5271ea Might have fixed bug related to mod uploads 2018-06-19 23:58:56 +02:00
Daniel Vestøl 447766c752 Fixed mod manager not exiting after downloading mods 2018-06-16 15:41:06 +02:00
Daniel Vestøl e4c276054b Don't spam internal errors when server is paused/shutting down 2018-06-13 20:59:39 +02:00
Daniel Vestøl b7f76bf451 resolves #90 - Added ups and gametick reporting to prometheus (/metrics) 2018-06-12 23:01:42 +02:00
Daniel Vestøl 780feb7eb8 Prevented \n in playerCount, webinterface function to get queryString params added 2018-06-10 09:53:22 +02:00
Daniel Vestøl b98cb9def6 Added icon to web interface, made by z4rg0n 2018-06-09 12:45:47 +02:00
Daniel Vestøl 1b6b0abaeb Decreased master load time by 30%, web postJSON now returns a promise 2018-06-09 00:00:18 +02:00
Daniel Vestøl ea8f819bd5 Fixes for last commit 2018-06-08 13:57:52 +02:00
Daniel Vestøl 94bbf62c26 Added support for both https AND http, config is now untracked by git 2018-06-08 09:33:19 +02:00
Daniel Vestøl a9532dd0d3 Merge branch 'master' into https 2018-06-08 09:11:38 +02:00
Daniel Vestøl 034db54152 Added lots of web interface related stuff for external plugin 2018-06-06 21:33:08 +02:00
Daniel Vestøl 17b5967b0f Added mention about masterAuthToken in the readme 2018-06-06 12:57:17 +02:00
Daniel Vestøl 226e157f69 Added onExit handler to masterPlugins (see description)
To use the onExit handler, simply add a method called onExit to your exported class. If it returns a promise, the master will wait for it to resolve.
2018-06-06 12:54:11 +02:00
Daniel Vestøl f72ea7357e Removed debug line that put auth info in the logs 2018-06-03 17:52:21 +02:00
Danielv123 812265d0fd messageInterface now returns a promise as well 2018-05-31 21:18:05 +02:00
Danielv123 da6f96a34b UPSdisplay now uses rcon return value instead of game.write_file 2018-05-31 21:15:34 +02:00
Danielv123 88e8ab454f Possibility of having master-only plugins 2018-05-31 21:03:05 +02:00
Danielv123 e87c702cf2 Added plugin functionality to master 2018-05-31 21:02:47 +02:00
Danielv123 e6c706a83e globalChat no longer breaks when the master is restarted 2018-05-26 13:16:59 +02:00
Danielv123 47b74a07d1 Merge remote-tracking branch 'origin/master' into dev 2018-05-26 12:52:40 +02:00
Daniel 0ee2c5c2f6 Resolves #103 2018-05-26 12:50:15 +02:00
Danielv123 e73c5055af !info to see servername (issue #101) 2018-05-21 00:00:42 +02:00
Danielv123 165cfffecb Use rcon.print instead of writefile for playercount reports 2018-05-13 09:58:25 +02:00
Danielv123 ce6dc3af28 messageInterface now takes a second arg for rcon return values (from rcon.print()) 2018-05-10 14:15:03 +02:00
Danielv123 1d9b6a26a6 Attempted fix to #97 2018-05-10 10:33:26 +02:00
Daniel db89099b2d Merge pull request #95 from Elbrasch/master
Issue #86
2018-05-09 11:12:48 +02:00
Stefan Asbeck 973f0b0e0a Create new random masterAuthSecret if secret-api-token.txt doesn't exist. 2018-05-08 22:04:02 +02:00
Danielv123 0b81fb6351 Made factorio command ratelimiting configurable 2018-05-07 18:11:46 +02:00
Danielv123 ac55b1c442 Prometheus track dole factor for imports 2018-05-06 20:45:52 +02:00
Danielv123 f07b6160bb Merge branch 'master' into dev 2018-05-06 16:07:51 +02:00
Daniel 480e2af03e Merge pull request #93 from Elbrasch/master
fixes issue #87 "Mod manager downloads oldest version of mod availabl…
2018-05-06 16:06:48 +02:00
Stefan Asbeck 8fe908aa5d fixes issue #87 "Mod manager downloads oldest version of mod available" by downloading the latest version
added factorio version to config file instead of hardcoding it.
2018-05-06 15:55:23 +02:00
Danielv123 cf1a97ac9f Added caching to /api/slaves (default 5s) 2018-05-06 14:30:03 +02:00
Danielv123 6dd25cb1e8 Don't assume playercount is 0 just because a command fails 2018-05-06 10:32:11 +02:00
Danielv123 ce24a40d8c Prometheus now tracks import/export stats PROPERLY 2018-05-06 10:23:55 +02:00
Danielv123 f5d1847975 Revert "Prometheus now tracks import/export statistics"
This reverts commit f580f06064.
2018-05-06 10:21:49 +02:00
Danielv123 f580f06064 Prometheus now tracks import/export statistics 2018-05-06 10:21:06 +02:00
Danielv123 f5e908eb5d Revert "Fix outdated leveldown dependency"
This reverts commit a6d98db6a9.
2018-05-05 14:34:50 +02:00
Danielv123 a6d98db6a9 Fix outdated leveldown dependency 2018-05-05 13:52:36 +02:00
Danielv123 b49a37e438 Use compression on master 2018-05-05 12:47:16 +02:00
Danielv123 c02bb6d58f #85 made instance directory configurable in config.json 2018-05-04 17:50:20 +02:00
Danielv123 a0e98ecb00 Format numbers with , (1,000) 2018-05-02 20:37:55 +02:00
Danielv123 6816db0628 More detailed Websocket usage statistics 2018-05-02 12:39:33 +02:00
psihius be73f2f3a5 Changed command splitting logic from json data to lua tables 2018-05-01 21:36:38 +03:00
Daniel d07112e85e PR #83 from Valansch: Shout displays instance name now 2018-05-01 14:32:27 +02:00
Valansch 1fede26564 Shout displays instance name now 2018-05-01 12:22:19 +02:00
MCRcortex c7e2308a33 Added commandBuffer 2018-05-01 08:23:49 +10:00
MCRcortex 5c064e4609 Merge pull request #1 from Danielv123/dev
Dev
2018-05-01 08:10:37 +10:00
Danielv123 6514462a1d Increased max command size for imports 2018-04-30 23:47:31 +02:00
Danielv123 1e0c275968 Use rcon instead of stdin on linux as well 2018-04-30 23:34:09 +02:00
Danielv123 968a306c5a Fixed issue where slaves would not register when ran on VM withou MAC 2018-04-30 23:05:55 +02:00
Danielv123 47430b5d28 Remove super exessive logging on master
Also added better prometheus endpoint logging
2018-04-30 14:30:01 +02:00
Danielv123 6e5585c86c Merge remote-tracking branch 'MCRcortex/dev' into dev 2018-04-30 14:26:14 +02:00
MCRcortex 9c9db7882c enabled inventoryFrame command splitting 2018-04-30 22:10:13 +10:00
MCRcortex d6369ba295 reverted stuff to prev commit
reverted stuff back to prev commit until the mod has been updated
2018-04-30 21:46:45 +10:00
MCRcortex f4e03899e7 fixed bug 2018-04-30 21:28:45 +10:00
MCRcortex 0afc3d7e78 split receiveInventory into multiple commands 2018-04-30 20:44:29 +10:00
Danielv123 355e8974a4 Added more websocket utilization metrics 2018-04-30 12:09:01 +02:00
MCRcortex 89ad2baa26 fixed bug
fixed so that it doesnt send when cmd=="["
2018-04-30 19:32:43 +10:00
MCRcortex 57f7dec956 recomments stuff 2018-04-30 18:41:02 +10:00
MCRcortex 5b59abb8e3 changeds timings 2018-04-30 18:38:48 +10:00
MCRcortex 66b64eae1b started fixing receiveInventory 2018-04-30 18:37:08 +10:00
Danielv123 f15ce3fb23 No longer auto install clusterio 1.0.0 2018-04-29 21:45:22 +02:00
Daniel 41541b01f0 Merge pull request #80 from Danielv123/snyk-fix-c5e06e84
[Snyk Update] New fixes for 4 vulnerable dependency paths
2018-04-29 11:46:54 +02:00
MCRcortex c5b55f27b7 fixed brakets v2 2018-04-29 19:42:19 +10:00
MCRcortex a67a1d3d1d fixed brakets 2018-04-29 19:30:44 +10:00
MCRcortex bcd1ec79cb fixed to get items 2018-04-29 18:51:08 +10:00
MCRcortex 594c994560 Tried to fix desyncs further
based on factorio networking stuff
2018-04-29 13:52:11 +10:00
MCRcortex f10d6bbfb5 tried to fix desyncs 2018-04-29 10:29:35 +10:00
Danielv123 1184e64467 Attempt to be even more long command friendly 2018-04-28 19:51:23 +02:00
Danielv123 02ab34dbf4 Don't remove all spaces from !shouts 2018-04-28 15:04:15 +02:00
Danielv123 fc1f3bd077 Attempted fix for command corruption 2018-04-28 14:47:53 +02:00
Danielv123 6a7042c0aa Improved performance of /api/getFactorioLocale with caching 2018-04-28 14:16:34 +02:00
Danielv123 2aff63e4a1 Strip all non a-z 0-9 characters from !shout commands 2018-04-28 03:10:57 +02:00
Danielv123 e38aed08bf Sanitizing !shout chat 2018-04-28 02:19:42 +02:00
Danielv123 1bd87c4cc9 Fixed crash on !shout 2018-04-27 21:58:03 +02:00
Danielv123 dc19457471 Update to master to support server wide alerts 2018-04-27 21:23:36 +02:00
Danielv123 84aaae05c2 Merge remote-tracking branch 'origin/dev' into dev 2018-04-27 11:37:15 +02:00
snyk-bot 73ce30ded4 fix: package.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/npm:hoek:20180212

Latest report for danielv123/factorioclusterio:
https://snyk.io/test/github/danielv123/factorioclusterio
2018-04-27 05:19:56 +00:00
Danielv123 1b00fbb328 Fixed crash when master was not there and we still got some sort of response 2018-04-27 06:22:32 +02:00
Daniel 29c38fc4d0 Merge pull request #79 from psihius/patch-1
README.md update: adjusted command
2018-04-27 06:18:59 +02:00
Arvids Godjuks 2e1f7b7f6d README.md update: adjusted command
Adjusted pm2 start command to have correct instance names in the process list
2018-04-26 23:58:26 +03:00
Danielv123 61e944b061 Attempted to add https support 2018-04-26 18:53:06 +02:00
Danielv123 ce0deef83b Websockets for listening to factorio logs and global chat plugin
You can now !shout things across the cluster
2018-04-26 18:33:29 +02:00
Daniel 641f980741 Merge pull request #77 from psihius/dev
Cluster wide command execution
2018-04-26 16:48:50 +02:00
psihius ebf2fd2204 Added cluster wide command execution + fixed some checks failing due to type mismatch 2018-04-26 15:40:47 +03:00
Danielv123 20e66cbd40 Token auth in researchSync and UPSdisplay 2018-04-25 12:55:03 +02:00
Danielv123 a26189947d Token auth in inventoryImports 2018-04-25 12:50:33 +02:00
Danielv123 0b0b2bc1a9 Token auth for all destructive endpoints 2018-04-25 12:44:31 +02:00
23 changed files with 944 additions and 452 deletions

No files matched your search

+4
View File
@@ -39,6 +39,7 @@ mod_cache.json
static/pictures/spritesheet.js
static/pictures/spritesheet.png
static/external
saves
# weird pattern to ignore sharedPlugins except not ignore some core modules
!sharedPlugins/
@@ -48,3 +49,6 @@ sharedPlugins/*
!sharedPlugins/remoteMap/
!sharedPlugins/remoteCommands/
!sharedPlugins/researchSync/
!sharedPlugins/globalChat/
\.cache-require-paths\.json
+29 -18
View File
@@ -70,26 +70,31 @@ NodeJS does not support EOL ubuntu releases. Make sure you are on the most recen
Master and all slaves:
sudo curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash - \
sudo apt install -y nodejs python-dev git wget curl tar build-essential \
sudo npm install pm2 -g \
git clone https://github.com/Danielv123/factorioClusterio.git \
cd factorioClusterio \
npm install \
curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64 \
sudo curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash -
sudo apt install -y nodejs python-dev git wget curl tar build-essential
sudo npm install pm2 -g
git clone https://github.com/Danielv123/factorioClusterio.git
cd factorioClusterio
curl -o factorio.tar.gz -L https://www.factorio.com/get-download/latest/headless/linux64
tar -xf factorio.tar.gz
npm install
node ./lib/npmPostinstall.js
downloads and installs nodejs, pm2, git and clusterio. To specify a version, change "latest" in the link to a version number like 0.14.21.
*Copy config.json.dist to config.json* - Otherwise it will crash, as you'd expect.
**Master**
pm2 start master.js --name master
**Server Host**
To download the mod for all its non vanilla features and items, (optional)
To download the mod for all its non vanilla features and items, (optional, but very recommended)
node client.js download
node client.js manage shared mods add clusterio
Add the master API key, found in the masters install directory (secret-api-token.txt) to your config.json masterAuthToken field
To create a new instance (its own save, set of mods and config files)
@@ -97,7 +102,7 @@ To create a new instance (its own save, set of mods and config files)
To launch an instance with pm2
pm2 start --name slave client.js -- start [instancename]
pm2 start --name [instancename] client.js -- start [instancename]
use `nano config.json` to change settings.
@@ -129,7 +134,7 @@ There are no requirements for other mods, they can be ran in any configuration a
**Requirements**
download and install nodeJS 6+ from http://nodejs.org
download and install nodeJS 8 or 9 from http://nodejs.org
download and install git from https://git-scm.com/
@@ -139,29 +144,35 @@ reboot when you are done, then proceed to the next steps. *reboots matter*
1. download and run https://puu.sh/toFHl/01eebbb333.bat
2. You do not *need* to follow the given instructions, but you should
2. Copy config.json.dist to config.json
3. type `node master.js`
3. Follow the instructions in the bat file
4. Run `node client.js manage shared mods add clusterio`
5. type `node master.js` to start the server
**Server Host**
1. download and run https://puu.sh/toFHl/01eebbb333.bat
2. Follow the instructions given
2. Copy config.json.dist to config.json
3. Type `node client.js start [instancename]` to create a new instance.
3. Follow the instructions given
4. Type `node client.js start [instancename]` to create a new instance.
To connect to a master server running on a remote machine, open config.json with your favourite text editor (notepad++). You can also set it up to use the official server browser.
Change masterIP to something like 31.152.123.14 (provided by master server owner)
e war
Change masterPort to something like 8080 (provided by master server owner)
Repeat step 3 for more servers on one machine. You should be able to find its port by looking at the slave section on master:8080 (the web interface)
**GameClient**
Fancy game client that does the following steps automatically: [clusterioClient](https://github.com/Danielv123/factorioClusterioClient)
Fancy game client that does the following steps automatically, but is really old so be warned: [clusterioClient](https://github.com/Danielv123/factorioClusterioClient)
1. Download the same version of the mod as the slave is running from [the mod portal](https://mods.factorio.com/mods/Danielv123/clusterio) or [github](https://github.com/Danielv123/factorioClusterioMod
@@ -194,5 +205,5 @@ npm install
3. Download the latest version of the factorioClusterioMod from its github repo
```
node client.js download
node client.js manage shared mods add clusterio
```
+185 -131
View File
@@ -6,7 +6,7 @@ const path = require('path');
const syncRequest = require('sync-request');
const request = require("request");
const ncp = require('ncp').ncp;
const Rcon = require('simple-rcon');
const Rcon = require('rcon-client').Rcon;
const hashFiles = require('hash-files');
const _ = require('underscore');
const deepmerge = require("deepmerge");
@@ -25,8 +25,8 @@ const configManager = require("./lib/manager/configManager.js");
var config = require('./config');
var global = {};
if (!fs.existsSync("./instances/")) {
fs.mkdirSync("instances");
if (!fs.existsSync(config.instanceDirectory)) {
fs.mkdirSync(config.instanceDirectory);
}
if (!fs.existsSync("./sharedPlugins/")) {
fs.mkdirSync("sharedPlugins");
@@ -35,23 +35,44 @@ if (!fs.existsSync("./sharedMods/")) {
fs.mkdirSync("sharedMods");
}
const instance = process.argv[3];
const instancedirectory = './instances/' + instance;
const instancedirectory = config.instanceDirectory + '/' + instance;
const command = process.argv[2];
// Set the process title, shows up as the title of the CMD window on windows
// and as the process name in ps/top on linux.
process.title = "clusterioClient "+instance;
// make sure we have the master access token (can't write to master without it since clusterio 2.0)
if(!config.masterAuthToken || typeof config.masterAuthToken !== "string"){
console.log("ERROR invalid config!");
console.log("Master server now needs an access token for write operations. As clusterio slaves depends \
upon this, please add your token to config.js in the field named masterAuthToken. \
You can retrieve your auth token from the master in secret-api-token.txt after running it once.");
}
const needleOptionsWithTokenAuthHeader = {
headers: {
'x-access-token': config.masterAuthToken
},
};
var instanceInfo = {};
var commandBuffer=[];
// messageInterface Management
setInterval(function(){
let command=commandBuffer.shift();
if(command){
messageInterfaceInternal(command[0], command[1], command[2], command[3]);
}
},config.msBetweenCommands || 50);
// function to handle sending commands into the game
function messageInterface(command, callback) {
async function messageInterfaceInternal(command, callback, resolve, reject) {
// try to save us if you send a buffer instead of string
if(typeof command == "object") {
command = command.toString('utf8');
}
if(process.platform == "linux" && typeof command == "string" && serverprocess) {
if(false && process.platform == "linux" && typeof command == "string" && serverprocess) {
/*
to send to stdin, use:
serverprocess.stdin.write("/c command;\n")
@@ -60,17 +81,28 @@ function messageInterface(command, callback) {
if(typeof callback == "function"){
callback();
}
} else if(typeof command == "string" && client && client.exec && typeof client.exec == "function") {
resolve();
} else if(typeof command == "string" && client && client.send && typeof client.send == "function") {
try {
client.exec(command+"\n", callback);
let str = await client.send(command+"\n");
if(typeof callback == "function") callback(str)
resolve(str)
} catch (err) {
console.log(err);
}
if(typeof callback == "function"){
callback();
console.log("RCON failed, server might be paused or shutting down");
// console.log(err);
// reject(err);
if(typeof callback == "function"){
callback();
}
}
}
}
function messageInterface(command, callback) {
return new Promise((resolve,reject) => {
commandBuffer.push([command,callback, resolve, reject]);
});
}
// handle commandline parameters
if (!command || command == "help" || command == "--help") {
@@ -84,7 +116,7 @@ if (!command || command == "help" || command == "--help") {
console.error("node client.js manage");
process.exit(1);
} else if (command == "list") {
let instanceNames = fileOps.getDirectoriesSync("./instances/");
let instanceNames = fileOps.getDirectoriesSync(config.instanceDirectory);
instanceNames.unshift("Name:");
let longestInstanceName = 0;
// determine longest instance name
@@ -107,7 +139,7 @@ if (!command || command == "help" || command == "--help") {
if(instance.includes("Name:")){
factorioPort = "Port:"
} else {
factorioPort = require("./instances/"+instance+"/config").factorioPort;
factorioPort = require(config.instanceDirectory+"/"+instance+"/config").factorioPort;
}
factorioPorts.push(factorioPort);
});
@@ -126,7 +158,7 @@ if (!command || command == "help" || command == "--help") {
});
displayLines.forEach(line => console.log(line));
process.exit(1);
process.exit(0);
} else if (command == "manage"){
// console.log("Launching mod manager");
//const fullUsage = 'node client.js manage [instance, "shared"] ["mods", "config"] ...';
@@ -143,20 +175,26 @@ if (!command || command == "help" || command == "--help") {
const action = process.argv[5] || "";
if(instance){
if(tool == "mods"){
// allow managing mods
if(action == "list"){
modManager.listMods(instance);
} else if(action == "search"){
modManager.findMods(process.argv[6]);
} else if(action == "add" || action == "download"){
modManager.addMod(process.argv[6], instance);
} else if(action == "remove" || action == "rm" || action == "delete"){
modManager.removeMod(process.argv[6], instance);
} else if(action == "update"){
modManager.updateAllMods();
} else {
usage(instance, tool);
}
(async function(){try{
// allow managing mods
if(action == "list"){
console.log(await modManager.listMods(instance));
} else if(action == "search"){
console.log(await modManager.findMods(process.argv[6]));
} else if(action == "add" || action == "download"){
await modManager.addMod(process.argv[6], instance);
} else if(action == "remove" || action == "rm" || action == "delete"){
await modManager.removeMod(process.argv[6], instance);
} else if(action == "update"){
await modManager.updateAllMods();
} else {
usage(instance, tool);
}
process.exit(0);
}catch(e){
console.log("Got error from modManager:")
console.log(e);
}})();
} else if(tool == "config"){
// allow managing the config
if(action == "list" || action == "show" || action == "display"){
@@ -181,18 +219,18 @@ if (!command || command == "help" || command == "--help") {
console.log('Usage:');
usage(instance);
}
// process.exit(1);
// process.exit(0);
} else if (command == "delete") {
if (!process.argv[3]) {
console.error("Usage: node client.js delete [instance]");
process.exit(1);
} else if (typeof process.argv[3] == "string" && fs.existsSync("./instances/" + process.argv[3]) && process.argv[3] != "/" && process.argv[3] != "") {
fileOps.deleteFolderRecursiveSync("./instances/" + process.argv[3]);
} else if (typeof process.argv[3] == "string" && fs.existsSync(config.instanceDirectory+"/" + process.argv[3]) && process.argv[3] != "/" && process.argv[3] != "") {
fileOps.deleteFolderRecursiveSync(config.instanceDirectory+"/" + process.argv[3]);
console.log("Deleted instance " + process.argv[3]);
process.exit(1);
process.exit(0);
} else {
console.error("Instance not found: " + process.argv[3]);
process.exit(1);
process.exit(0);
}
} else if (command == "download") {
console.log("Downloading mods...");
@@ -206,6 +244,7 @@ if (!command || command == "help" || command == "--help") {
https.get(url, function(response) {
response.pipe(file);
console.log("Downloaded "+name);
process.exit(0);
});
}
} else if (command == "start" && instance === undefined) {
@@ -224,10 +263,10 @@ if (!command || command == "help" || command == "--help") {
fs.mkdirSync(instancedirectory + "/mods/");
fs.mkdirSync(instancedirectory + "/instanceMods/");
// fs.symlinkSync('../../../sharedMods', instancedirectory + "/mods", 'junction') // This is broken because it can only take a file as first argument, not a folder
fs.writeFileSync(instancedirectory + "/config.ini", "[path]\r\n\
read-data=__PATH__executable__/../../data\r\n\
write-data=__PATH__executable__/../../../instances/" + instance + "\r\n\
");
fs.writeFileSync(instancedirectory + `/config.ini`, `[path]\r\n
read-data=${ path.resolve(config.factorioDirectory, "data") }\r\n
write-data=${ path.resolve(config.instanceDirectory, instance) }\r\n
`);
// this line is probably not needed anymore but Im not gonna remove it
fs.copySync('sharedMods', instancedirectory + "/mods");
@@ -271,10 +310,11 @@ write-data=__PATH__executable__/../../../instances/" + instance + "\r\n\
]
);
console.log("Instance created!");
process.exit(0);
} else if (command == "start" && typeof instance == "string" && instance != "/" && fs.existsSync(instancedirectory)) {
// Exit if no instance specified (it should be, just a safeguard);
if(instancedirectory != "./instances/undefined"){
var instanceconfig = require(instancedirectory + '/config');
if(instancedirectory != config.instanceDirectory+"/undefined"){
var instanceconfig = require(path.resolve(instancedirectory,'config'));
instanceconfig.unique = stringUtils.hashCode(instanceconfig.clientPassword);
if(process.env.FACTORIOPORT){
instanceconfig.factorioPort = process.env.FACTORIOPORT;
@@ -306,9 +346,7 @@ write-data=__PATH__executable__/../../../instances/" + instance + "\r\n\
}
// move mods from ./sharedMods to the instances mod directory
try {
fs.mkdirSync(instancedirectory + "/instanceMods/");
} catch(e){}
try{fs.mkdirSync(instancedirectory + "/instanceMods/");}catch(e){}
try{rmdirSync(instancedirectory + "/mods");}catch(e){}
try {
// mods directory that will be emptied (deleted) when closing the server to facilitate seperation of instanceMods and sharedMods
@@ -320,7 +358,7 @@ write-data=__PATH__executable__/../../../instances/" + instance + "\r\n\
fs.copySync(instancedirectory + "/instanceMods", instancedirectory + "/mods");
process.on('SIGINT', function () {
console.log("Caught interrupt signal");
console.log("Caught interrupt signal, sending /quit");
messageInterface("/quit");
});
@@ -328,8 +366,9 @@ write-data=__PATH__executable__/../../../instances/" + instance + "\r\n\
//var serverprocess = child_process.exec(commandline);
fileOps.getNewestFile(instancedirectory + "/saves/", fs.readdirSync(instancedirectory + "/saves/"),function(err, latestSave) {
if(err) {
console.log("Your savefile seems to be missing. This might because you created an instance without having factorio\
installed and configured properly. Try installing factorio and adding your savefile to instances/[instancename]/saves/");
console.error("ERROR!")
console.error("Your savefile seems to be missing. This might because you created an instance without having factorio\
installed and configured properly. Try installing factorio and adding your savefile to instances/[instancename]/saves/\n");
throw err;
}
// implicit global
@@ -358,24 +397,17 @@ write-data=__PATH__executable__/../../../instances/" + instance + "\r\n\
client.close();
client.connect();
}
if(process.platform == "linux"){
// we have to log on linux because linux only shows stdout of the first process launched
console.log('Factorio: ' + data);
}
// we have to do this to make logs visible on linux and in powershell. Causes log duplication for people with CMD.
console.log('Fact: ' + data);
});
serverprocess.stderr.on('data', (chunk) => {
console.log('ERR: ' + chunk);
});
// connect to the server with rcon
if(process.platform != "linux"){
if(true || process.platform != "linux"){
// IP, port, password
client = new Rcon({
host: 'localhost',
port: Number(process.env.RCONPORT) || instanceconfig.clientPort,
password: instanceconfig.clientPassword,
timeout: 0
});
client = new Rcon();
// check the logfile to see if the RCON interface is running as there is no way to continue without it
// we read the log every 2 seconds and stop looping when we start connecting to factorio
@@ -383,28 +415,35 @@ write-data=__PATH__executable__/../../../instances/" + instance + "\r\n\
fs.readFile(instancedirectory+"/factorio-current.log", function (err, data) {
// if (err) console.log(err);
if(data && data.indexOf('Starting RCON interface') > 0){
client.connect();
client.connect({
host: 'localhost',
port: Number(process.env.RCONPORT) || instanceconfig.clientPort,
password: instanceconfig.clientPassword,
timeout: 5000
});
} else {
setTimeout(function(){
checkRcon();
},2000);
},5000);
}
});
}
setTimeout(checkRcon, 5000);
client.on('authenticated', function () {
client.onDidAuthenticate(() => {
console.log('Clusterio | RCON Authenticated!');
instanceManagement(); // start using rcons
}).on('connected', function () {
});
client.onDidConnect(() => {
console.log('Clusterio | RCON Connected!');
// getID();
}).on('disconnected', function () {
});
client.onDidDisconnect(() => {
console.log('Clusterio | RCON Disconnected!');
process.exit(0); // exit because RCON disconnecting is undefined behaviour and we rather just wanna restart now
});
} else if(process.platform == "linux"){
// TODO: Check if this works fine on linux or if it has to be delayed until the server starts properly
// don't open an RCON connection and just use stdio instead, does not work on windows.
instanceManagement();
}
@@ -447,15 +486,20 @@ function instanceManagement() {
if(pluginConfig.binary == "nodePackage"){
// require index.js.main() of plugin and execute it as a class
let pluginClass = require("./sharedPlugins/" + pluginDirectories[I] + "/index.js");
plugins[I] = new pluginClass(combinedConfig, function(data){
plugins[I] = new pluginClass(combinedConfig, async function(data, callback){
if(data.toString('utf8')[0] != "/") {
log("Stdout: " + data.toString('utf8'));
return true;
} else {
messageInterface(data.toString('utf8'));
return messageInterface(data.toString('utf8'), callback);
}
}, { // extra functions to pass in object. Should have done it like this from the start, but won't break backwards compat.
socket, // socket.io connection to master (and ES6 destructuring, yay)
});
if(plugins[I].factorioOutput && typeof plugins[I].factorioOutput === "function"){
// when factorio logs a line, send it to the plugin. This includes things like autosaves, chat, errors etc
serverprocess.stdout.on("data", data => plugins[I].factorioOutput(data.toString()));
}
if(pluginConfig.scriptOutputFileSubscription && typeof pluginConfig.scriptOutputFileSubscription == "string"){
if(global.subscribedFiles[pluginConfig.scriptOutputFileSubscription]) {
// please choose a unique file to subscribe to. If you need plugins to share this interface, set up a direct communication
@@ -487,7 +531,9 @@ function instanceManagement() {
}
for(let i = 0; i < stuff.length; i++) {
if(stuff[i] && !stuff[i].includes('\u0000\u0000')) {
plugins[I].scriptOutput(stuff[i]);
try{
plugins[I].scriptOutput(stuff[i]);
}catch(e){console.log(e)}
}
}
},
@@ -497,7 +543,7 @@ function instanceManagement() {
}
}
console.log("Clusterio | Loaded plugin " + pluginDirectories[i]);
} else if(pluginConfig.binary != "nodePackage"){
} else if(pluginConfig.binary != "nodePackage" && pluginConfig.binary){
// handle as fragile executable plugin
let args = pluginConfig.args || [];
plugins[I]=child_process.spawn(pluginConfig.binary, args, {
@@ -558,6 +604,8 @@ function instanceManagement() {
plugins[i].on('close', (code) => {
log(`child process exited with code ${code}`);
});
} else {
// This plugin doesn't have a client portion
}
}
@@ -566,26 +614,7 @@ function instanceManagement() {
setInterval(getID, 10000);
getID();
function getID() {
messageInterface("/silent-command game.write_file('tempfile.txt', 'connected_players ' .. #game.connected_players .. '\\n', true, 0)", function(err) {setTimeout(function(){
// get array of lines in file
if(fs.existsSync(instancedirectory + "/script-output/tempfile.txt")) {
var data = fs.readFileSync(instancedirectory + "/script-output/tempfile.txt", "utf8").split("\n");
// delete when we are done
fs.unlink(instancedirectory + "/script-output/tempfile.txt", function(){});
}
// if we actually got anything from the file, proceed to categorize it
if (data && data[0]) {
while (data[0]) {
let q = data[0].split(" ");
// delete array element
data.splice(0,1);
if(q[0] == "connected_players" && Number(q[1]) != NaN) {
instanceInfo.playerCount = q[1];
}
}
} else {
instanceInfo.playerCount = 0;
}
messageInterface("/silent-command rcon.print(#game.connected_players)", function(playerCount) {
var payload = {
time: Date.now(),
rconPort: instanceconfig.clientPort,
@@ -594,37 +623,35 @@ function instanceManagement() {
unique: instanceconfig.unique,
publicIP: config.publicIP, // IP of the server should be global for all instances, so we pull that straight from the config
mods:modHashes,
playerCount: instanceInfo.playerCount || 0,
instanceName: instance,
playerCount:playerCount.replace(/(\r\n\t|\n|\r\t)/gm, ""),
}
function callback(err, mac) {
if (err) {
mac = "unknown";
console.log("##### getMac crashed, but we don't really give a shit because we are probably closing down #####");
} else {
payload.mac = mac
console.log("Registered our precense with master "+config.masterIP+" at " + payload.time);
needle.post(config.masterIP + ":" + config.masterPort + '/api/getID', payload, function (err, response, body) {
if (err && err.code != "ECONNRESET"){
console.error("We got problems, something went wrong when contacting master");
console.error(err);
} else if (response && response.body) {
// In the future we might be interested in whether or not we actually manage to send it, but honestly I don't care.
console.log(response.body);
}
});
}
payload.mac = mac;
console.log("Registered our precense with master "+config.masterIP+" at " + payload.time);
needle.post(config.masterIP + ":" + config.masterPort + '/api/getID', payload, needleOptionsWithTokenAuthHeader, function (err, response, body) {
if (err && err.code != "ECONNRESET"){
console.error("We got problems, something went wrong when contacting master");
console.error(err);
} else if (response && response.body) {
// In the future we might be interested in whether or not we actually manage to send it, but honestly I don't care.
console.log(response.body);
}
});
}
if(global.mac){
callback(undefined, global.mac);
} else {
getMac(callback);
}
},1000)});
});
}
});
// Mod uploading and management -----------------------------------------------
// get mod names and hashes
// string: instance, function: callback
@@ -637,17 +664,21 @@ function instanceManagement() {
modName: modHashes[i].modName,
hash: modHashes[i].hash,
}
needle.post(config.masterIP + ":" + config.masterPort + '/api/checkMod', payload, function (err, response, body) {
needle.post(config.masterIP + ":" + config.masterPort + '/api/checkMod', payload, needleOptionsWithTokenAuthHeader, function (err, response, body) {
if(err) console.log(new Error("Unable to contact master server! Please check your config.json."));
if(response && body && body == "found") {
console.log("master has mod");
console.log("master has mod "+modHashes[i].modName);
} else if (response && body && typeof body == "string") {
let mod = response.body;
if(config.uploadModsToMaster){
console.log("Sending mod: " + mod);
// Send mods master says it wants
// response.body is a string which is a modName.zip
var req = request.post("http://"+config.masterIP + ":" + config.masterPort + '/api/uploadMod', function (err, resp, body) {
var req = request.post({url: "http://"+config.masterIP + ":" + config.masterPort + '/api/uploadMod',
headers: {
"x-access-token": config.masterAuthToken,
},
}, function (err, resp, body) {
if (err) {
console.log(new Error("Unable to contact master server! Please check your config.json."));
} else {
@@ -655,7 +686,7 @@ function instanceManagement() {
}
});
var form = req.form();
form.append('file', fs.createReadStream("./instances/"+instance+"/mods/"+mod));
form.append('file', fs.createReadStream(config.instanceDirectory+"/"+instance+"/mods/"+mod));
} else {
console.log("Not sending mod: " + mod + " to master because config.uploadModsToMaster is not enabled")
}
@@ -705,7 +736,7 @@ function instanceManagement() {
}
}
console.log("Recorded flows, copper plate since last time: " + payload["copper-plate"]);
needle.post(config.masterIP + ":" + config.masterPort + '/api/logStats', {timestamp: timestamp, instanceID: instanceconfig.unique,data: payload}, function (err, response, body) {
needle.post(config.masterIP + ":" + config.masterPort + '/api/logStats', {timestamp: timestamp, instanceID: instanceconfig.unique,data: payload}, needleOptionsWithTokenAuthHeader, function (err, response, body) {
// we did it, keep going
});
}
@@ -737,7 +768,7 @@ function instanceManagement() {
count: g[1],
instanceName: instance, // name of instance
instanceID: instanceconfig.unique, // a hash computed from the randomly generated rcon password
}, function (err, resp, body) {
}, needleOptionsWithTokenAuthHeader, function (err, resp, body) {
if(body == "failure") console.error("#### Export failed! Lost: "+g[1]+" "+g[0]);
if(config.logItemTransfers){
if(body == "success") console.log(`Exported ${g[1]} ${g[0]} to master`);
@@ -794,12 +825,10 @@ function instanceManagement() {
// request our items, one item at a time
for (let i = 0; i < Object.keys(preparedPackage).length; i++) {
// console.log(preparedPackage[Object.keys(preparedPackage)[i]]);
needle.post(config.masterIP + ":" + config.masterPort + '/api/remove', preparedPackage[Object.keys(preparedPackage)[i]], function (err, response, body) {
needle.post(config.masterIP + ":" + config.masterPort + '/api/remove', preparedPackage[Object.keys(preparedPackage)[i]], needleOptionsWithTokenAuthHeader, function (err, response, body) {
if (response && response.body && typeof response.body == "object") {
// buffer confirmed orders
confirmedOrders[confirmedOrders.length] = {
[response.body.name]: response.body.count,
}
confirmedOrders[confirmedOrders.length] = {name:response.body.name,count:response.body.count}
if(config.logItemTransfers){
console.log(`Imported ${response.body.count} ${response.body.name} from master`);
}
@@ -808,11 +837,21 @@ function instanceManagement() {
}
// if we got some confirmed orders
// console.log("Importing " + confirmedOrders.length + " items! " + JSON.stringify(confirmedOrders));
sadas = JSON.stringify(confirmedOrders);
confirmedOrders = [];
// send our RCON command with whatever we got
messageInterface("/silent-command remote.call('clusterio', 'importMany', '" + sadas + "')");
sadas = null;
//if (!(confirmedOrders.length>0)){return;}
let cmd="local t={";
for(let i=0;i<confirmedOrders.length;i++)
{
cmd+='["'+confirmedOrders[i].name+'"]='+confirmedOrders[i].count+',';
if(cmd.length>320) // Factorio max packet size is 508
{
messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+cmd.slice(0, -1)+"}"+ " for k, item in pairs(t) do GiveItemsToStorage(k, item) end')");
cmd="local t={";
}
}
if (!(cmd==="local t={")){
messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+cmd.slice(0, -1)+"}"+ " for k, item in pairs(t) do GiveItemsToStorage(k, item) end')");
}
confirmedOrders=[];
}
}, 1000);
// COMBINATOR SIGNALS ---------------------------------------------------------
@@ -826,22 +865,37 @@ function instanceManagement() {
if(Buffer.isBuffer(response.body)) {console.log(response.body.toString("utf-8")); throw new Error();} // We are probably contacting the wrong webserver
try {
var inventory = JSON.parse(response.body);
var inventoryFrame = {};
for (let i = 0; i < inventory.length; i++) {
inventoryFrame[inventory[i].name] = Number(inventory[i].count);
if(inventoryFrame[inventory[i].name] >= Math.pow(2, 31)){
inventoryFrame[inventory[i].name] = Math.pow(2, 30); // set it waaay lower, 31 -1 would probably suffice
}
}
inventoryFrame["signal-unixtime"] = Math.floor(Date.now()/1000);
// console.log("RCONing inventory! " + JSON.stringify(inventoryFrame));
let first = true;
let cmd="local s={";
for (let key in inventoryFrame)
{
cmd+='["'+key+'"]='+inventoryFrame[key]+",";
if(first && cmd.length>300 || !first && cmd.length > 320) // Factorio max packet size is 508
{
messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+(first ? 'global.ticksSinceMasterPinged=0 ':'')+cmd.slice(0, -1)+"}"+ " for name,count in pairs(s) do global.invdata[name]=count end')");
cmd="local s={";
first = false;
}
}
if (!(cmd==="local s={")){
messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+(first ? 'global.ticksSinceMasterPinged=0 ':'')+cmd.slice(0, -1)+"}"+ " for name,count in pairs(s) do global.invdata[name]=count end')");
}
messageInterface("/silent-command remote.call('clusterio', 'runcode', 'UpdateInvCombinators()')");
} catch (e){
console.log(e);
}
var inventoryFrame = {};
for (let i = 0; i < inventory.length; i++) {
inventoryFrame[inventory[i].name] = Number(inventory[i].count);
if(inventoryFrame[inventory[i].name] >= Math.pow(2, 31)){
inventoryFrame[inventory[i].name] = Math.pow(2, 30); // set it waaay lower, 31 -1 would probably suffice
}
}
inventoryFrame["signal-unixtime"] = Math.floor(Date.now()/1000);
// console.log("RCONing inventory! " + JSON.stringify(inventoryFrame));
messageInterface("/silent-command remote.call('clusterio', 'receiveInventory', '" + JSON.stringify(inventoryFrame) + "')");
}
});
}, 1000);
}, 1550);
// Make sure world has its worldID
setTimeout(function(){
messageInterface("/silent-command remote.call('clusterio','setWorldID',"+instanceconfig.unique+")")
@@ -917,7 +971,7 @@ function hashMods(instanceName, callback) {
}
let hashedMods = [];
/*let mods = fs.readdirSync("./sharedMods/")*/
let instanceMods = fs.readdirSync("./instances/"+instanceName+"/mods/");
let instanceMods = fs.readdirSync(config.instanceDirectory+"/"+instanceName+"/mods/");
if(instanceMods.length == 0){
callback({}); // there are no mods installed, return an empty object
}
@@ -927,7 +981,7 @@ function hashMods(instanceName, callback) {
}
}
for(let i=0; i<instanceMods.length; i++){
let path = "./instances/"+instanceName+"/mods/"+instanceMods[i];
let path = config.instanceDirectory+"/"+instanceName+"/mods/"+instanceMods[i];
let name = instanceMods[i];
let options = {
files:path,
+18 -5
View File
@@ -1,25 +1,37 @@
{
"__comment": "Where factorio folder. Should be in the same folder as this config file.",
"factorioDirectory": "factorio",
"instanceDirectory": "instances",
"__comment1": "IP players should connect to to join your servers, should be portforwarded and stuff",
"publicIP": "localhost",
"__comment2": "IP the master web server runs at, can be ignored on the master system",
"masterIP": "localhost",
"__commentMasterPort": "HTTP port for the master to listen to, set to 0 or an empty string to not listen for HTTP requests",
"masterPort": 8080,
"__commentsslPort": "HTTPS port for the master to listen to. Set to 0 or an empty string to not use HTTPS.",
"sslPort": 443,
"sslCert": "database/certificates/cert.crt",
"sslPrivKey": "database/certificates/cert.key",
"__commentMasterAuthToken": "Authentication token to connect to the master server. Find it in secret-api-token.txt on the master.",
"masterAuthToken": "",
"__commentAuthSecret": "This is a long random string generated by keyboard mashing. Please mash you own string for security.",
"masterAuthSecret": "anfeåpvunuennuusnvzunsrevunåzeufnewnufasiojfåqwoiuajknwkjfdabewfkjadfnewåfundjkcvnewmfqewrmn123",
"__commentAuthSecret": "This is a long random string used for generating authentication tokens. Do not share. Regenerates when secret-api-token.txt is deleted.",
"masterAuthSecret": "",
"__comment3": "Your factorio.com username for factorio MP services, also username will be admin",
"__comment3": "Your factorio.com username for factorio MP services, also username will be admin on new instances",
"username": "",
"__comment4": "Token from your factorio.com profile page. Required for MP and mod manager services.",
"token": "",
"__comment7": "Used for communication with the mod portal, not needed if you manage mods by hand",
"factorio_version": "0.16.41",
"__comment5": "General user settings",
"verify_user_identity": false,
"verify_user_identity": true,
"auto_pause": false,
"allow_commands": "admins-only",
"game_password": "",
@@ -35,5 +47,6 @@
},
"logItemTransfers": false,
"uploadModsToMaster": true,
"allowRemoteCommandExecution": false
"msBetweenCommands": 10,
"allowRemoteCommandExecution": true
}
+15
View File
@@ -0,0 +1,15 @@
const jwt = require('jsonwebtoken');
const config = require("./../config");
var middleware = function (req, res, next) {
var token = req.headers['x-access-token'];
if(!token) return res.status(401).send({ auth: false, message: 'No token provided.', endpoint: req.route.path});
jwt.verify(token, config.masterAuthSecret, function(err, decoded) {
if(err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.', endpoint: req.route.path});
next();
});
}
module.exports = {middleware};
+17 -9
View File
@@ -6,6 +6,8 @@ Module for grabbing factorio locale from a local factorio install
const fs = require("fs");
const ini = require("ini");
const cache = {};
/**
Gets raw factorio locale from the .ini file.
Not available outside of module through exports.
@@ -32,14 +34,20 @@ Gets factorios locale as an object, does not respect mods.
*/
module.exports.asObject = function getLocaleAsObject(factorioDirectory, languageCode, callback){
if(typeof factorioDirectory != "string" || typeof languageCode != "string" || typeof callback != "function"){
throw "Error: wrong parameters provided";
throw new Error("Error: wrong parameters provided");
}
if(!cache[languageCode]){
getLocale(factorioDirectory, languageCode, (err, rawLocale) => {
if(err){
callback(err);
} else {
let processedLocale = ini.parse(rawLocale);
cache[languageCode] = processedLocale;
callback(undefined, processedLocale);
}
});
} else {
callback(undefined, cache[languageCode]);
}
getLocale(factorioDirectory, languageCode, (err, rawLocale) => {
if(err){
callback(err);
} else {
let processedLocale = ini.parse(rawLocale)
callback(undefined, processedLocale);
}
});
}
+2 -1
View File
@@ -1,10 +1,11 @@
const fileOps = require("_app/fileOps");
const config = require("./../config");
module.exports = {
sync: function(){
let instances;
try {
instances = fileOps.getDirectoriesSync("./instances/");
instances = fileOps.getDirectoriesSync(config.instanceDirectory);
} catch (e) {
// there are no instances created yet, probably missing the folder
instances = [];
+178 -143
View File
@@ -11,6 +11,7 @@ const fs = require("fs");
const https = require('follow-redirects').https;
const factorioAPI = require("node-factorio-api");
const sLog = require("single-line-log").stdout;
const query = require('cli-interact').getYesNo;
const fileOps = require("_app/fileOps");
const config = require("./../../config");
@@ -35,56 +36,62 @@ modManager.listMods("shared");
// console.logs mods located in /sharedMods as a nice table
*/
module.exports.listMods = function(instance){
if(instance && instances.includes(instance)){
factorioAPI.init(false, 'instances/'+instance+'/instanceMods', 'instances/'+instance+'/saves', '0.16.15');
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(token => {
console.log("* Currently installed:");
factorioAPI.readModZips().then(list => {
console.log(asTable(list.map(mod => {
return {
name:(mod.title || mod.name),
version:mod.version,
date:mod.date,
factorio_ver:mod.factorio_version,
author:mod.author,
}
})));
return new Promise((resolve, reject) => {
if(instance && instances.includes(instance)){
factorioAPI.init(false, config.instanceDirectory+'/'+instance+'/instanceMods', config.instanceDirectory+'/'+instance+'/saves', config.factorio_version);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(token => {
console.log("* Currently installed:");
factorioAPI.readModZips().then(list => {
console.log(asTable(list.map(mod => {
return {
name:(mod.title || mod.name),
version:mod.version,
date:mod.date,
factorio_ver:mod.factorio_version,
author:mod.author,
}
})));
});
// Had issues earlier due to bug in node-factorio-api, see https://github.com/Danacus/node-factorio-api/issues/2
// bug is resolved, thanks to @Danacus and @brotherpot
factorioAPI.getModsFromSaves().then(list => {
console.log("* Mods used in save:");
console.log(asTable(list[0].mods))
});
}).catch(reason => {
console.log("Failed to authenticate to factorio.com! Please check your connection and the username+token used for authentication in config.json");
resolve();
});
// Had issues earlier due to bug in node-factorio-api, see https://github.com/Danacus/node-factorio-api/issues/2
// bug is resolved, thanks to @Danacus and @brotherpot
factorioAPI.getModsFromSaves().then(list => {
console.log("* Mods used in save:");
console.log(asTable(list[0].mods))
} else {
// list shared mods in /sharedMods/
factorioAPI.init(false, 'sharedMods/', '', config.factorio_version);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(() => {
console.log("\r\n")
console.log("* Mods shared by all instances:")
factorioAPI.getInstalledMods().then(mods => {
console.log(asTable(mods));
resolve();
});
}).catch(e => {
reject(new Error(e));
});
}).catch(reason => {
console.log("Failed to authenticate to factorio.com! Please check your connection and the username+token used for authentication in config.json");
});
} else {
// list shared mods in /sharedMods/
factorioAPI.init(false, 'sharedMods/', '', '0.16.15');
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(() => {
console.log("\r\n")
console.log("* Mods shared by all instances:")
factorioAPI.getInstalledMods().then(mods => {
console.log(asTable(mods));
});
});
}
// get JSON data about GH releases
/*let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
console.log(JSON.parse(res.getBody()))
*/
}
// get JSON data about GH releases
/*let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
console.log(JSON.parse(res.getBody()))
*/
});
}
/**
Searches mods.factorio.com for mods matching a search term and prints
@@ -93,115 +100,143 @@ the results to console as a neat looking table
@param {string} searchTerm What to search for
*/
module.exports.findMods = function(searchTerm){
factorioAPI.init(false);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(token => {
factorioAPI.searchMods(searchTerm).then(mods => {
console.log(asTable(mods.reverse().map(mod => {
return {
title:ellipse(mod.title, 50),
name:ellipse(mod.name, 30),
downloads:mod.downloads_count,
updated: moment(mod.updated_at).format("DD.MM.YY"),
owner:mod.owner,
//license: mod.license_name,
}
})))
})
return new Promise((resolve, reject) => {
factorioAPI.init(false);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(token => {
factorioAPI.searchMods(searchTerm).then(mods => {
let result = asTable(mods.reverse().map(mod => {
return {
title:ellipse(mod.title, 50),
name:ellipse(mod.name, 30),
downloads:mod.downloads_count,
updated: moment(mod.updated_at).format("DD.MM.YY"),
latest_version: mod.latest_release.version,
owner:mod.owner,
//license: mod.license_name,
}
}));
resolve(result);
// console.log(result);
});
}).catch(e => {
if(e.contains("Insufficient information")) console.log("We are missing some parameters required for logging in! Make sure your factorio username and token are specified in config.json.");
reject(new Error(e));
});
});
}
module.exports.addMod = function(term, instance){
if(instance == "shared"){ // modDirectory has to end in /
var modDirectory = "sharedMods/";
} else {
var modDirectory = "instances/"+instance+"/instanceMods/";
}
if(term && term.length > 0){
if(term.includes("mods.factorio.com/mods/")){ // https://mods.factorio.com/mods/Danielv123/ChooChoo
// treat it as a link download
factorioAPI.init(false, modDirectory, "", "0.16.15");
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(() => {
factorioAPI.searchMods(term.split("/")[term.split("/").length-1]).then(body => {
console.log("Im gonna download: "+body[0].name);
factorioAPI.downloadMod(body[0]).then(()=>{
console.log("Mod downloaded");
return new Promise((resolve, reject) => {
if(instance == "shared"){ // modDirectory has to end in /
var modDirectory = "sharedMods/";
} else {
var modDirectory = config.instanceDirectory+"/"+instance+"/instanceMods/";
}
if(term && term.length > 0){
if(term.includes("mods.factorio.com/mods/")){ // https://mods.factorio.com/mods/Danielv123/ChooChoo
// treat it as a link download
factorioAPI.init(false, modDirectory, "", config.factorio_version);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(() => {
factorioAPI.searchMods(term.split("/")[term.split("/").length-1]).then(body => {
console.log("Im gonna download: "+body[0].name + " in version: " + body[0].latest_release.version);
factorioAPI.downloadMod( {name: body[0].name, version: body[0].latest_release.version}).then(()=>{
console.log("Mod downloaded");
resolve();
});
});
});
});
} else if(term.includes("github.com")){ // https://github.com/Danielv123/factorioClusterioMod
// treat it as a github releases download
let apiURL = "https://api.github.com/repos/" + term.split("/")[3] + "/" + term.split("/")[4] + "/releases"; // https://api.github.com/repos/Danielv123/factorioClusterioMod/releases
let res = syncRequest('GET', apiURL, {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
if(url) {
console.log("Downloading " + url);
let file = fs.createWriteStream(modDirectory + name);
let bytesDownloaded = 0;
let downloadStartTime = Date.now();
https.get(url, function(response) {
let bytesToDownload = response.headers['content-length'];
response.pipe(file);
response.on("data", chunk => {
bytesDownloaded += chunk.length;
let downloadTimeElapsed = Date.now() - downloadStartTime;
let bytesPerSecond = bytesDownloaded / (downloadTimeElapsed/1000);
let timeToComplete = Math.floor((bytesToDownload - bytesDownloaded)/bytesPerSecond);
sLog("Downloading "+name+" "+(bytesDownloaded/bytesToDownload*100).toPrecision(3)+"% ("+formatBytes(bytesDownloaded)+" / "+formatBytes(bytesToDownload)+")\r\n"+
"about "+timeToComplete+" seconds remaining ("+formatBytes(bytesPerSecond)+"/s)");
}).on("end", x => {
sLog.clear();
console.log(" Downloaded in "+moment.utc(moment.duration(Date.now()-downloadStartTime).as('milliseconds')).format('mm:ss')); // because sLog.clear() doesn't seem to do the job of ending the line properly
} else if(term.includes("github.com")){ // https://github.com/Danielv123/factorioClusterioMod
// treat it as a github releases download
let apiURL = "https://api.github.com/repos/" + term.split("/")[3] + "/" + term.split("/")[4] + "/releases"; // https://api.github.com/repos/Danielv123/factorioClusterioMod/releases
let res = syncRequest('GET', apiURL, {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
let name = JSON.parse(res.getBody())[0].assets[0].name;
if(url) {
console.log("Downloading " + url);
let file = fs.createWriteStream(modDirectory + name);
let bytesDownloaded = 0;
let downloadStartTime = Date.now();
https.get(url, function(response) {
let bytesToDownload = response.headers['content-length'];
response.pipe(file);
response.on("data", chunk => {
bytesDownloaded += chunk.length;
let downloadTimeElapsed = Date.now() - downloadStartTime;
let bytesPerSecond = bytesDownloaded / (downloadTimeElapsed/1000);
let timeToComplete = Math.floor((bytesToDownload - bytesDownloaded)/bytesPerSecond);
sLog("Downloading "+name+" "+(bytesDownloaded/bytesToDownload*100).toPrecision(3)+"% ("+formatBytes(bytesDownloaded)+" / "+formatBytes(bytesToDownload)+")\r\n"+
"about "+timeToComplete+" seconds remaining ("+formatBytes(bytesPerSecond)+"/s)");
}).on("end", x => {
sLog.clear();
console.log(" Downloaded in "+moment.utc(moment.duration(Date.now()-downloadStartTime).as('milliseconds')).format('mm:ss')); // because sLog.clear() doesn't seem to do the job of ending the line properly
resolve();
});
});
}
} else {
// treat it as a search and download the top result
factorioAPI.init(false, modDirectory, "", config.factorio_version);
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(() => {
factorioAPI.searchMods(term).then(body => {
console.log(body)
console.log("Im gonna download: "+body[0].name + " in version: " + body[0].latest_release.version);
factorioAPI.downloadMod( {name: body[0].name, version: body[0].latest_release.version}).then(()=>{
console.log("Mod downloaded");
resolve();
});
});
});
}
} else {
// treat it as a search and download the top result
factorioAPI.init(false, modDirectory, "", "0.16.15");
factorioAPI.authenticate({
username: config.username,
token: config.token,
require_ownership: false
}).then(() => {
factorioAPI.searchMods(term).then(body => {
console.log("Im gonna download: "+body[0].name);
factorioAPI.downloadMod(body[0]).then(()=>{
console.log("Mod downloaded");
});
});
});
reject(new Error("Invalid search term"));
}
}
});
}
module.exports.removeMod = function removeMod(term, instance, force = false){
if(instance == "shared"){ // modDirectory has to end in /
var modDirectory = "sharedMods/";
} else {
var modDirectory = "instances/"+instance+"/instanceMods/";
}
if(term && typeof term == "string" && term.length > 0){
let mods = fs.readdirSync(modDirectory);
let modsMatchingFilter = mods.filter(mod => mod.toLowerCase().includes(term.toLowerCase()));
if(modsMatchingFilter.length < 1){
console.log("No mods found matching search");
} else if(modsMatchingFilter.length == 1){
console.log("Deleting "+modsMatchingFilter[0]);
fs.unlinkSync(modDirectory+modsMatchingFilter[0]);
console.log("Deleted.");
} else if(modsMatchingFilter.length > 1){
console.log("Found multiple mods, not doing anything");
modsMatchingFilter.forEach(mod => console.log(mod));
return new Promise((resolve, reject) => {
if(instance == "shared"){ // modDirectory has to end in /
var modDirectory = "sharedMods/";
} else {
var modDirectory = config.instanceDirectory+"/"+instance+"/instanceMods/";
}
}
if(term && typeof term == "string" && term.length > 0){
let mods = fs.readdirSync(modDirectory);
let modsMatchingFilter = mods.filter(mod => mod.toLowerCase().includes(term.toLowerCase()));
if(modsMatchingFilter.length < 1){
console.log("No mods found matching search");
} else if(modsMatchingFilter.length == 1){
console.log("Deleting "+modsMatchingFilter[0]);
fs.unlinkSync(modDirectory+modsMatchingFilter[0]);
console.log("Deleted.");
} else if(modsMatchingFilter.length > 1){
console.log("Found multiple mods");
modsMatchingFilter.forEach(mod => console.log(mod));
if(query("Delete all mods matching filter?")){
let modsToDelete = modsMatchingFilter.length;
for(let i = 0; i < modsToDelete; i++){
fs.unlinkSync(modDirectory+modsMatchingFilter[i]);
console.log("Deleted "+modsMatchingFilter[i]);
}
} else console.log("Did not delete mods.")
}
resolve();
} else {
reject(new Error("Invalid search term"));
}
});
}
// 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases'
+1 -1
View File
@@ -5,7 +5,7 @@ var fs = require('fs');
var modManager = require("./manager/modManager.js");
modManager.addMod("clusterio", "shared");
// modManager.addMod("clusterio", "shared");
function makeDirectory(path){
return new Promise((resolve, reject) => {
+289 -78
View File
@@ -20,13 +20,21 @@ process.on('unhandledRejection', r => console.log(r));
// configgy stuff
debug = false;
// reduces loadtime from 1700 ms to 1200 ms on my i3 + SSD
// tested by logging Date.now() here and when the webserver started listening
require('cache-require-paths');
// constants
const masterModFolder = "./database/masterMods/";
var config = require('./config');
const config = require('./config');
// homebrew modules
const getFactorioLocale = require("./lib/getFactorioLocale");
const stringUtils = require("./lib/stringUtils");
const fileOps = require("_app/fileOps");
// homemade express middleware for token auth
const authenticate = require("./lib/authenticate");
// Library for create folder recursively if it does not exist
const mkdirp = require("mkdirp");
@@ -37,10 +45,34 @@ const deepmerge = require("deepmerge");
const path = require("path");
const fs = require("fs");
const nedb = require("nedb");
const https = require("https");
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const base64url = require('base64url');
/** Sync */
function randomStringAsBase64Url(size) {
return base64url(crypto.randomBytes(size));
}
if (!fs.existsSync("secret-api-token.txt")) {
config.masterAuthSecret = randomStringAsBase64Url(256);
fs.writeFileSync("config.json",JSON.stringify(config, null, 4));
fs.writeFileSync("secret-api-token.txt", jwt.sign({ id: "api" }, config.masterAuthSecret, {
expiresIn: 86400*365 // expires in 1 year
}));
console.log("Generated new master authentication private key!");
process.exit(0);
}
// write an auth token to file
fs.writeFileSync("secret-api-token.txt", jwt.sign({ id: "api" }, config.masterAuthSecret, {
expiresIn: 86400*365 // expires in 1 year
}));
const express = require("express");
const compression = require('compression');
const ejs = require("ejs");
// Required for express post requests
const bodyParser = require("body-parser");
@@ -48,18 +80,28 @@ const fileUpload = require('express-fileupload');
var app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.urlencoded({
parameterLimit: 100000,
limit: '10mb',
extended: true
}));
app.use(fileUpload());
app.use(compression());
// dynamic HTML generations with EJS
app.set('views', path.join(__dirname, 'static'));
app.set('view engine', 'html');
app.engine('html', ejs.renderFile);
var routes = require("./routes.js");
routes(app);
// give ejs access to some interesting information
app.use(function(req, res, next){
res.locals.res = res;
res.locals.req = req;
res.locals.masterPlugins = masterPlugins;
next();
});
require("./routes.js")(app);
// Set folder to serve static content from (the website)
app.use(express.static('static'));
// mod downloads
@@ -88,18 +130,43 @@ const prometheusConnectedInstancesCounter = new Prometheus.Gauge({
const prometheusWsUsageCounter = new Prometheus.Counter({
name: prometheusPrefix+'websocket_usage_counter',
help: 'Websocket traffic',
labelNames: ["connectionType"],
labelNames: ["connectionType", "instanceID"],
});
const prometheusProductionGauge = new Prometheus.Gauge({
name: prometheusPrefix+'production_gauge',
help: 'Items produced by instance',
labelNames: ["instanceID", "itemName"],
});
const prometheusExportGauge = new Prometheus.Gauge({
name: prometheusPrefix+'export_gauge',
help: 'Items exported by instance',
labelNames: ["instanceID", "itemName"],
});
const prometheusImportGauge = new Prometheus.Gauge({
name: prometheusPrefix+'import_gauge',
help: 'Items imported by instance',
labelNames: ["instanceID", "itemName"],
});
const prometheusDoleFactorGauge = new Prometheus.Gauge({
name: prometheusPrefix+'dole_factor_gauge',
help: 'The current dole division factor for this item',
labelNames: ["itemName"],
});
const prometheusPlayerCountGauge = new Prometheus.Gauge({
name: prometheusPrefix+'player_count_gauge',
help: 'Amount of players connected to this cluster',
labelNames: ["instanceID", "instanceName"],
});
const prometheusUPSGauge = new Prometheus.Gauge({
name: prometheusPrefix+'UPS_gauge',
help: 'UPS of the current server',
labelNames: ["instanceID", "instanceName"],
});
const prometheusTickGauge = new Prometheus.Gauge({
name: prometheusPrefix+'tick_gauge',
help: 'Servers current gametick',
labelNames: ["instanceID", "instanceName"],
});
const prometheusMasterInventoryGauge = new Prometheus.Gauge({
name: prometheusPrefix+'master_inventory_gauge',
help: 'Amount of items stored on master',
@@ -124,10 +191,17 @@ app.get('/metrics', (req, res) => {
res.set('Content-Type', Prometheus.register.contentType);
/// gather some static metrics
// playercount
for(let instanceID in slaves){try{
prometheusPlayerCountGauge.labels(instanceID, slaves[instanceID].instanceName).set(Number(slaves[instanceID].playerCount) || 0);
}catch(e){}}
for(let instanceID in slaves){
// playercount
try{
prometheusPlayerCountGauge.labels(instanceID, slaves[instanceID].instanceName).set(Number(slaves[instanceID].playerCount) || 0);
}catch(e){}
// UPS
try{
prometheusUPSGauge.labels(instanceID, slaves[instanceID].instanceName).set(Number(slaves[instanceID].meta.UPS) || 60);
if(slaves[instanceID].meta.tick && typeof slaves[instanceID].meta.tick === "number") prometheusUPSGauge.labels(instanceID, slaves[instanceID].instanceName).set(Number(slaves[instanceID].meta.tick) || 0);
}catch(e){}
}
// inventory
for(let key in db.items){
if(typeof db.items[key] == "number" || typeof db.items[key] == "string"){
@@ -200,8 +274,9 @@ db.items.removeItem = function(object) {
}
// store slaves and inventory in a .json full of JSON data
process.on('SIGINT', function () {
process.on('SIGINT', async function () {
console.log('Ctrl-C...');
let exitStartTime = Date.now();
// set insane limit to slave length, if its longer than this we are probably being ddosed or something
if(slaves && Object.keys(slaves).length < 2000000){
console.log("saving to slaves.json");
@@ -215,6 +290,15 @@ process.on('SIGINT', function () {
} else if(slaves) {
console.log("Item database too large, not saving ("+Object.keys(slaves).length+")");
}
for(let i in masterPlugins){
let plugin = masterPlugins[i];
if(plugin.main && plugin.main.onExit && typeof plugin.main.onExit == "function"){
let startTime = Date.now();
await plugin.main.onExit();
console.log("Plugin "+plugin.pluginConfig.name+" exited in "+(Date.now()-startTime)+"ms");
}
}
console.log("Exited in "+(Date.now()-exitStartTime)+"ms");
process.exit(2);
});
@@ -224,7 +308,7 @@ POST World ID management. Slaves post here to tell the server they exist
@instance
@alias /api/getID
*/
app.post("/api/getID", function(req,res) {
app.post("/api/getID", authenticate.middleware, function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
// request.body should be an object
@@ -253,7 +337,7 @@ POST Allows you to add metadata related to slaves for other tools, like owner da
@instance
@alias /api/editSlaveMeta
*/
app.post("/api/editSlaveMeta", function(req,res) {
app.post("/api/editSlaveMeta", authenticate.middleware, function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
// request.body should be an object
@@ -283,6 +367,8 @@ POST Get metadata from a single slave. For whenever you find the data returned b
@alias /api/getSlaveMeta
*/
app.post("/api/getSlaveMeta", function (req, res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
console.log("body", req.body);
if(req.body && req.body.instanceID && req.body.password){
console.log("returning meta for ", req.body.instanceID);
@@ -291,22 +377,7 @@ app.post("/api/getSlaveMeta", function (req, res) {
res.status(400);
res.send('{"INVALID REQUEST":1}');
}
});
// TODO: Remove deprecated function, also update researchSync to not use it
/**
POST DEPRECATED Get metadata from all slaves. This is already covered by /api/slaves, please use that one instead.
@memberof clusterioMaster
@instance
@alias /api/getSlavesMeta
*/
app.post("/api/getSlavesMeta", function (req, res) {
if(req.body.password){
let metas = {};
res.send(JSON.stringify(Object.keys(slaves).map(key => metas[key] = slaves[key].meta)));
} else {
res.status(400);
res.send('{"INVALID REQUEST":1}');
}
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
// mod management
// should handle uploading and checking if mods are uploaded
@@ -341,13 +412,10 @@ POST endpoint for uploading mods to the master server. Required for automatic mo
@instance
@alias /api/uploadMod
*/
app.post("/api/uploadMod", function(req,res) {
app.post("/api/uploadMod", authenticate.middleware, function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
if (!req.files) {
res.send('No files were uploaded.');
return;
} else {
if (req.files && req.files.file) {
console.log(req.files.file);
req.files.file.mv('./database/masterMods/'+req.files.file.name, function(err) {
if (err) {
@@ -358,7 +426,10 @@ app.post("/api/uploadMod", function(req,res) {
}
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
}
} else {
res.send('No files were uploaded.');
return;
}
});
/**
GET endpoint for getting information about all our slaves
@@ -366,17 +437,25 @@ GET endpoint for getting information about all our slaves
@instance
@alias /api/slaves
*/
let slaveCache = {
timestamp: Date.now(),
};
app.get("/api/slaves", function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
let copyOfSlaves = JSON.parse(JSON.stringify(slaves));
// filter out the rcon password because thats kindof not a safe thing to share
for(key in copyOfSlaves){
copyOfSlaves[key].rconPassword = "hidden";
if(!slaveCache.cache || Date.now() - slaveCache.timestamp > 5000){
let copyOfSlaves = JSON.parse(JSON.stringify(slaves));
// filter out the rcon password because thats kindof not a safe thing to share
for(key in copyOfSlaves){
copyOfSlaves[key].rconPassword = "hidden";
}
slaveCache.cache = copyOfSlaves;
slaveCache.timestamp = Date.now();
}
res.send(copyOfSlaves);
res.send(slaveCache.cache);
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
var recievedItemStatisticsBySlaveID = {};
@@ -398,7 +477,7 @@ POST endpoint for storing items in master's inventory.
@param {string} [itemStack.instanceName="unknown"] the name of an instance for identification in statistics, as provided when launching it. ex node client.js start [name]
@returns {string} status either "success" or "failure"
*/
app.post("/api/place", function(req, res) {
app.post("/api/place", authenticate.middleware, function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
res.header("Access-Control-Allow-Origin", "*");
@@ -441,11 +520,13 @@ app.post("/api/place", function(req, res) {
key:req.body.name,
value:req.body.count,
});
prometheusExportGauge.labels(x.instanceID, req.body.name).inc(Number(req.body.count) || 0);
// save items we get
db.items.addItem({
name:req.body.name,
count:req.body.count
});
// Attempt confirming
res.send("success");
} else {
@@ -465,7 +546,7 @@ POST endpoint to remove items from DB when client orders items.
@returns {itemStack} the number of items actually removed, may be lower than what was asked for due to shortages.
*/
_doleDivisionFactor = {}; //If the server regularly can't fulfill requests, this number grows until it can. Then it slowly shrinks back down.
app.post("/api/remove", function(req, res) {
app.post("/api/remove", authenticate.middleware, function(req, res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
const doleDivisionRetardation = 10; //lower rates will equal more dramatic swings
@@ -517,6 +598,9 @@ app.post("/api/remove", function(req, res) {
//console.log(sentItemStatistics.data)
sentItemStatisticsBySlaveID[object.instanceID] = sentItemStatistics;
}
prometheusDoleFactorGauge.labels(object.name).set(_doleDivisionFactor[object.name] || 0);
prometheusImportGauge.labels(object.instanceID, object.name).inc(Number(object.count) || 0);
res.send({count: object.count, name: object.name});
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
} else {
@@ -540,7 +624,7 @@ Gives no response
@param {object} circuitFrame
@param {number} circuitFrame.time
*/
app.post("/api/setSignal", function(req,res) {
app.post("/api/setSignal", authenticate.middleware, function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
if(typeof req.body == "object" && req.body.time){
@@ -625,7 +709,7 @@ course the timeSeries data.
@param {object} JSON {timestamp: Date.now(), instanceID: "string", data: {"item": number}}
@returns {string} failure
*/
app.post("/api/logStats", function(req,res) {
app.post("/api/logStats", authenticate.middleware, function(req,res) {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
if(typeof req.body == "object" && req.body.instanceID && req.body.timestamp && req.body.data) {
@@ -707,7 +791,7 @@ app.post("/api/getStats", function(req,res) {
} else if(req.body.statistic == "remove"){
console.log(`sending remove data for instanceID ${req.body.instanceID} ${req.body.itemName}`);
// Gather data
console.log(sentItemStatisticsBySlaveID)
// console.log(sentItemStatisticsBySlaveID)
let itemStats = sentItemStatisticsBySlaveID[req.body.instanceID];
console.log(itemStats)
if(typeof itemStats == "object"){
@@ -790,14 +874,12 @@ Requires x-access-token header to be set. Find you api token in secret-api-token
@memberof clusterioMaster
@instance
@alias api/runCommand
@param {object} JSON {instanceID:19412312, command:"/c game.print('hello')"}
@param {object} JSON {instanceID:19412312, broadcast:false, command:"/c game.print('hello')"}
@returns {object} Status {auth: bool, message: "Informative error", data:{}}
*/
// write an auth token to file
fs.writeFileSync("secret-api-token.txt", jwt.sign({ id: "api" }, config.masterAuthSecret, {
expiresIn: 86400*365 // expires in 1 year
}));
app.post("/api/runCommand", (req,res) => {
endpointHitCounter.labels(req.route.path).inc();
let reqStartTime = Date.now();
var token = req.headers['x-access-token'];
if(!token) return res.status(401).send({ auth: false, message: 'No token provided.' });
@@ -805,19 +887,38 @@ app.post("/api/runCommand", (req,res) => {
if(err) return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' });
// validate request.body
let body = req.body;
if(body.instanceID
&& Object.keys(wsSlaves).includes(body.instanceID)
&& body.command
&& typeof body.command == "string"
&& body.command[0] == "/"){
// execute command
wsSlaves[body.instanceID].runCommand(body.command, data => {
res.status(200).send({auth: true, message: "success", data});
});
} else {
res.status(400).send({auth: true, message: "Error: invalid request.body"});
}
let body = req.body;
if (
body.broadcast
&& body.command
&& typeof body.command == "string"
&& body.command[0] == "/")
{
let instanceResponses = [];
for (let instanceID in wsSlaves) {
// skip loop if the property is from prototype
if (!wsSlaves.hasOwnProperty(instanceID)) continue;
wsSlaves[instanceID].runCommand(body.command);
}
res.status(200).send({auth: true, message: "success", response: "Cluster wide messaging initiated"});
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
} else if (
body.instanceID
&& wsSlaves[body.instanceID]
&& body.command
&& typeof body.command == "string"
&& body.command[0] == "/")
{
// execute command
wsSlaves[body.instanceID].runCommand(body.command, data => {
res.status(200).send({auth: true, message: "success", data});
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
} else {
res.status(400).send({auth: true, message: "Error: invalid request.body"});
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
}
});
});
/**
@@ -836,10 +937,22 @@ app.get("/api/getFactorioLocale", function(req,res){
httpRequestDurationMilliseconds.labels(req.route.path).observe(Date.now()-reqStartTime);
});
});
var server = require("http").Server(app);
server.listen(config.masterPort || 8080, function () {
console.log("Listening on port %s...", server.address().port);
});
if(config.sslPort){
var certificate = fs.readFileSync( 'database/certificates/cert.crt' );
var privateKey = fs.readFileSync( 'database/certificates/cert.key' );
httpsServer = https.createServer({
key: privateKey,
cert: certificate
}, app).listen(config.sslPort);
}
if(config.masterPort){
server = require("http").Server(app);
server.listen(config.masterPort || 8080, function () {
console.log("Listening on port %s...", server.address().port);
});
}
/* Websockets for remoteMap */
var io = require("socket.io")(server);
var slaveMappers = {};
@@ -850,14 +963,17 @@ class slaveMapper {
this.lastBeat = Date.now();
this.socket.on("heartbeat", () => {
prometheusWsUsageCounter.labels('heartbeat', this.instanceID).inc();
// we aren't ready to die yet apparently
this.lastBeat = Date.now();
});
this.socket.on("sendChunk", function(data){
prometheusWsUsageCounter.labels('sendChunk', this.instanceID).inc();
mapRequesters[data.requesterID].socket.emit("displayChunk", data);
});
// slaveMapper sent us an entity update, process
this.socket.on("sendEntity", entity => {
prometheusWsUsageCounter.labels('sendEntity', this.instanceID).inc();
Object.keys(mapRequesters).forEach(requesterName => {
let requester = mapRequesters[requesterName];
@@ -878,12 +994,12 @@ class mapRequester {
this.lastBeat = Date.now();
this.socket.on("heartbeat", () => {
prometheusWsUsageCounter.labels('heartbeat').inc();
prometheusWsUsageCounter.labels('heartbeat', "other").inc();
// we aren't ready to die yet apparently
this.lastBeat = Date.now();
});
this.socket.on("requestChunk", loc => {
prometheusWsUsageCounter.labels('requestChunk').inc();
prometheusWsUsageCounter.labels('requestChunk', "other").inc();
loc.requesterID = this.requesterID;
let instanceID = loc.instanceID || this.instanceID;
if(slaveMappers[instanceID] && typeof loc.x == "number" && typeof loc.y == "number"){
@@ -891,7 +1007,7 @@ class mapRequester {
}
});
this.socket.on("requestEntity", req => {
prometheusWsUsageCounter.labels('requestEntity').inc();
prometheusWsUsageCounter.labels('requestEntity', "other").inc();
req.requesterID = this.requesterID;
let instanceID = req.instanceID || this.instanceID;
if(slaveMappers[instanceID] && typeof req.x == "number" && typeof req.y == "number"){
@@ -899,7 +1015,7 @@ class mapRequester {
}
});
this.socket.on("placeEntity", req => {
prometheusWsUsageCounter.labels('placeEntity').inc();
prometheusWsUsageCounter.labels('placeEntity', "other").inc();
req.requesterID = this.requesterID;
let instanceID = req.instanceID || this.instanceID;
if(slaveMappers[instanceID]){
@@ -917,11 +1033,11 @@ class wsSlave {
this.lastBeat = Date.now();
this.socket.on("heartbeat", () => {
prometheusWsUsageCounter.labels('heartbeat').inc();
prometheusWsUsageCounter.labels('heartbeat', this.instanceID).inc();
this.lastBeat = Date.now();
});
this.socket.on("combinatorSignal", circuitFrameWithMeta => {
prometheusWsUsageCounter.labels('combinatorSignal').inc();
prometheusWsUsageCounter.labels('combinatorSignal', this.instanceID).inc();
if(circuitFrameWithMeta && typeof circuitFrameWithMeta == "object"){
Object.keys(wsSlaves).forEach(instanceID => {
wsSlaves[instanceID].socket.emit("processCombinatorSignal", circuitFrameWithMeta);
@@ -931,16 +1047,39 @@ class wsSlave {
// handle command return values
this.commandsWaitingForReturn = {};
this.socket.on("runCommandReturnValue", resp => {
prometheusWsUsageCounter.labels('runCommandReturnValue', this.instanceID).inc();
if(resp.commandID && resp.body && this.commandsWaitingForReturn[resp.commandID] && this.commandsWaitingForReturn[resp.commandID].callback && typeof this.commandsWaitingForReturn[resp.commandID].callback == "function"){
this.commandsWaitingForReturn[resp.commandID].callback(resp.body);
delete this.commandsWaitingForReturn[resp.commandID];
}
});
this.socket.on("gameChat", data => {
prometheusWsUsageCounter.labels('gameChat', this.instanceID).inc();
if(typeof data === "object"){
data.timestamp = Date.now();
if(!global.wsChatRecievers) global.wsChatRecievers = [];
global.wsChatRecievers.forEach(socket => {
socket.emit("gameChat", data);
});
}
});
this.socket.on("alert", alert => {
prometheusWsUsageCounter.labels('alert', this.instanceID).inc();
if(typeof alert === "object"){
alert.timestamp = Date.now();
if(!global.wsAlertRecievers) global.wsAlertRecievers = [];
global.wsAlertRecievers.forEach(socket => {
socket.emit("alert", alert);
});
}
});
}
runCommand(command, callback){
let commandID = Math.random().toString();
this.socket.emit("runCommand", {command, commandID});
this.commandsWaitingForReturn[commandID] = {callback, timestamp: Date.now()};
if(commandID) this.commandsWaitingForReturn[commandID] = {callback, timestamp: Date.now()};
}
}
io.on('connection', function (socket) {
@@ -963,13 +1102,13 @@ io.on('connection', function (socket) {
/* initial processing for remoteMap */
socket.on('registerSlaveMapper', function (data) {
prometheusWsUsageCounter.labels('registerSlaveMapper').inc();
prometheusWsUsageCounter.labels('registerSlaveMapper', "other").inc();
slaveMappers[data.instanceID] = new slaveMapper(data.instanceID, socket);
console.log("remoteMap | SOCKET registered map provider for "+data.instanceID);
});
socket.on('registerMapRequester', function(data){
// data {instanceID:""}
prometheusWsUsageCounter.labels('registerMapRequester').inc();
prometheusWsUsageCounter.labels('registerMapRequester', "other").inc();
let requesterID = Math.random().toString();
mapRequesters[requesterID] = new mapRequester(requesterID, socket, data.instanceID);
socket.emit("mapRequesterReady", true);
@@ -978,13 +1117,85 @@ io.on('connection', function (socket) {
/* Websockets for send and recieve combinators */
socket.on("registerSlave", function(data) {
prometheusWsUsageCounter.labels('registerSlave').inc();
prometheusWsUsageCounter.labels('registerSlave', "other").inc();
if(data && data.instanceID){
wsSlaves[data.instanceID] = new wsSlave(data.instanceID, socket);
console.log("SOCKET | Created new wsSlave: "+ data.instanceID);
}
});
socket.on("registerChatReciever", function(data){
prometheusWsUsageCounter.labels("registerChatReciever", "other").inc();
if(!global.wsChatRecievers) global.wsChatRecievers = [];
global.wsChatRecievers.push(socket);
});
socket.on("registerAlertReciever", function(data){
prometheusWsUsageCounter.labels("registerAlertReciever", "other").inc();
if(!global.wsAlertRecievers) global.wsAlertRecievers = [];
global.wsAlertRecievers.push(socket);
});
});
// handle plugins on the master
pluginManagement();
masterPlugins = [];
async function pluginManagement(){
let startPluginLoad = Date.now();
masterPlugins = await getPlugins("sharedPlugins");
console.log("All plugins loaded in "+(Date.now() - startPluginLoad)+"ms");
}
function getPlugins(pluginDirectory){
return new Promise((resolve, reject) => {
let filesLeft = 0;
let plugins = [];
function checkLoadComplete(plugin){
if(!filesLeft){
resolve(plugins);
}
}
fs.readdir(pluginDirectory, function(err, files){
if(err){
reject(err);
} else {
files.forEach(file => {
let pluginStartedLoading = Date.now(); // just for logging
filesLeft++;
let pluginPath = path.join(pluginDirectory, file);
fs.stat(pluginPath, function(err, stats){
if(err){
reject(err);
checkLoadComplete();
} else if(stats.isDirectory()){
let pluginConfig = require(path.resolve(pluginPath, "config"));
--filesLeft;
if(pluginConfig.masterPlugin){
let masterPlugin = require(path.resolve(pluginPath, pluginConfig.masterPlugin));
plugins.push({
main:new masterPlugin({
config,
pluginConfig,
path: pluginPath,
socketio: io,
express: app,
}),
pluginConfig,
});
console.log("Loaded plugin "+pluginConfig.name+" in "+(Date.now() - pluginStartedLoading)+"ms");
checkLoadComplete();
} else {
checkLoadComplete();
}
} else {
checkLoadComplete();
}
});
});
}
});
});
}
module.exports = app;
+11 -4
View File
@@ -21,15 +21,21 @@
"app-root-path": "^2.0.1",
"as-table": "^1.0.31",
"averaged-timeseries": "^1.4.0",
"base64url": "^3.0.0",
"bcrypt": "^2.0.1",
"bcrypt-promise": "^2.0.0",
"bcryptjs": "^2.4.3",
"body-parser": "^1.18.2",
"codecov": "^2.3.1",
"cache-require-paths": "^0.3.0",
"cli-interact": "^0.1.9",
"codecov": "^3.0.1",
"compression": "^1.7.2",
"deepmerge": "^1.5.2",
"ejs": "^2.5.7",
"express": "^4.16.2",
"express-fileupload": "0.0.6",
"follow-redirects": "^1.4.1",
"fs-extra": "^1.0.0",
"follow-redirects": "^1.5.0",
"fs-extra": "^6.0.1",
"getmac": "^1.4.0",
"hash-files": "^1.1.1",
"html5-validator": "^1.0.7",
@@ -47,10 +53,11 @@
"node-factorio-api": "^0.3.1-alpha",
"node-forge": "^0.7.1",
"prom-client": "^10.2.3",
"rcon-client": "^3.0.2",
"request": "^2.83.0",
"right-pad": "^1.0.1",
"rmdir-sync": "^1.0.1",
"simple-rcon": "^0.3.1",
"sanitizer": "^0.1.3",
"single-line-log": "^1.1.2",
"socket.io": "^2.0.4",
"socket.io-client": "^2.0.4",
+12 -13
View File
@@ -3,12 +3,20 @@ const needle = require("needle");
class UPSdisplay {
constructor(slaveConfig, msgInterface){
constructor(slaveConfig, msgInterface, extras){
this.config = slaveConfig;
this.messageInterface = msgInterface;
this.socket = extras.socket;
this.historicalTicks = [];
setInterval(() => {
this.messageInterface("/silent-command game.write_file('UPSdisplay.txt', game.tick, true, 0)");
this.messageInterface("/silent-command rcon.print(game.tick)", data => {
this.tick = Number(data);
this.historicalTicks[this.historicalTicks.length] = {tick:Number(data), timestamp:Date.now()};
if(this.historicalTicks.length > 30){
this.historicalTicks.shift(); // delete last element in array (position 0);
}
});
},1000);
setInterval(() => {
if(this.historicalTicks && this.historicalTicks[0]){
@@ -17,24 +25,15 @@ class UPSdisplay {
let UPS = Math.round(ticksInPeriod / (timePeriod/1000));
// console.log("UPS: " + UPS);
try{
needle.post(this.config.masterIP+':'+this.config.masterPort+'/api/editSlaveMeta', {instanceID: this.config.unique, password: this.config.clientPassword, meta: {UPS:UPS}}, function(err, resp) {
needle.post(this.config.masterIP+':'+this.config.masterPort+'/api/editSlaveMeta', {instanceID: this.config.unique, password: this.config.clientPassword, meta: {UPS:UPS, tick:this.tick}}, {headers: {'x-access-token': this.config.masterAuthToken}}, function(err, resp) {
// success?
});
} catch (err){
console.log(err);
}
}
}, 10000);
}, 5000);
this.messageInterface("/silent-command game.print('UPSdisplay enabled')");
}
scriptOutput(data){
if (data !== null && !isNaN(data)){
// console.log("Tick: " + data);
this.historicalTicks[this.historicalTicks.length] = {tick:Number(data), timestamp:Date.now()};
if(this.historicalTicks.length > 30){
this.historicalTicks.shift(); // delete last element in array (position 0);
}
}
}
}
module.exports = UPSdisplay;
+10
View File
@@ -0,0 +1,10 @@
/*
Clusterio plugin to allow for chat between instances.
*/
module.exports = {
// Name of package. For display somewhere I guess.
name: "globalChat",
version: "1.0.0",
binary: "nodePackage",
description: "Clusterio plugin to allow for chat between instances.",
}
+71
View File
@@ -0,0 +1,71 @@
const needle = require("needle");
module.exports = class remoteCommands {
constructor(mergedConfig, messageInterface, extras){
this.messageInterface = messageInterface;
this.config = mergedConfig;
this.socket = extras.socket;
this.instances = {};
this.socket.on("hello", () => this.socket.emit("registerChatReciever"));
this.socket.on("gameChat", async data => {
if(data.instanceID && data.instanceID != this.config.unique.toString()){
if(data.data.includes("[CHAT]") && (data.data.toLowerCase().includes("/shout") || data.data.toLowerCase().includes("!shout"))){
let words = data.data.trim().split(" ");
words.shift();
words.shift();
words.shift();
let message = words.join(" ").replace(/[";]/g, " ").replace(/[']/g,"").replace("/shout ", "").replace("!shout", "");
let instance = await this.getInstanceName(data.instanceID);
this.messageInterface("/silent-command game.print('[" + instance + "] " + message + "')");
}
}
});
}
async factorioOutput(data){
try{
this.socket.emit("gameChat", {
instanceID: this.config.unique.toString(),
data,
});
if(data.includes("[CHAT]") && data.includes("!info")){
let infoMessage = [
"=== Server info ===",
"Name: "+await this.getInstanceName(this.config.unique)+" ("+this.config.unique+")",
];
infoMessage.forEach((line, i) => {
setTimeout(()=>this.messageInterface("/silent-command game.print('"+line+"')"),i*200);
});
}
}catch(e){console.log(e)}
}
getInstanceName(instanceID){
return new Promise((resolve, reject) => {
let instance = this.instances[instanceID];
if(!instance){
needle.get(this.config.masterIP+":"+this.config.masterPort+ '/api/slaves', (err, response) => {
if(err || response.statusCode != 200) {
console.log("Unable to get JSON master/api/slaves, master might be unaccessible");
} else if (response && response.body) {
if(Buffer.isBuffer(response.body)) {console.log(response.body.toString("utf-8")); throw new Error();}
try {
for (let index in response.body)
this.instances[index] = response.body[index].instanceName;
} catch (e){
console.log(e);
return null;
}
instance = this.instances[instanceID]
if (!instance) instance = instanceID; //somehow the master doesn't know the instance
resolve(instance);
}
});
} else {
resolve(instance);
}
});
}
}
@@ -76,7 +76,7 @@ function handleInventory(json, config, callback){
}
//console.log("Requesting: "+JSON.stringify(request));
// request the difference from master
needle.post(config.masterIP + ":" + config.masterPort + '/api/remove', request, {json:true}, function (err, response, body) {
needle.post(config.masterIP + ":" + config.masterPort + '/api/remove', request, {json:true,headers: {'x-access-token': config.masterAuthToken}}, function (err, response, body) {
if (!err && response && response.body && (typeof response.body == "object" || isJson(response.body))) {
// confirmed orders, already removed from master inventory
//response.body.name: response.body.count
@@ -103,7 +103,7 @@ function handleInventory(json, config, callback){
for(let i = 0; i < stackArray.length; i++){
if(stackArray[i].count > 0){
// console.log("Returning overflow: " + JSON.stringify(stackArray[i]));
needle.post(config.masterIP + ":" + config.masterPort + '/api/place', stackArray[i], function (err, resp, body) {
needle.post(config.masterIP + ":" + config.masterPort + '/api/place', stackArray[i], {headers: {'x-access-token': config.masterAuthToken}}, function (err, resp, body) {
// console.log(body);
});
}
+1 -1
View File
@@ -22,4 +22,4 @@ module.exports = class remoteCommands {
scriptOutput(data){
// we don't return anything yet
}
}
}
+1 -1
View File
@@ -83,7 +83,7 @@ class ResearchSync {
instanceID: this.config.unique,
password: this.config.clientPassword,
meta: {research: this.research}
}, function (err, resp) {
}, {headers: {'x-access-token': this.config.masterAuthToken}}, function (err, resp) {
// success?
});
});
+8 -8
View File
@@ -1,9 +1,9 @@
<title>Clusterio</title>
<link rel="stylesheet" type="text/css" href="style.css">
<script src="external/jquery.js"></script>
<script src="external/jquery-ui.js"></script>
<script src="external/canvasjs.js"></script>
<script src="external/moment.min.js"></script>
<script src="external/mousetrap.min.js"></script>
<script src="main.js"></script>
<link href="external/css/Roboto.css" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="/style.css">
<script src="/external/jquery.js"></script>
<script src="/external/jquery-ui.js"></script>
<script src="/external/canvasjs.js"></script>
<script src="/external/moment.min.js"></script>
<script src="/external/mousetrap.min.js"></script>
<script src="/main.js"></script>
<link href="/external/css/Roboto.css" rel="stylesheet">
+73 -33
View File
@@ -49,6 +49,24 @@ debugTools = {
}
}
let cachedInstanceNames = {};
async function getInstanceName(instanceID){
let instance = cachedInstanceNames[instanceID];
if(!instance){
let slaves = await getJSON("/api/slaves");
try {
for(id in slaves){
cachedInstanceNames[id] = slaves[id].instanceName;
}
} catch(e){
console.log("Error while looking for instance name!");
console.log(e);
}
return cachedInstanceNames[instanceID] || instanceID;
} else {
return cachedInstanceNames[instanceID];
}
}
var g = {}
contents = {
"iron-plate":100,
@@ -63,44 +81,66 @@ function djb2(str){
}
return hash;
}
// get queryString parameters
function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
// callback(err, json)
function getJSON(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'json';
xhr.onload = function() {
var status = xhr.status;
if (status == 200) {
callback(null, xhr.response);
} else {
callback(status);
}
};
// triggers if connection is refused
xhr.onerror = function(e){
callback(e);
};
xhr.send();
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'json';
xhr.onload = function() {
var status = xhr.status;
if (status == 200) {
if(callback) callback(null, xhr.response);
resolve(xhr.response);
} else {
if(callback) callback(status);
reject(status);
}
};
// triggers if connection is refused
xhr.onerror = function(e){
if(callback) callback(e);
reject(e);
};
xhr.send();
});
};
// callback(err, json)
function postJSON(url, data, callback) {
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.responseType = 'json';
xhr.onload = function() {
var status = xhr.status;
if (status == 200) {
callback(null, xhr.response);
} else {
callback(status);
}
};
// triggers if connection is refused
xhr.onerror = function(e){
callback(e);
};
xhr.send(JSON.stringify(data));
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.responseType = 'json';
xhr.onload = function() {
var status = xhr.status;
if (status == 200) {
if(callback) callback(null, xhr.response);
resolve(xhr.response);
} else {
if(callback) callback(status);
reject(status);
}
};
// triggers if connection is refused
xhr.onerror = function(e){
if(callback) callback(e);
reject(e);
};
xhr.send(JSON.stringify(data));
});
};
// return Boolean
function isJSON(string){
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+9 -3
View File
@@ -2,16 +2,22 @@
</div>
<div id="sidenav">
<div id="logo">
<div id="logo" style="height:50px;">
<img src="/pictures/clusterio_icon.png" style="width:50px;height:50px; float:left;"></img>
<h1>Clusterio</h1>
</div>
<a href="/"><div id="storage-menu" class="button-black menuItem">
<p>Storage</p>
</div></a>
<a href="nodes"><div id="slaves-menu" class="button-black menuItem">
<a href="/nodes"><div id="slaves-menu" class="button-black menuItem">
<p>Slaves</p>
</div></a>
<a href="settings"><div id="settings-menu" class="button-black menuItem">
<%# Very ugly ejs code to hide the Players button if the playerManager plugin isn`t installed %>
<% adsdf = "none"; masterPlugins.forEach(plug => {if(plug.pluginConfig.name == "playerManager"){ adsdf = "block" }}); %>
<a href="/playerManager" style="display:<%= adsdf %>"><div id="playerManager-menu" class="button-black menuItem">
<p>Players</p>
</div></a>
<a href="/settings"><div id="settings-menu" class="button-black menuItem">
<p>Settings</p>
</div></a>
</div>
+3
View File
@@ -16,6 +16,9 @@ function drawcontents(data) {
//update existing rows or create new ones
data.forEach(function(item, i) {
// format count to have .s in it for large number readability (1,000,000)
item.count = item.count.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
let row = rows[i];
if(!row) {
row = document.createElement('tr');
+5 -1
View File
@@ -19,6 +19,10 @@ button {
background-color: #262626;
color: #C8C8C8;
}
#logo > h1 {
padding-top: 6px;
transform: translateX(5%);
}
#storage {
float: left;
}
@@ -188,7 +192,7 @@ a:hover {
.button-black {
width:100%;
height:40px;
height:50px;
background-color:black;
color:white;
transition: all 300ms;