mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 15:14:52 +09:00
All possible Promise rejections and exception bubbling should be handled by either the code creating the exception or the code invoking the operation that could fail. Treat therefore uncaught exceptions and unhandled promise rejetions as fatal errors that log a traceback and immedately terminate the Node.js process.
352 lines
9.2 KiB
JavaScript
Executable File
352 lines
9.2 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Command line interface for controlling a Clusterio cluster
|
|
* @module ctl/ctl
|
|
*/
|
|
"use strict";
|
|
const fs = require("fs-extra");
|
|
const yargs = require("yargs");
|
|
const version = require("./package").version;
|
|
const setBlocking = require("set-blocking");
|
|
|
|
const libLink = require("@clusterio/lib/link");
|
|
const libErrors = require("@clusterio/lib/errors");
|
|
const libConfig = require("@clusterio/lib/config");
|
|
const libPlugin = require("@clusterio/lib/plugin");
|
|
const libPluginLoader = require("@clusterio/lib/plugin_loader");
|
|
const libCommand = require("@clusterio/lib/command");
|
|
const libSharedCommands = require("@clusterio/lib/shared_commands");
|
|
const { ConsoleTransport, levels, logger } = require("@clusterio/lib/logging");
|
|
const libLoggingUtils = require("@clusterio/lib/logging_utils");
|
|
|
|
const commands = require("./src/commands");
|
|
|
|
|
|
/**
|
|
* Connector for control connection to master server
|
|
* @private
|
|
*/
|
|
class ControlConnector extends libLink.WebSocketClientConnector {
|
|
constructor(url, maxReconnectDelay, tlsCa, token) {
|
|
super(url, maxReconnectDelay, tlsCa);
|
|
this._token = token;
|
|
}
|
|
|
|
register() {
|
|
logger.verbose("Connector | registering control");
|
|
this.sendHandshake("register_control", {
|
|
token: this._token,
|
|
agent: "clusterioctl",
|
|
version: version,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles running the control
|
|
*
|
|
* Connects to the master server over WebSocket and sends commands to it.
|
|
* @static
|
|
*/
|
|
class Control extends libLink.Link {
|
|
|
|
constructor(connector, controlConfig, tlsCa, controlPlugins) {
|
|
super("control", "master", connector);
|
|
libLink.attachAllMessages(this);
|
|
|
|
/**
|
|
* Control config used for connecting to the master.
|
|
* @type {module:lib/config.ControlConfig}
|
|
*/
|
|
this.config = controlConfig;
|
|
/**
|
|
* Certificate authority used to validate TLS connections to the master.
|
|
* @type {?string}
|
|
*/
|
|
this.tlsCa = tlsCa;
|
|
/**
|
|
* Mapping of plugin names to their instance for loaded plugins.
|
|
* @type {Map<string, module:lib/plugin.BaseControlPlugin>}
|
|
*/
|
|
this.plugins = controlPlugins;
|
|
for (let controlPlugin of controlPlugins.values()) {
|
|
libPlugin.attachPluginMessages(this, controlPlugin);
|
|
}
|
|
|
|
/**
|
|
* Keep the control connection alive after the command completes.
|
|
* @type {boolean}
|
|
*/
|
|
this.keepOpen = false;
|
|
}
|
|
|
|
async accountUpdateEventHandler() { }
|
|
|
|
async slaveUpdateEventHandler() { }
|
|
|
|
async instanceUpdateEventHandler() { }
|
|
|
|
async saveListUpdateEventHandler() { }
|
|
|
|
async modPackUpdateEventHandler() { }
|
|
|
|
async modUpdateEventHandler() { }
|
|
|
|
async userUpdateEventHandler() { }
|
|
|
|
async setLogSubscriptions({
|
|
all = false,
|
|
master = false,
|
|
slave_ids = [],
|
|
instance_ids = [],
|
|
max_level = null,
|
|
}) {
|
|
await libLink.messages.setLogSubscriptions.send(this, {
|
|
all, master, slave_ids, instance_ids, max_level,
|
|
});
|
|
}
|
|
|
|
async logMessageEventHandler(message) {
|
|
logger.log(message.data.info);
|
|
}
|
|
|
|
async debugWsMessageEventHandler(message) {
|
|
// eslint-disable-next-line no-console
|
|
console.log("WS", message.data.direction, message.data.content);
|
|
}
|
|
|
|
async shutdown() {
|
|
try {
|
|
await libLink.messages.prepareDisconnect.send(this);
|
|
} catch (err) {
|
|
if (!(err instanceof libErrors.SessionLost)) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
await this.connector.close(1000, "Control Quit");
|
|
}
|
|
}
|
|
|
|
async function loadPlugins(pluginList) {
|
|
let pluginInfos = await libPluginLoader.loadPluginInfos(pluginList);
|
|
libConfig.registerPluginConfigGroups(pluginInfos);
|
|
libConfig.finalizeConfigs();
|
|
|
|
let controlPlugins = new Map();
|
|
for (let pluginInfo of pluginInfos) {
|
|
if (!pluginInfo.controlEntrypoint) {
|
|
continue;
|
|
}
|
|
|
|
let ControlPluginClass = await libPluginLoader.loadControlPluginClass(pluginInfo);
|
|
let controlPlugin = new ControlPluginClass(pluginInfo, logger);
|
|
controlPlugins.set(pluginInfo.name, controlPlugin);
|
|
await controlPlugin.init();
|
|
}
|
|
return controlPlugins;
|
|
}
|
|
|
|
async function startControl() {
|
|
yargs
|
|
.scriptName("clusterioctl")
|
|
.usage("$0 <command> [options]")
|
|
.option("log-level", {
|
|
nargs: 1,
|
|
describe: "Log level to print to stderr",
|
|
default: "server",
|
|
choices: Object.keys(levels),
|
|
type: "string",
|
|
})
|
|
.option("config", {
|
|
nargs: 1,
|
|
describe: "config file to get credentails from",
|
|
default: "config-control.json",
|
|
defaultDescription: "auto",
|
|
type: "string",
|
|
})
|
|
.option("plugin-list", {
|
|
nargs: 1,
|
|
describe: "File containing list of plugins available with their install path",
|
|
default: "plugin-list.json",
|
|
type: "string",
|
|
})
|
|
.command("plugin", "Manage available plugins", libSharedCommands.pluginCommand)
|
|
.command("control-config", "Manage Control config", libSharedCommands.configCommand)
|
|
.wrap(yargs.terminalWidth())
|
|
.help(false) // Disable help to avoid triggering it on the first parse.
|
|
;
|
|
|
|
// Parse the args first to get the configured plugin list.
|
|
let args = yargs.parse();
|
|
|
|
// Log stream for the ctl session.
|
|
logger.add(new ConsoleTransport({
|
|
errorLevels: Object.keys(levels),
|
|
level: args.logLevel,
|
|
format: new libLoggingUtils.TerminalFormat(),
|
|
}));
|
|
libLoggingUtils.handleUnhandledErrors(logger);
|
|
|
|
logger.verbose(`Loading available plugins from ${args.pluginList}`);
|
|
let pluginList = new Map();
|
|
try {
|
|
pluginList = new Map(JSON.parse(await fs.readFile(args.pluginList)));
|
|
} catch (err) {
|
|
if (err.code !== "ENOENT") {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// If the command is plugin management we don't try to load plugins
|
|
if (args._[0] === "plugin") {
|
|
await libSharedCommands.handlePluginCommand(args, pluginList, args.pluginList);
|
|
return;
|
|
}
|
|
|
|
logger.verbose("Loading Plugins");
|
|
let controlPlugins = await loadPlugins(pluginList);
|
|
|
|
// Add all cluster management commands including ones from plugins
|
|
let rootCommands = await commands.registerCommands(controlPlugins, yargs);
|
|
|
|
// Reparse after commands have been added with help and strict checking.
|
|
args = yargs
|
|
.help()
|
|
.strict()
|
|
.parse()
|
|
;
|
|
|
|
logger.verbose(`Loading config from ${args.config}`);
|
|
let controlConfig = new libConfig.ControlConfig("control");
|
|
try {
|
|
await controlConfig.load(JSON.parse(await fs.readFile(args.config)));
|
|
|
|
} catch (err) {
|
|
if (err.code === "ENOENT") {
|
|
logger.verbose("Config not found, initializing new config");
|
|
await controlConfig.init();
|
|
|
|
} else {
|
|
throw new libErrors.StartupError(`Failed to load ${args.config}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
if (args._.length === 0) {
|
|
yargs.showHelp();
|
|
yargs.exit();
|
|
}
|
|
|
|
// Handle the control-config command before trying to connect.
|
|
if (args._[0] === "control-config") {
|
|
await libSharedCommands.handleConfigCommand(args, controlConfig, args.config);
|
|
return;
|
|
}
|
|
|
|
// Determine which command is being executed.
|
|
let commandPath = [...args._];
|
|
let targetCommand = rootCommands;
|
|
while (commandPath.length && targetCommand instanceof libCommand.CommandTree) {
|
|
targetCommand = targetCommand.get(commandPath.shift());
|
|
}
|
|
|
|
// The remaining commands require connecting to the master server.
|
|
if (!controlConfig.get("control.master_url") || !controlConfig.get("control.master_token")) {
|
|
logger.error("Missing URL and/or token to connect with. See README.md for setting up access.");
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
let tlsCa = null;
|
|
let tlsCaPath = controlConfig.get("control.tls_ca");
|
|
if (tlsCaPath) {
|
|
tlsCa = await fs.readFile(tlsCaPath);
|
|
}
|
|
|
|
let controlConnector = new ControlConnector(
|
|
controlConfig.get("control.master_url"),
|
|
controlConfig.get("control.max_reconnect_delay"),
|
|
tlsCa,
|
|
controlConfig.get("control.master_token")
|
|
);
|
|
let control = new Control(controlConnector, controlConfig, tlsCa, controlPlugins);
|
|
try {
|
|
await controlConnector.connect();
|
|
} catch (err) {
|
|
if (err instanceof libErrors.AuthenticationFailed) {
|
|
throw new libErrors.StartupError(err.message);
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
process.on("SIGINT", () => {
|
|
logger.info("Caught interrupt signal, closing connection");
|
|
control.shutdown().catch(err => {
|
|
setBlocking(true);
|
|
logger.error(err.stack);
|
|
// eslint-disable-next-line node/no-process-exit
|
|
process.exit(1);
|
|
});
|
|
});
|
|
|
|
try {
|
|
await targetCommand.run(args, control);
|
|
|
|
} catch (err) {
|
|
control.keepOpen = false;
|
|
if (err instanceof libErrors.CommandError) {
|
|
logger.error(`Error running command: ${err.message}`);
|
|
process.exitCode = 1;
|
|
|
|
} else if (err instanceof libErrors.RequestError) {
|
|
logger.error(`Error sending request: ${err.message}`);
|
|
process.exitCode = 1;
|
|
|
|
} else {
|
|
throw err;
|
|
}
|
|
|
|
} finally {
|
|
if (!control.keepOpen) {
|
|
await control.shutdown();
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
Control,
|
|
};
|
|
|
|
|
|
if (module === require.main) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn(`
|
|
+==========================================================+
|
|
I WARNING: This is the development branch for the 2.0 I
|
|
I version of clusterio. Expect things to break. I
|
|
+==========================================================+
|
|
`
|
|
);
|
|
startControl().catch(err => {
|
|
if (!(err instanceof libErrors.StartupError)) {
|
|
logger.fatal(`
|
|
+------------------------------------------------------------+
|
|
| Unexpected error occured while starting control, please |
|
|
| report it to https://github.com/clusterio/clusterio/issues |
|
|
+------------------------------------------------------------+
|
|
${err.stack}`
|
|
);
|
|
} else {
|
|
logger.error(`
|
|
+---------------------------------+
|
|
| Unable to to start clusterioctl |
|
|
+---------------------------------+
|
|
${err.stack}`
|
|
);
|
|
}
|
|
|
|
process.exitCode = 1;
|
|
});
|
|
}
|