const fs = require('fs-extra'); const Tail = require('tail').Tail; const https = require('follow-redirects').https; const needle = require("needle"); const child_process = require('child_process'); const path = require('path'); const syncRequest = require('sync-request'); const request = require("request"); const ncp = require('ncp').ncp; const Rcon = require('rcon-client-fork').Rcon; const deepmerge = require("deepmerge"); const getMac = require('getmac').getMac; const rmdirSync = require('rmdir-sync'); const ioClient = require("socket.io-client"); const asTable = require("as-table").configure({delimiter: ' | '}); const util = require("util"); const crypto = require("crypto"); // internal libraries const objectOps = require("lib/objectOps.js"); const fileOps = require("lib/fileOps"); const stringUtils = require("lib/stringUtils.js"); const pluginManager = require("lib/manager/pluginManager"); const modManager = require("lib/manager/modManager"); const hashFile = require('lib/hash').hashFile; // Uhm... var global = {}; /** * Keeps track of the runtime parameters of an instance */ class Instance { constructor(dir, name) { this._name = name; this._dir = dir; } /** * Name of the instance * * This should not be used for filesystem paths. See .path() for that. */ get name() { return this._name; } /** * Return path in instance * * Creates a path using path.join with the given parts that's relative to * the directory of the instance. For example instance.path("mods") * returns a path to the mods directory of the instance. If no parts are * given it returns a path to the directory of the instance. */ path(...parts) { return path.join(this._dir, ...parts); } } function checkFilename(name) { // All of these are bad in Windows only, except for /, . and .. // See: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file const badChars = /[<>:"\/\\|?*\x00-\x1f]/g; const badEnd = /[. ]$/; const oneToNine = [1, 2, 3, 4, 5, 6, 7, 8, 9]; const badNames = [ // Relative path components '.', '..', // Reserved filenames in Windows 'CON', 'PRN', 'AUX', 'NUL', ...oneToNine.map(n => `COM${n}`), ...oneToNine.map(n => `LPT${n}`), ]; if (typeof name !== "string") { throw new Error("must be a string"); } if (name === "") { throw new Error("cannot be empty"); } if (badChars.test(name)) { throw new Error('cannot contain <>:"\\/|=* or control characters'); } if (badNames.includes(name.toUpperCase())) { throw new Error( "cannot be named any of . .. CON PRN AUX NUL COM1-9 and LPT1-9" ); } if (badEnd.test(name)) { throw new Error("cannot end with . or space"); } } function needleOptionsWithTokenAuthHeader(config) { return { compressed: true, headers: { 'x-access-token': config.masterAuthToken }, }; } var instanceInfo = {}; var commandBuffer=[]; // function to handle sending commands into the game 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(false && 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(); } resolve(); } else if(typeof command == "string" && client && client.send && typeof client.send == "function") { try { let str = await client.send(command+"\n"); if(typeof callback == "function") callback(str) resolve(str) } catch (err) { console.log("RCON failed, server might be paused or shutting down"); // console.log(err); // reject(err); if(typeof callback == "function"){ callback(); } reject(err) } } } function messageInterface(command, callback) { return new Promise((resolve,reject) => { commandBuffer.push([command,callback, resolve, reject]); }); } /** * Give a random dynamic port * * Returns a random port number in the Dynamic Ports range as defined by * RFC 6335. * * @return {number} a number in the range 49152 to 65535. */ function randomDynamicPort() { const start = 49152; const end = 65535 + 1; return Math.floor(Math.random() * (end - start) + start) } /** * Generate a secure random password of the given length * * Uses crypto.randomBytes to generate a secure alphanumeric password of * the given length. * * @param {number} length - the length of the password to generate. * @return {string} password of the given length */ async function generatePassword(length) { function validChar(byte) { const ranges = ['az', 'AZ', '09']; return ranges.some(range => range.codePointAt(0) <= byte && byte <= range.codePointAt(1) ); } let randomBytesAsync = util.promisify(crypto.randomBytes); let password = ""; while (true) { let bytes = await randomBytesAsync((length - password.length) * 3); for (let byte of bytes) { // Crop to ASCII values only byte = byte & 0x7f; if (validChar(byte)) { password += String.fromCharCode(byte); if (password.length == length) { return password; } } } } } function printUsage() { 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 manage shared mods download clusterio"); console.error("To download a clusterio plugin, do"); console.error("node client.js manage shared plugins install https://github.com/Danielv123/playerManager"); console.error("For more management options, do"); console.error("node client.js manage"); } async function listInstances(config) { let instanceNames = fileOps.getDirectoriesSync(config.instanceDirectory); let instances = []; for (instance of instanceNames) { let cfg = path.resolve(config.instanceDirectory, instance, 'config'); let port = require(cfg).factorioPort; instances.push({ "Name": instance, "Port": port.toString(), }); } console.log(asTable(instances)); } async function manage(config, instance) { // console.log("Launching mod manager"); //const fullUsage = 'node client.js manage [instance, "shared"] ["mods", "config"] ...'; function usage(instance, tool, action){ if(tool && tool == "mods"){ console.log('node client.js manage '+instance.name+' '+tool+' ["list", "search", "add", "remove"]'); } else if(tool && tool == "plugins") { console.log(`node client.js manage ${instance.name} ${tool} ["list", "add", "remove"]`); } else { console.log('node client.js manage '+(instance && instance.name || '[instance, "shared"]') +' '+ (tool || '["mods", "plugins"]') + ' ...'); } } const tool = process.argv[4] || ""; const action = process.argv[5] || ""; if (instance !== undefined) { if(tool == "mods"){ (async function(){try{ // allow managing mods if(action == "list"){ console.log(await modManager.listMods(config, instance)); } else if(action == "search"){ console.log(await modManager.findMods(config, process.argv[6])); } else if(action == "add" || action == "download"){ await modManager.addMod(config, process.argv[6], instance); } else if(action == "remove" || action == "rm" || action == "delete"){ await modManager.removeMod(config, process.argv[6], instance); } else { usage(instance, tool); } process.exit(0); }catch(e){ console.log("Got error from modManager:") console.log(e); }})(); } else if(tool == "plugins"){ (async function(){try{ if(action == "list"){ await pluginManager.listPlugins(); } else if(action == "add" || action == "install" || action == "download"){ let status = await pluginManager.addPlugin(process.argv[6]); } else if(action == "remove" || action == "uninstall" || action == "delete"){ let status = await pluginManager.removePlugin(process.argv[6]); if(status && status.msg) console.log(status.msg); } else if(action == "enable"){ let status = await pluginManager.enablePlugin(process.argv[6]); if(status && status.msg) console.log(status.msg); } else if(action == "disable"){ let status = await pluginManager.disablePlugin(process.argv[6]); if(status && status.msg) console.log(status.msg); } process.exit(0); }catch(e){ console.error(e); process.exit(1); }})(); } else { usage(instance); } } else { console.log('Usage:'); usage(instance); } } async function deleteInstance(instance) { if (instance === undefined) { console.error("Usage: node client.js delete [instance]"); process.exit(1); } else if (fs.existsSync(instance.path())) { fileOps.deleteFolderRecursiveSync(instance.path()); // TODO: Check if this can cause i-craft users to format their server by using wrong paths console.log("Deleted instance " + instance.name); process.exit(0); } else { console.error("Instance not found: " + instance.name); process.exit(0); } } async function downloadMod() { console.log("Downloading mods..."); // get JSON data about releases let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"factorioClusterio"}}); 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); let file = fs.createWriteStream("sharedMods/"+name); https.get(url, function(response) { response.on('end', function () { console.log("Downloaded "+name); process.exit(0); }); response.pipe(file); }).end(); } } async function createInstance(config, args, instance) { // if instance does not exist, create it console.log("Creating instance..."); fs.mkdirSync(instance.path()); fs.mkdirSync(instance.path("script-output")); fs.writeFileSync(instance.path("script-output", "output.txt"), ""); fs.writeFileSync(instance.path("script-output", "orders.txt"), ""); fs.writeFileSync(instance.path("script-output", "txbuffer.txt"), ""); fs.mkdirSync(instance.path("mods")); fs.mkdirSync(instance.path("instanceMods")); fs.mkdirSync(instance.path("scenarios")); ncp("lib/scenarios", instance.path("scenarios"), err => { if (err) console.error(err) }); // fs.symlinkSync('../../../sharedMods', instance.path("mods"), 'junction') // This is broken because it can only take a file as first argument, not a folder fs.writeFileSync(instance.path("config.ini"), `[path]\r\n read-data=${ path.resolve(config.factorioDirectory, "data") }\r\n write-data=${ instance.path() }\r\n `); // this line is probably not needed anymore but Im not gonna remove it fs.copySync('sharedMods', instance.path("mods")); let instconf = { "factorioPort": args.port || process.env.FACTORIOPORT || randomDynamicPort(), "clientPort": args["rcon-port"] || process.env.RCONPORT || randomDynamicPort(), "__comment_clientPassword": "This is the rcon password. Its also used for making an instanceID. Make sure its unique and not blank.", "clientPassword": args["rcon-password"] || await generatePassword(10), "info": {} } console.log("Clusterio | Created instance with settings:") console.log(instconf); // create instance config fs.writeFileSync(instance.path("config.json"), JSON.stringify(instconf, null, 4)); let name = "Clusterio instance: " + instance.name; if (config.username) { name = config.username + "'s clusterio " + instance.name; } let serversettings = { "name": name, "description": config.description, "tags": ["clusterio"], "max_players": "20", "visibility": config.visibility, "username": config.username, "token": config.token, "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(instance.path("server-settings.json"), JSON.stringify(serversettings, null, 4)); console.log("Server settings: "+JSON.stringify(serversettings, null, 4)); console.log("Creating save ....."); let factorio = child_process.spawn( './' + config.factorioDirectory + '/bin/x64/factorio', [ '-c', instance.path("config.ini"), // '--create', instance.path("saves", "save.zip"), '--start-server-load-scenario', 'Hotpatch', '--server-settings', instance.path("server-settings.json"), '--rcon-port', Number(process.env.RCONPORT) || instconf.clientPort, '--rcon-password', instconf.clientPassword, ], { 'stdio': ['pipe', 'pipe', 'pipe'], 'detached': process.platform === "linux", } ); factorio.stdout.on("data", data => { data = data.toString("utf8").replace(/(\r\n\t|\n|\r\t)/gm,""); console.log(data); if(data.includes("Starting RCON interface")){ let client = new Rcon({ packetResponseTimeout: 200000, maxPending: 5 }); client.connect({ host: 'localhost', port: Number(process.env.RCONPORT) || instconf.clientPort, password: instconf.clientPassword }); client.onDidAuthenticate(() => { console.log('Clusterio | RCON Authenticated!'); }); client.onDidConnect(() => { console.log('Clusterio | RCON Connected, starting save'); client.send("/c game.server_save('hotpachSave')"); }); } if(data.includes("Saving finished")){ console.log("Map saved as hotpachSave.zip, exiting..."); console.log("Instance created!") process.exit(0); } if(data.includes("Downloading from auth server failed")){ console.error("Instance creation failed, unable to establish auth server connection."); console.error("Deleting broken instance..."); fileOps.deleteFolderRecursiveSync(path.resolve(config.instanceDirectory, process.argv[3])); process.exit(0); } }); } async function startInstance(config, args, instance) { var instanceconfig = JSON.parse(await fs.readFile(instance.path("config.json"))); instanceconfig.unique = stringUtils.hashCode(instanceconfig.clientPassword); if (process.env.FACTORIOPORT) { instanceconfig.factorioPort = process.env.FACTORIOPORT; } if (process.env.RCONPORT) { instanceconfig.rconPort = process.env.RCONPORT; } console.log("Deleting .tmp.zip files"); let savefiles = fs.readdirSync(instance.path("saves")); for(i = 0; i < savefiles.length; i++){ if(savefiles[i].substr(savefiles[i].length - 8, 8) == ".tmp.zip") { fs.unlinkSync(instance.path("saves", savefiles[i])); } } console.log("Clusterio | Rotating old logs..."); // clean old log file to avoid crash try{ let logPath = instance.path("factorio-current.log"); let stat = await fs.stat(logPath); console.log(stat) console.log(stat.isFile()) if(stat.isFile()){ let logFilename = `factorio-${Math.floor(Date.parse(stat.mtime)/1000)}.log`; await fs.rename(logPath, instance.path(logFilename)); console.log(`Log rotated as ${logFilename}`); } }catch(e){} // Math.floor(Date.now()/1000) // (new Date).toGMTString() // move mods from ./sharedMods to the instances mod directory try{fs.mkdirSync(instance.path("instanceMods"));}catch(e){} try{rmdirSync(instance.path("mods"));}catch(e){} try { // mods directory that will be emptied (deleted) when closing the server to facilitate seperation of instanceMods and sharedMods fs.mkdirSync(instance.path("mods")); } catch(e){} console.log("Clusterio | Moving shared mods from sharedMods/ to instance/mods..."); fs.copySync('sharedMods', instance.path("mods")); console.log("Clusterio | Moving instance specific mods from instance/instanceMods to instance/mods..."); fs.copySync(instance.path("instanceMods"), instance.path("mods")); // Set paths for factorio so it reads and writes from the correct place even if the instance is imported from somewhere else fs.writeFileSync(instance.path("config.ini"), `[path]\r\n read-data=${ path.resolve(config.factorioDirectory, "data") }\r\n write-data=${ instance.path() }\r\n `); // Spawn factorio server //var serverprocess = child_process.exec(commandline); fileOps.getNewestFile(instance.path("saves"), fs.readdirSync(instance.path("saves")),function(err, latestSave) { if(err) { 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 serverprocess = child_process.spawn( './' + config.factorioDirectory + '/bin/x64/factorio', [ '-c', instance.path("config.ini"), '--start-server', latestSave.file, '--rcon-port', args["rcon-port"] || Number(process.env.RCONPORT) || instanceconfig.clientPort, '--rcon-password', args["rcon-password"] || instanceconfig.clientPassword, '--server-settings', instance.path("server-settings.json"), '--port', args.port || Number(process.env.FACTORIOPORT) || instanceconfig.factorioPort ], { 'stdio': ['pipe', 'pipe', 'pipe'], 'detached': process.platform === "linux", } ); 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.disconnect(); client.connect(); } // 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.toString("utf8").replace("\n", "")); }); serverprocess.stderr.on('data', (chunk) => { console.log('ERR: ' + chunk); }); // connect to the server with rcon if(true || process.platform != "linux"){ // IP, port, password client = new Rcon({ packetResponseTimeout: 200000, // 200s, should allow for commands up to 1250kB in length maxPending: 5 }); // 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(instance.path("factorio-current.log"), function (err, data) { // if (err) console.error(err); if(data && data.indexOf('Starting RCON interface') > 0){ client.connect({ host: 'localhost', port: args["rcon-port"] || Number(process.env.RCONPORT) || instanceconfig.clientPort, password: args["rcon-password"] || instanceconfig.clientPassword, }); } else { setTimeout(function(){ checkRcon(); },5000); } }); } setTimeout(checkRcon, 5000); client.onDidAuthenticate(() => { console.log('Clusterio | RCON Authenticated!'); instanceManagement(config, instance, instanceconfig); // start using rcons }); client.onDidConnect(() => { console.log('Clusterio | RCON Connected!'); // getID(); }); client.onDidDisconnect(() => { console.log('Clusterio | RCON Disconnected!'); // process.exit(0); // exit because RCON disconnecting is undefined behaviour and we rather just wanna restart now }); process.on('SIGINT', function () { console.log("Caught interrupt signal, disconnecting RCON"); client.disconnect().then(()=>{ console.log("Rcon disconnected, Sending ^C"); // We don't actually do this on windows, because ctrl+c in windows CMD sends it to all subprocesses as well. Doing it twice will abort factorios save. if(process.platform == "linux"){ serverprocess.kill("SIGINT"); } }); }); } else if(process.platform == "linux"){ // don't open an RCON connection and just use stdio instead, does not work on windows. instanceManagement(config, instance, instanceconfig); process.on('SIGINT', function () { console.log("Caught interrupt signal, sending ^C"); serverprocess.kill("SIGINT"); }); } // set some globals confirmedOrders = []; lastSignalCheck = Date.now(); }); } async function startClient() { // argument parsing const args = require('minimist')(process.argv.slice(2)); const command = process.argv[2]; // add better stack traces on promise rejection process.on('unhandledRejection', r => console.log(r)); console.log(`Requiring config from ${args.config || './config'}`); const config = require(args.config || './config'); await fs.ensureDir(config.instanceDirectory); await fs.ensureDir("sharedPlugins"); await fs.ensureDir("sharedMods"); let instance; if (process.argv[3] !== undefined) { let name = process.argv[3]; try { checkFilename(name); } catch (err) { throw new Error(`Instance name ${err.message}`); } let dir = path.join(config.instanceDirectory, name); instance = new Instance(dir, name); } // 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. if (instance) { process.title = "clusterioClient "+instance.name; } // 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.error("ERROR invalid config!"); console.error( "Master server now needs an access token for write operations. As clusterio\n"+ "slaves depends upon this, please add your token to config.json in the field\n"+ "named masterAuthToken. You can retrieve your auth token from the master in\n"+ "secret-api-token.txt after running it once." ); process.exitCode = 1; return; } // messageInterface Management setInterval(function(){ let command=commandBuffer.shift(); if(command){ messageInterfaceInternal(command[0], command[1], command[2], command[3]); } },config.msBetweenCommands || 50); // handle commandline parameters if (!command || command == "help" || command == "--help") { printUsage(); process.exit(1); } else if (command == "list") { await listInstances(config); process.exit(0); } else if (command == "manage"){ await manage(config, instance); // process.exit(0); } else if (command == "delete") { await deleteInstance(instance); } else if (command == "download") { await downloadMod(); } else if (command == "start" && instance === undefined) { console.error("ERROR: No instanceName provided!"); console.error("Usage: node client.js start [instanceName]"); process.exit(0); } else if (command == "start" && !fs.existsSync(instance.path())) { await createInstance(config, args, instance); } else if (command == "start" && fs.existsSync(instance.path())) { await startInstance(config, args, instance); } else { console.error("Invalid arguments, quitting."); process.exit(1); } } // ensure instancemanagement only ever runs once var _instanceInitialized; async function instanceManagement(config, instance, instanceconfig) { if (_instanceInitialized) return; _instanceInitialized = true; console.log("Started instanceManagement();"); /* Open websocket connection to master */ var socket = ioClient(config.masterURL+"?token="+config.masterAuthToken); socket.on("error", err => { console.log("SOCKET | Error: ", err); }); socket.on("hello", data => { console.log("SOCKET | registering slave!"); socket.emit("registerSlave", { instanceID: instanceconfig.unique, }); }); setInterval(B=> socket.emit("heartbeat"), 10000); // load plugins and execute onLoad event let pluginsToLoad = await pluginManager.getPlugins(); let plugins = []; for(let i = 0; i < pluginsToLoad.length; i++){ let pluginLoadStarted = Date.now(); let combinedConfig = deepmerge(instanceconfig,config,{clone:true}); combinedConfig.instanceName = instance.name; let pluginConfig = pluginsToLoad[i]; if(!global.subscribedFiles) { global.subscribedFiles = {}; } if(pluginConfig.binary == "nodePackage" && pluginConfig.enabled){ // require plugin class and execute it let pluginClass = require(path.resolve(pluginConfig.pluginPath, "index.js")); plugins[i] = new pluginClass(combinedConfig, async function(data, callback){ if(data && data.toString('utf8')[0] != "/") { console.log("Clusterio | "+ pluginsToLoad[i].name + " | " + data.toString('utf8')); return true; } else if (data && data.toString('utf8')[0] == "/"){ 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 // between those plugins instead. throw "FATAL ERROR IN " + pluginConfig.name + " FILE ALREADY SUBSCRIBED " + pluginConfig.scriptOutputFileSubscription; } let outputPath = instance.path( "script-output", pluginConfig.scriptOutputFileSubscription ); if (!fs.existsSync(outputPath)) { // Do something fs.writeFileSync(outputPath, ""); } global.subscribedFiles[pluginConfig.scriptOutputFileSubscription] = true; console.log("Clusterio | Registered file subscription on "+outputPath); if(!pluginConfig.fileReadDelay || pluginConfig.fileReadDelay == 0) { // only wipe the file on restart for now, should most likely be rotated during runtime too fs.writeFileSync(outputPath, ""); let tail = new Tail(outputPath); tail.on("line", function (data) { plugins[i].scriptOutput(data); }); } else { fs.watch(outputPath, fileChangeHandler); // run once in case a plugin wrote out information before the plugin loaded fully // delay, so the socket got enough time to connect setTimeout(() => { fileChangeHandler(false, pluginConfig.scriptOutputFileSubscription); }, 500); // send file contents to plugin for processing function fileChangeHandler(eventType, filename) { if (filename != null) { setTimeout( () => { // get array of lines in file let stuff = fs.readFileSync(instance.path("script-output", filename), "utf8").split("\n"); // if you found anything, reset the file if (stuff[0]) { fs.writeFileSync(instance.path("script-output", filename), ""); } for (let o = 0; o < stuff.length; o++) { if (stuff[o] && !stuff[o].includes('\u0000\u0000')) { try { plugins[i].scriptOutput(stuff[o]); } catch (e) { console.error(e) } } } }, pluginConfig.fileReadDelay || 0 ); } } } } console.log(`Clusterio | Loaded plugin ${pluginsToLoad[i].name} in ${Date.now() - pluginLoadStarted}ms`); } else { // this plugin doesn't have a client portion. Maybe it runs on the master only? } } // world IDs ------------------------------------------------------------------ hashMods(instance, function(modHashes){ setInterval(getID, 10000); getID(); function getID() { messageInterface("/silent-command rcon.print(#game.connected_players)", function(playerCount) { 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, instanceName: instance.name, info: instanceconfig.info, }; if(playerCount){ payload.playerCount = playerCount.replace(/(\r\n\t|\n|\r\t)/gm, ""); } else { payload.playerCount = 0; } 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 #####"); } global.mac = mac; payload.mac = mac; // console.log("Registered our presence with master "+config.masterURL+" at " + payload.time); needle.post(config.masterURL + '/api/getID', payload, needleOptionsWithTokenAuthHeader(config), function (err, response, body) { if (err && err.code != "ECONNRESET"){ console.error("We got problems, something went wrong when contacting master "+config.masterURL+" at " + payload.time); 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. if(response.body !== "ok") { console.log("Got no \"ok\" while registering our precense with master "+config.masterURL+" at " + payload.time); console.log(response.body); } } }); } if(global.mac){ callback(undefined, global.mac); } else { getMac(callback); } }); } }); // 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 ( {modName: name, hash: hash} )); } } let promises = fs.readdirSync(instance.path("mods")).map(hashMod); Promise.all(promises).then(hashes => { // Remove null entries from hashMod callback(hashes.filter(entry => entry !== null)); }); } module.exports = { // For testing only _Instance: Instance, _checkFilename: checkFilename, _randomDynamicPort: randomDynamicPort, _generatePassword: generatePassword, }; if (module === require.main) { console.warn(` +==========================================================+ I WARNING: This is the development branch for the 2.0 I I version of clusterio. Expect things to break. I +==========================================================+ ` ); startClient().catch(err => { console.error(` +---------------------------------------------------------------+ | Unexpected error occured while starting client, please report | | it to https://github.com/clusterio/factorioClusterio/issues | +---------------------------------------------------------------+` ); console.error(err); process.exit(1); }); }