mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbd9876b8b | ||
|
|
cfa52dbcca | ||
|
|
8f8436a809 | ||
|
|
8267dae10e | ||
|
|
d177fb7304 | ||
|
|
7d99162072 | ||
|
|
2a8228c5a9 | ||
|
|
b86d4fa2ab | ||
|
|
b783ccf2cb | ||
|
|
010e5271ea | ||
|
|
447766c752 | ||
|
|
e4c276054b | ||
|
|
b7f76bf451 | ||
|
|
780feb7eb8 | ||
|
|
b98cb9def6 | ||
|
|
1b6b0abaeb | ||
|
|
ea8f819bd5 | ||
|
|
94bbf62c26 | ||
|
|
a9532dd0d3 | ||
|
|
034db54152 | ||
|
|
17b5967b0f | ||
|
|
226e157f69 | ||
|
|
f72ea7357e | ||
|
|
812265d0fd | ||
|
|
da6f96a34b | ||
|
|
88e8ab454f | ||
|
|
e87c702cf2 | ||
|
|
e6c706a83e | ||
|
|
47b74a07d1 | ||
|
|
0ee2c5c2f6 | ||
|
|
e73c5055af | ||
|
|
165cfffecb | ||
|
|
ce6dc3af28 | ||
|
|
1d9b6a26a6 | ||
|
|
db89099b2d | ||
|
|
973f0b0e0a | ||
|
|
0b81fb6351 | ||
|
|
ac55b1c442 | ||
|
|
f07b6160bb | ||
|
|
480e2af03e | ||
|
|
8fe908aa5d | ||
|
|
cf1a97ac9f | ||
|
|
6dd25cb1e8 | ||
|
|
ce24a40d8c | ||
|
|
f5d1847975 | ||
|
|
f580f06064 | ||
|
|
f5e908eb5d | ||
|
|
a6d98db6a9 | ||
|
|
b49a37e438 | ||
|
|
c02bb6d58f | ||
|
|
a0e98ecb00 | ||
|
|
6816db0628 | ||
|
|
be73f2f3a5 | ||
|
|
d07112e85e | ||
|
|
1fede26564 | ||
|
|
c7e2308a33 | ||
|
|
5c064e4609 | ||
|
|
6514462a1d | ||
|
|
1e0c275968 | ||
|
|
968a306c5a | ||
|
|
47430b5d28 | ||
|
|
6e5585c86c | ||
|
|
9c9db7882c | ||
|
|
d6369ba295 | ||
|
|
f4e03899e7 | ||
|
|
0afc3d7e78 | ||
|
|
355e8974a4 | ||
|
|
89ad2baa26 | ||
|
|
57f7dec956 | ||
|
|
5b59abb8e3 | ||
|
|
66b64eae1b | ||
|
|
f15ce3fb23 | ||
|
|
41541b01f0 | ||
|
|
c5b55f27b7 | ||
|
|
a67a1d3d1d | ||
|
|
bcd1ec79cb | ||
|
|
594c994560 | ||
|
|
f10d6bbfb5 | ||
|
|
1184e64467 | ||
|
|
02ab34dbf4 | ||
|
|
fc1f3bd077 | ||
|
|
6a7042c0aa | ||
|
|
2aff63e4a1 | ||
|
|
e38aed08bf | ||
|
|
1bd87c4cc9 | ||
|
|
dc19457471 | ||
|
|
84aaae05c2 | ||
|
|
73ce30ded4 | ||
|
|
1b00fbb328 | ||
|
|
29c38fc4d0 | ||
|
|
2e1f7b7f6d | ||
|
|
61e944b061 | ||
|
|
ce0deef83b | ||
|
|
641f980741 | ||
|
|
ebf2fd2204 | ||
|
|
20e66cbd40 | ||
|
|
a26189947d | ||
|
|
0b0b2bc1a9 |
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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};
|
||||
@@ -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
@@ -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
@@ -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'
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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.",
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,4 +22,4 @@ module.exports = class remoteCommands {
|
||||
scriptOutput(data){
|
||||
// we don't return anything yet
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user