var fs = require('fs-extra'); var https = require('follow-redirects').https; var needle = require("needle"); var child_process = require('child_process'); var path = require('path'); var syncRequest = require('sync-request'); var request = require("request"); var ncp = require('ncp').ncp; var Rcon = require('simple-rcon'); var hashFiles = require('hash-files'); var _ = require('underscore'); var deepmerge = require("deepmerge"); // internal libraries var objectOps = require("./lib/objectOps.js"); var fileOps = require("./lib/fileOps.js"); // require config.json var config = require('./config'); var global = {}; if (!fs.existsSync("./instances/")) { fs.mkdirSync("instances"); } if (!fs.existsSync("./sharedPlugins/")) { fs.mkdirSync("sharedPlugins"); } if (!fs.existsSync("./sharedMods/")) { fs.mkdirSync("sharedMods"); } const instance = process.argv[3]; const instancedirectory = './instances/' + instance; const command = process.argv[2]; var instanceInfo = {}; // simple string hasher String.prototype.hashCode = function () { var hash = 0; if (this.length == 0) return hash; for (let i = 0; i < this.length; i++) { char = this.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return hash; } // function to handle sending commands into the game function messageInterface(command, callback) { // 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) { /* to send to stdin, use: serverprocess.stdin.write("/c command;\n") */ serverprocess.stdin.write(command+"\n"); if(typeof callback == "function"){ callback(); } } else if(typeof command == "string" && client && client.exec && typeof client.exec == "function") { try { client.exec(command+"\n", callback); } catch (err) { console.log(err); } if(typeof callback == "function"){ callback(); } } } // handle commandline parameters if (!command || command == "help" || command == "--help") { console.error("Usage: "); console.error("node client.js start [instance name]"); console.error("node client.js list"); console.error("node client.js delete [instance]"); console.error("To download the latest version of the Clusterio lua mod, do"); console.error("node client.js download"); process.exit(1); } else if (command == "list") { console.dir(fileOps.getDirectoriesSync("./instances/")); process.exit(1); } 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]); console.log("Deleted instance " + process.argv[3]); process.exit(1); } else { console.error("Instance not found: " + process.argv[3]); process.exit(1); } } else if (command == "download") { console.log("Downloading mods..."); // get JSON data about 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; if(url) { console.log(url); var file = fs.createWriteStream("sharedMods/"+name); var request = https.get(url, function(response) { response.pipe(file); console.log("Downloaded "+name); }); } } else if (command == "start" && instance === undefined) { console.log("ERROR: No instanceName provided!"); console.log("Usage: node client.js start [instanceName]"); process.exit(0); } else if (command == "start" && typeof instance == "string" && instance != "/" && !fs.existsSync(instancedirectory)) { // if instance does not exist, create it console.log("Creating instance..."); fs.mkdirSync(instancedirectory); fs.mkdirSync(instancedirectory + "/script-output/"); fs.mkdirSync(instancedirectory + "/saves/"); fs.writeFileSync(instancedirectory + "/script-output/output.txt", ""); fs.writeFileSync(instancedirectory + "/script-output/orders.txt", ""); fs.writeFileSync(instancedirectory + "/script-output/txbuffer.txt", ""); fs.mkdirSync(instancedirectory + "/mods/"); // 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.copySync('sharedMods', instancedirectory + "/mods"); let instconf = { "factorioPort": process.env.FACTORIOPORT | Math.floor(Math.random() * 65535), "clientPort": process.env.RCONPORT | Math.floor(Math.random() * 65535), "clientPassword": Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 8), } console.log("Clusterio | Moving shared mods..."); console.log(instconf); // create instance config fs.writeFileSync(instancedirectory + "/config.json", JSON.stringify(instconf, null, 4)); let name = "Clusterio instance: " + instance; if (config.username) { name = config.username + "'s clusterio " + instance; } let serversettings = { "name": name, "description": config.description, "tags": ["clusterio"], "max_players": "20", "visibility": config.visibility, "username": config.username, "password": config.password, "token": config.usertoken, "game_password": config.game_password, "verify_user_identity": config.verify_user_identity, "admins": [config.username], "allow_commands": config.allow_commands, "autosave_interval": 10, "autosave_slots": 5, "afk_autokick_interval": 0, "auto_pause": config.auto_pause, } fs.writeFileSync(instancedirectory + "/server-settings.json", JSON.stringify(serversettings, null, 4)); let createSave = child_process.spawnSync( './' + config.factorioDirectory + '/bin/x64/factorio', [ '-c', instancedirectory + '/config.ini', '--create', instancedirectory + '/saves/save.zip', ] ); console.log("Instance created!"); } 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'); instanceconfig.unique = instanceconfig.clientPassword.hashCode(); if(process.env.FACTORIOPORT){ instanceconfig.factorioPort = process.env.FACTORIOPORT; } if(process.env.RCONPORT){ instanceconfig.rconPort = process.env.RCONPORT; } } else { process.exit(1); } console.log("Deleting .tmp.zip files"); let savefiles = fs.readdirSync(instancedirectory + "/saves/"); for(i = 0; i < savefiles.length; i++){ if(savefiles[i].substr(savefiles[i].length - 8, 8) == ".tmp.zip") { fs.unlinkSync(instancedirectory + "/saves/" + savefiles[i]); } } /*setInterval(function(){ console.log(process.env); },5000);*/ console.log("Deleting logs"); // clean old log file to avoid crash // file exists, delete so we don't get in trouble try { fs.unlinkSync(instancedirectory+'/factorio-current.log'); } catch (err){ if(err){ console.log(err); } else { console.log("Clusterio | Deleting old logs..."); } } // move mods from ./sharedMods to the instances mod directory console.log("Clusterio | Moving shared mods..."); fs.copySync('sharedMods', instancedirectory + "/mods"); process.on('SIGINT', function () { console.log("Caught interrupt signal"); messageInterface("/quit"); }); // Spawn factorio server //var serverprocess = child_process.exec(commandline); fileOps.getNewestFile(instancedirectory + "/saves/", fs.readdirSync(instancedirectory + "/saves/"),function(err, latestSave) { if(err) throw err; // implicit global serverprocess = child_process.spawn( './' + config.factorioDirectory + '/bin/x64/factorio', [ '-c', instancedirectory + '/config.ini', '--start-server', latestSave.file, '--rcon-port', Number(process.env.RCONPORT) || instanceconfig.clientPort, '--rcon-password', instanceconfig.clientPassword, '--server-settings', instancedirectory + '/server-settings.json', '--port', Number(process.env.FACTORIOPORT) || instanceconfig.factorioPort ], { 'stdio': ['pipe', 'pipe', 'pipe'] } ); serverprocess.on('close', (code) => { console.log(`child process exited with code ${code}`); process.exit(); }); serverprocess.stdout.on("data", (data) => { // log("Stdout: " + data); if(data.toString('utf8').includes("Couldn't parse RCON data: Maximum payload size exceeded")){ console.error("ERROR: RCON CONNECTION BROKE DUE TO TOO LARGE PACKET!"); console.error("Attempting reconnect..."); 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); } }); serverprocess.stderr.on('data', (chunk) => { console.log('ERR: ' + chunk); }); // connect to the server with rcon if(process.platform != "linux"){ // IP, port, password client = new Rcon({ host: 'localhost', port: Number(process.env.RCONPORT) || instanceconfig.clientPort, password: instanceconfig.clientPassword, timeout: 0 }); // 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 function checkRcon() { fs.readFile(instancedirectory+"/factorio-current.log", function (err, data) { // if (err) console.log(err); if(data && data.indexOf('Starting RCON interface') > 0){ client.connect(); } else { setTimeout(function(){ checkRcon(); },2000); } }); } setTimeout(checkRcon, 5000); client.on('authenticated', function () { console.log('Clusterio | RCON Authenticated!'); instanceManagement(); // start using rcons }).on('connected', function () { console.log('Clusterio | RCON Connected!'); // getID(); }).on('disconnected', function () { 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 instanceManagement(); } // set some globals confirmedOrders = []; lastSignalCheck = Date.now(); }); } // ensure instancemanagement only ever runs once _.once(instanceManagement); function instanceManagement() { console.log("Started instanceManagement();"); // load plugins and execute onLoad event let pluginDirectories = fileOps.getDirectoriesSync("./sharedPlugins/"); let plugins = []; for(let i=0; i { if(data.toString('utf8')[0] != "/") { log("Stdout: " + data.toString('utf8')) } else { messageInterface(data.toString('utf8')); } }); plugins[i].stderr.on("data", (data) => { log("STDERR: " + data); }); plugins[i].on('close', (code) => { log(`child process exited with code ${code}`); }); } // world IDs ------------------------------------------------------------------ hashMods(instance, function(modHashes){ 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; } var payload = { time: Date.now(), rconPort: instanceconfig.clientPort, rconPassword: instanceconfig.clientPassword, serverPort: instanceconfig.factorioPort, 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, } require('getmac').getMac(function (err, mac) { if (err) throw err 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); } }); }); },1000)}); } }); // Mod uploading and management ----------------------------------------------- // get mod names and hashes // string: instance, function: callback setTimeout(function(){hashMods(instance, uploadMods)}, 5000); function uploadMods(modHashes) { // [{modName:string,hash:string}, ... ] for(let i=0;i