monitor/core/routes/deployer/actions.js
2025-04-16 22:30:27 +07:00

287 lines
10 KiB
JavaScript

const modulename = 'WebServer:DeployerActions';
import path from 'node:path';
import { cloneDeep } from 'lodash-es';
import slash from 'slash';
import mysql from 'mysql2/promise';
import consts from '@shared/consts';
import { txEnv, txHostConfig } from '@core/globalData';
import { validateModifyServerConfig } from '@lib/fxserver/fxsConfigHelper';
import consoleFactory from '@lib/console';
import { SYM_RESET_CONFIG } from '@lib/symbols';
const console = consoleFactory(modulename);
//Helper functions
const isUndefined = (x) => (x === undefined);
/**
* Handle all the server control actions
* @param {object} ctx
*/
export default async function DeployerActions(ctx) {
//Sanity check
if (isUndefined(ctx.params.action)) {
return ctx.utils.error(400, 'Invalid Request');
}
const action = ctx.params.action;
//Check permissions
if (!ctx.admin.testPermission('master', modulename)) {
return ctx.send({ success: false, refresh: true });
}
//Check if this is the correct state for the deployer
if (txManager.deployer == null) {
return ctx.send({ success: false, refresh: true });
}
//Delegate to the specific action functions
if (action == 'confirmRecipe') {
return await handleConfirmRecipe(ctx);
} else if (action == 'setVariables') {
return await handleSetVariables(ctx);
} else if (action == 'commit') {
return await handleSaveConfig(ctx);
} else if (action == 'cancel') {
return await handleCancel(ctx);
} else {
return ctx.send({
type: 'danger',
message: 'Unknown setup action.',
});
}
};
//================================================================
/**
* Handle submition of user-edited recipe (record to deployer, starts the process)
* @param {object} ctx
*/
async function handleConfirmRecipe(ctx) {
//Sanity check
if (isUndefined(ctx.request.body.recipe)) {
return ctx.utils.error(400, 'Invalid Request - missing parameters');
}
const userEditedRecipe = ctx.request.body.recipe;
try {
ctx.admin.logAction('Setting recipe.');
await txManager.deployer.confirmRecipe(userEditedRecipe);
} catch (error) {
return ctx.send({ type: 'danger', message: error.message });
}
return ctx.send({ success: true });
}
//================================================================
/**
* Handle submition of the input variables/parameters
* @param {object} ctx
*/
async function handleSetVariables(ctx) {
//Sanity check
if (isUndefined(ctx.request.body.svLicense)) {
return ctx.utils.error(400, 'Invalid Request - missing parameters');
}
const userVars = cloneDeep(ctx.request.body);
//Validating sv_licenseKey
if (
!consts.regexSvLicenseNew.test(userVars.svLicense)
&& !consts.regexSvLicenseOld.test(userVars.svLicense)
) {
return ctx.send({ type: 'danger', message: 'The Server License does not appear to be valid.' });
}
//Validating steam api key requirement
if (
txManager.deployer.recipe.steamRequired
&& (typeof userVars.steam_webApiKey !== 'string' || userVars.steam_webApiKey.length < 24)
) {
return ctx.send({
type: 'danger',
message: 'This recipe requires steam_webApiKey to be set and valid.',
});
}
//DB Stuff
if (typeof userVars.dbDelete !== 'undefined') {
//Testing the db config
try {
userVars.dbPort = parseInt(userVars.dbPort);
if (isNaN(userVars.dbPort)) {
return ctx.send({ type: 'danger', message: 'The database port is invalid (non-integer). The default is 3306.' });
}
const mysqlOptions = {
host: userVars.dbHost,
port: userVars.dbPort,
user: userVars.dbUsername,
password: userVars.dbPassword,
connectTimeout: 5000,
};
await mysql.createConnection(mysqlOptions);
} catch (error) {
let outMessage = error?.message ?? 'Unknown error occurred.';
if (error?.code === 'ECONNREFUSED') {
let specificError = (txEnv.isWindows)
? 'If you do not have a database installed, you can download and run XAMPP.'
: 'If you do not have a database installed, you must download and run MySQL or MariaDB.';
if (userVars.dbPort !== 3306) {
specificError += '<br>\n<b>You are not using the default DB port 3306, make sure it is correct!</b>';
}
outMessage = `${error?.message}<br>\n${specificError}`;
} else if (error.message?.includes('auth_gssapi_client')) {
outMessage = `Your database does not accept the required authentication method. Please update your MySQL/MariaDB server and try again.`;
}
return ctx.send({ type: 'danger', message: `<b>Database connection failed:</b> ${outMessage}` });
}
//Setting connection string
userVars.dbDelete = (userVars.dbDelete === 'true');
const dbFullHost = (userVars.dbPort === 3306)
? userVars.dbHost
: `${userVars.dbHost}:${userVars.dbPort}`;
userVars.dbConnectionString = (userVars.dbPassword.length)
? `mysql://${userVars.dbUsername}:${userVars.dbPassword}@${dbFullHost}/${userVars.dbName}?charset=utf8mb4`
: `mysql://${userVars.dbUsername}@${dbFullHost}/${userVars.dbName}?charset=utf8mb4`;
}
//Max Clients & Server Endpoints
userVars.maxClients = (txHostConfig.forceMaxClients) ? txHostConfig.forceMaxClients : 48;
if (txHostConfig.netInterface || txHostConfig.fxsPort) {
const comment = `# ${txHostConfig.sourceName}: do not modify!`;
const endpointIface = txHostConfig.netInterface ?? '0.0.0.0';
const endpointPort = txHostConfig.fxsPort ?? 30120;
userVars.serverEndpoints = [
`endpoint_add_tcp "${endpointIface}:${endpointPort}" ${comment}`,
`endpoint_add_udp "${endpointIface}:${endpointPort}" ${comment}`,
].join('\n');
} else {
userVars.serverEndpoints = [
'endpoint_add_tcp "0.0.0.0:30120"',
'endpoint_add_udp "0.0.0.0:30120"',
].join('\n');
}
//Setting identifiers array
const admin = txCore.adminStore.getAdminByName(ctx.admin.name);
if (!admin) return ctx.send({ type: 'danger', message: 'Admin not found.' });
const addPrincipalLines = [];
Object.keys(admin.providers).forEach((providerName) => {
if (admin.providers[providerName].identifier) {
addPrincipalLines.push(`add_principal identifier.${admin.providers[providerName].identifier} group.admin #${ctx.admin.name}`);
}
});
userVars.addPrincipalsMaster = (addPrincipalLines.length)
? addPrincipalLines.join('\n')
: '# Deployer Note: this admin master has no identifiers to be automatically added.\n# add_principal identifier.discord:111111111111111111 group.admin #example';
//Start deployer
try {
ctx.admin.logAction('Running recipe.');
txManager.deployer.start(userVars);
} catch (error) {
return ctx.send({ type: 'danger', message: error.message });
}
return ctx.send({ success: true });
}
//================================================================
/**
* Handle the commit of a Recipe by receiving the user edited server.cfg
* @param {object} ctx
*/
async function handleSaveConfig(ctx) {
//Sanity check
if (isUndefined(ctx.request.body.serverCFG)) {
return ctx.utils.error(400, 'Invalid Request - missing parameters');
}
const serverCFG = ctx.request.body.serverCFG;
const cfgFilePath = path.join(txManager.deployer.deployPath, 'server.cfg');
txCore.cacheStore.set('deployer:recipe', txManager.deployer?.recipe?.name ?? 'unknown');
//Validating config contents + saving file and backup
try {
const result = await validateModifyServerConfig(serverCFG, cfgFilePath, txManager.deployer.deployPath);
if (result.errors) {
return ctx.send({
type: 'danger',
success: false,
markdown: true,
message: `**Cannot save \`server.cfg\` due to error(s) in your config file(s):**\n${result.errors}`,
});
}
} catch (error) {
return ctx.send({
type: 'danger',
success: false,
markdown: true,
message: `**Failed to save \`server.cfg\` with error:**\n${error.message}`,
});
}
//Preparing & saving config
let onesync = SYM_RESET_CONFIG;
if (typeof txManager.deployer?.recipe?.onesync === 'string' && txManager.deployer.recipe.onesync.length) {
onesync = txManager.deployer.recipe.onesync;
}
try {
txCore.configStore.saveConfigs({
server: {
dataPath: slash(path.normalize(txManager.deployer.deployPath)),
cfgPath: 'server.cfg',
onesync,
}
}, ctx.admin.name);
} catch (error) {
console.warn(`[${ctx.admin.name}] Error changing fxserver settings via deployer.`);
console.verbose.dir(error);
return ctx.send({
type: 'danger',
markdown: true,
message: `**Error saving the configuration file:** ${error.message}`
});
}
ctx.admin.logAction('Completed and committed server deploy.');
//If running (for some reason), kill it first
if (!txCore.fxRunner.isIdle) {
ctx.admin.logCommand('STOP SERVER');
await txCore.fxRunner.killServer('new server deployed', ctx.admin.name, true);
}
//Starting server
const spawnError = await txCore.fxRunner.spawnServer(false);
if (spawnError !== null) {
return ctx.send({
type: 'danger',
markdown: true,
message: `Config file saved, but failed to start server with error:\n${spawnError}`,
});
} else {
txManager.deployer = null;
txCore.webServer.webSocket.pushRefresh('status');
return ctx.send({ success: true });
}
}
//================================================================
/**
* Handle the cancellation of the deployer proguess
* @param {object} ctx
*/
async function handleCancel(ctx) {
txManager.deployer = null;
txCore.webServer.webSocket.pushRefresh('status');
return ctx.send({ success: true });
}