mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Config files hold the auth secret and tokens, so the default 0o644 lets every user on the system read them. Config.save, the ctl config written by the controller bootstrap, ctl host create-config and the config migration in create now pass mode 0o600. safeOutputFile also applies the mode to the temporary file it writes, since writeFile only honours mode on creation and a leftover temporary from a failed write would otherwise keep its old mode. Closes #1005 Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
917 lines
27 KiB
JavaScript
Executable File
917 lines
27 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import child_process from "node:child_process";
|
|
import fs from "node:fs/promises";
|
|
import inquirer from "inquirer";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import stream from "node:stream";
|
|
import util from "node:util";
|
|
import yargs from "yargs";
|
|
|
|
import { levels, logger, setLogLevel } from "./logging.js";
|
|
import { copyPluginTemplates } from "./template.js";
|
|
let dev = false;
|
|
const scriptExt = process.platform === "win32" ? ".cmd" : "";
|
|
import { escapeArg } from "./escape_arg.js";
|
|
|
|
// 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 pathExists(filePath) {
|
|
try {
|
|
await fs.access(filePath);
|
|
} catch (err) {
|
|
if (err.code === "ENOENT" || err.code === "ENOTDIR") {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async function safeOutputFile(file, data, options={}) {
|
|
let { dir, name, ext } = path.parse(file);
|
|
let temporary = path.join(dir, `${name}.tmp${ext}`);
|
|
if (dir) {
|
|
await fs.mkdir(dir, { recursive: true });
|
|
}
|
|
await fs.writeFile(temporary, data, { ...options, flush: true });
|
|
await fs.rename(temporary, file);
|
|
}
|
|
|
|
async function execFile(cmd, args) {
|
|
const command = [cmd, ...args.map(escapeArg)].join(" ");
|
|
logger.verbose(`executing ${command}`);
|
|
return new Promise((resolve, reject) => {
|
|
let child = child_process.execFile(
|
|
command,
|
|
{
|
|
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(import.meta.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(import.meta.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(import.meta.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 pathExists(source) && !await pathExists(destination)) {
|
|
await safeOutputFile(destination, JSON.stringify(
|
|
renameConfig(JSON.parse(await fs.readFile(source))), null, "\t"
|
|
), { mode: 0o600 });
|
|
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 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 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 = (await fs.open(inputFile)).createReadStream();
|
|
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 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 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 pathExists(path.join("logs", "cluster"))
|
|
&& !await 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 pathExists("sharedMods")
|
|
&& !await 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 pathExists("run-master.sh") || await pathExists("run-master.cmd");
|
|
const hasRunSlave = await pathExists("run-slave.sh") || await 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.writeFile("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 = (await import(plugin)).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, selfSignedCert) {
|
|
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") {
|
|
let extraCaCertsLine = selfSignedCert ? `set "NODE_EXTRA_CA_CERTS=${selfSignedCert}"\n` : "";
|
|
await safeOutputFile(
|
|
"run-host.cmd",
|
|
`\
|
|
@echo off
|
|
set "NODE_OPTIONS=--enable-source-maps %NODE_OPTIONS%"
|
|
${extraCaCertsLine}: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 {
|
|
let extraCaCertsLine = selfSignedCert ? `export "NODE_EXTRA_CA_CERTS=${selfSignedCert}"\n` : "";
|
|
await safeOutputFile(
|
|
"run-host.sh",
|
|
`\
|
|
#!/bin/bash
|
|
export "NODE_OPTIONS=--enable-source-maps $NODE_OPTIONS"
|
|
${extraCaCertsLine}while true; do
|
|
./node_modules/.bin/clusteriohost run --can-restart
|
|
if [[ $? -eq 0 || $? -eq 8 ]]; then exit $?; fi
|
|
done
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
extraCaCertsLine = selfSignedCert ? `Environment=NODE_EXTRA_CA_CERTS=${selfSignedCert}\n` : "";
|
|
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
|
|
${extraCaCertsLine}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 pathExists(path.join(location, "data", "changelog.txt"))) {
|
|
foundLocations.push(location);
|
|
}
|
|
}
|
|
|
|
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.mkdir("factorio", { recursive: true });
|
|
}
|
|
}
|
|
|
|
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 main() {
|
|
let yarg = yargs(process.argv.slice(2))
|
|
.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("self-signed-cert", {
|
|
nargs: 1, describe: "Path to self-signed certificate of the controller [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") {
|
|
yarg = yarg
|
|
.option("allow-install-as-root", {
|
|
nargs: 0, describe: "(Linux only) Allow installing as root (not recommended)", type: "boolean",
|
|
})
|
|
;
|
|
}
|
|
|
|
const args = yarg.parse();
|
|
|
|
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 (!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, args.selfSignedCert);
|
|
}
|
|
|
|
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 (import.meta.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}`
|
|
);
|
|
}
|
|
});
|
|
}
|