UNPKG

@clusterio/create

Version:

Installer for clusterio

978 lines (876 loc) 29 kB
#!/usr/bin/env node "use strict"; const child_process = require("child_process"); const fs = require("fs-extra"); const inquirer = require("inquirer"); const os = require("os"); const path = require("path"); const stream = require("stream"); const util = require("util"); const yargs = require("yargs"); const { levels, logger, setLogLevel } = require("./logging"); const { copyPluginTemplates } = require("./template"); let dev = false; const scriptExt = process.platform === "win32" ? ".cmd" : ""; const finished = util.promisify(stream.finished); const { escapeArg } = require("./escape_arg"); // I hate this, there is a bug with rl.close on windows that was meant to have been fixed in Node14.4 // See nodejs/node#21771 and nodejs/node#30701 for the apparent fix that was applied // One option is to not call rl.close: SBoudrias/Inquirer.js#767 // But this comes with its own issues: SBoudrias/Inquirer.js#899 // The solution below was adapted from aws-amplify/amplify-cli#12347 if (process.platform === "win32") { const _inquirer_close = inquirer.ui.Prompt.prototype.close; inquirer.ui.Prompt.prototype.close = function() { this.rl.terminal = false; _inquirer_close.call(this); }; } const availablePlugins = [ { name: "Global Chat", value: "@clusterio/plugin-global_chat" }, { name: "Inventory Sync", value: "@clusterio/plugin-inventory_sync" }, { name: "Player Auth", value: "@clusterio/plugin-player_auth" }, { name: "Research Sync", value: "@clusterio/plugin-research_sync" }, { name: "Statistics Exporter", value: "@clusterio/plugin-statistics_exporter" }, { name: "Subspace Storage", value: "@clusterio/plugin-subspace_storage" }, // Comunity plugins { name: "Discord Bridge", value: "@hornwitser/discord_bridge" }, { name: "Server Select", value: "@hornwitser/server_select" }, ]; const factorioLocations = { win32: [ "C:\\Program Files\\Factorio", "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Factorio", ], darwin: [ "/Applications/factorio.app/Contents", ], }; class LineSplitter extends stream.Transform { constructor(options) { super(options); this._partial = null; } _transform(chunk, encoding, callback) { if (this._partial) { chunk = Buffer.concat([this._partial, chunk]); this._partial = null; } while (chunk.length) { let end = chunk.indexOf("\n"); if (end === -1) { this._partial = chunk; break; } let next = end + 1; // Eat carriage return as well if present if (end >= 1 && chunk[end-1] === "\r".charCodeAt(0)) { end -= 1; } let line = chunk.slice(0, end); chunk = chunk.slice(next); this.push(line); } callback(); } _flush(callback) { if (this._partial) { this.push(this._partial); this._partial = null; } callback(); } } class InstallError extends Error { } async function safeOutputFile(file, data, options={}) { let { dir, name, ext } = path.parse(file); let temporary = path.join(dir, `${name}.tmp${ext}`); await fs.outputFile(temporary, data, options); await fs.rename(temporary, file); } async function execFile(cmd, args) { const escaped = args.map(escapeArg); logger.verbose(`executing ${cmd} ${escaped.join(" ")}`); return new Promise((resolve, reject) => { let child = child_process.execFile( cmd, escaped, { shell: true, env: process.platform === "win32" ? { ...process.env, pct: "%" } : undefined, }, (err, stdout, stderr) => { if (err) { reject(err); } else { resolve({ stdout, stderr }); } } ); let stdout = new LineSplitter({ readableObjectMode: true }); stdout.on("data", line => { logger.verbose(line.toString()); }); child.stdout.pipe(stdout); let stderr = new LineSplitter({ readableObjectMode: true }); stderr.on("data", line => { logger.verbose(`err: ${line.toString()}`); }); child.stderr.pipe(stderr); }); } async function execController(args) { if (dev) { return await execFile("node", [path.join(__dirname, "..", "controller"), ...args]); } return await execFile(path.join("node_modules", ".bin", `clusteriocontroller${scriptExt}`), args); } async function execHost(args) { if (dev) { return await execFile("node", [path.join(__dirname, "..", "host"), ...args]); } return await execFile(path.join("node_modules", ".bin", `clusteriohost${scriptExt}`), args); } async function execCtl(args) { if (dev) { return await execFile("node", [path.join(__dirname, "..", "ctl"), ...args]); } return await execFile(path.join("node_modules", ".bin", `clusterioctl${scriptExt}`), args); } function validateHostToken(token) { let parts = token.split("."); if (parts.length !== 3) { throw new InstallError("Invalid token"); } let parsed; try { parsed = JSON.parse(Buffer.from(parts[1], "base64")); } catch (err) { throw new InstallError("Invalid token"); } if (parsed.aud !== "host" || !Number.isInteger(parsed.host)) { throw new InstallError("Invalid token"); } } async function validateInstallDir() { let entries = new Set(await fs.readdir(".")); if (entries.size) { if (!entries.has("package.json")) { throw new InstallError("Refusing to install to non-empty directory"); } let packageData; try { packageData = JSON.parse(await fs.readFile("package.json")); } catch (err) { throw new InstallError(`Failed to read package.json: ${err.message}`); } if (packageData.name !== "clusterio-install" || !packageData.private) { throw new InstallError("Refusing to run on non-clusterio installation"); } } } async function validateNotRoot(args) { if (args.allowInstallAsRoot) { return; } if (os.userInfo().uid === 0) { throw new InstallError( "Refusing to install as root. Create a separate user account for clusterio " + "and then use su/sudo to switch to it before invoking the installer." ); } } async function migrateRename(args) { function rename(string) { return string.replace(/master/g, "controller").replace(/slave/g, "host"); } function renameConfig(config) { for (let group of config["groups"]) { group["name"] = rename(group["name"]); group["fields"] = Object.fromEntries( Object.entries(group["fields"]).map(([k, v]) => [rename(k), v]) ); } return config; } async function migrateConfig(source, destination) { if (await fs.pathExists(source) && !await fs.pathExists(destination)) { await safeOutputFile(destination, JSON.stringify( renameConfig(JSON.parse(await fs.readFile(source))), null, "\t" )); logger.info(`Migrated ${source} to ${destination}`); } } await migrateConfig("config-master.json", "config-controller.json"); await migrateConfig("config-slave.json", "config-host.json"); let instancesFile = path.join("database", "instances.json"); if (await fs.pathExists(instancesFile)) { await safeOutputFile(instancesFile, JSON.stringify( JSON.parse(await fs.readFile(instancesFile)).map(renameConfig), null, "\t" )); logger.info(`Migrated ${instancesFile}`); } let usersFile = path.join("database", "users.json"); if (await fs.pathExists(usersFile)) { let users = JSON.parse(await fs.readFile(usersFile)); for (let role of users["roles"]) { role["permissions"] = role["permissions"].map(rename); } await safeOutputFile(usersFile, JSON.stringify(users, null, "\t")); logger.info(`Migrated ${usersFile}`); } function renameLogLine(info) { return Object.fromEntries( Object.entries(info).map(([k, v]) => [rename(k), v]) ); } async function migrateLog(inputFile, outputFile) { let lineStream = new LineSplitter({ readableObjectMode: true }); let fileStream = fs.createReadStream(inputFile); fileStream.pipe(lineStream); let lines = []; for await (let inputLine of lineStream) { try { lines.push(Buffer.from(JSON.stringify(renameLogLine(JSON.parse(inputLine))))); lines.push(Buffer.from("\n")); } catch (err) { if (!(err instanceof SyntaxError)) { throw err; } logger.warn(`Invalid log line: ${inputLine.toString()}`); lines.push(inputLine); } } safeOutputFile(outputFile, Buffer.concat(lines)); } async function migrateLogsDir(inputLogs, outputLogs) { if (await fs.pathExists(inputLogs)) { logger.info(`Migrating ${inputLogs} to ${outputLogs}`); for (let logFile of await fs.readdir(inputLogs)) { if (!logFile.endsWith(".log")) { continue; } let inputFile = path.join(inputLogs, logFile); let outputFile = path.join(outputLogs, rename(logFile)); if (!await fs.pathExists(outputFile)) { await migrateLog(inputFile, outputFile); logger.verbose(`Migrated ${inputFile} to ${outputFile}`); } } } } await migrateLogsDir(path.join("logs", "master"), path.join("logs", "controller")); await migrateLogsDir(path.join("logs", "slave"), path.join("logs", "host")); if ( await fs.pathExists(path.join("logs", "cluster")) && !await fs.pathExists(path.join("logs", "cluster-prerename")) ) { await fs.rename(path.join("logs", "cluster"), path.join("logs", "cluster-prerename")); await migrateLogsDir(path.join("logs", "cluster-prerename"), path.join("logs", "cluster")); } if ( await fs.pathExists("sharedMods") && !await fs.pathExists("mods") ) { logger.info("Moving sharedMods/ to mods/"); await fs.rename("sharedMods", "mods"); } if (!args.dev) { let pkg = JSON.parse(await fs.readFile("package.json")); if (pkg.dependencies) { let convert = ["@clusterio/master", "@clusterio/slave"]; let uninstall = convert.filter(c => pkg.dependencies[c] !== undefined); let install = uninstall.map(rename); if (uninstall.length) { logger.info(`Replacing ${uninstall.join(" and ")}`); await execFile(`npm${scriptExt}`, ["install", ...install]); await execFile(`npm${scriptExt}`, ["uninstall", ...uninstall]); } } logger.info("Updating packages"); await execFile(`npm${scriptExt}`, ["update"]); } const hasRunMaster = await fs.pathExists("run-master.sh") || await fs.pathExists("run-master.cmd"); const hasRunSlave = await fs.pathExists("run-slave.sh") || await fs.pathExists("run-slave.cmd"); if (hasRunMaster || hasRunSlave) { logger.info("Writing run scripts"); let mode = "standalone"; if (!hasRunSlave) { mode = "controller"; } if (!hasRunMaster) { mode = "host"; } await writeScripts(mode); } logger.info( "Migration complete, you may now delete the following left over files and directories (if present):" + "\n- config-master.json" + "\n- config-slave.json" + "\n- logs/master" + "\n- logs/slave" + "\n- logs/cluster-prerename" + "\n- systemd/clusteriomaster.service" + "\n- systemd/clusterioslave.service" + "\n- run-master.sh / run-master.cmd" + "\n- run-slave.sh / run-slave.cmd" ); } async function installClusterio(mode, plugins) { try { await fs.outputFile("package.json", JSON.stringify({ name: "clusterio-install", private: true, }, null, 2), { flag: "wx" }); } catch (err) { if (err.code !== "EEXIST") { throw new InstallError(`Failed to write package.json: ${err.message}`); } } let components = []; if (["standalone", "controller"].includes(mode)) { components.push("@clusterio/controller"); } if (["standalone", "host"].includes(mode)) { components.push("@clusterio/host"); } if (mode === "ctl") { components.push("@clusterio/ctl"); } logger.info(`Please wait, installing ${mode}`); try { await execFile(`npm${scriptExt}`, ["install", ...components, ...plugins]); } catch (err) { throw new InstallError(`Failed to install: ${err.message}`); } if (plugins.length) { logger.info("Setting up plugins"); let pluginList; try { pluginList = new Map(JSON.parse(await fs.readFile("plugin-list.json"))); } catch (err) { if (err.code === "ENOENT") { pluginList = new Map(); } else { throw new InstallError(`Error loading plugin-list.json: ${err.message}`); } } for (let plugin of plugins) { if (!pluginList.has(plugin)) { let pluginInfo = require(require.resolve(plugin, { paths: [process.cwd()] })).plugin; pluginList.set(pluginInfo.name, plugin); } } try { await safeOutputFile("plugin-list.json", JSON.stringify([...pluginList], null, "\t")); } catch (err) { throw new InstallError(`Error writing plugin-list.json: ${err.message}`); } } } async function groupIdToName(gid) { try { let exec = util.promisify(child_process.exec); let { stdout } = await exec(`getent group ${gid}`); return stdout.split(":")[0]; } catch (err) { logger.warn(`getent group ${gid} failed: ${err.message}`); return gid; } } async function writeScripts(mode) { if (["standalone", "controller"].includes(mode)) { if (process.platform === "win32") { await safeOutputFile( "run-controller.cmd", `\ @echo off set "NODE_OPTIONS=--enable-source-maps %NODE_OPTIONS%" :restart call .\\node_modules\\.bin\\clusteriocontroller.cmd run --can-restart if %errorlevel% equ 0 exit /b if %errorlevel% equ 8 exit /b goto restart ` ); } else { await safeOutputFile( "run-controller.sh", `\ #!/bin/bash export "NODE_OPTIONS=--enable-source-maps $NODE_OPTIONS" while true; do ./node_modules/.bin/clusteriocontroller run --can-restart if [[ $? -eq 0 || $? -eq 8 ]]; then exit $?; fi done `, { mode: 0o755 }, ); await safeOutputFile( "systemd/clusteriocontroller.service", `\ [Unit] Description=Clusterio Controller [Service] User=${os.userInfo().username} Group=${await groupIdToName(os.userInfo().gid)} WorkingDirectory=${process.cwd()} KillMode=mixed KillSignal=SIGINT Environment=NODE_OPTIONS=--enable-source-maps ExecStart=${process.cwd()}/node_modules/.bin/clusteriocontroller run --log-level=warn --can-restart Restart=on-failure RestartPreventExitStatus=8 [Install] WantedBy=multi-user.target ` ); } } if (["standalone", "host"].includes(mode)) { if (process.platform === "win32") { await safeOutputFile( "run-host.cmd", `\ @echo off set "NODE_OPTIONS=--enable-source-maps %NODE_OPTIONS%" :restart call .\\node_modules\\.bin\\clusteriohost.cmd run --can-restart if %errorlevel% equ 0 exit /b if %errorlevel% equ 8 exit /b goto restart ` ); } else { await safeOutputFile( "run-host.sh", `\ #!/bin/bash export "NODE_OPTIONS=--enable-source-maps $NODE_OPTIONS" while true; do ./node_modules/.bin/clusteriohost run --can-restart if [[ $? -eq 0 || $? -eq 8 ]]; then exit $?; fi done `, { mode: 0o755 }, ); await safeOutputFile( "systemd/clusteriohost.service", `\ [Unit] Description=Clusterio Host [Service] User=${os.userInfo().username} Group=${await groupIdToName(os.userInfo().gid)} WorkingDirectory=${process.cwd()} KillMode=mixed KillSignal=SIGINT Environment=NODE_OPTIONS=--enable-source-maps ExecStart=${process.cwd()}/node_modules/.bin/clusteriohost run --log-level=warn --can-restart Restart=on-failure RestartPreventExitStatus=8 [Install] WantedBy=multi-user.target ` ); } } } // eslint-disable-next-line complexity async function inquirerMissingArgs(args) { let answers = {}; if (args.mode) { answers.mode = args.mode; } if (args["plugin-template"]) { answers.mode = "plugin-template"; } answers = await inquirer.prompt([ { type: "list", name: "mode", message: "Operating mode to install", default: "standalone", choices: [ { name: "Standalone (install both controller and host on this computer)", value: "standalone" }, { name: "Controller only", value: "controller" }, { name: "Host only", value: "host" }, { name: "Ctl only", value: "ctl" }, { name: "Plugins only", value: "plugins" }, { name: "Plugin template", value: "plugin-template" }, ], }, ], answers); if (["standalone", "controller"].includes(answers.mode)) { if (args.admin) { answers.admin = args.admin; } answers = await inquirer.prompt([ { type: "input", name: "admin", message: "Admin account name", validate: input => { if (!input) { return "May not be empty"; } return true; }, }, ], answers); if (args.httpPort) { answers.httpPort = args.httpPort; } answers = await inquirer.prompt([ { type: "input", name: "httpPort", message: "HTTP port to listen on", default: 8080, validate: input => { if (!input) { return "May not be empty"; } if (!Number.isInteger(Number(input))) { return "Must be a number"; } return true; }, }, ], answers); } if (answers.mode === "host") { if (args.hostName) { answers.hostName = args.hostName; } answers = await inquirer.prompt([ { type: "input", name: "hostName", message: "Name of host", }, ], answers); } if (["host", "ctl"].includes(answers.mode)) { if (args.controllerUrl) { answers.controllerUrl = args.controllerUrl; } answers = await inquirer.prompt([ { type: "input", name: "controllerUrl", message: "Controller URL", }, ], answers); if (args.controllerToken) { answers.controllerToken = args.controllerToken; } answers = await inquirer.prompt([ { type: "input", name: "controllerToken", message: "Controller authentication Token", }, ], answers); } if (answers.mode === "host") { validateHostToken(answers.controllerToken); } if (["standalone", "host"].includes(answers.mode)) { let myIp = "localhost"; if (args.publicAddress) { answers.publicAddress = args.publicAddress; } else { try { let result = await fetch("https://api.ipify.org/"); myIp = await result.text(); } catch (err) { /* ignore */ } } answers = await inquirer.prompt([ { type: "input", name: "publicAddress", message: "Public DNS/IP address of this server", default: myIp, }, ], answers); if (args.factorioDir) { answers.factorioDir = args.factorioDir; } let locations = factorioLocations[process.platform] || []; let foundLocations = []; for (let location of locations) { if (await fs.pathExists(path.join(location, "data", "changelog.txt"))) { foundLocations.push(location); } } if (process.platform === "linux") { if (args.hasOwnProperty("downloadHeadless")) { answers.downloadHeadless = args.downloadHeadless; } answers = await inquirer.prompt([ { type: "confirm", name: "downloadHeadless", message: "(Linux only) Automatically download latest factorio release?", default: true, }, ], answers); if (!answers.factorioDir && answers.downloadHeadless) { answers.factorioDir = "factorio"; } } answers = await inquirer.prompt([ { type: "list", name: "factorioDir", message: "Path to Factorio installation", choices: [ ...foundLocations.map(location => ({ name: `${location} (auto detected)`, value: location })), { name: "Use local factorio directory, you must copy an installation to it", value: "factorio" }, { name: "Provide path manually", value: null }, ], }, ], answers); if (answers.factorioDir === null) { answers = await inquirer.prompt([ { type: "input", name: "factorioDir", message: "Path to Factorio installation", askAnswered: true, }, ], answers); } else if (answers.factorioDir === "factorio") { await fs.ensureDir("factorio"); } } if (["standalone", "controller", "host"].includes(answers.mode)) { if (args.hasOwnProperty("remoteNpm")) { answers.remoteNpm = args.remoteNpm; } answers = await inquirer.prompt([ { type: "confirm", name: "remoteNpm", message: "Allow remote updates via npm?", default: true, }, ], answers); } if (answers.mode === "plugin-template") { if (args["plugin-template"] && args["plugin-template"].length > 0) { answers.pluginTemplate = args["plugin-template"]; } if (args["plugin-name"]) { answers.pluginName = args["plugin-name"]; } answers.plugins = []; answers = await inquirer.prompt([ { type: "checkbox", name: "pluginTemplate", message: "Plugin templates to use", choices: [ { name: "Controller", value: "controller" }, { name: "Host", value: "host" }, { name: "Instance", value: "instance" }, { name: "Lua Module", value: "module" }, { name: "Command Line", value: "ctl" }, { name: "Web UI", value: "web" }, { name: "No Typescript", value: "js" }, { name: "No Config", value: "no_config" }, ], }, { type: "input", name: "pluginName", message: "Name of your new plugin", default: path.basename(path.resolve()), }, ], answers); } if (dev) { answers.plugins = []; } else if (args.plugins) { answers.plugins = args.plugins; } answers = await inquirer.prompt([ { type: "checkbox", name: "plugins", message: "Plugins to install", choices: availablePlugins, pageSize: 20, }, ], answers); return answers; } async function downloadLinuxServer() { let res = await fetch("https://factorio.com/get-download/stable/headless/linux64"); const url = new URL(res.headers.location); // get the filename of the latest factorio archive from redirected url const filename = path.posix.basename(url.pathname); const match = filename.match(/(?<=factorio_headless_x64_|factorio-headless_linux_)\d+\.\d+\.\d+(?=\.tar\.xz)/); if (!match || !match.length) { throw Error(`Unable to extract version from filename: ${filename}`); } const version = match[0]; const tmpDir = "temp/create-temp/"; const archivePath = tmpDir + filename; const tmpArchivePath = `${archivePath}.tmp`; const factorioDir = `factorio/${version}/`; const tmpFactorioDir = tmpDir + version; if (await fs.pathExists(factorioDir)) { logger.warn(`setting downloadDir to ${factorioDir}, but not downloading because already existing`); } else { await fs.ensureDir(tmpDir); // follow the redirect res = await fetch(url.href); logger.info("Downloading latest Factorio server release. This may take a while."); const writeStream = fs.createWriteStream(tmpArchivePath); stream.Readable.fromWeb(res.body).pipe(writeStream); await finished(writeStream); await fs.rename(tmpArchivePath, archivePath); try { await fs.ensureDir(tmpFactorioDir); await execFile("tar", [ "xf", archivePath, "-C", tmpFactorioDir, "--strip-components", "1", ]); } catch (e) { logger.error("error executing command- do you have 'xz-utils' installed?"); throw e; } await fs.unlink(archivePath); await fs.rename(tmpFactorioDir, factorioDir); } } async function main() { let args = yargs .option("log-level", { nargs: 1, describe: "Log level to print to stdout", default: "info", choices: ["none"].concat(Object.keys(levels)), type: "string", }) .option("dev", { nargs: 0, describe: "Initialize development repository", hidden: true, default: false, type: "boolean", }) .option("mode", { nargs: 1, describe: "Operating mode to install", choices: ["standalone", "controller", "host", "ctl", "plugins", "plugin-template"], }) .option("migrate-rename", { nargs: 0, describe: "Migrate from before slave/master rename", default: false, type: "boolean", }) .option("admin", { nargs: 1, describe: "Admin account name [standalone/controller]", type: "string", }) .option("http-port", { nargs: 1, describe: "HTTP port to listen on [standalone/controller]", type: "number", }) .option("host-name", { nargs: 1, describe: "Host name [host]", type: "string", }) .option("controller-url", { nargs: 1, describe: "Controller URL [host/ctl]", type: "string", }) .option("controller-token", { nargs: 1, describe: "Controller authentication token [host/ctl]", type: "string", }) .option("public-address", { nargs: 1, describe: "DNS/IP Address to connect to this server [standalone/host]", type: "string", }) .option("factorio-dir", { nargs: 1, describe: "Path to Factorio installation [standalone/host]", type: "string", }) .option("remote-npm", { nargs: 0, description: "Allow remote updates via npm [standalone/controller/host]", type: "boolean", }) .option("plugins", { array: true, describe: "Plugins to install", type: "string", }) .option("plugin-template", { array: true, describe: "Plugin template to use [plugin-template]", type: "string", }) .option("plugin-name", { nargs: 1, describe: "Name of your new plugin [plugin-template]", type: "string", }) .strict() ; if (process.platform === "linux") { args = args .option("allow-install-as-root", { nargs: 0, describe: "(Linux only) Allow installing as root (not recommended)", type: "boolean", }) .option("download-headless", { nargs: 0, describe: "(Linux only) Automatically download and unpack the latest factorio release. " + "Can be set to false using --no-download-headless.", type: "boolean", }) ; } args = args.argv; setLogLevel(args.logLevel === "none" ? -1 : levels[args.logLevel]); dev = args.dev; if (!dev) { await validateInstallDir(); await validateNotRoot(args); } if (args.migrateRename) { await migrateRename(args); return; } let answers = await inquirerMissingArgs(args); logger.verbose(JSON.stringify(answers)); if (answers.pluginTemplate) { await copyPluginTemplates(answers.pluginName, answers.pluginTemplate); return; } if (answers.downloadHeadless) { if (answers.factorioDir !== "factorio") { throw new InstallError("--download-headless option requires --factorio-dir to be set to factorio"); } await downloadLinuxServer(); } if (!dev) { await installClusterio(answers.mode, answers.plugins); } let adminToken = null; if (["standalone", "controller"].includes(answers.mode)) { logger.info("Setting up controller"); await execController(["bootstrap", "create-admin", answers.admin]); await execController(["config", "set", "controller.http_port", answers.httpPort]); await execController(["config", "set", "controller.allow_remote_updates", answers.remoteNpm]); await execController(["config", "set", "controller.allow_plugin_updates", answers.remoteNpm]); let result = await execController(["bootstrap", "generate-user-token", answers.admin]); adminToken = result.stdout.split("\n").slice(-2)[0]; } if (answers.mode === "standalone") { logger.info("Setting up host"); await execHost(["config", "set", "host.name", "local"]); let result = await execHost(["config", "show", "host.id"]); let hostId = Number.parseInt(result.stdout.split("\n").slice(-2)[0], 10); result = await execController(["bootstrap", "generate-host-token", hostId]); let hostToken = result.stdout.split("\n").slice(-2)[0]; // Default to localhost on correct port for host in standalone mode await execHost(["config", "set", "host.controller_url", `http://localhost:${answers.httpPort}/`]); await execHost(["config", "set", "host.controller_token", hostToken]); await execHost(["config", "set", "host.public_address", answers.publicAddress]); await execHost(["config", "set", "host.factorio_directory", answers.factorioDir]); await execHost(["config", "set", "host.allow_remote_updates", answers.remoteNpm]); await execHost(["config", "set", "host.allow_plugin_updates", answers.remoteNpm]); } if (answers.mode === "host") { logger.info("Setting up host"); let hostId = JSON.parse(Buffer.from(answers.controllerToken.split(".")[1], "base64")).host; await execHost(["config", "set", "host.id", hostId]); await execHost(["config", "set", "host.name", answers.hostName]); await execHost(["config", "set", "host.controller_url", answers.controllerUrl]); await execHost(["config", "set", "host.controller_token", answers.controllerToken]); await execHost(["config", "set", "host.public_address", answers.publicAddress]); await execHost(["config", "set", "host.factorio_directory", answers.factorioDir]); await execHost(["config", "set", "host.allow_remote_updates", answers.remoteNpm]); await execHost(["config", "set", "host.allow_plugin_updates", answers.remoteNpm]); } if (!dev && ["standalone", "controller", "host"].includes(answers.mode)) { logger.info("Writing run scripts"); await writeScripts(answers.mode); } if (answers.mode === "ctl") { await execCtl(["control-config", "set", "control.controller_url", answers.controllerUrl]); await execCtl(["control-config", "set", "control.controller_token", answers.controllerToken]); } /* eslint-disable no-console */ console.log(`Successfully installed ${answers.mode}`); if (adminToken) { console.log(`Admin authentication token: ${adminToken}`); } /* eslint-enable no-console */ } if (module === require.main) { main().catch(err => { if (err instanceof InstallError) { logger.error(err.message); } else { logger.fatal(` +------------------------------------------------------------+ | Unexpected error occured installing clusterio, please | | report it to https://github.com/clusterio/clusterio/issues | +------------------------------------------------------------+ ${err.stack}` ); } }); }