Advertisement
Guest User

Untitled

a guest
Jul 17th, 2018
94
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 43.00 KB | None | 0 0
  1. const fs = require('fs-extra');
  2. const https = require('follow-redirects').https;
  3. const needle = require("needle");
  4. const child_process = require('child_process');
  5. const path = require('path');
  6. const syncRequest = require('sync-request');
  7. const request = require("request");
  8. const ncp = require('ncp').ncp;
  9. const Rcon = require('rcon-client').Rcon;
  10. const hashFiles = require('hash-files');
  11. const _ = require('underscore');
  12. const deepmerge = require("deepmerge");
  13. const getMac = require('getmac').getMac;
  14. const rmdirSync = require('rmdir-sync');
  15. const ioClient = require("socket.io-client");
  16.  
  17. // internal libraries
  18. const objectOps = require("./lib/objectOps.js");
  19. const fileOps = require("_app/fileOps");
  20. const stringUtils = require("./lib/stringUtils.js");
  21. const modManager = require("./lib/manager/modManager.js");
  22. const configManager = require("./lib/manager/configManager.js");
  23.  
  24. // argument parsing
  25. const args = require('minimist')(process.argv.slice(2));
  26.  
  27. // require config.json
  28. var config = require(args.config || './config');
  29. var global = {};
  30.  
  31. if (!fs.existsSync(config.instanceDirectory)) {
  32. fs.mkdirSync(config.instanceDirectory);
  33. }
  34. if (!fs.existsSync("./sharedPlugins/")) {
  35. fs.mkdirSync("sharedPlugins");
  36. }
  37. if (!fs.existsSync("./sharedMods/")) {
  38. fs.mkdirSync("sharedMods");
  39. }
  40. const instance = process.argv[3];
  41. const instancedirectory = config.instanceDirectory + '/' + instance;
  42. const command = process.argv[2];
  43.  
  44. // Set the process title, shows up as the title of the CMD window on windows
  45. // and as the process name in ps/top on linux.
  46. process.title = "clusterioClient "+instance;
  47.  
  48. // add better stack traces on promise rejection
  49. process.on('unhandledRejection', r => console.log(r));
  50.  
  51. // make sure we have the master access token (can't write to master without it since clusterio 2.0)
  52. if(!config.masterAuthToken || typeof config.masterAuthToken !== "string"){
  53. console.error("ERROR invalid config!");
  54. console.error("Master server now needs an access token for write operations. As clusterio slaves depends \
  55. upon this, please add your token to config.json in the field named masterAuthToken. \
  56. You can retrieve your auth token from the master in secret-api-token.txt after running it once.");
  57. }
  58. const needleOptionsWithTokenAuthHeader = {
  59. headers: {
  60. 'x-access-token': config.masterAuthToken
  61. },
  62. };
  63.  
  64. var instanceInfo = {};
  65. var commandBuffer=[];
  66. // messageInterface Management
  67. setInterval(function(){
  68. let command=commandBuffer.shift();
  69. if(command){
  70. messageInterfaceInternal(command[0], command[1], command[2], command[3]);
  71. }
  72. },config.msBetweenCommands || 50);
  73.  
  74. // function to handle sending commands into the game
  75. async function messageInterfaceInternal(command, callback, resolve, reject) {
  76. // try to save us if you send a buffer instead of string
  77. if(typeof command == "object") {
  78. command = command.toString('utf8');
  79. }
  80.  
  81. if(false && process.platform == "linux" && typeof command == "string" && serverprocess) {
  82. /*
  83. to send to stdin, use:
  84. serverprocess.stdin.write("/c command;\n")
  85. */
  86. serverprocess.stdin.write(command+"\n");
  87. if(typeof callback == "function"){
  88. callback();
  89. }
  90. resolve();
  91. } else if(typeof command == "string" && client && client.send && typeof client.send == "function") {
  92. try {
  93. let str = await client.send(command+"\n");
  94. if(typeof callback == "function") callback(str)
  95. resolve(str)
  96. } catch (err) {
  97. console.log("RCON failed, server might be paused or shutting down");
  98. // console.log(err);
  99. // reject(err);
  100. if(typeof callback == "function"){
  101. callback();
  102. }
  103. reject(err)
  104. }
  105. }
  106. }
  107. function messageInterface(command, callback) {
  108. return new Promise((resolve,reject) => {
  109. commandBuffer.push([command,callback, resolve, reject]);
  110. });
  111. }
  112.  
  113.  
  114. // handle commandline parameters
  115. if (!command || command == "help" || command == "--help") {
  116. console.error("Usage: ");
  117. console.error("node client.js start [instance name]");
  118. console.error("node client.js list");
  119. console.error("node client.js delete [instance]");
  120. console.error("To download the latest version of the Clusterio lua mod, do");
  121. console.error("node client.js manage shared mods download clusterio");
  122. console.error("For more management options, do");
  123. console.error("node client.js manage");
  124. process.exit(1);
  125. } else if (command == "list") {
  126. let instanceNames = fileOps.getDirectoriesSync(config.instanceDirectory);
  127. instanceNames.unshift("Name:");
  128. let longestInstanceName = 0;
  129. // determine longest instance name
  130. instanceNames.forEach(function(instance){
  131. if(instance.length > longestInstanceName) longestInstanceName = instance.length;
  132. });
  133. let displayLines = [];
  134. // push name coloumn to array
  135. instanceNames.forEach(function(instance){
  136. while(instance.length < longestInstanceName+1){
  137. instance += " ";
  138. }
  139. displayLines.push("| "+ instance + "|");
  140. });
  141. // create port colloumn
  142. let factorioPorts = [];
  143. instanceNames.forEach(function(instance){
  144. let factorioPort;
  145.  
  146. if(instance.includes("Name:")){
  147. factorioPort = "Port:"
  148. } else {
  149. factorioPort = require(path.resolve(config.instanceDirectory+"/"+instance,'config')).factorioPort;
  150. }
  151. factorioPorts.push(factorioPort);
  152. });
  153. factorioPorts.forEach((port, index) => {
  154. let longestPort = 0;
  155. factorioPorts.forEach((port, index) => {
  156. if(port.toString().length > longestPort) longestPort = port.toString().length;
  157. });
  158. while(port.toString().length < longestPort){
  159. port += " ";
  160. }
  161. factorioPorts[index] = port;
  162. });
  163. instanceNames.forEach(function(instance, index){
  164. displayLines[index] += " " + factorioPorts[index] + " |";
  165. });
  166.  
  167. displayLines.forEach(line => console.log(line));
  168. process.exit(0);
  169. } else if (command == "manage"){
  170. // console.log("Launching mod manager");
  171. //const fullUsage = 'node client.js manage [instance, "shared"] ["mods", "config"] ...';
  172. function usage(instance, tool, action){
  173. if(tool && tool == "mods"){
  174. console.log('node client.js manage '+instance+' '+tool+' ["list", "search", "add", "remove", "update"]');
  175. } else if(tool && tool == "config") {
  176. console.log('node client.js manage '+instance+' '+tool+' ["list", "edit"]');
  177. } else {
  178. console.log('node client.js manage '+(instance || '[instance, "shared"]') +' '+ (tool || '["mods", "config"]') + ' ...');
  179. }
  180. }
  181. const tool = process.argv[4] || "";
  182. const action = process.argv[5] || "";
  183. if(instance){
  184. if(tool == "mods"){
  185. (async function(){try{
  186. // allow managing mods
  187. if(action == "list"){
  188. console.log(await modManager.listMods(instance));
  189. } else if(action == "search"){
  190. console.log(await modManager.findMods(process.argv[6]));
  191. } else if(action == "add" || action == "download"){
  192. await modManager.addMod(process.argv[6], instance);
  193. } else if(action == "remove" || action == "rm" || action == "delete"){
  194. await modManager.removeMod(process.argv[6], instance);
  195. } else if(action == "update"){
  196. await modManager.updateAllMods();
  197. } else {
  198. usage(instance, tool);
  199. }
  200. process.exit(0);
  201. }catch(e){
  202. console.log("Got error from modManager:")
  203. console.log(e);
  204. }})();
  205. } else if(tool == "config"){
  206. // allow managing the config
  207. if(action == "list" || action == "show" || action == "display"){
  208. configManager.displayConfig(instance);
  209. } else if(action == "edit"){
  210. let newConfigValue = "";
  211. process.argv.forEach((arg, i)=>{
  212. if(i >= 8){
  213. newConfigValue += " "+arg;
  214. } else if(i >= 7){
  215. newConfigValue += arg;
  216. }
  217. });
  218. configManager.editConfig(instance, process.argv[6], newConfigValue);
  219. } else {
  220. usage(instance, tool);
  221. }
  222. } else {
  223. usage(instance);
  224. }
  225. } else {
  226. console.log('Usage:');
  227. usage(instance);
  228. }
  229. // process.exit(0);
  230. } else if (command == "delete") {
  231. if (!process.argv[3]) {
  232. console.error("Usage: node client.js delete [instance]");
  233. process.exit(1);
  234. } else if (typeof process.argv[3] == "string" && fs.existsSync(config.instanceDirectory+"/" + process.argv[3]) && process.argv[3] != "/" && process.argv[3] != "") {
  235. fileOps.deleteFolderRecursiveSync(config.instanceDirectory+"/" + process.argv[3]);
  236. console.log("Deleted instance " + process.argv[3]);
  237. process.exit(0);
  238. } else {
  239. console.error("Instance not found: " + process.argv[3]);
  240. process.exit(0);
  241. }
  242. } else if (command == "download") {
  243. console.log("Downloading mods...");
  244. // get JSON data about releases
  245. let res = syncRequest('GET', 'https://api.github.com/repos/Danielv123/factorioClusterioMod/releases', {"headers":{"User-Agent":"Fuck you for requiring user agents!"}});
  246. let url = JSON.parse(res.getBody())[0].assets[0].browser_download_url;
  247. let name = JSON.parse(res.getBody())[0].assets[0].name;
  248. if(url) {
  249. console.log(url);
  250. let file = fs.createWriteStream("sharedMods/"+name);
  251. https.get(url, function(response) {
  252. response.on('end', function () {
  253. console.log("Downloaded "+name);
  254. process.exit(0);
  255. });
  256. response.pipe(file);
  257. }).end();
  258. }
  259. } else if (command == "start" && instance === undefined) {
  260. console.error("ERROR: No instanceName provided!");
  261. console.error("Usage: node client.js start [instanceName]");
  262. process.exit(0);
  263. } else if (command == "start" && typeof instance == "string" && instance != "/" && !fs.existsSync(instancedirectory)) {
  264. // if instance does not exist, create it
  265. console.log("Creating instance...");
  266. fs.mkdirSync(instancedirectory);
  267. fs.mkdirSync(instancedirectory + "/script-output/");
  268. fs.mkdirSync(instancedirectory + "/saves/");
  269. fs.writeFileSync(instancedirectory + "/script-output/output.txt", "");
  270. fs.writeFileSync(instancedirectory + "/script-output/orders.txt", "");
  271. fs.writeFileSync(instancedirectory + "/script-output/txbuffer.txt", "");
  272. fs.mkdirSync(instancedirectory + "/mods/");
  273. fs.mkdirSync(instancedirectory + "/instanceMods/");
  274. fs.mkdirSync(instancedirectory + "/scenarios/");
  275. ncp("./lib/scenarios", instancedirectory + "/scenarios/", err => {
  276. if (err) console.error(err)
  277. });
  278.  
  279. // fs.symlinkSync('../../../sharedMods', instancedirectory + "/mods", 'junction') // This is broken because it can only take a file as first argument, not a folder
  280. fs.writeFileSync(instancedirectory + `/config.ini`, `[path]\r\n
  281. read-data=${ path.resolve(config.factorioDirectory, "data") }\r\n
  282. write-data=${ path.resolve(config.instanceDirectory, instance) }\r\n
  283. `);
  284.  
  285. // this line is probably not needed anymore but Im not gonna remove it
  286. fs.copySync('sharedMods', path.join(instancedirectory, "mods"));
  287. let instconf = {
  288. "factorioPort": args.port || process.env.FACTORIOPORT || Math.floor(Math.random() * 65535),
  289. "clientPort": args["rcon-port"] || process.env.RCONPORT || Math.floor(Math.random() * 65535),
  290. "clientPassword": args["rcon-password"] || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 8),
  291. }
  292. console.log("Clusterio | Created instance with settings:")
  293. console.log(instconf);
  294.  
  295. // create instance config
  296. fs.writeFileSync(instancedirectory + "/config.json", JSON.stringify(instconf, null, 4));
  297.  
  298. let name = "Clusterio instance: " + instance;
  299. if (config.username) {
  300. name = config.username + "'s clusterio " + instance;
  301. }
  302. let serversettings = {
  303. "name": name,
  304. "description": config.description,
  305. "tags": ["clusterio"],
  306. "max_players": "20",
  307. "visibility": config.visibility,
  308. "username": config.username,
  309. "token": config.token,
  310. "game_password": config.game_password,
  311. "verify_user_identity": config.verify_user_identity,
  312. "admins": [config.username],
  313. "allow_commands": config.allow_commands,
  314. "autosave_interval": 10,
  315. "autosave_slots": 5,
  316. "afk_autokick_interval": 0,
  317. "auto_pause": config.auto_pause,
  318. };
  319. fs.writeFileSync(instancedirectory + "/server-settings.json", JSON.stringify(serversettings, null, 4));
  320. console.log("Server settings: "+JSON.stringify(serversettings, null, 4));
  321. console.log("Creating save .....");
  322. let factorio = child_process.spawn(
  323. './' + config.factorioDirectory + '/bin/x64/factorio', [
  324. '-c', instancedirectory + '/config.ini',
  325. // '--create', instancedirectory + '/saves/save.zip',
  326. '--start-server-load-scenario', 'Hotpatch',
  327. '--server-settings', instancedirectory + '/server-settings.json',
  328. '--rcon-port', Number(process.env.RCONPORT) || instconf.clientPort,
  329. '--rcon-password', instconf.clientPassword,
  330. ], {
  331. 'stdio': ['pipe', 'pipe', 'pipe']
  332. }
  333. );
  334. factorio.stdout.on("data", data => {
  335. data = data.toString("utf8").replace(/(\r\n\t|\n|\r\t)/gm,"");
  336. console.log(data);
  337. if(data.includes("Starting RCON interface")){
  338. let client = new Rcon();
  339. client.connect({
  340. host: 'localhost',
  341. port: Number(process.env.RCONPORT) || instconf.clientPort,
  342. password: instconf.clientPassword,
  343. timeout: 5000
  344. });
  345. client.onDidAuthenticate(() => {
  346. console.log('Clusterio | RCON Authenticated!');
  347. });
  348. client.onDidConnect(() => {
  349. console.log('Clusterio | RCON Connected, starting save');
  350. client.send("/c game.server_save('hotpachSave')");
  351. });
  352. }
  353. if(data.includes("Saving finished")){
  354. console.log("Map saved as hotpachSave.zip, exiting...");
  355. console.log("Instance created!")
  356. process.exit(0);
  357. }
  358. });} else if (command == "start" && typeof instance == "string" && instance != "/" && fs.existsSync(instancedirectory)) {
  359. // Exit if no instance specified (it should be, just a safeguard);
  360. if(instancedirectory != config.instanceDirectory+"/undefined"){
  361. var instanceconfig = require(path.resolve(instancedirectory,'config'));
  362. instanceconfig.unique = stringUtils.hashCode(instanceconfig.clientPassword);
  363. if(process.env.FACTORIOPORT){
  364. instanceconfig.factorioPort = process.env.FACTORIOPORT;
  365. }
  366. if(process.env.RCONPORT){
  367. instanceconfig.rconPort = process.env.RCONPORT;
  368. }
  369. } else {
  370. process.exit(1);
  371. }
  372. console.log("Deleting .tmp.zip files");
  373. let savefiles = fs.readdirSync(path.join(instancedirectory,"saves"));
  374. for(i = 0; i < savefiles.length; i++){
  375. if(savefiles[i].substr(savefiles[i].length - 8, 8) == ".tmp.zip") {
  376. fs.unlinkSync(path.resolve(instancedirectory, "saves", savefiles[i]));
  377. }
  378. }
  379. console.log("Deleting logs");
  380. // clean old log file to avoid crash
  381. // file exists, delete so we don't get in trouble
  382. try {
  383. fs.unlinkSync(path.join(instancedirectory,'factorio-current.log'));
  384. } catch (err){
  385. if(err){
  386. console.error(err);
  387. } else {
  388. console.log("Clusterio | Deleting old logs...");
  389. }
  390. }
  391.  
  392. // move mods from ./sharedMods to the instances mod directory
  393. try{fs.mkdirSync(path.join(instancedirectory, "instanceMods"));}catch(e){}
  394. try{rmdirSync(path.join(instancedirectory, "mods"));}catch(e){}
  395. try {
  396. // mods directory that will be emptied (deleted) when closing the server to facilitate seperation of instanceMods and sharedMods
  397. fs.mkdirSync(path.join(instancedirectory, "mods"));
  398. } catch(e){}
  399. console.log("Clusterio | Moving shared mods from sharedMods/ to instance/mods...");
  400. fs.copySync('sharedMods', path.join(instancedirectory, "mods"));
  401. console.log("Clusterio | Moving instance specific mods from instance/instanceMods to instance/mods...");
  402. fs.copySync(path.join(instancedirectory, "instanceMods"), path.join(instancedirectory, "mods"));
  403.  
  404. process.on('SIGINT', function () {
  405. console.log("Caught interrupt signal, sending /quit");
  406. messageInterface("/quit");
  407. });
  408.  
  409. // Spawn factorio server
  410. //var serverprocess = child_process.exec(commandline);
  411. fileOps.getNewestFile(instancedirectory + "/saves/", fs.readdirSync(instancedirectory + "/saves/"),function(err, latestSave) {
  412. if(err) {
  413. console.error("ERROR!");
  414. console.error("Your savefile seems to be missing. This might because you created an instance without having factorio\
  415. installed and configured properly. Try installing factorio and adding your savefile to instances/[instancename]/saves/\n");
  416. throw err;
  417. }
  418. // implicit global
  419. serverprocess = child_process.spawn(
  420. './' + config.factorioDirectory + '/bin/x64/factorio', [
  421. '-c', instancedirectory + '/config.ini',
  422. '--start-server', latestSave.file,
  423. '--rcon-port', args["rcon-port"] || Number(process.env.RCONPORT) || instanceconfig.clientPort,
  424. '--rcon-password', args["rcon-password"] || instanceconfig.clientPassword,
  425. '--server-settings', instancedirectory + '/server-settings.json',
  426. '--port', args.port || Number(process.env.FACTORIOPORT) || instanceconfig.factorioPort
  427. ], {
  428. 'stdio': ['pipe', 'pipe', 'pipe']
  429. }
  430. );
  431.  
  432. serverprocess.on('close', code => {
  433. console.log(`child process exited with code ${code}`);
  434. process.exit();
  435. });
  436. serverprocess.stdout.on("data", data => {
  437. // log("Stdout: " + data);
  438. if(data.toString('utf8').includes("Couldn't parse RCON data: Maximum payload size exceeded")){
  439. console.error("ERROR: RCON CONNECTION BROKE DUE TO TOO LARGE PACKET!");
  440. console.error("Attempting reconnect...");
  441. client.disconnect();
  442. client.connect();
  443. }
  444. // we have to do this to make logs visible on linux and in powershell. Causes log duplication for people with CMD.
  445. console.log('Fact: ' + data.toString("utf8").replace("\n", ""));
  446. });
  447. serverprocess.stderr.on('data', (chunk) => {
  448. console.log('ERR: ' + chunk);
  449. });
  450.  
  451. // connect to the server with rcon
  452. if(true || process.platform != "linux"){
  453. // IP, port, password
  454. client = new Rcon();
  455.  
  456. // check the logfile to see if the RCON interface is running as there is no way to continue without it
  457. // we read the log every 2 seconds and stop looping when we start connecting to factorio
  458. function checkRcon() {
  459. fs.readFile(instancedirectory+"/factorio-current.log", function (err, data) {
  460. // if (err) console.error(err);
  461. if(data && data.indexOf('Starting RCON interface') > 0){
  462. client.connect({
  463. host: 'localhost',
  464. port: args["rcon-port"] || Number(process.env.RCONPORT) || instanceconfig.clientPort,
  465. password: args["rcon-password"] || instanceconfig.clientPassword,
  466. timeout: 5000
  467. });
  468. } else {
  469. setTimeout(function(){
  470. checkRcon();
  471. },5000);
  472. }
  473. });
  474. }
  475. setTimeout(checkRcon, 5000);
  476.  
  477. client.onDidAuthenticate(() => {
  478. console.log('Clusterio | RCON Authenticated!');
  479. instanceManagement(); // start using rcons
  480. });
  481. client.onDidConnect(() => {
  482. console.log('Clusterio | RCON Connected!');
  483. // getID();
  484. });
  485. client.onDidDisconnect(() => {
  486. console.log('Clusterio | RCON Disconnected!');
  487. process.exit(0); // exit because RCON disconnecting is undefined behaviour and we rather just wanna restart now
  488. });
  489. } else if(process.platform == "linux"){
  490. // don't open an RCON connection and just use stdio instead, does not work on windows.
  491. instanceManagement();
  492. }
  493.  
  494. // set some globals
  495. confirmedOrders = [];
  496. lastSignalCheck = Date.now();
  497. });
  498. } else {
  499. console.error("Invalid arguments, quitting.");
  500. process.exit(1);
  501. }
  502.  
  503. // ensure instancemanagement only ever runs once
  504. _.once(instanceManagement);
  505. function instanceManagement() {
  506. console.log("Started instanceManagement();");
  507.  
  508. /* Open websocket connection to master */
  509. var socket = ioClient("http://"+config.masterIP+":"+config.masterPort);
  510. socket.on("hello", data => {
  511. console.log("SOCKET | registering slave!");
  512. socket.emit("registerSlave", {
  513. instanceID: instanceconfig.unique,
  514. });
  515. });
  516. setInterval(B=> socket.emit("heartbeat"), 10000);
  517.  
  518. // load plugins and execute onLoad event
  519. let pluginDirectories = fileOps.getDirectoriesSync("./sharedPlugins/");
  520. let plugins = [];
  521. for(let i=0; i<pluginDirectories.length; i++) {
  522. let I = i
  523. let log = function(message) {
  524. console.log("Clusterio | "+ pluginDirectories[I] + " | " + message);
  525. }
  526. // these are our two config files. We need to send these in case plugin
  527. // wants to contact master or know something.
  528. let combinedConfig = deepmerge(instanceconfig,config,{clone:true})
  529. let pluginConfig = require("./sharedPlugins/" + pluginDirectories[i] + "/config.js");
  530.  
  531. if(!global.subscribedFiles) {
  532. global.subscribedFiles = {};
  533. }
  534. if(pluginConfig.binary == "nodePackage"){
  535. // require index.js.main() of plugin and execute it as a class
  536. let pluginClass = require("./sharedPlugins/" + pluginDirectories[I] + "/index.js");
  537. plugins[I] = new pluginClass(combinedConfig, async function(data, callback){
  538. if(data.toString('utf8')[0] != "/") {
  539. log("Stdout: " + data.toString('utf8'));
  540. return true;
  541. } else {
  542. return messageInterface(data.toString('utf8'), callback);
  543. }
  544. }, { // extra functions to pass in object. Should have done it like this from the start, but won't break backwards compat.
  545. socket, // socket.io connection to master (and ES6 destructuring, yay)
  546. });
  547. if(plugins[I].factorioOutput && typeof plugins[I].factorioOutput === "function"){
  548. // when factorio logs a line, send it to the plugin. This includes things like autosaves, chat, errors etc
  549. serverprocess.stdout.on("data", data => plugins[I].factorioOutput(data.toString()));
  550. }
  551. if(pluginConfig.scriptOutputFileSubscription && typeof pluginConfig.scriptOutputFileSubscription == "string"){
  552. if(global.subscribedFiles[pluginConfig.scriptOutputFileSubscription]) {
  553. // please choose a unique file to subscribe to. If you need plugins to share this interface, set up a direct communication
  554. // between those plugins instead.
  555. throw "FATAL ERROR IN " + pluginDirectories[i] + " FILE ALREADY SUBSCRIBED " + pluginConfig.scriptOutputFileSubscription;
  556. }
  557.  
  558. if (!fs.existsSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription)) {
  559. // Do something
  560. fs.writeFileSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, "");
  561. }
  562. global.subscribedFiles[pluginConfig.scriptOutputFileSubscription] = true;
  563. console.log("Clusterio | Registered file subscription on "+instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription);
  564. fs.watch(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, fileChangeHandler);
  565. // run once in case a plugin wrote out information before the plugin loaded fully
  566.  
  567. // delay, so the socket got enough time to connect
  568. setTimeout(()=> {
  569. fileChangeHandler(false, pluginConfig.scriptOutputFileSubscription);
  570. }, 500);
  571.  
  572. // send file contents to plugin for processing
  573. function fileChangeHandler(eventType, filename) {
  574. if(filename != null){
  575. setTimeout(
  576. ()=>{
  577. // get array of lines in file
  578. let stuff = fs.readFileSync(instancedirectory + "/script-output/" + filename, "utf8").split("\n");
  579.  
  580. // if you found anything, reset the file
  581. if (stuff[0]) {
  582. fs.writeFileSync(instancedirectory + "/script-output/" + filename, "");
  583. }
  584. for(let i = 0; i < stuff.length; i++) {
  585. if(stuff[i] && !stuff[i].includes('\u0000\u0000')) {
  586. try{
  587. plugins[I].scriptOutput(stuff[i]);
  588. }catch(e){console.error(e)}
  589. }
  590. }
  591. },
  592. pluginConfig.fileReadDelay || 0
  593. );
  594. }
  595. }
  596. }
  597. console.log("Clusterio | Loaded plugin " + pluginDirectories[i]);
  598. } else if(pluginConfig.binary != "nodePackage" && pluginConfig.binary){
  599. // handle as fragile executable plugin
  600. let args = pluginConfig.args || [];
  601. plugins[I]=child_process.spawn(pluginConfig.binary, args, {
  602. cwd: "./sharedPlugins/"+pluginDirectories[i],
  603. stdio: ['pipe', 'pipe', 'pipe'],
  604. });
  605.  
  606. /*
  607. to send to stdin, use:
  608. spawn.stdin.write("text\n");
  609. */
  610. // If plugin has subscribed to a file, send any text appearing in that file to stdin
  611. if(pluginConfig.scriptOutputFileSubscription && typeof pluginConfig.scriptOutputFileSubscription == "string") {
  612. if(global.subscribedFiles[pluginConfig.scriptOutputFileSubscription]) {
  613. // please choose a unique file to subscribe to. If you need plugins to share this interface, set up a direct communication
  614. // between those plugins instead.
  615. throw "FATAL ERROR IN " + pluginDirectories[i] + " FILE ALREADY SUBSCRIBED " + pluginConfig.scriptOutputFileSubscription;
  616. }
  617.  
  618. if (!fs.existsSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription)) {
  619. // Do something
  620. fs.writeFileSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, "");
  621. }
  622. global.subscribedFiles[pluginConfig.scriptOutputFileSubscription] = true;
  623. fs.watch(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, function (eventType, filename) {
  624. // get array of lines in file
  625. // TODO async
  626. let stuff = fs.readFileSync(instancedirectory + "/script-output/" + filename, "utf8").split("\n");
  627. // if you found anything, reset the file
  628. if (stuff[0]) {
  629. fs.writeFileSync(instancedirectory + "/script-output/" + filename, "");
  630. }
  631. for (let i = 0; i < stuff.length; i++) {
  632. if (stuff[i]) {
  633. plugins[I].stdin.write(stuff[i]);
  634. }
  635. }
  636. });
  637. }
  638. // these are our two config files. We need to send these in case plugin
  639. // wants to contact master or know something.
  640. // send through script-output file, maybe more compat?
  641. fs.writeFileSync(instancedirectory + "/script-output/" + pluginConfig.scriptOutputFileSubscription, JSON.stringify(combinedConfig)+"\r\n");
  642. // send directly through stdin
  643. // plugins[i].stdin.write(JSON.stringify(combinedConfig)+"\n");
  644.  
  645. console.log("Clusterio | Loaded plugin " + pluginDirectories[i]);
  646. plugins[i].stdout.on("data", (data) => {
  647. if(data.toString('utf8')[0] != "/") {
  648. log("Stdout: " + data.toString('utf8'))
  649. } else {
  650. messageInterface(data.toString('utf8'));
  651. }
  652. });
  653. plugins[i].stderr.on("data", (data) => {
  654. log("STDERR: " + data);
  655. });
  656. plugins[i].on('close', (code) => {
  657. log(`child process exited with code ${code}`);
  658. });
  659. } else {
  660. // This plugin doesn't have a client portion
  661. }
  662. }
  663.  
  664. // world IDs ------------------------------------------------------------------
  665. hashMods(instance, function(modHashes){
  666. setInterval(getID, 10000);
  667. getID();
  668. function getID() {
  669. messageInterface("/silent-command rcon.print(#game.connected_players)", function(playerCount) {
  670. var payload = {
  671. time: Date.now(),
  672. rconPort: instanceconfig.clientPort,
  673. rconPassword: instanceconfig.clientPassword,
  674. serverPort: instanceconfig.factorioPort,
  675. unique: instanceconfig.unique,
  676. publicIP: config.publicIP, // IP of the server should be global for all instances, so we pull that straight from the config
  677. mods:modHashes,
  678. instanceName: instance,
  679. playerCount:playerCount.replace(/(\r\n\t|\n|\r\t)/gm, ""),
  680. }
  681.  
  682. function callback(err, mac) {
  683. if (err) {
  684. mac = "unknown";
  685. console.log("##### getMac crashed, but we don't really give a shit because we are probably closing down #####");
  686. }
  687. payload.mac = mac;
  688. console.log("Registered our presence with master "+config.masterIP+" at " + payload.time);
  689. needle.post(config.masterIP + ":" + config.masterPort + '/api/getID', payload, needleOptionsWithTokenAuthHeader, function (err, response, body) {
  690. if (err && err.code != "ECONNRESET"){
  691. console.error("We got problems, something went wrong when contacting master"+config.masterIP+" at " + payload.time);
  692. console.error(err);
  693. } else if (response && response.body) {
  694. // In the future we might be interested in whether or not we actually manage to send it, but honestly I don't care.
  695. if(response.body !== "ok") {
  696. console.log("Got no \"ok\" while registering our precense with master "+config.masterIP+" at " + payload.time);
  697. console.log(response.body);
  698. }
  699. }
  700. });
  701. }
  702. if(global.mac){
  703. callback(undefined, global.mac);
  704. } else {
  705. getMac(callback);
  706. }
  707. });
  708. }
  709. });
  710. // Mod uploading and management -----------------------------------------------
  711. // get mod names and hashes
  712. // string: instance, function: callback
  713.  
  714. setTimeout(function(){hashMods(instance, uploadMods)}, 5000);
  715. function uploadMods(modHashes) {
  716. // [{modName:string,hash:string}, ... ]
  717. for(let i=0;i<modHashes.length;i++){
  718. let payload = {
  719. modName: modHashes[i].modName,
  720. hash: modHashes[i].hash,
  721. }
  722. needle.post(config.masterIP + ":" + config.masterPort + '/api/checkMod', payload, needleOptionsWithTokenAuthHeader, function (err, response, body) {
  723. if(err) console.error("Unable to contact master server! Please check your config.json.");
  724. if(response && body && body == "found") {
  725. console.log("master has mod "+modHashes[i].modName);
  726. } else if (response && body && typeof body == "string") {
  727. let mod = response.body;
  728. if(config.uploadModsToMaster){
  729. console.log("Sending mod: " + mod);
  730. // Send mods master says it wants
  731. // response.body is a string which is a modName.zip
  732. var req = request.post({url: "http://"+config.masterIP + ":" + config.masterPort + '/api/uploadMod',
  733. headers: {
  734. "x-access-token": config.masterAuthToken,
  735. },
  736. }, function (err, resp, body) {
  737. if (err) {
  738. console.error(new Error("Unable to contact master server! Please check your config.json."));
  739. } else {
  740. console.log('URL: ' + body);
  741. }
  742. });
  743. var form = req.form();
  744. form.append('file', fs.createReadStream(config.instanceDirectory+"/"+instance+"/mods/"+mod));
  745. } else {
  746. console.log("Not sending mod: " + mod + " to master because config.uploadModsToMaster is not enabled")
  747. }
  748. }
  749. });
  750. }
  751. }
  752.  
  753. // flow/production statistics ------------------------------------------------------------
  754. oldFlowStats = false
  755. setInterval(function(){
  756. fs.readFile(instancedirectory + "/script-output/flows.txt", {encoding: "utf8"}, function(err, data) {
  757. if(!err && data) {
  758. let timestamp = Date.now();
  759. data = data.split("\n");
  760. let flowStats = [];
  761. for(let i = 0; i < data.length; i++) {
  762. // try catch to remove any invalid json
  763. try{
  764. flowStats[flowStats.length] = JSON.parse(data[i]);
  765. } catch (e) {
  766. // console.log(" invalid json: " + i);
  767. // some lines of JSON are invalid but don't worry, we just filter em out
  768. }
  769. }
  770. // fluids
  771. let flowStat1 = flowStats[flowStats.length-1].flows.player.input_counts
  772. // items
  773. let flowStat2 = flowStats[flowStats.length-2].flows.player.input_counts
  774. // merge fluid and item flows
  775. let totalFlows = {};
  776. for(let key in flowStat1) totalFlows[key] = flowStat1[key];
  777. for(let key in flowStat2) totalFlows[key] = flowStat2[key];
  778. if(oldFlowStats && totalFlows && oldTimestamp) {
  779. let payload = objectOps.deepclone(totalFlows);
  780. // change from total reported to per time unit
  781. for(let key in oldFlowStats) {
  782. // get production per minute
  783. payload[key] = Math.floor((payload[key] - oldFlowStats[key])/(timestamp - oldTimestamp)*60000);
  784. if(payload[key] < 0) {
  785. payload[key] = 0;
  786. }
  787. }
  788. for(let key in payload) {
  789. if(payload[key] == '0') {
  790. delete payload[key];
  791. }
  792. }
  793. console.log("Recorded flows, copper plate since last time: " + payload["copper-plate"]);
  794. needle.post(config.masterIP + ":" + config.masterPort + '/api/logStats', {timestamp: timestamp, instanceID: instanceconfig.unique,data: payload}, needleOptionsWithTokenAuthHeader, function (err, response, body) {
  795. // we did it, keep going
  796. });
  797. }
  798. oldTimestamp = timestamp;
  799. oldFlowStats = totalFlows;
  800. fs.writeFileSync(instancedirectory + "/script-output/flows.txt", "");
  801. }
  802. });
  803. // we don't need to update stats quickly as that could be expensive
  804. }, 60000*5);
  805.  
  806. // provide items --------------------------------------------------------------
  807. // trigger when something happens to output.txt
  808. fs.watch(instancedirectory + "/script-output/output.txt", function (eventType, filename) {
  809. // get array of lines in file
  810. let items = fs.readFileSync(instancedirectory + "/script-output/output.txt", "utf8").split("\n");
  811. // if you found anything, reset the file
  812. if (items[0]) {
  813. fs.writeFileSync(instancedirectory + "/script-output/output.txt", "");
  814. }
  815. for (let i = 0; i < items.length; i++) {
  816. if (items[i]) {
  817. let g = items[i].split(" ");
  818. g[0] = g[0].replace("\u0000", "");
  819. // console.log("exporting " + JSON.stringify(g));
  820. // send our entity and count to the master for him to keep track of
  821. needle.post(config.masterIP + ":" + config.masterPort + '/api/place', {
  822. name: g[0],
  823. count: g[1],
  824. instanceName: instance, // name of instance
  825. instanceID: instanceconfig.unique, // a hash computed from the randomly generated rcon password
  826. }, needleOptionsWithTokenAuthHeader, function (err, resp, body) {
  827. if(body == "failure") console.error("#### Export failed! Lost: "+g[1]+" "+g[0]);
  828. if(config.logItemTransfers){
  829. if(body == "success") console.log(`Exported ${g[1]} ${g[0]} to master`);
  830. }
  831. });
  832. }
  833. }
  834. });
  835. // request items --------------------------------------------------------------
  836. setInterval(function () {
  837. // get array of lines in file
  838. let items = fs.readFileSync(instancedirectory + "/script-output/orders.txt", "utf8").split("\n");
  839. // if we actually got anything from the file, proceed and reset file
  840. if (items[0]) {
  841. fs.writeFileSync(instancedirectory + "/script-output/orders.txt", "");
  842. // prepare a package of all our requested items in a more tranfer friendly format
  843. var preparedPackage = {};
  844. for (let i = 0; i < items.length; i++) {
  845. (function (i) {
  846. if (items[i]) {
  847. items[i] = items[i].split(" ");
  848. items[i][0] = items[i][0].replace("\u0000", "");
  849. items[i][0] = items[i][0].replace(",", "");
  850. if (preparedPackage[items[i][0]]) {
  851. // if we have buffered some already, sum the new items
  852. if (typeof Number(preparedPackage[items[i][0]].count) == "number" && typeof Number(items[i][1]) == "number") {
  853. preparedPackage[items[i][0]] = {
  854. "name": items[i][0],
  855. "count": Number(preparedPackage[items[i][0]].count) + Number(items[i][1]),
  856. "instanceName":instance,
  857. "instanceID":instanceconfig.unique,
  858. };
  859. // else just add em in without summing
  860. } else if (typeof Number(items[i][1]) == "number") {
  861. preparedPackage[items[i][0]] = {
  862. "name": items[i][0],
  863. "count": Number(items[i][1]),
  864. "instanceName":instance,
  865. "instanceID":instanceconfig.unique,
  866. };
  867. }
  868. // this condition will NEVER be triggered but we know how that goes
  869. } else if (typeof Number(items[i][1]) == "number") {
  870. preparedPackage[items[i][0]] = {
  871. "name": items[i][0],
  872. "count": Number(items[i][1]),
  873. "instanceName":instance,
  874. "instanceID":instanceconfig.unique,
  875. };
  876. }
  877. }
  878. })(i);
  879. }
  880. // request our items, one item at a time
  881. for (let i = 0; i < Object.keys(preparedPackage).length; i++) {
  882. // console.log(preparedPackage[Object.keys(preparedPackage)[i]]);
  883. needle.post(config.masterIP + ":" + config.masterPort + '/api/remove', preparedPackage[Object.keys(preparedPackage)[i]], needleOptionsWithTokenAuthHeader, function (err, response, body) {
  884. if (response && response.body && typeof response.body == "object") {
  885. // buffer confirmed orders
  886. confirmedOrders[confirmedOrders.length] = {name:response.body.name,count:response.body.count}
  887. if(config.logItemTransfers){
  888. console.log(`Imported ${response.body.count} ${response.body.name} from master`);
  889. }
  890. }
  891. });
  892. }
  893. // if we got some confirmed orders
  894. // console.log("Importing " + confirmedOrders.length + " items! " + JSON.stringify(confirmedOrders));
  895. //if (!(confirmedOrders.length>0)){return;}
  896. let cmd="local t={";
  897. for(let i=0;i<confirmedOrders.length;i++)
  898. {
  899. cmd+='["'+confirmedOrders[i].name+'"]='+confirmedOrders[i].count+',';
  900. if(cmd.length>320) // Factorio max packet size is 508
  901. {
  902. messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+cmd.slice(0, -1)+"}"+ " for k, item in pairs(t) do GiveItemsToStorage(k, item) end')");
  903. cmd="local t={";
  904. }
  905. }
  906. if (!(cmd==="local t={")){
  907. messageInterface("/silent-command remote.call('clusterio', 'runcode', '"+cmd.slice(0, -1)+"}"+ " for k, item in pairs(t) do GiveItemsToStorage(k, item) end')");
  908. }
  909. confirmedOrders=[];
  910. }
  911. }, 1000);
  912. // COMBINATOR SIGNALS ---------------------------------------------------------
  913. // get inventory from Master and RCON it to our slave
  914. setInterval(function () {
  915. needle.get(config.masterIP + ":" + config.masterPort + '/api/inventory', function (err, response, body) {
  916. if(err){
  917. console.error("Unable to get JSON master/api/inventory, master might be unaccessible");
  918. } else if (response && response.body) {
  919. // Take the inventory we (hopefully) got and turn it into the format LUA accepts
  920. if(Buffer.isBuffer(response.body)) {console.log(response.body.toString("utf-8")); throw new Error();} // We are probably contacting the wrong webserver
  921. try {
  922. var inventory = JSON.parse(response.body);
  923. var inventoryFrame = {};
  924. for (let i = 0; i < inventory.length; i++) {
  925. inventoryFrame[inventory[i].name] = Number(inventory[i].count);
  926. if(inventoryFrame[inventory[i].name] >= Math.pow(2, 31)){
  927. inventoryFrame[inventory[i].name] = Math.pow(2, 30); // set it waaay lower, 31 -1 would probably suffice
  928. }
  929. }
  930. inventoryFrame["signal-unixtime"] = Math.floor(Date.now()/1000);
  931. // console.log("RCONing inventory! " + JSON.stringify(inventoryFrame));
  932. let first = true;
  933. let cmd="local s={";
  934. for (let key in inventoryFrame)
  935. {
  936. cmd+='["'+key+'"]='+inventoryFrame[key]+",";
  937. if(first && cmd.length>300 || !first && cmd.length > 320) // Factorio max packet size is 508
  938. {
  939. 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')");
  940. cmd="local s={";
  941. first = false;
  942. }
  943. }
  944. if (!(cmd==="local s={")){
  945. 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')");
  946. }
  947. messageInterface("/silent-command remote.call('clusterio', 'runcode', 'UpdateInvCombinators()')");
  948. } catch (e){
  949. console.log(e);
  950. }
  951. }
  952. });
  953. }, 1550);
  954. // Make sure world has its worldID
  955. setTimeout(function(){
  956. messageInterface("/silent-command remote.call('clusterio','setWorldID',"+instanceconfig.unique+")")
  957. }, 20000);
  958. /* REMOTE SIGNALLING
  959. * send any signals the slave has been told to send
  960. * Fetch combinator signals from the server
  961. */
  962. socket.on("processCombinatorSignal", circuitFrameWithMeta => {
  963. if(circuitFrameWithMeta && typeof circuitFrameWithMeta == "object" && circuitFrameWithMeta.frame && Array.isArray(circuitFrameWithMeta.frame)){
  964. messageInterface("/silent-command remote.call('clusterio', 'receiveFrame', '"+JSON.stringify(circuitFrameWithMeta.frame)+"')");
  965. }
  966. });
  967. // get outbound frames from file and send to master
  968. // get array of lines in file, each line should correspond to a JSON encoded frame
  969. let signals = fs.readFileSync(instancedirectory + "/script-output/txbuffer.txt", "utf8").split("\n");
  970. // if we actually got anything from the file, proceed and reset file
  971. let readingTxBufferSoon = false;
  972. let txBufferClearCounter = 0;
  973. fs.watch(instancedirectory + "/script-output/txbuffer.txt", "utf-8", (eventType, filename) => {
  974. if(!readingTxBufferSoon){ // use a 100ms delay to avoid messing with rapid sequential writes from factorio (I think that might be a problem maybe?)
  975. readingTxBufferSoon = true;
  976. setTimeout(()=>{
  977. txBufferClearCounter++;
  978. fs.readFile(instancedirectory + "/script-output/txbuffer.txt", "utf-8", (err, signals) => {
  979. signals = signals.split("\n");
  980. if (signals[0]) {
  981. //if(txBufferClearCounter > 500){
  982. fs.writeFileSync(instancedirectory + "/script-output/txbuffer.txt", "");
  983. // txBufferClearCounter = 0;
  984. //}
  985.  
  986. // loop through all our frames
  987. for (let i = 0; i < signals.length; i++) {
  988. if (signals[i] && objectOps.isJSON(signals[i])) {
  989. // signals[i] is a JSON array called a "frame" of signals. We timestamp it for storage on master
  990. // then we unpack and RCON in this.frame to the game later.
  991. let framepart = JSON.parse(signals[i]);
  992. let doneframe = {
  993. time: Date.now(),
  994. frame: framepart, // thats our array of objects(single signals);
  995. }
  996. // send to master using socket.io, opened at the top of instanceManagement()
  997. socket.emit("combinatorSignal", doneframe);
  998. } else {
  999. // console.log("Invalid jsony: "+typeof signals[i])
  1000. }
  1001. }
  1002. }
  1003. });
  1004. readingTxBufferSoon = false;
  1005. },100);
  1006. }
  1007. });
  1008. } // END OF INSTANCE START ---------------------------------------------------------------------
  1009.  
  1010. // string, function
  1011. // returns [{modName:string,hash:string}, ... ]
  1012. function hashMods(instanceName, callback) {
  1013. if(!callback) {
  1014. throw new Error("ERROR in function hashMods NO CALLBACK");
  1015. }
  1016. function callback2(hash, modName){
  1017. hashedMods[hashedMods.length] = {
  1018. modName: modName,
  1019. hash: hash,
  1020. }
  1021. // check if this callback has ran once for each mod
  1022. if(hashedMods.length == instanceMods.length) {
  1023. callback(hashedMods);
  1024. }
  1025. //console.log(modname);
  1026. }
  1027. let hashedMods = [];
  1028. /*let mods = fs.readdirSync("./sharedMods/")*/
  1029. let instanceMods = fs.readdirSync(config.instanceDirectory+"/"+instanceName+"/mods/");
  1030. if(instanceMods.length == 0){
  1031. callback({}); // there are no mods installed, return an empty object
  1032. }
  1033. for(let o=0;o<instanceMods.length;o++) {
  1034. if(path.extname(instanceMods[o]) != ".zip") {
  1035. instanceMods.splice(instanceMods.indexOf(instanceMods[o]), 1); // remove element from array, we can't hash unzipped mods
  1036. }
  1037. }
  1038. for(let i=0; i<instanceMods.length; i++){
  1039. let path = config.instanceDirectory+"/"+instanceName+"/mods/"+instanceMods[i];
  1040. let name = instanceMods[i];
  1041. let options = {
  1042. files:path,
  1043. }
  1044. // options {files:[array of paths]}
  1045. hashFiles(options, function(error, hash) {
  1046. // hash will be a string if no error occurred
  1047. if(!error){
  1048. callback2(hash, name);
  1049. } else {
  1050. throw error;
  1051. }
  1052. });
  1053. }
  1054. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement