mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Controller.bootstrap and Host.bootstrap returned constructor arguments that the caller had to spread into new Controller/Host, and DatastoreProvider.bootstrap did the same for datastores. They are replaced by Controller.fromConfig, Host.fromConfig and KeyValueDatastore/SubscribableDatastore.fromProvider, which load the data and return the finished instance. Closes #1126 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
310 lines
8.5 KiB
JavaScript
Executable File
310 lines
8.5 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Clusterio host
|
|
*
|
|
* Connects to the controller and hosts Factorio servers that can
|
|
* communicate with the cluster. It is remotely controlled by {@link
|
|
* module:controller/controller}.
|
|
*
|
|
* @module host/host
|
|
* @author Danielv123, Hornwitser
|
|
* @example
|
|
* npx clusteriohost run
|
|
*/
|
|
import fs from "node:fs/promises";
|
|
import path from "path";
|
|
import yargs from "yargs";
|
|
import packageConfig from "@clusterio/host/package.json" with { type: "json" };
|
|
import winston from "winston";
|
|
import "winston-daily-rotate-file";
|
|
|
|
// internal libraries
|
|
import * as lib from "@clusterio/lib";
|
|
import { ConsoleTransport, levels, logger } from "@clusterio/lib";
|
|
|
|
import Host from "./src/Host.js";
|
|
import { createScenarioCommand, handleCreateScenarioCommand } from "./src/create_scenario.js";
|
|
|
|
let host: Host | undefined;
|
|
|
|
void new lib.Gauge(
|
|
"clusterio_host_pending_requests",
|
|
"Count of pending link requests currently waiting in memory on the host.",
|
|
{
|
|
labels: ["host_id"],
|
|
callback: (gauge) => {
|
|
if (!host) {
|
|
return;
|
|
}
|
|
|
|
let count = host.pendingRequestCount;
|
|
for (const connection of host.instanceConnections.values()) {
|
|
count += connection.pendingRequestCount;
|
|
count += connection.instance.pendingRequestCount;
|
|
}
|
|
gauge.labels(String(host.config.get("host.id"))).set(count);
|
|
},
|
|
}
|
|
);
|
|
|
|
export class HostConnector extends lib.WebSocketClientConnector {
|
|
constructor(
|
|
public hostConfig: lib.HostConfig,
|
|
public pluginInfos: lib.PluginNodeEnvInfo[]
|
|
) {
|
|
super(
|
|
hostConfig.get("host.controller_url"),
|
|
hostConfig.get("host.max_reconnect_delay"),
|
|
);
|
|
}
|
|
|
|
register() {
|
|
logger.info("Connector | registering host");
|
|
let plugins: Record<string, string> = {};
|
|
for (let pluginInfo of this.pluginInfos) {
|
|
plugins[pluginInfo.name] = pluginInfo.version;
|
|
}
|
|
|
|
this.sendHandshake(
|
|
new lib.MessageRegisterHost(
|
|
new lib.RegisterHostData(
|
|
this.hostConfig.get("host.controller_token"),
|
|
packageConfig.version,
|
|
this.hostConfig.get("host.id"),
|
|
plugins,
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
async function startHost() {
|
|
// argument parsing
|
|
// eslint-disable-next-line node/no-sync
|
|
const args = yargs(process.argv.slice(2))
|
|
.scriptName("host")
|
|
.usage("$0 <command> [options]")
|
|
.option("log-level", {
|
|
nargs: 1,
|
|
describe: "Log level to print to stdout",
|
|
default: "info",
|
|
choices: ["none"].concat(Object.keys(levels)),
|
|
type: "string",
|
|
})
|
|
.option("log-directory", {
|
|
nargs: 1,
|
|
describe: "Directory to place logs in",
|
|
default: "logs",
|
|
type: "string",
|
|
})
|
|
.option("config", {
|
|
nargs: 1,
|
|
describe: "host config file to use",
|
|
default: "config-host.json",
|
|
type: "string",
|
|
})
|
|
.option("create-config", {
|
|
nargs: 0,
|
|
describe: "Create config file if it does not exist",
|
|
default: false,
|
|
type: "boolean",
|
|
})
|
|
.option("plugin-list", {
|
|
nargs: 1,
|
|
describe: "File containing list of plugins available with their install path",
|
|
default: "plugin-list.json",
|
|
type: "string",
|
|
})
|
|
.option("bypass-lock-file", { hidden: true, type: "boolean", nargs: 0, default: false })
|
|
.command("plugin", "Manage available plugins", lib.pluginCommand)
|
|
.command("config", "Manage Host config", lib.configCommand)
|
|
.command(
|
|
"create-scenario <output>",
|
|
"Create a scenario patched with modules for use outside Clusterio",
|
|
createScenarioCommand,
|
|
)
|
|
.command("run", "Run host", yargs => {
|
|
yargs.option("can-restart", {
|
|
type: "boolean", nargs: 0, default: false,
|
|
describe: "Indicate that a process monitor will restart this host on failure",
|
|
});
|
|
yargs.option("recovery", {
|
|
type: "boolean", nargs: 0, default: false,
|
|
describe: "Start the host in recovery mode with all plugins disabled and controller disconnected",
|
|
});
|
|
})
|
|
.demandCommand(1, "You need to specify a command to run")
|
|
.strict()
|
|
.parseSync()
|
|
;
|
|
|
|
logger.add(new winston.transports.DailyRotateFile({
|
|
format: winston.format.json(),
|
|
filename: "host-%DATE%.log",
|
|
utc: true,
|
|
dirname: path.join(args.logDirectory, "host"),
|
|
}));
|
|
if (args.logLevel !== "none") {
|
|
logger.add(new ConsoleTransport({
|
|
level: args.logLevel,
|
|
format: new lib.TerminalFormat(),
|
|
filter: (info: any) => info.instance_id === undefined,
|
|
}));
|
|
}
|
|
lib.handleUnhandledErrors();
|
|
|
|
let command = args._[0];
|
|
if (command === "run") {
|
|
logger.info(`Starting Clusterio host ${packageConfig.version}`);
|
|
if (args.recovery) {
|
|
logger.warn("Host recovery mode enabled. Some features will be disabled.");
|
|
}
|
|
}
|
|
|
|
logger.info(`Loading available plugins from ${args.pluginList}`);
|
|
let pluginList = await lib.loadPluginList(args.pluginList);
|
|
|
|
// If the command is plugin management we don't try to load plugins
|
|
if (command === "plugin") {
|
|
await lib.handlePluginCommand(args, pluginList, args.pluginList);
|
|
return;
|
|
}
|
|
|
|
logger.info("Loading Plugin info");
|
|
let pluginInfos = await lib.loadPluginInfos(pluginList);
|
|
lib.registerPluginMessages(pluginInfos);
|
|
lib.registerPluginPermissions(pluginInfos);
|
|
lib.addPluginConfigFields(pluginInfos);
|
|
if (!args.recovery) {
|
|
await lib.loadPluginMigrations(pluginInfos);
|
|
}
|
|
|
|
const [hostConfig, hostConfigLock] = await lib.loadConfigFromArgs(
|
|
args,
|
|
lib.HostConfig,
|
|
"host",
|
|
);
|
|
|
|
hostConfig.set("host.version", packageConfig.version); // Allows tracking last loaded version
|
|
|
|
if (command === "config") {
|
|
await lib.handleConfigCommand(args, hostConfig, hostConfigLock);
|
|
return;
|
|
}
|
|
|
|
if (command === "create-scenario") {
|
|
await handleCreateScenarioCommand(args, hostConfig, pluginInfos);
|
|
return;
|
|
}
|
|
|
|
// From this point on the host should run
|
|
await hostConfigLock.acquire();
|
|
|
|
// If we get here the command was run
|
|
await fs.mkdir(hostConfig.get("host.instances_directory"), { recursive: true });
|
|
await fs.mkdir(hostConfig.get("host.mods_directory"), { recursive: true });
|
|
await fs.mkdir("modules", { recursive: true });
|
|
|
|
// Set the process title, shows up as the title of the CMD window on windows
|
|
// and as the process name in ps/top on linux.
|
|
process.title = "clusteriohost";
|
|
|
|
// make sure we have the controller access token
|
|
if (hostConfig.get("host.controller_token") === "enter token here") {
|
|
logger.fatal("ERROR invalid config!");
|
|
logger.fatal(
|
|
"Controller requires an access token for socket operations. As clusterio\n"+
|
|
"hosts depends upon this, please set your token using the command npx\n"+
|
|
"clusteriohost config set host.controller_token <token>. You can generate an\n"+
|
|
"auth token using npx clusterioctl generate-host-token."
|
|
);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
// make sure url ends with /
|
|
if (!hostConfig.get("host.controller_url").endsWith("/")) {
|
|
logger.fatal("ERROR invalid config!");
|
|
logger.fatal("host.controller_url must end with '/'");
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
let hostConnector = new HostConnector(hostConfig, pluginInfos);
|
|
host = await Host.fromConfig(
|
|
hostConnector,
|
|
hostConfig,
|
|
pluginInfos,
|
|
Boolean(args.canRestart),
|
|
Boolean(args.recovery),
|
|
);
|
|
|
|
// Handle interrupts
|
|
process.on("SIGINT", lib.createShutdownGuard(logger, "interrupt", host.shutdown.bind(host)));
|
|
process.on("SIGTERM", lib.createShutdownGuard(logger, "termination", host.shutdown.bind(host)));
|
|
process.on("SIGHUP", () => {
|
|
logger.info("Terminal closed, shutting down");
|
|
host!.shutdown();
|
|
});
|
|
|
|
try {
|
|
await host.loadPlugins();
|
|
} catch (err) {
|
|
await host.shutdown();
|
|
throw err;
|
|
}
|
|
|
|
hostConnector.once("connect", () => {
|
|
logger.add(new lib.LinkTransport({ link: host! }));
|
|
});
|
|
|
|
await hostConnector.connect();
|
|
host.startedAtMs = Date.now();
|
|
logger.info("Started host");
|
|
}
|
|
|
|
export function bootstrap() {
|
|
startHost().catch(err => {
|
|
if (err instanceof lib.WebSocketError) {
|
|
// Already handled by connector.on("error") within Host
|
|
|
|
} else if (err instanceof lib.StartupError || err.code === "InstallationError") {
|
|
logger.fatal(`
|
|
+----------------------------------+
|
|
| Unable to to start clusteriohost |
|
|
+----------------------------------+
|
|
${err.stack}`
|
|
);
|
|
|
|
} else if (err instanceof lib.PluginError) {
|
|
logger.fatal(`
|
|
${err.pluginName} plugin threw an unexpected error
|
|
during startup, please report it to the plugin author.
|
|
------------------------------------------------------
|
|
${err.original.stack}`
|
|
);
|
|
|
|
} else {
|
|
logger.fatal(`
|
|
+------------------------------------------------------------+
|
|
| Unexpected error occured while starting host, please |
|
|
| report it to https://github.com/clusterio/clusterio/issues |
|
|
+------------------------------------------------------------+
|
|
${err.stack}`
|
|
);
|
|
}
|
|
|
|
if (err instanceof lib.AuthenticationFailed) {
|
|
process.exitCode = 8;
|
|
} else {
|
|
process.exitCode = 1;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
bootstrap();
|
|
}
|